知识体系之C++

目录

预处理\编译\汇编\链接

1.封装\继承\多态

1.1.概念介绍

1.2.封装修饰符private\protected/public

1.3.继承

1.3.1.构造函数和析构函数

1.3.2.成员初始化列表的初始化顺序

1.4.多态

1.4.1.虚函数表VTable

1.4.2. 虚函数指针vptr

1.4.3. 父类的指针指向子类的对象,调用重写的方法,发生多态

1.5.纯虚函数

1.6.友元函数/友元类 note

2.重载\覆盖\隐藏

2.1.重载

2.2.覆盖(又叫重写)

2.3.隐藏

3.空类

4.内存布局

4.1.内存空间/用户空间

4.2.C++内存布局(5个)

 4.2.1.堆栈区别

5.内存管理

5.1.章节一

5.1.1.概念

5.1.2.虚拟内存和物理内存之间的映射

5.1.3.线性地址空间到物理地址空间的转换

5.1.4.内存申请

5.1.5.内部碎片\外部碎片

5.2.章节二: 伙伴算法

5.3.章节三: slab分配器

5.3.1.概括

5.3.2.高速缓存、slab、对象

6.菱形继承 \ 虚继承

7.C/C++语法关键字

7.1.extern 'c' note

7.2.const / mutable

7.2.const/define对比、枚举\宏定义

7.3.static

7.4.new/malloc

7.5.struct和union的区别、struct和class的区别、struct位域

7.6.inline函数

7.7.constexpr常量表达式

7.8.explicit隐式转换构造函数

7.9.strlen/size区别

7.10.不要对有指针成员的类对象/struct对象做memset(__,0,sizeof___)操作

7.11.volatile

7.11.1.作用

7.11.2.场景

7.11.3.问答

8. C++11新特性

8.1.右值引用\移动语义\完美转发

8.1.1.右值引用 \ move移动语义

8.1.2.完美转发forward   note

8.2.Lambda表达式

8.3.智能指针

 8.4.override

 8.5.final

8.6.类型转换: static_cast\reinterpret_cast\const_cast\dynamic_cast

8.7.类型获取/推导/判断: typeid\decltype\auto

8.7.1.typeid

8.7.2.decltype

8.7.3.auto

8.7.4.decltype\auto区别

9.Effective C++

9.1.多态父类的析构函数要声明为virtual

9.2.绝不在构造函数/析构函数中调用vritual函数

9.3. =default、=delete

9.4. 别让异常从析构函数中抛出 

9.5.参数传递/返回值

9.6.RAII(资源获取就初始化)

10. STL

10.1. STL六大部件综述

10.1.1.仿函数

10.1.2.适配器

10.1.3.分配器

10.2.容器vector/list/map/unorder_map

10.2.1.底层数据结构

10.2.2.vector面试题

10.2.3.迭代器失效问题

11.模板template(泛型编程)

11.1. 定义/偏特化/全特化

11.2.类型萃取


预处理\编译\汇编\链接

55f36b43e89d426a9e4d91b7f56b3536.png

静态链接:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去

优点:在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行。

缺点:

  1. 浪费内存空间。在多进程的操作系统下,同一时间,内存中可能存在多个相同的公共库函数。
  2. 程序的开发与发布流程受模块制约。 只要有一个模块更新,那么就需要重新编译打包整个代码。

动态链接:在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。

优点: 解决了静态链接的缺陷,更适应现代的大规模的软件开发

  1. 更加节省内存并减少页面交换;
  2. DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
  3. 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数
  4. 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试

缺点:

  1. 结构复杂
    1. 对于静态链接来说,系统只需要加载一个文件(可执行文件)到内存即可,但是在动态链接下,系统需要映射一个主程序和多个动态链接模块,因此,相比于静态链接,动态链接使得内存的空间分布更加复杂。
    2. 不同模块在内存中的装载位置一定要保证不一样
  2. 由于是运行时加载,可能会影响程序的前期执行性能
  3. 引入了安全问题,这也是我们能够进行PLT HOOK的基础。

 95e81d0622c94cdf8dad913ec32291f9.png

e95d00cc86fa4b3c9bddeea3df650787.png

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。

8ca79ce39056463f932c544d8d77f316.png

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. 可以是一些函数,特定的运算符具有多种功能(最常用的就是,运算符重载)

0fbeec3e542343ed8fd47cf4ff2f05f5.png

2.2.覆盖(又叫重写)

        覆盖是指派生类方法覆盖从基类继承过来的方法(覆盖也叫重写)

        覆盖其实就是实现多态

覆盖特征:

        1. 不同的作用域范围(分别位于派生类与基类)

        2. 成员函数名相同,且参数相同,返回类型相同

        3. 基类函数必须有virtual关键字(虚函数)

2.3.隐藏

        1. 不同的作用域范围(分别位于派生类与基类)

        2. 两种情况

        case1: 派生类的函数与基类的函数同名,但不同参。此时,无论基类是否有无virtural关键字,基类的函数将被隐藏;(与重载不同的是:重载发生在同类中)

        case2: 派生类的函数与基类的函数同名,且参数相同,但基类中无virtural关键字。此时,基类的函数被隐藏(与重写不同的是:重写必须加virtural关键字)

3.空类

c79a94af77db471794889a9baca9a3f5.png

77972a7ae9684185997a62b412c4f8a3.png

41e6671b5c3a4fa9b50939d73519d163.png

a0ca827d805e4f8cbb12f7a8dce6c851.png

4.内存布局

4.1.内存空间/用户空间

94f4771d9d1947d2bac42c06eab06d59.png

4.2.C++内存布局(5个)

945c45d0b70f411c81324786b51130eb.png

 4.2.1.堆栈区别

6604f39f1ae8468fa071fb3bf4ca8dec.png

c02f00b723744d65a9158f7c55aa3ec5.png

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大小的页面了)

        3ecd315d85c1458d8f64bce6a0c9066c.png

        在32位操作系统下,页目录X页表X物理内存页 = 1024X1024X4KB=4GB,刚好等于4GB(也就是说,32位操作系统下,只需要2级页表,就可以寻址4GB大小的空间了)。

        通过上面的操作,进程的虚拟地址空间的页面,就能够和物理空间的页面一一的对应上了!

5.1.3.线性地址空间到物理地址空间的转换

线性地址由10bit\10bit\12bit的二进制位组成

        第一个10bit:可以从页目录中定位到一个页表

        第二个10bit:可以从页表中定位到一个物理页

        最后的12bit:存储的是offset,12bit刚好可以覆盖4KB个偏移,就可以找到具体的物理地址了

        3fee094165ce47959459bf1d192e42ce.png

        这样就实现了一个虚拟内存页到物理内存页之间的映射,线性地址可以转换为物理地址。

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 区间的内存块将不会被使用,变成外部碎片。

为什么要做内存对齐?

        dcacfafd7aaa47c089b0ceb8d843ca4a.png

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. 分配的内存不会释放,而是返回到对应的组,重复利用

        7d0aeba70c9440f3a7304ed720930b8f.png

5.3.2.高速缓存、slab、对象

        7c85480c0c804455a147ce7e245b2b5a.png

每个高速缓存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.菱形继承 \ 虚继承

        9a03ad0c916c4e5e86c31c176aceb161.png

菱形继承存在的问题

        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:     aa98dd9e1deb49aba1e929d9e94c9153.png

        在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

Q3: const成员函数(使用场景)

A3:

        1. 使用场景:如果一个成员函数不对对象的任何成员数据的进行修改(最常见的为打印成员信息的函数),那么我们可以将这个成员函数设置为const函数,以保护对象数据。 如果在该函数里面修改对象的成员数据,则编译器就会报错。

        2. const函数可以使用类中的所有成员变量、但是不能修改成员变量的值(但是也有例外,如果一个变量被mutable修饰了,即使在const成员函数中,也可以修改该mutable变量的值)

Q4: mutable 可变的

A4: 作用是可以打破在const成员函数中不能修改成员变量的限制

7.2.const/define对比、枚举\宏定义

0b6a29029980438a9bd253fdc1af75cb.png

6b798eadded34cfbba1ce58aba7374b5.png

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

0c2564041c19460bb162fd8fce04b174.png

     7e36c91e262e45879a193bb0882c2eb9.png

7.5.struct和union的区别、struct和class的区别、struct位域

fb3e719b67e1486aac934258b957be20.png

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隐式转换构造函数

8c3a5ba49a504f1c956bcc64f9482dae.png

7.9.strlen/size区别

a802f631d27b46caa3b1a7e65c540917.png

7.10.不要对有指针成员的类对象/struct对象做memset(__,0,sizeof___)操作

memset会将这个对象成员清空,进而导致:指针成员被设为NULL,导致①该指针对象分配的内存之后不能被释放,造成内存泄漏;②该指针被设置为NULL,之后就不能再被使用了

7.11.volatile

volatile是一个特征修饰符,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

7.11.1.作用

防止编译器对代码进行优化(不去读取寄存器中的值,而是每次都读取真实的值)

  • 可见性:加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值
  • 有序性:禁止指令重排序
  • 单个变量读写保证原子性
    • ​ 原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的
    • 所以,volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性

7.11.2.场景

先说个案例:

一般说来,volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile
  2. 多任务环境下各任务间共享的标志应该加volatile
  3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2 中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

7.11.3.问答

        这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

Q1: 一个参数既可以是const还可以是volatile吗?解释为什么。

A1: 可以。如,只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

Q2: 一个指针可以是volatile 吗?解释为什么。

A2: 可以。尽管这并不常见。如,当一个中断服务子程序修改一个指向一个buffer的指针时

8. C++11新特性

8.1.右值引用\移动语义\完美转发

8.1.1.右值引用 \ move移动语义

aefe58a9584a4e5585b769681a45e08b.png

8.1.2.完美转发forward   note

8.2.Lambda表达式

1d42fa9a481b40ec9d47fc23b55b757a.png

8.3.智能指针

553dacd73c7e45688a8bcdf7a0e8c830.png

 shard_ptr几个小问题

bba46286f340450d98950fc3e9b2956f.png

#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虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。

ae627e80c89843c88eff15064d7b30ca.png

9.Effective C++

9.1.多态父类的析构函数要声明为virtual

ddd17655b0a844e1a7b46086f69b770e.png

9.2.绝不在构造函数/析构函数中调用vritual函数

d273360876db4eaa871d1928cef8dfee.png

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: 不知道如何去释放它,导致内存泄漏

9.6.RAII(资源获取就初始化)

目的:RAII是用来管理资源、避免资源泄漏的方法

在编程使用系统资源时,都必须遵循一个步骤:

  1. 申请资源
  2. 使用资源
  3. 释放资源

当在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

使用案例:lock_guard对锁的管理

10. STL

10.1. STL六大部件综述

        1.算法/容器/迭代器:Alogrithm看不见Container,对其一无所知,所以,算法需要的一切信息都必须从Iterator中获得,Iterator是连接Alogrithm和Container的桥梁

        2.分配器:为容器服务,分配内存空间

        3.仿函数:为算法服务,是类对象,重写了operator()函数,比较大小的准则

        4.适配器:容器适配器/迭代器适配器/仿函数适配器

10.1.1.仿函数

10500720a2d74263af69faf13fe3336f.png

C++11function仿函数

6305635bf0ce43ce8571d9a1558d0737.png

C++11bind

8c233dbcb5f9426ab672277ad184af68.png

10.1.2.适配器

574a9ede4ece41f48f04fee49319840b.png

10.1.3.分配器

一级分配器

39071032ebea4ac583ef117ba5169976.png

二级分配器

 fc9be6846dc84f72bad1a039db11b06a.png

10.2.容器vector/list/map/unorder_map

10.2.1.底层数据结构

8ac560c85da64f58aefa9d9d73b0b98a.png

4adac4c9fd294d14b92a1e2cebe6ceea.png

红黑树\平衡树

6533f52d9d3a4a57a1c20b114162c95c.png

10.2.2.vector面试题

1.emplace_back为什么比push_back快这么多?

        因为push_back要求输入的参数是一个已经存在的对象。 当输入的参数,不是这样的对象时,会调用对应类的构造函数,构造一个临时的对象。然后把这个对象执行拷贝构造函数或者移动构造函数插入到vector中。

        emplace_back可以直接使用参数,在本地构建对象(即原地构造)。这样一来,只需要调用构造函数,没有调用拷贝构造函数或者移动构造函数的过程。

2.vectorreserveresize的区别?

673639fff6e946cea56974de35235995.png

3.vector动态扩容的过程? 如何避免vector的动态扩容? 扩容倍数1.5/2

447226072ff946c58be2b5ae6f092ffd.png

4.vectorswap减少容器的大小

8b8e173fc91d4a8ab647ffb4e0440e1d.png

10.2.3.迭代器失效问题

bd3bb93178b34262b10f0d172afadda6.png

11.模板template(泛型编程)

11.1. 定义/偏特化/全特化

3d6462d72191489fa064f58d34b6533d.png

11.2.类型萃取

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值