C ++ primer 第五版
前言
1、编程语言存在意义:
编程语言通过抽象的方式减少了直接使用机器语言编程的复杂性。程序员可以不必关注计算机硬件的具体细节,而是将注意力集中在如何实现功能逻辑上。
2、封装
标记访问边界,封装的主要目的是隐藏对象的内部细节,只暴露必要的接口给外部使用,从而保护对象不被意外修改,同时也增加了代码的模块化和可维护性。
3、继承、多态
代码复用和灵活。
C++基础
2 变量和基本类型
2.1 基本内置类型
2.1.1 算术类型
-
算术类型分为整型(包括字符和布尔类型)和浮点型。
C++规定了尺寸的最小值, char:1 short int:2 int:4 long int 4; float:4; double:8;long long int 8. bool类型最小尺寸未定义。 -
带符号和无符号类型。
整形可以分为带符号的和无符号的,表示范围不一样。
字符比较特殊,字符类型被分为了三种,char、signed char、unsigned char。 char的类型和编译器有关,有时候是无符号,有时候是有符号。建议使用char的时候指定有符号,无符号。不要用char。
2.1.2 类型转换
有符号数与无符号数转换。
同类型, 有无符号转换规则:在范围内不变,否则平移
不同类型,转换规则:先变长度(同类型平移),然后类型转换。
换算完之后,也能逆算回来
隐式转换
运算中,有无符号同时存在,都会转成无符号再参与运算。
只要有 float 类型,都会转换为double 类型,再参与运算。
2.1.3 字面值常量
每个字面值常量都表示一种数据类型。
转义序列
分为2类,一类是不可打印的字符,如退格,因为没有可视的图符;另一类是有特殊含义的字符(单双引号,问号,反斜号)。用\n表示。
> \\ \ 反斜线
> \" " 双引号
> \' ' 单引号
八进制转义字符
• 格式:\ddd,其中 ddd 是一个 1 到 3 位的八进制数。
• 范围:0 到 255(八进制 000 到 377)。
2. 十六进制转义字符
• 格式:\xHH,其中 HH 是一个两位的十六进制数。
• 范围:00 到 FF(十六进制 00 到 FF)
2.2 变量
变量是存储空间的抽象,变量类型表达了内存大小和操作范围。
2.2.2 变量声明和定义的关系
程序分为多个文件,为了在文件间共享代码,需要支持分离式编译。声明使得名字为程序所知,一个文件如果想要使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。
声明一个变量:
在变量前加extern,并且不要显示初始化他。
2.2.4 名字的作用域
作用域以花括号分割,表示变量的作用范围。最近作用域原则。
2.3 复合类型
复合类型是指基于其他类型定义的类型,本章主要讲引用和指针。
2.3.1 引用
引用即别名,只能绑定变量,不能绑定字面值。
通过将声明符写为&d来定义引用类型。
作用:内存共享。
2.3.2 指针
定义指针类型的方法是将声明符写成*p的形式。
2.3.3 理解复合类型的声明
指向指针的引用。
int * &r = p;
从右往左读,离变量名最近的符号对变量类型有最直接的影响。
2.4 const 限定符
在 C++ 中,const 关键字用于声明常量或指定对象的某些属性为只读.
2.4.1 const的引用
const 引用被称作 对常量的引用。
引用的类型通常必须与其所引用的对象的类型一致。然而,存在两个重要的例外情况,具体如下:
在初始化常量引用时,允许使用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型。字面值也行。
2.4.2 指针和const
const 指针:int * const p; 从右往左读,就近原则。
指向常量的指针: const int *p; 指针不能修改里面的值。
2.4.3 顶层const
顶层const表示指针本身是个常量,底层const表示指针所指向的对象是个常量。
指针常量(Pointer to Constant)
常量指针(Constant Pointer)
2.5 处理类型
随着程序越来越复杂,程序中的变量也越来越复杂。难以“拼写”和阅读时难以弄清类型是什么。
2.5.1 类型别名
使用typedef和using.
typedef double wages;
using SI = Sales_item;
2.5.2 auto类型说明符
auto 让编译器通过初始值来推断变量的类型。
复合类型、常量和auto
1、当引用作为初始值时,真正参与的是引用对象的值。
2、会忽略顶层const但保留底层const.
3、可以结合使用,使用auto时用const和&等。
2.6 自定义数据结构
结构体
1. 对齐字节需要知道的3个概念:自身对齐值、指定对齐值、有效对齐值。
2. 自身对齐值:数据类型本身的对齐值,例如char类型的自身对齐值是1,short类型是2;
指定对齐值:编译器或程序员指定的对齐值,32位单片机的指定对齐值默认是4;#progma pack(n) 指定对齐字节值
有效对齐值:自身对齐值和指定对齐值中较小的那个。
对齐有两个规则:
1、存放成员的起始地址必须是该成员有效对齐值的整数倍。
2、结构体大小是最大对齐要求的整数倍,即结构体的有效对齐值是其最大数据成员的自身对齐值;
3. 共用体(联合体) union 取最大值,所有成员相对于基址地址的偏移量为0。可以用来判断大小端。大端:高字节放在低地址。小端,低字节放在低地址。高数据截断类似。网络是大端结构,大多数主机是小端结构。
4. 枚举 缺省值为0,1,2…;若赋值,自动加1
赋值时,需要赋枚举里面的值,不能直接1,2
3 字符串、向量和数组
3.1 命名空间的using声明
通过using指令,编译器可以识别并引入指定的命名空间,使得程序员可以直接使用该命名空间中的类型,而无需写出完整的命名空间路径。
头文件不应该using声明,可能会产生难以预料的名字冲突。
3.2 标准库类型 string
std::string (也就是C++中的string):标准并没有规定字符串必须以\0字符结尾。编译器在实现时既可以在结尾加上\0,也可以不加,这个是由编译器决定的,但是当通过c_str()或者data()转换得到的const char *时,会发现最后一个字符一定是\0。
因为string是一个类,它的长度信息已经封装到类的私有变量里面了。
3.2.1 定义和初始化string对象
直接初始化和拷贝初始化
直接初始化是使用圆括号()来进行的初始化。它的特点是尽可能使用构造函数来直接构造对象。
拷贝初始化是使用等号=来进行的初始化。它的特点是首先使用默认构造函数(如果可能)创建一个临时对象,然后使用赋值运算符=将该临时对象的值赋给目标对象。如果类型支持,这个过程中可能会调用拷贝构造函数或移动构造函数。
区分主要是做性能区分。
3.2.3 处理string对象中的字符
处理每个字符,基于范围使用的for语句
for (auto c : string)
截取字符串
.substr(起始位置,要截取的长度);
字符串和数字的相互转换
数字转换为字符串:std::to_string(数字);
字符串转换为数字:std::stoi(字符串);
增加:
append;
+=;
insert
删除:
erase
clear
改:
replace
查找:
find
3.3 标准库类型vector
vector存放的是某种给定类型对象的可变长序列。
3.3.1 定义和初始化vector对象
- 列表初始化 {}
- 值初始化 () vector vec(10, -1); 生成10个-1的元素,如果没写-1,则为0. string则默认初始化。 二维数组:vector<vector> vec(m, vector(n,0));
m*n的二维数组,所有元素都为0
插入
- vec.insert(vec.begin()+i,a);在下标为i的元素前面插入a;
- 插入时写emplace_back,而不是push_back;
如果需要构造对象(传入构造参数)时,push_back会先构造,然后移动/拷贝到容器里;而emplace_back直接在容器里构造。
容量
1.vec.size() 当前容器所存储的元素个数
2.vec.capacity() 容器在分配新的存储空间之前能存储的元素总数
3. resize()操作:创建指定数量的的元素并指定vector的存储空间; reserve()操作:保留,capacity;
区别:
- 当n大于当前元素个数,resize和reserve都会capacity。根据分配策略,可能会有更大的一块。resize未指定初始化参数,按类型默认初始化,添加元素。而reserve不会添加元素。
- n < 当前元素个数,resize删除多余的,capacity不改变。而reserve什么也不做。
内存扩充策略
满了的时候,成倍扩充,然后拷贝原有数据到新内存,释放原内存。
内存泄漏:
clear会调用对象的析构函数,但不会释放内存。 clear完之后,size不变。
析构函数会执行对象的清理工作,不会释放内存。
Vector 内部数组是放在堆上的。
Vector 释放capacity
1、shrink_to_fit ,是一个请求,具体能否释放看实现
2、a.swap(b); 或std::sawp(a, b); 交换2个容器内容。
查找
find(vec.begin(), vec.end(), i) != vec.end();
删除元素
- vec.erease(vec.begin(), vec.begin()+1); 删除第一个元素 左闭右开
- pop_back() 删除最后一个元素,尽量不要从中间删除。
测试网站:https://cpp.sh/
3.4 迭代器
指针,提供访问容器内元素方法。
- 所有标准库容器都可以使用迭代器,只有少数几种支持下标运算符。
- v.end();表示尾元素的下一位置,当容器为空时,begin==end
- *iterator 返回所指元素的引用
- iterator->mem 等价于 (*iterator).mem
- 迭代器类型 iterator(读写) const_iterator(只读) cbegin返回const_iterator.
- erase删除容器后,返回下一个迭代器。
迭代器使用:可以理解为封装了指针
容器::iterator,如map<char,string>::iterator
迭代器失效
意思发生改变,或野指针。
- 会引起其底层空间改变的操作,都有可能是迭代器失效 ,
比如: resize 、 reserve 、 insert 、 assign 、push_back等。 - 指定位置元素的删除操作 - -erase
erase 删除 pos 位置元素后, pos 位置之后的元素会往前搬移,没有导致底层空间的改变,
理论上讲迭代器不应该会失效,但是:如果pos 刚好是最后一个元素,
删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么pos 就失效了。
因此删除 vector 中任意位置上元素时, vs 就认为该位置迭代器失效了。
指针是否相等,表示什么意思
和变量相等一样, 这样好理解。
3.5 数组
初始化
没有明确时,内置类型是随机值;其他类型是默认初始化。
复杂的数组声明解析,从数组的名字按照从内向外的顺序读,先右后左。 多维数组,指的是数组的数组,按照名字从内到外的顺序阅读。
4. 表达式
表达式由一个或多个运算对象(operand)组成, 对表达式求值将得到一个结果(result)。 字面值和变量是最简单的表达式 (expression), 其结果就是字面值和变量的值。
4.1.1 左值和右值
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的 时候,用的是对象的身份(在内存中的位置)。
在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。
4.1.2 优先级和结合律
优先级(Precedence)
优先级是指运算符在表达式中的优先顺序。优先级高的运算符会优先于优先级低的运算符进行计算。如乘法优先于加法。
结合律(Associativity)
结合律是指当多个相同优先级的运算符出现在一个表达式中时,它们的计算顺序。
• 左结合:从左到右计算。例如,表达式 a - b - c 按照 (a - b) - c 的顺序计算。
• 右结合:从右到左计算。例如,表达式 a = b = c 按照 a = (b = c) 的顺序计算。
括号无视优先级和结合律
右值的时
候,用的是对象的内容;左值的时候,用的是对象的身份。算术运算符满足左结合律,如果优先级相同,按照从左往右的顺序。
括号无视优先级与结合律。
求值顺序
大多数运算符未说明对象按照什么顺序执行
如int a = f1() * f2(); f1和f2的执行顺序未知。
4.2 算术运算符(左结合律)
定义:执行算术运算,加减乘除,正负,取余
运算对象和求值结果都是右值。
除法:2个运算对象符合相同商为正,否则商为负。商直接取整。
取余:余数不为0,符号与m相同。 m%n.
4.3 逻辑和关系运算符
运算对象和返回结果都是右值,结果是bool值。
非(!)是右结合律,其他都是左结合律。
逻辑与和逻辑或 短路求值
逻辑与运算符,当且仅当左侧运算对象为真时才对右侧运算对象求值。
逻辑或运算符,当且仅当左侧运算对象为假时才对右侧运算对象求值。
关系运算符(Relational Operators)
是用于比较两个值的符号。这些运算符返回一个布尔值(true 或 false),表示比较的结果。
4.4 赋值运算符(右结合律)
赋值运算符的左侧运算对象必须是一个可修改的左值,赋值运算的结果是它的左侧运算对象,并且是一个左值。
STL容器模板支持{}初始化,如v = {1, 2, 3};
复合赋值运算符
+= ,效率更高(减少临时变量的创建)
4.5 递增和递减运算符
优点:1、效率更高(减少临时变量的创建)
2、有些迭代器不支持算数运算符,如链表实现的。
前置版本,这种形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。 ++ i
后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本 i ++
使用简洁版本,降低出错可能性。
Cout << *p ++ << endl;
解引用指针: 运算符用于获取指针所指向的内存中的值。*
4.6 成员访问运算符
左值,右值都行,具体需要看上下文。
点运算符和箭头运算符都可用于访问成员,
n =p->size ();等价于 (*p).size ()
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
4.7 条件运算符
cond ? expr1 : expr2;
首先求 cond 的值, 如果条件为真对 expr1 求值并返回该值,否则对expr2求值并返回该值。
作用:简洁,把简单的 if-else 逻辑嵌入到单个表达式当中。
### 4.8 位运算符
左移运算符(<<)在右侧插入值为0的二进制位。
右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位;
如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择 要视具体环境而定。
4.9 sizeof运算符
返回一条表达式或一个类型名字所占的字节数。
sizeof运算符的结果部分地依赖于其作用的类型:
- 对char或者类型为char的表达式执行sizeof运算,结果得1
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
- 对指针执行sizeof运算得到指针本身所占空间的大小。
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
4.10 逗号运算符(左结合性)
先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。
如果右侧运算对象是左值,那么最终的求值结果也是左值
逗号运算符经常被用在for循环当中:
for(vector<int>:: size_type ix=0; ix!= ivec.size(); ++x,-cnt)
4.11 类型转换
强制类型转换
有4种类型。
- static_cast 静态转换
任何具有明确定义的类型转换,只要不包含底层const。
基本数据类型之间的转换。
派生类指针(或引用)到基类指针(或引用)的转换。 引用是一个复合类型
用户自定义类型之间的转换。
-
reinterpret_cast,即重新解释
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
即: reinterpret_cast 不相近类型之间的类型 -
const_cast
最常用的用途就是删除变量的const属性,方便赋值 -
dynamic_cast 动态转换
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
运行时类型检查,判断一个基类对象是否可以转换为子类对象。
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的),用static_cast不安全。
旧式的强制类型转换
type (expr): //函数形式的强制类型转换
(type)expr //C语言风格的强制类型转换
缺点:
缺乏类型安全性、难以辨识和调试等。
5.语句
5.1 简单语句
语句是构成程序的基本单位,用于执行特定的操作或表达特定的意图。每个语句通常以分号(;)结尾,以表示其结束。
空语句
不执行任何操作。为了在语法上需要一条语句但实际上逻辑上不需要执行任何操作的场合下使用。
为增加可读性,加了空语句需要加注释。
if和while循环处不要多写,会改变语义。
复合语句(块)
用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块(block)。
一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。
如果在程序的某个地方,语法上需要一条语句,但是逻辑上需要多条语句,则应该使用复合语句。例如,while或者for的循环体必须是一条语句,但是我们常常需要在循环体内做很多事情,此时就需要将多条语句用花括号括起来,从而把语句序列转变成块。
5.3 条件语句
按照条件执行的语句。
5.3.1 if 语句
格式
if (条件1)
{
// 条件1为真时执行的代码
}
else
{
// 其他情况时执行的代码
}
悬垂else
if分支会多于else分支时,C++规定else与离它最近的尚未匹配的if匹配,从而消除了程序的二义性。
5.3.2 switch语句
提供了一条便利的途径使得我们能够在若干固定选项中做出选择。
格式:
switch (expression)
{
case constant1:
// 代码块 1
break;
case constant2:
// 代码块 2
break;
// 可以有任意多个 case 语句
default:
// 默认代码块
}
expression: 表达式的求值结果可以转换为整型。
case constantN: case 标签必须是整型常量表达式,即编译时能确定的值,浮点数即便可以转换也不行。
Switch内部控制流
如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。
break语句
中断当前的控制流。将控制权转移到switch语句外面。
每个标签都加上break,这样最后加分支时,不用考虑其他因素。
default标签
如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后面的语句。
即使不准备在default标签下做任何工作,定义一个default标签也是有用的。其的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。
标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case标签。如空语句。
switch内部的变量定义
C++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。
5.4 送代语句
选代语句通常称为循环,它重复执行操作直到满足某个条件才停下来。
5.4.1 while语句
格式
while (condition)
{
// 代码块
}
条件为真时一直执行。
5.4.2 传统的for语句
格式
for (初始化表达式; 条件表达式; 迭代表达式)
{
// 循环体:条件为真时重复执行的代码块
}
初始化表达式:在循环开始之前执行,通常用于声明并初始化一个或多个循环控制变量。这个表达式只会在循环开始前执行一次。
条件表达式:在每次迭代之前评估。如果条件为真(非零),则执行循环体;如果为假(零),则跳出循环。
迭代表达式:在每次循环体执行完毕后执行,通常用于更新循环控制变量。
省略for语句头的某些部分
能省略掉ini-stalement、condiion和expression中的任何一个(或者全部)。
5.4.3 范围for语句
C++11新标准引入了一种更简单的for语句,这种语句可以遍历容器或其他序列的所有元素。范围for语句(rangeforstatement)的语法形式是:
for(declaration:expression)
statement
expression表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者vector或string等类型的对象,这些类型的共同特点是拥有能返回选代器的begin和end成员。
declararion定义一个变量,序列中的每个元素都得能转换成该变量的类型。
每次代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行statement。
for(auto &r:V)
等价于
for(auto beg = v.begin),end =v.end();beg = end;++beg)
auto&r=*beg;//必须是引用类型,这样才能对元素执行写操作
不同通过范围for增加,删除vector(容器)元素。
5.4.4 do while语句
格式
do
{
// 循环体:每次迭代都要执行的代码块
} while (条件表达式);
do while语句应该在括号包围起来的条件后面用个分号表示语句结束。
5.5 跳转语句
5.5.1 break语句
负责终止离它最近的while、dowhile、for或switch语句,并从这些语句之后的第一条语句开始继续执行。
跳出循环或Switch条件语句。
5.5.2 continue语句
负责终止最近的循环中的当前迭代并立即开始下一次迭代。
for、while和do while等。
5.6try语句块和异常处理
条件语句
- 多层嵌套,可以提逻辑,减少嵌套层数
- else与离他最近的未匹配的if匹配
switch语句
- switch对表达式求值,然后值转变为整型
- case标签必须是整型常量表达式
- 如果表达式和某个case匹配,直到switch结尾或遇到break结束。接着执行switch之后的语句。
- 一般加default,表示我们考虑到了这个情况。
- switch内部定义的变量,如果没有初始化,其他分支可以用。初始化了其他分支不可以用。
迭代语句
- while语句,定义在条件部分和循环体内的变量,每次迭代都经历创建和销毁的过程。
- 传统for语句。 for (init;condition;expression)
- 范围for语句。
for(declaration: expression).
expression必须是一个序列。 declaration定义一个变量,每次迭代都重新定义变量,并将其初始化序列中的下一个值。
对范围for语句,不能增加vector对象的元素。因为for(auto r : v) 等价于 for(auto beg =
v.begin(), end = v.end(); beg != end; ++beg)。
跳转语句:
- break语句,终止离最近的while for do while switch语句,并执行其之后的第一条语句。
- continue,终止最里面的的循环中的当前迭代,并立即开始下一次迭代。对于传统for,继续执行for语句头的expression;对于范围for,用序列中的下一个元素初始化循环控制变量。
只有switch嵌套在迭代语句里,才能用continue。
6.函数
函数是一个命名了的代码块,通过调用函数会执行对应的代码。函数可以重载,同一个名字可以对应几个不同的函数。
6.1 函数基础
我们通过调用运算符 (call operator)来执行函数。
一对圆括号,作用于函数或者指向函数的指针,圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
调用函数
函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。
return语句也完成两项工作:一是返回return 语句中的值 ,
编译器会将返回值存储在一个临时位置;
二是将控制权从被调函数转移回主调函数。
return执行完成后,会清理局部变量。
形参和实参
一一对应,编译器能以任意可行的顺序对实参求值。
6.1.1 局部对象
作用域和生命周期
名字的作用域是程序文本的一部分,名字在其中可见。 ·
对象的生命周期是程序执行过程中该对象存在的一段时间。
局部变量仅在函数的作用域内可见,同时局部变量还会隐藏 (hide)在外层作用域中同名的其他所有声明中。
局部静态对象
未初始化的 static 局部变量会被自动初始化为零(对于整数类型)或 nullptr(对于指针类型)。这是 C++ 标准规定的行为,确保这些变量在使用前具有确定的值。
6.1.2 函数声明
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
可以无须形参的名字,但考虑可读性,需要加上。
6.1.3 分离式编译
将一个程序(或项目)分解为多个源文件(.cpp文件),每个源文件单独进行编译,生成对应的目标文件(通常是.o文件或.obj文件),然后在链接阶段将这些目标文件以及所需的库文件链接在一起,最终形成一个单一的可执行文件。
编译速度加快。
6.2 参数传递
6.2.1传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。把指针指向其他地方,不影响原来的。
6.2.2 传引用参数
1、避免拷贝
2、返回额外信息
6.2.3 const形参和实参
和其他初始化过程一样, 当用实参初始化形参时会忽略掉顶层 const。
以下2个重复声明。
void func(const int i);
void func(int i);
指针或引用形参与const
可以使用非常量初始化一个底层const对象,但是反过来不行;
尽量使用常量引用
普通引用存在的问题
1、误导调用者,函数内部可以改变其值。
2、限制函数所能接受的实参类型。不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
6.2.4 数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许贝数组以及使用数组时会将其转换成指针。
当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
//尽管形式不同,但这三个print函数是等价的 //每个函数都有一个constint*类型的形参
void print (const int*);
void print(const int[ ]); //可以看出来,函数的意图是作用于一个数组
void print(const int [10]); //这里的维度表示我们期望数组念有多少元素,实际不一定
确保函数内对数组访问不越界
显式传递一个表示数组大小的形参
数组形参和const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
格式如下
void print(int(arr)[10])
只能将函数作用于大小为10的数组。
传递多维数组
实际传递的是指针
格式
void print(int(*matrix)[10],introwSize)
6.3 返回类型和return语句
6.3.1 无返回值函数
没有返回值的return语句只能用在返回类型是void的函数中。
返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。
6.3.2 有返回值函数
return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
编译器的局限性
在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。
返回类类型的函数和调用运算符
调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
auto sz = shorterString(sl2).size();
声明一个返回数组指针的函数
格式
int(*func(int i))[10];
type(*function (parameter list)) n]
return
6.4 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数:
函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。
main函数不能重载。
重载和const形参
顶层const 无法重载。
低层const可以重载,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
const_cast和重载
在4.11.3节(第145页)中我们说过,const_cast在重载函数的情景中最有用。
//比较两个string对象的长度,返回较短的那个引用
const string & shorterString(const string &sl,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
string &shorterString(string &slstring &s2)
{
auto r = shorterstring(const_cast<const string&>(sl)), const_cast<const strings&>(s2));
return const_cast<string&>(r);
}
6.5 特殊用途语言特性
6.5.1 默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。
默认实参声明
在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
//wd、def和ht的声明必须出现在函数之外
SZ Wd = 80;
char def = ' ';
sZ ht();
string screen(sz = ht(), sz = wd, char = def);
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。
void f2()
{
def = '*';
SZ wd = 100; //隐藏了外层定义的d,但是没有改变认值
window = screen() //调用screen(ht(),80,*)
}
6.5.2 内联函数和const expr函数
内联函数可避免函数调用的开销
是一种建议编译器将函数调用直接替换为函数体代码的优化手段。
只是向编译器发出的一个请求,编译器可以选择忽略这个请求
内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。
定义方法:
1、inline关键字可以出现在函数声明或定义中,不需要都加。
2、在类定义中定义成员函数(隐式内联)。
6.5.3 调试帮助
程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准各发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG
assert预处理宏
assert(expression);
调试期间检查程序中的条件是否为真。如果条件为假(即表达式的结果为0),则 assert 会输出一条错误信息并终止程序执行。
NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量:
_ _ func _ 当前函数的名字,const char 数组
C++预处理器定义:
_ _ FILE _ 存放文件名
_ _ LINE_ _ 存放当前行号
_ _ TIME_ _ 存放文件编译时间
_ _ DATE_ _ 存放文件编译日期
6.6 函数匹配(有时间扩充)
如果没有选出一个最优的优于其他的,则编译器报错。二义性等
顺序
1、精确匹配
2、通过const转换实现的匹配
3、通过类型提升实现的匹配
4、算术类型转换
5、类类型转换
6.7 函数指针(有时间扩充)
6.8 函数调用
1.关键寄存器
1.程序计数器
全称:Program Counter (PC) / Instruction Pointer (IP)
PC作用:永远指向下一条待执行指令的内存地址
2.栈指针
全称:Stack Pointer (SP)
特殊功能:自动调整用于栈操作(push时递减,pop时递增)
3.基址指针
全称:Base Pointer (BP) / Frame Pointer (FP)
双重作用:
1.作为栈帧基准点(通过[ebp-4]访问局部变量)
2.用于栈回溯(形成调用链)
2.函数调用过程
1.调用阶段
CPU 要执行 call add 前,需要做两件事:
步骤1.参数入栈(从右到左)
C/C++ 的 cdecl 调用约定:参数从右往左压栈。
步骤2.执行 call add 指令
这条指令做两件事:
1.把下一条指令的地址(也就是 call 之后那条指令的地址)压入栈中(这个叫“返回地址”)。
2.跳转到add函数的入口。
2.函数入口(prologue)
1.保存调用者的 EBP
2.设置自己的 EBP
3.为局部变量分配空间
3.函数执行期间
通过[ebp+8]访问第一个参数
通过[ebp-4]访问第一个局部变量
Return:
把返回值放进 EAX 寄存器.
4.函数返回(Epilogue)——清理现场,回家
1.释放局部变量空间 mov esp, ebp ; 把 esp 移回 ebp 的位置
2.恢复调用者的EBP
3.执行 ret 指令
ret这条指令做两件事:
1.从栈顶弹出一个值(当前是“返回地址”),放到 EIP。
2.CPU 跳转到那个地址继续执行。
4.调用者清理参数(cdecl 约定)
main 函数要自己清理参数:
add esp, 8 ; 两个参数,共 8 字节
栈顶 ESP 回到调用前的位置
3.返回值传递机制
1.函数调用过程中的返回值传递机制
1.寄存器传递(小对象)
2.内存传递(大对象)
当返回值超过架构规定的寄存器容量时,编译器会:
1.调用者在栈上分配临时空间
2.将该空间地址作为隐式第一个参数传递
3.被调用函数直接在该地址构造对象(return时)
4.调用者从该地址读取结果
2.引用返回的基本原理
引用返回实际是地址传递,使用指针寄存器(RAX)
6.9 编译过程
1.编译过程是对cpp进行处理。
2.编译时符号解析
1.符号解析过程
1. 收集所有.o文件和库中的符号
2. 构建符号表,记录定义和引用
3. 解析未定义符号
4. 处理重复定义
1.符号分类
1.强符号
强符号在链接时不允许重复定义,如果存在多个强符号会导致链接错误。
1.函数类型
1.普通函数定义
void normalFunc() {} // 强符号(T)
2.类成员函数在源文件中定义
3.显式实例化的模板
template class std::vector; // 强符号
2.变量类型
1.已初始化的全局变量
int globalVar = 42; // 强符号(D)
2.类静态成员变量定义
3.constexpr变量
constexpr int MAX_SIZE = 1024; // 强符号®
2.弱符号(Weak Symbols)
弱符号允许多个编译单元中存在相同定义,链接时会自动合并。
1.函数类型
inline函数。每个.cpp都有副本 仅保留一份 链接器识别为弱符号,只保留一个副本(不能内联展开时,如果可以则代码膨胀)。
模板函数实例化
虚函数表(vtable)
class Base { virtual void foo(); }; // 生成弱符号(V)
2.变量类型
未初始化的全局变量
3.特殊符号类型
1.局部符号(不参与链接)
static函数/变量
static int localVar; // 局部符号(d)
static void localFunc() {} // 局部符号(t)
2.类定义作用
1.类定义虽然不存储在可执行文件中,但它通过以下方式起作用:
1.编译时固化:
对象大小和内存布局 → 固定偏移量
成员函数 → 具体函数地址和调用约定
类型关系 → 继承结构和虚函数表
2.运行时行为:
成员访问 → 直接内存偏移计算
函数调用 → 直接跳转或通过vtable间接调用
类型检查 → 主要在编译时完成,运行时仅限RTTI
核心思想:类定义是编译器的"工作指南",它的价值在于指导编译器生成正确的机器代码,而不是作为运行时数据存在。一旦编译完成,它的使命已经完成,其"精神"已完全融入生成的代码和数据结构中。
7.类
类是一种用户定义的数据类型,包含数据和操作方法。类的实例是对象。
7.1 定义抽象数据类型
7.1.2 定义改进的sales_data类
定义在类内部的函数是隐式的inline函数。
定义成员函数
引入this
成员函数含有this指针,this是一个常量指针,顶层const,无法改变指向。
引入const成员函数
const用于修饰this指针,底层const.
声明和定义都要在最后加const
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
类作用域和成员函数
类本身就是一个作用域。
7.1.3 定义类相关的非成员函数
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。放在类里面。
7.1.4 构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
合成的默认构造函数
未定义构造函数时,编译器会创建一个合成的默认构造函数。不带任何参数,按照下面方式初始化数据成员。
a.如果存在类内的初始值,则用它来初始化。如头文件={};
b.否则,默认初始化该成员。内置类型在函数体内未知值。类对象调用其构造函数。
某些类不能依赖于合成的默认构造函数
- 编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。
- 含有内置类型或复合类型成员的类如果没有默认值,采用的默认初始化,值未定义。
- 编译器无法为某些类合成默认的构造函数。如果类中包含一个类类型没有默认的构造函数,则无法初始化该成员。
= default 含义
想要默认函数等同于合成的默认构造函数。
sales_data() = default;
1.减轻程序员的编程工作量;
2.获得编译器自动生成的默认特殊成员函数的高的代码执行效率
构造函数初始值列表
用于在构造函数的函数体执行之前初始化类的成员变量。它提供了一种高效且简洁的方式来初始化成员变量,尤其是对于那些没有默认构造函数的类类型成员。
ClassName(parameters) : member1(initializer1), member2(initializer2), ...
{
// 构造函数体(可选)
}
高效原因:在函数体内执行前已经初始化一次。
成员初始化顺序与在类内定义的出现顺序一致,与初始化列表的顺序无关。
sales_data obj(); //声明了一个函数而非对象
C++ primer(第5版)中写道:类内初始值的提供必需以=或者花括号{}的形式。不能用园括号()。
7.1.5 拷贝、赋值和析构
如果我们不主动定义这些操作,则编译器会帮我们合成他们。
某些类不能依赖于合成的版本。
7.2 访问控制和封装
定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节。
使用class或struct关键字
使用class和struet定义类唯一的区别就是默认的访问权限,class默认为private,struct默认为public。
7.2.1 友元
声明访问权限,不是函数声明。为了使函数可见,还需要加声明。
类可以允许其他类或者函数访问它的非公有成员。
方法:
类中增加一条以friend开头的函数声明。
一般最好在类定义开始结束前的位置集中声明友元。
C++中的友元关系是针对函数或类。
每个类负责控制自己的友元类或友元函数。友元关系不具备传递性。
单个成员函数也能成为友元函数。
7.3 类的其他特性
7.3.1 类成员再探
令成员作为内联函数
内联函数定义在头文件中。
可变数据成员
变量声明成mutable,永远不会为const。任何时刻都可以更改它,即便是在const成员函数里。
7.3.3 类类型
类的声明
可以只声明类而暂时不定义它,称为前向声明。称为不完全类型。
可以用于:定义指向这种类型的指针或引用,也可以声明(不可以定义)以不完全类型作为参数或者返回类型的函数。
定义一个函数,编译器就会为函数的形参和返回值预先留出合适的内存空间。
7.4 类的作用域(有时间扩充)
作用域和定义在类外的成员函数
一个类就是一个作用域。一旦遇到类名,参数列表和函数体就在类的作用域之内的。返回类型在函数名之前,所以要加作用域。
7.5 构造函数再探(有时间扩充)
7.6 类的静态成员(有时间扩充)
- 与类关联而不是与类的对象关联。
- 静态成员函数不能被声明为const,不包含this指针。
- static关键字只出现在类内部的声明语句中,无法出现在类外部。
- double Account::interestRate = initRate(); 虽然initRate是私有的,也能这样用。从类名开始,这条语句剩余的部分就都位于类的作用域之内了。
- 静态成员可以是不完全类型。如在class Bar里面定义static Bar mem1;
C++标准库
8. IO库
C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即从string读取数据,向string写入数据。
之前已经使用过的IO库设施小结:
| istream | 输入流类型,提供输入操作 |
|---|---|
| ostream | 输出流类型,提供输出操作 |
| cin | 一个istream对象,从标准输入读取数据 |
| cout | 一个ostream对象,向标准输出写入数据 |
| cerr | 一个ostream对象,通常用于输出程序错误信息,写入到标准错误 |
| >> | 用来从一个istream对象读取输入数据 |
| << | 用来向一个ostream对象写入输出数据 |
| getline函数 | 从一个给定的istream对象读取一行数据,存入一个给定的string对象中 |
8.1 IO类
目前为止,我们使用的IO类型和对象都是操纵char数据的,标准库还提供了支持宽字符读写的操作。

IO对象无拷贝或赋值
注意:不能拷贝IO对象,因此也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
一个流一旦发生错误,其上的后续的IO操作都会发生失败。在使用流之前,检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用。
int ival;
cin >> ival;
// 如果在标准输入上键入Boo,读操作就会失败。若输入文件结束标识,cin也会进入错误状态。
while (cin >> word)
// ok: 读取成功
while循环检查 >>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。
管理输出缓冲
- 刷新输出缓冲区可以使用endl、flush和ends
- 如果想在每次输出后都刷新缓冲区,可以使用unitbuf操纵符。
- 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出符。
cout << unitbuf; // 所有输出都刷新缓冲区
cout << nounitbuf; // 回到正常的缓冲方式
输出刷新缓冲区
cout << "hi!" << endl; //输出hi和一个换行符,然后刷新缓冲区
cout << "hi!" << flush; //输出hi然后属性缓冲区
cout << "hi!" << ends; //输出hi和一个空字符,然后刷新缓冲区
unitbuf操纵符
cout << unitbuf; // 所有输出都刷新缓冲区
cout << nounitbuf; // 回到正常的缓冲方式
cin >> ival; // 标准库将cin和cout关联在一起,此语句也会导致cout的缓冲区被刷新
我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream。
cin.tie(&cout); // old_tie 指向当前关联到cin的流(如果有的话)。这句仅仅用来展示:标准库已经默认将cin和cout关联在一起
ostream *old_tie = cin.tie(nullptr); // cin不再与其他流关联
cin.tie(&cerr); // 读取cin会刷新cerr,而不是cout。这不是一个好主意,因为cin应该关联到cout
cin.tie(old_tie); // 重建cin和cout间的正常关联
8.2 文件输入输出
除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流相关的文件。

自动构造和析构:当一个fstream对象被销毁时,close会被自动调用。
文件模式:每个流都有一个关联的文件模式。

8.3 string流

9. 顺序容器
容器是对象的集合,顺序容器提供了控制元素存储和访问顺序的能力。
9.1 顺序容器概述
Array 固定大小数组
Vector 可变大小数组
Deque 双端队列
String 与 vector 相似的容器,但专门用于保存字符
list 双向链表
Forward_list 单向链表
9.2 容器库概览
容器操作
构造函数
C c 默认构造函数,构造空容器
C c(c2); 构造c2的拷贝c1
C c(b,e); 构造c,将选代器b和e指定的范围内的元素贝到c(array不支持)
Cc {a, C…} 列表初始化c
赋值与swap
c1=c2 将c1中的元素替换为c2中元素
cl={a,b, c, ……}; . 将c1中的元素替换为列表中元素(不适用于array)
a.swap(b) 交换a和b的元素
Swap(a b) 与a.swap(b)等价
添加/删除元素
c.insert(args) 将args中的元素拷贝到c
c.emplace(inits) 使用inits构造c中的一个元素
c.erase(args) 删除args指定的元素
c.clear() 删除c中所有元素
9.2.1 迭代器
迭代器范围
一个迭代器范围由一对迭代器表示,左闭右开区间。不相等时说明有元素。
9.2.4 容器定义和初始化
只有顺序容器(不包括array)的构造函数才能接受大小参数
C seg(n) seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的(参见7.5.4节,第265页)。(string不适用)
Cse(n,t) seg包含n个初始化为值t的元素
默认初始化 内置类型随机值(全局变量为0)
值初始化 0
将一个容器初始化为另一个容器的拷贝
1、直接拷贝整个容器, 要求容器类型和元素类型相同。
2、 (array 除外) 拷贝由一个迭代器对指定的元素范围。 要求元素可以转换。
标准库array具有固定大小
初始化方式
array<int, 10> ia1; //10 个默认初始化的
int array<int, 10> ia2 ={0,1,2,3,4,5,6,7,8,9}; // 列表初始化
array<int, 10> ia3 ={42}; //ia3 [0]为42,剩余元素为0
内置数组不支持拷贝或赋值,array 支持,需要保证元素类型,数组大小一致。
9.2.5 赋值和swap
使用assign(仅顺序容器)
seq.assign(b,e) 将se中的元素替换为送代器b和e所表示的范围中的元素。代器b和e不能指向se中的元素
seq.assign(il) 将seg中的元素替换为初始化列表i1中的元素
Seq.assign(n,t) 将seg中的元素替换为n个值为t的元素
使用swap
swap操作交换2个相同容器的内容,只会交换2个容器的内部数据结构,而不会交换元素。
赋值操作会使迭代器、引用和指针失效,而swap不会。
9.2.7 关系运算符
1、如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等:否则两个容器不等。
2、如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器
3、如果两个容器都不是另一个容器的前缓子序列,则它们的比较结果取决于第一个不相等的元素的比较结果
容器的关系运算符使用元素的关系运算符完成比较
只有当共元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
push_back();
push_front();
在容器中的特定位置添加元素
每个insert函数都接受一个送代器作为其第一个参数。表示在哪个迭代器之前插入元素。
返回一个迭代器,该迭代器指向新插入元素的位置。
插入范围内元素
insert其他重载版本支持插入范围。
使用emplace操作
传入构造参数,emplace函数在容器中直接构造元素。比push_back效率高。
9.3.2 访问元素
c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中首元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标为n的元素的引用,n是一个无符号整数。若n>=c.size(),则函数行为未定义
c.at(n) 返回下标为n的元素的引用。如果下标越界,则抛出out_of_rang异常
9.3.3 删除元素
c.pop back() 删除c中尾元素。若c为空,则函数行为未定义。函数返回void
C.pop_front()删除c中首元素。若c为空,则函数行为未定义。函数返回void
c.erase(p) 删除送代器P所指定的元素,返回一个指向被删元素之后元素的送代器,若p指向尾元素,则返回尾后送代器。若p是尾后选代器,则函数行为未定义
C.erase(b,e)删除选代器b和e所指定范围内的元素。返回一个指向最后一个被删元素之后元素的代器,若e本身就是尾后代器,则函数也返回尾后代器
c.clear() 删除c中的所有元素。返回void
Erase 左闭右开
传入迭代器范围(两个迭代器),erase 方法会删除该范围内的所有元素,返回最后一个没被删除的迭代器位置。
删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它们是存在的。
9.3.6 容器操作可能使代器失效
在向容器添加元素后或删除元素后,可能导致迭代器失效。
不要保存end返回的选代器
直接使用,添加/删除元素后,end变化。
9.4 vector对象是如何增长的
管理容量的成员函数
c.shrink_to_fit(); 请求将capacity减少为size大小。只适用于vector。string和deque。
c.reserve(n); 分配至少能容纳n个元素的内存空间。
9.5 额外的string操作
substr操作
s.substr(pos, n); 返回从pso开始的n个字符的拷贝。
9.5.2 改变string的其他方法
s.append(args); 将args追加到s
s.replace(range, args); 删除s范围range中的字符,替换为args。range可以是一对迭代器或一个下标和一个长度。
9.5.3 string搜索操作
找到返回下标,未找到返回-1.
s.find(args) 查找s中args第一次出现的位置。
s.rfind(args)查找s中args最后一次出现的位置,
s.find_first_of(args)在s中查找args中任何一个字符第一次出现的位置。
s.find_last_of(args)在s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) 在s中查找第一个不在args中的字符。
s.find_last_not_of(args) 在s中查找最后一个不在args中的字符。
args必须是以下形式之一
c, pos 从s中位置pos开始查找字符/字符串/以空字符结尾的c风格字符串c,pos默认为0.
cp,pos,n,从s中位置pos开始查找指针cp指向的数组的前n个字符,pos和n无默认值。
9.5.5 数值转换
- 转换为string. to_string(i);
- string转换为其他: stod(dValue); stoi,stol,stoul等。 如果string不能转换为一个数值,会抛出invalid_argument异常。如果转换得到的数值无法用任何类型来表示,会抛出out_of_range异常。
10. 泛型算法
标准库容器定义的操作集合惊人的小。标准库并未给每个容器添加大量的功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。
10.1 概述
算法永远不会执行容器的操作
泛型算法永远不会执行容器的操作,作用于迭代器之上。
10.2 初识泛型算法
10.2.1 只读算法
accumulate:求和算法。
find
count
equal 用于确定两个序列是否保存相同的值,相同返回true,否则返回false。
10.2.2 写容器元素的算法
fill
copy
replace
fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
10.2.3 重排容器元素的算法
sort 使用<
unique 返回指向不重复区域之后一个位置的迭代器
10.3 定制操作
10.3.1 向算法传递函数
谓词
谓词是一个可调用的表达式,返回结果是一个能用作条件的值。谓词分为2类,一元谓词和二元谓词。表示参数个数。
10.3.2 Lambda表达式
介绍Lambda
一个lambda表达式表示一个可调用代码单元,可以理解为一个未命名的内联函数。
[capture list](parameter list)->return type{function body}
参数列表和返回类型可以省略。
向lambda传递参数
lambda函数不能有默认参数。
使用捕获列表
lambda只有在捕获列表捕获所在函数的局部变量,才能在函数体内使用该变量。
10.3.3 捕获和返回
定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型,包括所捕获变量的数据成员。数据成员在对象被创建时初始化。
使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
捕获方式
- 普通捕获:[变量名] ,值拷贝
- 隐式捕获 :[=] [&]
- 混合方式捕获:[=,变量列表] [&,变量列表] 在变量列表使用另一种捕获方式。
10.3.4 参数绑定
标准库bind函数
接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。
如果希望使用引用,使用ref。
使用成员函数:
TClass::mainThread(std::bind(&report::getOne, this));
11.关联容器
类型map和multimap定义在头文件map中:set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。
平衡树结构:std::map 和 std::set 通常基于红黑树实现,这确保了插入、删除和查找操作的时间复杂度为 O(log n)。
哈希表结构:std::unordered_map 和 std::unordered_set 基于哈希表实现,平均情况下插入、删除和查找操作的时间复杂度为 O(1)。
关联容器类型
按关键字有序保存元素
map 关联数组:保存关键字-值对
set 关键字即值,即只保存关键字的容器
multimap 关键字可重复出现的map
multiset 关键字可重复出现的set
无序集合
unordered_map 用哈希函数组织的map
unordered set 用哈希函数组织的set
unordered multimap 哈希组织的map:关键字可以重复出现
unordered multiset 哈希组织的set:关键字可以重复出现
11.2 关联容器概述
关联容器都支持构造和赋值操作,关联容器的送代器都是双向的。
11.2.2 关键字类型的要求
对于有序容器一一map、 multimap、 set 以及 multiset, 必须定义<运算符来比较两个关键字。
1、两个关键字不能同时"小于等于"对方;如果 k1"小于等于"k2,那么k2绝不 能"小于等于"k1。
2、如果k1"小于等于"k2,且k2"小于等于"k3,那么k1必须"小于等于"k3。
3、如果存在两个关键字,任何一个都不"小于等于"另一个,那么我们称这两个关键字是"等价"的。
11.2.3 pair类型
一个 pair保存两个数据成员。是模板,默认构造函数对数据成员进行值初始化。
pair 的数据成员是 public的,分别命名为 first 和 second。
pair < 运算符
1、如果 p1.first < p2.first,则 result 为 true。
2、如果 p1.first == p2.first 且 p1.second < p2.second,则 result 为 true。否则,result 为 false。
pI==p2 当first和second成员分别相等时,两个pair相等。相等性
std::make_pair 可以自动推导出 std::pair 的类型,无需显式指定类型。
11.3 关联容器操作
11.3.1 关联容器达代器
当解引用一个关联容器代器时,我们会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。
set的选代器是const的
一个set中的关键字也是const的。可以用一个set送代器来读取元素的值,但不能修改。
关联容器和算法
关联容器只适用与只读算法,因为key是const.
关联容器定义的专用的find成员会比调用泛型find快得多。泛型find是遍历查找,时间复杂度为o(n)。
11.3.2 添加元素
关联容器insert操作
c.insert(v) v是value_type类型的对象:angs用米构造一个元素
c.emplace(angs) 对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。函数返回一个pair,包含一个送代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。
insert 方法插入一个已经存在的键时,同样插入操作会失败,也不会覆盖已有的键值对。
检测insert的返回值
insert 方法返回一个 std::pair<iterator, bool> 对象。
1、iterator:它指向插入的元素或者已经存在的元素。如果插入成功,它指向新插入的元素;如果插入失败(键已存在),它指向容器中已有的那个具有相同键的元素。
2、bool:表示插入操作是否成功。如果键不存在,插入成功,bool 值为 true;如果键已经存在,插入失败,bool 值为 false。
11.3.3 删除元素
c.erase(k) 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量
c.erase(p) 从c中删除送代器p指定的元素。必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的选代器,若P指向c中的尾元素,则返回c.end()
c.erase(b,e) 删除选代器对b和e所表示的范围中的元素。返回e
11.3.4 map的下标操作
c[k] 返回关键字为k的元素:如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k) 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_ofrange异常
使用下标操作的返回值
返回存储的值的类型的引用。
11.3.5 访问元素
C.find(k) 返回一个代器,指向第一个关键字为K的元素,若K不在容器中,则返回尾后选代器
C.Count(k)返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返口值水远是0或1
C.lower bound(k) 返回一个选代器,指向第一个关键字不小于k的元素
C.upper bound(k) 返回一个选代器,指向第一个关键字大于k的元素
c.equal range(k) 返回一个选代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()
11.4 无序容器(后面有时间扩充)
12. 动态内存
静态内存:
指在程序编译时分配的内存。生命周期通常与程序的生命周期相同,或者与某个作用域的生命周期相同。
分配和释放由编译器和操作系统管理。在栈上。
动态内存:
在程序运行时动态分配的内存。需要手动申请和释放。在堆上。
优点:动态内存可以根据实际需求分配,减少不必要的内存开销。
不受作用域的限制,可以在程序的任何地方使用。
虽然使用动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。
12.1 动态内存与智能指针
new,在动态内存中为对象分配空间并返回一个指向该对象的指针,会调用对象构造函数。
delete,接受一个动态对象的指针,调用析构函数,然后释放内存。
delete[]用于释放数组的内存。
delete后需要重置指针,即为nullptr.
动态内存实际使用容易碰到问题:忘记释放(内存泄漏),释放太早(野指针)。
12.1.1 shared_ptr类
是模板,需要提供指针可以指向的类型。
默认初始化的智能指针中保存着一个空指针。
shared_ptr和unique_ptr都支持的操作
当作指针使用的操作。
p 将P用作一个条件判断,若指向一个对象,则为true
*p 获取指针所指向的对象。
p->mem 即指针指向的mem。
对象操作:
p.get(); 获取原始指针
swap(p, q); p.swap(q); 交换内部保存信息。
make_shared函数
函数模板,需要传入指针所指向的类型,以及构造需要的参数,返回指向此对象的shared_ptr。
内置类型使用值初始化为0,如
Shared ptr p5=make_shared();
默认初始化
默认初始化是指在没有显式提供初始值时对对象进行初始化的过程。以下几种情况会触发默认初始化:
1、定义内置类型变量且未提供初始值:在函数体内部定义的内置类型变量如果未初始化,其值是未定义的;而在全局作用域或命名空间作用域中定义的内置类型变量会被初始化为 0。
2、定义类类型对象且未使用构造函数提供初始值:如果类有默认构造函数(可以是用户定义的,也可以是编译器合成的),则会调用该默认构造函数进行初始化;如果类没有默认构造函数,那么使用默认初始化会导致编译错误。
值初始化 使用()
值初始化是指使用空的初始化器对对象进行初始化。以下几种情况会触发值初始化:
1、使用 new 表达式并提供空括号:例如 new int() 会对新分配的 int 对象进行值初始化。
2、使用类模板的默认构造函数,且该类模板的构造函数使用了值初始化:像 std::vector(10) 中的元素会被值初始化。
3、使用类类型的空括号或花括号初始化:例如 MyClass() 或 MyClass{}。
4、内置类型值初始化为0。
5、使用pair。
6、使用map下标。
shared_ptr的拷贝和赋值
拷贝构造函数:增加引用计数。
赋值运算符:减少左边的引用计数,增加右边的引用计数。
shared_ptr自动销毁所管理的对象
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0 shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
12.1.2 直接管理内存
new 和delete.
其他已讲述过。
int *pi=new int; //i指向一个未初始化的int
12.1.3 shared_ptr和new结合使用
shared_ptr 构造函数是explicit的,只能直接构造,不能隐式转换。
只能用2
1、shared ptr pl=new int(1024) //错误:必须使用直接初始化形式
2、shared ptr p2(newint(1024)); //正确:使用了直接初始化形式
不要混合使用普通指针和智能指针
当将一个shared_ptr绑定到一个普通指针时,就不应该再直接使用原指针了,可能已经被释放掉。
也不要使用get初始化另一个智能指针或为智能指针赋值
赋值时,无法直接用指针,构造函数不支持隐式转换。
可以使用reset,接受原始指针。
其他shared_ptr操作
用reset来将一个新的原始指针赋予一个shared_ptr:
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。
12.1.4 智能指针和异常
异常发生,跳出函数时,临时变量(智能指针)会被销毁,程序员自己管理的则不会。
智能指针和哑类
分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误一一程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏
优化方法,使用智能指针
void f(destination &d/*其他参数*/)
{
connection C=connect(&d)
shared ptr<connection> p(&c,end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
12.1.5 unique_ptr
unique_ptr 独自拥有它指向的对象,不支持拷贝和赋值操作。
unique_ptr操作
u =nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,并将u置为空
用法
p2.reset(p3.release());// reset释放了p2原来指向的内存
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr. 右值引用。

12.1.6 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。
weak_ptr的操作
weak_ptr 构造和赋值
为weak_ptr 或 shared_ptr。
常用函数:
p.lock(); 检查 std::weak_ptr 所指向的对象是否还存在,如果存在,就返回一个指向该对象的 std::shared_ptr;如果对象已经被销毁,就返回一个空的 std::shared_ptr
使用场景:
- 解决循环shared_ptr引用无法释放的问题
- 安全地观察 std::shared_ptr 管理的对象,又不影响对象被销毁。
12.2 动态数组(后续补充)
类设计者的工具
13. 拷贝控制
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
参数
参数必须为引用,否则会不断递归。
合成拷贝构造函数
如果我们没有为一个类定义贝构造函数,编译器会为我们定义一个。
默认的作用
浅拷贝:
在浅拷贝中,对于内置类型成员(如int、float等),编译器会直接按字节复制其值。
对于自定义类型成员(如类对象、结构体等),编译器会调用这些成员的拷贝构造函数来完成复制。但需要注意的是,如果自定义类型成员包含指针,并且这些指针指向动态分配的内存,那么浅拷贝会导致两个对象中的指针都指向同一块内存空间。这可能会引发“双重删除”问题,即同一块内存被两个对象的析构函数分别释放两次,从而导致程序崩溃。
直接初始化和拷贝初始化
直接初始化:使用圆括号()来调用构造函数。
拷贝初始化:使用等号=来进行赋值。
直接初始化总是调用与提供的初始化器匹配的构造函数。
拷贝初始化首先尝试使用等号右侧的表达式来构造一个临时对象(如果可能的话),然后使用拷贝构造函数(或移动构造函数)将这个临时对象复制到新创建的对象中。然而,在大多数现代编译器中,对于内置类型和像std::string这样的标准库类型,拷贝初始化通常会被优化为直接调用构造函数,以避免不必要的临时对象创建。但是,对于用户定义的类型,特别是那些涉及动态内存分配或复杂资源管理的类型,了解这两种初始化方式之间的潜在差异就很重要了。
以下是直接初始化
std::string str1("Hello"); // 直接初始化
std::string str2(str1); // 直接使用str1来初始化str2,也是直接初始化
拷贝初始化的限制
使用explicit关键字,关键字用于防止隐式类型转换,通常用于单参数的构造函数
即通过参数生成新的对象。
只要是通过单参构造函数,都可以用explicit。
13.1.2 拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
作用:
1、避免不必要的对象复制,返回对象,会调用拷贝构造会增加不必要的开销。
2、返回引用允许链式赋值操作,a = b = c; 返回对象时,临时对象是右值,赋值运算符不支持右值。
13.1.3 析构函数
析构函数完成什么工作
首先执行函数体,然后销毁成员,成员按照初始化顺序的逆序销毁。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
什么时候会调用析构函数
无论何时一个对象被销毁,就会自动调用其析构函数:
1、变量在离开其作用域时被销毁。
2、当一个对象被销毁时,其成员被销毁。
3、容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
4、对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
5、对于临时对象,当创建它的完整表达式结束时被销毁。
合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
13.1.4 三/五法则
1、需要析构函数的类也需要拷贝和赋值操作
2、需要拷贝操作的类也需要赋值操作,反之亦然。不一定需要析构函数。如每个类对象拥有唯一id.
13.1.5 使用=default
显式要求编译器生成合成版本
13.1.6 阻止拷贝
定义删除的函数
使用=delete。
显式使用delete,可以阻止编译器合成对应函数。
析构函数不能是删除的成员
对于析构函已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
合成的拷贝控制成员可能是删除的
当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
13.2 拷贝控制和资源管理
通常,管理类外资源的类必须定义拷贝控制成员。如需要通过析构函数来释放对象所分配的资源,则必须需要一个拷贝构造函数和拷贝赋值操作符。
拷贝语义有2种定义方式,可以定义拷贝操作,使类的行为看起来像一个值或一个指针。
1、类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然。
2、行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然。
13.2.1 行为像值的类
类值拷贝赋值运算符
1、如果将一个对象赋予它自身,赋值运算符必须能正确工作。
2、大多数赋值运算符组合了析构函数和拷贝构造函数的工作。 指针
一个好的方法是在销毁左侧对象前先拷贝右侧运算对象。
13.2.2 定义行为像指针的类
13.3 交换操作
重排算法会调用swap交换元素,因此定义自己的swap可以提高效率
void swap(demoClass& d1, demoClass& d2)
{
using std::swap; //可以扩大搜索域,优先使用自身的。
swap(d1.str, d2.str); // 直接交换指针
}
13.6 对象移动
新标准的一个最主要的特性是可以移动而非只拷贝对象的能力。
在其中某些情况下,对象拷贝后就立即被销毁了。用移动而非拷贝对象会大幅度提升性能。
标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值引用
右值引用是必须绑定到右值的引用,使用&&获得。
左值持久,右值短暂
因为右值只能绑定到临时对象,所以
1、所引用的对象将要销毁
2、该对象没有其他用户
所以使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值,因此我们不能将一个右值引用直接定到一个变量上,即使这个变量是右值引用类型也不行。
标准库move函数
显式地将一个左值转换为对应的右值引用类型。
告诉编译器,有一个左值,但我们希望像一个右值一样处理它。调用时必须保证,调用后不再访问其值。
格式
使用std::move,可避免命名冲突。
13.6.2 移动构造函数和移动赋值运算符
1、完成资源移动
2、确保移后源对象处于这样一个状态一一销毁它是无害的。如把指针置空。
移动操作、标准库容器和异常
异常安全性
C++标准库中的容器(如std::vector)在进行元素插入、删除或内存重分配等操作时,需要保证异常安全性。这意味着,如果某个操作在执行过程中抛出异常,容器必须能够恢复到操作开始之前的状态,以避免资源泄漏、数据不一致或其他潜在问题。
不抛出异常的移动构造函和移动赋值运算符必须标记为noexcept。
移动构造函数未标记为noexcept,标准库会优先使用拷贝构造函数。将移动构造函数标记为noexcept可以向标准库容器表明,该构造函数在执行过程中不会抛出异常。标准库会优先使用移动构造函数,提高效率。
格式:
在移动构造函数的声明末尾添加noexcept关键字,仅需要在函数声明时加。
移后源对象必须可析构
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
合成的移动操作
只有当一个类没有定义任何自版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函或移动赋值运算符。
如果我们显式地要求编译器生成-default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。
移动右值,拷贝左值
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则。
移动右值,拷贝左值。但如果没有移动构造函数,右值也被拷贝。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似。
建议:更新三/五法则
所有五个拷贝控制成员应该看作一个整体,定义了一个,就应该定义其他的。
三原则
拷贝构造函数、赋值运算符、析构函数
五原则
新加移动构造函数、移动赋值运算符
14. 重载运算符与类型转换
当运算符被用于类类型的对象时,C++语言允许我们为其指定新的含义;同时,我们也能自定义类类型之间的转换规则。和内置类型的转换一样,类类型转换隐式地将一种类型的对象转换成另一种我们所需类型的对象。
14.1 基本概念
1、重载运算符的名字:operator和运算符号共同组成。其他部分与普通函数类似。
2、参数数量与该运算符作用的运算对象数量一样多。为成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个。
直接调用一个重载的运算符函数
1、直接使用运算符隐式调用。
2、直接函数名+参数调用。
某些运算符不应该被重载
1、某些运算符指定了运算对象求值的顺序。重载后无法应用到重载的运算符上,所以不应该重载。
2、逗号运算符和取地址运算符有特殊含义,也不应该重载。
通常情况下,不应该重载号、取地址、逻辑与和逻辑或运算符。
选择作为成员或者非成员
1、赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
2、复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
3、改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
4、具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
14.2 输入和输出运算符(待完善)
15. 面向对象程序设计
15.1 OOP概述
继承
是一种面向对象编程的特性,它允许一个类继承另一个类的属性和方法。通过这种方式,子类可以重用和扩展基类的功能,从而增强代码的可重用性和模块化。
类派生列表的形式:冒号(:)+ 逗号分隔的基类列表,每个基类前面可以加访问说明符(public等)。
动态绑定
使用基类的指针或引用调用一个虚函数时将发生动态绑定。
15.2 定义基类和派生类
15.2.1 定义基类
基类都应该定义一个虚析构函数,即使该函数不执行任何操作也是如此。
成员函数与继承
1、虚函数,派生类需要提供自己的新定义以覆盖(override)从基类继承而来的旧定义。
2、当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
3、关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
4、成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
访问控制与继承
1、派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
public:公有访问权限。都可。
protected:保护访问权限。类外无法访问,派生类,类内public方式。
private:私有访问权限。类内访问,派生类、类外无法访问。
private成员派生类不可访问,其他成员根据继承方式,在派生类中保持新的访问权限。
public 继承:保持基类成员的访问权限不变。
protected 继承:将基类的 public 成员变为 protected,protected 成员保持不变。
private 继承:将基类的 public 和 protected 成员变为 private。派生类可以访问基类的public和protected权限。
15.2.2 定义派生类
1、类派生列表的形式:冒号(:)+ 逗号分隔的基类列表,每个基类前面可以加访问说明符(public等)。
2、访问说明符用于控制基类成员在派生类中的访问权限以及派生类对象对这些成员的可见性和可访问性。
派生类中的虚函数
1、派生类未重写虚函数。则会直接继承其在基类中的版本。
2、virtual关键字可加可不加。
3、声明时加override关键字,可显式地注明覆盖基类的虚函数,并增加编译器检测功能。
派生类对象及派生类向基类的类型转换
1、派生类对象包含基类部分,可以把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。
派生类构造函数
1、每个类控制它自己的成员初始化过程。派生类也必须使用基类的构造函数来初始化它的基类部分。在初始化列表中以类名加圆括号内的实参列表的形式为构造函数提供初始值。
2、首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的对象。
派生类构造函数的执行过程
1、调用基类构造函数:
2、调用内嵌对象成员的构造函数
3、执行派生类构造函数的函数体
派生类析构函数的执行过程
1、执行派生类析构函数的函数体
2、调用内嵌对象成员的析构函数
3、调用基类析构函数
派生类使用基类的成员
派生类的作用域嵌套在基类的作用域里面。
并不是在说作用域有大小之分或包含关系,而是在描述一种作用域查找的层次和访问权限的关系。
1)“继承”并不意味着“复制”。派生类并没有创建基类成员的新副本;相反,它共享了基类成员(在对象布局上,这通常意味着派生类对象在内存中包含了一个基类子对象)。派生类可以访问基类成员,即派生类有一个特殊的访问路径到这些基类成员。
2)尽管派生类可以访问基类的公有和保护成员,但它并不“拥有”这些成员。这些成员仍然属于基类,而派生类只是被授权在其自己的作用域内访问它们。
关键概念:遵循基类的接口
每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。因此,派生类对象不能直接初始化基类的成员。
继承与静态成员
基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义。
防止继承的发生
在类名后面加一个关键字final
class A final{};
15.2.3 类型转换与继承
1、通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与
对象的类型一致,或者对象的类型含有个可接受的const类型转换规则。存在继承关系的类是一个重
要的例外:我们可以将基类的指针或引用绑定到派生类对象上。
需要继承类型为public,此时在类外可以访问。其他继承方式看使用的转换的位置,类外,派生类中。
2、和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
静态类型与动态类型
当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。
1、表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型。
2、动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
3、如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
不存在从基类向派生类的隐式类型转换
编译器在编译时无法判断转换是否安全,需要显示转换,使用dynamic_ cast或static_cast。
在对象之间不存在类型转换
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值, 它的派生类部分将被忽略。即切割掉。也与访问权限和位置有关。
基类成员在派生类中的访问权限,以及引用操作所在的作用域是否满足对这些成员的访问要求。
15.3 虚函数
final和override说明符
override表示覆盖了基类的虚函数,如果没有则编译器报错
final可以避免虚函数再次被覆盖,覆盖则报错。
虚函数与默认实参
1、如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
2、如果虚函数使用默认实参,则基类和派生类中定义的认实参最好一致。
回避虚函数的机制
强行调用基类的虚函数。
basep->BASE::print();
15.4 抽象基类
纯虚函数
1、和普通的虚函数不一样,一个纯虚函数无须定义。将一个虚函数声明为= 0。
含有纯虚函数的类是抽象基类
1、含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。
2、我们不能创建抽象基类的对象。
15.5 访问控制与继承
每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问。
受保护的成员
protected说明符可以看做是public和private中和后的产物
1、和私有成员类似,受保护的成员对于类的用户来说是不可访问的
2、和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
3、派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。无法通过基类对象去访问。
派生类向基类转换的可访问性
转换:基类引用或指针指向派生类, 派生类对象给基类对象赋值
派生类向基类的转换是否可访问由使用该转换的代码位置决定,同时派生类的派生访问说明符也会有影响。
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
友元与继承
1、就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
改变个别成员的可访问性
用的少。
默认的继承保护级别
1、默认情况下,使用class关键字定义的派生类是私有继承的:而使用struct关键字定义的派生类是
2、一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。
15.6 继承中的类作用域
派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
名字冲突与继承
定义在派生类的名字将隐藏定义在基类的名字,可以加类名重新指定。
关键概念:名字查找与继承
1、首先确定p(或ob)的静态类型。
2、在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错
3、一旦找到了mem,就进行常规的类型检查以确认对于当前找到的mem,本次调用是否合法
4、假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型
反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用
一如往常,名字指定先于类型检查
名字相同,则不再检查类型。如派生类成员变量可以隐藏基类同名成员函数。
虚函数与作用域
基类与派生类的虚函数接受的实参不同,会发生隐藏。
覆盖重载的函数
基类多个重载版本,派生类重载基类的某个版本,类内using声明。
15.7 构造函数与拷贝控制
15.7.1 虚析构函数
虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
15.7.2 合成拷贝控制与继承
派生类中删除的拷贝控制与基类的关系
1、基类的同名删除,派生类无法使用。
2、析构函数被删除,编译器将不会生成默认的拷贝构造函数和拷贝赋值函数。为了防止潜在的资源管理错误,因为删除析构函数通常意味着类需要显式地管理资源。
15.7.3派生类的拷贝控制成员
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
定义派生类的拷贝或移动构造函数
在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
派生类赋值运算符
需调用基类的,Base::operator=(rhs);
派生类析构函数
对象的基类部分是隐式销毁的。派生类析构函数只负责销毁由派生类自己分配的资源。
在构造函数和析构函数中调用虚函数
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
15.7.4,未看懂,先不搞
15.8 容器与继承
当我们使用容器存放继承体系中的对象时,不能直接存放对象,需要放置(智能)指针。
放对象:派生类赋值基类时,会切割。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。主要指拷贝构造和拷贝赋值。
编译器在检测到类定义了移动操作时,默认将拷贝操作定义为删除的,是为了防止潜在的错误。如果开发者需要支持拷贝操作,必须显式地定义这些成员函数。
如指针,资源管理错误。
如果类中显式定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,编译器将不会自动合成移动构造函数和移动赋值运算符。即便定义时是=default。
16. 模板与泛型编程
范类型编程思想,对多种不同类型的数据执行相同操作的情况,例如通用的算法实现。
一个模板就是一个创建类或函数的蓝图或者说公式。
16.1 定义模板
16.1.1 函数模板
模板定义以template <模板参数列表>开头,模板参数列表类似与函数参数列表,表示在类或函数定义中用到的类型或值。
template < typename T >
实例化函数模板
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。
模板类型参数
模板类型参数前必须加关键字class或typename。推荐使用typename。
非类型模板参数
表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。 当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。需要在编译期确定。必须是常量表达式
函数模板定义和普通函数差不多,只是之前加了template 描述类型信息,
然后函数参数使用模板描述的类型。
16.1.2 类模板
编译器不能为类模板推断模板参数类型,调用时需要使用<>提供额外信息。
类模板成员函数的实例化
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
在类代码内简化模板类名的使用
类模板的名字不是一个类型名。类型包含模板参数。但在一个类模板的作用城内,我们可以直接使用模板名而不必指定模板实参。
16.1.3 模板参数
默认模板实参
可以提供默认模板实参。
16.1.5 控制实例化
显式实例化,解决多个文件生成多个同样模板类问题。
extern template declaration; //实例化声明
template declaration; //实例化定义,编译器会生成代码。
如
extern temnplate class Blob //声明
template int compare(const int&);//定义
实例化定义会实例化所有成员。
编译器不了解会使用哪些,所以都用了。
16.2 模板实参推断
16.2.1类型转换与模板类型参数
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
正常类型转换应用于普通函数实参
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
16.2.2 函数模板显式实参
在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
指定显式模板实参
显式模板实参按由左至右的顺序与对应的模板参数匹配;第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
正常类型转换应用于显式指定的实参
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
long lng;
compare(lng,1024); //错误:模板参数不匹配
compare < long>(lng, 1024) //正确:实例化compare(long,long)
compare < int>(lng,1024); //正确:实例化compare(int,int)
16.2.3 尾置返回类型与类型转换
基本语法
auto function_name(parameters) -> return_type
{
// 函数体
}
auto 是占位符,return_type 是实际的返回类型,它位于函数参数列表之后。
使用场景
- 函数返回类型依赖于参数类型,如使用模板时,类型需要根据参数推导
- 复杂的返回类型
当函数的返回类型比较复杂时,尾置返回类型可以提高代码的可读性。例如,返回一个函数指针或者一个嵌套的类型。
16.2.4 函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参
如
template int compare(const T& const T&)
//pf1指向实例int compareconst int&,const int)
int(*pfl)(const int &,const int &) = compare;
16.2.5 模板实参推断和引用
非引用参数
忽略实参的引用属性和 const 修饰符。
从左值引用函数参数推断类型
去除引用属性。
const T&(去除引用和 const 修饰符)
右值引用参数(通用引用)
会依据实参是左值还是右值来进行不同的推断。
若实参是左值,T 会被推断为左值引用类型;
若实参是右值,T 会被推断为非引用类型。
引用折叠和右值引用参数
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
• T& &、T& && 和 T&& & 都折叠成 T&
• T&& && 折叠成 T&&
完美转发
std::forward完美转发,来源于C++的引用折叠规则和模板参数推导。
完美转发总是和“通用引用”(Universal Reference)T&& 配合使用。
当函数模板参数是通用引用 (T&&) 时,如果传入一个纯右值(如字面量 10),T被推导为非引用类型 int,而不是 int&&。
让T在非引用类型和引用类型之间变化,而不是在左值引用和右值引用之间变化。简化实现。
16.2.6 理解std:move
可以用move获得一个绑定到左值上的右值引用。
源码实现
template
typename std::remove_reference::type && move(T&& arg) noexcept
{
return static_cast<typename std::remove_reference::type&&>(arg);
}
从一个左值static_cast到一个右值引用是允许的
16.2.7 转发
定义能保持类型信息的函数参数
如果一个函数参数是指向模板类型参数的右值引用(如&&),它对应的实参的const属性和左值/右值属性将得到保持。
在调用中使用std:forward 保持类型信息
16.3 重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。
匹配规则
如果同样好的函数中只有一个是非模板函数,则选择此函数。
如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
否则,此调用有岐义。
16.4 可变参数模板
定义
class或typename 后接… 表示接下来的参数表示零个或多个类型的列表。
sizeof…运算符
返回可变参数个数
16.5 模板特例化
通用模板的定义对特定类型是不适合的,需要对特定类型实例化。
定义函数模板特例化
关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参
函数重载与模板特例化
特例化的本质是实例化一个模板,而非重载它。因此,特例化不彩响函数匹配
关键概念:普通作用域规则应用于特例化
模板及其特例化版本应该声明在同一个头文件中。避免生成2个一样的。
类模板部分特例化
只能部分特例化类模板,而不能部分特例化函数模板
18. 用于大型程序的工具
19. 特殊工具与技术
19.1 控制内存分配
19.1.1 重载new和delete
new做的事
1、new表达式调用一个名为operator new(或者operator new])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储持定类型的对象(或者对象的数组)。
2、编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
3、对象被分配了空间并构造完成,返回一个指向该对象的指针。
delete做的事
1、对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数。
2、编译器调用名为operator delete(或者operator delete [])的标准库函数释放内存空间
如何重载
定义自己的operator new函数和operator delete函数。
首先在类作用域寻找,然后在全局作用域寻找。
::new 在全局作用域寻找。
19.1.2 定位new表达式
当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。
new (place addresspe) type
new (place addresspe) type (initializers)
new (place address) type [size]
new (place address) type [size] {braced initializer list}
显式的析构函数调用
string *sp =new string”a value”); //分配并初始化一个string对象
Sp->~string();
调用析构函数会销毁对象,但是不会释放内存。

33万+

被折叠的 条评论
为什么被折叠?



