C++ DevNote
* 新增C++ 文件,可直接在Xcode 对应目录右键New,按步骤操作就行,将文件目的路径选到对应目录,生成的源文件及头文件都会出现在目录中。要将头文件显示到Header File目录,需要执行tbt_build.sh 脚本(无需加pull),更新工程信息就行。
* 在头文件中使用到其他类的指针或者引用,可以不将对应的头文件引进来,做一个类名的声明,然后将对应的头文件引入到实现文件中,这样可以减少头文件依赖,提升编译速度。但对于普通变量不行,因为编译时需要知道他们的内存占用。
* 头文件循环依赖时,会出现引入了头文件,还提示找不到类的情况。此时应该按第二条解依赖
* C++ int 转枚举值,可以使用static_cast<Enum>(int) 或者Enum(int)两种方式,如果int值在枚举范围之外,也会返回一个对应值的枚举,不会报错。如果枚举中未定义负数,而int值为负数,则返回的枚举值会很大,后续可研究一下?
* 在inttypes.h中定义了宏PRId32和PRIu64,用于打印intN_t和unitN_t这样的整数类型,这样可以表示数据宽度,在32和64位系统上替换为不同的形式,实现可移植性。
* Xcode调试时,可以直接在output窗口中使用 p [表达式] 的方式执行表达式
* memset注意事项: 1、类中有对象类型,如string、数组、list、set、map等时,memset会导致对象内存被破坏,应该使用构造函数; 2.类中有虚函数时,memset会破坏虚函数表,后续对虚函数的调用会出现异常;3. 类中的指针类型,值会被置为0,其所指对象不受影响。
* LLDB中,以p开头加空格,后跟变量名或方法,可以查看变量值或方法返回值。
* 字符数组必须以null字符(‘\0’)结束,是为了标记字符结束位置,字符数组才能正确识别内容,否则可能出现乱码,另外如strlen的字符数组操作函数,会一直向后搜索知道遇到null字符才会结束,产生未定义的结果。
* 定义常量在多个文件中共享,需要在头文件的声明和cpp文件的定义中都加上extern,这样该常量仅会定义一次。
* for循环中的index不建议使用auto或者unsigned类型,最好直接写明使用int类型,因为unsign int -- 变为负数时,会自动转换为unsign,高位的符号也被视为数值,此时将得到一个非常大的index值,导致循环无法退出;
* 任何时候都不能混用有符号和无符号整数,否则有符号会自动转为无符号,将得到非常大的值。
* C++ 中用不建议用const char* 作为map的key或者作为find的实参,理由是可能:1.某些编译器可能会直接比较指针,导致查找失败;2.Map数据重复,导致find陷入死循环不退出,非得要用定义比较函数时也应该用strncmp而非strcmp
* 常量引用可以指向其他类型的对象,因为实际指向的是编译器会生成同类型的临时变量;普通引用不能指向其他类型的对象,因为即时编译器生成了临时变量,也失去了改变引用值的作用,所以C++将这种操作定义为非法
* 定义在函数之外的和方法内有效范围超出方法的变量(如方法内的静态变量),拥有固定地址,可以用于初始化constexpr的指针和引用;方法内定义的变量,地址不固定
* constexpr的作用是,开发人员不确定变量是否是真正的常量,交由编译器来检测,如果不是,编译器会报错。如果要使用方法初始化constexpr变量,只能使用constexpr方法(只应有一个return语句,且形参及返回值应该为字面值)。constexpr的指针使用顶层const,即指针对象本身不可变。
* 常见类型的const都是顶层const,表示变量本身不可改变,顶层const在复制拷贝时没什么影响;指针和引用拥有底层const,表示指向的对象不可改变,底层const在拷贝复制时,需要有同样的底层const声明。
* 定义别名有两种方式,typedef和using, using pstring = char*
* 别名用于复合类型时,需要注意:using pstring = char* , const pstring str却表示str是指向char的常量指针(顶层const)
* 函数调用包含一系列工作:调用前先保存寄存器,返回时恢复,可能需要拷贝实参:程序转向一个新的位置继续执行。
* 内联函数效率高,是因为它会在每个调用点“内联的”展开。适用于优化规模较小、流程直接、频繁调用的函数。很多编译器不支持内联递归函数;内联说明只是向编译器发出一个请求,编译器可能忽略之。
* 内联函数和constexpr函数可以多次定义(为了方便在调用点展开),但每个定义必须完全一致。
* NDEBUG宏用于识别当前运行环境是否是调试环境,可以直接使用ifndef NDEBUG判断是在调试环境。编译程序时,可通过命令行选项加上NDEBUG定义,如下在main.c文件的开头加上 #define NDEBUG:
$ CC -D NDEBUG main.c
* assert是一种预处理宏(预处理变量,由预处理器管理),仅用于在调试期间(依据NDEBUG宏而定)检查不能发生的事情。如果传入的表达式为false,assert输出信息并终止程序。
* 预处理器定义了多个变量来输出信息帮助调试,每个变量都是const char的一个静态数组,存放对应的字面值:
__func__ 输出当前调试函数名字
__FILE__ 当前文件名
__LINE__ 行号
__TIME__ 编译时间
__DATE__ 编译日期
* 除了作为函数返回值时(此时需要把返回值写成指针形式),其他时候函数名(不需要带调用符号)都能自动转换为函数指针,如实参、赋值等。使用函数指针时,不需要解引用(解也没问题);可以将函数指针置为nullptr或者0。使用decltype推断函数类型时,返回的是函数类型,而非函数指针,需要手动为返回结果加上 * 才能称为函数指针。
* int (*f1(int))(int*, int) 表示一个一个函数f1,返回函数指针int (*)(int*,int);等价的有尾置返回方式: auto f1(int)->int (*)(int*,int)
* &和*只修饰变量而非类型,所以 int* p,q的声明中,q并非指针。
* int *&r表名r是一个对int指针的引用。遇到复杂的类型定义时,应该从右向左读其定义,最近的&表明r是个引用,其次* 表明引用的是指针。
* auto定义的变量必须有初始值(用于推断类型)。auto可以在一条语句中定义多个变量。
* auto推断类型时(除了推断出是引用)通常会忽略顶层const(可以手动加上:const auto),而保留底层const。
* decltype会保留顶层const;decltype(* p)和decltype((var))永远返回引用,因为这两个都是返回左值的表达式;decltype(var)返回变量对应的类型
* 不要混用有符号和无符号整数,二者一起运算时,会按照类型所占空间大小和能否容纳另一类型的值进行类型转换,一般是向宽转换,即有符号转为无符号,很容易将有符号转成大整数,导致结果(不同机器结果不一样,大小判断或其他运算都可能出错)出错;当从无符号数中减去一个数时,须确保结果不能是负数(出现在for循环中会导致死循环);
* 当吧字符字面值和字符串字面值与string相加是,加号的两侧必须有一个是string类型,因为另两种类型不支持相加操作
* C++为了兼容C的标准库,将同内容的C头文件命名为c+原文件名,并去掉了.h后缀,如cctype原为ctype.h。与原文件的一个很大的区别是:重命名后的头文件中的内容属于std命名空间,所以强烈建议使用新的文件
* 遍历string可使用迭代器或for(auto c :str)的形式,如果要改变其中内容,需要将循环变量定义成引用,如for(auto &c:str)
* 因为引用并非对象,所以不能作为vector的元素。vector是模板而非类型,早期C++标准要求定义元素为vector的vector时,需在外层vector的右尖括号和元素类型间加一个空格,如vector<vector<int> > 而非vector<vector<int>>,现在部分编译器可能还会如此要求。
* 范围for语句体内不应该改变所遍历序列的大小,即使是添加元素。
* 整型提升负责把小整数类型转换成大整数类型。如bool、(u)char、(u)short,只要它们所有的值都能存在int里,则都会提升为int;否则提升为uint。较大的char(wchar_t, char16_t,char32_t)提升为(u)int\(u)long\(u)longlong中最小的一种,前提是能容纳其所有可能的值。
* 如果某个运算符的运算对象不一致,将会转成同一种类型后再运算。如果其中一个是无符号类型,转换的结果依赖于机器中各整数类型的相对大小。首先进行整形提升,如果提升后类型一致,则无需进一步转换。若提升后要么都是无符号,要么都是有符号,则小类型向大类型转换;若提升后一个为无符号,一个有符号,且有符号类型小于无符号,则向无符号转换;若有符号大于无符号类型,转换结果依赖于机器实现。若无符号能够完全存入有符号类型,则向有符号转换,否则向有符号转换。
* 数组大小不变,因此性能更好,但损失了灵活性。数组的大小也是类型的一部分,不同大小的数组,即使元素类型相同,其类型也不同。数组大小必须是一个常量。
* 字符数组可以用字符串字面值来初始化,但数组长度必须为字符数+1,因为字符串字面值结尾还有一个空字符。
* 数组不允许直接拷贝或赋值给其他数组,即使某些编译器扩展支持了数组间的赋值,也是非标准特性,不建议使用
* 复杂数组类型应该从内向外,从右向左读,如 int *(&array)[],首先括号内表明array是一个引用,右边[]表明array是对数组的引用, 最后的int *表名引用的数组的元素是int指针
* 指针和数组有着非常紧密的联系,使用数组时编译器一般会把它转换成指向数组首元素的指针。使用auto推断数组时,会得到元素的指针类型;使用decltype时,返回数组类型而不是元素指针类型。
* 可以使用标准库函数begin和end获取数组的首元素和尾后指针,像遍历vector一样遍历数组。需注意end返回的是尾后指针,不能解引用和递增。
* 获取数组大小有两种方式,sizeof(array)/sizeof(array[1])和 end(array)-begin(array), 两者都只能使用数组类型,不能使用数组转换成的指针(sizeof(指针)得到的是指针所占内存大小,一般机器为8字节),后者返回ptrdiff_t类型,是有符号的。
* 如果两个指针指向同一个数组,则可以进行比较大小,否则比较没有意义。比较时,返回值表示的是指针的大小
* int *p = &array[2], 则p[1]对应array[3], p[-2] 对应array[0]。 注意,数组的下标可以为负数,vector等标准库容器和string则不行。
* C风格字符串以空字符结束(’\0‘),用于标识字符的结尾,不然strlen等函数可能一直向前查找知道遇到空字符。strcmp用于比较, strcat用于附加,strcpy用于拷贝, 用后面两个时,需确保有足够的空间容纳数据。使用string更安全,更高效。
* string.c_str()返回的是一个以空字符结尾的const char*, 如果需要一直使用返回的值,最好拷贝一份,因为原始值很可能在其他地方被修改。
* 类的成员函数参数列表后加上const后,该函数无法修改实例的状态,因为隐式的this指针变为const,指向了const的对象。
* 常量对象,及其指针或引用,只能调用常量成员函数,因为非常量成员函数需要改变对象状态,这是相悖的;非常量对象,可以调用常量或非常量的成员函数。
* 编译器分两步处理类,先编译成员的声明,然后才编译成员函数。所以成员函数能够访问所有的成员,而无需关注成员的出现顺序。
* 类的内部是指类声明时的花括号以内,超出该括号都视为类的外部。类的外部要想使用类的成员,需要加上类名和作用域运算符指明。外部定义的成员函数正因为带了作用域运算符,所以其参数和函数体内可以访问类的任意成员,但返回值不在作用域运算符作用范围内,所以返回值如果使用了类内部的类型,还需要另加作用域运算符。
* 类的非成员函数,也可以按声明和定义来实现,声明跟类定义放在一个头文件中,这样在函数互相调用时,不用关心出现顺序。也可以直接定义在CPP文件中,但是需要注意出现顺序,而且其他类无法使用这些方法了。
* 类成员的初始化,有三种方式,构造函数初始值列表(推荐,走每个成员的构造函数);类内初始值(变量直接赋值,部分编译器不支持,可能被初始值列表覆盖);构造函数体类手动赋值。如果上述三者都没做,执行默认初始化。
* =default(C++11)可以让编译生成合成的默认的构造函数。某些场景不适合生成默认构造函数,1.已经定义构造函数;2.类含有内置类型或复合类型(数组和指针)而没有类内初始值时,会得到未定义的值;类包含了没有默认构造函数的类类型成员时。
* vctor和string能帮助我们管理元素的内存空间,避免分配和释放内存的复杂性。使用动态内存的类可以使用它们来协助管理内存。因为vector会负责元素的拷贝、赋值和销毁。
* 类定义时,访问说明符的数量无限制,一个访问说明符的有效范围从声明开始,直到下一个访问说明符出现或类结束才终止。
* 定义类时,使用class和struct的唯一区别是默认的访问权限, class默认为private ,而struct 默认为public。
* 使用友元,类可以让其他类或类的成员函数访问它的非公开成员(不包括成员函数)。友元一般声明在类定义的开始处,且只能出现类定义的内部。只需在函数或类前加上friend即可。友元不具备传递性,即一个类的友元类不能访问以该类为友元的类的非公开成员。
* 友元仅仅指定了访问权限,它对应的函数仍需真正的声明(部分编译器未限制),否则将无法使用。
* 当类被修改后,无需修改用户代码,但是使用了类的文件必须重新编译。
* 类可以定义其他类型在类中的别名,并必须通过访问说明符public或private修饰,控制用户是否能够使用该类型别名。
* 可以把类的数据成员声明为mutable,让其变为可变数据成员,这样即使在const成员函数中,也可以改变该成员的值。
* 类内初始值,必须使用=或者花括号来初始化对应的成员。
* 成员函数返回*this时,返回值应该采用引用的方式,否则返回的是副本。const成员函数返回的引用将是const引用。
* const函数也可以重载非const版本,反之亦然。编译器根据调用对象是const还是非const选择对应的版本。
* 定义类型变量时,加上class或者struct前缀也是合法的。如 class user myUser;
* 仅声明而未定义的类为不完全类,可以定义指向它的指针和引用,也可以声明(但不能定义)以它作为参数或返回值的函数。类只有在定义完成后才能当做类型使用(编译器才知道它所需空间),所以类不能有以它自己为类型的成员,但可以有自己为类型的指针或引用。
* 若想让A类的成员函数成为B类的友元,则需要: 1.先定义A类并声明对应的成员函数(但不定义);2. 定义B类,并将A的成员函数声明为友元;3. 定义A的成员函数,此时才能访问到B的非公开成员。
* 类中的类型名不能重复定义,所以类型名通常应该统一出现在类的开始处,确保所用使用该类型的成员都出现在类型名定义之后。
* 在成员函数中,如果外层的成员变量被(参数或局部变量)隐藏掉了,可以通过作用域(仅::即可)运算符访问它
* 成员变量的初始化和赋值有较大差异,初始化是在构造函数体执行前进行的,赋值是在构造函数体中进行的,前者直接初始化成员,效率更高,后者先初始化再赋值,使用初始值列表还能避免某些意想不到的编译错误(如有的类含有需要构造函数初始值初始化的成员);const或者引用类型的成员,以及无默认构造函数的成员,只能通过初始值列表初始化。
* 成员变量的初始化顺序由声明成员的出现顺序决定,而非初始值列表的顺序决定,毕竟不是所有的类都有初始值列表,但却必须有成员初始化顺序;不过仍然建议初始值列表与成员出现顺序一致(若不一致有的编译器会警告);避免用一个成员初始化另一个成员
* 如果构造函数的所有参数都提供了默认值,则该构造函数也是默认构造函数
* 委托构造函数,在类外部声明,如同用初始值初始化该类一样,如:User::User(name):User(name, 18){}, 委托给了User(name,age)
* 当构造函数委托给另一个构造函数时,总是先执行受委托者的初始值和构造函数体,然后才执行自己的函数体;委托者不能再有初始值列表。
* 默认初始化发生在:
- 在块作用域中,不使用任何初始值声明非静态的变量或数组时
- 类含有类类型成员且使用合成的默认构造函数时
- 类类型成员没有在初始值列表中显示初始化时
* 值初始化发生在:
- 数组的初始值数量少于数组大小时
- 不使用初始值定义局部静态变量时
- 使用T()的形式显示请求值初始化时
* 使用默认构造函数时,不用加(), 如局部变量User user() 定义了user函数, 而User user 表示用默认构造函数创建了user对象。
* 多维数组时数组的数组,即数组的元素仍是数组,使用下标索引多维数组时,如果下标维度小于数组维度,返回的是数组类型;下标维度等于数组维度时,返回的是给定类型的元素。
* 使用范围for语句(类似Java的foreach)遍历多维数组时,除了最内层的循环变量不用使用引用类型外,其余没层的循环变量都必须使用引用类型,否则循环变量会被自动转换为数组首元素指针,而我们无法对指针进行范围for循环。
* 多维数组的名字也会自动被转为指向首元素的指针;访问多维数组外层维度时,使用auto或者decltype能避免书写复杂的数组元素类型;另外为外层维度类型定义别名也能简化理解书写:
- typedef int IntArray[4]
- using IntArray = int[4]
用法: IntArray *p = array;
* 只含有一个参数的构造函数允许隐式的将参数类型的对象转换为类对象,如string的只有const char*的构造函数,允许将const char* 直接赋值给string对象,实际上是隐式转换为了string。很多时候这种转换容易导致问题,可通过在声明该构造函数时在前面加上explicit来抑制转换,加上该关键字后可以使用static_cast 来显示转换,也可以用直接初始化的方式,即User user(“ARES”)这样的形式 。
* 聚合类,是一个纯数据类,允许像用初始值列表初始化数组一样初始化这样的类,但是不推荐使用,因为一旦新增和删除成员,所有初始化的地方都需要更新,这种类需要包含以下条件:
- 所有成员都是public的
- 没有构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
* 基本类型,引用,指针都是字面值类型(?能直接确定所占空间大小的类型?)
* constexpr构造函数一般是空的(因为需要同时满足),用于生成constexpr对象,以及constexpr函数的参数和返回值(都要求是字面值类型)
* 字面值常量类的对象可以用作编译时的常量表达式,数据成员都是字面值类型的聚合类是字面值常量类,另外包含以下条件的类也是:
- 数据成员都是字面值类型
- 至少含有一个constexpr构造函数,成员函数必须是constexpr的
- 如果成员含有类内初始值,则其初始值必须是常量表达式;或者如果成员时类类型,初始值必须使用成员类型的constexpr构造函数。
- 类必须使用默认析构函数
* 在外部使用类的静态成员,可以通过类名::的方式使用,也可以通过对象、引用和指针使用。
* 类的静态成员函数,既可以在类内部定义,也可以在类外部定义,在外部定义时,也需要指出所属类名,另外不能重复static关键字。
* 类的静态成员变量,只能在类内部声明,但是必须在外部定义(初始化),且必须定义,否则会出现找不到变量的问题, 如 int User::MaxAge = 100; 要确保对象只被定义一次,最好将静态成员变量和非内联函数的定义放在同一个(CPP)文件中(因为CPP文件不会被多次引用,而头文件多次引用后会导致多次定义而出错)
* 仅用于编译时替换的const、constexpr静态成员变量,可直接在声明时用常量表达式初始化即可,但是也建议在外部定义一下,但是不用初始化(即重复赋初值)。
* 静态成员的优势:
- 可以是不完全类型,意味着可以在类的定义中,可以声明类自身类型的静态成员,如果是非静态成员,则只能声明引用和指针
- 可以作为成员函数的默认实参
* 定义类类型的局部变量时,如果不赋值,且有默认构造函数时,局部变量会默认初始化;赋值会走拷贝构造函数,可避免无意义的默认初始化,所以不建议在赋值的场景先定义后赋值,应该在定义的同时赋值;成员变量的默认初始化是在构造函数初始值时进行的,也可以通过构造函数初始值初始化避免默认初始化。
* IO对象不能拷贝和赋值,通常以引用的方式进行传递;另外读写IO对象会改变其状态,所以IO对象的引用不能是const的
* 通常将流操作作为一个条件来用,如 while(cin >> value),这样操作失败时就会退出循环。流对象还提供了eof()、fail()、bad()、good()、clear()、setstate(flags)、rdstate()等方法来判断、设置和获取流状态,流状态是bit flag类型,其中badbit时,流无法恢复和使用了。
* iostream是读写流的基类,fstream用于读写文件,sstream用于读写string,宽字符版本的会在前面加上w前缀,如wsstream
* 导致缓冲刷新的原因有:
- 程序正常结束,main 函数的return操作会刷新缓冲区
- 缓冲区满时
- 手动使用endl等显式刷新
- 每个输出操作后,用unitbuf操作符刷新。cerr设置了unitbuf,所以是立即刷新的
cout << unitbuf; //所有输出操作后都立即刷新
cout << nounitbuf; //取消立即刷新
- 输出流关联到另一个流,这样读写另一个流时就会刷新输出流,如cout是关联到cin的,交互式系统都应该如此,保证输出信息在读之前都被打印出来。
cout.tie(&cin); //关联到cin
cout.tie(nullptr); //取消关联
cout.tie(); //返回当前关联的流指针或nullptr
* 每个流同时最多关联到一个流,但是一个流可以同时被多个流关联
* 调用ofstream out的open后,最好用if(out)判断stream对象的状态看看是否open成功;ofstream析构时,会自动调用close()关闭文件。
* 如果程序异常终止,输出缓冲区不会被刷新,所以异常终止的程序日志不全,很大可能不是因为没执行到代码,而是因为缓冲区没来得及刷新
* 文件流可以用in(读), out(写), app(追加), ate(定位到末尾), trunc(截断丢弃已有内容), binary(二进制)等方式打开,trunc的前提是out,out默认是trunc,会丢弃已有内容,除非同时指定app或in(读写模式),trunc和app互斥。如 ofstream(file,ofstream::out)
* istringstream可绑定一个string,通过输入操作符>>从string中读取数据,如同从标准输入流读取一样,读取时遇到空格中断一次,视为一次读取
* ostringstream如同Java的StringBuffer,可以将多个字符用输出操作符<<拼接后,最后统一处理。
* 顺序容器:
- vector 可变大小数组,支持快速随机访问,在尾部之外插入和删除元素很慢,类似与Java的ArrayList
- deque,双向队列,支持快速随机访问,在头尾插入删除速度快
- list 双向链表,只支持双向顺序访问。任何位置插入删除都快
- forward_list 单向链表。只支持单向顺序访问。任何位置插入删除都快,不支持size操作
- array 固定大小数组,支持快速随机访问,不能添加和删除
- string 与vector相似的用于保存字符的容器,随机访问和在尾部插入删除快。
* 选择容器的原则:
- 如果没有更好的理由选择其他容器,都选择vector
- 如果有很多小元素,且很介意空间的额外开销,就不要用list和forward_list
- 如果要求随机范围元素,选vector或deque
- 若要在中间插入和删除,选则list和forward_list
- 若要在首尾插入和删除,不会在中间插入和删除,选deque
- 使用vector和list的公共操作,如迭代器,不要用下标操作,需要时可随时互相切换
* 使用指定元素个数的构造方法初始化vector时,若元素无默认构造函数,则必须同时指定初始化器,如vector vc<Type>(10, init); 只有顺序容器支持大小参数,关联容器不支持
* 顺序容器还提供了静态成员value_type用于获取元素类型,reference获取元素的左值类型, 与value_type& 含义相同,const_reference用于获取元素的const左值类型,即const value_type& ; difference_type用于表示两个迭代器之间的距离
vector<int>::value_type type;
decltype(vec)::value_type type;
* swap(a, b)等价于a.swap(b), 可以交换ab两个容器的元素
* 容器的insert是直接(赋值内容)插入元素,emplace是使用参数调用元素的构造函数构造对象并放入容器中。
* 容器支持reverse_iterator, const_reverse_iterator及对应的rbegin, crbegin 等成员,forward_list不支持逆向操作。
* 当将容器初始化为另一个容器的拷贝(赋值或直接初始化)时,要求两个容器的类型必须一样;使用迭代器范围初始化时,只需要保证元素可以转为为目标容器的元素类型即可
* assign(begin, end)将容器的元素指派为迭代范围之间的元素,assign的参数还支持初始化列表和n个值的形式。不同于赋值操作,assign只要求元素内容相容,能够互相转换就行。
* array 和内置数组很像,大小也是它类型的一部分,array可以作为函数返回值,同时array支持拷贝和赋值,前提是类型相同(元素类型和大小相同);由于右边的运算对象可能与左边的运算对象大小不同,因此array不支持assign和花括号列表赋值。
* swap 操作只是交换了两个容器的内部数据结构(包括迭代器等数据成员),元素本身并未交换,不会对元素进行拷贝、插入和删除,所以速度很快,原迭代器所指的值不会变化;但是array会真正交换对应位置的元素,速度与元素个数成正比,迭代器的值也会变为对方对应位置的值。
* swap的两个容器,元素多少可以不相同。
* 赋值操作会导致指向左边容器的迭代器,指针和引用失效; swap操作只是交换容器内容不会导致容器的迭代器、引用和指针失效(array和string除外)
* 容器支持用关系运算符比较,所有容器都支持=和!=, 除了无序关联容器外,都支持大小比较运算符,前提是比较的两个容器类型及元素类型必须相同,且元素必须也同时支持相应的运算符,因为容器比较是用元素的运算符完成的
* 向vector、string、deque插入元素会导致其迭代器,引用和指针失效
* 容器中所有元素都是通过构造(emplace)或拷贝而来,跟用来初始化它的元素没有引用关系,对元素的修改不会影响原先的对象。
* 容器的emplace(本意安放)方法,使用对应的参数调用对应的构造函数直接在容器中构造元素,不传参数时,会调用元素的默认构造函数构造对象。
* 顺序容器的pop_front和pop_back分别删除元素的首尾元素;erase删除迭代器制定的元素,返回被删元素下一位置的元素迭代器;clear删除所有元素。
* 容器的插入都是在传入的迭代器所指位置之前插入,返回插入元素的迭代器,插入多个时返回最新插入元素的迭代器;删除元素后,返回下一元素或尾后迭代器。
* 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效;指向vector或string删除点之后的迭代器、引用和指针都会失效。
* string、vector、deque和array支持at和下标操作,提供随机访问能力;下标和at操作的区别在于,数组越界时,at会抛出out_of_rang异常,下标行为未定义。
* 顺序容器还支持front和back操作,分别返回容器的首元素引用和尾元素引用,如果容器为空,行为未定义。
* 容器访问元素的成员函数返回的是元素的引用,如果容器是const的,则为const 引用,不能改变元素的值;如果用auto来保存返回值,需要将变量定义为引用才能改变元素的值。
* 等号右边即使是引用,左边也可用非引用的变量来接住它的值 ,所以auto并不能推断等号右侧的对象为引用,需要手动将左边变量声明为引用类型(auto &var)。
* 容器的迭代器指针地址可能为负数,所以迭代器不一定是地址,可能只是一个映射地址。
* vector的正序迭代器,一般对应的就是元素的地址, 尾后指针也确实是尾元素的后一内存位置,逆序迭代器的值则一直是最后一个元素的地址,应该是重载了++运算符,解引用时返回了不同元素的值。
* deque双向队列通常是有一个映射表+一系列不一定连续但固定大小的数组块实现的,映射表保存了指向数组块的指针,这种实现提供了随机访问的能力,也保证了双端插入和删除的效率。
* forward_list是单向链表,不方便获取元素的前驱,所以提供的方法都是后向操作的,如insert_after、emplace_after和erase_after, 另外还提供了before_begin(首前迭代器,对应尾后迭代器),用于在第一个元素前插入元素。
* resize用于调整容器大小,变小会丢弃多出的元素,变大时使用传入的初始值参数初始化元素,或者进行值初始化,如果元素使类类型,必须提供初始值,或者该类型拥有默认构造函数。
* 如果resize缩小容器,则指向被删除元素的迭代器、指针和引用都会失效; 对vector、string或deque进行resize可能导致迭代器、指针和引用失效,因为可能重新分配内存,复制元素。
* 向容器添加元素时:
- 对于vector或string,若内存重新分配,则迭代器、引用和指针都失效;否则,插入位置之后的迭代器、引用和指针都失效
- 对于deque,插入到首尾位置之外的位置都会是迭代器、引用和指针失效;插入到首尾位置,迭代器会失效,但指向已存在元素的引用和指针不会失效。
- 对于list和forward_list, 没有影响
* 从容器删除元素时:
- 对于list和forward_list, 指向其他位置的迭代器、引用和指针仍有效
- 对于deque,若在首尾之外的任何位置删除,其他位置也会失效;若删除的是尾元素,尾后迭代器会失效,其他迭代器不受影响;若删除首元素,其他位置不受影响
- 对于vector和string,指向被删元素之前的迭代器、引用和指针仍有效,尾后迭代器总失效
* 让当前线程睡眠 10s: std::this_thread::sleep_for(std::chrono::seconds(10)); 也可调用unistd头文件中sleep(seconds)实现。
* 使用 [[deprecated("Use newFunction() instead")]] 标记方法、类、别名等已弃用
* 强类型枚举(enum class)用作unordered_map的key时,需要实例化对应类型的hash模板,否则会报错: implicit instantiation of undefined template
* 出于性能考虑,C++基本类型的非静态(全局、局部)变量不会自动初始化,需要手动初始化;复杂类型的具有默认构造函数的变量会自动初始化
* RAII全称Resource Acquisition Is Initialization, 常用于线程锁,利用作用域自动完成加锁和释放锁。ru:
std::lock_guard<std::mutex> lock(mLock); // 使用RAII方法管理互斥量