# 前言
本篇笔记主要包含原书第一部分,第一章~第七章,从变量定义到类结束。
这次仔细从头开始看,包括最基础的语法都给重新过了一遍,虽然可能看得稍微有点快,而且做不到看完就熟稔了,不过至少重点记录了笔记,后续翻看一下,总能回忆不少印象。
说实话的话,补完这几章,再去看 UE 的 C++ 代码的时候,看起来就感觉更轻松些了 —— 毕竟理论知识确实是基础,自然比全然不知道的情况下去总结经验来得强。
这本书还是有点厚,不知道这次国庆节抽出的时间看不看得完。
- 像编译器一样来思考和理解 C++
- 本书主要为 C++11 标准
# 第一章 开始
错误的注释比完全没有注释更糟糕。
点运算符只能用于类类型对象。
# 第二章 变量和基本类型
为赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据类型,类型决定数据所占比特数及如何解释这些比特内容。
整型和浮点型具有字面值
- 整型字面值的具体数据类型由它的值和符号决定 (int、long、long long 尺寸最小的一个)
- 严格来说,十进制字面值不会是负数,『-42』这种字面值的负号不在字面值之内,作用是对字面值取负值
- 浮点型的字面值是一个 double
- 类型 short 没有对应的字面值
字符串字面值
- 是由常量字符构成的数组,字符串结尾会被编译器添加一个空字符 (\0),因此字符串字面值实际长度比内容多一个。
指针字面值
- nullptr
指定字面值类型
- 通过添加值的前缀或后缀,可指定字面值的默认类型
- 如 3.14F 表示 float
# 变量
通常情况下,对象指一块能够存储数据并具有某种类型的内存空间。
# 初始值 (值初始化)
当对象在创建时获得了一个特定的值
注:(虽然都是等号) 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义则是把对象当前值擦除,而以一个新值替代
# 列表初始化
- 可使用花括号初始值,称为列表初始化
- 注:对结构的数据成员进行初始化时,必须使用列表初始化进行
# 默认初始化
- 若定义变量时没有指定初值,则变量被默认初始化
- 此时变量被赋予 "默认值"
- 内置类型定义于函数体内时,将不被初始化,未初始化内置类型变量值属于未定义,访问将引发错误。(定义于函数体外则被默认初始化为 0)
注:每个类各自决定其初始化对象的方式,是否允许不经初始化就定义对象也由类自己决定。多数类都支持无需显式初始化而定义对象。
# 变量声明和定义
为支持分离式编译,C++ 将声明 (declaration) 和定义 (definition) 区分开类。
- 声明规定了变量类型和名字
- 定义除声明功能外,还申请存储空间,或为变量赋一个初始值
例如
- 仅声明:extern int i;
- 声明并定义:int j;
- 注:包含显式初始化的声明即成为定义,如 extern int x=12 表示的是定义 (extern 声明相当于无效)
注 1:变量可以被多次声明,但只能被定义一次
注 2:C++ 是一种静态类型语言,编译器会在编译阶段检查类型,因此要求我们在使用某个变量之前必须声明其类型
# 作用域
当第一次使用变量时再定义它
全局作用域
- :: 变量名
- 如果函数有可能用到某全局变量,则不宜再定义一个同名局部变量 (此处记录表示可以有这种操作)
# 复合类型
# 引用
引用为对象起了另外一个名字,引用类型引用另外一种类型。
- 通过声明符 & 定义引用类型
- 定义引用时,程序把引用和它的初始值绑定在一起
- 对引用的所有操作都是在与之绑定的对象上进行
- 引用必须被初始化,并且无法再重新绑定给另外对象
- (引用本身不是一个对象,因此不能定义引用的引用)
# 指针
指针是指向另外一种类型的复合类型,与引用类似,指针也实现了对其它对象的间接访问。
- 指针本身是一个对象,允许对指针赋值和拷贝
- 指针允许无需在定义时赋初值
- (与其它内置类型一样,块作用域定义的指针若未初始化则具有不确定值)
解引用符 (*) 可用于访问指针指向的对象 (仅适用于确实指向了某个对象的有效指针)
空指针
- 赋值 nullptr (推荐)
- 赋值 0
- 赋值 NULL (cstdlib 预处理变量,值实际为 0)
void* 指针
- 特殊的指针类型,可用于存放任意对象的地址
- 不能直接操作 void* 指针所指的对象 (因为不确定类型)
- 即 void* 仅代表内存空间,无法访问内存空间中所存储的对象
注:面对一条比较复杂的指针或引用声明时,从右往左阅读有助于弄清楚它的真实含义。
# const 限定符
const 对象一旦创建后其值就不能改变因此 const 对象必须初始化。
默认状态下,const 对象仅在文件内有效
- 编译器将在编译过程中把用到该变量的地方都替换成对应的值
- 添加 extern 关键字可以改变默认行为,使其全局 (多个文件之间共享) 生效
# const 的引用
- 可以把引用绑定到 const 对象上,称为对常量的引用,对常量的引用不能修改它所绑定的对象
- const int &r1=xxx
- 注:常量引用仅对引用可参与的操作做出限定,对于引用本身是否是一个常量未做限定
# 指针和 const
- 指针添加 const 称为 指向常量的指针
- const double *ptr=&xxx
- 指向常量的指针不能指向非常量地址,也不能修改指向对象的值 (也可以指向非常量对象,相当于多了不能修改的限制)
# const 指针
- 常量指针必须初始化,而且一旦初始化完成,其值 (指向的地址) 就不能再改变
- 把 * 放在 const 之前说明指针是一个常量,不变的是指针本身的值而非指向地址的值
- int *const ptr=&xxx
# constexpr 和常量表达式
常量表达式
- 指值不会改变并且在编译过程就能得到计算结果的表达式
# constexpr 变量
- 在一个复杂系统中很难分辨一个初始值到底是不是常量表达式
- 因此 C++11 规定,允许将变量声明为 constexpr 类型以便又编译器来验证变量的值是否是一个常量表达式
# constexpr 函数
- 这种函数应该足够简单以使得编译时就可以计算其结果
- 这样就能用 constexpr 函数去初始化 constexpr 变量了
注:常量表达式的值需要在编译时就得到计算,因此对声明 constexpr 的类型必须有所限制 —— 称为字面值类型
指针和 constexpr
- constexpr 仅对指针有效,与指针所指对象无关
- 即:constexpr 定义的一定是 常量指针 (指针本身不可变)
# 处理类型
# 类型别名
- typedef a b
- using b=a
# auto 类型说明符
- auto 让编译器通过初始值推算变量的类型 (因此定义必须有初始值)
# decltype 类型指示符
- 选择并返回操作数的数据类型
- 编译器分析表达式并得到它的类型,却不实际计算表达式的值
- 如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型
- decltype (func ()) sum=x; //sum 的类型即 func 函数返回类型
注 1:decltype 与 auto 的区别是 decltype 的结果类型与表达式形式密切相关。
注 2:decltype ((variable))(注意是双括号) 的结果永远是引用,而 decltype (variable) 结果只有当 variable 本身就是一个引用时才是引用。
# 自定义数据结构
可使用 struct 定义自己的数据类型
确保头文件多次包含仍能安全工作的常用技术是雨处理器。
头文件保护符
- #define
- #ifdef
- #ifndef
- #endif
# 第三章 字符串、向量和数组
# 命名空间的 using 声明
使用 using namespace::name 后,可直接访问 name
- using std::cin;
- cin>>x; // 正确
注 1:每个 using 声明引入命名空间中的 一个 成员
注 2:头文件不应包含 using 声明 (可以做,但可能导致名字冲突)
# 直接初始化和拷贝初始化
- 拷贝初始化:使用等号初始化 (只能初始化一个变量)
- 直接初始化:不使用等号 (可初始化具有多个初始值的对象)
# string
初始化方式
- string s1
- string s2(s1)
- string s3=s1
- string s3="value"
- string s4(n,'c')
注 1:字符串字面值与 string 是不同类型 (因为历史及与 C 兼容等原因)
注 2:cctype 头文件中提供了一系列判断字符操作,例如判断是否是字母、数字、大写字母 等。
处理每个字符
- 使用基于范围的 for 语句
- 如果想改变,则定义循环变量为引用类型
- 也可以通过索引访问,不过需要特别注意不要越界
# vector
- 允许使用列表初始化
- 允许提供一个元素数量,默认初始化 指定数量的元素
- 允许索引访问元素,但不能用下标形式添加元素
操作
- push_back:添加到末尾
# 迭代器
begin ():指向第一个元素
end ():指向尾元素的下一个位置 (尾后迭代器)
注 1:容器为空时,均为尾后迭代器
注 2:C++11 引入了常量迭代器 - cbegin ()、cend ()
其它
- *iter:返回迭代器 iter 所指元素的引用
- iter->mem:解引用 iter 并获取名为 mem 的成员 (等价 (*iter).mem)
- ++iter:令 iter 指示容器中的下一个元素
- --iter:令迭代器指示容器中的上一个元素
- 注:迭代器运算也支持增减具体数字,使其移动若干个位置
- iter1==iter2:判断两个迭代器是否相等 (指示同一元素或是同一个容器的尾后迭代器)
- iter1<iter2:判断前者是否比后者指示位置小
- iter1-iter2:获得两个迭代器的距离 (右侧迭代器向前移动多少位置可以追上左侧迭代器,可能为负)
箭头运算符 (->):把解引用和成员访问两个操作结合在一起。
- 成员访问即。操作
# 数组
- 数组的维度必须是一个常量表达式
- 注:据说是因为数组作为 C++ 的内置数据类型,其空间分配在栈内存中,这部分空间的大小在编译时就要确定 (有的编译器支持了,但都不建议,因为容易爆栈)
- 动态大小可以使用 new 运算符分配到堆上
- 默认情况下数组元素被默认初始化
- 注:与内置类型一样,函数内部定义了某种内置类型数组,默认初始化会令数组含有未定义的值
注:与 vector 一样,数组元素应为对象,因此不存在引用的数组。
# 显式初始化数组元素
- 可对数组元素进行列表初始化 (此时允许忽略数组维度,若未指定维度则编译器自动推测)
字符数组特殊性
- 字符数组可以用字符串字面值初始化,此时会额外多一个空字符 (\0)
注:不允许对数组进行拷贝给其它数组和赋值给其它数组
- 但是运行使用数组初始化 vector 对象,指明拷贝的首元素地址和尾后地址即可
- vector
ivec(begin(array),end(array))
注:(重要!) 在大多数表达式中,使用数组类型对象其实是使用一个指向该数组首元素的指针
- 例如当使用数组作为一个 auto 变量初始值时,推断得到的类型是指针而非数组
- 指向数组元素的指针拥有更多功能,迭代器支持的运算数组的指针均支持
- 尾后指针:指向数组尾元素之后那个不存在的元素的地址,利用尾后指针可以像尾后迭代器一样判断
标准库函数 begin、end:
- 使用指针运算容易出错
- C++11 提供了两个函数,分别返回数组首元素指针和尾后指针
- begin(array)、end(array)
关系运算符
- 若两个指针指向同一个数组的元素,则还可以使用关系运算符进行比较
# 多维数组
严格来说,C++ 语言中没有多维数组,通常所说的多维数组是数组的数组。
循环遍历多维数组时,需将外层类型定义为引用才能正确遍历下层数组
- 如 for (auto &row:array){
- for(auto col:row)
- } 才是合法的
指针
- 由多维数组名转换得来的指针是指向第一个内层数组的指针
# 第四章 表达式
左值和右值
- 左值:指求值结果为对象或函数的表达式,一个表示对象的非常量左值可以作为赋值运算符左侧运算对象。
- 右值:指一种表达式,其结果是值而非值所在的位置
表达式语句
- C++ 中最低级别的计算,每个表达式都有对应的求值结果
不为 0 的数值转 bool 都是 true
C++11 新标准规定商一律向 0 取整 (直接舍弃小数部分)
进行比较时,除非比较对象是布尔类型,否则不要使用布尔字面值 true 或 false 作为运算对象。
# 赋值运算符
# 复合赋值运算符
- 使用复合赋值运算符只求值一次,使用普通运算符则求值两次 (一次运算,一次赋值)
# 递增和递减运算符
- 前置版本将对象本身作为左值返回 (值加 1 后直接返回改变了的运算对象)
- 后置版本则将对象原始值的副本作为右值返回
- 注:允许在一条语句中混用解引用和递增运算符
# 成员访问运算符
- 点运算符和箭头运算符均可用于访问成员
- ptr->mem 等价于 (*ptr).mem
# sizeof 运算符
- 返回一条表达式或一个类型名字所占用的字节数
- 注:表达式类型返回的是其结果类型大小,并不实际计算其运算对象的值
运算结果
- 引用类型:得到被引用对象所占空间的大小
- 指针:得到指针本身所占空间大小
- 解引用指针:得到指针指向的对象所占空间的大小 (指针可以不需要有效)
- 数组:得到整个数组所占空间的大小,等同于对数组中所有元素各执行一次 sizeof 运算并求和 (注:sizeof 不会将数组转为指针来处理)
- 因此可以以此除以单个元素大小得到数组中元素个数
- 对 string 或 vector:返回该类型固定部分大小,不会计算对象中的元素占用了多少空间
注:sizeof 返回的是一个常量表达式
# 类型转换
# 隐式转换
- 低于整型类型可能会提升为整型
- 计算中,两个不同类型数值计算,宽度较小类型会提升为更大精度类型
- 在条件中,非布尔值转换成布尔类型
- 数组转换成指针:数组自动转换成指向数组首元素的指针
- 当数组被用作 decltype、取地址符、sizeof、typeid 等运算符的运算对象时转换不会发生
指针的转换
- 常量整数值 0 或字面值 nullptr 能转换成任意指针类型
- 指向任意非常量的指针能转换成 void*
- 指向任意对象的指针能转换成 const void*
# 显式转换
注:避免强制类型转换
# 旧式强制类型转换
- type (expr)
- (type)expr
- 注:旧式强制类型转换分别具有与 const_cast、static_cast 或 reinterpret_cast 相似的行为。
# static_cast
- 任何具有明确定义的类型转换,只要不包含底层 const 就可以使用
- T t=static_cast
(val) - 还可以使用这个将存放于 void* 的指针转换回来
- T t=static_cast
# const_cast
- 可以将常量对象转换为非常量对象:即去掉 const 性质
- 转换后编译器就不会阻止我们对该对象进行写操作
- 注:如果对象本身不是常量,获得写操作是合法的。但若本身是一个常量,写操作可能产生未定义行为
# reinterpret_cast
- 通常为运算对象的位模式提供较低层次上的重新解释
# 第五章 语句
空语句:『;』
空块:『{}』
异常处理
- throw 表达式
- try 语句块
- 一套异常类 (异常类型具有 what 成员函数,返回 C 风格字符串,无初始值异常类型返回内容由编译器决定)
# 第六章 函数
- 形参和实参
- 函数返回类型不能是数组或函数类型,不过可以是指向数组或函数的指针
局部静态对象
- 标记为 static 的局部变量
- 在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止才销毁
- 生命周期不受函数本身控制
- 注:若未有显式初始值,则执行值初始化
参数传递
- 引用传递 (传引用调用):形参是引用类型时
- 也可以利用这个返回额外信息
- 值传递:实参的值被拷贝给形参时 (两个对象独立)
- 注:指针形参与其它非引用类型一样,也属于拷贝的指针的值
注:若函数无需改变引用形参的值,最好将其声明为常量引用。
- 常量引用可接受范围更大 (包括常量和非常量对象),而且可以起到标记函数不会修改传入对象的功能
数组作为参数
- 一般需要传递长度相关信息
- 或使用数组引用形参
- func(int (&array)[10])
main 的命令行选项
- main 函数可选具有形参的实现,使用户可从命令行传递参数
# 可变形参的函数
当无法预知应该向函数传递几个实参时
- 如果所有实参类型相同:initializer_list (标准库类型),用于表示某种特定类型的值的数组
- 实参类型不同:可变参数模板
- 注:还有一种特殊形参类型 (省略符)
initializer_list
- initializer_list
lst - lst.size()
- lst.begin ():首元素指针
- lst.end ():尾后指针
- 注:其中的元素永远是常量值
省略符形参 (...)
- 为了便于 C++ 程序访问某些特殊 C 代码设计
- 只能出现在形参列表的最后一个位置
- 通常,省略符形参不应用于其它目的
# 不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也会随之释放。
- 函数终止意味着局部变量的引用将指向无效内存区域
# 返回值
- 调用一个返回引用的函数得到左值
- 其它返回类型得到右值
列表初始化返回值
- C++11 标准规定:函数可以返回花括号包围的值的列表
返回数组指针
- 尾置返回类型:尾置返回类型跟在形参列表后并以一个 -> 符号开头
- auto func (int i)->int (*)[10] //func 接受一个 int 类型实参,返回一个指向含有 10 个整数的指针
- 注:若知道返回指针指向的数组,可使用 decltype 表示返回类型
# 函数重载
- 如果形参是某种类型的指针或引用,可通过区分指向的是常量对象或非常量对象实现函数重载
- 其它 const 形参与普通形参的差别则不能构成重载
const_cast 与重载
- 将非常量对象转换为常量实参,以做到调用常量形参的重载函数 (如果同时存在非常量版本,而想调用常量版本时)
# 默认参数
- 局部变量不能作为默认实参
- 此外只要表达式的类型能转换成形参所需类型,该表达式就能作为默认实参:同名局部变量虽然会隐藏外层对象,但不会影响默认实参
# 内联函数与 constexpr 函数
- 函数前加上 inline (内联说明只是向编译器发出请求,编译器可以选择忽略)
- constexpr 函数返回类型及所有形参类型都得是字面值类型
注 1:constexpr 函数被隐式指定为内联函数,编译器会在编译过程中展开
注 2:constexpr 函数调用时,传递常量表达式则返回常量表达式。传递非常量表达式则返回的也会是非常量表达式。即 constexpr 不一定返回常量表达式。
# 调试帮助
assert 预处理宏
- assert(expr)
- 对 expr 求值,若表达式为 false,assert 输出信息并终止程序执行。否则什么也不做。
NDEBUG 预处理变量
- assert 的行为依赖 NDEBUG 的预处理变量的状态
- 若定义了 NDEBUG 则 assert 什么也不做 (默认状态下未定义,assert 将执行运行时检查)
__func__
- 编译器定义的一个局部静态变量,用于存放函数的名字
__FILE__
- 存放文件名的字符串字面值
__LINE__
- 存放当前行号的整型字面值
__TIME__
- 存放文件编译时间的字符串字面值
__DATA__
- 存放文件编译日期的字符串字面值
# 函数指针
要想声明一个可以指向函数的指针,只需用指针替换函数名即可
- bool (*pf)(xxxxx) // 注:*pf 的括号必不可少,否则就是一个返回值为 bool 指针的函数了
- 当我们把函数名作为一个值使用时,该函数自动地转换成指针
- 此外可以直接使用指向函数的指针调用该函数,无需提前解引用指针
函数指针形参
- 与数组类似,不能直接定义函数类型的形参
- 但是形参可以是指向函数的指针
- void func(bool (*pf)(xxxxx))
- 如果明确知道返回函数是哪一个,可使用类型别名或 decltype 简化
- typedef decltype(myfunc) *callbackName
- 可以直接把函数作为实参使用
# 第七章 类
定义在类内部的函数是隐式的 inline 函数。
当我调用成员函数时,实际上是替某个对象调用。成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个堆对象。
- 默认情况下,this 是指向类类型非常量版本的常量指针
- 常量对象以及常量对象的引用或指针都只能调用常量成员函数
- 注:一个 const 成员函数如果以引用方式返回 *this,则返回类型将是常量引用 (不可再做修改)。
# 构造函数
- 构造函数不能声明为 const
- 当我们创建类的一个 const 对象时,直到构造函数完成初始化过程,对象才能真正取得其 常量 属性:因此构造函数在 const 对象构造过程中可以向其写值
注:当对象被默认初始化或值初始化时自动执行默认构造函数。
例如:
- Sales_data obj1 (); // 错误:声明了一个函数而非对象
- Sales_data obj3; // 正确:默认初始化 obj2 对象
# 合成的默认构造函数
只有当类没有声明任何构造函数时编译器才会自动生成
- 如果存在类内初始值,用其初始化成员
- 否则,默认初始化该成员
- 即:通过相应类内初始值初始化或执行默认初始化
- =default:要求编译器生成构造函数
# 构造函数初始值列表
- (构造函数中参数后冒号与花括号之间) 可用于成员初始化
- fun(const std::string &name):Name(name){}
- 例如 const 只能使用这种方式初始值 (而不能在花括号里面赋值)
- 注:该列表只说明用于初始化成员的值,而不限定具体执行顺序,具体顺序与其在类中定义的出现顺序一致。(因此最好保持顺序一致,或避免使用某些成员初始化其它成员)
注 1:当我们提供一个类内初始值时,必须以符号 = 或 花括号表示。
注 2:初始化和赋值的区别事关底层效率问题,前者直接初始化数据成员,后者则先初始化再赋值。(因此建议使用构造函数初始值)
# 委托构造函数
一个委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程。
- 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
# 隐式的类类型转换
使用 explicit 可抑制构造函数定义的隐式转换
- 当以 explicit 声明构造函数时,它将只能以直接初始化的形式使用
- 且编译器不会在自动转换过程中使用该构造函数
# class 与 struct
唯一区别是:两者默认访问权限不一样
- struct:定义在第一个访问修饰符之前的成员是 public
- class:第一个访问修饰符之前的成员是 private
- 一般当定义类所有成员都是 public 时采用 struct
# 可变数据成员
变量声明中加入 mutable 关键字
- 永远不会是 const
- 即使作为 const 对象成员
# 友元
每个类复制控制自己的友元类或友元函数
- 还可以某类成员函数成为自己的友元
# 类的声明
前向声明
- class className;
- 引入类名并指明这是一个类类型
- 在它声明之后定义之前是一个『不完全类型』
不完全类型
- 只能在非常有限的情况下使用:可以定义指向这种类型的指针或引用,也可以声明 (但不能定义) 以不完全类型作为参数或返回类型的函数。
# 类作用域
编译器处理完类中全部声明后才会处理成员函数的定义。
# 聚合类
聚合类使得用户可直接访问其成员,且具有特殊的初始化语法形式。
- 所有成员都是 public 的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,有没有虚函数
可以使用花括号初始化聚合类的数据成员,初始值的顺序必须与声明一致。
# 字面值常量类
数据成员都是字面值类型的聚合类是字面值常量类。
- 注:还有一些满足额外条件的非聚合类也可以是字面值常量类
# 类的静态成员
成员与类本身直接相关,而不是与类的各个对象保持关联。
静态成员函数
- 静态成员函数不包含 this 指针,静态成员函数不能声明为 const
静态数据成员
- 类似于全局变量,静态数据成员定义在任何函数之外,一旦被定义,将存在于程序整个生命周期
- 通常类的静态成员不应在类内部初始化,不过可以为其提供 const 类内初始值
- 注:
- 静态数据成员可以是不完全类型
- 静态数据成员类型可以就是它所属类型 (普通成员只能声明所属类型的指针或引用)
- 可以使用静态成员作为默认实参