C是面向过程的程序设计语言,而C++是面向对象的程序设计语言
在C语言中动态分配和释放内存,使用malloc/calloc/realloc和free函数,在C++语言中动态分配和释放内存,则使用new和delete操作符
C语言提供了指针但没有提供引用,C++语言既提供了指针也提供了引用
C语言中没有类的概念,C++语言中有类的概念,并基于类提供了封装、继承、多态等面向对象特性
C语言中的函数不能重载,C++语言中的函数可以重载
C语言中没有模板,C++语言中有模板,并基于模板提供了泛型化的容器、迭代器及操作容器的算法库
数组名和数组名的地址具有相同的值,都是数组中首元素的地址
xxxxxxxxxx
41int a[4] = {10, 20, 30, 40};
2printf("%p\n", a); // 0061FF08
3printf("%p\n", &a); // 0061FF08
4printf("%p\n", &a[0]); // 0061FF08
数组名和数组名的地址具有不同的数据类型,数组名是指向数组首元素的指针,而数组名的地址则是指向整个数组的指针
xxxxxxxxxx
21int* p = a;
2int (*q)[4] = &a;
数组名和数组名的地址具有不同的指针计算规则,数组名加一只是增加一个数组元素的字节数,而数组名的地址加一则增加了整个数组的字节数
xxxxxxxxxx
21printf("%p\n", a + 1); // 0061FF0C
2printf("%p\n", &a + 1); // 0061FF18
对数组名和数组名的地址做解引用操作的结果不同,对数组名解引用得到的是数组的首元素,而对数组名的地址解引用得到的是数组首元素的地址
xxxxxxxxxx
31printf("%d\n", *a); // 10
2printf("%p\n", *&a); // 0061FF08
3printf("%d\n", **&a); // 10
数组名的地址可通过强制类型转换获得与解引用相同的效果,但数组名却不行
xxxxxxxxxx
21printf("%d\n", *(int*)&a); // 10
2printf("%08X\n", (int)a); // 0061FF08
被static关键字修饰的全局变量:静态存储区中分配内存,仅在定义该变量的源代码文件中可以访问
被static关键字修饰的成员变量:静态存储区中分配内存,为定义该变量的类及其子类的所有对象共享
被static关键字修饰的局部变量:静态存储区中分配内存,定义该变量的函数首次被调用时初始化,之后再次调用该函数,变量中保存的仍是上次调用该函数时遗留的值
被static关键字修饰的全局函数:仅在定义该函数的源代码文件中可以调用
被static关键字修饰的成员函数:不接受this指针,只能访问静态成员,无需实例化对象即可访问
处理主体不同:宏在编译预处理阶段由预编译器处理,而const关键字则在编译阶段由编译器处理
类型检查不同:宏扩展的本质是源代码级的文本替换,不涉及类型检查,而const是一个类型修饰符,编译器会执行一系列类型安全检查
内存分配不同:通过宏定义的常量在编译后,通常以立即数的形式,作为指令的一部分,位于代码区中,常量本身不占用独立的内存空间,而用const表示的常量,则往往会根据其数据类型,分配相应字节数的内存空间,或在数据区中,或在栈区中
作用域不同:通过宏定义的常量不受作用域的限制,而用const表示的常量则仅在其作用域内可被访问
频繁调用的简短函数适合被声明为内联函数
内联函数的本质是用函数编译后的二进制代码,替换对该函数的调用指令,降低函数调用和返回过程中的时间开销,提高运行效率
内联函数的代价是增大了整个程序代码的体积,而且一旦内联函数被修改,所有调用该函数的代码都必须重新编译
只有频繁调用的简短函数,内联才是划算的,因为它们的总执行时间远小于总调用时间,而代码量本身并不大,只需牺牲少量的空间,就能换取更多的时间
稀少调用的复杂函数是不适合内联的,这样的函数总执行时间比总调用时间长得多,而代码量却很大,牺牲了大量的空间,所获得的时间性能提升却并不明显
某些函数是不可能被处理为内联的,如递归函数、虚函数等
通过inline关键字将函数声明为内联,只是一种建议,会否真正被内联由编辑器决定,实际上并不可控
智能指针是一个封装了平凡指针的类类型对象,当其离开作用域时析构函数会被执行,该析构函数负责释放平凡指针所指向的内存
智能指针的作用在于规避内存泄漏的风险,同时兼顾释放悬空指针导致的重析构问题
C++的智能指针分为以下四种:
auto_ptr:独占式拥有,即同一时间只能有一个智能指针指向特定的对象。存在以下问题:
以转移实现复制,在函数传参时,对象所有权不会返还,导致潜在的内存崩溃风险
不能指向数组,也不能作为STL容器的元素
unique_ptr:独占式拥有,即同一时间只能有一个智能指针指向特定的对象。有如下改进:
禁止拷贝构造和拷贝赋值
支持移动构造和移动赋值
shared_ptr:共享式拥有,即允许多个智能指针指向同一个对象。基于引用计数的生命期管理:
每增加一个指向特定对象的智能指针,该对象的引用计数即加一
每减少一个指向特定对象的智能指针,该对象的引用计数即减一
当引用计数减至零时,即没有智能指针指向该对象,该对象所占用的资源即被释放
weak_ptr:针对特定对象的弱引用,可以绑定到某个已有的shared_ptr之上,但不增加其目标对象的引用计数。用于解决使用shared_ptr时潜在的内存泄漏问题:
两个shared_ptr分别指向两个不同的对象
这两个对象中各包含一个指向对方的shared_ptr
这两个对象的引用计数都是2
当这两个shared_ptr离开作用域时,两个对象的引用计数都会变成1
但不会变成0,因此对象不会被析构,形成内存泄漏
只要将两个对象中的任何一个指向对方的shared_ptr换成weak_ptr就可以解决此问题
shared_ptr在构造函数中,将目标对象的引用计数初始化为1
shared_ptr在拷贝构造函数中,将目标对象的引用计数加1
shared_ptr在拷贝赋值操作符函数中,将右操作数目标对象的引用计数加1,同时将左操作数目标对象的引用计数减1
shared_ptr在析构函数中,将目标对象的引用计数减1
shared_ptr在拷贝赋值操作符函数和析构函数中,将引用计数被减至0的目标对象予以释放
右值引用的主要作用就是实现转移语义和完美转发,规避对象间不必要的内存复制,同时使泛型函数的定义更加简洁明确
野指针:定义但未初始化的指针称为野指针
悬空指针:目标内存已被释放的指针称为悬空指针
空指针:值为零的指针称为空指针
对野指针或悬空指针做解引用,其结果将是未定义的,可能导致崩溃,可能引发逻辑错误,也可能一切正常,这将为程序带来极大的不确定性
解引用一个空指针一定会导致程序崩溃,这有助于在开发和测试阶段发现潜在的指针错误
良好的编程习惯是在定义指针的同时将其初始化为空,并在释放指针的目标内存后立即将其置空
静态链接是指将被调用函数的二进制代码与调用该函数的代码一起打包到可执行文件或库文件中
优点:所发布的程序不需要依赖库即可独立运行
缺点:程序的体积通常较大,完全相同的函数代码会在所有使用该函数的程序中都存在一份拷贝,而且被调用函数一旦修改,整个程序必须重新编译链接
动态链接是指将被调用函数单独编译链接成独立的库文件,在调用该函数的代码中插入一段指令,待到程序运行时执行这段指令,将库文件加载到内存,找到函数入口并完成调用
优点:程序的体积通常较小,多个程序可以共享库中的函数代码,更新库不需要重新链接程序
缺点:发布程序时需要提供所依赖的库,加载库文件占用运行时间,影响程序的前期执行性能
变量的定义通常伴随着内存和地址的分配,有时还需要初始化
变量的声明只是告诉编译器这个标识符是什么,与内存和地址的分配无关,与初始化亦无关
全局变量需要通过extern关键字声明
类的静态成员变量需要在类的外部单独定义并初始化
局部变量和成员变量无需单独声明,定义即声明
借助条件编译,可使部分代码只在满足特定条件时被编译,而在条件不满足时被编译器忽略
例如:
xxxxxxxxxx
51
2 mediaPlayer.setHwnd(frmVideo.winId());
3
4 mediaPlayer.SetXwindow(frmVideo.winId());
5
又如:
xxxxxxxxxx
81
2
3
4class MainWindow {
5 ...
6};
7
8
int
xxxxxxxxxx
11if (n == 0)
xxxxxxxxxx
11if (n != 0)
bool
xxxxxxxxxx
11if (b)
xxxxxxxxxx
11if (!b)
float
xxxxxxxxxx
11if (-1e-6 < f && f < 1e-6)
xxxxxxxxxx
11if (f <= -1e-6 || 1e-6 <= f)
指针
xxxxxxxxxx
11if (p == NULL)
xxxxxxxxxx
11if (p != NULL)
结构体类型的变量之间可以直接赋值,赋值的过程相当于内存复制
当结构体中包含指针型成员时,该结构体变量的赋值需要格外小心
结构体型变量的赋值只是浅拷贝,即只复制指针型成员本身,而不复制该指针的目标对象
当有多个指针指向同一段内存时,需要特别关注由此导致的数据耦合和重析构问题
sizeof是一个操作符,用于计算操作数类型的字节数;strlen是一个标准库函数,用于计算C风格字符串以字节为单位的长度
sizeof的操作数可以是数据类型,也可以是变量或表达式,但不对表达式求值;strlen的参数是一个指向空字符结尾字符串的字符指针,或一个包含空字符结尾字符串的字符数组
sizeof由编译器在编译阶段处理;strlen在程序运行时执行
将数组名作为sizeof的操作数,类型不发生退化,得到的是整个数组的字节数;将数组名作为strlen的参数,会退化为指针类型
static关键字在C语言中,可以修饰局部变量、全局变量和函数,而在C++语言中,除上述以外,还可以修饰类的成员变量和成员函数
C语言中的静态局部变量,可以在被不同时间调用的同一个函数间,实现数据通信,而C++语言中的静态成员变量,则可以在同一个类的不同对象间,实现数据通信
volatile关键字是一个类型修饰符,被volatile关键字修饰的变量具有挥发性或易变性,即该变量的值可能因某些编译器未知的因素而改变。这些因素包括但不限于:操作系统、硬件设备、其它线程等
对于具有挥发性的变量,编译器会放弃对该变量的访问优化,以确保每次都小心地从特定地址重新读取该变量的值,而不使用保存在寄存器中的备份
volatile关键字经常用于以下场合:
并行设备的硬件寄存器(如状态寄存器)
被中断服务例程(如信号处理函数)访问的全局变量和静态局部变量
被多个线程共享的变量
volatile关键字在嵌入式领域使用得比较多,在一般应用开发,尤其是涉及到多线程的场合,也会用到
一个变量可以同时被const关键字和volatile关键字修饰,既不变又易变,二者并无矛盾
const关键字强调该变量不应在程序代码中被修改
volatile关键字强调该变量可能在程序代码以外(如硬件中)被修改
const关键字仅在编译期起作用,旨在告诉编译器该变量的值不可被修改,它并没有禁止该变量内存被写入的能力
全局变量的生命周期自程序启动时开始,至程序终止时结束,在程序代码的任何位置都可以访问
局部变量的生命周期自模块入口处开始,至模块出口处结束,仅在定义该变量的模块内可以访问
操作系统和编译器是根据变量在进程内存空间中的位置,区分它是全局变量还是局部变量的,前者位于静态存储区,而后者则位于栈区
函数功能不同
strcpy用于复制字符串的内容
sprintf用于将其它类型的数据格式化为字符串
memcpy用于复制内存块中的字节序列
操作对象不同
strcpy的操作对象是两个字符串,一个目的字符串,一个源字符串
sprintf的操作对象是一个目的字符串,和不定数量的被格式化数据
memcpy的操作对象是两个内存地址,一个目的地址,一个源地址
执行效率不同
memcpy的执行效率最高
strcpy的执行效率居中
sprintf的执行效率最低
(*(void(*)())0)()
语句的含义(*(void(*)())0)()
语句是将0作为一个不带参数且没有返回值的函数的入口地址,并调用该函数
void(*)()
是一个函数指针类型,该类型的指针指向一个不带参数且没有返回值的函数
(void(*)())0
是一个强制类型转换,将0转换为上述函数指针类型
*(void(*)())0
是对上述值为0的函数指针做解引用,表示该指针所指向的函数
(*(void(*)())0)()
是对上述函数的调用
指针可以不初始化,其目标可在初始化后改变,除非指针本身带有const属性;引用必须初始化,且一旦初始化就无法改变其目标,引用本身不能也无需带有const属性
可以定义指针的指针,不能定义引用的引用
可以定义指针的引用,不能定义引用的指针
可以定义指针数组,不能定义引用数组,可以定义数组引用
引用的本质是指针
用法不同
typedef用于为已有的数据类型定义别名
define用于定义表示常量或常用代码的宏
执行时间不同
typedef在编译期间处理,编译器会执行相关的类型检查
define在预编译阶段处理,预处理器只做简单的文本替换,不做类型检查
作用域不同
typedef定义的类型别名,只在该定义所在的模块中有效
define定义的宏,在该定义之后的任何代码中都可使用,不受作用域的限制
写法不同
typedef是一条语句,必须以分号结尾
define是一条预编译指令,不能以分号结尾
常量指针即指向常量的指针,其定义形式类似“const int* p
”或“int const* p
”,强调的是指针的目标具有常属性,即无法通过这样的指针修改其目标变量的值
指针常量即指针类型的常量,其定义形式类似“int* const p
”,强调的是指针本身具有常属性。这样的指针一经初始化,就无法修改其中保存的地址,即无法再指向其它变量
也可以定义常量指针常量,形如“int const* const p
”,这样的指针,无论是其指向的目标,还是指针本身,都不可被修改
堆区:进程内存空间的特定区域,该区域中的内存由程序员手动分配和释放,若不释放,操作系统会在进程结束后自动回收。堆区中的内存以类似链表的方式,遵循后进先出的规则分配和释放
栈区:进程内存空间的特定区域,该区域中的内存由编译器自动分配和释放,用于存储函数的参数、局部变量以及返回值等。栈区中的内存以类似数组的方式,遵循后进先出的规则分配和释放
堆栈:一种数据结构,可以链表或数组的方式实现,遵循后进先出的规则,压入和弹出数据
队列:一种数据结构,可以链表或数组的方式实现,遵循先进先出的规则,压入和弹出数据
先将0x67a9强制类型转换为整型指针,再通过解引用该指针,将0xaa66存入其目标内存中
xxxxxxxxxx
11*(int*)0x67a9 = 0xaa66;
任何时候,一个整型数据总可以被强制转换为指针类型,只要该指针的目标内存有效即可
C语言的结构体中只能定义成员变量,而C++的结构体中除了可以定义成员变量,还可以定义成员函数、构造函数、析构函数等
C语言的结构体中不能使用public、private、protected等访问控制限定符,但C++可以,且缺省访问控制限定符为public,与C兼容
C语言的结构体不能从另一个结构体继承,但C++可以
C语言的结构体只是一到多个类型相同或不同的数据成员的简单集合,C++的结构体其实就是类,除了缺省访问控制限定符为public而非private外,与类无异,而类是C++语言实现面向对象特性的语法基础
句柄是某个内存对象的标识,通过句柄可以引用其所代表的对象,但不能直接访问该对象所在的内存。句柄在保证对象可被使用的同时,又防止对象被有意或无意地破坏
指针是某个内存对象的地址,通过指针可以引用其所代表的对象,也可以直接访问该对象所在的内存。指针没有隐藏对象细节的功能,无法防止对象在使用过程中被破坏
extern "C"
的作用是什么?extern "C"
主要用于在C和C++语言代码间相互调用的场合
C++语言支持函数重载,因此编译器在编译函数时,会将函数的参数信息与函数的视在函数名组合编码为该函数的实际函数名,是为C++换名
C语言不支持函数重载,也不会对代码中的函数进行换名,编译后的实际函数名,与代码中的视在函数名完全一样
在C和C++语言代码相互调用时,实际函数名的不同将引发链接错误
extern "C"
的作用是告诉C++编译器不要对其后面的函数做换名,而是按照C语言的方式处理函数入口,这样在和C语言代码相互调用时,就不会因函数名不一致而引发错误
extern "C"
的使用方法
C++调C:在C++代码中,于被调用C函数的声明语句,或包含被调用C函数声明语句的头文件之前,使用extern "C"
C调C++:在C++代码中,于被调用C++函数的声明语句之前,使用extern "C"
,如果没有声明语句,就在该函数定义的函数头之前,使用extern "C"
可以在extern "C"
后面使用一对花括号,括起多个函数
函数调用约定时对函数调用的一系列约束和规定,具体包括:
函数参数进入调用栈的顺序
调用栈由被调函数在返回前清空,还是由调用者在返回后清空
调用过程中是否使用寄存器,使用哪个寄存器
函数修饰名的生成方法
函数调用约定包括如下几种:
__cdecl:C语言标准调用约定
参数从右至左依次入栈
调用栈由调用者在返回后清空
返回值位于EAX寄存器中
函数修饰名因编译器而异,如:_函数名
__stdcall:C++语言标准调用约定
参数从右至左依次入栈,对于类的成员函数,最后入栈的是this指针
调用栈由被调函数在返回前清空
返回值位于EAX寄存器中
函数修饰名因编译器而异,如:_函数名@参数信息
__fastcall:快速调用约定
借助ECX和EDX寄存器传递前两个双字或更小的参数,其它参数从右至左依次入栈
调用栈由被调函数在返回前清空
返回值位于EAX寄存器中
函数修饰名因编译器而异,如:@函数名@参数信息
__thiscall:C++类成员函数调用约定,仅限编译内部使用,不能写在代码中
参数从右至左依次入栈,如果参数个数确定,则借助TCX寄存器传递this指针,否则在所有参数入栈后压入this指针
如果参数个数确定,调用栈由被调函数在返回前清空,否则由调用者在返回后清空
返回值位于EAX寄存器中
函数修饰名因编译器而异,如:_函数名@参数信息
__pascal:同__stdcall
封装:将客观事物的属性和行为,以成员变量和成员函数的形式包装到类中,并通过必要的访问控制限定,将可被外部访问的部分暴露出来,而将不希望被外部访问的部分隐藏起来,以确保对象的完整性和自洽性
继承:最大程度地利用已有类的功能,并在此基础上扩展新功能,满足新需求,提高代码的可复用性
多态:一种操作在不同的场合表现出多样的行为,多个实现共享同一套外部接口,提高代码的抽象性
public:公有成员可被包括类的内部、子类及间接子类、其它类或函数等在内的任何代码访问
private:私有成员只能在类的内部被访问,子类及间接子类、其它类或函数都不能访问
protected:保护成员可在类的内部及子类中被访问,不能被其它类或函数访问
动态多态:借助继承与虚函数实现的多态,在运行期间确定,称为动态多态
静态多态:借助重载和模板实现的多态,在编译期间确定,称为静态多态
作用
隐藏实现细节,减少代码耦合,提高程序的抽象性
是基类的代码能够调用子类的实现,提高程序的可扩展性
条件
从包含虚函数的基类中派生出子类
在子类中实现对基类虚函数的有效覆盖
通过指向子类对象的基类指针,或引用子类对象的基类引用,调用虚函数
当编译器发现一个类中含有虚函数时,会为该类生成一张虚函数表,表中存放虚函数的入口地址
当子类继承父类时也会继承父类的虚函数表,并用子类中覆盖版本虚函数的入口地址替换虚函数表中基类版本虚函数的入口地址
当包含虚函数的类被实例化为对象时,会在该对象的内部创建一个指针,指向该类的虚函数表
当通过一个指针或引用调用虚函数时,会根据该指针或引用的目标对象中的虚表指针,找到虚函数表,再从虚函数表中找到所调用虚函数的入口地址,完成对虚函数的调用
纯虚函数代表一种抽象的行为,无法给出具体的定义
包含纯虚函数的类称为抽象类,抽象类无法实例化为对象
抽象类的直接或间接子类覆盖基类中的纯虚函数,并给出具体实现
编写纯虚函数只需在函数声明的结尾处加上“=0”即可
虚函数表是针对类而非对象的,一个类一张虚函数表
该类的所有对象通过自己的虚表指针,共享这张虚函数表
类的构造函数不能被声明为虚函数
虚函数调用需要依赖虚函数表,而指向虚函数表的虚表指针需要在构造函数中初始化,因此构造函数本身不能是虚函数
类的析构函数被执行时对象已经存在,根据虚表指针获得虚函数表,再从虚函数表中获得虚析构函数的入口地址,完成对虚析构函数的调用。因此类的析构函数完全可以被声明为虚函数
在一个存在多态继承关系的父子类之间,将基类的析构函数声明为虚函数是有必要的,因为只有这样才能保证当delete一个指向子类对象的基类指针时,子类的析构函数被执行,否则被执行的只能是基类的析构函数,子类对象中的特有资源将形成内存泄漏
在类的构造函数中可以抛出异常,一旦异常被抛出,所得到就是一个不完整对象,不完整对象的析构函数是不会被执行的,因此在抛出异常之前,必须显式释放所有已分配的资源,否则将形成内存泄漏
在类的析构函数中不要抛出异常,一旦异常被抛出,析构函数就会被执行,然后再次抛出异常,上一个异常还未被处理又抛出新的异常,这个过程会一直持续下去,最终导致程序崩溃
在类中声明一个纯虚函数,如果实在没有哪个函数适合作为纯虚函数,就把析构函数声明为纯虚函数
将类的构造函数声明为私有成员函数
一个子类的多个基类中如果包含同名成员,对该成员的访问将导致同名二义性问题。解决方法:
通过作用域限定符“::”显式指明所引用的标识符源自那个基类
在子类中定义同名成员,隐藏所有基类中的同名成员
一个子类的多个基类如果继承自同一根类,子类对象的每个基类子对象中都有一个根类子对象,沿不同继承路径访问根类对象将导致路径二义性问题。解决方法:
以虚继承的方式继承根类,子类对象的多个基类子对象共享同一个根类子对象
对一个空类求sizeof会得到1
空类也可以被实例化为对象,只要是对象就必然要占用内存并获得地址,因此至少需要一个字节
重载发生在同一个作用域中,名字相同参数表不同的多个函数构成重载关系
覆盖发生在类的继承结构中,子类对基类中的虚函数进行覆盖,覆盖版本与基类版本拥有完全相同的原型,即函数名、参数表、返回值类型完全相同
隐藏发生在类的继承结构中,子类中存在与基类完全相同的标识符
拷贝构造函数用于创建已有对象的副本,被构造的副本对象在构造之前并不持有资源,因此只需分配足够的资源并复制源对象的内容即可
拷贝赋值操作符函数用于将源对象的内容复制到目标对象中,目标对象在接受赋值前是持有资源的,因此需要先释放已有的资源,再分配足够的资源并复制源对象的内容
一般而言,但凡存在指针型成员变量的类,都需要考虑为其提供自定义的构造函数、析构函数、拷贝构造函数和拷贝赋值操作符函数
如果将一个类中的某个除构造函数以外的非静态成员函数声明为虚函数,并在该类的子类中提供对此虚函数的有效覆盖,那么当通过一个指向子类对象的基类指针,或一个引用子类对象的基类引用调用此虚函数时,被调用的将是子类中的覆盖版本,而非基类中的原始版本,这种语法现象称为动态多态
当编译器发现一个类中含有虚函数时,会为该类生成一张虚函数表,表中存放虚函数的入口地址。当子类继承父类时也会继承父类的虚函数表,并用子类中覆盖版本虚函数的入口地址替换虚函数表中基类版本虚函数的入口地址。当包含虚函数的类被实例化为对象时,会在该对象的内部创建一个指针,指向该类的虚函数表。当通过一个指针或引用调用虚函数时,会根据该指针或引用的目标对象中的虚表指针,找到虚函数表,再从虚函数表中找到所调用虚函数的入口地址,完成对虚函数的调用
使用虚函数,在获得多态性的同时,也会增加程序的内存开销,降低程序的运行效率
C++语言中的struct和class都可以用于定义类。二者的区别在于,用struct关键字定义的类,其默认访问控制属性为公有,而用class关键字定义的类,其默认访问控制属性则为私有
C++语言中的class关键字除了用于定义类以外,还可用于声明模板的类型参数,与typename关键字相当,但struct关键字没有这个功能
C++语言中的struct与C语言中的struct百分之百地向下兼容,但class并没有这种兼容性要求
在C++语言中保留struct,很大程度上是为了让已有的C语言代码可以更容易地移植到C++中
静态类型转换操作符:static_cast
用于非多态类型间的转换
不做运行时类型检查
常用于基本数据类型间的转换、void*和其它类型指针间的转换
在类的继承结构中移动指针
子类到基类,向上造型,安全
基类到子类,向下造型,不安全
动态类型转换操作符:dynamic_cast
用于多态类型间的转换
执行运行时类型检查
只能用于指针或引用
转换指针失败得到空指针,转换引用失败抛出bad_cast异常
在类的继承结构中移动指针
子类到基类,向上造型,安全
基类到子类,向下造型,安全
去常类型转换操作符:const_cast
去除指针和引用上的const、volatile、__unaligned限定
重解释类型转换操作符:reinterpret_cast
用于不同类型指针或引用间的转换,但不能去除其上的const、volatile、__unaligned限定
用于指针类型和整数类型间的转换
对比项 | 重载 | 覆盖 | 隐藏 |
---|---|---|---|
范围 | 同一个类中 | 父子类中 | 父子类中 |
函数名 | 相同 | 相同 | 相同 |
参数表和const限定 | 不同 | 相同 | 相同或不同 |
返回值类型 | 相同或不同 | 相同 | 相同或不同 |
异常说明 | 相同或不同 | 相同 | 相同或不同 |
virtual关键字 | 可有可无 | 基类必须有,子类可有可无 | 可有可无 |
同时满足覆盖和隐藏条件的,算作覆盖
覆盖的其它条件都满足,仅返回值类型一项不满足,编译错误,除非返回值的类型是指针或引用,且子类版本所返回指针或引用的目标类型,是基类版本所返回指针或引用的目标类型的子类,类型协变
虽然重载和覆盖都被认为是C++语言实现多态性的基础,但二者的实现技术完全不同:
基于重载的多态性是在编译期间确定的,称为静态多态
基于覆盖的多态性是在运行期间确定的,称为动态多态
RTTI即运行时类型识别,其功能体现在以下两个操作符中:
typeid:用于获取操作数的类型信息,在多态继承结构中,借助运行时类型识别,获取一个指针或引用的实际目标对象的类型
dynamic_cast:用于多态类型间的转换,借助运行时类型识别,将一个基类类型的指针或引用安全地转换为子类类型的指针或引用
C语言的强制类型转换语法在C++语言中可以继续使用
同时C++语言又提供了诸如static_cast、dynamic_cast、const_cast和reinterpret_cast等类型转换操作符
C++语言的每种类型转换操作符都有其特定的应用场景,意图更加明确,相应的安全检查也更加完善
C++编译器会为一个空类提供如下六个缺省成员函数:
缺省构造函数和析构函数
缺省拷贝构造函数和拷贝赋值操作符函数
缺省取地址操作符函数及其const版本
出于性能优化的考虑,只有在实际使用到这些函数时,编译器才会生成相应的实现
无论是函数模板还是类模板,其设计初衷都是令其满足所有类型的功能要求,但对于某些特殊类型,其通用版本未必能完全符合预期,为此就需要针对这些类型给出模板的特殊实现,这就叫模板特例化
函数模板的通用实现和特化实现必须位于同一个头文件中,且特化实现位于通用实现之后
对类模板而言,不但可以针对整个类模板特例化,也可以只针对部分成员函数特例化,谓之成员特化
对类模板而言,不但可以针对所有类型参数特例化,也可以只针对部分类型参数特例化,谓之偏特化
在一个具有多态性的继承结构中,基类类型的指针可以指向子类类型的对象,如果该子类对象是通过new操作符动态创建的,那么当需要销毁该对象时,就应将指向它的基类指针交给delete操作符
如果基类的析构函数没有被声明为虚函数,那么基于编译器的静态绑定规则,被调用的将是基类的析构函数,基类的析构函数不会调用子类的析构函数,子类对象的特有资源得不到释放,形成内存泄漏
如果将基类的析构函数声明为虚函数,那么基于编译器的动态绑定规则,被调用的将是子类的析构函数,子类的析构函数在完成子类对象特有资源的释放后,自动调用基类的析构函数,避免了内存泄漏
因此在具有多态性的继承结构中,将基类的析构函数声明为虚函数是十分必要的,尤其是在子类的析构函数中存在与资源释放有关的操作时。即便没有这些操作,甚或非多态继承,将基类的析构函数声明为虚函数也没有坏处
类型转换构造函数,用于将参数类型的对象,转换为定义该函数的类型的对象
xxxxxxxxxx
81class Integer {
2public:
3 ...
4 Integer(int n) _n(n) {}
5 ...
6private:
7 int _n;
8};
类型转换操作符函数,用于将定义该函数的类型的对象,转换为该函数所指定的类型的对象
xxxxxxxxxx
101class Integer {
2public:
3 ...
4 operator int(void) {
5 return _n;
6 }
7 ...
8private:
9 int _n;
10};
拷贝构造函数,用于创建已有对象的同类型副本
xxxxxxxxxx
81class Integer {
2public:
3 ...
4 Integer(Integer const& i) _n(i._n) {}
5 ...
6private:
7 int _n;
8};
拷贝赋值操作符函数,用于将源对象的内容复制到目标对象中
xxxxxxxxxx
101class Integer {
2public:
3 ...
4 Integer& operator=(Integer const& i) {
5 _n = i._n;
6 }
7 ...
8private:
9 int _n;
10};
调用举例
int n = 0; | 描述 | 执行函数 |
---|---|---|
Integer i1(n); | 直接初始化 | 直接调用类型转换构造函数构造i1 |
Integer i2 = n; | 拷贝初始化 | 先调用类型转换构造函数构造一个临时对象, 再调用拷贝构造函数构造临时对象的副本i2 |
Integer i3(i2); | 直接初始化 | 直接调用拷贝构造函数构造i2的副本i3 |
Integer i4 = i3; | 拷贝初始化 | 直接调用拷贝构造函数构造i3的副本i4 |
i4 = i3; | 赋值 | 直接调用拷贝赋值操作符函数将i3的内容复制到i4 |
i4 = n; | 类型转换 | 先调用类型转换构造函数构造一个临时对象, 再调用拷贝赋值操作符函数将临时对象的内容复制到i4 |
n = i4; | 类型转换 | 先调用类型转换操作符函数将i4转换为int型临时变量 再将临时变量的值赋给n |
出于性能优化的考虑,某些编译器可能会将针对临时对象的拷贝构造过程省略
容器
线性容器
数组(array)
向量(vector)
单向链表(forward_list)
双向链表(list)
双端队列(deque)
适配器容器
堆栈(stack)
队列(queue)
优先队列(priority_queue)
关联容器
映射(map)
无序映射(unordered_map)
多重映射(multimap)
无序多重映射(unordered_multimap)
集合(set)
无序集合(unordered_set)
多重集合(multiset)
无序多重集合(unordered_multiset)
迭代器
输入迭代器
输出迭代器
单向迭代器
双向迭代器
随机迭代器
泛型算法:排序、查找、复制,等等
在hash_map中查找特定元素的平均时间复杂度为常数级,而在map中则为对数级,这意味着在hash_map中查找特定元素所花费的时间与数据量无关,而在map中则与数据量的对数成正比,但这并不表示hash_map的查找速度一定比map快,而且hash函数本身也是有时间开销的
hash_map的构造速度比map慢
hash_map的内存占用比map多
一般而言,只在单个容器中的数据量比较大,而容器总数不多的场合下,考虑使用hash_map
C++标准模板库中的hashtable使用拉链法解决哈希冲突
vector内部共维护三个迭代器,一个指向首元素,一个指向尾元素的下一个位置,还有一个指向备用空间尾部
当备用空间耗尽时,vector会自动分配一片更大的连续内存空间,通常是原空间大小的1.5或2倍,然后将原空间中的数据元素复制到新空间中,最后释放原来的内存空间
当从vector中删除数据时,并不会释放内存空间,而仅仅是调整其内部维护的迭代器,以表示数据被删除了
对vector执行任何操作,一旦引起内存空间的重新配置,之前获得的迭代器都会失效,需要重新获取当前值
resize
改变向量的大小
向量的大小可以增加也可以减少,大小增加时会将一些被初始化为适当类型的零(基本类型向量),或以缺省构造函数初始化(类类型向量)的元素添加到向量中,大小减少时则会引发对象的析构
当向量的大小突破其容量限制时,会按照1.5或2倍的倍率扩充其容量,以容纳更多的元素
reserve
为向量预留一些未被初始化的内存,扩充其容量,但并不改变向量的大小
预留的容量只能比当前容量更大而不能更小
位于向量的容量范围内但不在其大小范围内的元素,也可以通过下标或迭代器进行访问,但其值未定义
size返回向量的大小,即当前持有元素的个数
capacity返回向量的容量,即最多可容纳元素的个数
vector的底层实现使用一段连续的内存空间存放容器中的元素
vector中的每个元素都占用特定字节数的内存空间并拥有独一无二的内存地址
引用并非对象,引用本身不占用内存空间,也没有内存地址
因此vector中无法存放引用类型的数据元素
在vector中插入元素时,可能引发内存空间的重新分配,之前获得的迭代器随即失效
从vector中删除元素时,会改变其后面元素的内存地址,之前获得的迭代器随即失效
清空只是删除容器中的元素,其间可能引发对象的析构,但并不释放元素所占用的内存空间
释放是在清空的基础上,将元素所占用的内存空间一并释放掉
举例
xxxxxxxxxx
11v.clear(); // 清空内容,但不释放内存
xxxxxxxxxx
11vector().swap(v); // 清空内容,同时释放内存
xxxxxxxxxx
11v.shrink_to_fit(); // 根据大小调整容量
xxxxxxxxxx
21v.clear(); // 清空内容
2v.shrink_to_fit(); // 释放内存
底层结构不同
list的底层结构是一个双向线型链表,元素存放在链表节点中,每个链表节点在内存中并不连续,彼此通过指针相互引用
vector的底层结构是一个动态数组,元素存放在连续的内容空间中,可通过下标访问
迭代方式不同
list支持顺序迭代,只能从一个元素迭代到下一个元素
vector支持随机迭代,可随机存取任意位置的元素
运行效率不同:在list中插入和删除元素的速度快于vector
数据量不同:list不受可分配连续内存的限制,可容纳数量更多的元素
相比于vector,在deque的首端增加或删除元素的时间复杂度,与在其尾端执行相同的操作几乎完全一样
相比于list,deque采用连续内存空间存放数据元素,支持下标访问和随机迭代
vector
支持对元素的随机访问,但在除尾端以外的位置增删元素效率很低
适于存放偶尔增删但频繁读写的简单对象
list
在任何位置增删元素的效率都很高,但不支持对元素的随机访问
适于存放偶尔读写但频繁增删的复杂对象
deque
支持对元素的随机访问,在任何位置增删元素的效率都差不多
迭代器比较复杂,一般能用vector的地方就不要用deque
map、multimap、set、multiset四种关联容器的底层数据结构是一棵红黑树
map
在红黑树中存放键值对
键必须唯一
顺序迭代获得关于键的有序序列
增删改查操作的平均时间复杂度为对数级
multimap
在红黑树中存放键值对
键可以重复
顺序迭代获得关于键的有序序列
增删改查操作的平均时间复杂度为对数级
set
在红黑树中存放元素
元素必须唯一
顺序迭代获得元素的有序序列
增删改查操作的平均时间复杂度为对数级
multiset
在红黑树中存放元素
元素可以重复
顺序迭代获得元素的有序序列
增删改查操作的平均时间复杂度为对数级
通过map的count方法获取与给定键匹配的键值对的个数,得到0即不存在,否则即存在
通过map的find方法获取与给定键匹配的键值对的迭代器,得到终止迭代器即不存在,否则即存在
这些容器中的数据元素都是存放在离散的内存节点中,节点之间通过指针彼此关联
插入或删除元素只需创建新节点或销毁已有节点,同时调整节点中指针的指向,其间不需要大量的内存复制或移动,故效率较高
这些容器的迭代器中保存的是节点的地址,插入或删除元素并不会改变原有节点在内存中的存储位置,即地址不变,故之前获得的迭代器,在插入或删除元素后依然有效
vector采用连续内存空间存放数据元素,原有内存空间不够时,需要重新分配足够的连续内存,并复制容器中的现有元素。提前预留内存空间,可以减少内存分配和元素复制的频率,优化性能
map或set中的数据元素存放在内存地址不连续的离散节点中,不存在因内存空间不足,需要重新分配并复制元素,而导致的性能问题。因此,预留内存空间对于这样的容器而言,并不十分必要
与vector不同,map或set中存放的并非数据元素本身,而是包含数据元素的节点,节点内存的分配与释放机制由容器内部控制,而预留内存空间的分配与释放,使用的却是用户在实例化容器模板时提供的内存分配器类型,后者只针对元素本身,二者并不一致
unordered_map和unordered_set的底层数据结构是一张基于除留取余法的防冗余哈希表
基于哈希表存储数据的最大优点是,增删改查操作的平均时间复杂度为常数级,即与数据量无关
基于哈希表存储数据的最大缺点是,占用比实际需要更多的内存空间
基于哈希表存取数据的工作原理(以拉链法解决冲突为例)
创建一个下标范围足够大的数组,谓之哈希表或散列表,数组元素为指向桶链表的指针
定义一个哈希函数,亦称散列函数,用于计算键的哈希值
对于每个被插入的键值对,通过哈希函数计算键的哈希值,以该值作为下标,在哈希表中找到对应的桶链表,将该键值对追加到桶链表的末尾
对于每个被检索的键,通过哈希函数计算键的哈希值,以该值作为下标,在哈希表中找到对应的桶链表,在桶链表中找到与被检索键匹配的键值对,获取其值
unordered_map
以哈希表作为底层数据结构
需要提供哈希函数
增删改查操作的平均时间复杂度为常数级
占用内存多,构造速度慢
map
以红黑树作为底层数据结构
需要提供比较函数
增删改查操作的平均时间复杂度为对数级
占用内存少,构造速度快
一般而言,如果容器数量不多,但需要在每个容器中保存大量数据,则优先选择unordered_map,而如果容器数量较多,但在每个容器中只需保存少量数据,则优先选择map
输入迭代器
又名只读迭代器
在每个被迭代的位置上只能读取一次
输出迭代器
又名只写迭代器
在每个被迭代的位置上只能写入一次
单向迭代器
在每个迭代位置上既能读取也能写入
只能通过“++”操作迭代到下一个位置,不能通过“--”操作迭代到前一个位置
双向迭代器
在每个迭代位置上既能读取也能写入
既能通过“++”操作迭代到下一个位置,也能通过“--”操作迭代到前一个位置
随机迭代器
在每个迭代位置上既能读取也能写入
既能通过“++”操作迭代到下一个位置,也能通过“--”操作迭代到前一个位置
支持与整数的加减和下标运算,向前或向后迭代多个位置
支持与同型迭代器的减法运算,获得两个迭代器间的距离
在实例化容器对象时,提供自定义的内存分配器
在自定义的内存分配器中,实现在共享内存中创建、获取和销毁元素对象的操作
容器对象本身在每个进程自己的内存空间中,而被容器管理的数据元素则在共享内存中
方法一:利用pair模板类构造键值对,插入map
xxxxxxxxxx
11paysheet.insert(pair<string, int>("张飞", 20000));
方法二:利用value_type成员类型构造键值对,插入map
xxxxxxxxxx
11paysheet.insert(map<string, int>::value_type("张飞", 20000));
方法三:利用make_pair模板函数构造键值对,插入map
xxxxxxxxxx
11paysheet.insert(make_pair("张飞", 20000));
方法四:利用下标操作符,将键值对插入map
xxxxxxxxxx
11paysheet["张飞"] = 20000;
使用连续内存空间存放数据元素的容器,如vector、deque等,删除其中一个元素,指向被删除元素及其后所有元素的迭代器都会失效。正确的做法是接收erase方法的返回值,作为指向被删除元素下一个元素的迭代器
xxxxxxxxxx
11it = container.erase(it);
使用离散内存节点存放数据元素的容器,如list、map、set等,删除其中一个元素,只有指向被删除元素的迭代器失效,指向其后所有元素的迭代器依然可用。因此,这些容器的erase方法不返回任何值
xxxxxxxxxx
11container.erase(it++);
vector的下标操作符不做边界检查,下标越界可能导致未定义的结果,通常是程序崩溃
map的下标操作实际上根据键获取对值的引用,如果给定的键并不存在,则插入新的键值对
无论是调用vector的erase函数删除特定的元素,还是调用vector的clear函数删除所有的元素,元素对象的析构函数会被执行,其中如果包含释放内存的操作,这些内存会得到释放,但元素对象本身所占用的内存空间并不会被释放,除非通过vector的shrink_to_fit方法,将容器的容量重新调整到与其大小相匹配,或者使用deque容器,已获得在删除内容的同时释放内存的效果
map的下标操作符返回的,是与给定键相匹配的键值对中值的引用。如果给定键并不存在,则插入一个新的键值对,键为给定键,值取缺省值,返回该值的引用
map的find函数返回的,是指向与给定键相匹配的键值对的迭代器。如果给定键并不存在,则返回容器的终止迭代器
频繁调用vector的push_back方法在向量的尾端追加元素,会导致容器的内存空间耗尽。这时,vector会重新分配一块更大,通常是原大小的1.5或2倍的连续内存空间以容纳更多的元素,并将原内存中的数据元素复制的新内存中,最后再将原内存释放掉。这个过程会消耗大量的运行时间,如果过于频繁会严重影响程序的性能
new和delete是操作符,由C++语言提供,不需要包含任何头文件;malloc和free是函数,由标准库提供,需要包含<stdlib.h>头文件
通过new分配内存需要指定类型,并返回相应类型的指针,编译器会执行类型安全检查;通过malloc分配内存只需指定所要分配的字节数,并返回void*类型的指针,编译器不执行类型安全检查
new除分配内存外,还对所分配内存做与类型相关的初始化;malloc不做任何初始化
new和delete作为操作符可以被重载,以自定义的方式实现特殊的内存管理策略;malloc和free不行
new和delete除分配和释放内存外,还会调用构造和析构函数;malloc和free只负责分配和释放内存,不会调用构造和析构函数
对于基本类型的对象,无论是单个对象,还是对象数组,只要是通过new创建的,既可以通过delete销毁,也可以通过delete[]销毁
对于类类型的对象,如果既没有自定义的析构函数,也没有编译器自动生成的析构函数,无论是单个对象,还是对象数组,只要是通过new创建的,既可以通过delete销毁,也可以通过delete[]销毁
对于类类型的对象,如果有自定义的或编译器自动生成的析构函数,用delete销毁由new创建的单个对象,用delete[]销毁由new创建的对象数组,只有这样才能保证数组中每个元素的析构函数都被正确执行
作为一般性规则,凡是通过new创建的单个对象,都用delete销毁,凡是通过new创建的对象数组,都用delete[]销毁,在任何情况下总是正确的
如果通过malloc/calloc/realloc分配内存,一旦分配失败即返回空指针,调用者需检查这些函数的返回值,据此判断内存分配是否失败
如果通过new分配内存,一旦分配失败会抛出bad_alloc异常,调用者需捕获该异常,据此判断内存分配是否失败
如果在执行new之前,通过set_new_handler设置了处理函数,一旦分配失败该处理函数会被调用,调用者可在该处理函数中获知内存分配失败
发生内存泄漏的典型场景
由于某种原因,执行了malloc或者new,但没有执行与之对应的free或者delete/delete[]
在构造函数中分配了内存,但没有在析构函数中对等地释放内存
在构造函数中抛出了异常,但没有在抛出异常前释放已分配的内存
在拷贝赋值操作符函数中,没有释放左操作数对象在接受赋值前动态分配的内存
在多态继承结构中,没有将基类的析构函数声明为虚函数
定位内存泄漏的常用工具
valgrind
mtrace
在只读常量区分配内存:字符串字面值
在静态存储区分配内存:全局变量、静态局部变量、静态成员变量
在堆区分配内存:malloc/calloc/realloc、new
在栈区分配内存:局部变量、alloca
在自由存储区分配内存:堆区和栈区中的内存均来自自由存储区
分配释放不同
堆内存在程序运行期间,通过malloc/calloc/realloc函数或new操作符动态分配,通过free函数或delete/delete[]操作符动态释放
栈内存的分配和释放指令由编译器在编译期间静态生成,也可以在程序运行期间,通过alloca函数动态分配,并由编译器静态生成的指令自动释放
产生碎片不同
堆内存的分配和释放是随机的,这会造成内存空间的不连续,产生内存碎片,影响运行性能
栈内存的分配和释放是连续的,不存在内存碎片问题
增长方向不同
堆内存位于自由存储区的低地址侧,从低地址向高地址方向增长
栈内存位于自由存储区的高地址侧,从高地址向低地址方向增长
大小变化不同
栈内存的大小是在编译阶段确定的,字节数固定
堆内存的大小是在运行阶段确定的,字节数不定
静态内存的分配与释放指令,由编译器在编译期间静态生成;动态内存则是在运行期间,通过执行特定的库函数或操作符,实现分配与释放的
静态内存的分配与释放发生在只读常量区、静态存储区和栈区;动态内存的分配与释放则发生在堆区和栈区
访问静态内存中的数据不需要通过指针或引用;访问动态内存中的数据只能通过指针或引用
静态内存是按计划分配的,编译器知道每个内存块的大小;动态内存是按需分配的,编译器并不知道需要分配多少字节
静态内存分配与释放的主动权在编译器手中;动态内存分配与释放的主动权在程序员手中
静态内存分配与释放的效率高于动态内存,动态内存管理不当极易形成内存泄漏
只能在堆区创建对象的类:将析构函数声明为私有
只能在栈区创建对象的类:将重载的new和delete操作符函数声明为私有
浅拷贝:只复制指向某个对象的指针,而不复制该对象本身,两个指针指向同一个对象
深拷贝:单独创建一个新对象,并将原对象的内容复制到新对象中,两个指针指向不同但等值的对象
结构体的成员,从偏移为0的位置开始,按照它们被声明的顺序,地址从低到高依次排列
若未使用“#pragma pack(n)
”
结构体中各个成员的首地址,是其自身大小的整数倍,是为内存对齐
结构体的总字节数,是其最大成员的字节数的整数倍,是为内存补齐
若使用了“#pragma pack(n)
”
结构体中各个成员的首地址,是其自身大小和n中较小者的整数倍,是为内存对齐
结构体的总字节数,是其最大成员的字节数和n中较小者的整数倍,是为内存补齐
用new分配的内存不能用free释放,因为:
free只是释放内存,不会调用析构函数
new所做的工作比malloc多,它返回的内存地址未必是所分配动态内存的起始地址,将这样的地址交给free将引发未定义的后果
理论上讲,用malloc分配的内存可以用delete释放,但一般不会这样做,在某些C++实现上可能导致错误