C++知识点汇总
预处理\编译\汇编\链接
静态链接:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去
优点:在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行。
缺点:
- 浪费内存空间。在多进程的操作系统下,同一时间,内存中可能存在多个相同的公共库函数。
- 程序的开发与发布流程受模块制约。 只要有一个模块更新,那么就需要重新编译打包整个代码。
动态链接:在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。
优点: 解决了静态链接的缺陷,更适应现代的大规模的软件开发
- 更加节省内存并减少页面交换;
- DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
- 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数
- 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试
缺点:
- 结构复杂
- 对于静态链接来说,系统只需要加载一个文件(可执行文件)到内存即可,但是在动态链接下,系统需要映射一个主程序和多个动态链接模块,因此,相比于静态链接,动态链接使得内存的空间分布更加复杂。
- 不同模块在内存中的装载位置一定要保证不一样
- 由于是运行时加载,可能会影响程序的前期执行性能
- 引入了安全问题,这也是我们能够进行PLT HOOK的基础。
1.封装\继承\多态
1.1.概念介绍
1. 封装:将类的某些信息隐藏在内部,不允许外部程序直接访问
2. 继承:子类继承父类的特征和行为,提高代码复用性
3. 多态:同一个行为(函数接口),具有多个不同的表现形式
1.2.封装修饰符private\protected/public
1. public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
2.private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用,私有财产神圣不可侵犯嘛,即便是子女,朋友,都不可以使用。
3.protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
1.3.继承
1.3.1.构造函数和析构函数
构造函数:先构造父类,再构造子类
析构函数:先析构子类,再析构父类
1.3.2.成员初始化列表的初始化顺序
结论:与成员函数定义的顺序有关(与成员初始化列表中书写的顺序无关)
1.4.多态
详解
实现:①父类的指针指向子类的对象 ②父类有virtual函数 ③子类重写了父类的virtual函数
原理:虚函数表VTable、虚函数指针Vptr
1.4.1.虚函数表VTable
1.1. 虚函数表是编译器在编译时创建的,一个类只存在一份,所有的类对象共用这一张表
1.2. 虚函数表的内容,有下面2种情况
①如果派生类中重写了基类的虚函数,那么派生类虚函数表中的函数地址就是派生类的函数地址
②如果没有重写基类的虚函数,那么派生类虚函数表中的函数地址依然是基类的函数地址
1.4.2. 虚函数指针vptr
2.1. 所有类对象存在Vptr指针,该指针处于该类对象存储空间的起始位置偏移量为0
2.2. Vptr指向该虚函数表
1.4.3. 父类的指针指向子类的对象,调用重写的方法,发生多态
如Animal* p = New Cat(); 因为指针p指向的是子类Cat,所以,指针p的虚表指针Vptr指向的是Cat子类的虚函数表(而不是Animal父类的虚函数表)===> 这样,如果调用了重写的方法,就会发生多态
1.5.纯虚函数
纯虚函数:vritual=0 声明的函数,是纯虚函数
抽象类:包含纯虚函数的类,是抽象类
抽象类:不能被实例化,只能被继承
1.6.友元函数/友元类 note
2.重载\覆盖\隐藏
2.1.重载
1. 同一个作用域范围
2. 函数名相同
3. 参数列表不同 (参数个数不同、参数类型不同 或 两个皆不同)
注意:返回值类型可不同(只有返回值不同是不能重载的)
重载的好处:
1. 不用因为参数类型或参数个数不同,而写多个函数。
2. 可以是一些函数,特定的运算符具有多种功能(最常用的就是,运算符重载)
2.2.覆盖(又叫重写)
覆盖是指派生类方法覆盖从基类继承过来的方法(覆盖也叫重写)
覆盖其实就是实现多态
覆盖特征:
1. 不同的作用域范围(分别位于派生类与基类)
2. 成员函数名相同,且参数相同,返回类型相同
3. 基类函数必须有virtual关键字(虚函数)
2.3.隐藏
1. 不同的作用域范围(分别位于派生类与基类)
2. 两种情况
case1: 派生类的函数与基类的函数同名,但不同参。此时,无论基类是否有无virtural关键字,基类的函数将被隐藏;(与重载不同的是:重载发生在同类中)
case2: 派生类的函数与基类的函数同名,且参数相同,但基类中无virtural关键字。此时,基类的函数被隐藏(与重写不同的是:重写必须加virtural关键字)
3.空类
4.内存布局
4.1.内存空间/用户空间
4.2.C++内存布局(5个)
4.2.1.堆栈区别
5.内存管理
5.1.章节一
5.1.1.概念
1. 物理内存:实际存在的内存,程序最终运行的地方
2. 程序直接操作物理内存,是非常危险的。为了做到进程隔离,引出了虚拟内存,进程只能看到虚拟内存,由OS通过MMU实现虚拟内存到物理内存之间的映射
5.1.2.虚拟内存和物理内存之间的映射
1. OS将物理内存按照4KB为单位分成一个个页,同时,也将进程的虚拟地址空间以4KB为单位分成一个个页
2. 每个进程有进程控制块都对应一个task_struct结构体,该结构体中有一个指针(见下图)
2.1. 该指针首先指向当前进程的“页目录”,页目录里面存放的也是指针,该指针指向页表
2.2. 页表中存放的也是指针,指向最终的物理页(每个物理也就是上面说的4KB大小的页面了)
在32位操作系统下,页目录X页表X物理内存页 = 1024X1024X4KB=4GB,刚好等于4GB(也就是说,32位操作系统下,只需要2级页表,就可以寻址4GB大小的空间了)。
通过上面的操作,进程的虚拟地址空间的页面,就能够和物理空间的页面一一的对应上了!
5.1.3.线性地址空间到物理地址空间的转换
线性地址由10bit\10bit\12bit的二进制位组成
第一个10bit:可以从页目录中定位到一个页表
第二个10bit:可以从页表中定位到一个物理页
最后的12bit:存储的是offset,12bit刚好可以覆盖4KB个偏移,就可以找到具体的物理地址了
这样就实现了一个虚拟内存页到物理内存页之间的映射,线性地址可以转换为物理地址。
5.1.4.内存申请
在内核态申请内存:内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请;
而在用户态申请时,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存的使用权,最终通过缺页异常获得一块真正的物理内存
5.1.5.内部碎片\外部碎片
内部碎片的产生:因为所有的内存分配必须起始于可被4、8或16整除(内存对齐,视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配客户)。就是分配满足上面对齐条件的最小的大小内存,如果申请的不满足对齐条件,势必会多分配一点不需要的多余内存空间,造成内部碎片。如:申请43Byte,因为没有合适大小的内存,会分配44Byte或48Byte,就会存在1Byte或3Byte的多余空间。
外部碎片的产生:频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,从而产生外部碎片。比如有一块共有100个单位的连续空闲内存空间,范围为0~99,如果从中申请了一块10 个单位的内存块,那么分配出来的就是0~9。这时再继续申请一块 5个单位的内存块,这样分配出来的就是 10~14。如果将第一块释放,此时整个内存块只占用了 10~14区间共 5个单位的内存块。然后再申请20个单位的内存块,此时只能从 15开始,分配15~24区间的内存块,如果以后申请的内存块都大于10个单位,那么 0~9 区间的内存块将不会被使用,变成外部碎片。
为什么要做内存对齐?
5.2.章节二: 伙伴算法
优点:避免外部碎片的产生
伙伴算法:就是将内存分成若干块,然后尽可能以最适合的方式满足程序内存需求的一种内存管理算法(分配的是页框)
所有的空闲页框分组为11块链表,每个链表分别包含大小为1,2,4,8,16...,1024个连续的页框,每个页框4KB。(最大的连续内存块为1024x4KB=4M)
分配过程:
1. 先从空闲内存中搜索比申请的内存大的最小的内存块。存在,直接就分配;
2. 不存在,则会寻找更大的空闲内存块。然后将这块内存平分成2部分:一部分返回给程序使用,另外一部分作为空闲的内存块等待下一次被分配
释放过程:将大小为b的一对空闲伙伴块合并为一个大小为2b的块,满足一下条件的2个块为伙伴,可以进行合并
1. 两个块具有相同的大小,记作b
2. 它们的物理地址是连续的
3. 第一块的第一个页框的物理地址是2*b*2^12的倍数
说明:这个算法是迭代的,如果它成功合并,合并会会试图合并2b的块,以再次试图形成更大的块
补充:伙伴算法可以解决外部碎片,但是解决不了内部碎片
5.3.章节三: slab分配器
解决内部碎片,支持分配小尺寸object
5.3.1.概括
1. 将分配的内存分割成各种尺寸的块,并把相同尺寸的块分成组
2. 分配的内存不会释放,而是返回到对应的组,重复利用
5.3.2.高速缓存、slab、对象
每个高速缓存Cache_chain:包含3个链表slabs_full、slabs_partial、slabs_empty
slab列表:包含了多个page,即多个slab组
每个page:包含多个object,相同page中存放的object大小尺寸相同
进行分配时,①如果没有Cache_chain,就会去创建一个;②如果有Cache_chain,依次从Cache_chain中的slabs_partial、slabs_empty申请(存在多个slab组,每个slab组中存放多个对象的集合,同一个slab中对象的尺寸大小相同)
6.菱形继承 \ 虚继承
菱形继承存在的问题
1. 冗余数据:D中存在2份对象A的数据
2. 访问的二义性:d对象直接访问A中的变量和函数,编译会报错(可以使用对象.类名称::
访问)
解决方案:虚继承
1. 实现方式:B和C在继承A的时候,加上virtual(注意:D继承B和C时,无需加上virtual)
2. 实例D中,不是存放2份A的数据,而是存放“一个指针+偏移量”,指针指向A
补充问题:虚函数和虚继承有什么关系?如果没有虚函数可不可以用虚继承?
答:虚函数和虚继承无任何关系(虚函数是为了多态设计的,虚继承是为了解决菱形继承设计的)。没有虚函数,也可以使用虚继承。
7.C/C++语法关键字
7.1.extern 'c' note
#ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件, extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译
一言以蔽之,将C++代码,按照C语言的方式编译
7.2.const / mutable
Q1: const是函数签名么
A1: 是,区分只读操作和赋值操作。const函数和非const函数是重载(本质上是因为参数变量被const修饰了)
Q2: const作用
A2:
Q3: const成员函数(使用场景)
A3:
1. 使用场景:如果一个成员函数不对对象的任何成员数据的进行修改(最常见的为打印成员信息的函数),那么我们可以将这个成员函数设置为const函数,以保护对象数据。 如果在该函数里面修改对象的成员数据,则编译器就会报错。
2. const函数可以使用类中的所有成员变量、但是不能修改成员变量的值(但是也有例外,如果一个变量被mutable修饰了,即使在const成员函数中,也可以修改该mutable变量的值)
Q4: mutable 可变的
A4: 作用是可以打破在const成员函数中不能修改成员变量的限制
7.2.const/define对比、枚举\宏定义
7.3.static
Q1: static作用
A1:
1. 修饰全局变量/函数,表示它的作用域只在该文件中
2. 修饰局部变量,表示该变量的值不会因为函数终止而丢失(数据存储在内存静态区的数据段,其生命周期和整个程序的生命周期一致)
3. 修饰类成员变量时,表明该类的所有对象对该成员变量只有一个实例,static成员函数不存在this指针,不能访问非static变量
Q2: 为什么static和const不能同时修饰函数?
A2: const修饰的变量/函数必须是含有this指针的,而static本质是共有一个实例,没有this指针
Q3: 为什么静态成员函数不能申明为const?
A3: 这是C++的规则,在类中,const修饰符用于表示函数不能修改成员变量的值,并且const实际修饰的就是指向类的this函数,而类中的static函数本质上是全局函数,不能用const来修饰它。一个静态成员函数可以访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const的作用是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了
Q4: 在头文件把一个变量声明为static变量,那么引用该.h文件的源文件能够访问到该变量么?
A4: 可以。声明static变量一般是为了在本cpp文件中的static变量不能被其他的cpp文件引用,但是对于头文件,因为cpp文件中包含了头文件,故相当于该static变量在本cpp文件中也可见。当多个cpp文件包含该头文件中,这个static变量将在各个cpp文件中将是独立的,彼此修改不会对相互有影响。
Q5: 为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义
A5: 因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
Q6: static关键字为什么只能出现在类内部的声明语句中,而不能重复出现在类外的定义中。
A6: 如果类外定义函数时在函数名前加了static,因为作用域的限制,就只能在当前cpp里用,类本来就是为了给程序里各种地方用的,其他地方使用类是包含类的头文件,而无法包含类的源文件。
7.4.new/malloc
7.5.struct和union的区别、struct和class的区别、struct位域
7.6.inline函数
1.inline函数的原理
在使用时(和宏类似)是代码替换,执行效率高,是一种空间换时间的思想
编译器使用inline函数,首先会检查参数问题,保证调用正确性
2.探讨inline的使用场景
因为inline还是是将代码段展开的,所以具有for循环的代码,不要声明为内联,这样会占用过多的空间
一般,声明为inline函数的代码行数最好控制的在5行内
7.7.constexpr常量表达式
constexpr表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。
1. 不能用普通函数作为constexpr变量的初始值,只能用constexpr函数去初始化constexpr变量。这种函数足够简单,以使得编译时就可以计算其结果。
2. constexpr函数是指能用于常量表达式的函数。该函数要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句
7.8.explicit隐式转换构造函数
7.9.strlen/size区别
7.10.不要对有指针成员的类对象/struct对象做memset(__,0,sizeof___)操作
memset会将这个对象成员清空,进而导致:指针成员被设为NULL,导致①该指针对象分配的内存之后不能被释放,造成内存泄漏;②该指针被设置为NULL,之后就不能再被使用了
8. C++11新特性
8.1.右值引用\移动语义\完美转发
8.1.1.右值引用 \ move移动语义
8.1.2.完美转发forward note
8.2.Lambda表达式
8.3.智能指针:C++中的RAII机制
shard_ptr几个小问题
#include <iostream>
using namespace std;
// https://blog.csdn.net/weixin_44980842/article/details/121843881
void func(shared_ptr<int> ptr)
{
cout << "use_count=" << ptr.use_count() << endl;
return;
}
// case1: 慎用裸指针来构造shared_ptr
// 凡是使用了裸指针来构造shared_ptr智能指针时,那么在后续的代码中。就不要再使用该裸指针了。因为会造成不安全的case!
void test1()
{
// 错误用法
{
int *p = new int(100); //裸指针
cout << "1." << *p << endl;
func(shared_ptr<int>(p));
//这里传入func函数中的参数是一个临时的shared_ptr
//用一个裸指针显式地构造这个临时的shared_ptr
cout << "2." << *p << endl;
//但是,这个裸指针p的内存在传入func后被释放了
//此时你无法对它的内存空间do事情了!
*p = 45; //×!此时p的内存空间已经被释放了!
}
// 正确用法1
{
int *p = new int(100); //裸指针
cout << "1." << *p << endl;
shared_ptr<int> p2(p); //把裸指针绑定到shared_ptr上
cout << "2." << *p2 << endl;
func(p2); //传入一个shared_ptr指针,即:我把内存交给p2管理了!很好!
cout << "3." << *p2 << endl;
*p2 = 188; // ok!没问题的!安全的!
}
// 正确用法2
{
shared_ptr<int> p2(new int(100)); //把裸指针绑定到shared_ptr上
cout << "1." << *p2 << endl;
func(p2); //传入一个shared_ptr指针,即:我把内存交给p2管理了!很好!
cout << "2." << *p2 << endl;
*p2 = 188; // ok!没问题的!安全的!
}
}
// case2: 使用裸指针构造shared_ptr时候,一个裸指针只能绑定到一个shared_ptr中,不能绑定到多个shared_ptr中!
// 当一个裸指针被绑定到多个shared_ptr智能指针上时,就会出现
// ①一旦某个智能指针被释放时,别的绑定到该裸指针上的智能指针就会指向一个乱码的对象。(因为你已经释放了裸指针p了,p所指向的内存别的指向它的shared_ptr指针没有权限访问了)
// ②如果别的指向裸指针的shared_ptr指针释放时,还会导致另外一个问题:重复释放同一内存空间。这会导致你的程序产生异常!
void test2()
{
// 错误用法
{
int *p = new int(100); // 裸指针
shared_ptr<int> p1(p); // 把裸指针p绑定到p1上
shared_ptr<int> p2(p); // 把裸指针p绑定到p2上
// 当退出作用域时,程序会崩溃!
// 因为p1先自动释放它指向的内存p,之后p2也自动释放它指向的内存p,两块内存被释放两次,就会出现core
}
// 正确用法
{
int *p = new int(100); // 裸指针
shared_ptr<int> p1(p); // 把裸指针p绑定到p1上
shared_ptr<int> p2(p1); // 把p1绑定到p2上
}
}
// case2: 慎用get()返回的指针
// 慎用理由:对shared_ptr智能指针用.get()方法得到的裸指针你千万不能随意delete!因为这个权限你要交给shared_ptr去管理!(若你随意delete后,shared_ptr就没办法帮助我们正常管理该内存了)
void test3()
{
// {
// shared_ptr<int> p1(new int(100));
// shared_ptr<int> p2(p1);
// auto pp = p1.get();
// delete pp; //错误!你delete了,那还用智能指针shared_ptr帮你管理内存干嘛?
// }
// {
// shared_ptr<int> p1(new int(100));
// auto pp = p1.get(); // 获取裸指针
// {
// shared_ptr<int> p3(pp);
// }
// //相当于将裸指针pp既绑定到shared_ptr指针p1上,也绑定到p3上了
// //这和前面的一个裸指针被绑定到多个shared_ptr指针上产生异常的case是一样的!
// }
{
shared_ptr<int> sp1(new int(1));
shared_ptr<int> sp2(std::move(sp1));
//移动语义:移动构造一个新的智能指针对象
//此时,sp1就不再指向该int型的val==1的对象了(变为空)
//引用计数依旧为1
shared_ptr<int> sp3(std::move(sp2));
//此时sp2就变为空,但其引用计数仍然为1
//用std::move()函数移动赋值肯定比单纯的赋值块:因为单纯的赋值时你还需要增加引用计数,但是移动则不需要!
cout << sp1.use_count() << sp2.use_count() << sp3.use_count() << endl; // 0 0 1
}
}
// class CT : public enable_shared_from_this<CT>
// {
// public:
// shared_ptr<CT> getself()
// {
// return shared_from_this();
// // 通过模板类enable_shared_from_this中的方法shared_from_this将this指针用作shared_ptr指针来do返回!
// }
// };
class CT
{
public:
shared_ptr<CT> getself()
{
return shared_ptr<CT>(this); // 用裸指针初始化了多个shared_ptr!
}
};
// case4: 不要把类对象指针(this指针)作为shared_ptr返回,改用enable_shared_from_this
// 当你在类中将this指针绑定给shared_ptr时,又在类的外部将该shared_ptr给别的shared_ptr指针做初始化的话,就又会造成上述使用裸指针时出现的问题:用一个裸指针绑定到多个shared_ptr上时,会造成重复释放用一份内存空间的异常问题!
void test4()
{
CT *ct = new CT();
// ct裸指针给pct1
shared_ptr<CT> pct1(ct);
// this裸指针给pct1
shared_ptr<CT> pct2 = pct1->getself();
}
int main()
{
test4();
}
8.4.override
该关键字声明在子类的成员函数中,表示该成员函数一定是继承父类的virtual
class A
{
virtual void foo(){};
};
class B : A
{ // override声明该函数是对父类virtual的重写!
virtual void foo1() override; // 编译失败,防止程序员在重写父类虚函数virtual时,因为手抖,写错函数名,闹出乌龙!
//'foo1' marked 'override' but does not override any member functions
};
8.5.final
修饰类名:该类不可以被继承
修饰类中的成员函数:该函数不能被子类覆盖实现(即,不允许重载,但是允许被重写,实现多态)
8.6.类型转换: static_cast\reinterpret_cast\const_cast\dynamic_cast
C语言中的类型转换: TYPE A = (TYPE)B
static_cast:相当于C语言中的类型转换:编译器会做类型检查,有的转换会失败,更加安全
reinterpret_cast:强制类型转换,不要使用!
const_cast:删除const属性
dynamic_cast:
1.相同类型: 转换一定成功
2.子类转父类,一定成功。(子类转父类,无论父类是否有虚函数,都能编译通过 && 转换成功)
3.父类转子类
2.1.编译问题: 父类没有virtual函数,编译失败
2.2.转换问题: 当前仅当父类指针指向子类对象时,父类指针才能成功动态转化为子类指针,否则转换失败,结果为nullptr
8.7.类型获取/推导/判断: typeid\decltype\auto
8.7.1.typeid
介绍:
1. typeid是一个运算符,类似与sizeof
2. 功能是可以打印目标的类型
3. typeid可用于动态类型(多态时,父类的指针指向子类的对象:判断该父类的指针指向子类对象的类型),也可以用于静态类型,静态类型和动态类型分别对应的是编译和运行。
4. typeid多数运用于class和继承中
8.7.2.decltype
声明表达式类型,声明变量时不必赋初值。类型由编译器根据表达式自动推导
8.7.3.auto
自动类型推导,声明变量时必须赋初值,类型由右值的决定。换句话说:auto让编译器通过初始值来推算变量的类型,显然,auto 定义的变量必须有初始值。
8.7.4.decltype\auto区别
- 编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层 const,而把底层const保留下来。与之相反,decltype会保留变量的顶层const。
- 与auto不同,decltype 的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型
- auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
9.Effective C++
9.1.多态父类的析构函数要声明为virtual
9.2.绝不在构造函数/析构函数中调用vritual函数
9.3. =default、=delete
上面我们知道,C++会为一个类声明默认的6个函数,C++11提供了2个关键字,可以控制它们会不会默认生成这6个函数。
=default: 可以让编译器自动为我们生成函数体(程序员只需在函数声明后加上=default,就可将该函数声明为 default 函数,编译器将为显式声明的default函数自动生成函数体),该函数使用时有如下两点要注意的:
- Defaulted 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。
- Defaulted 函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义
-
class Fruit { public: Fruit() = default; Fruit(int n1) : a1(n1) {} private: int a1; };
-
=delete
- 禁止类使用默认生成的成员函数,最好设置为private,同时设置为=delete
-
class A{ public: A(){} ~A(){} private: A(const A&) = delete;//拷贝构造函数 A& operator=(const A&) = delete;//赋值运算符 A* operator&() = delete;//取值运算符 const A* operator&()const = delete;//取址运算符 const }
-
- 禁止类使用其他类成员函数
-
#include <iostream> using namespace std; class A{ public: A(){} int fun1(int a){return a;} int fun2(int a) = delete; }; int main(){ A* temp = new A(); cout << temp->fun1(1) << endl; //正确 //temp->fun2(1); //使用错误 return 0; }
-
9.4. 别让异常从析构函数中抛出
1. 如果析构函数中可能抛出异常,应该捕获该异常,然后吞下它们或结束程序
2. 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该函数
9.5.参数传递/返回值
Q1: 返回值为引用/指针时,该引用/指针不能指向local变量
A1: 引用和指针执行local,该local被返回给外部后,就会被销毁,那么,返回出去的值没法用
Q2: 返回值为引用时,该引用不能指向堆上分配的内存
A2: 不知道如何去释放它,导致内存泄漏
10. STL
10.1. STL六大部件综述
1.算法/容器/迭代器:Alogrithm看不见Container,对其一无所知,所以,算法需要的一切信息都必须从Iterator中获得,Iterator是连接Alogrithm和Container的桥梁
2.分配器:为容器服务,分配内存空间
3.仿函数:为算法服务,是类对象,重写了operator()函数,比较大小的准则
4.适配器:容器适配器/迭代器适配器/仿函数适配器
10.1.1.仿函数
C++11之function仿函数
C++11之bind
10.1.2.适配器
10.1.3.分配器
一级分配器
二级分配器
10.2.容器vector/list/map/unorder_map