1. 多态中的虚析构函数
多态中,父类指针可以事项子类对象,如果程序结束,需要释放对象资源,如果字类不重写父类析构函数,此时只会释放子类对象中属于父类资源的那一部分,子类对象并没有完全释放,因此多态析构函数要设置为虚函数,并且每个子类都需要重写,使得子类对象可以释放完。
2. gcc工作流程
源代码进行预处理,去掉注释等无用代码;
编译器将预处理后的源代码编译为汇编代码;
汇编代码经过汇编器成目标代码(机器代码01);
目标代码和+启动代码+库代码+其他代码,经过链接器,得到可执行程序;

3. gcc与g++区别
gcc和g++都可以编译c或者cpp代码,只会根据文件后缀判别程序类型。对于编译,两者是等价的,但是gcc不能自动和c++程序使用的库链接,所以需要g++完成链接,为了统一,就直接使用G++完成编译和链接,并不是说cpp只能用g++编译。
4. 静态库和动态库对比
链接阶段如何处理:静态链接和动态链接。
静态库的代码复制到可执行文件中:

动态库的信息打包到可执行文件中,使用时在找对应动态库。

编译预处理:将源代码中的预处理指令、头文件、去注释、宏替换;得到.i文件
编译:预处理后的文件进行词语分析、语法分析、汇编代码生成,并根据特定CPU平台进行优化。得到.s汇编代码;
汇编:将汇编代码翻译成机制指令.o文件;
链接:将生成的多个.o文件链接成一个整体。生成一个可被操作系统加载执行的ELF程序文件;
5.大端和小端序
字节的排列方式常见的方式有两种:将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序(Little-Endian),在变量指针转换的时候地址保持不变;反之则称大端序(Big-Endian)内存顺序和数字的书写顺序是一致的,对于人的直观思维比较容易理解。为什么需要字节序这个规定,主要是因为在网络应用中字节序是一个必须被考虑的因素,对于不同 CPU 可能采用不同标准的字节序,所以均按照网络标准转化成相应的字节序。
6.内存泄漏
程序在堆中申请的动态内存,在程序使用完成时没有得到及时的释放。当这些变量的生命周期已结束时,该变量在堆中所占用的内存未能得到释放,从而就导致了堆中可使用的内存越来越少,最终可能产生系统运行较慢或者系统因内存不足而崩溃的问题。由于内存未得到及时释放,从而可能导致可使用的动态内存空间会越来越少,一旦内存空间全部使用完,则程序可能会导致因为内存不够中止运行。
在 C++ 中需要将基类的析构函数定义为虚函数;
遵循 RAII(Resource acquisition is initialization)原则:在对象构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源;
尽量使用智能指针;
有效引入内存检测工具
7.auto新特性decltype :
编译器会在 编译期间 通过初始值或者函数返回值推导出变量的类型,通过 auto 定义的变量必须有初始值。能自动推导出顶层的 const 或者 volatile,也不能自动推导出引用类型。初始化表达式为数组,auto 关键字推导的类型为指针。数组名在初始化表达式中自动隐式转换为首元素地址的右值。
decltype 关键字:decltype 是 “declare type” 的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
8.lambda 表达式
又称匿名函数,某个函数简短,不值得单独定义。就在使用处以特定格式写开
[] // 没有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。
8.右值引用(区分一系列概念)
左值: 程序中真实存在于内存中的数据,有地址
左值引用: 对右值取地址,直接利用取址符号&获得变量地址,直接操作值,避免变量拷贝
右值: 程序中临时创建的值,要被销毁,并没有地址
右值引用: 用于实现移动语义和完美转发;
移动语义: 其他对象(通常是临时对象)拥有的内存资源移动给其他对象;浅拷贝深拷贝问题应该都了解。那就有个情景。我们实现了深拷贝,但在程序中有大量的=,临时变量。那是不是每次都要使用深拷贝,但如果类中指针开辟的内存很大,这么重复的不能拷贝,显然不值得,太浪费资源了。而浅拷贝出现的问题主要是,复制后两个指针指向同一片资源而最后重复释放报错,那我们是不是就想,改动一下,新的指针指向内存时,旧的指针指向nullptr?这不就解决浅拷贝问题了。同时避免深拷贝过多,这就是移动语义,“内存资源移动给其他对象”。
完美转发std::forward(): 它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
std::move(): 可获得绑定到左值上的右值引用。通过 move 获取变量的右值引用,从而可以调用对象的移动拷贝构造函数和移动赋值构造函数。
**Fun(const int &):**函数形参是个常量引用类型,因此只接受右值、非常量左值、常量左值。
拷贝构造函数
#include <iostream>
#include <utility> // 包含std::move的头文件
#include <vector>
using namespace std;
class LargeData {
public:
std::vector<int> data;
LargeData(int size) : data(size, 0) { cout<<"size!"<<endl;} // 构造函数,初始化一个指定大小的vector
LargeData(const LargeData &other) : data(other.data) { // 拷贝构造函数
std::cout << "copy constructor is called" << std::endl;
}
LargeData(LargeData &&other) noexcept : data(std::move(other.data)) { // 移动构造函数
std::cout << "Move constructor is called" << std::endl;
}
};
LargeData createLargeData(int size) {
return LargeData(size); // 返回一个临时对象,可以被移动而非复制
}
int main() {
LargeData d1 = createLargeData(1000);
std::cout << "start state of d1's data size: " << d1.data.size() << std::endl; // 输出d1的状态
LargeData d2 = d1;
LargeData d3 = std::move(d1); // 使用std::move强制移动而非复制
std::cout << "Final state of d1's data size: " << d1.data.size() << std::endl; // 输出d1的状态
return 0;
}

重温一边move用法;定义了三个构造函数,一个赋值构造函数,一个拷贝构造函数,一个移动构造函数;
直接总结关键点:
- 第23行d1的初始化。如果没有优化的版本,就是两次构造函数,一个赋值构造函数,再把局部对象进行拷贝给d1.但实际只有一次赋值构造函数。因此编译器启动用RVO返回值优化,直接在d1的定义处构造对象,不会先产生局部对象再拷贝。而是直接赋值构造。
- 第25行,就是一个普通的拷贝构造函数。d2的产生不影响d1。是将d1的data复制了一份
- 第26行,移动构造函数,首先,move(d1)就是将一个左值转化为右值,对应的移动构造函数使用万能引用参数&&获得右值。其次,d3的data就使用了move,将d1的data转移给了d3,并不是拷贝,这也是移动构造函数的关键。
- 最后,两次打印d1的data,在没有使用移动构造函数之前,d1没有变化,拷贝不影响d1;但d3使用了移动构造函数,因此之后d1的data移动给了d3,所以再查看d1的data为空。
9.default和delete函数
default表示允许编译器生成默认函数;delete表示禁止编译器使用这个函数。通常在类中使用
10.constexpr
C++ 11 引进关键字 constexpr 允许用户保证函数或是对象构造函数是编译期常量,编译器在编译时将去验证函数返回常量。
const表示变量为常量,并希望初始值设定为某个常量表达式,但是实际使用中,初始值并非常量表达式,因此c11提出constexpr让编译器检查变量的值是否是一个常量表达式,硬性要求。
- constexpr修饰常量要求必须是常量表达式,如果是个函数返回值,那么函数必须是constexpr函数
- constexpr函数要求定义的函数足够简单,使得编译就能得到结果
- constexpr可以修饰类得构造函数,构造函数的函数体必须为空,所有成员变量的初始化都放到初始化列表中,要求参数也是constexpr类型,这样产生的对象也是constexpr对象
11.sizeof 和 strlen 的区别
strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束,而 sizeof 测量的是对象或者表达式类型占用的字节大小。
12. static
全局静态变量: 限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
局部静态变量局部静态变量只能被初始化一次。与全局静态变量不同的是静态局部变量的作用域仅限于函数内部,它的作用域与函数内部的局部变量相同。
静态函数: 函数限制函数的作用域,仅可在定义该函数的文件内部调用
静态成员变量: 类内进行声明,在类外进行定义和初始化;所有对象共享一个静态成员变量。静态成员变量可以作为成员函数的参数,而普通成员变量不可以。静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。
静态成员函数: 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。
static 对象: 静态对象的生存周期为整个程序的生命周期,而非静态对象的生命周期只存在于某个循环中
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
13. const作用和用法
将变量定义为常量;
修饰函数参数,保证函数内部不会修改参数值;
修饰成员函数,保证成员函数不会修改成员变量
const 指针(常量指针):指针指向的内容不能变,但指针本身可以变
指针const(指针常量):指针指向得内容可以变,但指针本身地址不能边
与define区别: define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值;define 定义的宏常量没有数据类型,只是进行简单的代码替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误;define 定义的宏定义只是作为代码替换的表达式而已,宏定义本身不占用内存空间,define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,实际使用宏定义替换代码时占用的是代码段的空间;const 定义的常量占用静态存储区的只读空间,程序运行过程中常量只有一份。
14. inline作用用法
可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。
类内定义成员函数默认是内联函数;
当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字;
15.volatile 的作用
当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile,告知编译器不应对这样的对象进行优化。volatile 关键字修饰变量后,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
volatile 修饰 i 后,表明每次使用 i 时必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。如果不加 volatile,编译器会进行优化,编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中,而不是重新从 i 里面读,如果 i 是一个寄存器变量,则 i 可能已经被外部程序进行改写,因此 volatile可以保证对特殊地址的稳定访问。
16.define和inline区别
内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销,在编译后的代码段中可以看到内联函数的定义。宏定义编写较为复杂,常需要增加一些括号来避免歧义。宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查,因此在实际使用宏时非常容易出错。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
17.extern c
由于 C 语言并不支持函数重载,在 C 语言中函数不能重名,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在 C++ 中调用一个使用 C 语言编写的模块中的某个函数 test,C++ 是根据 C++ 的函数名称修饰方式来查找并链接这个函数,去在生成的符号表查找 _Z4testv 这个函数的代码,此时就会发生链接错误。而此时我们用 extern C 声明,那么在链接时,C++ 编译器则按照 C 语言的函数命名规则 test 去符号表中查找对应的函数。因此当 C++ 程序需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern "C" 指出任意非 C++ 函数所用的语言。
18.explicit 的作用
用来声明类构造函数是显式调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换和赋值初始化。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显式调用的,再加上 explicit 关键字也没有什么意义。
19. 面向对象三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在 C++ 中多态一般是使用虚函数来实现的,使用基类指针调用函数方法时,如果该指针指向的是一个基类的对象,则调用的是基类的虚函数;如果该指针指向的是一个派生类的对象,则调用的是派生类的虚函数。
C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现
20.重载、重写、隐藏的区别
函数重载:
重载是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型
函数隐藏:
函数隐藏是指派生类的函数屏蔽了与其同名的基类函数,只要是与基类同名的成员函数,不管参数列表是否相同,基类函数都会被隐藏。
函数重写(覆盖):
函数覆盖是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
21.多态
多态:就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。
在类中用 virtual 关键字声明的函数叫做虚函数;
存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
当基类指针指向派生类对象,基类指针调用虚函数时,该基类指针指的虚表指针实际指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数然后调用执行。
如果使用虚函数,基类指针指向派生类对象并调用对象方法时,使用的是子类的方法;
如果未使用虚函数,则是普通的隐藏,则基类指针指向派生类对象时,使用的是基类的方法(与指针类型看齐)
基类指针能指向派生类对象,但是派生类指针不能指向基类对象
22.虚函数和纯虚函数
纯虚函数在类中声明时,用 virtual 关键字修饰且加上 =0,且没有函数的具体实现;
含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口定义,没有具体的实现方法;
继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
对于抽象类需要说明的是:
抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
可以声明抽象类指针,可以声明抽象类的引用;
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
纯虚函数的作用:含有纯虚函数的基类要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。定义纯虚函数是为了实现统一的接口属性,用来规范派生类的接口属性,也即强制要求继承这个类的程序员必须实现这个函数。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以要求实现纯虚函数的属性,在面对对象设计中非常有用的一个特性。
纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象
23. 虚函数实现机制
虚函数的实现原理:
实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚函数表。该表是编译器在编译时设置的静态数组,一般我们称为 vtable。虚函数表包含可由该类调用的虚函数,此表中的每个条目是一个函数指针,指向该类可访问的虚函数。
每个对象在创建时,编译器会为对象生成一个指向该类的虚函数表的指针,我们称之为 vptr。vptr 在创建类实例时自动设置,以便指向该类的虚拟表。如果对象(或者父类)中含有虚函数,则编译器一定会为其分配一个 vptr;如果对象不包含(父类也不含有),此时编译器则不会为其分配 vptr。与 this 指针不同,this 指针实际上是编译器用来解析自引用的函数参数,vptr 是一个真正的指针。
虚函数表相关知识点:
虚函数表存放的内容:类的虚函数的地址。
虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable。
24. 菱形继承
C基类派生两个A和B。X有同时多继承于A和B。此时X就会有两份源头C的数据。会造成歧义;
解决办法1:显式声明C中使用的是A还是B中的数据
2: A和B采用虚继承,保证X中只有一份
25. 深拷贝、浅拷贝
如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。
深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在栈空间中的内容,又拷贝存储在堆空间中的内容。
浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于栈空间中的内容。
当类中指针变量太多,程序设计大量局部类对象,需要多次隐式拷贝时,深拷贝会降低性能,因此需要移动语义。
26. 成员初始化列表效率高
对象的成员函数数据类型可分为语言内置类型和用户自定义类,对于用户自定义类型,利用成员初始化列表效率高。用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,由于 C++ 规定对象的成员变量的初始化动作发生在进入自身的构造函数本体之前,那么在执行构造函数之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,再显式调用该成员变量对应的构造函数。因此使用列表初始化会减少调用默认的构造函数的过程,效率更高一些
27.静态绑定和动态绑定的实现
静态类型:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。
动态类型:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。
静态绑定和动态绑定:
静态绑定是指程序在编译阶段确定对象的类型(静态类型)。
动态绑定是指程序在运行阶段确定对象的类型(动态类型)。
静态绑定和动态绑定的区别:
发生的时期不同:如上。
对象的静态类型不能更改,动态类型可以更改。
注:对于类的成员函数,只有虚函数是动态绑定,其他都是静态绑定。
28. std::move原理
折叠原理:右值传递给形参T&&依旧是右值, 左值传递给形参T&&依旧是左值。不过都变成了引用;
- 根据万能模板对传入参数t做处理,保证可以接受任意类型参数,返回原始类型;对t做一次右值引用,根据折叠原理,返回原始类型的引用
- 通过remove_refrence移除引用,得到t的具体类型。
- 最后通过static_cast强制类型转换,返回右值引用。实现了左值变右值,并且是右值引用,省去拷贝
与forward区别:move进行类型转换时,利用remove_reference将外层引用去掉,在强制转换成指定类型,而forward利用折叠引用技巧,保留原变量的类型
29. 函数指针
函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。我们知道所有的函数最终的编译都生成代码段,每个函数的都只是代码段中一部分而已,在每个函数在代码段中都有其调用的起始地址与结束地址,因此我们可以用指针变量指向函数的在代码段中的起始地址。
#include<iostream>
using namespace std;
int add(int a, int b){
return a+b;
}
int main(){
int (*f)(int, int) = add;
int c = f(1, 2);
cout<<c<<endl;
return 0;
}
具体应用: 回调函数,函数A中把函数B作为参数,这个参数类型就利用函数指针来表示,例如
#include<iostream>
using namespace std;
int add(int a, int b){
return a+b;
}
int add2(int a, int b, int (*fun)(int, int)){
return (a+b) * (fun(a, b));
}
int main(){
int (*f)(int, int) = add;
int c = add2(1,2, f); //回调函数
cout<<c<<endl;
return 0;
}
30.值传递、引用传递、指针传递
值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入。当函数内部可能需要改变参数具体的内容时,我们则采用形参,在组成原理上来说,对于值传递的方式我们采用直接寻址。
指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后实参和形参是不同的指针,但指向的地址都相同。通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。在组成原理上来说,对于指针传递的方式一般采用间接寻址。
引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。当然不同的编译器对于引用有不同的实现,部分编译器在底层也是使用指针来实现引用传递。
31. 迭代器
一种抽象的设计概念,在设计模式中有迭代器模式,即提供一种方法,使之能够依序寻访某个容器所含的各个元素,而无需暴露该容器的内部表述方式。迭代器只是一种概念上的抽象,具有迭代器通用功能和方法的对象都可以叫做迭代器。迭代器有很多不同的能力,可以把抽象容器和通用算法有机的统一起来。迭代器基本分为五种,输入输出迭代器,前向逆向迭代器,双向迭代器和随机迭代器。
输入迭代器(Input Iterator):只能向前单步迭代元素,不允许修改由该迭代器所引用的元素;
输出迭代器(Output Iterator):只能向前单步迭代元素,对由该迭代器所引用的元素只有写权限;
向前迭代器(Forward Iterator):该迭代器可以在一个区间中进行读写操作,它拥有输入迭代器的所有特性和输出迭代器的部分特性,以及向前单步迭代元素的能力;
双向迭代器(Bidirectional Iterator):在向前迭代器的基础上增加了向后单步迭代元素的能力;
随机访问迭代器(Random Access Iterator):不仅综合以后 4 种迭代器的所有功能,还可以像指针那样进行算术计算;
在 C++ STL 中,容器 vector、deque 提供随机访问迭代器,list 提供双向迭代器,set 和 map 提供向前迭代器。
32. 悬空指针、野指针
悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。如果对悬空指针再次释放可能会出现不可预估的错误,比如可能该段内存被别的程序申请使用了,而此时对该段内存进行释放可能会产生不可预估的后果。
野指针:“野指针” 是指不确定其指向的指针,未初始化的指针为“野指针”,未初始化的指针的初始值可能是随机的,如果使用未初始化的指针可能会导致段错误,从而程序会崩溃。c语言中free释放内存后,需要将指针指向null,否则就是野指针。
33. 强制类型转化
static_cast: static_cast 是“静态转换”的意思,也即在编译期间转换,转换失败的话会抛出一个编译错误。一般用于如下:
用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。
用于基本数据类型的转换。
用于类层次之间的基类和派生类之间指针或者引用的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
可以将空指针转化成目标类型的空指针。
可以将任何类型的表达式转化成 void 类型。
不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换。
const_cast: 主要用于 const 与非 const、volatile 与非 volatile 之间的转换。强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。
reinterpret_cast: 改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。
dynamic_cast: 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。
在向上进行转换时,即派生类的指针转换成基类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。
34. nullptr和null区别
NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。
nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。
函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
35.模板特化
模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。
模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化
函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。
特化分为全特化和偏特化:
全特化:模板中的模板参数全部特例化。
偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。
说明:要区分下函数重载与函数模板特化
定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。
36.泛型编程
模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。
泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。
容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。
迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。
模板:可参考本章节中的模板相关问题。
泛型编程优缺点:
通用性强:泛型算法是建立在语法一致性上,运用到的类型集是无限的/非绑定的。
效率高:编译期能确定静态类型信息,其效率与针对某特定数据类型而设计的算法相同。
类型检查严:静态类型信息被完整的保存在了编译期,在编译时可以发现更多潜在的错误。
二进制复用性差:泛型算法是建立在语法一致性上,语法是代码层面的,语法上的约定无法体现在机器指令中。泛型算法实现的库,其源代码基本上是必须公开的,引用泛型中库都需要重新编译生成新的机器指令。而传统的 C 库全是以二进制目标文件形式发布的,需要使用这些库时直接动态链接加载使用即可,不需要进行再次编译
37.可变参数模板
接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。
模板参数包:表示零个或多个模板参数;
函数参数包:表示零个或多个函数参数。
用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof... 运算符。
38.Redis五种数据结构
- 字符串String,可以被修改、称为动态字符串(SDS),结构包括数组容量、实际长度、标志位、数组内容。容量小于1MB时扩容翻倍,大于1MB时扩容1MB;
- 列表list,容量小时是个连续链表,较大时是个快速列表结构。
- hash字典,当发生散列冲突时将元素添加到列表中。值得注意的是,Redis Hash 值只能是字符串。Hash 和 String 都可以用来存储用户信息,但不同点在于 Hash 可以分别存储用户信息的每个字段。存储所有用户信息的序列化字符串。如果要修改用户字段,必须先查询所有用户信息字符串,解析为对应的用户信息对象,修改后再序列化为字符串。但是,哈希只可以修改某个字段,从而节省网络流量。但是,哈希的内存占用比 String 大,这是哈希的缺点
- 集合set,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
- zset有序集合,zset 也称为 SortedSet。一方面,它是一个集合,它保证了内部值的唯一性。另一方面,它可以为每个值分配一个分数,表示该值的排序权重。它的内部实现使用一种称为“跳转列表”的数据结构。
39.Redis内存淘汰机制
redis是基于内存的,因此内存占用空间会越来越高,需要进行替换
40.mysql为什么用B+树
数组:虽然可以实现随机访问,但增删元素需要移动过多元素,成本过高,并且需要一段连续的内存空间。
链表:插入、删除元素高效,但不能随即查找,需要遍历。
二叉树:相比较前两者,查找相对高效,但特殊情况会退化成链表。
平衡二叉树:磁盘IO进一步降低,但数据量较大时,树层数较多,查找效率也不行
B树:树层数进一步降低,但区间查找需要从叶子节点回到根节点,再去叶子节点
B+树:数据只在叶子节点,非叶子节点只有目录,存储量更大。叶子节点双向指针,支持区间查找,不用回根节点
41. IO多路复用-select
IO多路复用是一种网络通信手段,同时检测多个文件描述符并且过程是阻塞的,检测到就绪文件描述符,阻塞就会解除,基于这些文件描述符进行通信。实现单线程下的并发服务器访问。
区分多线程和IO多路复用:多线程下监听文件描述符是主线程,然后通信是另一个线程;而多路复用是委托服务器检查所有文件描述符状态,包括监听和通信两种,会导致主线程阻塞。检测对于不同状态做不同处理。
select: 跨平台,调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态。
select函数参数包括读缓冲区、写缓冲区、异常缓冲区;检查这三个文件描述符集合状态。
虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:
待检测集合(第2、3、4个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
内核对于select传递进来的待检测集合的检测方式是线性的
如果集合内待检测的文件描述符很多,检测效率会比较低
如果集合内待检测的文件描述符相对较少,检测效率会比较高
使用select能够检测的最大文件描述符个数有上限,默认是1024,这是在内核中被写死了的。
42. IO多路复用-poll
内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
poll和select检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
select检测的文件描述符个数上限是1024,poll没有最大文件描述符数量的限制
select可以跨平台使用,poll只能在Linux平台使用
函数使用poll,第一个参数是pollfd数组,表示需要检测的文件描述符集合,但是一个结构体元素,里面revents表示检测后的结果。
43. IO多路复用-epoll
- 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
- select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
- select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
- 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测。
- 使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
水平工作模式:内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行IO操作了。如果我们不作任何操作,内核还是会继续通知使用者。
边沿模式:文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。
epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。
函数:
- epoll_create(int size),创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,本身就是创建一个文件描述符,因此使用完后必须close,size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没有任何意义。

2. epoll_ctl(int epfd, int op, int fd, struct epoll_event *event), epoll事件注册函数,向epoll对象中增加、修改、删除事件,epfd表示创建的epoll实例。op表示操作动作(增加、修改、删除)。fd表示需要监听的文件描述符。event表示监听事件,struct epoll_event类型,

3. int num = epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):等待事件的产生。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误
- 循环遍历events,里面num个都是就绪的文件描述符,需要处理,取出其中data.fd,如果等于服务端监听文件描述符,说明需要建立连接,那就创建个event,把accept文件描述符装载,再放入epfd中就好。如果不是监听文件描述符,那只能是读写操作的了,
for(int i=0; i<num; i++){
int fd = evs[i].data.fd;
if(fd == sfd){
struct sockaddr_in cliAddr;
socklen_t cliLen = sizeof(cliAddr);
int cfd = accept(sfd, (struct sockaddr*)&cliAddr, &cliLen);
if(cfd < 0){
perror("socket accept error!");
exit(1);
}else{
std::cout<<"client ip: "<<inet_ntoa(cliAddr.sin_addr)<<"; port: "<<ntohs(cliAddr.sin_port)<<std::endl;
}
struct epoll_event ev_c; //创建epoll事件
ev_c.events = EPOLLIN | EPOLLET; //边沿触发模式
ev_c.data.fd = cfd;
int epctl = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev_c);
if(epctl < 0){
perror("epoll con ctl error!");
exit(1);
}
}else{
// std::thread t(ser_th, fd, epfd);
// t.detach();
char buff[5];
int readfd = read(fd, buff, sizeof(buff));
if(readfd < 0){
perror("buff read error!");
exit(1);
}else if(readfd == 0){//客户端断开连接
std::cout<<"client connection cut off"<<std::endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
std::cout<<"message form client: "<<buff<<std::endl;
toUpperCase(buff);
std::cout<<"message send: "<<buff<<std::endl;
int writefd = write(fd, buff, sizeof(buff));
if(writefd < 0){
perror("buff write error!");
exit(1);
}
}
}
44. call_onc
在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。
45.this_thread
线程的命名空间std::this_thread,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。
get_id():得到目前代码所处的线程id
sleep_for():调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。
sleep_until():指定线程阻塞到某一个指定的时间点time_point类型,之后解除阻塞
yield():在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。使用这个函数的时候需要注意一点,线程调用了yield()之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况。
46.C++ 锁
std::mutex:独占的互斥锁,不能递归使用
std::timed_mutex:带超时的独占互斥锁,不能递归使用
std::recursive_mutex:递归互斥锁,不带超时功能
std::recursive_timed_mutex:带超时的递归互斥锁;
lock锁函数,如果已经有锁,会阻塞。try_lock函数不会阻塞;
lock_guard是有关的锁的模板类,构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
std::timed_mutex是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
47. struct和class
- struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
- struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的。
- 在继承关系中,struct 默认是公有继承,而 class 是私有继承;
- class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数
48. 内存分区
- 栈:存储局部变量、函数参数、函数地址等等,由编译器统一管理
- 堆:new分配的内存空间,编译器不管,有应用程序负责释放。
- 全局/静态存储区:全局变量、静态变量分配的内存空间
- 常量存储区:存放常量,不允许被修改
- 代码区:存放函数的可执行二进制代码,由操作系统进行管理

再来谈一谈从段角度看程序内存:一个程序由bss段、data段、text段构成。而bss段不在可执行文件中。在段式内存管理中。
- bss段指存在程序中未初始化的全局变量的一块内存区域,初始化时就会清零,也就是程序执行时就会清零。bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
- text段用于存放程序代码的区域,编译时确定,只读。最后链接时将各个目标文件链接在一起,其对应的各个text段也会结合在一起。
- data段,存放编译阶段就能确定的数据,可读可写,也是通常所说的静态存储区,赋了初值的全局变量、常量和静态变量都存放在这个域。data段(已手动初始化的数据)为数据分配空间,数据保存在目标文件中。
内存错误:未分配,但使用;已经分配但未初始化;操作过界;没有释放内存;释放后继续使用
解决办法: 定义指针先初始化为nullptr;new后判断是否为空;new后必须delete;free后要指向NULL;
内存泄漏: 申请了一块内存,但使用完后没有释放:new后没有delete、类继承时基类析构函数不是虚函数等
内存对齐: 编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。为了快速准确的访问,若没有字节对齐则会出现多次访问浪费时间。
49. new/malloc区别
malloc是c标准库函数,new是C++运算符,都用于申请动态内存,但对于一个非内部数据类型的对象而言,例如c++自定义的类,对象在创建时需要自动执行构造函数,消亡时要执行析构函数,但malloc函数并没有这个权限,因此有了new运算符。
虚拟内存空间: 实现内存管理的一种机制。操作系统为每个进程维护一个虚拟内存空间。虚拟内存空间的顶部是内核管理的内存空间,下面则是用户的内存空间,用户空间无权访问内核的内存空间。运行时堆上方有一块地址,表示还未映射的内存区。实际物理地址中,已经映射的内存空间尾部有一个break指针,这个指针下面是映射好的内存,可以访问,上面则是未映射的访问,不能访问。可以通过系统调用sbrk(位移量)能一定brk指针的位置,同时返回brk指针的位置,达到申请内存的目。brk(void *addr)系统调用可以直接将brk设置为某个地址,成功返回0,不成功返回-1。在操作系统角度来看,分配内存有两种方式,一种是采用推进brk指针来增加堆的有效区域来申请内存空间,还有一种是采用mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式都是分配虚拟内存,只有当第一次访问虚拟地址空间时,操作系统给分配物理内存空间。
malloc实现原理: 系统中可用的内存块会连接成一个单链表-空闲链表,调用mal时,会从单链表寻找一块足够满足空间要求的内存块,把该内存块以分为2,A和B,A空间刚好满足申请要求,B是多余剩下的空间。再将A给用户,B在放回空闲链表中,free时A再回到空闲链表中。如果没有搜索到满足条件的内存块,才会申请更大的内存块,而有两种方式向操作系统申请堆内存。当申请内存小的时候,利用刚才说的系统调brk用扩大堆内存,但free后不会归还给操作系统,而是缓存在内存池中,为了下次使用;申请内存过大,则利用mmap再文件映射区分配内存,free时会归还给操作系统。malloc() 分配的是虚拟内存。如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。
为什么不都用mmap: mmap系统调用会设计运行态的转换,并且会发生缺页中断、整体性能消耗过大。
为什么不都使用brk: 随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。
搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。
- 首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
- 下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
- 最佳适配:对堆进行彻底的搜索,从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。
new: new会先调用operator new申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
new与malloc区别:
- new不需要指定请求内容的大小,自己会判断;malloc需要提供大小参数
- new返回的是指定类型的指针,不需要强制转换;malloc返回void*类型,需要强制转换
- new需要进行初始化;
50. const和define区别
const用来定义常量,define定义宏,define作用更多,同时也可以定义常量。都用于定于常量时,区别如下:
- define会在预处理阶段进行处理,const则是编译阶段
- const定义的常量需要额外的内存空间,define则是直接的操作数,不会放在内存中
- const是带类型的,define没有类型,因此有使用风险。
51. 指针函数与函数指针
指针函数本质是一个函数,只是返回值类型是指针
int *fun(int, int);
函数指针就是一个指向函数地址的指针,之前有总结过
52.智能指针
智能指针就是利用RAII技术,免去手动释放指针指向资源的步骤,做到自动释放内存资源。
解决问题的理念就是构建一个类,构造函数中new,析构函数中delete。这样,程序结束自动调用析构函数时就自动释放资源了。
C98中提出了auto_ptr,实现了智能指针的思想,但存在问题,出现拷贝情况是,旧指针指向空,但还存在,后续使用就会出现问题,存在内存泄漏问题。C11改进为unique_ptr,此时拷贝,旧指针会直接释放,后续就不涉及调用问题,也就没有了内存泄漏。对应三个函数,这里坑很多。。。
- get():获取智能指针托管的内存地址,实际就是指针存储的地址。智能指针不可以通过p直接访问地址,会报错
- release(): 取消智能指针对动态内存的托管,相当于指针指向空,但关联内容依旧存在,该函数会传出原始托管地址,可以赋值给正常指针:int *p2 = p1.release();也可以利用下一条要说的reset赋值给另一个智能指针。
记住,智能指针依旧存在,只是指向nullptr - reset(arg): 重新托管地址,如果arg为空,则表示取消地址管理,智能指针指向NULL;arg是新的托管地址,智能指针指向新地址,这个新地址就有说法了,是个普通指针地址还是智能指针托管的地址;如果是智能指针托管的地址,因为unique的专一性,会转移所有权,arg智能指针指向nullptr。如果是个普通指针,那就有趣了,虽然地址托管给了智能指针,但原始普通指针仍然可以访问之前指向的地址!但此时不能delete该原始指针,不然会二次释放,报错。可以赋值新的地址,这样这个指针就指向新内容了。
注意:
- 在初始化一个uniqueptr时,如果是利用原始指针赋值,此时智能指针虽然管理该地址,但原始指针同样也管理该地址,并且可以操作,但会发生未定义行为(异常、正常、错误等等),我测试时会出现“正常”情况,对原始指针依旧能正常操作,这个智能指针失去作用了,我delete原始指针也不会报错,按理会出现双重释放得错误,很迷。所以这个未定义行为有多种可能,不能保证程序按照需求运行,因此,需要及时将原始指针指向nullptr。
- 如果是利用reset重新托管,如果传入的是个普通指针,同样会出现上一条说的问题,因此需要将原始指针指向nullptr;并且,get函数返回的是原始指针,如果想利用get函数获取某个智能指针托管地址,然后作为reset参数让另一个智能指针管理,跟传一个普通指针没区别!
- 因此独占指针之间转移所有权最好的方法是move
- 如果非要利用reset,则是p1.reset(p.release()); 利用release释放并传出地址,然后另一个指针reset接受
特点:` 1. 排他锁有权模式,资源只能由一个unique管理; 2. 复制时需要move为左值
shared_ptr: 多个指针共享资源,利用计数表示有多少个指针共享资源,每构造一个就++,析构就--,如果计数为0,表示没有指针管理该资源,则释放该资源,使用use_count函数获得计数。构造函数形式类似于unique,但多了一个利用make_shared,这样分配效率更高;swap函数用来交换两个shared指针管理的对象。用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况,会造成内存泄漏;同时,避免同一个raw pointer初始化多个shared_ptr,此时虽然多个共享指针指向同一块资源,但是会产生不同的计数控制块,如果是两个shared,此时两个指针的use_count结果都是1!那么最后析构就会造成重复释放的问题。
weak_ptr:weak_ptr 设计的目的是为配合 shared_ptr , 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少,避免纯shared_ptr互相调用造成死锁。可以利用lock()函数讲weak变成shared属性。use_count获得计数。但不支持*和->访问,没有重载。
expired函数:判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true
weak_ptr使用场景:当作缓存;避免循环引用;实现单例模式;
其他问题:
- 尽可能使用make_unique和make_shared,这两个只进行一次内存分配,返回智能指针对象;而shared_ptr和unique_ptr会有两个内存分配,第一次分配给T类型,第二次给智能指针对象。std::shared_ptr p(new int(50));如果new失败会抛出异常,p智能指针将创建失败,但new分配的内存不会释放,造成了内存泄漏。
- 与普通指针做性能对比:shared_ptr占据更多内存、原子操作维护计数,因此性能慢;其他智能指针和普通指针性能差不多,但安全性更高
- 如果多个线程同时拷贝同一个 shared_ptr 对象,不会有问题,因为 shared_ptr 的引用计数是线程安全(原子操作)的。但是如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr 对象,并且有写操作,需要使用互斥量来保护。
- 再烧脑一下,如果现在需求是,类内部需要获得一个指向当前对象的sharedptr,怎么办,想法是std::shared_ptr(this),但是,当这个sharedptr销毁时,this指向的内存也会释放,那不就是类对象自身?这不就怪了,我只是释放sharedptr,但类对象this指针不能释放啊,万一之后还要用呢。解决办法:类继承enable_shared_from_this类,通过shared_from_this()返回一个指向自身的sharedptr!这个方法内部是通过weakptr包装成一个sharedptr,所以不影响计数,也就不能释放this,也就是类对象本身了!
- 智能指针可以指向数组吗?当然可以,并且再C17后,重载了[]运算符,就可以像操作数组那样访问该智能指针了。
53. 面向对象
面向对象是一种编程思想,把一切东西都看作一个个对象,这个对象有基本属性变量和操作这些属性的函数构成。
面向对象: 数据与函数绑定在一起,进行封装,加快开发流程。
面向过程:根据业务逻辑从上到下开发
**面向对象三大特性:**封装、继承、多态
封装:将数据和操作数据的方法进行结合,隐藏对象的属性和实现细节,仅对外提供公开接口来操作对象。
继承:使用现有类的所有功能,不用重新编写原有代码
public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员
派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。
protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;
派生类对象不可以访问基类的public、protected、private成员。
private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;
派生类对象不可以访问基类的public、protected、private成员。
多态:父类指针指向子类实例,通过父类指针调用子类成员函数;实现方式-重写、重载
重写: 子类存在重新定义的函数,函数名、参数列表、返回值类型都跟基类函数相同,只有函数体的实现不同,父类中用virtual修饰函数表示该函数会在字类中重写。
重载:某些函数功能相同,但细节不同,指具有不同参数列表的同名函数,根据实际参数调用不同的函数。利用命名倾轧技术更改了函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。编译时将参数类型加入用于区分。
虚函数:
- virtual声明的函数,属于成员函数
- 有虚函数的类都有一个虚函数表-虚表,类对象有一个只想虚表开始的虚指针,虚表和类对应,虚指针和实例对象对应。
- 纯虚函数-虚函数=0,此时父类变成了抽象类,不能实例化;并且子类必须继承这个虚函数
构造函数类型:
- 默认构造函数:在定义类的对象的时候,完成对象的初始化工作。有了有参的构造了,编译器就不提供默认的构造函数
- 拷贝构造函数:用于复制类对象,但是浅拷贝;参数必须是引用类型,传值的话会无穷无尽调用拷贝构造函数。
- 移动构造函数:用于将其他类型的变量,隐式转换为本类对象 /深拷贝
注意:
- 如果只定义析构函数,类会自己定义默认构造函数和拷贝构造函数
- 一个空类默认生成哪些函数:无参构造函数;拷贝构造函数;重载赋值运算符;析构函数;
- 构造函数调用顺序:父类构造函数–>成员类对象构造函数–>自身构造函数
- 析构函数调用顺序:与构造函数相反
54. 析构函数的虚函数
虚析构:将可能会被继承的基类析构函数设置为虚函数,这样在new一个子类对象,用基类指针指向该子类对象时,最后在释放基类指针时,就会准确调用子类改写的析构函数,准确释放内存,防止内存泄漏。如果析构函数没有重写,那么就是正常的函数覆盖,基类指针会调用基类的析构函数,子类内存得不到释放,就出现了内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存
不能虚构造:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用?出现悖论
55. 模板类
模板实例化:分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现
模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理
56.常函数
类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立
57. 构造函数中的能不能调用虚方法
不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。虚函数就是为了实现多态,但构造函数中如果调用虚函数,结果是先调用父类的虚函数,然后调用子类的虚函数。虚函数两个版本都会调用,显然与虚函数作用不相符,我们只想调用子类的虚函数。
58. 仿函数
又称函数对象,指能行使函数功能的类,使用方法与普通函数相同,并且仿函数必须重载()运算符,才能达到像普通函数那样访问。
59. 类模板和模板类
-
类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
-
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
60. 类的大小
c++中,类得大小主要取决雨成员变量的大小,但存在以下特殊情况
- 静态成员变量,因为多有对象共享同一个静态成员变量,因此静态成员变量不占类内存,而是在全局静态区中
- 内存对齐:最大成员变量的整数倍内存,方便读取数据。例如char+double类型,本身共占9个字节,但实际会占16个字节,8的整数倍。
- 虚函数:会有一个虚函数指针占8字节
- 虚基类指针:虚基类指针也占8字节;但vs中子类会有个基类的缓存内存,如果基类有个int,那么子类就会多4字节的缓存空间,再加上内存对齐(虚基类指针要8字节,最大),因此会再多8字节内存。
- 空类:占一个字节,保证地址独立i