2024年最全的C++面试题

来源面试题仓库

一、C++基础

1. C和C++有什么区别?

  • C++是面向对象的语言,而C是面向过程的语言;

  • C++引入new/delete运算符,取代了C中的malloc/free库函数;

  • C++引入引用的概念,而C中没有;

  • C++引入类的概念,而C中没有;

  • C++引入函数重载的特性,而C中没有

2. a和&a有什么区别?

  • 假设数组int a[10]; int (*p)[10] = &a;其中:

  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。

  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。

  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

3. static关键字有什么作用?

  • 修饰局部变量时,使得该变量在静态存储区分配内存;只能在首次函数调用中进行首次初始化,之后的函数调用不再进行初始化;其生命周期与程序相同,但其作用域为局部作用域,并不能一直被访问;

  • 修饰全局变量时,使得该变量在静态存储区分配内存;在声明该变量的整个文件中都是可见的,而在文件外是不可见的;

  • 修饰函数时,在声明该函数的整个文件中都是可见的,而在文件外是不可见的,从而可以在多人协作时避免同名的函数冲突;

  • 修饰成员变量时,所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;不需要实例化对象即可访问;不能在类内部初始化,一般在类外部初始化,并且初始化时不加static;

  • 修饰成员函数时,该函数不接受this指针,只能访问类的静态成员;不需要实例化对象即可访问。

4. #define和const有什么区别?

  • 编译器处理方式不同:#define宏是在预处理阶段展开,不能对宏定义进行调试,而const常量是在编译阶段使用;

  • 类型和安全检查不同:#define宏没有类型,不做任何类型检查,仅仅是代码展开,可能产生边际效应等错误,而const常量有具体类型,在编译阶段会执行类型检查;

  • 存储方式不同:#define宏仅仅是代码展开,在多个地方进行字符串替换,不会分配内存,存储于程序的代码段中,而const常量会分配内存,但只维持一份拷贝,存储于程序的数据段中。

  • 定义域不同:#define宏不受定义域限制,而const常量只在定义域内有效。

5. 静态链接和动态链接有什么区别?

  • 静态链接是在编译链接时直接将需要的执行代码拷贝到调用处; 优点在于程序在发布时不需要依赖库,可以独立执行,缺点在于程序的体积会相对较大,而且如果静态库更新之后,所有可执行文件需要重新链接;

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

  • 优点在于多个程序可以共享同一个动态库,节省资源;

  • 缺点在于由于运行时加载,可能影响程序的前期执行性能。

6. 变量的声明和定义有什么区别

变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。一个变量可以在多个地方声明, 但是只在一个地方定义。加入extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间, 如外部变量。

int main() 
{
   extern int A;
   //这是个声明而不是定义,声明A是一个已经定义了的外部变量
   //注意:声明外部变量时可以把变量类型去掉如:extern A;
   dosth(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量

7. 简述#ifdef、#else、#endif和#ifndef的作用

利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。

#ifdef MATH
#include "math.c"
#endif

在子程序前加上标记,以便于追踪和调试。

#ifdef DEBUG
printf ("Indebugging......!");
#endif

应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直接写出预期结果。

「注意」:虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长(因为所有语句都编译),运行时间长(因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。

8. 写出int 、bool、 float 、指针变量与 “零值”比较的if 语句

首先给个提示:题目中要求的是零值比较,而非与0进行比较,在C++里“零值”的范围可就大了,可以是0, 0.0 , FALSE或者“空指针”。

下面是答案。

//int与零值比较 
if ( n == 0 )
if ( n != 0 )

//bool与零值比较 
if   (flag) //   表示flag为真 
if   (!flag) //   表示flag为假 

//float与零值比较 
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON) //其中EPSINON是允许的误差(即精度)。
//指针变量与零值比较 
if (p == NULL)
if (p != NULL)

详细解释

  • int:int 是整型,可以直接和 0 比较。

  • bool:根据布尔类型的语义,零值为假(记为FALSE),任何非零值都是真(记为TRUE)。TRUE 的值究竟是什么并没有统一的标准。

  • 例如Visual C++ 将TRUE 定义为1,而Visual Basic 则将TRUE 定义为 -1。所以我们不可以将布尔变量直接与TRUE、FALSE 或者1、0 进行比较

  • float:千万要留意,无论是float 还是double 类型的变量,都有精度限制,都不可以用==”或!=与任何数字比较,应该设法转化成>=或<=`形式。

  • 其中EPSINON 是允许的误差(即精度)

  • 指针:指针变量的零值就是NULL

9. 结构体可以直接赋值吗

声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但是当结构体中含有指针“成员”时一定要小心。

  • 「注意」:当有多个指针指向同一段内存时,某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前一定要确保其他指针不再使用这段内存空间。

10. sizeof 和strlen 的区别

  • sizeof是一个操作符,strlen是库函数。

  • sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数。

  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。

  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了

11. C 语言的关键字 static 和 C++ 的关键字 static 有什么区别

在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。

  • 「注意」:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。

12. volatile有什么作用

  • 状态寄存器一类的并行设备硬件寄存器。

  • 一个中断服务子程序会访问到的非自动变量。

  • 多线程间被几个任务共享的变量。

  • 「注意」:虽然volatile在嵌入式方面应用比较多,但是在PC软件的多线程中,volatile修饰的临界变量也是非常实用的。

13. 一个参数可以既是const又是volatile吗

  • 可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。

  • 注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。

14. 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?

  • 全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);

  • 而局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。

  • 操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。

15. 简述strcpy、sprintf 与memcpy 的区别

  • 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, 目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

  • 执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。

  • 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷贝。

  • 「注意」:strcpy、sprintf 与memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来 选择合适的函数实现拷贝功能。

16. 对于一个频繁使用的短小函数,应该使用什么来实现?有什么优缺点?

应该使用inline内联函数,即编译器将inline内联函数内的代码替换到函数被调用的地方。

优点:

  • 在内联函数被调用的地方进行代码展开,省去函数调用的时间,从而提高程序运行效率;

  • 相比于宏函数,内联函数在代码展开时,编译器会进行语法安全检查或数据类型转换,使用更加安全;

缺点:

  • 代码膨胀,产生更多的开销;

  • 如果内联函数内代码块的执行时间比调用时间长得多,那么效率的提升并没有那么大; -如果修改内联函数,那么所有调用该函数的代码文件都需要重新编译;

  • 内联声明只是建议,是否内联由编译器决定,所以实际并不可控。

17. 什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?

智能指针是一个RAII类模型,用于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作用域时调用析构函数,使用delete删除指针所指向的内存空间。

智能指针的作用是,能够处理内存泄漏问题和空悬指针问题。

分为auto_ptr、unique_ptr、shared_ptr和weak_ptr四种,各自的特点:

  • 对于auto_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象;但auto_ptr

在C++11中被摒弃,其主要问题在于:

  • 对象所有权的转移,比如在函数传参过程中,对象所有权不会返还,从而存在潜在的内存崩溃问题;

  • 不能指向数组,也不能作为STL容器的成员。

  • 对于unique_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象,因为无法进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值;

  • 对于shared_ptr,实现共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象及相关资源会在其所指对象不再使用之后,自动释放与对象相关的资源;

  • 对于weak_ptr,解决shared_ptr相互引用时,两个指针的引用计数永远不会下降为0,从而导致死锁问题。而weak_ptr是对对象的一种弱引用,可以绑定到shared_ptr,但不会增加对象的引用计数。

18. shared_ptr是如何实现的?

  • 构造函数中计数初始化为1;

  • 拷贝构造函数中计数值加1;

  • 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;

  • 析构函数中引用计数减1;

  • 在赋值运算符和析构函数中,如果减1后为0,则调用delete释放对象。

19. 右值引用有什么作用?

右值引用的主要目的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷贝,也能够更加简洁明确地定义泛型函数

20.悬挂指针与野指针有什么区别?

  • 悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针;

  • 野指针:未初始化的指针被称为野指针。

21. 动态多态有什么作用?有哪些必要条件?

动态多态性:

  • 1.虚方法的动态多态性

  • 2.抽象方法的动态多态性

  • 3.接口方法的动态多态性

动态多态性的作用:

  • 1.实现”功能定义" 与 "功能实现" 相脱离,实现代码进一步灵活性

  • 2.实现"系统设计" 与 "系统代码" 相脱离,实现代码稳定性,减少系统BUG

22.请解析((void ()( ) )0)( )的含义

  • void (*0)( ) :是一个返回值为void,参数为空的函数指针0。

  • (void (*)( ))0:把0转变成一个返回值为void,参数为空的函数指针。

    • (void (**)( ))0:在上句的基础上加表示整个是一个返回值为void,无参数,并且起始地址为0的函数的名字。

  • ((void ()( ))0)( ):这就是上句的函数名所对应的函数的调用。

23. C语言的指针和引用和c++的有什么区别?

  • 指针有自己的一块空间,而引用只是一个别名;

  • 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

  • 可以有const指针,但是没有const引用;(具体解释看评论区)

  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

  • 指针可以有多级指针(**p),而引用止于一级;

  • 指针和引用使用++运算符的意义不一样;

  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

24. typedef 和define 有什么区别

  • 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。

  • 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生- 在编译之前,只是简单的进行字符串的替换,不进行类型的检查。

  • 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。

  • 对指针的操作不同:typedef 和define 定义的指针时有很大的区别。

  • 「注意」:typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号。

25. 指针常量与常量指针区别

  • 指针常量:指针本身的值是不可改变的,但指针指向的变量的值是可以改变的。

  • 常量指针:指针指向的变量的值是不可改变的,但指针本身的值是可以改变的

26. 简述队列和栈的异同

队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。

「注意」:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分 配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员 分配释放, 若程序员不释放,程序结束时可能由OS 回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

27. 设置地址为0x67a9 的整型变量的值为0xaa66

int *ptr; 
ptr = (int *)0x67a9; 
*ptr = 0xaa66; 

「注意」:这道题就是强制类型转换的典型例子,绝大部份情况下,地址长度和整型数据的长度是一样的(此时的整型指的是 long), 即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

28. C语言的结构体和C++的有什么区别

  • C语言的结构体是不能有函数成员的,而C++的类可以有。

  • C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问限定。

  • C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。

  • 「注意」:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。

29. 如何避免“野指针”

  • 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。

  • 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。

  • 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

30.句柄和指针的区别和联系是什么?

句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念。

31. 说一说extern“C”

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。

  • C++代码调用C语言代码

  • 在C++的头文件中使用

  • 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

32. 对c++中的smart pointer四个智能指针:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

  • auto_ptr(c++98的方案,cpp11已经抛弃) 采用所有权模式。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

  • unique_ptr(替换auto_ptr) unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

采用所有权模式。

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

「注意」:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
  • shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

  • use_count 返回引用计数的个数

  • unique 返回是否是独占所有权( use_count 为 1)

  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptrsp(new int(1)); sp 与 sp.get()是等价的

  • weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
     cout<<"A delete
";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
    cout<<"B delete
";
}
};
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
int main()
{
    fun();
    return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

「注意」:不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

33. C++的顶层const和底层const ?

  • 底层const是代表对象本身是一个常量(不可改变);

  • 顶层const是代表指针的值是一个常量,而指针的值(即对象的地址)的内容可以改变(指向的不可改变);

34. 拷贝初始化和直接初始化,初始化和赋值的区别?

  • ClassTest ct1(“ab”); 这条语句属于直接初始化,它不需要调用复制构造函数,直接调用构造函数ClassTest(constchar *pc),所以当复制构造函数变为私有时,它还是能直接执行的。

  • ClassTest ct2 = “ab”; 这条语句为复制初始化,它首先调用构造函数 ClassTest(const char* pc) 函数创建一个临时对象,然后调用复制构造函数,把这个临时对象作为参数,构造对象ct2;所以当复制构造函数变为私有时,该语句不能编译通过。

  • ClassTest ct3 = ct1;这条语句为复制初始化,因为 ct1 本来已经存在,所以不需要调用相关的构造函数,而直接调用复制构造函数,把它值复制给对象 ct3;所以当复制构造函数变为私有时,该语句不能编译通过。

  • ClassTest ct4(ct1);这条语句为直接初始化,因为 ct1 本来已经存在,直接调用复制构造函数,生成对象 ct3 的副本对象 ct4。所以当复制构造函数变为私有时,该语句不能编译通过。

要点就是拷贝初始化和直接初始化调用的构造函数是不一样的,但是当类进行复制时,类会自动生成一个临时的对象,然后再进行拷贝初始化。

35. C++异常机制

在C++中,使用异常机制可以提高程序的健壮性和可维护性。异常是在程序运行时发生的一个事件,它会打断正在执行的程序的正常流程。C++异常处理机制可以使程序在出现异常时,进行异常处理,而不是退出程序。

  • 基本的异常处理

#include <iostream>
using namespace std;
 
int main() {
    try {
        throw "错误";  // 抛出异常
    } catch (const char* msg) {
        cerr << msg << endl;  // 捕获并处理异常
    }
    return 0;
}
  • 使用不同类型的异常

  #include <iostream>
using namespace std;
 
class MyException : public exception {
    const char* what() const throw() {
        return "MyException occurred!";
    }
};
 
int main() {
    try {
        throw MyException();  // 抛出异常
    } catch (MyException& e) {
        cerr << "MyException caught: " << e.what() << endl;  // 捕获并处理异常
    } catch (exception& e) {
        cerr << "Exception caught: " << e.what() << endl;
    }
    return 0;
}
  • 在函数中使用异常声明

#include <iostream>
using namespace std;

double divide(double a, double b) throw(int) {
  if (b == 0)
      throw 1;  // 抛出一个整型异常
  return a / b;
}

int main() {
  try {
      divide(1, 0);
  } catch (int e) {
      cerr << "Divided by zero! " << e << endl;  // 捕获并处理异常
  }
  return 0;
}
  • 在函数中使用异常声明

#include <iostream>
using namespace std;

double divide(double a, double b) throw(int) {
  if (b == 0)
      throw 1;  // 抛出一个整型异常
  return a / b;
}

int main() {
  try {
      divide(1, 0);
  } catch (int e) {
      cerr << "Divided by zero! " << e << endl;  // 捕获并处理异常
  }
  return 0;
}
  • 使用标准异常类

#include <iostream>
#include <stdexcept>
using namespace std;

int main() {
  try {
      throw out_of_range("数组越界");  // 抛出异常
  } catch (const out_of_range& e) {
      cerr << e.what() << endl;  // 捕获并处理异常
  }
  return 0;
}
  • 在构造函数中抛出异常

#include <iostream>
using namespace std;

class Test {
public:
  Test(int x) {
      if (x < 0)
          throw "负数错误";  // 在构造函数中抛出异常
  }
};

int main() {
  try {
      Test t(-1);
  } catch (const char* msg) {
      cerr << msg << endl;  // 捕获并处理异常
  }
  return 0;
}
  • 在析构函数中捕获异常

 #include <iostream>
using namespace std;
 
class Test {
public:
    ~Test() {
        try {
            throw "异常";  // 在析构函数中抛出异常
        } catch (const char* msg) {
            cerr << msg << endl;  // 在析构函数中捕获并处理异常
        }
    }
};
 
int main() {
    Test t;
    return 0;
}

以上就是C++异常处理的一些基本用法。在实际编程中,应该尽量避免使用C++异常规范,尽可能使用C++标准库提供的异常类,并在函数声明中明确指出可能抛出的异常类型,以保证代码的可维护性和可读性。

二、C++面向对象

1. 面向对象的三大特征是哪些?

  • 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。

  • 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行拓展;

  • 多态:一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。

2. C++中类成员的访问权限

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

3. 多态的实现有哪几种?

多态分为静态多态和动态多态。其中,静态多态是通过重载和模板技术实现的,在编译期间确定;动态多态是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。

4. 动态绑定是如何实现的?

当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。

5. 动态多态有什么作用?有哪些必要条件?

动态多态的作用:

  • 隐藏实现细节,使代码模块化,提高代码的可复用性;

  • 接口重用,使派生类的功能可以被基类的指针/引用所调用,即向后兼容,提高代码的可扩充性和可维护性。

动态多态的必要条件:

  • 需要有继承;

  • 需要有虚函数覆盖;

  • 需要有基类指针/引用指向子类对象

6. 纯虚函数有什么作用?如何实现?

  • 定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。

  • 实现方式是在虚函数声明的结尾加上= 0即可。

7. 虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?

虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。

8. 为什么基类的构造函数不能定义为虚函数?

虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。

9. 为什么基类的析构函数需要定义为虚函数?

为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据,所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而销毁派生类对象中的所有数据。

10. 构造函数和析构函数能抛出异常吗?

  • 从语法的角度来说,构造函数可以抛出异常,但从逻辑和风险控制的角度来说,尽量不要抛出异常,否则可能导致内存泄漏。

  • 析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄露的问题;而且当异常发生时,C++通常会调用对象的析构函数来释放资源,如果此时析构函数也抛出异常,即前一个异常未处理又出现了新的异常,从而造成程序崩溃的问题。

11. 如何让一个类不能实例化?

将类定义为抽象类(也就是存在纯虚函数)或者将构造函数声明为private。

12. 多继承存在什么问题?如何消除多继承中的二义性?

增加程序的复杂度,使得程序的编写和维护比较困难,容易出错; 在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性;

消除同名二义性的方法:

  • 利用作用域运算符::,用于限定派生类使用的是哪个基类的成员;

  • 在派生类中定义同名成员,覆盖基类中的相关成员;

当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类的成员时,将产生另一种不确定性,即路径二义性;

消除路径二义性的方法:

  • 消除同名二义性的两种方法都可以;

  • 使用虚继承,使得不同路径继承来的同名成员在内存中只有一份拷贝。

13. 如果类A是一个空类,那么sizeof(A)的值为多少?

sizeof(A)的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。

14. 覆盖和重载之间有什么区别?

  • 覆盖是指派生类中重新定义的函数,其函数名、参数列表、返回类型与父类完全相同,只是函数体存在区别;覆盖只发生在类的成员函数中;

  • 重载是指两个函数具有相同的函数名,不同的参数列表,不关心返回值;当调用函数时,根据传递的参数列表来判断调用哪个函数;重载可以是类的成员函数,也可以是普通函数。

15. 拷贝构造函数和赋值运算符重载之间有什么区别?

拷贝构造函数用于构造新的对象;

Student s;
Student s1 = s; // 隐式调用拷贝构造函数
Student s2(s);  // 显式调用拷贝构造函数

赋值运算符重载用于将源对象的内容拷贝到目标对象中,而且若源对象中包含未释放的内存需要先将其释放;

Student s;
Student s1;
s1 = s; // 使用赋值运算符

一般情况下,类中包含指针变量时需要重载拷贝构造函数、赋值运算符和析构函数。

16. 对虚函数和多态的理解

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

17. 请你来说一下C++中struct和class的区别

在C++中,class和struct做类型定义如下区别:

  • 默认继承权限不同,class继承默认是private继承,而struct默认是public继承

  • class还可用于定义模板参数,像typename,但是关键字struct不能用于定义模板参数 C++保留struct关键字,主要有如下原因

  • 保证与C语言的向下兼容性,C++必须提供一个struct

  • C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制

  • 对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

18. 说说C++的四种强制类型转换运算符

1、reinterpret_cast

reinterpret_cast< type-id > (expression)

type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

2、const_cast

const_cast (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象

  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象

  • const_cast一般用于修改底指针。如const char *p形式

3、static_cast

static_cast < type-id > (expression)

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换

  • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的

  • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的

  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把空指针转换成目标类型的空指针

  • 把任何类型的表达式转换成void类型 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

4、dynamic_cast

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast (expression)

该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*

如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么 expression 也必须是一个引用

dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的

在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

举个例子:

#include <bits/stdc++.h>
using namespace std;

class Base
{
public:
    Base() :b(1) {}
    virtual void fun() {};
    int b;
};

class Son : public Base
{
public:
    Son() :d(2) {}
    int d;
};

int main()
{
    int n = 97;

    //reinterpret_cast
    int *p = &n;
    //以下两者效果相同
    char *c = reinterpret_cast<char*> (p); 
    char *c2 =  (char*)(p);
    cout << "reinterpret_cast输出:"<< *c2 << endl;
    //const_cast
    const int *p2 = &n;
    int *p3 = const_cast<int*>(p2);
    *p3 = 100;
    cout << "const_cast输出:" << *p3 << endl;

    Base* b1 = new Son;
    Base* b2 = new Base;

    //static_cast
    Son* s1 = static_cast<Son*>(b1); //同类型转换
    Son* s2 = static_cast<Son*>(b2); //下行转换,不安全
    cout << "static_cast输出:"<< endl;
    cout << s1->d << endl;
    cout << s2->d << endl; //下行转换,原先父对象没有d成员,输出垃圾值

    //dynamic_cast
    Son* s3 = dynamic_cast<Son*>(b1); //同类型转换
    Son* s4 = dynamic_cast<Son*>(b2); //下行转换,安全
    cout << "dynamic_cast输出:" << endl;
    cout << s3->d << endl;
    if(s4 == nullptr)
        cout << "s4指针为nullptr" << endl;
    else
        cout << s4->d << endl;


    return 0;
}
//输出结果
//reinterpret_cast输出:a
//const_cast输出:100
//static_cast输出:
//2
//-33686019
//dynamic_cast输出:
//2
//s4指针为nullptr

从输出结果可以看出,在进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。

19. 简述类成员函数的重写、重载和隐藏的区别

(1)重写和重载主要有以下几点不同。

  • 范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。

  • 参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一 定不同。

  • virtual 的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被 virtual 修饰,也可以没有。

(2)隐藏和重写、重载有以下几点不同。

  • 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。

  • 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的函数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。

「注意」:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完 全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

20. 类型转换分为哪几种?各自有什么样的特点?

  • static_cast:用于基本数据类型之间的转换、子类向父类的安全转换、void*和其他类型指针之间的转换;

  • const_cast:用于去除const或volatile属性;

  • dynamic_cast:用于子类和父类之间的安全转换,可以实现向上向下转换,因为编译器默认向上转换总是安全的,而向下转换时,dynamic_cast具有类型检查的功能;

  • dynamic_cast转换失败时,对于指针会返回目标类型的nullptr,对于引用会返回bad_cast异常;

  • reinterpret_cast:用于不同类型指针之间、不同类型引用之间、指针和能容纳指针的整数类型之间的转换。

21. RTTI是什么?其原理是什么?

RTTI即运行时类型识别,其功能由两个运算符实现:

  • typeid运算符,用于返回表达式的类型,可以通过基类的指针获取派生类的数据类型;

  • dynamic_cast运算符,具有类型检查的功能,用于将基类的指针或引用安全地转换成派生类的指针或引用。

22. 说一说c++中四种cast转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

1、const_cast

  • 用于将const变量转为非const 2、static_cast

  • 用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

3、dynamic_cast

用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

  • 向上转换:指的是子类向基类的转换

  • 向下转换:指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

4、reinterpret_cast

  • 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

5、为什么不使用C的强制转换?

  • C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

23. C++的空类有哪些成员函数

  • 缺省构造函数。

  • 缺省拷贝构造函数。

  • 省析构函数。

  • 赋值运算符。

  • 取址运算符。

  • 取址运算符 const 。

「注意」:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是 空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

24. 模板函数和模板类的特例化

「引入原因」

编写单一的模板,它能适应多种类型的需求,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化

「定义」对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上

(1)模板函数特例化

必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参,举例如下:

template<typename T> //模板函数
int compare(const T &v1,const T &v2)
{
    if(v1 > v2) return -1;
    if(v2 > v1) return 1;
    return 0;
}
//模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T
template<> 
int compare(const char* const &v1,const char* const &v2)
{
    return strcmp(p1,p2);
}

「本质」特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如,此处如果是compare(3,5),则调用普通的模板,若为compare(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意二者函数体的语句不一样了,实现不同功能。

「注意」模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。

(2)类模板特例化

原理类似函数模板,不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。对类进行特例化时,仍然用template<>表示是一个特例化版本,例如:

template<>
class hash<sales_data>
{
    size_t operator()(sales_data& s);
    //里面所有T都换成特例化类型版本sales_data
    //按照最佳匹配原则,若T != sales_data,就用普通类模板,否则,就使用含有特定功能的特例化版本。
};

「类模板的部分特例化」

不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,哪个最匹配,就用相应的模板)

「特例化类中的部分成员」

可以特例化类中的部分成员函数而不是整个类,举个例子:

template<typename T>
class Foo
{
    void Bar();
    void Barst(T a)();
};

template<>
void Foo<int>::Bar()
{
    //进行int类型的特例化处理
    cout << "我是int型特例化" << endl;
}

Foo<string> fs;
Foo<int> fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo<string>::Bar()
fi.Bar();//特例化版本,执行Foo<int>::Bar()
//Foo<string>::Bar()和Foo<int>::Bar()功能不同

25. 为什么析构函数一般写成虚函数

由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。举个例子:

#include <iostream>
using namespace std;

class Parent{
public:
    Parent(){
        cout << "Parent construct function"  << endl;
    };
    ~Parent(){
        cout << "Parent destructor function" <<endl;
    }
};

class Son : public Parent{
public:
    Son(){
        cout << "Son construct function"  << endl;
    };
    ~Son(){
        cout << "Son destructor function" <<endl;
    }
};

int main()
{
    Parent* p = new Son();
    delete p;
    p = NULL;
    return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Parent destructor function

将基类的析构函数声明为虚函数:

#include <iostream>
using namespace std;

class Parent{
public:
    Parent(){
        cout << "Parent construct function"  << endl;
    };
    virtual ~Parent(){
        cout << "Parent destructor function" <<endl;
    }
};

class Son : public Parent{
public:
    Son(){
        cout << "Son construct function"  << endl;
    };
    ~Son(){
        cout << "Son destructor function" <<endl;
    }
};

int main()
{
    Parent* p = new Son();
    delete p;
    p = NULL;
    return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Son destructor function
//Parent destructor function

三、C++ STL

1.什么是C++ STL?

C++ STL从广义来讲包括了三类:算法,容器和迭代器。

  • 算法包括排序,复制等常用算法,以及不同容器特定的算法。

  • 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。

  • 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

2. 什么时候需要用hash_map,什么时候需要用map?

总体来说,hash_map 查找速度会比 map 快,而且查找速度基本和数据数据量大小无关,属于常数级别;而 map 的查找速度是 log(n) 级别。

并不一定常数就比 log(n) 小,hash 还有 hash 函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑 hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map 可能会让你陷入尴尬,特别是当你的 hash_map 对象特别多时,你就更无法控制了。而且 hash_map 的构造速度较慢。

现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用 。

3. STL中hashtable的底层实现?

STL中的hashtable使用的是开链法解决hash冲突问题

b99fa907658140ecc8258b4e324b5cdf_1566639786045.png

hashtable中的bucket所维护的list既不是list也不是slist,而是其自己定义的由hashtable_node数据结构组成的linked-list,而bucket聚合体本身使用vector进行存储。hashtable的迭代器只提供前进操作,不提供后退操作

在hashtable设计bucket的数量上,其内置了28个质数[53, 97, 193,…,429496729],在创建hashtable时,会根据存入的元素个数选择大于等于元素个数的质数作为hashtable的容量(vector的长度),其中每个bucket所维护的linked-list长度也等于hashtable的容量。如果插入hashtable的元素个数超过了bucket的容量,就要进行重建table操作,即找出下一个质数,创建新的buckets vector,重新计算元素在新hashtable的位置。

4. vector 底层原理及其相关面试题

(1)vector的底层原理

vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。

当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。

60a71c3d66223b587e83dd18d57f6f09_70.png

60a71c3d66223b587e83dd18d57f6f09_70.png

(2)vector中的reserve和resize的区别

reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。

resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。

(3)vector中的size和capacity的区别

size表示当前vector中有多少个元素(finish – start),而capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage – start)。

(4)vector的元素类型可以是引用吗?

vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。

(5)vector迭代器失效的情况

当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。

当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。

(6)正确释放vector的内存(clear(), swap(), shrink_to_fit())

  • vec.clear():清空内容,但是不释放内存。

  • vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。

  • vec.shrink_to_fit():请求容器降低其capacity和size匹配。

  • vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

(7)vector 扩容为什么要以1.5倍或者2倍扩容?

根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2倍的方式扩容,或者以1.5倍的方式扩容。

以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间:

929920f50fde91c86bcc25d2ff600525_20160915175318430.png

929920f50fde91c86bcc25d2ff600525_20160915175318430.png

(8)vector的常用函数

vector<int> vec(10,100);        创建10个元素,每个元素值为100
vec.resize(r,vector<int>(c,0)); 二维数组初始化
reverse(vec.begin(),vec.end())  将元素翻转
sort(vec.begin(),vec.end());    排序,默认升序排列
vec.push_back(val);             尾部插入数字
vec.size();                     向量大小
find(vec.begin(),vec.end(),1);  查找元素
iterator = vec.erase(iterator)  删除元素

5.list 底层原理及其相关面试题

list 底层原理及其相关面试题

(1)list的底层原理

list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。

list不支持随机存取,适合需要大量的插入和删除,而不关心随即存取的应用场景。

2ac9186574aae0cc4d9406e994da19d5_format,png.png

2ac9186574aae0cc4d9406e994da19d5_format,png.png

(2)list的常用函数

list.push_back(elem)    在尾部加入一个数据
list.pop_back()         删除尾部数据
list.push_front(elem)   在头部插入一个数据
list.pop_front()        删除头部数据
list.size()             返回容器中实际数据的个数
list.sort()             排序,默认由小到大 
list.unique()           移除数值相同的连续元素
list.back()             取尾部迭代器
list.erase(iterator)    删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置

6.deque底层原理及其相关面试题

(1)deque的底层原理

deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。

a67f1746a8bc1a5a08fa4bbb2e4dcb4e_1565876324016.png

(2)什么情况下用vector,什么情况下用list,什么情况下用deque

vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。

list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。

需要从首尾两端进行插入或删除操作的时候需要选择deque。

(3)deque的常用函数

deque.push_back(elem)   在尾部加入一个数据。
deque.pop_back()        删除尾部数据。
deque.push_front(elem)  在头部插入一个数据。
deque.pop_front()       删除头部数据。
deque.size()            返回容器中实际数据的个数。
deque.at(idx)           传回索引idx所指的数据,如果idx越界,抛出out_of_range。

7.Vector如何释放空间?

由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。

如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。

vector(Vec).swap(Vec); //将Vec的内存清除; 
vector().swap(Vec); //清空Vec的内存;

8.如何在共享内存上使用STL标准库?

  1. 想像一下把STL容器,例如map, vector, list等等,放入共享内存中,IPC一旦有了这些强大的通用数据结构做辅助,无疑进程间通信的能力一下子强大了很多。

我们没必要再为共享内存设计其他额外的数据结构,另外,STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装,默认情况下有它们自己的内存管理方案。

当一个元素被插入到一个STL列表(list)中时,列表容器自动为其分配内存,保存数据。考虑到要将STL容器放到共享内存中,而容器却自己在堆上分配内存。

一个最笨拙的办法是在堆上构造STL容器,然后把容器复制到共享内存,并且确保所有容器的内部分配的内存指向共享内存中的相应区域,这基本是个不可能完成的任务。

  1. 假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?

一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。

进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

9. map插入方式有哪几种?

  1. 用insert函数插入pair数据,

mapStudent.insert(pair<int, string>(1, "student_one")); 
  1. 用insert函数插入value_type数据

mapStudent.insert(map<int, string>::value_type (1, "student_one"));
  1. 在insert函数中使用make_pair()函数

mapStudent.insert(make_pair(1, "student_one"));
  1. 用数组方式插入数据

mapStudent[1] = "student_one"; 

10.map 、set、multiset、multimap 底层原理及其相关面试题

(1)map 、set、multiset、multimap的底层原理

map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。

红黑树的特性:

  • 每个结点或是红色或是黑色;

  • 根结点是黑色;

  • 每个叶结点是黑的;

  • 如果一个结点是红的,则它的两个儿子均是黑色;

  • 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。 对于STL里的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0统计的是key出现的次数,因此只能为0/1,而mp.find(key) != mp.end()则表示key存在。

(2)map 、set、multiset、multimap的特点

set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。

map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。

map和set的增删改查速度为都是logn,是比较高效的。

(3)为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?

因为存储的是结点,不需要内存拷贝和内存移动。

因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

(4)为何map和set不能像vector一样有个reserve函数来预分配数据?

因为在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。

(5)map 、set、multiset、multimap的常用函数

it map.begin()          返回指向容器起始位置的迭代器(iterator) 
it map.end()             返回指向容器末尾位置的迭代器 
bool map.empty()         若容器为空,则返回true,否则false
it map.find(k)           寻找键值为k的元素,并用返回其地址
int map.size()           返回map中已存在元素的数量
map.insert({int,string}) 插入元素
for (itor = map.begin(); itor != map.end();)
{
    if (itor->second == "target")
        map.erase(itor++) ; // erase之后,令当前迭代器指向其后继。
    else
        ++itor;
}

11.unordered_map、unordered_set 底层原理及其相关面试题

(1)unordered_map、unordered_set的底层原理

unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。

使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数(一般使用除留取余法),也叫做散列函数),使得每个元素的key都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照key为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。

但是,不能够保证每个元素的key与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。 一般可采用拉链法解决冲突:

3efb425132ea886e81dfc9922787f396_format,png-20210805162602354.png

(2)哈希表的实现

#include <iostream>
#include <vector>
#include <list>
#include <random>
#include <ctime>
using namespace std;

const int hashsize = 12;

//定一个节点的结构体
template <typename T, typename U>
struct HashNode 
{
    T _key;
    U _value;
};

//使用拉链法实现哈希表类
template <typename T, typename U>
class HashTable
{
public:
    HashTable() : vec(hashsize) {}//类中的容器需要通过构造函数来指定大小
    ~HashTable() {}
    bool insert_data(const T &key, const U &value);
    int hash(const T &key);
    bool hash_find(const T &key);
private:
    vector<list<HashNode<T, U>>> vec;//将节点存储到容器中
};

//哈希函数(除留取余)
template <typename T, typename U>
int HashTable<T, U>::hash(const T &key)
{
    return key % 13;
}

//哈希查找
template <typename T, typename U>
bool HashTable<T, U>::hash_find(const T &key)
{
    int index = hash(key);//计算哈希值
    for (auto it = vec[index].begin(); it != vec[index].end(); ++it)
    {
        if (key == it -> _key)//如果找到则打印其关联值
        {
            cout << it->_value << endl;//输出数据前应该确认是否包含相应类型
            return true;
        }
    }
    return false;
}

//插入数据
template <typename T, typename U>
bool HashTable<T, U>::insert_data(const T &key, const U &value)
{
    //初始化数据
    HashNode<T, U> node;
    node._key = key;
    node._value = value;
    for (int i = 0; i < hashsize; ++i)
    {
        if (i == hash(key))//如果溢出则把相应的键值添加进链表
        {
            vec[i].push_back(node);
            return true;
        }
    }
}

int main(int argc, char const *argv[])
{
    HashTable<int, int> ht;
    static default_random_engine e;
    static uniform_int_distribution<unsigned> u(0, 100);
    long long int a = 10000000;
    for (long long int i = 0; i < a; ++i)
        ht.insert_data(i, u(e));
    clock_t start_time = clock();
    ht.hash_find(114);
    clock_t end_time = clock();
    cout << "Running time is: " << static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000 <<
        "ms" << endl;//输出运行时间。
    system("pause");
    system("pause");
    return 0;
}

(3)unordered_map 与map的区别?使用场景?

构造函数:unordered_map 需要hash函数,等于函数;map只需要比较函数(小于函数).

存储结构:unordered_map 采用hash表存储,map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。

总体来说,unordered_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑unordered_map 。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,unordered_map 可能会让你陷入尴尬,特别是当你的unordered_map 对象特别多时,你就更无法控制了,而且unordered_map 的构造速度较慢。

(4)unordered_map、unordered_set的常用函数

unordered_map.begin()     返回指向容器起始位置的迭代器(iterator) 
unordered_map.end()       返回指向容器末尾位置的迭代器 
unordered_map.cbegin()     返回指向容器起始位置的常迭代器(const_iterator) 
unordered_map.cend()      返回指向容器末尾位置的常迭代器 
unordered_map.size()      返回有效元素个数 
unordered_map.insert(key)  插入元素 
unordered_map.find(key)   查找元素,返回迭代器
unordered_map.count(key)  返回匹配给定主键的元素的个数 

12. 迭代器的底层机制和失效的问题

1、迭代器的底层原理

迭代器是连接容器和算法的一种重要桥梁,通过迭代器可以在不了解容器内部原理的情况下遍历容器。它的底层实现包含两个重要的部分:萃取技术和模板偏特化。

萃取技术(traits)可以进行类型推导,根据不同类型可以执行不同的处理流程,比如容器是vector,那么traits必须推导出其迭代器类型为随机访问迭代器,而list则为双向迭代器。

例如STL算法库中的distance函数,distance函数接受两个迭代器参数,然后计算他们两者之间的距离。显然对于不同的迭代器计算效率差别很大。比如对于vector容器来说,由于内存是连续分配的,因此指针直接相减即可获得两者的距离;而list容器是链式表,内存一般都不是连续分配,因此只能通过一级一级调用next()或其他函数,每调用一次再判断迭代器是否相等来计算距离。vector迭代器计算distance的效率为O(1),而list则为O(n),n为距离的大小。

使用萃取技术(traits)进行类型推导的过程中会使用到模板偏特化。模板偏特化可以用来推导参数,如果我们自定义了多个类型,除非我们把这些自定义类型的特化版本写出来,否则我们只能判断他们是内置类型,并不能判断他们具体属于是个类型。

template <typename T>
struct TraitsHelper {
     static const bool isPointer = false;
};
template <typename T>
struct TraitsHelper<T*> {
     static const bool isPointer = true;
};

if (TraitsHelper<T>::isPointer)
     ...... // 可以得出当前类型int*为指针类型
else
     ...... // 可以得出当前类型int非指针类型
C++
2、一个理解traits的例子
// 需要在T为int类型时,Compute方法的参数为int,返回类型也为int,
// 当T为float时,Compute方法的参数为float,返回类型为int
template <typename T>
class Test {
public:
     TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
     T mData;
};

template <typename T>
struct TraitsHelper {
     typedef T ret_type;
     typedef T par_type;
};

// 模板偏特化,处理int类型
template <>
struct TraitsHelper<int> {
     typedef int ret_type;
     typedef int par_type;
};

// 模板偏特化,处理float类型
template <>
struct TraitsHelper<float> {
     typedef float ret_type;
     typedef int par_type;
};

当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits会是一种很好的解决方案。

2、迭代器的种类

输入迭代器:是只读迭代器,在每个被遍历的位置上只能读取一次。例如上面find函数参数就是输入迭代器。

输出迭代器:是只写迭代器,在每个被遍历的位置上只能被写一次。

前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。

双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。

随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + n、it – n、it += n、 it -= n、it1 – it2和it[n]等操作。

3、迭代器失效的问题

  • 插入操作

对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;

对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

  • 删除操作

对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;

对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

13. 为什么vector的插入操作可能会导致迭代器失效?

vector动态增加大小时,并不是在原空间后增加新的空间,而是以原大小的两倍在另外配置一片较大的新空间,然后将内容拷贝过来,并释放原来的空间。由于操作改变了空间,所以迭代器失效。

14. vector的reserve()和resize()方法之间有什么区别?

  • 首先,vector的容量capacity()是指在不分配更多内存的情况下可以保存的最多元素个数,而vector的大小size()是指实际包含的元素个数;

  • 其次,vector的reserve(n)方法只改变vector的容量,如果当前容量小于n,则重新分配内存空间,调整容量为n;如果当前容量大于等于n,则无操作;

  • 最后,vector的resize(n)方法改变vector的大小,如果当前容量小于n,则调整容量为n,同时将其全部元素填充为初始值;如果当前容量大于等于n,则不调整容量,只将其前n个元素填充为初始值。

15. 标准库中有哪些容器?分别有什么特点?

标准库中的容器主要分为三类:顺序容器、关联容器、容器适配器。

顺序容器包括五种类型:

  • array<T, N>数组:固定大小数组,支持快速随机访问,但不能插入或删除元素;

  • vector动态数组:支持快速随机访问,尾位插入和删除的速度很快;

  • deque双向队列:支持快速随机访问,首尾位置插入和删除的速度很快;(可以看作是vector的增强版,与- - vector相比,可以快速地在首位插入和删除元素)

  • list双向链表:只支持双向顺序访问,任何位置插入和删除的速度都很快;

  • forward_list单向链表:只支持单向顺序访问,任何位置插入和删除的速度都很快。

关联容器包含两种类型:

map容器:

  • map<K, T>关联数组:用于保存关键字-值对;

  • multimap<K, T>:关键字可重复出现的map;

  • unordered_map<K, T>:用哈希函数组织的map;

  • unordered_multimap<K, T>:关键词可重复出现的unordered_map

set容器:

  • set:只保存关键字;

  • multiset:关键字可重复出现的set;

  • unordered_set:用哈希函数组织的set;

  • unordered_multiset:关键词可重复出现的unordered_set;

容器适配器包含三种类型:

  • stack栈、queue队列、priority_queue优先队列。

16. 容器内部删除一个元素

1) 顺序容器(序列式容器,比如vector、deque)

erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;

It = c.erase(it);

2) 关联容器(关联式容器,比如map、set、multimap、multiset等)

erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;

c.erase(it++)

17. vector越界访问下标,map越界访问下标?vector删除元素时会不会释放空间?

  • 通过下标访问vector中的元素时会做边界检查,但该处的实现方式要看具体IDE,不同IDE的实现方式不一样,确保不可访问越界地址。

  • map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,就将一个具有该key和value的某人值插入这个map。

  • erase()函数,只能删除内容,不能改变容量大小;

erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。

18. map中[ ]与find的区别?

  • map的下标运算符[ ]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。

  • map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。

19. STL内存优化?

STL内存管理使用二级内存配置器。

(1) 第一级配置器:

第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。一级空间配置器分配的是大于128字节的空间,如果分配不成功,调用句柄释放一部分内存,如果还不能分配成功,抛出异常。

第一级配置器只是对malloc函数和free函数的简单封装,在allocate内调用malloc,在deallocate内调用free。同时第一级配置器的oom_malloc函数,用来处理malloc失败的情况。

(2) 第二级配置器:

第一级配置器直接调用malloc和free带来了几个问题:

  • 内存分配/释放的效率低

  • 当配置大量的小内存块时,会导致内存碎片比较严重

  • 配置内存时,需要额外的部分空间存储内存块信息,所以配置大量的小内存块时,还会导致额外内存负担 如果分配的区块小于128bytes,则以内存池管理,第二级配置器维护了一个自由链表数组,每次需要分配内存时,直接从相应的链表上取出一个内存节点就完成工作,效率很高

自由链表数组:自由链表数组其实就是个指针数组,数组中的每个指针元素指向一个链表的起始节点。数组大小为16,即维护了16个链表,链表的每个节点就是实际的内存块,相同链表上的内存块大小都相同,不同链表的内存块大小不同,从8一直到128。如下所示,obj为链表上的节点,free_list就是链表数组。

内存分配:allocate函数内先判断要分配的内存大小,若大于128字节,直接调用第一级配置器,否则根据要分配的内存大小从16个链表中选出一个链表,取出该链表的第一个节点。若相应的链表为空,则调用refill函数填充该链表。默认是取出20个数据块。

填充链表 refill:若allocate函数内要取出节点的链表为空,则会调用refill函数填充该链表。refill函数内会先调用chunk_alloc函数从内存池分配一大块内存,该内存大小默认为20个链表节点大小,当内存池的内存也不足时,返回的内存块节点数目会不足20个。接着refill的工作就是将这一大块内存分成20份相同大小的内存块,并将各内存块连接起来形成一个链表。

内存池:chunk_alloc函数内管理了一块内存池,当refill函数要填充链表时,就会调用chunk_alloc函数,从内存池取出相应的内存。

  • 在chunk_alloc函数内首先判断内存池大小是否足够填充一个有20个节点的链表,若内存池足够大,则直接返回20个内存节点大小的内存块给refill;

  • 若内存池大小无法满足20个内存节点的大小,但至少满足1个内存节点,则直接返回相应的内存节点大小的内存块给refill;

  • 若内存池连1个内存节点大小的内存块都无法提供,则chunk_alloc函数会将内存池中那一点点的内存大小分配给其他合适的链表,然后去调用malloc函数分配的内存大小为所需的两倍。若malloc成功,则返回相应的内存大小给refill;若malloc失败,会先搜寻其他链表的可用的内存块,添加到内存池,然后递归调用chunk_alloc函数来分配内存,若其他链表也无内存块可用,则只能调用第一级空间配置器。

20. 频繁对vector调用push_back()对性能的影响和原因?

在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程与实战

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值