Getting Started
编译与运行
g++ hello.cpp —std=c++11 -o hello
./hello
优点
大部分基础计算机系统的开发语言:Linux, MySQL, OpenCV, TensorFlow后端,PyTorch……
高效率:高强度优化编译器,直接访问内存,高效计算,AI算法实现
函数的声明与定义
1 | //prototype/declaration (*.h; *.hpp) |
编译与链接
编译错误,链接错误,运行错误
语法错误,找不到.o文件中所指的内容,运行时发生的异常
预处理器和宏
预处理器会处理#开头的指令
预处理指令:define, undef, include, if, ifdef, ifndef, else, elif, endif, line, error, pragma
输入输出
1 | std::ostream cout; |
命令行参数
1 | int main(){...} |
1 | g++ hello.cpp -o hello |
这个命令有三个命令行参数(g++不算在内)
1 | ./a.out arg1 arg2 arg3 |
上面这个例子就有四个命令行参数
Data Types and Arithmetic Operators
变量初始化
未初始化的变量可能会拥有随机值,因平台而异
溢出
数字过大导致溢出至符号位,正变负
整数类型
sizeof
操作符可以返回数据类型的位宽,单位字节(byte)
char
是8-bit integer类型
bool
是8-bit integer类型
typedef
创建类型
size_t
是unsigned integer类型,是sizeof
的结果类型,32-bit或64-bit
浮点类型
float
:单精度浮点类型,32-bit
double
:双精度浮点型,64-bit
long double
:扩展精度浮点型,如果支持则128-bit,否则64-bit
运算速度:double<float<integers
float
变量用==
比较可能带来意料外的结果
±inf
: Exponent=11111111, fraction=0
nan
: Exponent=11111111, fraction!=0
常数
1 | 95 // decimal |
const
:类型修饰符
auto
:类型占位符,由初始化赋值决定其类型,确定后不再改变
1 | auto c; //valid in C, error in C++ |
运算符优先级
ysq:考试考这个就没意思了
类型转换
有显式有隐式,隐式转换可能导致编译通过,产生意料之外的结果,尽可能用显式转换
数据损失
long double->double->float->long->int->short->char
运算
当除号左右都是整数类型时,进行整型除法(下取整),有一边是浮点型就按浮点型运算
操作数不属于int, long, float, double
四者之一时,先无损转换至这四者再运算
Branching and Looping Statements
就近配对
if-else在模糊语义下是就近配对
1 | if (1) |
三目运算符
sign=a<0?-1:1
条件
bool, chat, int, float
都可以直接塞进if(),如果可以隐式转换为bool
就行,指针也行(用来判断是否为NULL)
关系语句
大于,小于,等于,不等于
逻辑表达式
优先级:!>&&>||
循环
while, do-while, for; break, continue
for(;;)是可以的,但for()是不可以的
goto: 依托答辩,降低可读性
选择支
switch,别忘了break!
Data Structures
数组
C99及以后,数组可以根据变量开长度
数组在C/C++中并不是对象,而可以当作“地址”
数组之间不支持直接赋值,可视作常量指针:
1 | int a[4]={1,2,3,4}; |
未知后n-1维尺寸的n维数组不能作为函数参数
1 | void foo(int mat[][]);//error! |
常量数组可以作为函数参数使用,避免修改数组内容
字符串
长度为n的char数组,最好只存n-1个字符,最后一个存'\0'
若char数组中间出现了'\0'
,则strlen只会统计其前的字符数,但sizeof会忠诚地记录下所有字符,包括'\0'
1 | char *strcpy( char* dest, const char* src ); |
string class相对而言不那么容易导致问题
结构体struct
成员变量的序列,内存安排在连续的内存序列中,变量顺序可能导致结构体大小变化,为了对齐会按照最大的成员为单位申请内存
C语言中,为结构体生成对象需要struct name var
,使用typedef可以省点力。而C++中则可以name var
联合体union
类似于结构体,但成员共享内存。
由@NeumoNeumo提供的union
的使用场景:
在 linux 0.11 的 kernel/sched.c 中,Linus 大大将进程的描述符及其用户栈放在同一页中,于是可以使用位运算很方便地由栈定位进程的描述符,反之亦然。
1
2
3
4
5union task_union
{
struct task_struct task;
char stack[PAGE_SIZE];
};Small String Optimization 短字符串优化(可以参考下面大致理解,但实际并非如此)
1
2
3
4
5
6
7
8
9
10
11
12
13class string
{
size_t capacity;
union
{
struct
{
char *ptr;
size_t size;
} heapbuf;
char stackbuf[sizeof(heapbuf)];
};
};
枚举类enum
const的代替品,成员是整数,但不能参与表达式的运算,不过可以赋值给整型变量
默认从0开始,定义时可以指定对应的整数
typedef
用于代替复杂类型名称,对C结构体特攻,也可以结合预处理语句根据编译选项将不同类型对接给指定类型名
1 |
|
Pointers and Dynamic Memory Management
指针
32-bit/64-bit整数,用type *
定义,用&
取址,用*
取值
p->member
等价于(*p).member
常量与指针
1 | int num = 1; |
指针与数组
数组是常指针,用sizeof可以得到数组所有元素的内存大小和,用sizeof只能得到指针变量的大小:4/8
指针也可以用中括号访问对应内存
指针的运算是按对应元素的下标进行的,p+1代表下一个元素,跳跃字节数是对应类型的字节数
小心越界
申请内存: C style
1 | int len; |
如果为一个int型指针malloc(1),只分配了1字节,系统则会授予4字节的访问权限,导致可能的越界,非常危险。
内存释放: C style
1 | void foo() |
函数结束时只有临时指针变量p被释放,申请的那块内存却没有free,导致内存泄漏,应该在return前加个free(p)
申请内存: C++ style
new 和 new[]
1 | //allocate an int, default initializer (do nothing) |
释放内存: C++ style
1 | //deallocate memory |
Basics of Functions
函数的声明、定义在跨文件之间的顺序
调用函数是有代价的:调用栈存储返回值地址,存储寄存器变量,存储临时变量
参数
值传递,指针的值传递,引用传递(C++)
引用(C++)
引用必须在声明时初始化,相比指针更安全
传递引用会导致对函数参数的修改会影响到被传入的变量,为防止其修改,可将传入参数用const修饰
内联
编译时,编译器会试图将内联函数展开,增加编译用时但减少运行用时
在极短情况下,宏是个不错的选择
Advances in Functions
默认参数(C++)
默认参数只能集中放在最后几个参数位
函数的重载(C++)
同名,不同参数类型/个数,不同返回类型
函数模板
1 | template<typename T> |
函数指针
可以和函数名混用,加*
加&
无所谓,可以当作参数传入函数:
1 | void qsort( void *ptr, size_t count, size_t size, int (*comp)(const void *, const void *) ); |
需要给定函数的返回类型和参数列表、参数类型
函数引用
1 | float norm_l1(float x, float y); //declaration |
存在意义不明
递归函数
适用树形遍历等场景,短小精悍
占用栈内存,慢,难实现/debug
Speedup Your Program
ARM is a family of reduced instruction set computing (RISC) architectures for computer processors.
ARM是一个用于计算机处理器的精简指令集计算(RISC)架构系列。
ARM is the most widely used instruction set architecture (ISA) and the ISA produced in the largest quantity.
ARM是使用最广泛的指令集架构(ISA),也是生产数量最多的ISA。
Simple is beautiful.
SIMD: Single instruction, multiple data
将多个数据合并为一个变量,同时进行运算
OpenMP
循环展开进行多线程并行,但展开也需要耗时,每层都展开可能反而会慢
一般而言,若外层循环次数远小于内层,则#pragma放内层,否则放外层
Basics of Classes
访问权限指定
private, public, protected, default
成员函数
可以在类内外定义,加inline相当于就在类内,甚至可以跨文件
构造函数
创建对象时:
结构体:申请内存
类:申请内存,调用构造器
构造器名字和类名一样,无返回值
析构函数
销毁对象时调用,用~
加类名表示,无参数无返回值
delete[]时会调用对象数组中每个对象的析构函数,delete则只会调用第一个对象的析构函数
this指针
指向调用当前函数的对象
常量成员
常量成员变量和常变量一样,不能修改
const放在函数参数列表后修饰成员函数,保证该函数不会修改成员变量
静态成员
静态成员与实例不绑定,所有实例共享
Advances in Classes
重载操作符
通过成员函数重载:
1 | //类内 |
这样重载的符号左右顺序不能反,a+b
相当于a.operator+(b)
友元函数
部分操作符可以通过友元函数重载,但赋值操作符=
不行
友元函数需要在类中声明,可以在类内外定义,可以访问类的成员(含私有),但说到底也只是友元,不是成员。
1 | //类内定义 |
标准输入输出流重载
由于重载的是std命名空间中的输入输出流操作符,所以是std::而不是MyTime::
1 | friend std::ostream & operator<<(std::ostream & os, const MyTime & t) |
类型转换
1 | //隐式转换 |
也可通过转换构造器完成
重载赋值操作符
只能通过成员函数重载
1 | class_type & operator=(rhs_type &rhs) |
重载自增自减操作符
1 | // prefix increment |
可重载操作符列表
重载操作符不会更改其优先级!
Dynamic Memory Management in Classes
默认构造函数
没定义任何构造器时会有个默认的,啥也不干
定义构造器后默认的构造器会消失,因此可能会没有默认构造器
所有参数都有默认值的构造器就是默认构造器,只能有一个
默认析构函数
没定义析构函数时会有个默认的,啥也不干
默认复制函数
只有一个参数,或者其他参数都有默认值
默认复制所有非静态的数据成员
复制赋值操作符
1 | class_type & class_type::operator=(class_type &rhs){...} |
参数和返回值以及当前类是同一类型
默认复制所有非静态的数据成员
硬拷贝 vs. 软拷贝
硬拷贝把所有内容,以及指向的内容全部单独复制过来
软拷贝对于指针只会复制指针的值,使得多个对象的指针成员指向同一块内存
共享指针
1 | std::shared_ptr<MyTime> mt1(new MyTime(10)); |
能保证对象在没有指针指向它的时候能删掉
独有指针
1 | std::unique_ptr<MyTime> mt1(new MyTime(10)); |
一个独有指针能阻止其他指针指向它的对象,但可以通过move转移给别的指针
Class Inheritance
基类 vs. 派生类
派生类从基类继承属性与函数,C++支持多基类继承和多层继承
派生类的构造函数
实例化一个派生类对象时,分配内存,派生类构造器被调用,必须首先调用基类构造器(会默认调用无参构造器,一般通过初始化列表将基类信息传给基类构造器),然后再执行派生类构造器的其他内容,初始化派生类独占成员。
派生类的析构函数
与构造函数相反,首先调用派生类的析构函数,然后调用基类的析构函数
访问控制
protected:类似private,成员、友元和派生类可以访问
protected inheritance:基类中的public和protected成员在派生类中成为protected,只能被各级派生类访问
private inheritance:基类中的public和protected成员在派生类中成为private,只能被一级派生类访问
虚函数
如果使用语句base *p=derived_object
,且派生类和基类拥有相同函数头的函数,则会绑定基类函数。
但如果基类函数设置为virtual,则会使这个函数在基类和所有派生类中均虚拟,动态绑定到派生类函数
虚析构器
如果基类构造器未设置为virtual,则上述例子delete p
时只会调用基类析构函数,不会调用派生类析构函数!
动态内存分配
如果基类使用了动态内存分配,并且重载了复制操作符和赋值操作符:
- 如果派生类没有动态内存分配,就无所谓
- 如果派生类有动态内存分配,则应该重定义复制操作符和赋值操作符
Class Templates and std Library
类模板
1 | template<typename T> |
非类型参数
参数可以有:类型模板参数,模板模板参数,非类型模板参数(整数、浮点、指针、左值引用…)
1 | template<typename T, size_t rows, size_t cols> |
特例化
1 | template <> |
std类
一些基于模板类的,封装好的容器
Error Handling
stdin,stdout,stderr
1 | fprintf(stdout,"info",...);//C |
重定向
1 | ./a.out <1.in >1.out 2>1.err |
assert
如果条件句为true则不做处理,否则输出诊断信息并调用abort()中止程序。
异常
throw了可以不catch,只不过抛到main里还不捉就会中止运行。
一个try可以被多个catch跟着,但不能没有,如果没有被catch就会继续抛到调用函数的函数
catch(…)可以捉住所有异常
catch(const Base &base)可以捉住子类异常
noexcept关键字保证函数不会抛任何异常
nothrow在new的时候会使用一个不抛异常的申请函数
Nested Classes and RTTI
友元类
友元类可以在任何权限域声明,可以访问所有成员
一个类成员函数可以声明为他类友元函数,不过要注意声明顺序问题
1 | class Sniper |
嵌套类
也有三个scope,private, protected, public
RTTI
dynamic_cast:多态类型间转换
typeid:鉴别两个类是否相同
type_info:typeid操作符返回一个该类型引用,可以用重载比较符比较是否为同类
1 | derived_object=dynamic_cast<Base>(&base_object); |
const_cast:常数转换
static_cast:只有能隐式转换的时候才有效,不然返回错误