Cpp
C++ 语法
1. 输入数据
std:cin >>
进行输入,如果申明了using namespace std
,那么就不需要std:
,cout
同理。C++ 原标准中,可以使用
()
来代替=
来赋值,在 C11 中,还可以使用大括号来统一初始化列表。
例如:1
2
3int a = 10;
int a(10);
int a{10};这三个结果相同。
2. 三目表达式
c = (表达式) ? 表达式1 : 表达式2;
1
2
3
4
5
6
7
### 3. 跳转表达式(不建议使用)
1. ```c++
goto xxx;
xxx:
// 待执行的内容
4. 函数分文件编写
- 头文件(*.h):需要包含的头文件,指定命名空间,申明全局变量,函数的声明,数据结构和类的声明等。
- 源文件(*.cpp):函数的定义、类的定义。
通过#include "头文件名"
来包括头文件
5. sizeof
运算符
- 用于求数据类型或者变量占用的内存空间。
- 注意:
- 32 位和 64 位的 OS 占用的内存大小可能不一样。
string
不是 C++ 的基本数据类型,用sizeof
求其内存大小没有意义。
6. 整型的取值
- 取值范围根据所占字节位数。
用sizeof
可以看每种类型占用的字节数,注意unsigned
修饰的整型。 - 超过范围的数据会被截断
- C11 中,增加了
long long
数据类型,在 Windows 中占 8B。注意 Linux 和 Windows 中,不同类型占用的字节数可能不同。
7. 整数的进制
- 用
0b 或者 0B 开头
来表示二进制 - 用
0 开头
表示八进制 - 用
0x 或者 0X
来表示十六进制
8. 原始字面量(C11)
R"(字符串)"
,这样里面的反斜线就不会执行转义的功能。- 同时使用这个,字符串换行时不需要用
\
进行连接。
9. 字符串拼接
用
+
号需要注意的是,如果多个常量字符串进行拼接,直接不能用
+
,会报错。直接放在一起就行。
例如:1
2
3
4// 这个会报错
cout << "你我他" + "还就那个丁真" << endl;
// 这个就可以
cout << "你我他" " 还就那个丽丽" << endl;
10. 别名
- 常用的就是:
1
2
3
4
5
6
7
8// Windows 平台中,short 是 2B,int 是 4B,long 也是 4B,longlong 是 8B
typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;
// Linux 中,short 是 2B,int 是 4B,long 是 8B,longlong 是 8B
typedef short int16_t;
typedef int int32_t;
typedef long int64_t;
11. 用 const
修饰指针
常量指针
const 数据类型 *变量名
注意点:常量指针的值不能改变,但是可以指向的对象可以改变。
例如:1
2
3
4
5
6int a = 10, b = 5;
const int *p = &a;
// 这个可以
p = &b;
// 这个不行捏
p = 10;一般用于修饰函数的形参,表示其在函数中不能被更改。
1
void fun(const int no);
用
const
修饰形参,虽然指向的对象可以改变,但一般不这么做,用const
就是表名形参的值不需要改变,增加程序可读性。
指针常量
数据类型 * const 变量名
和常量指针的特性相反,可以通过解引用来修改内存的值,但是不能修改指向的对象。
一般不同, C++ 编译器把指针常量做了一些特别的处理,改成了新的东西,叫引用,引用在实际开发中用的多。常指针常量
const 数据类型 * const 变量名
这些都不能改。同样的,后面还有常引用。常量指针用的多,多记住这个就行。
12. 出现烫烫烫的原因
- C++ 中,
char 和 char *
为 C 风格字符串,没有被赋值的地方自动赋 0xf。最后会用 0xcc 来做中断保护。而这些 16 进制在 GBK 编码下显示时就会转义成“烫”和“屯” - 所以在初始化时,建议先用
\0
来进行初始化,这样未被赋值的地方就会当成\0
结束符。
13. void *
的其他用途
- 函数的形参用
void *
,表示接受任意数据类型的指针。 - 注意点:
- 不能用
void
声明变量。 - 不能对
void *
指针直接解引用(需要转换成其他类型的指针)。 - 把
void *
赋值给其他类型的指针需要转换。
- 不能用
14. C++ 内存模型
- 栈和堆的区别:
- 管理方式不同:栈是系统自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程序中不释放,程序结束时由操作系统回收。
- 空间大小不同:堆内存的大小受限于物理内存空间;而栈就小得可怜,一般只有8M(可以修改系统参数)。
- 分配方式不同:堆是动态分配;栈有静态分配和动态分配(都是自动释放)。
- 分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由C++函数库提供的。
- 是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片,太多的碎片会导致性能的下降。
15. 动态分配地址(使用堆空间)
- 申请内存:
new 数据类型(初始值)
- 释放内存:
delete 地址
- 注意点:
- 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
- 如果内存不用了,一定要通过
delete
进行释放。 - 程序退出时,动态分配的内存自动回收。
- 即使指针的作用域已经失效,所指向的内存也不会释放。
- 当堆的空间不够分配时,程序会崩溃,为了不让其崩溃,有关键字:
new (std::nothrow)
,这样子当空间申请失败时,返回空地址nullptr
。
16. 二级指针
二级指针指向一级指针的地址。
注意,通过解引用修改二级指针所指内容,即修改了所指的一级指针的地址内容,那么此时一级指针的内容就会被修改。
例如:1
2
3
4
5
6
7
8
9
10int* p = 0;
{
int** pp = &p;
*pp = new int(3);
cout << "pp = " << pp << ", *pp = " << *pp << endl;
}
cout << " p = " << p << ", *p = " << *p << endl;
// 输出结果为:
// pp = 00000061F770F928, *pp = 000002336C3762C0
// p = 000002336C3762C0, *p = 3指针指向地址(即内容是地址,但其本身也有自己所在地址),* 为取所指地址的内容,& 为取所在地址,直接输出为其所指地址/指针内容
17. 空指针
- 对空指针进行解引用会造成程序会崩溃。
- 但是对空指针进行
delete
时系统自动无视。 - 写函数时,尽量对空函数进行判断。
- C11 中,因为 0 和
NULL
表示空指针会产生歧义(因为重载),所以建议用nullptr
来表示空指针,也就是(void *)0
18. 野指针
- 定义的时候没有初始化。
- 动态分配后释放的指针,其不会被置空,但指向的地址已经失效。
- 指针指向的变量超出了其作用域(例如函数返回函数内的形参,然后在函数外访问返回的地址。)
- 规避方法:
- 用
nullptr
。 - 函数不要返回局部变量的地址。
- 用
19. 函数指针
格式:
返回值 (*指针名)(数据类型1, 数据类型2...)
理论:函数的类别取决于其返回值以及形参的数据类型,即函数的“数据类型”。函数指针可以指向特定的函数。
调用:
1
2
3
4// C++
函数指针(具体参数);
// 或者 C
(*指针)(具体参数);函数指针常用于回调函数,在 Java 中一般是定义接口,然后其他函数继承该接口并实现回调函数。C++ 中就是把 Java 的接口换成函数指针,放在函数的形参中,其他对象要实现和函数指针同类的函数,然后调用·调用回调函数的函数。
20. 一维数组和指针
- 指针变量/地址的值 +1 后,增加的量等于它所指向的数据类型的字节数。
- 数组直接取地址默认是头地址。
- 指针和数组对应,即 C++ 编译器将
数组名[下标]
解释为(数组首地址 + 下标 * 数据类型)
- 数组名未必会被解释成地址,例如
sizeof
计算数组时,返回的是整个数组的长度,此外数组名是常量,不可进行修改。
21. 一维数组用于函数的参数
中途更改操作类型的方法:
1
2
3
4
5
6
7char a[20];
// 将 char 改成 int,一个 int 占 4 字节
int* p = (int*)a;
for (int i = 0; i < 5; i++) {
p[i] = i + 300;
cout << "p[" << i << "]的值为" << p[i] << endl;
}也就是开辟了内存空间,但是中途改变了操作内存的方法。
函数的参数
void fun(数据类型 arr[])
这样传入时,传入的是指针,注意此时如果在函数中对其sizeof
,那么值恒为 8,也就是指针固定的长度。因此数组作为形参时,必须将长度也作为形参传入。
22. 创建动态内存的数组
- 创建:
数据类型 *arr = new 数据类型[长度]
- 销毁:
delete []arr
- 注意点:
- 动态创建的数组没有数组名,只有指向的指针,因此不能用
sizeof
- 可以使用数组表示法来使用数组。
- 对空指针使用
delete[]
是安全的,所以释放内存后,将其置空以防止误操作。 - 不用动态分配的话,栈的空间是优先的,很小,超过一定的长度就会强制中断。
- 动态创建的数组没有数组名,只有指向的指针,因此不能用
23. 排序——库函数 qsort()
函数原型,类似 Java 的
arrays.sort()
:
void qsort(void *base, size_t nmemb, size_t size, int (* compar)(const void*, const void *));
base
:数组的起始地址
nmemb
:数组元素的个数(数组长度)
size
:数组元素的长度(即sizeof(数据类型)
)
int compar(const void *p1, const void *p2)
:即比较函数。<0 p1 在 p2 前面,=0 不确定,>0 p1 在 p2 后面。
例如:1
2
3
4
5// 从小到大排序函数
int compasc(const void* p1, const void* p2)
{
return *((int*)p1) - *((int*)p2);
}小细节:
size_t
是 C 标准库中定义的,在 64 位系统中是 8 字节无符号整型(unsigned long long
)。
24. C 风格字符串
- C 语言规定,
char
数组的末尾有'\0'
,那么这个数组就是字符串。
string
的本质就是:
假设string
长度为 n,那么其就是char[n + 1]
,且最后一位为'\0'
。 - 创建时一定要进行初始化,否则直接调用时,会一直向后寻找 0 为止,这个过程会越界。
最常用的初始化:char arr[长度] = {0};
。把所有元素初始化为 0。 - 清空字符串:
- 使用
memset(arr, 0, sizeof(arr))
清空,推荐。 - 直接
arr[0] = 0
,利用 0 作为结尾的规定,不规范且有隐患(后面的元素会指向垃圾值)。
- 使用
- 字符串的复制或者赋值
char *strcpy(char* dest, const char* src)
将src
字符串拷贝到dest
所指地址。返回dest
字符串的起始地址。
复制完后,会在dest
结尾追加 0('\0'
);
此外,如果dest
所指的内存空间不够大,会导致数组的越界。char *strncpy(char* dest, const char* src, const size_t n);
将src
的前 n 个字符内容复制到 dest 中。返回值为dest
字符串的起始地址。
如果src
的长度小于 n,那么会补 0 补到 n 个;但是,如果src
的长度大于 n,那么就截取src
中的前 n 个字符,而且需要注意的是,其不会在后面添加 0(这和strcpy
不一样)。因此使用的时候,需要先初始化。
同样的,如果dest
所指的空间不够大,会导致数组的越界。
- 拼接
strcat() 和 strncat()
- 还有比较
strcmp()
和strncmp()
- 字符查找
strchr()
和strrchr()
;字符串查找strstr()
- C 风格和
string
可以混用。 - 注意:
- 字符串标志结尾为 0,因此编译器不会判断数组是否越界,只会找末尾的 0。
- 字符串使用前尽量都初始化。
- 不要对字符串指针用
sizeof
。 - VS 默认 C 标准的字符串操作函数是不安全的,因此在使用时需要在源代码文件最上面添加:
#define _CRT_SECURE_NO_WARNINGS
在 VS 中,可以使用strcpy_s
和strcat_s
两个安全函数。但在 Linux 就没有,所以没必要。
25. 行指针
- 行指针:
数据类型 (*p)[列长]
所以int a[10]; &a + 1;
,此时&a + 1
就是a[0] + 10 * sizeof(int)
,因此&a
实际上是行指针。 - 函数参数行指针:
fun(int p[][n], int len)
或者fun(int (*p)[n], int len)
26. 结构体
- 结构体初始化:
结构体 name = {0}
,可以没有 0,C11 可以没有等于号,一定要初始化,否则会有垃圾值; - 结构体的
sizeof
不一定就是结构体内各元素所占空间的总和,因为其还要满足内存对齐的规则。通过#pragma pack(字节数)
来修改内存对齐的规则。因此在实际开发中要合理使用内存对其的规则,否则某些节省内存的做法毫无意义。 - 清空结构体,在 C++ 中使用
memset(结构体地址, 0, sizeof)
来清空。用bzero(结构体地址, sizeof)
也可以。 - 结构体复制:万能的
memcpy()
函数或者=
(这两个都只适用于 C++ 基本数据类型)
27. 结构体指针
- 结构体访问成员就是:
结构体.成员变量
- 结构体指针访问结构体成员:
(* 指针名).成员变量名 或者 指针名->成员变量名
需要注意的是,.
的优先级高于->
。 - 两大用途:函数的参数以及动态分配内存。
28. 结构体数组
- 赋值方式:一个一个元素赋值。
C11 中,使用结构体数组[n] = {...}
来赋值。
29. 结构体中的指针
如果结构体中的指针指向的是动态分派的内存地址:
结构体的
sizeof
可能没有意义,因为里面有动态分配的元素,可能是数组等,此时它的大小会动态变化。此时对结构体用
memset()
函数进行清空可能会导致内存泄漏,即假设结构体内有动态分配的数组,对结构体进行清空后,指针变为空指针,但是动态分配的数组所占用的空间没有被释放,此时该空间丢失了指针,无法被释放回收。因此正确的做法是先将结构体内动态分配的空间menset()
或者delete()
,然后对结构体进行menset()
。注意,
string
中有一个指向动态分配的内存地址指针,即当赋值的字节数超过当前的字节数,它就会释放并重新分配空间。相当于:s1
2
3
4
5
6struct string
{
// 指向动态分配内存的地址
char *ptr;
...
}因此:
string
类型不能用memset()
,否则memset()
会破坏string
本身的结构,导致在之后对string
对象进行操作时,会引发内存分配的动作,从而使得string
内*ptr
变成空指针。后面再赋值使用时会发生错误。所以要想用menset()
,就必须使用 C 风格字符串。
30. 共同体
定义:
1
2
3
4
5
6union 共同体名
{
成员数据类型1 成员名1;
成员数据类型2 成员名2;
...
}注意,共同体中只能有
int、double、char
三种类型。同一时间只能存储其中的一种类型。特点:
- 共同体占用的大小为最大的成员的大小且满足内存对齐。
- 全部成员共用一个空间。
- 共同体中的值为最后被赋值的那个成员的值。
- 匿名共同体没有名字,可以在定义时创建匿名共同体变量(在 Linux 中可以),也可以嵌入在结构体中(用的不多)
31. 枚举
枚举是一种创建常量的方法,其他两种方法为:宏定义和
const
修饰的变量。语法:
1
enum 枚举名{枚举量1,枚举量2,...}
作用:枚举量 1,枚举量2 … 为符号常量,为 0、1…
使用:
1
2
3
4
5
6
7
8
9enum colors{red, yellow, blue};
// c 的取值只能为 red(0),yellow(1),blue(2)
colors c = red;
switch(cc){
case red: xxx;break;
case yellow: xxx;break;
case blue: xxx;break;
default: xxx;
}可以显式的设置枚举量的值,未必就是 0,但必须为整数。此外,枚举值可以重复。
可以将整数强制转换成枚举量:
枚举名(整数)
32. 引用
- 引用的作用:函数的形参和返回值。
- 语法:
数据类型 &引用名 = 原变量名
- 注意:
- 引用的数据类型要保持一致,无法强制转换。
- 引用名和原变量共用同一内存和地址。
- 引用必须在定义的时候初始化,之后引用的对象不可改变,
=
只能改变其值,而不能改变其引用的对象。 &
在 C 和 C++ 中都有“指示/取变量地址”的作用,但在 C++ 中又额外多了这个“引用”这个含义。
- 引用在底层的实现逻辑就是“指针常量”,去除了指针的一些缺点,禁止了部分不安全操作。但在高级语言层面上,他就是一个变量的“别名”。
33. 引用用于函数的参数
把函数的形参声明改成引用,实际上就是形参变成了传入的变量的引用,也就是别名,本质上和传指针一样,也叫传地址。
传引用的话:
代码更简洁
不必使用二级指针(用于子函数中改变指针的值)
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// 通过二级指针
void fun1(int** p)
{
*p = new int(3);
cout << "通过二级指针解引用的方式来修改内存后一级指针指向的地址为:" << *p << ";内存中的值为:" << **p << endl;
}
int main()
{
int* p = nullptr;
fun1(&p);
cout << "main 中指针指向的地址为:" << p << ",内存中的值为:" << *p << endl;
}
// 结果为:
// 通过二级指针解引用的方式来修改内存后一级指针指向的地址为:0000022A00192A70;内存中的值为:3
// main 中指针指向的地址为:0000022A00192A70,内存中的值为:3
// 通过引用:
void fun2(int*& p)
{
p = new int(3);
cout << "通过引用的方式来修改内存后一级指针指向的地址为:" << p << ";内存中的值为:" << *p << endl;
}
int main()
{
int* p = nullptr;
fun2(p);
cout << "main 中指针指向的地址为:" << p << ",内存中的值为:" << *p << endl;
}
// 结果为:
// 通过引用的方式来修改内存后一级指针指向的地址为:000002897F432B70;内存中的值为:3
// main 中指针指向的地址为:000002897F432B70,内存中的值为:3可以看出,使用引用后,函数内没有使用二级指针,从而使得代码更容易理解。
引用的参数和
const
。即如果引用的数据类型不匹配,那么当引用为const
时,C++ 将创建临时变量,让引用指向临时变量。
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 错误代码:
void fun3(int& p, string& str)
{
cout << "p is " << p << " and str is " << str << endl;
}
int main()
{
int p = 3;
fun3(3, "起飞");
cout << "main 中指针指向的地址为:" << p << ",内存中的值为:" << *p << endl;
}
// 此时 fun3 报错,两个参数都不对
// 第一个参数错是因为没地址,第二个参数错是因为 C 风格字符串不能转成 string
// 正确代码:
void fun3(const int& p,const string& str)
{
cout << "p is " << p << " and str is " << str << endl;
}
int main()
{
int p = 3;
fun3(3, "起飞");
}增加了
const
修饰形参后,完成了两个功能:常量 “3”(非左值,左值就是可以被引用的数据对象,即有地址,表面意思就是可以放在=
的左边) 被暂时添加了指针变量,有了地址;然后就是数据类型发生转换,注意是可转换的才行。
使用
const
的理由:- 避免修改数据的编程错误。
- 使得函数能够处理非
const
实参。 - 能正确生成并使用临时变量。
34. 各种形参的使用场景
- 如果不需要在函数中修改实参(即除了小实参外能用“引用”用“引用”):
- 实参很小,如 C++ 内置的数据类型或小型结构体,则按值传递。
- 实参是数组,则使用
const
指针,因为这是唯一的选择(没有为数组建立引用的说法)。 - 实参是较大的结构,则使用
const
指针或const
引用。 - 如果实参是类,则使用
const
引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。
- 如果需要在函数中修改实参(注意都不用
const
修饰)(即除了小实参外能用“引用”用“引用”):- 如果实参是内置的数据类型,则使用指针,目的是增加可读性。例如:
fun(&x)
一看就知道是传地址,x 的值将会被修改,而如果使用引用传递,则不知道传过去的值是否会被修改。 - 实参为数组时,只能使用指针。
- 实参是结构体,则使用指针或引用。
- 实参是类,则使用引用。
- 如果实参是内置的数据类型,则使用指针,目的是增加可读性。例如:
35. 函数参数缺省值的特殊情况
- 当函数的定义和实现分开时,缺省值需要在定义时指定。
- 调用函数的时候,如果指定了某个参数的值,那么该参数前面所有的参数都必须指定。
36. 函数重载的细节
使用重载函数时,如果数据类型不匹配,C++ 将尝试使用类型转换与形参进行匹配,如果转换后有多个函数能匹配上,编译器将报错。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13void fun(int i)
{
cout << "i is " << i << endl;
}
void fun(long i)
{
cout << "i is " << i << endl;
}
void main()
{
short i = 1;
fun(i);
}用以上的代码执行就会报错,“对重载函数的调用不明确”。
引用可以作为函数重载的条件(即相同数据类型下,是否带引用可以区分),但是调用重载函数的时候,编译器会将形参类型的本身和类型引用视为同一特征。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void fun(int &i)
{
cout << "i is " << i << endl;
}
void fun(int i)
{
cout << "i is " << i << endl;
}
void main()
{
int i = 1;
// 报错
fun(i);
// 不报错
fun(1);
}如果重载函数有默认参数,调用函数时,可能导致匹配失败。
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void fun(int i)
{
cout << "i is " << i << endl;
}
void fun(int i, int j = 2)
{
cout << "i is " << i << "; j is" << j << endl;
}
void main()
{
int i = 1;
// 报错
fun(i);
}因为重载函数有缺省值,不知道调用哪个。
const
和返回值数据类型不能作为函数重载的特征。此外,重载函数,虽然表面上看是同一个函数,但是编译时,编译器会对每个函数名进行加密,此时重载函数名之间名字就会不同(本质上就变成了不同名的函数)。
37. 内联函数
- 用法:
inline
关键字放在函数的最前面。 - 作用:当
main()
多次调用同一个函数时,内联函数就相当于把函数内容/块嵌入到main()
当中,这样就不需要多次进行函数的跳转,即用空间来换时间。 - 注意点:
- 如果内联函数过大,编译器可能不会将其作为内联函数,一般内联函数不超过 10 行。
- 内联函数不能递归,逻辑上行不通。
- 不提倡内联函数中包含循环和选择体,因为循环和选择体占用的内存过多,有很大的空间浪费,编译器可能不会将其作为内联函数。
38. 类
前言:结构体内可以定义函数,然后结构体内的函数可以使用结构体内的成员变量,而不用在
main()
调用的时候额外赋值。上述的结构实际上就是类的前身,在结构体中添加
public
等关键字,将struct
改成class
,基本上就是类了。一些细节:
- 可以在类内定义方法,在类外实现方法,语法:
返回值 类名::方法名(形参){}
- 对象的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同。
- 可以在类内定义方法,在类外实现方法,语法:
39. 类的访问权限:public
、private
、protected
- 在类中,成员缺省为私有。
40. 类的一些使用思想
- 类指针的用法与结构体指针用法相同。
- 类的成员可以是任意数据类型(类中枚举)。
- 可以为类的成员指定缺省值(C11 标准)。
- 类可以创建对象数组,就像结构体数组一样。
- 对象可以作为实参传递给函数,一般传引用。
- 可以用
new
动态创建对象,用delete
释放对象。 - 对象一般不用
memset()
清空成员变量(和结构体指针一样),可以写一个专用于清空成员变量的成员函数。 - 对类和对象用
sizeof()
运算意义不大,一般不用。 - 用结构体描述纯粹的数据,用类描述对象。纯粹的数据就是 C++ 内置的数据类型和 C 风格的字符串,没有函数和类,数据结构中比较常见,此外有些库函数是 C 编写的,那么就只有结构体,不用不行。
- 在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了
inline
限定符,也是内联函数。 - 为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如
m_name
或name_
。 - 类的分文件编写。
41. 构造函数和析构函数
构造函数:
- 访问权限必须为
public
。 - 函数名必须和类名相同。
- 没有返回值
- 可以有参数、重载和默认参数。
- 不可手工调用。
- 访问权限必须为
析构函数:
- 语法:
~类名(){...}
- 访问权限必须是
public
。 ~
必须加,且没有返回值。- 无参且不能重载。
- 销毁对象前只会自动调用一次,但是可以手工调用。
- 语法:
细节:
在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象,即构造函数无法被调用;匿名对象创建后直接被析构掉。
隐式创建对象:
1
2Object object;
Object object("...", ...);显式创建对象:
1
2Object object = Object();
Object object = Object("...", ...);一般隐式用的多,注意和 Java 的语法做区分。
接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)。例如:
1
2
3
4
5
6
7class Object{
int m_num;
Object(int num){
m_num = num;
}
}
Object object = 1;以下的两种声明方法有本质区别:
1
2
3
4
5
6
7
8
9// 第一种
// 显式创建对象。
Object object = Object(1);
// 第二种
// 创建对象
Object object;
// 创建匿名对象,然后给现有的对象赋值,即创建了两个对象
object = Object(1);用
new/delete
创建/销毁对象,也会调用构造/析构函数。不建议在构造/析构函数中写太多的代码,可以调用成员函数。
除了初始化,不建议让构造函数做太多工作(只能成功不会失败)。
C11 支持使用统一初始化列表:
1
Object object = {参数1, 参数2}
如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构成员类。
42. 拷贝构造函数
用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数。
如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数,它的功能是把已存在对象的成员变量赋值给新对象的成员变量。
语法:
1
2类名 新对象名(已存在的对象名);
类名 新对象名 = 已存在的对象名;拷贝构造函数的语法:
类名(const 类名 &对象名, ...){......}
注意:一定要有类本身的常引用。注意事项:
- 访问权限必须是
public
。 - 函数名必须和类名相同。
- 没有返回值。
- 如果有定义了拷贝构造函数,那编译器不提供拷贝构造函数。
- 以值传递(不是地址传递)的方式调用函数时,如果实参为对象,会调用拷贝构造函数。
- 函数以值的方式返回对象时,可能会调用拷贝构造函数(VS 会调用,Linux 不会,g++ 编译器做了优化,形参和实参共用同一个地址)。
- 如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数(即不带其他参数的),编译器也会提供默认的拷贝构造函数。
- 访问权限必须是
43. 浅拷贝和深拷贝
浅拷贝就是拷贝后两个指针指向同一个内存地址,当有一个类执行析构函数后,空间和指针释放,但是另一个指针就变成了野指针,此时另一个类再执行析构函数后,就会对野指针释放空间,导致程序崩溃。
出现浅拷贝的常见情况就是,类中定义了一个指针,令一个类通过默认的拷贝构造函数进行拷贝,但是该拷贝构造函数中,将一个指针赋值为同一类型的另一个有效指针。这就导致了两个指针共同指向了同一片地址。
此外,既然共用同一片地址,那么对其中一个类的指针指向的空间的值进行修改,那么会影响另一个类的指针。深拷贝就是两个指针指向不同的内存地址,但是值相同,这样就能解决浅拷贝的问题。注意:编译器提供的默认拷贝构造函数是浅拷贝函数。
要想变成深拷贝,那么在拷贝构造函数中要对指针成员进行特殊处理:
1
2
3
4
5
6
7
8// 当指针指向的空间存储的是简易的数据类型时,直接赋值即可
// 创建新空间
m_ptr = new int;
// 直接赋值
*m_ptr = *other.m_ptr;
// 当指针指向的空间存储的是复杂的数据类型时,使用 memcpy 函数
memcpy(m_ptr, other.m_ptr, sizeof(m_ptr));
44. 初始化列表(用的多)
- 其嵌入在构造函数中,语法:
类名(形参列表):成员一(值一), 成员二(值二)...
- 注意点:
- 如果成员已经在初始化列表中,则其不应该再在构造函数中被赋值,否则会被覆盖。
- 括号中的值,可以是具体的值,也可以是构造函数的形参名,还可以是表达式,这个用的最多。
- 初始化列表与赋值有着本质的区别,如果一个类中有成员是类,那么使用初始化列表时,调用的是成员类的拷贝构造函数(即深拷贝后,一步操作完成空间创建和同值);而在构造中直接赋值的话,就是先创建成员类的对象,同时创建的时候调用成员类的构造函数,然后再赋值,相当于两步操作,效率没有第一个快。
- 如果成员是常量和引用,必须使用初始化列表而不能使用任何构造函数,因为常量和引用只能在定义的时候初始化。
- 如果有成员是没有默认构造函数的类,则必须使用初始化列表。(不多见)
- 拷贝构造函数也可以有初始化列表,但很少用,偶尔会用到。
- 类的成员变量可以不出现在初始化列表中,此时编译器会采用默认的初始化方法。
45. 用 const
修饰成员函数
- 在类的成员函数后面加
const
关键字,表示在成员函数中保证不会修改调用对象的成员变量。 - 实际开发中,如果成员函数不会对成员变量进行修改,那么就要加
const
进行约束。例如:
void fun() const{ ... }
- 当成员变量被
mutable
修饰时,其可以在const
修饰的函数中被修改。 const
成员函数不能调用非const
成员函数。- 同理,
const
对象只能调用const
修饰的成员函数,不能调用非const
修饰的成员函数。 - 而没有被
const
修饰的没有调用限制 - 为什么要保护类的成员变量不被修改?为什么用
const
保护了成员变量,还要再定义一个mutable
关键字来突破const
的封锁线?到底有没有必要使用const
和mutable
这两个关键字?
保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const
关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable
则是为了能突破const
的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const
和mutable
关键字当然没有错,**const
和mutable
关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。**
46. this 指针
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29#include <iostream>
using namespace std;
class obj {
int num;
public:
obj(int num) {
this->num = num;
}
/*
返回最大值
注意这里一定要加两个 const。因为被 const 修饰的成员对象只能调用 const 成员函数
*/
const obj& pk(const obj& obj2) const{
return obj2.num > this->num ? obj2 : *this;
}
void show() const {
cout << "obj 的数字为 " << num << endl;
}
};
int main()
{
obj obj1(1), obj2(2), obj3(3);
// 注意这里体现的 C++ 编程思想
const obj& obj0 = obj1.pk(obj2).pk(obj3);
obj0.show();
}特别注意,
this
是指针,因此调用其成员要用->
。
47. 静态成员
被
static
修饰的成员必须放在程序的全局区单独声明,例如:1
2
3
4
5
6
7
8
9
10class obj{
...;
static xxx 成员名
};
// 不初始化默认为 0
类型 xxx 类名 obj::静态成员名 = ...;
...;
int main(){
...
}静态成员可以使用
类名::成员
进行访问,无需创建对象。::
为范围解析运算符。静态成员的本质:它和类的对象独立开,不属于任何一个对象(因此静态成员函数没有
this
指针)。静态成员函数只能访问静态成员,不能访问非静态成员。
私有静态成员在类外无法访问。
48. 简单对象模型
C++ 用类描述抽象数据类型 ADT,在类中定义了数据和函数,把数据和函数关联起来(而 C 语言数据和处理数据的操作-函数是分开的)。
对象中维护了多个指针表,表中放了成员与地址的对应关系。对象内存的大小包括:
- 所有非静态数据成员的大小,静态成员变量属于类,不计算在对象的大小之内;
- 由内存对齐而填补的内存大小;
- 为了支持 virtual 成员而产生的额外负担。
成员函数是分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算在对象大小之内。
非静态成员变量之间的地址在一起,静态成员变量(属于类)与全局变量的地址在一起,成员函数的地址和普通函数的地址在一起。成员变量的地址和成员函数的地址并不在一起。
由于 C 本身是没有“成员”这个概念,所以在 C++ 中,要想取得非静态成员对象,就要通过指针表,即要用到
this
指针(默认是省略的)。用空指针可以调用没有用到
this
指针的非静态成员函数,也就是没有用到非静态成员的函数,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class clazz
{
public:
int i;
void show()
{
// 这个可以执行
cout << "数字" << endl;
// 当有这个不可以执行
cout << "数字 i" << i << endl;
}
}
int main()
{
clazz* c = nullptr;
c->show();
}崩溃的本质,实际上是访问了空指针
this->i
,从而报空指针异常。对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增加一个 1 字节的占位成员。
49. 使用类(友元类、运算符重载、自动类型转换和转换函数)
1. 友元类
友元类提供了另一访问类的私有成员的方案。友元有三种:
友元全局函数
被指定的函数可以直接访问另一个类的私有成员。1
2
3
4class clazz
{
friend 函数返回值 函数名称();
}此后这个函数就可以访问这个类的私有成员。如果出现多个函数多层调用,那么所有的函数都要是友元函数。
友元类
在友元类所有成员函数中,都可以访问另一个类的所有成员。1
2
3
4class clazz1
{
friend class clazz2;
}- 友元关系不能被继承。
- 友元关系是单向的,不具备交换性。
友元成员函数
针对友元类的限制,友元类某成员函数可以访问另一个类的所有成员。原理和友元全局函数一样,但要注意声明和定义的顺序。
顺序:- 指定友元类的类的前置声明
- 友元类定义(内部函数声明)(只是说是友元类,但实际上并不是友元类,而是其中的方法是友元的)
- 指定友元类的类的定义(包括指定友元类中的方法)
- 友元方法定义
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 1. 声明类(因为 clazz2 内成员函数的定义用到了 clazz1)
class clazz1;
// 2. 定义友元类(非真正的友元类)
class clazz2
{
public:
void func1(const clazz1& clz1);
void func2(const clazz1& clz1);
};
// 3. 指定友元类的类的定义
class clazz1
{
// clazz2::func1 是 clazz1 的友元函数
friend void clazz2::func1(const clazz1& clz1);
private:
int num = 1;
public:
string call = "clazz1.show() is called";
};
// 4. 友元方法定义
void clazz2::func1(const clazz1& clz1) { cout << "num is " << clz1.num << endl; }
// 注意 clazz1.call 是 public
void clazz2::func2(const clazz1& clz1) { cout << "call is " << clz1.call << endl; }
int main()
{
clazz1 clz1;
clazz2 clz2;
clz2.func1(clz1);
clz2.func2(clz1);
}
2. 运算符重载
- 好处:简化代码、增加可读性以及美观
- 语法:
返回值 operator运算符(参数列表);
运算符重载函数的返回值
类型要与运算符本身的含义一致。 - 本质:函数的调用
- 一些细节:
- 运算符重载分为成员函数版本和非成员函数版本,对于非成员函数版本,需要保证“形参”个数与运算符的“操作数”个数相同;
对于成员函数版本,其参数就可以少一个,函数体内涉及运算可以用this
来代替了。即成员函数版本的重载运算符函数的形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象。
此外,如果同时重载了非成员函数和成员函数版本,会出现二义性。 - 返回自定义数据类型的引用可以让多个运算符表达式串联起来(就好像 1 + 1 + 1…)。(不要返回局部变量的引用)
- 重载函数参数列表中的顺序决定了操作数的位置。即和数学常识中的
+
不同,如果+
的左右颠倒,那么本质上是调用两个不同的函数。 - 重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
- 编程规范:如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
- 重载函数不能违背运算符原来的含义和优先级。
- 不能创建新的运算符。
- 以下运算符不可重载:
sizeof
.
成员运算符.*
成员指针运算符::
作用域解析运算符?:
条件运算符typeid
一个 RTTI 运算符const_cast
强制类型转换运算符dynamic_cast
强制类型转换运算符reinterpret_cast
强制类型转换运算符static_cast
强制类型转换运算符
- 以下运算符只能通过成员函数进行重载:
=
赋值运算符()
函数调用运算符[]
下标运算符->
通过指针访问类成员运算符
- 运算符重载分为成员函数版本和非成员函数版本,对于非成员函数版本,需要保证“形参”个数与运算符的“操作数”个数相同;
1. 关系、左移运算符重载
重载关系运算符(
==
、!=
、>
、>=
、<
、<=
)用于比较两个自定义数据类型的大小。可以使用非成员函数和成员函数两种版本,建议采用成员函数版本。重载左移运算符(
<<
)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
常见的<<
的使用就是cout
。但想让cout <<
输出自定义对象,就需要重载运算符<<
。
代码(思路来源自cout
的定义):1
2
3
4
5
6// 别忘了友元
ostream& operator<<(ostream& cout, const 自定义对象)
{
...;
return cout;
}只能使用非成员函数版本,如果用成员函数,那么要输入的变量在
cout
左边,这样不符合书写习惯。
如果要输出对象的私有成员,可以配合友元一起使用。如果定义了公有的 getter,那么不用友元也可以。
2. 重载下标运算符 []
- 如果对象中有数组,重载下标运算符
[]
,操作对象中的数组将像操作普通数组一样方便,常见的例子就是string
类,对[]
进行了重载,使得string
可以通过string[]
来获取一个字符。 - 与左移运算符
<<
相反。下标运算符必须以成员函数的形式进行重载。 - 语法(注意一定是返回引用):
返回值类型 &operator[](参数,一般是下标);
const 返回值类型 &operator[](参数) const;
前后都增加const
修饰时,调用对象[i]
时就不能修改数组元素了,第一种方法可以修改数组元素(一定要注意返回值是引用返回)- 在实际开发中,应该同时提供以上两种形式,这样做是为了适应
const
对象,因为通过const
常对象只能调用const
成员函数,如果不提供第二种形式,那么常对象(为了使得这种对象内的属性不会修改)将无法访问const
对象的任何数组元素。
- 在重载函数中,可以对下标做合法性检查,防止数组越界。
3. 重载赋值运算符
- 默认赋值函数, 对成员变量进行浅拷贝。
- 对象的赋值运算是用一个已经存在的对象,给另一个已经存在的对象赋值。如果类的定义中没有重载赋值函数,编译器就会提供一个默认赋值函数。如果类中重载了赋值函数,编译器将不提供默认赋值函数。
- 重载赋值函数的语法:
类名& operator=(const 类名& 源对象)
- 注意事项:
- 编译器提供的默认赋值函数,是浅拷贝。如果对象中不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝。
注意,深拷贝下,对象内的成员指针需要判断=
右边,也就是源对象的成员指针是否为空指针,为空的话那目标对象的成员指针也得释放空间且置为空。如果不为空,还需要判断=
左边,也就是被赋值的目标对象的成员指针是否为空,为空的话还得分配内存,有内存后才开始用memcpy()
复制数据。 - 赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
- 编译器提供的默认赋值函数,是浅拷贝。如果对象中不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝。
4. 重载 new
和 delete
运算符
重载
new
和delete
运算符的目是为了自定义内存分配的细节。(内存池:快速分配和归还,无碎片)在 C++ 中,使用
new
时,编译器做了两件事情:- 调用标准库函数
operator new()
分配内存; - 调用构造函数初始化内存;
- 调用标准库函数
使用
delete
时,也做了两件事情:- 调用析构函数;
- 调用标准库函数
operator delete()
释放内存。
构造函数和析构函数由编译器调用,程序员无法控制。但是,可以重载内存分配函数
operator new()
和释放函数operator delete()
。示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 参数必须是 size_t(unsigned long long),返回值必须是 void*,表示一块与数据类型无关的内存空间
void* operator new(size_t size)
{
cout << "调用了全局重载的new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
// 返回申请到的起始地址
return ptr;
}
void operator delete(void* ptr) // 参数必须是 void *,返回值必须是 void。
{
cout << "调用了全局重载的delete。\n";
if (ptr == 0) return; // 对空指针 delete 是安全的。
free(ptr); // 释放内存。
}内存分配函数
operator new()
和释放函数operator delete()
可以定义为一个类的成员函数。当创建对象时,就会调用成员内存分配/释放函数。为一个类重载
new
和delete
时,尽管不必显式地使用static
,但实际上仍在创建static
成员函数。即函数内不能访问类中的非静态成员。new[]
和delete[]
也可以重载,主要用于动态分配和释放数组。原理和语法和重载new
和delete
一样,但用的很少。重载
new
和delete
的主要目的——实现内存池。内存池是池化技术中的一种形式。通常我们在编写程序的时候回使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次申请内存和释放内存的时候,都需要和操作系统的系统调用打交道,从堆中分配所需的内存。如果这样的操作太过频繁,就会找成大量的内存碎片进而降低内存的分配性能,甚至出现内存分配失败的情况。
而内存池就是为了解决这个问题而产生的一种技术。从内存分配的概念上看,内存申请无非就是向内存分配方索要一个指针,当向操作系统申请内存时,操作系统需要进行复杂的内存管理调度之后,才能正确的分配出一个相应的指针。而这个分配的过程中,我们还面临着分配失败的风险。
所以,每一次进行内存分配,就会消耗一次分配内存的时间,设这个时间为 T,那么进行 n 次分配总共消耗的时间就是nT
;如果我们一开始就确定好我们可能需要多少内存,那么在最初的时候就分配好这样的一块内存区域,当我们需要内存的时候,直接从这块已经分配好的内存中使用即可,那么总共需要的分配时间仅仅只有 T。当 n 越大时,节约的时间就越多。即提高了分配和归还的速度实现的核心思想就是申请一段连续的空间,根据要申请的对象大小,分成 n 块,每块的大小是 “对象的大小” + 1,每块第一位作为标志位,用来判断这块内存是否被分配了。
课件的 Demo 程序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// 超女类 CGirl。根据成员来看,这个类的对象大小应该为 9 子节
class CGirl
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
static char* m_pool; // 内存池的起始地址。
// 初始化内存池的函数。
static bool initpool()
{
m_pool = (char*)malloc(18); // 向系统申请 18 字节的内存。
if (m_pool == 0) return false; // 如果申请内存失败,返回false。
memset(m_pool, 0, 18); // 把内存池中的内容初始化为0。
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
// 释放内存池
static void freepool()
{
if (m_pool == 0) return; // 如果内存池为空,不需要释放,直接返回。
free(m_pool); // 把内存池归还给系统。
cout << "内存池已释放。\n";
}
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
// 参数必须是 size_t(unsigned long long),返回值必须是 void*。
void* operator new(size_t size)
{
// 判断第一个位置是否空闲。
if (m_pool[0] == 0)
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; // 把第一个位置标记为已分配。
return m_pool + 1; // 返回第一个用于存放对象的址。
}
// 判断第二个位置是否空闲。
if (m_pool[9] == 0)
{
cout << "分配了第二块内存:" << (void*)(m_pool + 10) << endl;
m_pool[9] = 1; // 把第二个位置标记为已分配。
return m_pool + 10; // 返回第二个用于存放对象的址。
}
// 如果以上两个位置都不可用,那就直接系统申请内存。
void* ptr = malloc(size); // 申请内存。
cout << "直接通过系统申请到的内存的地址是:" << ptr << endl;
return ptr;
}
// 参数必须是void *,返回值必须是void。
void operator delete(void* ptr)
{
// 如果传进来的地址为空,直接返回。
if (ptr == 0) return;
// 如果传进来的地址是内存池的第一个位置。
if (ptr == m_pool + 1)
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; // 把第一个位置标记为空闲。
return;
}
// 如果传进来的地址是内存池的第二个位置。
if (ptr == m_pool + 10)
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; // 把第二个位置标记为空闲。
return;
}
// 如果传进来的地址不属于内存池,把它归还给系统。
free(ptr); // 释放内存。
}
};
char* CGirl::m_pool = 0; // 初始化内存池的指针。
int main()
{
// 初始化内存池。
if (CGirl::initpool() == false) { cout << "初始化内存池失败。\n"; return -1; }
CGirl* p1 = new CGirl(3, 8); // 将使用内存池的第一个位置。
cout << "p1的地址是:" << p1 << ",编号:" << p1->m_bh << ",胸围:" << p1->m_xw << endl;
CGirl* p2 = new CGirl(4, 7); // 将使用内存池的第二个位置。
cout << "p2的地址是:" << p2 << ",编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
CGirl* p3 = new CGirl(6, 9); // 将使用系统的内存。
cout << "p3的地址是:" << p3 << ",编号:" << p3->m_bh << ",胸围:" << p3->m_xw << endl;
delete p1; // 将释放内存池的第一个位置。
CGirl* p4 = new CGirl(5, 3); // 将使用内存池的第一个位置。
cout << "p4的地址是:" << p4 << ",编号:" << p4->m_bh << ",胸围:" << p4->m_xw << endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CGirl::freepool(); // 释放内存池。
}
5. 重载括号运算符
括号运算符
()
也可以重载,对象名可以当成函数来使用(函数对象、仿函数)。语法:
返回值类型 operator()(参数列表)
。注意点:
括号运算符必须以成员函数的形式进行重载。
括号运算符重载函数具备普通函数全部的特征(没有返回值类型和参数数量限制)。
如果类的对象/函数对象与全局函数同名,按作用域规则选择调用的函数。一般是对象创建后优先调用类中函数对象,如果想确保调用的是全局函数,那么就要使用
::
关键字。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void show(string str)
{
cout << "全局函数 " << str << endl;
}
class clazz
{
public:
void operator()(string str)
{
cout << "重载函数/函数对象 " << str << endl;
}
};
int main()
{
clazz show;
// 调用全局函数
::show("你好");
// 调用重载函数
show("你好");
}
函数对象/重载
()
运算符的用途:- 表面像函数,部分场景中可以代替函数,在 STL 中得到广泛的应用;
- 函数对象本质是类,可以用成员变量存放更多的信息;
- 函数对象有自己的数据类型;
- 可以提供继承体系。
6. 重载一元运算符
一元运算符通常出现在它们所操作的对象的左边,这点尤为注意,因此默认重载后,运算符在对象的左边(之前都是右边);可重载的一元运算符有:
++
和--
自增和自减运算符!
逻辑非&
取地址~
二进制反码*
解引用+
一元加-
一元求反
一元运算符的重载,成员函数版本和全局函数版本都可以。
C++ 规定,重载
++
或--
时,如果重载函数有一个int
形参,编译器处理后置表达式时将调用这个重载函数。1
2
3
4
5
6
7// 成员函数版
Clazz &operator++(); // ++前置
Clazz operator++(int); // 后置++
// 全局函数版
Clazz &operator++(Clazz &); // ++前置
Clazz operator++(Clazz &,int); // 后置++将返回值置为
Clazz
可以实现连续多个前置自增/自减,注意后置自增/自减不能连续多个。还需要注意的是:
后置自增/自减,函数的返回值不能为引用,因为临时对象在函数调用后释放,不能被引用;同时需要一个临时对象来保存自增/自减之前的状态。1
2
3
4
5
6
7Clazz operator++(int)
{
Clazz temp = *this;
// 做 ++ 运算
...;
return temp;
}这样的目的就是为了实现后置的效果——先做修改,然后把修改前的状态返回。这样在使用后置自增/自减做运算时,满足“先运算,后自增/自减”的效果。
3. 自动类型转换
对于自定义数据类型的转换,要使用将某种数据类型转换为类的类型,例如将字符数组转换成
string
。即如果某种类型与类相关,从某种类型转换为类类型是有意义的。在 C++ 中,将只有一个参数的构造函数用作自动类型转换函数,它是自动进行的,不需要显式的转换。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class clazz
{
public:
int m_num;
string m_str;
/*
* 默认的构造函数
*/
clazz()
{
m_num = 0;
m_str.clear();
}
/*
* 对 m_num 赋值的构造函数
*/
clazz(int num)
{
m_num = num;
m_str.clear();
}
void show()
{
cout << "m_num is " << m_num << ", m_str is " << m_str << endl;
}
};
int main()
{
clazz clz(1); // 常规写法
//clazz clz = clazz(1); // 显式转换
//clazz clz = 1; // 隐式转换
//clazz clz; // 创建对象
//clz = 8; // 用 clazz(8) 创建匿名临时对象,然后再赋值
clz.show();
}注意事项:
一个类可以有多个转换函数,也就是多个构造函数。
多个参数的构造函数,除第一个参数外,如果其它参数都有缺省值,也可以作为转换函数。
隐式转换的一些场景:
基础类型直接赋值给类,例如:
1
clazz clz = 1;
通过匿名临时对象赋值也是。
1
2clazz clz;
clz = 8;将基础类型值传递给接受“类的实例化对象”参数的函数时:
1
2
3
4
5
6
7
8void show(clazz clz)
{
clz.show()
}
int main()
{
show(1);
}返回值为“类的实例化对象”,但在方法中试图返回基础类型值。
在上述任意一种情况下,使用可转换为基础类型的内置类型时。
1
2
3
4
5
6// 假设 clazz 有构造函数:clazz(int num)
clazz func()
{
char num = 1;
return num;
}因为
char
可以向上转换成int
如果自动类型转换有二义性,编译将报错。
将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。
explicit
关键字用于关闭这种自动特性,但仍允许显式转换。
实际开发中如果强调的是构造,建议使用explicit
,如果强调的是类型转换,则不使用explicit
。
4. 转换函数
自动类型转换中的构造函数只用于基本数据类型到类类型的转换,如果要进行相反的转换,就需要使用特殊的运算符函数 - 转换函数。
转换函数必须是类的成员函数;不能指定返回值类型;不能有参数。
语法格式:
operator 数据类型();
例子如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class clazz
{
public:
int m_num;
string m_str;
/*
* 默认构造函数
*/
explicit clazz()
{
m_num = 0;
m_str.clear();
}
/*
* int 型转换函数
*/
operator int()
{
return m_num;
}
/*
* string 型转换函数
*/
operator string()
{
return m_str;
}
};
int main()
{
clazz clz;
int num = clz; // 隐式转换
int num = int(clz); // 显式转换
cout << "num is " << num << endl;
}如果隐式转换存在二义性,编译器将报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class clazz
{
public:
int m_num;
string m_str;
short m_numShort;
/*
* 默认构造函数
*/
explicit clazz()
{
m_num = 0;
m_numShort = 0;
m_str.clear();
}
/*
* int 型转换函数
*/
operator int()
{
return m_num;
}
/*
* short 型转换函数
*/
operator short()
{
return m_numShort;
}
/*
* string 型转换函数
*/
operator string()
{
return m_str;
}
};
int main()
{
clazz clz;
// 报错:应用了多个从 "clazz" 到 "double" 的转换函数
// 注意,虽然没有 double 的转换函数,但编译器也会尝试其他的转换函数
double num = clz;
cout << "num is " << num << endl;
}在 C98 中,关键字
explicit
不能用于转换函数,但 C11 消除了这种限制,可以将转换函数声明为显式的(和自动类型转换函数一样)。还有一种方法是:用一个功能相同的普通成员函数代替转换函数,普通成员函数只有被调用时才会执行。例如上述的例子:1
2
3
4
5
6
7
8
9int to_int()
{
return m_num;
}
int main()
{
clazz clz;
int ii = clz.to_int();
}应谨慎的使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的成员函数。毕竟转换函数不见得能让程序优雅和简化,而且隐式转换又容易出 bug。
最常见的例子就是,C++ 中string str = "string"
,官方提供了由char *
转换成string
的转换函数,但是const char* ptr = str
就会报错,官方没有提供string
转换成char*
的转换函数。因此官方给了:const char* ptr = str.c_str();
。提供了这一成员函数。
50. 继承
1. 继承的概念
- 语法:
class 派生类名:[继承方式]基类名
2. 继承方式
类成员的访问权限由高到低依次为:
public --> protected --> private
,public
成员在类外可以访问,private
成员只能在类的成员函数中访问。如果不考虑继承关系,
protected
成员和private
成员一样,类外不能访问。但是,当存在继承关系时,protected
和private
就不一样了。基类中的protected
成员可以在派生类中访问,而基类中的private
成员不能在派生类中访问。继承方式有三种:
public
(公有的)、protected
(受保护的)和private
(私有的)。它是可选的,如果不写,那么默认为private
。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。注意在 Java 中,是单继承模式:一个子类只能继承一个父类;而 C++ 中是一个子类可以继承多个父类。
继承方式的不同,会导致成员在基类和派生类中的等级发生变化:
继承后的访问等级不会高于继承方式,高于的会降级(注意
public
为最高级)。由于
private
和protected
继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以,在实际开发中,一般使用public
。在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。Java 中体现的例子就是 setter 和 getter 方法。
使用
using
关键字可以改变基类成员在派生类中的访问权限。using
只能改变基类中public
和protected
成员的访问权限,不能改变private
成员的访问权限,因为基类中的private
成员在派生类中是不可见的,根本不能使用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class Father
{
public:
int m_public = 10;
protected:
int m_protected = 20;
private:
int m_private = 30;
};
class Child : public Father
{
public:
// 将 protected 权限改成 public
using Father::m_protected;
private:
// 将 public 权限改成 private
using Father::m_public;
};
int main()
{
Child child;
cout << "protected 成员为" << child.m_protected <<endl;
}
3. 继承的对象模型
- 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。如果手工调用派生类的析构函数,也会调用基类的析构函数。 - 创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,
this
指针相同的。
创建派生类对象时,先初始化基类对象,再初始化派生类对象。 - 对派生类对象用
sizeof()
得到的是基类所有成员(包括私有成员)+ 派生类对象所有成员的大小。 - 在 C++ 中,不同继承方式的访问权限只是语法上的处理。因此对派生类对象用
memset()
会清空基类私有成员。
因此menset()
一般不用于类,可能会造成意外的结果,这就是其中一种情况。
此外,既然继承方式是语法上的限制,那么可以用指针访问到基类中的私有成员(内存对齐)以突破语法限制(奇巧淫技)。
4. 如何构造基类
可以用初始化列表指明要使用的基类非默认构造函数。
1
2
3
4派生类(int param1, int param2):基类(param1), 派生类的方法(param2)
{
cout<< "调用了派生类的构造函数,且调用了基类的非默认构造函数" << endl;
}同样的,拷基类的拷贝构造函数也可以:
1
2
3
4派生类(const 基类& a, int param2):基类(a), 派生类的方法(param2)
{
cout<< "调用了派生类的构造函数,且调用了基类的非默认构造函数" << endl;
}
5. 名字遮蔽(表象)与类作用域(本质)
名字遮蔽:如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的。(用子不用父)。
注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。注意是所有,包括基类的重载函数。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Father
{
public:
void func()
{
cout << "调用了 Father 的 func()" << endl;
}
void func(int a)
{
cout << "调用了 Father 的 func(" << a << ")" << endl;
}
}
class Child: public Father
{
public:
void func()
{
cout << "调用了 Child 的 func()" << endl;
}
}
int main()
{
Child child;
// 此时这里会报错
child.func(1);
}在成员名前面加
.
、类名和域解析符(::
)可以访问对象的成员(在类中的效果就和this
的效果一样)。如果不存在继承关系,类名和域解析符可以省略不写。当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域中已经找到,就不会在基类作用域中继续查找(这也解释了“为什么派生类会遮蔽基类的所有同名函数);如果没有找到,则继续在基类作用域中查找。
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Father
{
public:
int a = 1;
}
class Child: public Father
{
public:
int a = 2;
}
int main()
{
Child child;
// 此时输出 1
cout << child.Father::a << endl;
}
6. 继承的特殊关系
可以把派生类对象赋值给基类对象(包括派生类中的私有成员也会覆盖基类的),但是,此时的基类对象会舍弃非基类的成员(即派生类自己额外定义的成员会被舍弃)。本质上是调用了基类的赋值函数。
基类指针可以在不进行显式转换的情况下指向派生类对象,即
1
2
3
4
5
6int main()
{
Child child;
// 基类指针指向派生类对象
Father* Father = &child;
}在 C++ 中,数据类型决定操作数据的方法,即不管基类指针指向哪个派生类对象,其都必定按照基类中的方法来操作数据(这和 Java 完全相反),同时派生类中额外的东西,基类指针访问不到。
还需要注意的是,基类和派生类对象的内存模型是一样的,这表示用基类指针操作派生类是绝对安全的,如果内存模型不一致,程序就会崩溃。基类引用可以在不进行显式转换的情况下引用派生类对象。毕竟引用的本质就是“指针常量”的伪装,还是指针。
一些注意事项:
- 可以用派生类构造基类。
- 如果函数的形参是基类,那么实参就可以用派生类。即派生类对象能用于赋值函数的参数,那就可以用于其他函数的参数。
- C++ 要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。
7. 类继承:多继承与虚继承(这两个应用场景都非常少)
语法格式:
1
2
3
4class 派生类名 : [继承方式1] 基类名1, [继承方式2] 基类名2,......
{
派生类新增加的成员
};菱形继承:
此时会产生“二义性”和“数据冗余”问题。
因此,为了解决菱形继承问题,产生了虚继承。
虚继承语法:在继承方式前加
virtual
关键字,此时二义性的变量会变成同一个变量(注意都要添加virtual
关键字)。继承关系内存模型:
右边有两份 A 的空间。
不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要使用多继承。
51. 类的多态
1. 多态的基本概念
- 基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
但如果在基类的成员函数前加virtual
关键字,把它声明为虚函数,基类指针就可以调用派生类中同名的成员函数,通过派生类中同名的成员函数,就可以访问派生对象的成员变量。 - 有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据(同名的才行,派生类多出来的用不了),基类指针表现出了多种形式,这种现象称为多态。(Java 中接口变量被赋值为实现类,也是多态的表现,即向上转型。换句话说,父类的引用,指向父类自己就调用父类,指向子类调用的就是重写的方法。对于 Java 而言,默认就是虚函数)。
- 注意事项:
- 只需要在基类的函数声明中加上
virtual
关键字,函数定义时不能加。 - 在派生类中重定义虚函数时,函数特征要相同(没有
virtual
关键字,其他一律相同)。 - 当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
- 在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符(因为默认会调用派生类的函数)。
- 开发建议:如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先普通效率更高;其次,指出不要重新定义该函数。设置为虚函数是为了告诉继承的类尽量重新定义函数。
- 只需要在基类的函数声明中加上
2. 多态的应用场景
一般认为,一些功能只要继承基类的普通派生类就可以达成业务需求,但是如果使用多态(
virtual
),那么代码量将会得到精简。示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class Hero // 英雄基类
{
public:
int viability; // 生存能力。
int attack; // 攻击伤害。
virtual void skill1() { cout << "英雄释放了一技能。\n"; }
virtual void skill2() { cout << "英雄释放了二技能。\n"; }
virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};
class XS :public Hero // 西施派生类
{
public:
void skill1() { cout << "西施释放了一技能。\n"; }
void skill2() { cout << "西施释放了二技能。\n"; }
void uskill() { cout << "西施释放了大招。\n"; }
};
class HX :public Hero // 韩信派生类
{
public:
void skill1() { cout << "韩信释放了一技能。\n"; }
void skill2() { cout << "韩信释放了二技能。\n"; }
void uskill() { cout << "韩信释放了大招。\n"; }
};
class LB :public Hero // 李白派生类
{
public:
void skill1() { cout << "李白释放了一技能。\n"; }
void skill2() { cout << "李白释放了二技能。\n"; }
void uskill() { cout << "李白释放了大招。\n"; }
};
int main()
{
// 根据用户选择的英雄,施展一技能、二技能和大招。
int id = 0; // 英雄的id。
cout << "请输入英雄(1-西施;2-韩信;3-李白。):" <<endl;
cin >> id;
// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数。
Hero* ptr = nullptr;
if (id == 1) { // 1-西施
ptr=new XS;
}
else if (id == 2) { // 2-韩信
ptr = new HX;
}
else if (id == 3) { // 3-李白
ptr = new LB;
}
if (ptr != nullptr) {
ptr->skill1();
ptr->skill2();
ptr->uskill();
delete ptr;
}
}像是 Java 中接口根据业务要求实例成不同的子类。而且如果别的方法需要调用对象的地址,而如果不使用多态,那么每个子类都对应一个变量,然后每个变量都要传进去,那么相应的就要重载对应数量的方法。但如果使用了多态,那只要传基类指针或者引用即可。
本质上就是要减少子类变量的创建个数,用一个基类变量,根据业务逻辑进行“变身”。
3. 多态的对象模型
- 类的普通成员函数的地址是静态的,在编译阶段已指定,存放在程序的二进制代码中。
- 如果基类中有虚函数,那么基类对象的内存模型中有一个虚指针,该指针指向虚函数表,表中存放了基类的函数名和地址。
此时调用普通成员函数时,因为其地址在二进制码中有记录,所以直接调用即可。
但如果是虚函数,要先查找虚函数表,找到函数的地址,然后再执行。因此,这也解释了如果函数不考虑多态,那么就不要将其设置为虚函数,会降低效率(见 51-1)。 - 如果派生类中重定义了基类的虚函数,创建派生类对象时,将用派生类的函数取代虚函数表中基类的函数。从内存上来看,使用虚函数时,派生类对象把虚函数表中基类的函数的地址替换了,这使得“数据类型决定操作数据的方法”失效,同时也证明了“虚函数中基类指针指向派生类对象时就使用派生类的成员函数和数据”。
- C++ 中的多态分为两种:静态多态与动态多态。
静态多态:也称为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板。
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。
4. 在多态的场景下如何析构派生类
构造函数不能继承,创建派生类对象时,先执行基类构造函数,再执行派生类构造函数。析构函数不能继承,而销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数。
派生类的析构函数在执行完后,会自动执行基类的析构函数。而且如果手工的调用派生类的析构函数,也会自动调用基类的析构函数。
但是如果是指向派生类的基类指针,其执行析构函数时,由于“数据类型决定操作数据的方法”,使得其只会调用基类的析构函数而忽视派生类的析构函数;倘若派生类中有资源没有被释放掉,此时会容易导致内存泄漏。因此需要将基类的析构函数设置成虚函数,这时基类指针再调用析构函数时,就会执行派生类的析构函数,然后再会自动执行基类的析构函数。
因此对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数,防止出现“派生类有析构函数但是最后不会调用”的情况。析构函数可以手工调用,如果对象中有堆内存,析构函数中以下代码是必要的:
1
2delete ptr;
ptr = nulllptr;上文中提到了基类的析构函数为虚函数时,优先会调用派生类的。但是这本质上是名字遮蔽/复写,然而基类和派生类的析构函数的名字不可能是相同的,因此 C++ 编译器对虚析构函数做了特别的处理,以保证基类和派生类析构函数内存模型的相同。
赋值运算符重载函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特征标随类而异,它包含了一个类型为其所属类的形参。
友元函数不是类成员,不能继承。
5. 纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在某些情况下,基类中不能对虚函数给出有意义的实现,把它声明为纯虚函数。一般基类的虚函数用于实现缺省的、通用的功能,如果不需要这些功能,那就把基类的虚函数设置为纯虚函数。
语法:
1
virtual 返回值类型 函数名 (参数列表) = 0
纯虚函数在基类中为派生类保留一个函数的名字,以便派生类对它进行重定义。如果在基类中没有保留函数名字,则无法支持多态性。
含有纯虚函数的类被称为抽象类,不能实例化对象,可以创建指针和引用。只有在实现了基类纯虚函数的情况下才能实例化,否则也属于抽象类。
纯虚函数其实可以给他代码实现,这和 Java 的抽象函数有点不同。但是,基类中的纯虚析构函数必须需要实现,因为派生类的析构函数调用结束后,会自动调用基类的析构函数,而如果基类的析构函数没有实现,就会报错。
基类的纯虚析构函数的意义在于:如果想使一个类成为抽象类,但刚好又没有任何纯虚函数,怎么办?此时就在想要成为抽象类的类中声明一个纯虚析构函数即可。
6. 运行阶段类型识别 dynamic_cast
运行阶段类型识别(RTTI RunTime Type Identification)为程序在运行阶段确定对象的类型,只适用于包含虚函数的类。
基类指针可以指向派生类对象,但是如何知道基类指针指向的是哪种派生类的对象呢?(此时是想调用派生类中的非虚函数,要不然通过基类指针只能调用基类的函数)。
在 C 中,对于 2 的问题,就需要使用强制类型转换,将基类强行转换成派生类。但是这又需要程序员保证目标类型的正确。否则就会出现问题。
因此,
dynamic_cast
运算符用指向基类的指针来生成派生类的指针,它不能回答“指针指向的是什么类的对象”的问题,但能回答“是否可以安全的将对象的地址赋给特定类的指针”的问题。语法:
1
派生类指针 = dynamic_cast<派生类类型 *>(基类指针);
如果转换成功,
dynamic_cast
返回对象的地址,如果失败,返回nullptr
。注意事项:
dynamic_cast
只适用于包含虚函数的类。因为其会查询虚函数表。dynamic_cast
可以将派生类指针转换为基类指针,这种画蛇添足的做法没有意义。(除非想在派生类的友元函数中,用派生类指针强转成基类指针以调用基类的友元函数,因为友元函数不能被继承)dynamic_cast
可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出现bad_cast
异常。而且 C11 已经放弃异常了,因此不建议用于引用。
7. typeid
运算符和 type_info
类
typeid
运算符用于获取数据类型的信息。typeid
语法:typeid(数据类型)
typeid(变量名或表达式)
typeid
运算符返回type_info
类(在头文件<typeinfo>
中定义)的对象的引用。type_info
类的实现随编译器而异,但至少有name()
成员函数,该函数返回一个字符串,通常是类名。因此type_info.name()
在实际开发中只用于调试,而不建议用name()
成员函数返回的字符串作为判断数据类型的依据。- 比较有用的是
type_info
重载了==
和!=
运算符,用于对类型进行比较。 - 一些注意事项:
type_info
类的构造函数是private
属性,也没有拷贝构造函数,所以不能直接实例化,只能由编译器在内部实例化。typeid
运算符可以用于多态的场景,在运行阶段识别对象的数据类型。即对于基类指针,虽然其可能会指向派生类,但是其数据类型还是基类,所以typeid(基类指针)
一定是基类指针,所以尽量不用指针而是解引用。- 假设有表达式
typeid(*ptr)
,当ptr
是空指针时,如果ptr
是多态的类型,将引发bad_typeid
异常。
52. C++ 泛编程(自动推导类型 auto
,函数模板,类模板)
1. 自动推导类型 auto
在 C 语言和 C98 中,
auto
关键字用于修饰变量(自动存储的局部变量)。但在 C11 中,赋予了auto
全新的含义,不再用于修饰变量,而是作为一个类型指示符(例如int
,string
),指示编译器在编译时自动推导auto
声明的变量的数据类型。语法:
auto 变量名 = 初始值;
在Linux平台下,编译需要加-std=c++11
参数。注意事项:
auto
声明的变量必须在定义时初始化。- 初始化的右值可以是具体的数值,也可以是表达式和函数的返回值等。
auto
不能作为函数的形参类型,不能直接声明数组,不能定义类的非静态成员变量。
不要滥用
auto
,auto
在编程时真正的用途如下:代替冗长复杂的变量声明,在 C++ 标准变量库中用的多,也可以用在函数指针的声明当中。
以前在声明函数指针时,需要将函数的变量一一对应声明,而现在只需要赋值即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
double func(double b, const char* c, float d, short e, long f)
{
cout << ",b=" << b << ",c=" << c << ",d=" << d << ",e=" << e << ",f=" << f << endl;
return 5.5;
}
int main()
{
double (*pf)( double , const char* , float , short , long ); // 声明函数指针pf。
pf = func;
pf(2, "str", 3, 4, 5);
auto pf1 = func; // 使用 auto 简化函数指针声明
pf1(2, "str", 3, 4, 5);
}在模板中,用于声明依赖模板参数的变量。
函数模板依赖模板参数的返回值。
用于 lambda 表达式中。
2. 函数模板的基本概念
函数模板是通用的函数描述,使用任意类型(泛型)来描述函数。编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成该类型的函数定义。生成函数定义的过程被称为实例化。
创建交换两个变量的函数模板:
1
2
3
4
5
6
7template <typename T>
void Swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}以前的思想就是重载,但是数据类型很多,但现在就是写一个函数模板。
在 C98 添加关键字
typename
之前,C++ 使用关键字class
来创建模板。如果考虑向后兼容,函数模板应使用typename
,而不是class
。编译器先检查源代码中全部函数调用的代码,找到函数模板的调用处后,分析/推导参数的数据类型,然后根据模板,创建对应类型的函数(底层原理就是用一些符号添加到函数名上,创建新函数,然后调用)。
函数模板实例化可以让编译器自动推导,也可以在调用的代码中显式的指定。
1
2// 调用的时候
func<数据类型, ...>(参数 1, ...);
3. 函数模板的注意事项
可以为类的成员函数创建模板,但不能是虚函数和析构函数。
使用函数模板时,必须明确数据类型(无论自动推导还是手动指定),确保实参与函数模板能匹配上。
“明确数据类型”也包括:当模板的参数没有泛型T
时,调用函数模板时,也必须手动指定一种数据类型。使用函数模板时,推导的数据类型必须适应函数模板中的代码。比如:
1
2
3
4
5
6// 定义了两个变量相加的函数
template <typename T>
T Add(T a, T b)
{
return a + b;
}如果传入的参数的类型是自定义类(且没有重载
+
运算符),那么由于+
的原因,必然会报错。
即虽然模板函数适应任何数据类型,但是模板内的代码不一定。使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13template <typename T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
int a = 10;
char b = 20;
int c = Add(a, b); // 报错
int c = Add<int>(a, b) // 发生隐式转换,char 转 int
}函数模板支持多个通用数据类型的参数。(用的不多)
1
2
3
4
5template <typename T1, T2>
void show(T1 arg1, T2 agr2)
{
...;
}函数模板支持重载(参数个数不同),模板的参数中可以有非通用数据类型的参数。
4. 函数模板的具体化
为了满足特殊需求,可以提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。
具体化的语法:
1
2
3
4
5
6
7
8
9
10// 具体化函数的返回值、函数名和形参列表与函数模板相同
// 注意具体化函数模板没有泛型
template<>
void 函数模板名<数据类型>(参数列表)
// 两种声明方法选一种即可
template<>
void 函数模板名 (参数列表)
{
// 函数体。
}对于给定的函数名,现在有普通函数、函数模板和具体化的函数模板,以及它们的重载版本。编译器使用函数规则的顺序:
普通函数 > 具体化 > 常规模板。
如果希望使用函数模板,可以用空模板参数强制使用函数模板。
如果函数模板能产生更好的匹配,将优先于普通函数。示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// 普通函数
void Swap(int& a, int& b)
{
int temp = b;
b = a;
a = temp;
cout << "a is " << a << "; and b is " << b << endl;
cout << "使用了普通的函数" << endl;
}
// 模板函数
template<typename T>
void Swap(T& a, T& b)
{
int temp = b;
b = a;
a = temp;
cout << "a is " << a << "; and b is " << b << endl;
cout << "使用了模板函数" << endl;
}
// 具体化的模板函数
template <>
void Swap <char>(char& a, char& b)
{
char temp = b;
b = a;
a = temp;
cout << "a is " << a << "; and b is " << b << endl;
cout << "使用了具体化的模板函数" << endl;
}
int main()
{
char a = 'a', b = 'b';
Swap(a, b);
}因为普通函数还需要类型转换,所以这里选择了具体化的模板函数。
而如果在此基础上,具体化的模板函数的形参与传入的参数的类型需要隐式转换时,则会调用模板函数。
5. 函数模板分文件编写
- 普通函数的声明放在头文件中,定义放在源文件中。而函数模板只是函数的描述,没有实体,创建函数模板的所有代码放在头文件中。
- 函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中。
6. 函数模板高级 —— C11 对函数模板的扩展
1. decltype
关键字
在 C11 中,
decltype
操作符,用于查询表达式的数据类型。
语法:decltype(expression) var
也就是说,关键字decltype
分析表达式并得到其类型,可以直接用于定义变量。其不会计算执行表达式;函数调用也一种表达式,因此不必担心在使用decltype
时()
内部执行了函数。decltype
的意义在于,原先的函数模板中两个泛型运算的结果,如果用变量表示的话,无法定义该变量的类型,因此decltype
很好的解决了gai’wen’tidecltype
推导规则(按步骤):如果
expression
是一个没有用括号括起来的标识符(decltype
自带的()
不算),则var
的类型与该标识符的类型相同,包括const
等限定符以及指针。如果
expression
是一个函数调用,则var
的类型与函数的返回值类型相同(函数不能返回void
,但可以返回void *
)。如果
expression
是一个函数名(没有()
),那么结果就是一个函数类型,前面再加个*
就变成了函数指针。如果
expression
是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var
的类型是expression
的引用。
例如:1
2
3
4
5
6
7short a = 5;
// 此时 da 为 short 的引用,即 short&
decltype(++a) da = a;
// 同理:
decltype((a)) da = a;
// 当然,函数也是一样:返回函数的引用
decltype((func)) f = func;如果上面的条件都不满足,则
var
的类型与expression
的类型相同。
综合来看,
decltype
的结果,要么是表达式的引用,要么与表达式相同。虽然
decltype
和auto
都可以自动推导表达式的类型,但是decltype
不会执行表达式,而且不需要指定初始值,而auto
需要指定初始值(即需要赋值)而且会执行表达式的内容,例如:1
2
3
4
5
6
7
8void func()
{
cout << "执行了 func()" << endl;
}
// 可以不指定初始值,而且不会执行 func()
decltype(func()) f1;
// 需要指定初始值,才能让 auto 进行推导;而且会执行 func()
auto f2 = func();如果需要多次使用
decltype
,可以结合typedef
和using
,即可以给推导出来的数据类型起别名。
2. 函数后置返回类型
int func(int x,double y);
等同auto func(int x,double y) -> int;
将返回类型移到了函数声明的后面。auto
是一个占位符(C11给auto
新增的角色), 为函数返回值占了一个位置。
即上述语法也可以用于函数定义:1
2
3
4
5auto func(int x, double y) -> decltype(x + y)
{
// 函数体
return x + y;
}C14 标准对函数返回类型推导规则做了优化,函数的返回值可以用
auto
,不必尾随返回类型:1
2
3
4
5auto func(int x, double y)
{
// 函数体
return x + y;
}
7. 类模板/模板类的基本概念
原理和函数模板相同:类模板是通用类的描述,使用任意类型(泛型)来描述类的定义。使用类模板的时候,指定具体的数据类型,让编译器生成该类型的类定义。
语法:
1
2
3
4
5template <class T>
class 类模板名
{
类的定义;
};函数模板建议用
typename
描述通用数据类型,类模板建议用class
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template <class T1, class T2>
class AA
{
public:
T1 m_a; // 通用类型用于成员变量。
T2 m_b; // 通用类型用于成员变量。
AA() {} // 默认构造函数是空的。
// 通用类型用于成员函数的参数。
AA(T1 a, T2 b) :m_a(a), m_b(b) {}
// 通用类型用于成员函数的返回值。
T1 geta() // 获取成员m_a的值。
{
T1 a = 2; // 通用类型用于成员函数的代码中。
return m_a + a;
}
T2 getb(); // 获取成员m_b的值。
};
template <typename T1, typename T2>
T2 AA<T1, T2>::getb() // 获取成员m_b的值。
{
return m_b;
}
int main()
{
AA<int, string>* a = new AA<int, string>(3, "aa"); // 用模板类AA创建对象a。
cout << "a->geta()=" << a->geta() << endl;
cout << "a->getb()=" << a->getb() << endl;
delete a;
}注意事项:
在创建对象的时候,必须指明具体的数据类型。
使用类模板时,数据类型必须适应类模板中的代码。
类模板可以为通用数据类型指定缺省的数据类型(C++11标准的函数模板也可以)。比如:
1
2
3
4
5template <class T1, class T2 = string>
class A
{
...;
}模板类的成员函数可以在类外实现。
1
2
3
4
5
6
7
8
9
10
11template <class T1, class T2>
class AA
{
T2 getb();
}
// 类外声明
template <typename T1, typename T2>
T2 AA<T1, T2>::getb() // 获取成员m_b的值。
{
return m_b;
}可以用
new
创建模板类对象。1
AA<int, string> *a = new AA<int, string>(指定参数);
在程序中,模板类的成员函数使用了才会创建。
8. 模板类的应用开发 —— 栈
当没有使用模板类时,栈的代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
/*
* 栈类
*/
class Stack
{
private:
int* items; // 栈数组
int stackSize; // 栈实际的大小
int top; // 栈顶位置指针,指向下一个待存入空间
public:
Stack(int size) :stackSize(size), top(0)
{
items = new int[stackSize];
}
~Stack()
{
delete [] items;
items = nullptr;
}
bool isEmpty() const
{
return top == 0;
}
bool isFull() const
{
return top >= stackSize;
}
bool push(const int& item)
{
if (top < stackSize)
{
items[top++] = item;
return true;
}
else return false;
}
bool pop(int& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else return false;
}
};
int main()
{
Stack s(4);
s.push(1); s.push(2); s.push(3); s.push(4);
while (!s.isEmpty())
{
int item = 0;
s.pop(item);
cout << "item is " << item << endl;
}
}由于没有使用模板,所以上述代码只支持
int
类型。因此为了让其能支持多种数据类型,做出以下更改:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
typedef int DataType; // 定义栈元素的数据类型
/*
* 栈类
*/
class Stack
{
private:
DataType* items; // 栈数组
int stackSize; // 栈实际的大小
int top; // 栈顶位置指针,指向下一个待存入空间
public:
Stack(int size) :stackSize(size), top(0)
{
items = new DataType[stackSize];
}
~Stack()
{
delete [] items;
items = nullptr;
}
bool isEmpty() const
{
return top == 0;
}
bool isFull() const
{
return top >= stackSize;
}
bool push(const DataType& item)
{
if (top < stackSize)
{
items[top++] = item;
return true;
}
else return false;
}
bool pop(DataType& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else return false;
}
};
int main()
{
Stack s(4);
s.push(1); s.push(2); s.push(3); s.push(4);
while (!s.isEmpty())
{
DataType item = 0;
s.pop(item);
cout << "item is " << item << endl;
}
}这样做没用用到函数模板,但是也可以实现多种类,只需要将
typedef
后定义的类型修改即可。改为模板类的话,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// typedef int DataType; // 定义栈元素的数据类型
/*
* 栈类
*/
template <class DataType>
class Stack
{
private:
DataType* items; // 栈数组
int stackSize; // 栈实际的大小
int top; // 栈顶位置指针,指向下一个待存入空间
public:
Stack(int size) :stackSize(size), top(0)
{
items = new DataType[stackSize];
}
~Stack()
{
delete [] items;
items = nullptr;
}
bool isEmpty() const
{
return top == 0;
}
bool isFull() const
{
return top >= stackSize;
}
bool push(const DataType& item)
{
if (top < stackSize)
{
items[top++] = item;
return true;
}
else return false;
}
bool pop(DataType& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else return false;
}
};
int main()
{
Stack<int> s(4); // 注意这里初始化的时候需要指定类型。
s.push(1); s.push(2); s.push(3); s.push(4);
while (!s.isEmpty())
{
int item = 0;
s.pop(item);
cout << "item is " << item << endl;
}
}模板类的方法:
- 先写一个普通类,用具体的数据类型
- 调试普通类
- 把普通类改成模板类
9. 模板类的应用开发 —— 数组
C++ 中有定长数组 ——
array 容器(C11 标准)
和可变数组 ——vector
容器。简单的定长数组容器示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
/*
* 指定了非通用参数
*/
template <class T, int len = 5>
class Array
{
private:
T items[len]; // 数组元素
public:
/*
* 默认构造函数
*/
Array()
{
// 如果想要兼容 string,那么这行初始化代码就要删掉。因为 string 有一个指向堆区内存的指针。
memset(items, 0, sizeof(items));
}
/*
* 析构函数,由于没有用到堆区内存,所以啥也不用做
*/
~Array() {}
/*
* 重载下标运算符
*/
T& operator[](int i)
{
return items[i];
}
const T& operator[](int i) const
{
return items[i];
}
};
int main()
{
Array<char, 10> arr;
for (int i = 0; i < 10; i++)
{
arr[i] = i + 50;
}
for (int i = 0; i < 10; i++)
{
cout << "arr[" << i << "] is " << arr[i] << endl;
}
}上述例子中设计到了非通用类型参数
- 通常是整型(C20 标准可以用其他类型)
- 实例化模板时必须用常量表达式。
- 模板中不能修改参数的值
- 可以为非通用类型参数提供默认值。
可变长数组容器的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template <class T>
class Vector
{
private:
int len; // 数组长度
T* items; // 数组元素
public:
/*
* 默认构造函数
*/
Vector(int size = 10):len(size)
{
items = new T[len];
}
/*
* 析构函数
*/
~Vector() {
delete[] items;
items = nullptr;
}
/*
* 扩展数组的内存空间,只能往更大的空间扩展
*/
void reSize(int size)
{
if (size <= len)
{
return;
}
else
{
// 创建新数组并把值给拿过来
T* temp = new T[size];
for (int i = 0; i < len; i++)
{
temp[i] = items[i];
}
// 删除旧数组的指向,指向新的数组
delete[] items;
items = temp;
// 更新数组长度
len = size;
}
}
/*
* 获取数组长度
*/
int size() const
{
return len;
}
/*
* 重载下标运算符
*/
T& operator[](int i)
{
// 如果访问的元素超过了数组长度,那么就动态扩展到访问的长度
if (i >= len) reSize(i + 1);
return items[i];
}
const T& operator[](int i) const
{
// 只读的就不需要扩展了
return items[i];
}
};
int main()
{
Vector<string> v(1); // 初始的长度为 1
v[0] = "你好";
v[1] = "再见"; // 动态扩展了
for (int i = 0; i < v.size(); i++)
{
cout << "v[" << i << "] is " << v[i] << endl;
}
}Array:
优点:在栈上分配内存,易维护,执行速度快,合适小型数组。
缺点:在程序中,不同的非通用类型参数将导致编译器生成不同的类。
1
2Array<char, 10> arr;
Array<char, 11> arr;这两个是不同的类,会造成程序二进制代码的过大。
Vector 效率低,但是不会出现第二点问题,更通用。
10. 嵌套和递归使用模板类
常见的使用场景为:
- 容器中有容器
- 数组的元素可以是栈
- 栈中的元素可以是数组
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
/*
* 栈类
*/
template <class DataType>
class Stack
{
private:
DataType* items; // 栈数组
int stackSize; // 栈实际的大小
int top; // 栈顶位置指针,指向下一个待存入空间
public:
Stack(int size = 3) :stackSize(size), top(0)
{
items = new DataType[stackSize];
}
~Stack()
{
delete[] items;
items = nullptr;
}
/*
* 重载赋值运算符函数,实现深拷贝
*/
Stack& operator=(const Stack& s)
{
delete[] items; // 释放内存
stackSize = s.stackSize; // 栈的实际大小
items = new DataType[stackSize]; // 重新分配数组
// 复制数组中的元素
for (int i = 0; i < stackSize; i++)
{
items[i] = s.items[i];
}
top = s.top; // 栈顶指针
return *this;
}
bool isEmpty() const
{
return top == 0;
}
bool isFull() const
{
return top >= stackSize;
}
bool push(const DataType& item)
{
if (top < stackSize)
{
items[top++] = item;
return true;
}
else return false;
}
bool pop(DataType& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else return false;
}
};
template <class T>
class Vector
{
private:
int len; // 数组长度
T* items; // 数组元素
public:
/*
* 默认构造函数
*/
Vector(int size = 2):len(size)
{
items = new T[len];
}
/*
* 析构函数
*/
~Vector() {
delete[] items;
items = nullptr;
}
/*
* 重载赋值运算符函数,实现深拷贝
*/
Vector& operator=(const Vector& v)
{
delete[] items; // 释放内存
len = v.len; // 栈的实际大小
items = new T[len]; // 重新分配数组
// 复制数组中的元素
for (int i = 0; i < len; i++)
{
items[i] = v.items[i];
}
return *this;
}
/*
* 扩展数组的内存空间,只能往更大的空间扩展
*/
void reSize(int size)
{
if (size <= len)
{
return;
}
else
{
// 创建新数组并把值给拿过来
T* temp = new T[size];
for (int i = 0; i < len; i++)
{
// 如果复制的是类,且类中使用了堆区内存,那么就存在浅拷贝的问题,需要重载运算符 = 函数
temp[i] = items[i];
}
// 删除旧数组的指向,指向新的数组
delete[] items;
items = temp;
// 更新数组长度
len = size;
}
}
/*
* 获取数组长度
*/
int size() const
{
return len;
}
/*
* 重载下标运算符
*/
T& operator[](int i)
{
// 如果访问的元素超过了数组长度,那么就动态扩展到访问的长度
if (i >= len) reSize(i + 1);
return items[i];
}
const T& operator[](int i) const
{
// 只读的就不需要扩展了
return items[i];
}
};
int main()
{
// Vector 容器的大小是 2(默认),Stack 容器的大小是 3(默认)
// 第一步:创建 Vector 容器,容器中的元素用 Stack。(C11 之前,两个 < 之间要加上空格)
Vector<Stack<string>> vs;
// 第二步:手工的往容器中插入数据
vs[0].push("你好1"); vs[0].push("你好2"); vs[0].push("你好3");
vs[1].push("再见1"); vs[1].push("再见2"); vs[1].push("再见3");
// 第三步:用嵌套的循环,把容器中的数据显示出来
for (int i = 0; i < vs.size(); i++)
{
while (!vs[i].isEmpty())
{
string item;
vs[i].pop(item);
cout << "item is " << item << endl;
}
}
// 创建 Stack 容器,容器中的元素为 Vector<string>,3 * 2
Stack<Vector<string>> sv;
Vector<string> temp;
temp[0] = "aa1"; temp[1] = "bb1"; sv.push(temp);
temp[0] = "aa2"; temp[1] = "bb2"; sv.push(temp);
temp[0] = "aa3"; temp[1] = "bb3"; temp[2] = "cc3"; temp[3] = "dd3"; sv.push(temp);
while (!sv.isEmpty())
{
sv.pop(temp);
for (int i = 0; i < temp.size(); i++)
{
cout << "vt[" << i << "] = " << temp[i] << endl;
}
}
}注意这种嵌套模板类时,内部的深拷贝问题。
11. 模板类具体化
模板类具体化(特化、特例化)有两种:完全具体化和部分具体化。
语法:
1
2
3
4
5
6
7
8
9
10
11
12// 假设原来模板有两个通用参数
// 完全具体化
template<>
class AA<int, string>{
// 编写特别的代码
}
// 部分具体化
template<class T2>
class AA<int, T2>
{
//...
}
12. 模板类与继承
模板类继承普通类(常见),注意模板类的构造函数要指定基类的构造函数。
普通类继承模板类的实例化版本。(属于普通的继承)
1
2
3
4
5
6
7
8
9
10template<class T1, class T2>
class AA{
...;
}
// 继承模板类的实例化版本
class BB : public AA<int, string>
{
...
}普通类继承模板类。(常见)
普通类继承模板类,要转化成模板类继承模板类,因为只有模板类才能继承模板类。模板类继承模板类
要将通用参数继承,派生类的构造函数也要做出改变。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template<class T1, class T2>
class AA{
...;
}
// T3 是 BB 类中自己的。
template<class T1, class T2, class T3>
class BB : public AA<T1, T2>
{
...;
public:
// BB 的构造函数
BB():AA<T1, T2>()
{
...;
}
}模板类继承模板参数给出的基类(不能是模板类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// 普通类 AA
class AA {
public:
AA() { cout << "调用了AA的构造函数AA()。\n"; }
AA(int a) { cout << "调用了AA的构造函数AA(int a)。\n"; }
};
// 普通类 BB
class BB {
public:
BB() { cout << "调用了BB的构造函数BB()。\n"; }
BB(int a) { cout << "调用了BB的构造函数BB(int a)。\n"; }
};
// 普通类 CC
class CC {
public:
CC() { cout << "调用了CC的构造函数CC()。\n"; }
CC(int a) { cout << "调用了CC的构造函数CC(int a)。\n"; }
};
// 模板类 DD,有个通用变量 T
template<class T>
class DD {
public:
DD() { cout << "调用了DD的构造函数DD()。\n"; }
DD(int a) { cout << "调用了DD的构造函数DD(int a)。\n"; }
};
// 模板类 EE,继承 T
template<class T>
class EE : public T { // 模板类继承模板参数给出的基类。
public:
EE() :T() { cout << "调用了EE的构造函数EE()。\n"; }
EE(int a) :T(a) { cout << "调用了EE的构造函数EE(int a)。\n"; }
};
int main()
{
EE<AA> ea1; // AA作为基类。
EE<BB> eb1; // BB作为基类。
EE<CC> ec1; // CC作为基类。
EE<DD<int>> ed1; // EE<int>作为基类。
// EE<DD> ed1; // DD作为基类,错误。
}
13. 模板类与函数
模板类可用于函数的参数和返回值,有三种形式:
普通函数,参数和返回值是模板类的实例化版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template<class T1, class T2>
class AA
{
public:
T1 mX;
T2 mY;
AA(const T1 x, const T2 y):mX(x), mY(y) { }
void show() const { cout << "show() x = " << mX << ",y = " << mY << endl; }
};
// 普通函数,其参数和返回值都是模板类的实例化版本
AA<int, string> func(AA<int, string>& aa)
{
aa.show();
cout << "调用了 func(AA<int, string>)" << endl;
return aa;
}
int main()
{
AA<int, string> aa(1, "你好");
func(aa);
}函数模板,参数和返回值是模板类(不建议使用,不规范,不体现精髓)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template<class T1, class T2>
class AA
{
public:
T1 mX;
T2 mY;
AA(const T1 x, const T2 y):mX(x), mY(y) { }
void show() const { cout << "show() x = " << mX << ",y = " << mY << endl; }
};
// 函数模板,其参数和返回值都是模板类
template<typename T1, typename T2>
AA<T1, T2> func(AA<T1, T2>& aa)
{
aa.show();
cout << "调用了 func(AA<int, string>)" << endl;
return aa;
}
int main()
{
AA<int, string> aa(1, "你好");
func(aa);
}函数模板,参数和返回值是任意类型(支持普通类和模板类和其它类型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template<class T1, class T2>
class AA
{
public:
T1 mX;
T2 mY;
AA(const T1 x, const T2 y):mX(x), mY(y) { }
void show() const { cout << "show() x = " << mX << ",y = " << mY << endl; }
};
template<typename T>
T func(T& aa)
{
aa.show();
cout << "调用了 func(AA<int, string>)" << endl;
return aa;
}
int main()
{
AA<int, string> aa(1, "你好");
func(aa);
}第二种方法仅支持 AA 这一种模板类,而这种方法还用的少。
而这一种方法,只要传入的类有show()
方法,那么它就可以被支持。实际上,第三种方法不仅可以传入普通类和模板类,其甚至可以传入函数指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void show()
{
cout << "调用了普通 show() 函数" << endl;
}
template<typename T>
void func(T aa)
{
aa();
}
int main()
{
// 传入了函数指针
func(show);
}
14. 模板类和友元
非模板友元:友元函数不是模板函数,而是利用模板类的通用参数生成的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34#include <iostream>
using namespace std;
template<class T1, class T2>
class AA
{
T1 m_x;
T2 m_y;
public:
// 构造函数
AA(const T1 x, const T2 y) :m_x(x), m_y(y) {}
// 非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数。
// 友元函数的形参使用通用数据类型,编译器自动生成友元函数
// 需要注意的是,尽管使用了通用数据类型,但这个友元函数并不是模板函数,因为当创建模板类实例时,编译器会生成友元函数的实体,从而和下面的代码冲突
// 此外,非模板友元只能用于这个模板类,不能用于其他的模板类
friend void show(const AA<T1, T2>& a)
{
cout << "x is " << a.m_x << "; y is " << a.m_y << endl;
}
//friend void show(const AA<int, string>& a);
};
// 这种方法就要求为每个实例化版本的模板类准备一个友元函数,很麻烦
//void show(const AA<int, string>& a)
//{
// cout << "x is " << a.m_x << "; y is " << a.m_y << endl;
//}
int main()
{
AA<int, string> a (1, "你好");
show(a);
}约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数。(一般这个用的多,实际开发中直接抄)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// 约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数。
// 第一步:在模板类 AA 的定义前面,声明友元的函数模板。
template <typename T>
void show(T& a);
// 模板类 AA。
template<class T1, class T2>
class AA
{
// 第二步:在模板类中,再次声明友元函数模板;为了让编译器知道需要实例化的友元函数模板
// 这行代码让编译器在实例化某种数据类型的模板类时,也会实例化这种数据类型的模板函数。
// 此时模板类 AA 于上文中定义的函数模板 show() 产生了关系
friend void show<>(AA<T1, T2>& a);
T1 m_x;
T2 m_y;
public:
AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};
// 第三步:友元函数模板的定义。
template <typename T>
void show(T& a)
{
cout << "通用:x = " << a.m_x << ", y = " << a.m_y << endl;
}
// 第三步的具体化版本。
template <>
void show(AA<int, string>& a)
{
cout << "具体 AA<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}
// 友元的函数模板 show() 用于多个模板类
// 模板类 BB。
template<class T1, class T2>
class BB
{
// 第二步:在模板类中,再次声明友元函数模板。
friend void show<>(BB<T1, T2>& a);
T1 m_x;
T2 m_y;
public:
BB(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};
// 第三步:具体化版本。
template <>
void show(BB<int, string>& a)
{
cout << "具体 BB<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}
int main()
{
AA<int, string> a1(88, "我是一只傻傻鸟。");
show(a1); // 将使用具体化的版本。
AA<char, string> a2(88, "我是一只傻傻鸟。");
show(a2); // 将使用通用的版本。
BB<int, string> b1(88, "我是一只傻傻鸟。");
show(b1); // 将使用具体化的版本。
BB<char, string> b2(88, "我是一只傻傻鸟。");
show(b2); // 将使用通用的版本。
}非约束模板友元:模板类实例化时,如果实例化了 n 个类,也会实例化 n 个友元函数,每个实例化的类都拥有 n 个友元函数。(一般不用,因为每个实例化的类基本只需要一个友元函数,其他具体数据类型的友元函数和自己关系不大)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// 非类模板约束的友元函数,实例化后,每个函数都是每个类的友元。
template<class T1, class T2>
class AA
{
T1 m_x;
T2 m_y;
// 把模板函数设置为友元。
template <typename T> friend void show(T& a);
public:
AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};
// 通用的函数模板。
template <typename T> void show(T& a)
{
cout << "通用:x = " << a.m_x << ", y = " << a.m_y << endl;
}
// 函数模板的具体版本。
template <>void show(AA<int, string>& a)
{
cout << "具体<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}
int main()
{
AA<int, string> a(88, "我是一只傻傻鸟。");
show(a); // 将使用具体化的版本。
AA<char, string> b(88, "我是一只傻傻鸟。");
show(b); // 将使用通用的版本。
}
15. 成员模板类
即在模板类中创建模板类和函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template<class T1, class T2>
class AA
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x, const T2 y) : m_x(x), m_y(y) {}
void show()
{
cout << "m_x is " << m_x << "; m_y is " << m_y << endl;
}
template<class T>
class BB
{
public:
T m_a;
T1 m_b;
T2 m_c;
BB(){};
void show()
{
cout << "m_a = " << m_a << "; m_b is " << m_b << "; m_c is " << m_c << endl;
}
};
BB<string> m_bb;
template<typename T>
// 注意它和 AA 中的 show() 是重载关系
void show(T t)
{
cout << "成员函数模板 t is " << t << endl;
}
};
int main()
{
AA<int, string> a(1, "你好");
a.show();
a.m_bb.m_a = "bb 的第一个参数";
a.m_bb.show();
a.show("调用成员函数模板");
}如果模板类中的模板类的方法想在类外定义,那就要这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template<class T1, class T2>
class AA
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x, const T2 y) : m_x(x), m_y(y) {}
void show()
{
cout << "m_x is " << m_x << "; m_y is " << m_y << endl;
}
template<class T>
class BB
{
public:
T m_a;
T1 m_b;
T2 m_c;
BB(){};
// 这里只保留声明
void show()
};
// 定义成员变量
BB<string> m_bb;
template<typename T>
// 注意它和 AA 中的 show() 是重载关系
void show(T t)
{
cout << "成员函数模板 t is " << t << endl;
}
};
// 在类外定义模板类中的模板类的方法
template<class T1, class T2>
template<class T>
void AA<T1, T2>::BB<T>::show()
{
cout << "m_a = " << m_a << "; m_b is " << m_b << "; m_c is " << m_c << endl;
}
int main()
{
AA<int, string> a(1, "你好");
a.show();
a.m_bb.m_a = "bb 的第一个参数";
a.m_bb.show();
a.show("调用成员函数模板");
}
16. 将模板类用作参数
实例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 线性表模板类:tabletype-线性表类型,datatype-线性表的数据类型。
// 第一个参数 template<class, int> class tabletype 就是将模板类作为参数
template<template<class, int >class tabletype, class datatype, int len>
class LinearList
{
public:
tabletype<datatype, len> m_table; // 创建线性表对象。
void insert() { m_table.insert(); } // 线性表插入操作。
void ddelete() { m_table.ddelete(); } // 线性表删除操作。
void update() { m_table.update(); } // 线性表更新操作。
void oper() // 按业务要求操作线性表。
{
cout << "len=" << m_table.m_len << endl;
m_table.insert();
m_table.update();
}
};
int main()
{
// 创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20。
LinearList<LinkList, int, 20> a;
a.insert();
a.ddelete();
a.update();
// 创建线性表对象,容器类型为数组,数组的数据类型为string,表长为20。
LinearList<Array, string, 20> b;
b.insert();
b.ddelete();
b.update();
}将模板类作为参数过于复杂,一般多用于数据结构的设计当中。
53. 编译预处理
- C++ 程序编译的过程:预处理、编译(优化、汇编)、链接
- 三种预处理的指令:
- 包含头文件:
#include
- 宏定义:
#define
- 用于定义宏,#undef
- 删除宏 - 条件编译:
#ifdef
、#ifndef
- 包含头文件:
1. 包含头文件
#include
包含头文件有两种方式:#include <文件名>
直接从编译器自带的函数库目录中寻找文件。#include "文件名"
先从自定义的目录中寻找文件,如果找不到,再从编译器自带的函数库目录中寻找。#include
也包含其他的文件,如*.h
、*.cpp
或其他的文件。
- C 的标准库:老版本的有
.h
后缀;新版本没有.h
的后缀,增加了字符c
的前缀。例如:老版本是<stdio.h>
,新版本是<cstdio>
,新老版本库中的内容是一样的。在程序中,不指定std
命名空间也能使用库中的内容。 - C++ 的标准库:老版本的有
.h
后缀;新版本没有.h
的后缀。例如:老版本是<iostream.h>
,新版本是<iostream>
,老版本已弃用,只能用新版本。在程序中,必须指定std
命名空间才能使用库中的内容。 - 注意:用户自定义的头文件还是用
.h
为后缀。
2. 宏定义指令
- 无参数的宏:
#define 宏名 宏内容
- 有参数的宏:
#define MAX(x, y) ((x)>(y) ? (x) : (y))
(很少用,一般用 C++ 的内联函数) - 编译的时候,编译器把程序中的宏名用宏内容替换,是为宏展开(宏替换)。本质就是完全替换,包括符号。
- 宏可以只有宏名,没有宏内容。
- C++ 中常用的宏:
- 当前源代码文件名:
__FILE__
- 当前源代码函数名:
__FUNCTION__
- 当前源代码行号:
__LINE__
- 编译的日期:
__DATE__
- 编译的时间:
__TIME__
- 编译的时间戳:
__TIMESTAMP__
- 当用 C++ 编译程序时,宏
__cplusplus
就会被定义。
- 当前源代码文件名:
3. 条件编译
最常用的两种:
#ifdef
和#ifndef
。即 if define 和 if not define 的缩写。#ifdef
的用法:1
2
3
4
5
6// 如果 #ifdef 后面的宏名已经存在,则使用程序段 1,否则使用程序段 2
#ifdef 宏名
程序段 1;
#else
程序段 2;
#endif#ifndef
的语法结构和#ifdef
一样,但是条件是“后面的宏名”不存在时执行。常见的一个用法就是根据不同的操作系统来定义宏:
1
2
3
4
5
6
7#ifdef _WIN32
cout << "这是 Windows 系统" << endl;
typedef long long int64;
#else
cout << "这不是 Windows 系统" << endl;
typedef long int64;
#endif需要注意的是,这种条件编译和
if else
不同。if else
在编译器中,两个分支都会编译。而条件编译是只编译符合条件的一个分支。
4. 解决头文件中代码重复包含的问题
在 C/C++ 中,在使用预编译指令
#include
的时候,为了防止头文件被重复包含(即两个文件都互相包含对方,会造成包含的死循环)。此外,倘若去除其中一个对另一个的包含,还可能出现重复包含的情况,即 B 包含 A,C 需要用 B 和 A,此时它还会引入 B 和 A。这是 C 就会出现重复包含的情况。
解决办法:
将
#pragma once
放在文件的开头或者对被包含的文件中使用条件编译指令
#ifndef
,这样不论被包含的文件被#include
的多少次,#ifndef
的代码只会被包含一次。1
2
3
4#ifndef 宏名
#define 宏名
...;
#endif如果内部的代码比较简单,那么
#ifndef
的内容可以提前,少空格。#ifndef
方式受 C/C++ 语言标准的支持,不受编译器的任何限制。而#pragma once
操作简单,但有些编译器不支持。
54. 编译和链接
源代码的组织:
- 头文件(
.h
):#include
头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、#define
和const
定义的常量等。 - 源文件(
*.cpp
):函数的定义、类的定义、模板具体化的定义。 - 主程序(
main
函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include
包含进来。
- 头文件(
预处理的包括以下方面:
- 处理
#include
头文件包含指令。 - 处理
#ifdef
、#else
、#endif
、#ifndef
、#else
、#endif
条件编译指令。 - 处理
#define
宏定义。 - 为代码添加行号、文件名和函数名。
- 删除注释。
- 保留部分
#pragma
编译指令(编译的时候会用到)。
编译预处理后会生成临时文件。
- 处理
编译(只有编译源文件的说法,没法编译头文件):
将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制目标文件,*.obj
)。链接:
将编译后的目标文件,以及它们所需要的库文件(例如 C++ 标准库文件 msvc*.lib)链接在一起,形成一个整体(.exe)。一些细节:
- 分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高。
- 编译单个
*.cpp
文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。(直接和间接包含头文件都可以) - 编译单个
*.cpp
文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译。 - 如果函数和类的定义不存在,编译不会报错,但链接时会出现无法解析的外部命令。(因为 C++ 编译的时候只检查名称是否合法,不检查名称的定义(实体)是否存在。链接的时候会寻找名称的定义)
- 链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误。(如果把变量、函数和类的定义放在
*.h
文件中,*.h
会被多次包含,链接前可能存在多个副本;如果放在*.cpp
文件中,*.cpp
文件不会被包含,只会被编译一次,链接前只存在一个版本),因此把变量、函数和类的定义放在*.h
中是不规范的做法,如果*.h
被多个*.cpp
包含,会出现重定义。 - 用
#include
包含*.cpp
也是不规范的做法,原理同上。 - 可能不使用全局变量,如果一定要用,要在
*.h
文件中声明(需要加extern
关键字),在*.cpp
文件中定义。一般全局变量的声明都放在头文件中(定义/赋值不要放在头文件中)。 - 全局的
const
常量在头文件中定义(const
常量仅在单个文件内有效)。 *.h
文件重复包含的处理方法只对单个的*.cpp
文件有效,不是整个项目。(即头文件在整个项目中不会只包含一次)- 函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在
*.h
文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp
文件中。 - Linux 下 C++ 编译和链接的原理与 VS 一样。
55. 命名空间
在实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字冲突的情况。命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突。
语法:
1
2
3
4
5
6
7// 创建命名空间
namespace 命名空间的名字
{
// 类、函数、模板、变量的声明和定义
}
// 创建命名空间的别名:
namespace 别名 = 原名命名空间外使用空间中名字的方法:
通过运算符
::
:命名空间::名字
用
using
声明:
using 命名空间::名字
,使用该声明后,就可以直接使用名称。但如果同一个声明区域有相同的名字,就会报错。用
using
编译指令:
using namespace 命名空间
using
编译指令将使得整个命名空间中的名字可用。如果声明区域有相同的名字,局部版本将隐藏命名空间中的名字,不过,可以使用域名解析符::
来使用命名空间中的名称。
注意事项:
- 命名空间是全局的,可以分布在多个文件中。
- 命名空间可以嵌套。(用的不多)
- 在命名空间中声明全局变量,而不是使用外部全局变量和静态变量。
如果变量不在头文件中声明,只在源文件的命名空间中定义;那么这个变量只能在命名空间内部使用,外面根本不知道,效果和静态变量是一样的。 - 对于
using
声明,首选将其作用域设置为局部而不是全局。 - 不要在头文件中使用
using
编译指令,否则整个命名空间中的名称将在整个项目的全局区域可用,这样就体现不出“隔离”的理念。如果非要使用,应将它放在所有的#include
之后。 - 匿名的命名空间,从创建的位置到文件结束有效。如果是匿名的命名空间,那么它里面的名称在当前文件中可以直接使用。
56. C++ 风格的类型转换
- C 风格的类型转换很容易理解:
目标类型 表达式
或者目标类型(表达式)
。 - C++ 也是支持 C 风格的强制类型转换,但是 C 风格的强制类型转换可能会带来一些隐患,出现一些难以察觉的问题,所以 C++ 又推出了四种新的强制类型转换来替代 C 风格的强制类型转换,降低使用风险。
- C++ 中新增的四个关键字:
static_cast
、const_cast
、reinterpret_cast
和dynamic_cast
(用于多态)。用于支持 C++ 风格的强制类型转换。 - 这四个语法通用:
static_cast/const_cast/reinterpret_cast/dynamic_cast <目标类型>(表达式)
- C++ 风格的强制类型转换能更清晰的表明它们要干什么,程序员只要看一眼这样的代码,立即能知道强制转换的目的,并且,在多态场景也只能使用 C++ 风格的强制类型转换。
1. static_cast
static_cast
是最常用的 C++ 风格的强制类型转换,主要是为了执行那些较为合理的强制类型转换。用于基本内置数据类型之间的转换:C 风格的转换编译器可能会提示警告信息,而使用
static_cast
就不会提示警告信息。指针之间的转换:C 风格的可用于各种类型指针之间的转换,但是
static_cast
使得各种类型指针之间不允许直接转换,必须要借助void*
作为中间介质。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
{
int ii = 10;
// C 风格的转换:
double* ii2 = (double*)ⅈ
cout << "C 风格转换后的结果为:" << *ii2 << endl;
// 任何类型的指针都可以隐式转换成 void*,这里先转换成 void*
void* temp = ⅈ
// static_cast 可以将任意的 void* 转换成其他类型的指针
double* ii3 = static_cast<double*>(temp);
cout << "C++ 风格转换后的结果为:" << *ii3 << endl;
}这种需要
void*
的场景,一般是函数调用。当一个函数需要指针参数时,就可以将参数的类型定义为void*
,这样在函数中就可以进行转换使用。1
2
3
4void func(void* ptr)
{
double* pp = static_cast<double*>(ptr);
}
2. const_cast
static_cast
不能丢掉指针(引用)的const
和volitale
属性,但是const_cast
可以。1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func(int* ii){}
int main()
{
const int* aa = nullptr;
int* bb = (int*)aa; // C 风格,强制丢掉 const 限定符
int* cc = const_cast<int*>(aa); // C++ 风格
// 应用场景:需要去掉 const 限定,否则传不进去
func(const_cast<int*>(aa));
}
3. reinterpret_cast
static_cast
不能用于转换不同类型的指针(引用)(不考虑有继承关系的情况),reinterpret_cast
可以。reinterpret_cast
的意思是重新解释,能够将一种对象类型转换为另一种,不管它们是否有关系。它的使用要求<目标类型>
和(表达式)
中必须有一个是指针(引用)类型。reinterpret_cast
不能丢掉(表达式)
的const
或volitale
属性。应用场景:
reinterpret_cast
的第一种用途是改变指针(引用)的类型。即不需要通过中介void*
,将类型直接进行转换。reinterpret_cast
的第二种用途是将指针(引用)转换成整型变量。整型与指针占用的字节数必须一致,否则会出现警告,转换可能损失精度。reinterpret_cast
的第三种用途是将一个整型变量转换成指针(引用)。
这些应用场景在多线程、函数回调和网络编程中使用较多。
57. string 类/容器
string
是字符容器,内部维护了一个动态的字符数组。- 与普通的字符数组相比,
string
容器有三个优点:- 使用的时候,不必考虑内存分配和释放的问题;
- 动态管理内存(可扩展);
- 提供了大量操作容器的 API。缺点是效率略有降低,占用的资源也更多。
1. 构造和析构
静态常量成员
string::npos
为字符数组的最大长度(通常为unsigned int
的最大值);NBTS(null-terminated string):C 风格的字符串(即以空字符 0 结束的字符串)。
string
类有七个构造函数(C11 新增了两个)string()
:创建一个长度为 0 的string
对象(默认构造函数)。string(const char *s)
:将string
对象初始化为s
指向的NBTS(该构造函数本质是转换函数)。string(const string &str)
:将string
对象初始化为str
(拷贝构造函数)。string(const char *s,size_t n)
:将string
对象初始化为s
指向的地址后n
字节的内容。string(const string &str,size_t pos=0,size_t n=npos)
:将sring
对象初始化为str
从位置pos
开始到结尾的字符(或从位置pos
开始的n
个字符)。template<class T> string(T begin,T end)
:将string
对象初始化为区间[begin,end]
内的字符,其中begin
和end
的行为就像指针,用于指定位置,范围包括begin
在内,但不包括end
。string(size_t n,char c)
:创建一个由n
个字符c
组成的string
对象。
C11 新增的两个如下:
string(string && str) noexcept
:它将一个string
对象初始化为string
对象str
,并可能修改str
(移动构造函数,需要学习右值引用知识)。string(initializer_list<char> il)
:它将一个string
对象初始化为初始化列表il
中的字符。
例如:string ss = { ‘h’,’e’,’l’,’l’,’o’ };
前 3 个构造函数是基础,后 4 个在处理文件和网络编程中经常使用。
七个构造函数的详解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72#include <iostream>
using namespace std;
int main()
{
// 1. string():创建一个长度为 0 的 string 对象(默认构造函数)。
string s1; // 创建一个长度为0的string对象
cout << "s1=" << s1 << endl; // 将输出s1=
// 返回当前容量,可以存放字符的总数。
// 容器.capacity() 方法返回一个容器的大小,string 默认为 15,其大小都会比容器.size() 大点,目的是为了避免频繁的重新分配内存。
cout << "s1.capacity()=" << s1.capacity() << endl;
cout << "s1.size()=" << s1.size() << endl; // 返回容器中数据的大小。
cout << "容器动态数组的首地址=" << (void*)s1.c_str() << endl;
s1 = "xxxxxxxxxxxxxxxxxxxx";
cout << "s1.capacity()=" << s1.capacity() << endl; // 返回当前容量,可以存放字符的总数。这次超过了 15 字节,因此会扩展一次
cout << "s1.size()=" << s1.size() << endl; // 返回容器中数据的大小。
cout << "容器动态数组的首地址=" << (void*)s1.c_str() << endl;
// 可以看出,经过内存扩展后,地址不同,因此可以看出其扩展的本质就是:将内容复制到新的空间,再把以前的空间释放掉。
// 2. string(const char *s):将 string 对象初始化为 s 指向的 NBTS(本质是转换函数)。
// 换句话说,就是将 C 风格的字符串转换成 string 对象
string s2("hello world");
cout << "s2=" << s2 << endl; // 将输出s2=hello world
string s3 = "hello world";
cout << "s3=" << s3 << endl; // 将输出s3=hello world
// 3. string(const string & str):将 string 对象初始化为 str(本质是拷贝构造函数)。
string s4(s3); // s3 = "hello world";
cout << "s4=" << s4 << endl; // 将输出s4=hello world
string s5 = s3;
cout << "s5=" << s5 << endl; // 将输出s5=hello world
// 注意:string 类中有一个指向动态数组的指针,因此 string 类的拷贝构造函数一定是深拷贝
// 4. string(const char* s, size_t n):将 string 对象初始化为 s 指向的 NBTS 的前 n 个字符,即使超过了 NBTS 结尾。
string s6("hello world", 5);
cout << "s6=" << s6 << endl; // 将输出 s6=hello
cout << "s6.capacity()=" << s6.capacity() << endl; // 返回当前容量,可以存放字符的总数。
cout << "s6.size()=" << s6.size() << endl; // 返回容器中数据的大小。
string s7("hello world", 50);
cout << "s7=" << s7 << endl; // 将输出 s7=hello world 后未知内容,直至 50 个字节
cout << "s7.capacity()=" << s7.capacity() << endl; // 返回当前容量,可以存放字符的总数。
cout << "s7.size()=" << s7.size() << endl; // 返回容器中数据的大小。
// 5. string(const string & str, size_t pos = 0, size_t n = npos):
// 将 string 对象初始化为 str 从位置 pos 开始到结尾的字符,或从位置 pos 开始的 n 个字符。
// 需要注意的是,这个构造函数会判断 str 的结尾标志,不像第 4 点构造函数。
string s8(s3, 3, 5); // s3 = "hello world";
cout << "s8=" << s8 << endl; // 将输出 s8=lo wo
string s9(s3, 3);
cout << "s9=" << s9 << endl; // 将输出s9=lo world
cout << "s9.capacity()=" << s9.capacity() << endl; // 返回当前容量,可以存放字符的总数。
cout << "s9.size()=" << s9.size() << endl; // 返回容器中数据的大小。
string s10("hello world", 3, 5);
cout << "s10=" << s10 << endl; // 将输出s10=lo wo
// 注意:不会用构造函数 5,而是用构造函数 4
// 本质的原因是因为:"" 这个实际上是 const char[],而不是 string。从这个角度来看,其和第四个构造函数更匹配
string s11("hello world", 3);
cout << "s11=" << s11 << endl; // 将输出s11=hel
// 6. template<class T> string(T begin, T end):将 string 对象初始化为区间 [begin, end] 内的字符,
// 其中begin 和 end 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括 end。
// 本质是用迭代器创建 string 对象
// 7. string(size_t n, char c):创建一个由 n 个字符 c 组成的 string 对象。
string s12(8, 'x');
cout << "s12=" << s12 << endl; // 将输出 s12=xxxxxxxx
cout << "s12.capacity()=" << s12.capacity() << endl; // s12.capacity()=15
cout << "s12.size()=" << s12.size() << endl; // s12.size()=8
string s13(30, 0);
cout << "s13=" << s13 << endl; // 将输出s13=
cout << "s13.capacity()=" << s13.capacity() << endl; // s13.capacity()=31
cout << "s13.size()=" << s13.size() << endl; // s12.size()=30
2. 设计目标
char elem[i]
表示开辟了多少字节的空间,char
只是用来指定这段内存的读取方式。因此,
string
是以字节为最小存储单元的动态容器;其不存放空字符0
(这是 C 风格字符串独有的);它实际上又是存放数据的内存空间。因此在实际开发中,string
可以用作缓冲区,可以用于存储任何数据类型。因此,第四个函数的作用就体现在这里,即从一个缓冲区的某个位子向后取数据,其第一个参数应理解成一个起始地址(而不是 C 风格的字符或字符串),如果在 C 中,其参数类型应该为void*
而不是char*
(因为 C++ 要求强类型)。string
内部的三个指针:char *start_
:动态分配内存块开始的地址。char *end_
:动态分配内存块最后的地址。char *finish_
:已使用空间的最后的地址。
正因为有这三个指针,所有用
string
存放字符串时,不需要空字符0
。
容器大小:end_ - start_
,已用空间大小:finishi_ - start_
3. string
容器的特性操作
size_t max_size() const
:返回 string 对象的最大长度 string::npos,此函数意义不大。
size_t capacity() const
:返回当前容量,可以存放字符的总数。size_t length() const
:返回容器中数据的长短(字符串语义)。
size_t size() const
:返回容器中数据的大小(容器语义)。bool empty() const
:判断容器是否为空。void clear()
:清空容器。void shrink_to_fit()
:将容器的容量降到实际大小(需要重新分配内存)。void reserve(size_t size=0)
:将容器的容量设置为至少size
,即提前预分配而不是让他一点一点涨。void resize(size_t len,char c=0)
:把容器的实际大小置为len
,如果len
< 实际大小,会截断多出的部分;如果len
> 实际大小,就用字符c
填充。
4. string
容器的字符操作
char &operator[](size_t n)
const char &operator[](size_t n) const
:第二个只读。两者重载了下标运算符。char &at(size_t n)`` ``const char &at(size_t n) const
:第二个只读。和第一个不同,用()
来访问元素,比较麻烦。operator[]
和at()
返回容器中的第n
个元素,但at()
函数提供范围检查,当越界时会抛出out_of_range
异常,operator[]
不提供范围检查。const char *c_str() const
:返回容器中动态数组的首地址,语义:寻找以null
结尾的字符串。
const char *data() const
:返回容器中动态数组的首地址,语义:只关心容器中的数据。int copy(char *s, int n, int pos = 0) const
:把当前容器中的内容,从pos
开始的n
个字节拷贝到s
中,返回实际拷贝的数目。
5. 赋值操作
- 给已存在的容器赋值,将覆盖容器中原有的内容。
string &operator=(const string &str)
:把容器str
赋值给当前容器。string &assign(const char *s)
:将string
对象赋值为s
指向的NBTS。string &assign(const string &str)
:将string
对象赋值为str
。string &assign(const char *s,size_t n)
:将string
对象赋值为s
指向的地址后n字节的内容。string &assign(const string &str,size_t pos=0,size_t n=npos)
:将sring
对象赋值为str
从位置pos
开始到结尾的字符(或从位置pos
开始的n
个字符)。template<class T> string &assign(T begin,T end)
:将string
对象赋值为区间[begin,end]
内的字符。string &assign(size_t n,char c)
:将string
对象赋值为由n
个字符c。- 除了第一个,其他的和构造函数相同。
6. 连接操作
- 把内容追加到已存在容器的后面。
string &operator+=(const string &str)
:把容器str
连接到当前容器。string &append(const char *s)
:把指向s
的 NBTS 连接到当前容器。string &append(const string &str)
:把容器str
连接到当前容器。string &append(const char *s,size_t n)
; :将s
指向的地址后n字节的内容连接到当前容器。string &append(const string &str,size_t pos=0,size_t n=npos)
:将str
从位置pos
开始到结尾的字符(或从位置pos
开始的n
个字符)连接到当前容器。template<class T> string &append (T begin,T end)
:将区间[begin,end]
内的字符连接到容器。string &append(size_t n,char c)
:将n
个字符c
连接到当前容器。- 除了第一个,其他的和构造函数相同。
7. 交换操作
void swap(string &str)
:把当前容器与str
交换。- 如果数据量很小,交换的是动态数组中的内容,如果数据量比较大,交换的是动态数组的地址。
8. 截取操作
string substr(size_t pos = 0,size_t n = npos) const
:返回pos
开始的n
个字节组成的子容器。
9. 比较操作
bool operator==(const string &str1,const string &str2) const
:比较两个字符串是否相等。int compare(const string &str) const
:比较当前字符串和str1
的大小。int compare(size_t pos, size_t n,const string &str) const
:比较当前字符串从pos
开始的n
个字符组成的字符串与str
的大小。int compare(size_t pos, size_t n,const string &str,size_t pos2,size_t n2)const
:比较当前字符串从pos
开始的n
个字符组成的字符串与str
中pos2
开始的n2
个字符组成的字符串的大小。- 以下几个函数用于和C风格字符串比较。
int compare(const char *s) const
int compare(size_t pos, size_t n,const char *s) const
int compare(size_t pos, size_t n,const char *s, size_t pos2) const
10. 其他操作
- 除去上述的操作,还有“查找”、“替换”、“插入”、“删除”。如果从“缓冲区”和“容器”的角度来看,这些操作是没有意义的。
- 如果从字符串的角度来看,这些操作才有用途。
- 这四个操作查手册吧。
58. Vector
容器
vector 容器封装了动态数组,支持任意类型。
使用时需要包含头文件:
#include<vector>
vector 模板类声明:
1
2
3
4
5
6
7
8
9
10
11// 第一个模板参数填数组的数据类型
// 第二个模板参数指定分配器,缺省用 STL 提供的分配器
template<class T, class Alloc = allocator<T>>
class vector
{
private:
T *start_;
T *finish_;
T *end_;
// ...
}分配器:各种 STL 容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。如果省略该模板参数的值,将默认使用
allocator<T>
,用new
和delete
分配和释放内存。
如果程序员觉得allocator<T>
的效率不高,可以自己创建,亦或者采用内存池技术。
1. 构造函数
vector()
:创建一个空的vector容器。vector(initializer_list<T> il)
:使用统一初始化列表。vector(const vector<T>& v)
:拷贝构造函数。vector(Iterator first, Iterator last)
:用迭代器创建 vector 容器。vector(vector<T>&& v)
:移动构造函数(C11 标准)。explicit vector(const size_t n)
:创建 vector 容器,元素个数为 n (容量和实际大小都是 n)。注意其有explicit
关键字,其禁止把这个构造函数当成转换函数使用。vector(const size_t n, const T& value)
:创建 vector 容器,元素个数为n
,值均为value
。- 析构函数
~vector()
释放内存空间。
2. 特性操作
size_t max_size() const
:返回容器的最大长度,此函数意义不大size_t capacity() const
:返回容器的容量。size_t size() const
:返回容器的实际大小(已使用的空间)。bool empty() const
:判断容器是否为空。void clear()
:清空容器。void reserve(size_t size)
:将容器的容量设置为至少size
。void shrink_to_fit()
:将容器的容量降到实际大小(需要重新分配内存)。void resize(size_t size)
:把容器的实际大小置为size
。void resize(size_t size,const T &value)
:把容器的实际大小置为size
,如果size
< 实际大小,会截断多出的部分;如果size
>实际大小,就用value
填充。
3. 元素操作
T &operator[](size_t n)
;
const T &operator[](size_t n) const
:只读。T &at(size_t n)
;
const T &at(size_t n) const
:只读。T *data()
:返回容器中动态数组的首地址。
const T *data() const
:返回容器中动态数组的首地址。T &front()
:第一个元素。
const T &front()
:第一个元素,只读。const T &back()
:最后一个元素,只读。
T &back()
:最后一个元素。
4. 赋值操作
- 给已存在的容器赋值,将覆盖容器中原有的内容。
vector &operator=(const vector<T> &v)
:把容器v赋值给当前容器。vector &operator=(initializer_list<T> il)
:用统一初始化列表给当前容器赋值。void assign(initializer_list<T> il)
:使用统一初始化列表赋值。void assign(Iterator first, Iterator last)
:用迭代器赋值。void assign(const size_t n, const T& value)
:把n个value给容器赋值。
5. 交换操作
void swap(vector<T> &v)
:把当前容器与v
交换。
6. 比较操作
bool operator == (const vector<T> & v) const
bool operator != (const vector<T> & v) const
7. 插入和删除
void push_back(const T& value)
:在容器的尾部追加一个元素。void emplace_back(…)
:在容器的尾部追加一个元素,…
用于构造元素,也就是说,如果 vector 内存放的是类,调用这个函数时传入类需要的参数,那么其就会用这些参数创建一个类对象然后存入,而不用先创建类对象,再存入。C11 标准。iterator insert(iterator pos, const T& value)
:在指定位置插入一个元素,返回指向插入元素的迭代器。iterator emplace (iterator pos, …)
:在指定位置插入一个元素,…用于构造元素,返回指向插入元素的迭代器。C++11iterator insert(iterator pos, iterator first, iterator last)
:在指定位置插入一个区间的元素,返回指向第一个插入元素的迭代器。void pop_back()
:从容器尾部删除一个元素。iterator erase(iterator pos)
:删除指定位置的元素,返回下一个有效的迭代器。iterator erase(iterator first, iterator last)
:删除指定区间的元素,返回下一个有效的迭代器。- 需要注意的是,vector 底层是数组,那么对其中间元素进行插入删除时,需要逐个挪动元素。因此尽可能不要在 vector 容器中间插入和删除。
8. vector 的嵌套
- 有点像二维数组。但其长度可变,因此更加灵活。
9. 注意事项
- 迭代器失效的问题:
resize()
、reserve()
、assign()
、push_back()
、pop_back()
、insert()
、erase()
等函数会引起 vector 容器的动态数组发生变化,可能导致 vector 迭代器失效。
59. 迭代器
- 迭代器是访问容器中元素的通用方法。如果使用迭代器,不同的容器,访问元素的方法是相同的。
- 迭代器支持的基本操作:赋值(
=
)(即把一个迭代器赋值到另一个迭代器)、解引用(*
)、比较(==
和!=
)、从左向右遍历(++
)。 - 一般情况下,迭代器是指针和移动指针的方法。特殊来讲,迭代器还可以是一个链表(虽然链表没有
++
操作,但是链表可以重载++
运算符)。
1. 正向迭代器
只能使用
++
运算符从左向右遍历容器,每次沿容器向右移动一个元素。符合这种条件的,比如单链表。定义的语法:
1
2
3
4// 正向迭代器
容器名<元素类型>::iterator 迭代器名;
// 常·正向迭代器
容器名<元素类型>::const_iterator 迭代器名;相关的成员函数:
1
2
3
4
5
6
7
8// 表示容器的开始
iterator begin();
const_iterator begin();
const_iterator cbegin(); // 配合 auto 使用。
// 表示容器的尾部
iterator end();
const_iterator end();
const_iterator cend();需要注意的是,容器的尾部,不是最后一个元素所在的地址(头部地址),而是最后一个元素后的首地址。.
2. 双向迭代器
具备正向迭代器的功能,还可以反向(从右到左)遍历容器(**也是用
++
**),不管是正向还是反向遍历,都可以用--
让迭代器后退一个元素。例如双链表。定义的语法:
1
2
3
4// 反向迭代器
容器名<元素类型>::reverse_iterator 迭代器名;
// 常反向迭代器
容器名<元素类型>::const_reverse_iterator 迭代器名;相关的成员函数(其余用法和正向迭代器相同):
1
2
3
4
5
6
7// rbegin 表示 reverseBegin,相当于 end()
reverse_iterator rbegin();
const_reverse_iterator crbegin();
// 同理,rend() 表示 reverseEnd,相当于 Begin()
reverse_iterator rend();
const_reverse_iterator crend();
3. 随机访问迭代器
- 具备双向迭代器的功能,还支持以下操作:
- 用于比较两个迭代器相对位置的关系运算(
<
、<=
、>
、>=
)。 - 迭代器和一个整数值的加减法运算(
+
、+=
、-
、-=
)。 - 支持下标运算(
iter[n]
)
- 用于比较两个迭代器相对位置的关系运算(
- 数组的指针是纯天然的随机访问迭代器。
4. 输入和输出迭代器
- 这两种迭代器比较特殊,它们不是把容器当做操作对象,而是把输入/输出流作为操作对象。
60. C11:基于范围的 for
循环
对于一个有范围的集合来说,在程序代码中指定循环的范围有时候是多余的,还可能犯错误。因此 C11 引入了基于范围的
for
循环。语法:
1
2
3
4for (迭代变量类型 迭代变量 : 迭代范围变量)
{
// 循环体
}例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vv = { 1, 2, 3, 4, 5 };
// 用迭代器遍历容器 vv
for (auto it = vv.begin(); it != vv.end(); it++)
{
cout << *it << " ";
}
cout << endl;
// 用基于范围的 for 循环遍历容器 vv
// 迭代变量类型一般用 auto 居多
for (int val : vv)
{
cout << val << " ";
}
cout << endl;
}注意事项:
迭代的范围可以是数组名、容器名、初始化列表或者可迭代的对象(支持
begin()
、end()
、++
、==
)。数组名传入函数后,已退化成指针,不能作为容器名(即不能用基于范围的
for
循环遍历了。如果容器中的元素是结构体和类,迭代器变量应该申明为引用,加
const
约束表示只读。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43#include <iostream>
#include <vector>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << "默认构造函数AA()。\n"; }
AA(const string& name) : m_name(name) { cout << "构造函数,name=" << m_name << "。\n"; }
AA(const AA& a) : m_name(a.m_name) { cout << "拷贝构造函数,name=" << m_name << "。\n"; }
AA& operator=(const AA& a) { m_name = a.m_name; cout << "赋值函数,name=" << m_name << "。\n"; return *this; }
~AA() { cout << "析构函数,name=" << m_name << "。\n"; }
};
int main()
{
vector<AA> v;
cout << "刚开始创建容器时占用空间大小,v.capacity()=" << v.capacity() << "\n";
v.emplace_back("张三");
cout << "插入一个元素后容器占用空间大小,v.capacity()=" << v.capacity() << "\n";
v.emplace_back("李四");
cout << "插入两个元素后容器占用空间大小,v.capacity()=" << v.capacity() << "\n";
v.emplace_back("王五");
cout << "插入三个元素后容器占用空间大小,v.capacity()=" << v.capacity() << "\n";
cout << endl << "-------------------------------------" << endl << endl;
// 如果迭代器变量不申明为引用
for (auto a : v)
{
cout << a.m_name << endl;
}
cout << endl << "-------------------------------------" << endl << endl;
// 使用 const 且引用
for (const auto &a : v)
cout << a.m_name << endl;
cout << endl;
}结果如下:
刚开始创建容器时占用空间大小,v.capacity()=0
构造函数,name=张三。
插入一个元素后容器占用空间大小,v.capacity()=1
构造函数,name=李四。
拷贝构造函数,name=张三。
析构函数,name=张三。
插入两个元素后容器占用空间大小,v.capacity()=2
构造函数,name=王五。
拷贝构造函数,name=张三。
拷贝构造函数,name=李四。
析构函数,name=张三。
析构函数,name=李四。
插入三个元素后容器占用空间大小,v.capacity()=3
拷贝构造函数,name=张三。
张三
析构函数,name=张三。
拷贝构造函数,name=李四。
李四
析构函数,name=李四。
拷贝构造函数,name=王五。
王五
析构函数,name=王五。
张三
李四
王五析构函数,name=张三。
析构函数,name=李四。
析构函数,name=王五。通过结果得出以下结论:
- 当向 vector 容器插入类时,会有以下过程:
- 调用构造函数,创建类对象
- 调用容器内已有全部的类的拷贝构造函数;因为 vector 动态变化的本质就是开辟新内存空间,然后将原空间内的对象通过拷贝构造函数转移过去
- 调用原先容器内已有全部的类的析构函数,因为内存已经转移,所以之前内存的类要全部回收释放。
- 如果迭代器变量不申明为引用,那么每次都需要将容器中的一个元素拷贝到迭代变量,用完了还要调用析构函数进行销毁,因此效率低。
- 当然,如果不希望修改每次迭代的元素,那么就加
const
进行约束。
- 当向 vector 容器插入类时,会有以下过程:
注意迭代器失效的问题。
一般就是不要在遍历的过程中对容器进行插入和删除,因为容器内的迭代器的地址会发生改变(上文中的结论 1),然而遍历的过程始终是对同一个迭代器进行操作。
61. list
容器
list 容器封装了双链表。
使用 list 容器需要包含头文件:
#include <list>
list 类模板的声明:
1
2
3
4
5
6
7
8template<class T, class Alloc = allocator<T>>
class list
{
private:
iterator head;
iterator tail;
...;
}
1. 构造函数(常用的为前 5 个)
list()
:创建一个空的 list 容器。list(initializer_list<T> il)
:使用统一初始化列表。list(const list<T>& l)
: 拷贝构造函数。list(Iterator first, Iterator last)
:用迭代器创建 list 容器。list(list<T>&& l)
:移动构造函数(C11 标准)。explicit list(const size_t n)
:创建 list 容器,元素个数为n
。list(const size_t n, const T& value)
:创建 list 容器,元素个数为n
,值均为value
。不同容器的构造函数和迭代器的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
// 1. list(); // 创建一个空的list容器。
list<int> l1;
// cout << "li.capacity()=" << l1.capacity() << endl; // 链表没有容量说法。
cout << "li.size()=" << l1.size() << endl;
// 2. list(initializer_list<T> il); // 使用统一初始化列表。
list<int> l2({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
// 第二种写法:用于转换函数中 -- list<int> l2={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 第三种写法:省略 = ; list<int> l2 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int value : l2) // 用基于范围的 for 循环遍历容器。
cout << value << " ";
cout << endl;
// 3. list(const list<T>& l); // 拷贝构造函数。
list<int> l3(l2);
// list<int> l3=l2;
for (int value : l3)
cout << value << " ";
cout << endl;
// 4. list(Iterator first, Iterator last); // 用迭代器创建 list 容器。
// 用 list3 容器的迭代器创建 list4 容器。
// 注意:list 容器的迭代器不支持随机访问
list<int> l4(l3.begin(), l3.end());
for (int value : l4)
cout << value << " ";
cout << endl;
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 创建vector容器。
// 用 vector 容器的迭代器创建 list 容器。
// 和上面进行区别,vector 容器的迭代器支持随机访问
list<int> l5(v1.begin() + 2, v1.end() - 3);
for (int value : l5)
cout << value << " ";
cout << endl;
int a1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 创建数组。
// 用数组的指针作为迭代器创建list容器。
// 数组指针是天然的随机访问迭代器,但他不支持其他迭代器的那些函数,例如 begin()
list<int> l6(a1 + 2, a1 + 10 - 3);
for (int value : l6)
cout << value << " ";
cout << endl;
// 迭代器在不同容器之间转换
char str[] = "hello world"; // 定义 C 风格字符串。
string s1(str + 1, str + 7); // 用 C 风格字符串创建 string 容器。
for (auto value : s1) // 遍历 string 容器。
cout << value << " ";
cout << endl;
cout << s1 << endl; // 以字符串的方式显示 string 容器。
vector<int> v2(l3.begin(), l3.end()); // 用 list 迭代器创建 vector 容器。
for (auto value : v2) // 遍历 vector 容器。
cout << value << " ";
cout << endl;
}
2. list 容器的特性操作
size_t size() const
:返回容器的实际大小(已使用的空间)。bool empty() const
:判断容器是否为空。void clear()
:清空容器。void resize(size_t size)
:把容器的实际大小置为size
。
3. 元素操作(链表只有这四个操作)
T &front()
:第一个元素。const T &front()
:第一个元素,只读。const T &back()
:最后一个元素,只读。T &back()
:最后一个元素。
4. 赋值操作(链表和数组是一样的)
list &operator=(const list<T> &l)
:把容器l赋值给当前容器。list &operator=(initializer_list<T> il)
:用统一初始化列表给当前容器赋值。list assign(initializer_list<T> il)
:使用统一初始化列表赋值。list assign(Iterator first, Iterator last)
:用迭代器赋值。
5. 交换、反转、排序、归并
void swap(list<T> &l)
:把当前容器与l交换,交换的是链表结点的地址。void reverse()
:反转链表。void sort()
:对容器中的元素进行升序排序。void sort(_Pr2 _Pred)
:对容器中的元素进行排序,排序的方法由_Pred
决定(二元函数)。void merge(list<T> &l)
:采用归并法合并两个已排序的 list 容器,合并后的 list 容器仍是有序的。
6. 比较操作
bool operator == (const vector<T> & l) const`` ``bool operator != (const vector<T> & l) const
7. 插入和删除操作
void push_back(const T& value)
:在链表的尾部追加一个元素。void emplace_back(…)
:在链表的尾部追加一个元素,…
用于构造元素。C++11iterator insert(iterator pos, const T& value)
:在指定位置插入一个元素,返回指向插入元素的迭代器。iterator emplace (iterator pos, …)
:在指定位置插入一个元素,…用于构造元素,返回指向插入元素的迭代器。C11iterator insert(iterator pos, iterator first, iterator last)
:在指定位置插入一个区间的元素,返回指向第一个插入元素的迭代器。void pop_back()
:从链表尾部删除一个元素。iterator erase(iterator pos)
:删除指定位置的元素,返回下一个有效的迭代器。iterator erase(iterator first, iterator last)
:删除指定区间的元素,返回下一个有效的迭代器。- 这八个函数和 vector 容器是一样的。
push_front(const T& value)
:在链表的头部插入一个元素。emplace_front(…)
:在链表的头部插入一个元素,…
用于构造元素。C11splice(iterator pos, const vector<T> & l)
:把另一个链表连接到当前链表。splice(iterator pos, const vector<T> & l, iterator first, iterator last)
:把另一个链表指定的区间连接到当前链表。splice(iterator pos, const vector<T> & l, iterator first)
:把另一个链表从first
开始的结点连接到当前链表。void remove(const T& value)
:删除链表中所有值等于value
的元素。void remove_if(_Pr1 _Pred)
:删除链表中满足条件的元素,参数_Pred
是一元函数。void unique()
:删除链表中相邻的重复元素,只保留一个。void pop_front()
:从链表头部删除一个元素。
62. pair 键值对
pair 是类模板,一般用于表示 key/value 数据,其实现是结构体。
pair 结构模板的定义如下:
1
2
3
4
5
6
7
8
9
10template <class T1, class T2>
struct pair
{
T1 first; // 第一个成员,一般表示key。
T2 second; // 第二个成员,一般表示value。
pair(); // 默认构造函数。
pair(const T1 &val1,const T2 &val2); // 有两个参数的构造函数。
pair(const pair<T1,T2> &p); // 拷贝构造函数。
void swap(pair<T1,T2> &p); // 交换两个pair。
};make_pair 函数模板的定义如下:
1
2
3
4
5
6template <class T1, class T2>
make_pair(const T1 &first,const T2 &second)
{
// 返回一个 pair 模板
return pair<T1,T2>(first, second);
}相关知识点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59#include <iostream>
using namespace std;
int main()
{
// 创建一个空的键值对
pair<int, string> p0;
cout << "p0 first=" << p0.first << ",second=" << p0.second << endl;
// 两个参数的构造函数。
pair<int, string> p1(1, "西施1");
cout << "p1 first=" << p1.first << ",second=" << p1.second << endl;
// 运用拷贝构造函数来构造。
pair<int, string> p2 = p1;
cout << "p2 first=" << p2.first << ",second=" << p2.second << endl;
pair<int, string> p3 = { 3, "西施3" }; // 两个参数的构造函数。
pair<int, string> p3 { 3, "西施3" }; // 两个参数的构造函数,省略了等于号。
cout << "p3 first=" << p3.first << ",second=" << p3.second << endl;
// 注意:这两种方法只会调用一次有两个参数的构造函数。
// 不是先创建匿名对象,然后调用拷贝构造函数。应理解成先创建匿名对象,然后用 p4 的名字
auto p4 = pair<int, string>(4, "西施4"); // 使用匿名对象(或者理解为显式调用构造函数)。
cout << "p4 first=" << p4.first << ",second=" << p4.second << endl;
// 同理,这里也不是创建临时对象后,调用拷贝构造函数。
// 根本原因在于,在 VS 的函数中,返回临时对象和返回局部对象是不一样的。
// 返回临时对象:直接 return xxxx;而返回局部对象是:var a = ?; return a;
// 如果返回的是局部对象,那么就会是一开始那错误的理解方法
// 但是在 Linux 中,返回临时对象和返回局部对象这两者是一样的
auto p5 = make_pair<int, string>(5, "西施5"); // make_pair()返回的临时对象进行构造。
cout << "p5 first=" << p5.first << ",second=" << p5.second << endl;
pair<int, string> p6 = make_pair(6, "西施6"); // 慎用,让make_pair()函数自动推导,再调用拷贝构造,再隐式转换。
cout << "p6 first=" << p6.first << ",second=" << p6.second << endl;
// 慎用,让make_pair()函数自动推导,再调用拷贝构造。
// 这种用法就是滥用 auto,因为第二个参数是 const char*,而不是 string
auto p7 = make_pair(7, "西施7");
cout << "p7 first=" << p7.first << ",second=" << p7.second << endl;
p5.swap(p4); // 交换两个pair。
cout << "p4 first=" << p4.first << ",second=" << p4.second << endl;
cout << "p5 first=" << p5.first << ",second=" << p5.second << endl;
// pair 也可以用于存放结构体数据
struct student
{
string name;
int age;
double height;
};
pair<int, student> p = { 3,{"张三",23,48.6} };
cout << "p first=" << p.first << endl;
cout << "p second.name=" << p.second.name << endl;
cout << "p second.age=" << p.second.age << endl;
cout << "p second.height=" << p.second.height << endl;
}
63. Map 容器
Map 容器封装了红黑树(平衡二叉排序树),用于查找。
使用时需要包含头文件:
#include<map>
Map 容器的元素是 pair 键值对。
Map 类模板的声明:
1
2
3
4
5template <class K, class V, class P = less<K>, class _Alloc = allocator<pair<const K, V >>>
class map : public _Tree<_Tmap_traits< K, V, P, _Alloc, false>>
{
…
}- 第一个模板参数
K
:key 的数据类型(pair.first)。 - 第二个模板参数
V
:value 的数据类型(pair.second)。 - 第三个模板参数
P
:排序方法,缺省按 key 升序。 - 第四个模板参数
_Alloc
:分配器,缺省用new
和delete
。
- 第一个模板参数
Map 提供了双向迭代器,采用了二叉链表:
1
2
3
4
5
6
7struct BTNode
{
pair<K,V> p; // 键值对。
BTNode *parent; // 父节点。
BTNode *lchirld; // 左子树。
BTNode *rchild; // 右子树。
};
1. 构造函数
map()
:创建一个空的 Map 容器。map(initializer_list<pair<K,V>> il)
:使用统一初始化列表。map(const map<K,V>& m)
:拷贝构造函数。map(Iterator first, Iterator last)
:用迭代器创建 Map 容器。map(map<K,V>&& m)
:移动构造函数(C11 标准)。
2. 特性操作
size_t size() const
:返回容器的实际大小(已使用的空间)。bool empty() const
:判断容器是否为空。void clear()
:清空容器。
3. 元素操作
V &operator[](K key)
:用给定的 key 访问元素。
const V &operator[](K key) const
:用给定的 key 访问元素,只读。V &at(K key)
:用给定的 key 访问元素。
const V &at(K key) const
:用给定的 key 访问元素,只读。- 注意:
[ ]
运算符:如果指定键不存在,会向容器中添加新的键值对;如果指定键存在,则读取或修改容器中指定键的值。at()
成员函数:如果指定键不存在,不会向容器中添加新的键值对,而是直接抛出 out_of_range 异常。
4. 赋值操作
- 给已存在的容器赋值,将覆盖容器中原有的内容。
map<K,V> &operator=(const map<K,V>& m)
:把容器m
赋值给当前容器。map<K,V> &operator=(initializer_list<pair<K,V>> il)
:用统一初始化列表给当前容器赋值。
5. 交换操作
void swap(map<K,V>& m)
:把当前容器与m
交换。交换的是树的根结点。
6. 比较操作
bool operator == (const map<K,V>& m) const
bool operator != (const map<K,V>& m) const
7. 查找操作
- 查找键值为 key 的键值对
在 Map 容器中查找键值为 key 的键值对,如果成功找到,则返回指向该键值对的迭代器;失败返回end()
。
iterator find(const K &key)
const_iterator find(const K &key) const
:只读。 - 查找键值
>=key
的键值对
在 Map 容器中查找第一个键值>=key
的键值对,成功返回迭代器;失败返回end()
。
iterator lower_bound(const K &key)
const_iterator lower_bound(const K &key) const
:只读。 - 查找键
>key
的键值对
在 Map 容器中查找第一个键值>key
的键值对,成功返回迭代器;失败返回end()
。
iterator upper_bound(const K &key)
const_iterator upper_bound(const K &key) const
:只读。 - 统计键值对的个数
统计map容器中键值为key的键值对的个数。
size_t count(const K &key) const
8. 插入和删除
void insert(initializer_list<pair<K,V>> il)
:用统一初始化列表在容器中插入多个元素。pair<iterator,bool> insert(const pair<K,V> &value)
:在容器中插入一个元素,返回值pair
。pair.first
是已插入元素的迭代器,pair.second
是插入结果。1
2
3
4
5
6
7
8
9
10
11
12// 返回值为 pair
auto ret = map.insert(pair<int, string>(1, "张三"));
// pair 的 second 是插入结果
if(ret.second == true)
{
// pair.first 是已插入元素的迭代器
cout << "插入成功" << ret.first->first << ", " << ret.first->second << endl;
}
else
{
cout << "插入失败" << endl;
}void insert(iterator first,iterator last)
:用迭代器插入一个区间的元素。pair<iterator,bool> emplace (...)
:和第二点类似(但效率更高)。将创建新键值对所需的数据作为参数直接传入,Map 容器将直接构造元素。返回值和第二点一样。iterator emplace_hint (const_iterator pos,...)
:功能与第 4 个函数相同,第一个参数提示插入位置,该参数只有参考意义,如果提示的位置是正确的,对性能有提升,如果提示的位置不正确,性能反而略有下降,但是,插入是否成功与该参数元关。该参数常用end()
和begin()
。成功返回新插入元素的迭代器;如果元素已经存在,则插入失败,返回现有元素的迭代器。size_t erase(const K & key)
:从容器中删除指定 key 的元素,返回已删除元素的个数。iterator erase(iterator pos)
:用迭代器删除元素,返回下一个有效的迭代器。iterator erase(iterator first,iterator last)
:用迭代器删除一个区间的元素,返回下一个有效的迭代器。
64. unordered_map 容器
本质上就是 hash 表。
使用时包含头文件:
#include<unordered_map>
unordered_map 容器的元素是 pair 键值对
unordered_map 类模板的声明:
1
2
3
4
5
6
7
8template <class K,
class V,
class _Hasher = hash<K>,
class _Keyeq = equal_to<K>,
class _Alloc = allocator<pair<const K, V>>> class unordered_map : public _Hash<_Umap_traits<K, V, _Uhash_compare<K, _Hasher, _Keyeq>, _Alloc, false>>
{
…
}第一个模板参数
K
:key
的数据类型(pair.first
)。第二个模板参数
V
:value
的数据类型(pair.second
)。第三个模板参数
_Hasher
:哈希函数,默认值为std::hash<K>_
第四个模板参数
_Keyeq
:比较函数,用于判断两个key
是否相等,默认值是std::equal_to<K>
。第五个模板参数
_Alloc
:分配器,缺省用new
和delete
。
创建
std::unordered_map
类模板的别名:1
2template<class K,class V>
using umap = std::unordered_map<K, V>;map 容器和 umap 容器的各种操作,基本上是相同的,只有一些不同。(有问题查文档吧,笔记实在是太多了)
1. 特性操作(有区别的地方)
size_t size() const
:返回容器中元素的个数。bool empty() const
:判断容器是否为空。void clear()
:清空容器。- 前三个和其他容器是一样的。
size_t bucket_count()
:返回容器桶的数量,空容器有 8 个桶。float load_factor()
:返回容器当前的装填因子,load_factor() = size() / bucket_count()
。float max_load_factor()
:返回容器的最大装填因子,达到该值后,容器将扩充,缺省为 1。装填因子可以超过 1。
需要注意的是,当装填因子超过 1 时,每个桶中的元素不止一个。可通过迭代器访问桶中的元素。void max_load_factor (float z )
:设置容器的最大装填因子。
在 Hash 表中,桶是数组,桶中的元素是链表。因此 Hash 表如果要扩容,必须像 vector 一样重新分配内存,然后重新 Hash 和散列,将元素分配到不同的桶中。iterator begin(size_t n)
:返回第n
个桶中第一个元素的迭代器。iterator end(size_t n)
:返回第n
个桶中最后一个元素尾后的迭代器。void reserve(size_t n)
:将容器设置为至少n
个桶。void rehash(size_t n)
:将桶的数量调整为>=n
。如果n
大于当前容器的桶数,该方法会将容器重新哈希;如果n
的值小于当前容器的桶数,该方法可能没有任何作用。size_t bucket_size(size_t n)
:返回第n
个桶中元素的个数,0 <= n < bucket_count()
。size_t bucket(K &key)
:返回值为key
的元素对应的桶的编号。
65. queue 容器
queue 容器的逻辑结构是队列,物理结构可以是数组或链表,主要用于多线程之间的数据共享。
使用时需要包含头文件:
#include<queue>
queue 类模板的声明:
1
2
3
4template <class T, class _Container = deque<T>>
class queue{
……
}- 第一个模板参数
T
:元素的数据类型。 - 第二个模板参数
_Container
:底层容器的类型,缺省是std::deque
,可以用std::list
,还可以用自定义的类模板。
即 queue 是对容器的二次封装。
需要注意的是,底层容器的类型不能是 vector。
- 第一个模板参数
queue 容器不支持迭代器。
1. 构造函数
queue()
:创建一个空的队列。queue(const queue<T>& q)
:拷贝构造函数。queue(queue<T>&& q)
:移动构造函数(C11 标准)。
析构函数~queue()
释放内存空间。- 其他操作看文档吧。
66. STL 其他容器(需要使用时查文档)
1. array(静态数组)
- 在栈上分配内存,创建数组的时候,数组长度必须是常量,创建后的数组大小不可变。
- 部分场景中,比常规数组更方便(能用于模板),可以代替常规数组。
2. deque(双端队列)
- deque 容器存储数据的空间是多段等长的连续空间构成,各段空间之间并不一定是连续的。
- 为了管理这些连续空间的分段,deque 容器用一个数组存放着各分段的首地址。
- 通过建立数组,deque容器的分段的连续空间能实现整体连续的效果。当 deque 容器在头部或尾部增加元素时,会申请一段新的连续空间,同时在数组中添加指向该空间的指针。
- 特点:
- 提高了在两端插入和删除元素的效率,扩展空间的时候,不需要拷贝以前的元素。
- 在中间插入和删除元素的效率比 vector 更糟糕。
- 随机访问的效率比vector容器略低。
3. forward_list(单链表)
4. multimap
- 底层是红黑树。
- multimap 和 map 的区别在:multimap 允许关键字重复,而 map 不允许重复。各种操作与map容器相同。
5. set & multiset
- 底层是红黑树
- set 和 map 的区别在:map 中存储的是键值对,而 set 只保存关键字。
- multiset 和 set 的区别在:multiset 允许关键字重复,而 set 不允许重复。
- 这两个的各种操作与 map 容器相同。
6. unordered_multimap
- 底层是哈希表
- unordered_multimap 和 unordered_map 的区别在:unordered_multimap 允许关键字重复,而 unordered_map 不允许重复。
- 各种操作与 unordered_map 容器相同。
7. unordered_set & unordered_multiset
- 底层是哈希表。
- unordered_set 和 unordered_map 的区别在:unordered_map 中存储的是键值对,而 unordered_set 只保存关键字。
- unordered_multiset 和 unordered_set 的区别在:unordered_multiset 允许关键字重复,而 unordered_set 不允许重复。
- 各种操作与 unordered_map 容器相同。
8. priority_queue(优先队列)
- 优先级队列相当于一个有权值的单向队列 queue,在这个队列中,所有元素是按照优先级排列的。
- 底层容器可以用 deque 和 list。
- 各种操作与 queue 容器相同。
9. stack(栈)
- 底层容器可以用 deque 和 list。