Introduction
弃坑一段时间BGO后,为了填补上班这一个小时的空闲时间,重新看了一遍《Primer c++ 》。“温故而知新”这句话还是很有道理的。竟然发现自己在实际工作当中,有很多c++的优秀特性没有利用上。趁热打铁,重新整理一下C++笔记。
第一章 开始
- 略,太基础了!
- 注意:
- 在包含头文件时,<> 和 “” 的选择
- <> :包含来自标准库的头文件时使用。
- 使用尖括号时,编译器会在包含路径列表中搜索文件。
- 个人觉得,使用标准库、引用的第三方库,或自己编写的库时,使用 <>
- “” :先在当前目录搜索(即正在编译的模块所在的目录),然后搜索包含路径列表。
- 一般是使用具体实现代码的头文件时使用双引号。
- <> :包含来自标准库的头文件时使用。
- 在工作中看到很多人无脑使用双引号,自己为了保持编码风格,也就染上了这个习惯。看来分情况使用<> 和 ""会减轻编译负担。
- 在包含头文件时,<> 和 “” 的选择
第二章 变量和基本类型
-
使用unsigned 类型变量进行递减迭代
- 在实际开发中还真没注意这种情况!如下:
for(unsigned i=0; i>=0; --i) ...
- 该循环为死循环。因为unsigned 是无符号数,不存在小于0的情况。因此,当 i = 0 i=0 i=0 进行减法操作时, i i i 会溢出变为最大值再进行递减,这样循环无法终止。
- 虽然一般不会有人这样写循环。但是有必要注意一下unsigned 变量的用法。因为工作中遇到过这种奇葩问题。坑了我两个多小时。再向引擎请求返回某个uniform值的时候,每次都获取到一个很怪异的值。最后发现,在查找时,使用unsigned 返回索引,而当查找不到字符串的时候该方法返回-1。真是醉了!
- 在实际开发中还真没注意这种情况!如下:
-
整型字面值
- 0开头八进制
- 0X或0x开头为十六进制
-
字符串字面值的实际长度是内容+1
- 有空字符结尾占一位(’\0’)
-
如果只需要声明非定义变量使用extern关键字
- extern int i;
- 变量只能被定义一次,但可以被多次声明
-
定义在函数体外的标识符不能以下划线开头
-
当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。
int i = 1; { int i = 2; int j = ::i; //获取全局变量的值 int j = i; //获取局部变量值 }
-
引用
- 引用必须初始化
- 引用并非对象,它只是一个已存在对象的另一个名字
- 不能定义引用的引用
-
指针
- 指针为一个对象
- 指针无需定义初始值
- &(取地址符) 获取对象指针
- 指针类型要与它指向的对象严格匹配(声明类型)
- 指针的值有四种状态
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,不指向任何对象
- 无效指针,除了上述情况之外的其他值
- *解引用符访问对象(适用于指向对象的有效指针)
- 空指针字面值 nullptr(字面值一般是0)
- c++新标准下尽量不使用NULL
- 比较两个指针指向地址的同异可以使用 == 或 !=
- 必须是相同类型指针
- 结果是bool值
- void* 是一种特殊的指针类型,可以存放任意对象的地址
- 用法有限
- 做指针比较
- 作为函数输入输出
- 赋给另一个void*指针
- 不能直接操作void*指针指向的对象
- 用法有限
-
引用本身不是一个对象,不能定义指向引用的指针
-
指针是对象,可以定义指针的引用
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字
-
const的引用
- 允许一个常量引用绑定非常量对象、字面值,甚至一个表达式
-
常量指针
- 指向常量的指针不能用于改变其所指对象的值
- 允许一个常量指针指向非常量对象
- 不能通过常量指针改变对象的值
- 常量指针必须初始化
- 一旦初始化它的值就不能改变
- 把*放在const关键字前
- 指针是一个常量
- 意义:不变的是指针本身的值而不是指向的那个值
- 顶层const
- 指针本身是常量
- 底层const
- 指针所指的对象是常量
int i = 0; int *const p1 = &i; //顶层const const int j = 0; const int *p2 = &j; //底层const
-
constexpr 和常量表达式(const expression)
- 常量表达式是指不会改变并且在编译过程中能得到计算结果的表达式
- c++11新标准规定,允许将变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式
- 如果认定变量是一个常量表达式,就把它声明成constexpr类型
- constexpr 指针的初始值必须是nullptr或0
- constexpr 指针指向的是一个数,而非一个数据类型
-
类型别名
- 传统方式使用关键字 typedef
- typedef double vages;
- 新标准使用using
- using SI = Sales_item
- 传统方式使用关键字 typedef
-
auto 类型说明符
- auto定义的变量必须有初始值
- auto会忽略掉顶层const,同时底层const会保留下来
-
decltype 类型指示符
- 选择并返回操作数据类型
- decltype(f()) sum = x; //sum的数据类型是函数f的返回类型
- decltype((variable))(注意双层括号)的结果永远是引用
- decltype(variable)结果只有当 variable本身是一个引用时才是引用
-
头文件
- 头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明
- 预处理器
- 确保头文件多次包含仍能安全工作的常用技术
- 预处理器是在编译之前执行的一段程序,可以部分改变我们所写的程序
- 如:当预处理器看到#include标记时就会用指定头文件的内容代替#include
- 头文件保护符
- 依赖于预处理变量
- 预处理变量有两种状态
- 已定义
- 未定义
- #define
- 把一个名字设定为预处理变量
- #ifdef
- 当且仅当变量已定义为真
- #ifndef
- 当且仅当变量未定义为真
- #endif 终止符
- 预处理变量无视C++语言关于作用域的规则
- 为了避免与程序中的其他实体发生名字冲突,一般预处理变量的名字全部大写
第三章 字符串、向量和数组
- 头文件不应包含using声明
- string
string s(10, 'c')
初始化有10个c的字符串。(没注意到还可以这么用)- 使用等号(=)初始化变量,实际上执行拷贝初始化
- 不使用等号(=)执行直接初始化
1. string s = string(10, 'c')';
2. string temp(10, 'c');
3. string s8 = temp;
- 1 和 2+3 等价,因此,尽量使用直接初始化
- 当把string对象和字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧运算对象至少有一个是string
- 访问string对象中的单个字符串
- 使用下标
- 使用迭代器
- C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如
name.h
,C++则将这些文件命名为cname
。也就是去掉了.h
后缀,而文件名name
之前添加了字母c, 这里的c表示这是一个属于C语言标准库的头文件。 for (auto &c : str)
迭代使用引用比较好- 迭代器
- end 成员负责返回指向容器’‘尾元素的下一位置的迭代器’’
*iter
返回迭代器iter所指元素的引用- 使用递增(++)运算符从一个元素移动到下一个元素
- 如果vector 或 string对象是一个常量,只能使用const_iterator
- 如果vector 或 string对象不是常量,即能用iterator也能使用const_iterator
- C++引入 cbegin 和 cend
- 与begin 和 end 相同点
- 返回容器第一个元素或最后元素下一位置的迭代器
- 与begin 和 end 不同点
- 无论vector对象或string对象本身是否是常量,返回值都是const_iterator
- 与begin 和 end 相同点
- (*it).empty()中的圆括号必不可少
- C++定义了箭头运算符(->)
- 把解引用和成员访问两个操作结合在一起
it->men
和(*it).mem
等价
- 当两个迭代器指向同一个容器时,就能将其相减,所得到的结果为两个迭代器的距离
- 距离指:右侧迭代器向前移动多少位置能追上左侧的迭代器。
- 类型为difference_type 的带符号整型数
- 距离指:右侧迭代器向前移动多少位置能追上左侧的迭代器。
- 数组
- 数组与vector的同异
- 相似处
- 都是存放类型相同对象的容器,这些对象没有名字,需要通过其所在位置访问。
- 不同
- 数组的大小确定不变,不能随意向数组中增加元素
- 相似处
- 数组是一种复合类型
- 默认情况下,数组的元素被默认初始化
- 定义数组时,必须指定数组类型
- 数组初始化
- 不允许使用auto关键字由初始值的列表推断类型
- 数组在声明时没有指明维度,编译器会根据初始值的数量计算并推断出
- 如果指明了维度,则初始值的总数量不应该超出指定的大小。
- 如果维度比提供的初始值数量大时,提供的初始值会初始化数组靠前的元素,剩下的元素被初始化成默认值。
- 不允许拷贝和赋值
- 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
- 有些编译器支持数组赋值(编译器扩展),但不建议使用非标准特性
- 复杂的数组声明
int *ptrs[10]; //含有10个整型指针的数组
int &refs[10] = /* ? */; // 错误,不存在引用的数组
int (*Parray)[10] = &arr; Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; arrRef引用一个含有10个整数的数组
- 默认情况下,类型修饰符从右向左依次绑定。
- 在使用数组下标的时候,通常将其定义为size_t类型
- size_t是一种机器相关的无符号类型
- 检测下标的值
- 这个开发时经常遇到。特别注意vector下标越界问题。因为vector使用动态数组,有可能实际分配的内存大于我们定义的大小。因此,越界时,越界下标有可能访问到有效值。从而难以定位程序出现的问题。
- 数组与vector的同异
- 指针和数组
- 在大多数表达式中,使用数组类型的对象其实是使用一个指向数组首元素的指针
- 内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。
int k = p[-2];
索引值为负数的时候,实际上获取数组0号位的值
- 对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效
- 使用数组初始化vector对象
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec (begin(int_arr), end(int_arr));
- C++程序应当尽量使用vector和迭代器, 避免使用内置数组和指针;尽量使用string,避免使用C风格的基于数组的字符串。
- 多维数组
- C++语言中没有多维数组,通常多维数组其实是数组的数组
- 数组初始化
int ia[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};
int ia[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
int ix[3][4] = {{0}, {4}, {8}} //显式初始化每行首元素
int ix[3][4] = {0, 3, 6, 9} //显式初始化第一行, 其他默认初始化
- 范围for语句处理多维数组, 除了最内层的循环外, 其他所有循环的控制变量都应该是引用类型
- 多维数组的指针
int ia[3][4];
int (*p)[4] = ia; //p指向含有4个整数的数组
p = &ia[2]; //p指向ia的尾元素
int *ip[4]; //整型指针的数组
int (*p)[4]; //指向含有4个整数的数组
第四章 表达式
- C++表达式要不然是右值,要不然是左值
- 左值可以位于赋值语句的左侧, 右值则不能
- 一个左值表达式的求值结果是一个对象或者一个函数
- 以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象
- 当一个对象被用作右值的时候,用的是对象的值(内容)
- 当对象被用作左值的时候,用的是对象的身份(内存中的位置)
- 用到左值的运算符
- 赋值运算符需要一个(非常量)左值作为左侧运算对象,结果仍然是一个左值。
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
- 内置解引用运算符、下标运算符
- 迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值,
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值
- 关系运算符都满足左结合律
- 赋值运算符满足右结合律
- 条件运算符满足右结合律
- 复合运算符(+=)和普通运算符(+)求值次数
a+=1;
复合运算符只求值一次a = a+1;
普通运算符求值两次- 一次作为右边子表达式的一部分求值,另一次作为赋值运算符的左侧运算对象求值。
- 解引用运算符优先级低于点运算符
- 条件运算的嵌套层数最好低于2到3层
- 位运算符
- ~ 位求反
- 1置为0,0置为1
- << 左移
- 在右侧插入值为0的二进制
-
>
>
>>
>> 右移
- 有符号位
- 在左侧插入符号位副本,或值位0的二进制
- 无符号位
- 在左侧插入0的二进制
- 有符号位
- & 位与
- 两个运算对象对应位置都为1侧该位是1, 其他情况为0
- ^ 位异或
- 两个运算对象的对应位置有且只有一个为1,运算结果中该位置为1,否则为0
- | 位或
- 两个运算对象的对应位置有一个为1,运算结果中该位置为1,否则为0
- 建议仅将位运算符用于处理无符号类型
- 左移和右移,移除边界外的位会被舍弃掉
- 位运算符优先级低于算术运算符,高于关系、赋值、条件运算符
- ~ 位求反
- sizeof 运算符
- 返回一条表达式或一个类型名字所占的字节数
- 满足右结合律
- 返回值是size_t
- sizeof 结果依赖于其作用的类型
- char 结果 1
- 引用类型 结果为被引用对象所占空间大小
- 指针类型 结果为指针本身所占空间的大小
- 对解引用指针 结果指针指向的对象所占空间的大小,指针不需有效
- 数组 结果为整个数组所占空间的大小,等价于对数组中所有元素进行sizeof再求和
- string或vector 结果为该类型固定部分的大小,不会计算对象中元素占用的空间
- 逗号运算符
- 先对左侧的表达式求值,然后将求值结果丢弃。逗号运算符真正的结果是右侧表达式的值。如果右侧运算符对象是左值。那么最终的求值结果也是左值。
++ix, --cnt;
- 先对左侧的表达式求值,然后将求值结果丢弃。逗号运算符真正的结果是右侧表达式的值。如果右侧运算符对象是左值。那么最终的求值结果也是左值。
- 显示转换
- static_cast
- 任何具有明确定义的类型转换,只要不包含底层const,都可以使用
- const_cast
- 只能改变运算对象的底层const
const char *pc;
char *p = const_cast<char*>(pc)
- 称去掉const性质
- 只能改变运算对象的底层const
- reinterpret_cast
- 通常为运算对象的位模式提供较底层的重新解释
int *ip;
char *pc = reinterpret_cast<char*>(ip)
- 谨慎使用,因为该转换将类型改变了
- 通常为运算对象的位模式提供较底层的重新解释
- static_cast
第五章 语句
- 标准异常
- 报告标准库遇到的问题
- 四个头文件
- exception
- 最通用异常,只报告异常发生,不提供任何额外信息
- stdexcept
- 定义常用的异常类型
- exception 最常见的问题
- runtime_error 只有在运行时才能检测出的问题
- range_error 运行时错误:生成的结果超出了阈值范围
- overflow_error 运行时错误:计算上溢
- underflow_error 运行时错误:计算下溢
- logic_error 程序逻辑错误
- domain_error 逻辑错误:参数对应的结果值不存在
- invalid_argument 逻辑错误:无效参数
- length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
- out_of_range 逻辑错误:使用一个超出有效范围的值
- 定义常用的异常类型
- exception
第六章 函数
- C++语言中,运行我们定义若干具有相同名字的函数,前提是不同函数的形参列表应该有明显的区别。
- 注意顶层const会被忽略掉,形参
const int i
和int i
被认定形参列表相同
- 注意顶层const会被忽略掉,形参
- 数组形参
- 三个等价的形参表现形式,都自动转换为指向数组首元素的指针
- const int*
- const int[]
- const int[10]
- 管理指针的三种常用技术
- 使用标记指定数组长度
- 在数组的最后一个字符后面跟一个空字符,判断是否停止
- 使用标准库规范
- 传递指向数组首元素和尾元素的指针
- 显示传递一个表示数组大小的形参
- 使用标记指定数组长度
- 三个等价的形参表现形式,都自动转换为指向数组首元素的指针
- initializer_list 形参
- 如果函数的实参数量未知,但全部实参的类型都相同即可使用
- 用法于vector相同
- 省略号形参
- 省略号形参只能出现在列表最后一个位置
void foo(parm_list, ...)
void foo(...)
- 省略号形参只能出现在列表最后一个位置
- 返回值类型
- C++11 新标准规定,函数可以返回花括号包围的值的列表
- 数组不能被拷贝,所有函数不能返回数组
- 声明一个返回数组指针的函数
int (*func(int i))[10];
func(int i)
表示调用func函数时需要一个int类型的实参(*func(int i))
表示我们可以对函数调用的结果执行解引用操作(*func(int i))[10]
表示解引用func的调用将得到一个大小是10的数组int (*func(int i))[10]
表示数组中的元素是int类型
- 尾置返回类型
- 在形参列表后面以一个->符号开头
auto func(int i) -> int(*) [10]
- func接受一个int类型实参,返回一个指针,该指针指向有10个整数的数组
- 在形参列表后面以一个->符号开头
-
使用decltype
- 知道函数返回的指针指向哪个数组
int odd[] = {1, 2, 3}
decltype(odd) *arrPtr(int i) { return &odd; }
- arrPtr 使用关键字decltype表示它的返回类型指针
- decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是一个数组
- 返回指针必须在函数声明时加一个*符号
- 知道函数返回的指针指向哪个数组
- 重载与作用域
- C++中,名字查找发生在类型检测之前
- 特殊用途语言特性
- 默认实参
- 某一形参被赋予默认值后,它后面的所有形参必须有默认值
- 局部变量不能作为默认实参
- 只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
- 内联函数
- 大多数机器上,一次函数调用其实包含着一系列工作
- 调用前要先保存寄存器
- 并在返回时恢复
- 可能需要拷贝实参
- 程序转向一个新的位置继续执行
- 内联函数可避免函数调用的开销
- 可以将该内联函数的每个调用点上“内联地”展开
cout << shorterString(s1, s2)<<endl;
cout << (si.size() < s2.size() ? s1 : s2) << endl
展开成- 声明内联函数只需在返回类型前面加上关键字inline
- 可以将该内联函数的每个调用点上“内联地”展开
- 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
- 内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数。
- 大多数机器上,一次函数调用其实包含着一系列工作
- 默认实参
- constexpr 函数
- 能用于常量表达式的函数
- 约定
- 函数的返回类型及所有形参的类型都得是字面值类型
- 函数体中必须有且只有一条return语句
- 被隐式地指定为内联函数
- 约定
- 把内联函数和constexpr函数放在头文件内
- 能用于常量表达式的函数
- 调试帮助
- 结合assert 和 NDEBUG
- assert 预处理宏
- 是预处理变量
assert(expr);
- 首先对expr求值
- 如果表达式为假,assert输出信息并终止程序的执行
- NDEBUG预处理变量
- 如果定义了NDEBUG,assert什么都不做
__func__
存放函数名字__FILE__
存放文件名的字符串字面值__LINE__
存放当前行号的整型字面值__TIME__
存放文件编译时间的字符串字面值__DATE__
存放文件编译日期的字符串字面值
- 是预处理变量
- 函数匹配
- 确定候选函数和可行函数
- 函数匹配的第一步是选定本次调用对应的重载函数集合(候选函数)
- 特征
- 被调用的函数同名
- 声明调用点可见
- 特征
- 第二步考察本次调用提供的实参。从候选函数中选出能被这组实参调用的函数(可行函数)
- 特征
- 形参数量与本次调用提供的实参数量相等
- 每个实参的类型与对应的形参类型相同
- 能转换成形参的类型
- 特征
- 如果函数含有默认实参,则在调用该函数时传入的实参数量可能少于它实际使用的实参数量
- 如果没有找到可行函数,编译器将报告无匹配函数的错误
- 函数匹配的第一步是选定本次调用对应的重载函数集合(候选函数)
- 确定候选函数和可行函数
- 寻找最佳匹配
- 实参类型与形参类型越接近,他们匹配的越好
- 含有多个形参的函数匹配
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配优于其他可行函数提供的匹配
- 编译器拒绝请求
- 每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断优劣
- 调用重载函数时尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理
- 实参类型转换
- 精确匹配
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换
- 通过类类型转换实现的匹配
- 精确匹配
- 函数指针
- 指向某特定类型
- 函数的类型由它的返回类型和形参类型共同决定,于函数名无关
bool lengthCompare(const string&, const string&);
- 该函数类型为
bool(const string&, const string&)
- 声明指向该函数的指针只需用指针替换函数名即可
bool (*pf)(const string&, const string&); //未初始化
- *pf两端的括号必不可少,如果不写括号。则pf是一个返回值为bool指针的函数
- 指向不同函数类型的指针间不存在转换规则
- 当使用重载函数的指针时,上下文必须清晰地界定到底应该选用哪个函数
- 可以直接把函数作为实参使用,此时它会自动转换成指针
useBigger(s1, s2, lengthCompare);
- 等价函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;
- 等价指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;
- decltype 返回函数类型,所以只有在结果前面加上*才能得到指针。
- 返回指向函数的指针
- 虽然不能返回一个函数,但是能返回指向函数类型的指针。
- 必须把返回类型写成指针形式
- 编译器不会自动将函数返回类型当成对应的指针类型处理
- 声明一个返回函数指针的函数
- 使用类型别名
using F = int(int*, int);
//F时函数类型,不是指针using PF = int(*)(int*, int);
//PF是指针类型
- 必须显式地将返回类型指定为指针
PF f1(int);
正确F fi(int);
错误F *fi(int)
正确- 等同于上面函数,直接声明f1
int (*f1(int))(int*, int);
- 按照由内向外的顺序阅读这条声明语句
- f1 有形参列表,则为一个函数
- f1 前面有* 则f1返回指针
- 指针的类型本身也包含形参列表
- 因此为指针指向函数
- 该函数的返回类型是int
- 按照由内向外的顺序阅读这条声明语句
- 尾置返回类型方式
auto f1(int) -> int (*)(int*, int);
- 使用类型别名