**前言:**还有几个月就是秋招了,为了向大厂奋斗,比较全面地总结了C++相关的知识和面试题,后续还会持续更新。供自己学习的同时也希望能够帮助到志同道合的小伙伴,大家一起加油啊。如果有错误的地方或者有补充的地方,大家可以一起在评论区提出讨论。
C++部分
1.指针和引用的区别
- ①指针是一个变量,系统会为其分配地址空间,存放的是指向对象的地址。指针的空间大小不由类型决定,只和操作系统有关,32位操作系统下为4字节,64位操作系统下为8字节。而引用只是为已经存在的变量起一个别名,不会再开辟空间,使用的是和被引用对象相同的一块空间。
②指针可以有多级,引用只有一级 ③指针可以初始化为NULL,但是引用必须有明确的引用对象
④指针被初始化后,还可以改变指向,引用一旦被初始化,就不能改变其引用的对象 ⑤使用指针和引用对变量进行值的修改,都是有效的
⑥引用的底层实际上是通过指针来实现的,因此效率是一致的
2. 在函数参数传递的时候,什么时候使用指针,什么时候使用引用?
- ①需要在函数内部改变指针指向时,需要用到多级指针,此时只能传递指针变量才有效
②需要返回函数内部开辟空间的局部变量时,需要使用指针,但是需要注意的是,要使得局部开辟的空间在函数外有效,需要在堆上申请内存。并且用完后要记得释放内存,不然会造成内存泄漏。
③由于引用传递不会创建临时变量,不会再分配新的空间,开销更小,所以能使用引用的时候尽量使用引用。(比如递归这种对栈空间敏感的时候)
④类对象作为参数传入的时候使用引用,这是C++类对象传递的标准形式
3.堆和栈的区别
- ①栈是由编译器自动分配释放,存放函数的参数值、局部变量等
②堆一般是由程序员分配和释放,使用new和malloc开辟内存,使用free和delete释放内存。如果不释放,在程序运行期间一直占用着内存,造成内存泄漏。程序结束时可能由操作系统回收
③栈的生长空间向下,是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的,能从栈获得的空间较小,一旦栈空间不够,就会报异常提示栈溢出。
④堆是向高地址扩展的,是不连续的内存区域,堆的大小受限于计算机系统的有效虚拟内存空间,由此说明,堆获得的空间比较灵活,也比较大。
⑤堆空间因为会有频繁的分配释放操作,容易产生内存碎片
4. 堆快一点还是栈快一点?
- 栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
5. new和malloc的区别和联系
- ①new一个对象时,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数进行初始化。delete会调用对象的析构函数,然后调用free回收内存。
②使用malloc时,必须确定对象的长度,返回的是一个void指针,C++不允许将void赋值给其他任何指针,必须强转后才能进行赋值
③malloc和free属于库函数,new和delete属于运算符 ④malloc(free)不会调用构造函数和析构函数
⑤malloc可能分配失败,失败后返回NULL,因此需要通过检查返回值判断。而new失败后会抛出bad_alloc类型的异常
6. C和C++的区别和联系
- ①C++是C语言的加强,以C语言为基础的,并且完全兼容C语言的特性
②C++是面向过程的语言,C++是面向对象的的语言,有封装、继承、多态的特性。封装即将成员变量和方法封装在一起,隐藏实现细节,并对成员加以访问权限。继承即子类继承父类,实现了代码重用,减少代码和数据冗余。多态即一个接口多种形态,在子类重写父类的虚函数后,父类指针指向子类时,父类调用哪个函数会由指向的对象类型决定
③C++中有::作用域运算符和namespace命名空间 ④C++中的变量检测增强,不允许定义多个重名的全局变量
⑤C++中所有的函数和变量都必须具有类型。int fun() 和 int fun(void) 具有相同的意义,都表示返回值为 int
的无参函数。而C中int fun() 表示返回值为 int,接受任意参数的函数,int fun(void) 表示返回值为 int
的无参函数 ⑥C++中具有更严格的类型转换,不同类型的变量一般是不能直接赋值的,需要相应的强转,比如不能直接接收void*类型指针变量
⑦C++中struct 类型中可以存放函数,并且创建该类型变量时可以省去struct ⑧C++中才有bool数据类型
⑨C++对三目运算符的增强,C++中返回的是变量的引用,可以作为左值使用
⑩C++对const的增强,C中的局部const是一个伪常量,可以通过指针间接修改变量的值。C++中const变量初始时并不会分配地址空间,为了和C兼容,只有取地址时才分配空间,但是使用时是从符号表中取值,取地址并没有实际意义,即使通过指针间接修改地址空间的值,也无效
⑪C++中还有函数重载、引用和模板等概念
7. new(delete)和new的区别
- ①用new开辟内存,只会调用一次构造函数,初始化一个实例,new[]会调用多个构造函数,初始化多个实例
②delete只会调用一次析构函数,delete[]会调用每个成员的析构函数
③用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
8.C中Struct和C++中Struct的区别
- ①C++中可以有成员函数,和静态成员,C中不能 ②C++中可以对成员加以访问权限,C中不能,默认全部是public不可修改
③C++由于是面向对象的,支持继承,C中不能
9. C++中Struct和class的区别
- ①struct默认防控属性是public的,而class默认的防控属性是private的
②在继承关系上,struct默认是public的,而class是private ③class可以用作模板,而struct不能
10. #define 和const的联系与区别
- ① # define定义的常量没有类型,是在编译的预处理阶段对宏进行展开替换,可能会有多个拷贝,占用的内存空间大。
②const定义的常量是有类型的,是在编译阶段确定它的值,存放在静态存储区,只有一个拷贝,占用的内存空间小。 ③#
define不重视作用域,一旦宏被定义,它就在其后的编译过程有效,因此无法用来定义class中的专有常量,也不能提供任何封装性。
④define不会进行类型安全检查,而const会进行类型安全检查,安全性更高。 ⑤const可以定义函数而define不可以
11.尽量使用const, enum, inline替换#define的原因(或者说后者的缺点,前者的优点)
- ①#define是在预处理阶段就对变量进行了宏替换,因此编译器看不到变量名称,如果在编译过程中该变量出错,出错信息是提示宏值而不是变量名,因此很难找到错误的根源
②#define不重视作用域(上一条中已经阐述)
③使用#define定义宏函数时,即使宏中的所有实参都加上小括号,也可能会出现错误的结果。如下代码,如果a大于b,期望的结果是++a执行一次,但是宏展开后却执行了两次。
#define GetMax(a,b) (a)>(b)?(a):(b) GetMax(++a,b);
12. 在C++中const的用途
- ①const修饰类的成员变量时,表示常量不能被修改
②const修饰类的成员函数,表示常成员函数内不会修改类中的成员变量,不会调用其他非const的成员函数
③用const修饰一个对象时,常对象只能调用常成员函数,不能调用普通成员函数;且常对象不能修改成员属性
④对于以上中的②和③,如果依然想修改某些特殊成员变量,可以在成员变量声明时加上mutable
13. C++中的static用法和意义
- ①修饰变量时,静态变量是存放在静态存储区的,被声明后,就在之后的程序运行过程中一直存在。全局静态变量作用域为整个文件,局部静态变量作用域为函数体内
②修饰函数时,限定了函数的作用域仅为本文件,其他文件不能调用,也不会和其他文件产生同名冲突
③修饰类的成员变量时,静态成员变量属于整个类,所有对象共享,不占用对象的存储空间,即可以通过对象访问,也可以通过类名访问。静态成员变量必须在类中声明,在类外定义初始化
③修饰类的成员函数时,静态成员函数属于整个类所有,既可以通过对象访问,也可以通过类名访问。静态成员函数内如果要访问非静态成员,只能访问指定对象的非静态变量(对象名.变量)
14.对一个对象进行sizeof相关计算问题
- ①用空类实例化一个对象,sizeof的结果并不是0。这是因为每个实例在内存中都占据独一无二的地址,因此,在声明一个类型的实例时,必须在内存中占据一定的空间,否则无法使用该实例。至于占多少内存,右编译器决定,在VS下是空类的实例是占一个字节
②成员函数和成员变量是分开存储的,成员函数并不会占据类对象的空间 ③静态成员变量是存储在静态存储区的,也不会占用类对象的空间
④如果类中存在虚函数,C++编译器会为该类型生成一个虚函数表,并在该类型中的每一个实例中添加一个指向虚函数表的指针,这个指针是属于类对象的,占据类对象的存储空间。和其他类型指针一样,32位操作系统下占4字节空间,64位操作系统下占8字节空间
15.函数重载的条件
- ①同一作用域下,函数名相同,函数参数个数或者类型、或者顺序不同 ②有无引用可以作为重载条件
③const引用和非const引用可以作为重载条件 ④const和非const不可作为重载条件 ⑤函数返回值不同,不可作为重载条件
16.C++中重载和重写的区别
- ①重载是同一作用域下,函数名相同,函数参数不同
②重写是父类有虚函数,子类继承父类,并对父类中的虚函数进行重写。重写要求子类函数返回值、函数名和函数参数均要和父类虚函数保持一致,此时子类会用重写的函数覆盖从父类继承下来的那个虚函数
17.什么是动态性
- ①所谓多态,就是一个接口,多种实现方法,传入不同类型的数据对象,会调用不同的函数,分为静态多态和动态多态。
②函数重载就是静态多态,在编译阶段就绑定好函数入口地址。动态多态发生载继承中,在运行时才根据传输的对象类型决定调用哪个函数
18.动态多态的实现和使用
- ①父类中有虚函数,子类继承父类 ②子类对父类中的虚函数进行重写,此时子类会用重写的函数覆盖从父类继承下来的那个虚函数
③用父类指针指向子类对象后,父类指针调用虚函数时,会根据指向对象的类型,调用实际对象类型的那个函数
19.虚函数以及动态多态的实现原理
- ①用virtual声明为虚函数后,C++编译器会为该类型生成一个虚表,同时会为每个对象生成一个虚表指针。虚表指针是指向虚表的,虚表存放的是指向虚函数的指针。
②如果子类继承了有虚函数的父类,子类对象也会有自己的虚指针和虚表,虚表中的部分内容是从父类虚表中继承下来的,即存放的函数地址是父类虚函数的函数地址。如果子类重写了父类的虚函数,那么子类虚表中就会用重写后的函数地址覆盖从父类继承下来的虚函数地址。
③不管是父类指针还是子类指针,如果调用的是虚函数,那么编译器就不会在编译阶段就不会将指针类型和函数入口进行静态绑定,而是在运行时,先找到指向对象的虚表指针,通过子类的虚表指针在子类虚表中找到函数地址再调用
20.C++内存管理
- ①C++内存分为四区:栈区、堆区、全局区和代码区,其中全局区又分为静态区和常量区
②栈区:由编译器自动分配和释放,存放局部变量、函数参数值,函数运行完自动释放
③堆区:一般由程序员分配和释放,如果程序员不释放,程序运行结束时可能由操作系统回收
④静态区:存放全局变量和静态变量,初始化的放在一块区域,未初始化的放在一块区域,程序结束后系统自动释放
⑤常量区:存放字符串常量和其他常量 ⑥代码区:存放程序的二进制代码
21.为什么基类的析构函数一般写成虚函数
- ①首先析构函数也是可以写成虚函数的
②析构函数为虚函数时,如果父类指针指向子类对象,析构父类指针时,编译器可以根据虚函数表寻找到子类的析构函数,先调用子类的析构函数析构子类对象,再调用父类的析构函数
③如果基类的析构函数不是虚函数,则②中,析构父类指针时,不会调用子类的析构函数,如果子类对象在堆上开辟了内存,就无法释放,造成内存泄漏
22.什么是抽象类
- ①首先引入纯虚函数的概念:如果一个类不需要对函数进行实现时,可以将该函数写成纯虚函数,语法为virtual 函数体=0;如virtual int fun()=0;
②如果一个类含有纯虚函数,那么这个类无法实例化对象,这个类称之为抽象类
③抽象类的子类必须要重写父类中的纯虚函数,否则子类该函数也是纯虚函数,即子类也是一个抽象类,无法实例化对象
23.析构函数可以是纯虚函数吗
- ①析构函数也可以是纯虚函数,语法和普通虚函数语法相同,唯一不同的是纯虚析构函数需要有函数实现
②类内声明为纯虚析构函数,类外实现,类外实现时和正常析构函数的实现方式相同,不用加virtual
③如果一个类有纯虚析构函数,则这个类也是抽象类,无法实例化对象
24.为什么构造函数不定义为虚函数
- ①从作用角度看,虚函数的意义在于根据指针指向的对象选择调用哪个函数,但是构造函数是创建对象时自己主动调用的,一旦对象创建后就不会再调用,所以构造函数写成虚函数是没有意义的。并且构造函数调用前,对象还未创建,何谈根据对象类型选择调用哪个函数
②从编译器的角度看,编译器会给虚函数生成一个虚表,并且每个对象隐含一个虚表指针这个虚表指针是存储在对象的内存空间的。但是如果构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,也就没法通过虚表指针在虚表中找到函数的地址了
25. 构造函数和析构函数可以调用虚函数吗
- ①在C++中,提倡不在构造函数和析构函数中调用虚函数,
②因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编,调用的父类的函数实现方式
③销毁一个对象时,先调用子类的析构函数,再调用基类的析构函数。在调用基类的析构函数时,子类对象已经销毁,这个时候再调用子类的虚函数没有任何意义。
④构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本
26.浅拷贝和深拷贝
- ①浅拷贝是指如果对象中有指针成员,将一个对象拷贝给另一个对象时,指针成员之间的拷贝只是将指针变量的值进行拷贝,也就是拷贝后两个对象的指针成员变量指向的是同一块地址空间
②使用浅拷贝,如果其中一个对象的指针成员修改了指向空间值,那么另一个对象指针成员指向空间的值也会改变,引起写冲突。同时两个对象销毁时都会各自调用自己的析构函数释放内存,造成堆区内存的重复释放
③解决方法是使用深拷贝,拷贝时先为指针变量在堆中申请内存空间,再将指针指向空间内容拷贝到新申请的内存空间,而不是拷贝指针的指向(地址)
27.编译器默认为一个类提供的函数有哪些
- ①默认构造函数、拷贝构造函数、赋值运算符重载函数、析构函数
28.什么时侯会调用拷贝构造函数
- ①用已经创建好的对象来初始化新的对象时
- ②对象作为函数参数时
- ③函数返回值是一个对象时
29.结构体内存对齐的原因和方式
①由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来,提高读取效率
②第一个变量的起始地址为0,之后每个变量的起始地址是当前类型大小和对齐数两者较小值的整数倍。最后所有变量内存大小必须是最大变量类型大小和对齐数两者较小值的整数倍
③启示:为了节省内存空间,在声明结构体类型时,把内存空间较小的变量定义在前面
30.什么是内存泄漏,应该如何避免
①内存泄漏是指在堆上开辟的内存用完后没有手动释放,在程序运行期间就会一直占用内存
②new和delete配套使用,new[]和delete[]配套使用,malloc和free配套使用
③如果使用父类指针指向子类对象,那么父类的析构函数应该定义为虚函数
④可以使用智能指针,内部自动释放内存
31.C++智能指针
-
①智能指针其实是将指针进行了封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存地址造成内存泄漏。C++中的智能指针有auto_ptr,
shared_ptr, weak_ptr和unique_ptr②auto_ptr是较早版本的智能指针,在进行指针拷贝和赋值的时候,新指针直接接管旧指针的资源并且将旧指针指向空,但是这种方式在需要访问旧指针的时候,就会出现问题
③unique_ptr是auto_ptr的一个改良版,不能赋值也不能拷贝,保证一个对象同一时间只有一个智能指针④shared_ptr可以使得一个对象可以有多个智能指针,当这个对象所有的智能指针被销毁时就会自动进行回收。(内部使用计数机制进行维护)
⑤weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁
32.什么是野指针,应该如何避免
- ①野指针可以理解为访问了一个无效的指针,即指针的指向空间是无效的,比如指针越界访问,指针定义时未初始化,释放内存后没有将指针指向空
②指针定义时应该初始化,或者指向NULL ③保证指针的合法访问,不要越界 ④释放指针指向空间后,不要忘记将其指向NULL
33. inline关键字,和宏函数的区别
- ①C++可以通过inline请求将函数声明为内联函数,但是需要注意的是这只是一种请求,编译器不一定允许这种请求
②对于内联函数,C++编译器是直接将函数体插入到函数调用的地方,从而省去了普通函数调用时的额外开销。但是会额外占用一些代码存储空间
③宏函数的在预处理时进行宏替换的,而内联函数是编译时将代码直接插入到函数调用的地方
④为了避免二义性,宏函数的参数需要用括号括起来,但是某些情况下,即使使用了括号,还是会出错。但是内联函数不会出现二义性和错误
⑤尽量使用inline替代宏函数
34.模板的用法和实现原理
-
①用template< typename T >声明,告诉编译器紧跟着的函数或者类中出现的T表示一种数据类型,不会报错
②编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,这次编译只会进行语法检查,并不会生成具体的代码。在调用的地方对参数替换后的代码进行编译,生成具体的函数代码
35.成员初始化列表和构造函数相关知识
-
①初始化不是赋值,初始化的含义是在创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值代替
②成员初始化列表是对成员变量进行初始化操作,它是先于构造函数执行的③进入构造函数体内后,进行的是计算和赋值操作,而不是初始化操作,赋值操作和初始化操作是不同的。如果成员没有被初始化,进入构造函数后,会先调用一次隐式的默认构造函数初始化数据成员,然后才进行赋值操作。因此使用成员初始化列表效率更高
④常量成员和引用必须使用成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的
⑤没有默认构造函数的对象必须使用成员初始化列表进行初始化。因为未初始化的成员在构造函数内会隐式调用默认构造函数,但是如果没有默认构造函数就没法调用
36.C++11新特性(列举部分,C++ Primer目录部分附有详细的C++1新特性)
- ①long long 类型是C++11中新定义的 ②支持列表初始化,用{}进行初始化操作:int x={0};或者int x{0};
③可以用字面值nullptr来初始化指针,它是一种特殊的字面值,可以被转换为任意其它类型的指针
④auto自动类型推导:可以根据初始值的类型来推导变量的类型
⑤可以为数据成员提供一个类内的初始值,创建对象时,类内初始值将用于初始化数据成员
⑥可以使用范围for语句对给定序列中的元素进行遍历for(auto &c : arry) ⑦lambda表达式
37. extern用法
- ①由于c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。用extern “C”可以告诉编译器该函数使用C语言方式链接: extern “C” void show(); //告诉编译器show函数是用C语言方式链接 ②可以用extern 声明引用其它文件中的变量
③同样,也可以用extern 声明引用其它文件中的函数
38. C++函数调用的压栈过程
- 以函数f1()内调用函数f2()为例: ①先将f1的运行状态压栈,然后将f2的返回地址、f2函数参数、f2的变量依次压栈
②f2运行完后,f2的所有压栈都会被编译器弹出释放,再从栈中接收到f1的函数运行状态,衔接调用f2之前的操作,继续执行f1
③f1中不能使用f2中栈上分配的内存,因为f2结束后,栈上的内存都释放了,但是可以使用堆上开辟的内存
39. C++的四种类型转换
- ①静态转换 语法:static_cast<目标类型>对象 特点:不会进行安全检查,当父类指针转换为子类指针时,会存在安全问题
使用场景:允许允许内置数据类型之间的转换;允许父子之间引用和指针的转换;如果两个类之间没有父子关系,则转换无效 ②动态类型转换
语法:dynamic_cast<目标类型>对象 特点:会进行安全性检查,如果不安全,则会报错
使用场景:多态下,父子之间引用和指针可以相互转换(因为总是安全的);不允许内置类型之间的转换(因为可能丢失数据,不安全);非多态下,不允许父类指针或者引用转换为子类指针或者引用(不安全);不允许无父子关系之间的两个类之间的转换
③常量转换 语法:const_cast<原对象类型>对象
特点:用于去除const常量属性,<>中是原对象类型而不是目标对象类型,是因为const_cast只是改变常量属性,并不能改变类型
注意点:在C++中,const常量使用的是符号表中的值,所以使用const_cast改变了对象的常量属性,也仅仅是可以通过非常量指针指向它不报错,即使通过指针修改了值,也只是地址空间的值变了,但是常量并没有使用地址空间的值。所以对常量本身取值并不会改变
使用场景:只允许指针或者引用之间的转换 ④重新解释转换 语法:reinterpret_cast<目标类型>对象
特点:即使是毫不相关的两个数据类型之间也可以转换,所以它是最不安全的一种转换,不推荐使用
40. string的底层实现
- ①string继承自basic_string,其实是对char进行了封装,封装的string包含了char数组,容量,长度等等属性
②string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容
41.可执行文件的生成过程或者编译过程是怎样的
- ①流程是预处理—>编译—>汇编—>链接 ②预处理:主要编译器对各种预处理命令进行处理,包括头文件的包含、宏定义的展开、条件编译的选择等
③编译:将源代码翻译成汇编语言 ④汇编:将汇编语言转为机器语言,即二进制代码,生成目标文件
⑤将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体
42. set,map和vector的插入复杂度
- ①set,map的插入复杂度就是红黑树的插入复杂度,是log(N)
②unordered_set,unordered_map的插入复杂度是常数,最坏是O(N)
③vector的插入复杂度是O(N),最坏的情况下(从头插入)就要对所有其他元素进行移动,或者扩容重新拷贝
43.声明和定义的区别
- ①声明是告诉编译器变量的类型和名字,不会为变量分配空间
②定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
44. typdef和define区别
- ①#define是预处理命令,在预处理时执行简单的替换,不做正确性的检查
②typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型起一个别名
45. 被free回收的内存是立即返还给操作系统吗?为什么
- ①不会立即返还给操作系统。被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
②ptmalloc可以简单理解为一种管理内存的方式(内存池的管理方式)
46. 引用作为函数参数以及返回值的好处和使用时需要注意的事项
- ①引用作为函数参数,可以在函数内部修改此参数,函数外仍有效 ②引用传入只是起一个别名,不会有空间消耗,开销小,可以提高效率
③函数返回值如果是引用,可以作为左值进行修改 ④不能返回局部变量的引用
⑤不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成内存泄漏
47.友元函数
- ①如果在一个函数想要访问类的私有成员,则需要在类中声明该函数为此类的友元函数
②友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数
③一个函数可以是多个类的友元函数,只需要在各个类中分别声明
48.友元类
- ①A类想访问B类中的私有成员时,需要在B类中声明A类是B类的友元类
- ②声明友元类时,友元类必须是一个已经存在的类
③友元类不具有传递性,也不具有双向性(交换性)
49. volatile关键字的作用
- ①用volatile关键字修饰变量后,编译器就不会对这个变量进行优化(CPU的优化是让该变量存放到CPU寄存器而不是内存),进而提供稳定的访问。每次读取volatile的变量时,系统总是会从内存中读取这个变量,并且将它的值立刻保存
50.为什么C++没有实现垃圾回收?
- ①实现一个垃圾回收器会带来额外的时间和空间开销
②垃圾回收所带来的系统开销,违反了C++的设计哲学,“不为不必要的功能支付代价”,不符合C++高效的特性,使得不适合做底层工作
③不同的应用环境,也许需要不同的垃圾回收器,不管三七二十一使用垃圾回收,需要将这些不同类型的垃圾回收器整合在一起,即使可以成功(对此我感到怀疑),也会导致效率成本的增加。设计一个合适自己需要的垃圾回收器,正是对喜爱C++的程序员的一种挑战
51. explicit关键字
- ①C++中的explicit关键字只能用于修饰只有一个参数的类构造函数,或者出了第一个参数以外其它参数都有默认值的时候(此时调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数)
②作用是表明该构造函数是显示的,而非隐式的,防止类构造函数的隐式自动转换来构造对象。
比如person p1=p2+2,由于加号右边的2不是person对象,所以可能调用一个参数的构造函数,将2作为构造函数的参数构造一个对象进行运算
52.继承中和类中成员变量有其它对象时,构造函数和析构函数的调用
- ①先调用父类构造函数,再调用子类成员变量中其它对象的构造函数,最后调用子类自身的构造函数
②析构函数的调用顺序和构造函数相反
③子类可以使用初始化列表形式显示调用父类中指定的构造函数 ④子类并没有继承父类的所有构造、析构和赋值运算符重载函数
53.虚继承
- ①子类继承父类时,如果继承方式是虚继承,则继承的是一个虚表指针,即使发生菱形继承时,最低端的子类继承了两个父类,也不会继承重复的数据,因为继承的是虚表指针,通过虚表指针在虚表中获取数据地址去访问成员,而虚表中的地址最终指向的都是来自祖先的同一份数据
54.函数指针
- ①函数指针本身是一个指针变量,该指针变量指向的是一个具体的函数,也即存放的是某个函数的入口地址
②编译时,每个函数都有一个入口地址,函数指针指向的就是这个入口地址,有了函数指针后,可以通过函数指针调用函数
③作用是调用函数,因为它是一个变量,所以可以作为函数参数回调函数
55.C语言函数参数压栈的顺序
从右到左
56.C++中的拷贝构造函数传入参数是使用的什么形式,为什么?
- ①拷贝构造函数传入参数必须写成引用,因为如果不写成引用,在函数参数传入时又会调用拷贝构造函数,如此会陷入死循环,最终栈溢出
57.C++右值引用
- ①右值引用是C++11引入的一个新特性,所谓的右值引用就是必须绑定到右值的引用,用&&而不是&
②消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
③能够更简洁明确地定义泛型函数
58. include头文件双引号””和尖括号<>的区别
- ①使用“”时是先在当前目录下寻找,如果找不到再去编译器设置的头文件路径查找,如果还是找不到,再去系统类库目录里查找
②使用<>时是先去编译器设置的头文件路径查找,如果找不到,再去系统类库目录里查找,如果还是找不到,就报错
59.什么是组合
- ①组合即一个类的数据成员是其它类对象
②创建组合类对象时,不仅要初始化自身的基本类型成员,还要使用初始化成员列表形式调用内嵌对象的构造函数初始化内嵌对象的成员。
③如果有多个内嵌对象,内嵌对象构造函数的调用顺序和初始化列表中的顺序无关,至于定义时的现后顺序有关析构时的顺序和构造时的顺序相反
60.stl介绍
- ①从广义上分为容器(container)、算法(algorithm)
、迭代器(iterator),又可以细分为容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
②容器是用来存放各种数据的结构,容器和算法之间通过迭代器进行无缝连接
③迭代器是一种智能指针,从实现的角度来看,就是将operator* , operator-> ,
operator++,operator–等指针相关操作予以重载,封装成一个class
template,所有STL容器都附带有自己专属的迭代器,不暴露容器内部结构,只有容器的设计者才知道如何遍历自己的元素。
④仿函数从实现角度来看,是一种重载了operator()的class 或者class template
⑤适配器:来修饰容器或者仿函数或迭代器接口的,进行接口适配
⑥空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template
61.什么是哈希冲突,有哪些解决方法
- ①哈希表是存储的是关键值key和key对应的地址单元,地址单元称之为哈希值或者哈希地址,哈希值是根据key值的映射f(key)得到的,每个key对应唯一的哈希值,如果存储key时发现哈希值f(key)已经被使用,就是哈希冲突
②线性探测发:如果哈希冲突,就顺序一一往后查找下一个单元,直到查找到能够使用的哈希值,表达式为(f(key)+i)%m,(i=1,2,…,m-1)
③二次探测法:如果哈希冲突,就顺序探测后面平方次位置的单元,直到查找到能够使用的哈希值,表达式为(f(key)+i)%m,(i=12,22,…,k2)
k2<=m ④双散列函数法:也即使用两个散列函数计算哈希值,当第一个散列函数发生冲突的时候,使用第二个散列函数就算哈希值
⑤拉链法(链地址法):每个哈希值维护一个链表,将哈希值相同的key用同一个链表进行保存,不在继续寻找其它哈希值存放
⑥建立公共溢出区:将哈希表分为基本表和溢出表两部分,将所有冲突的数据放在公共溢出区
62. STL源码中的hash表的实现
- ①STL中的哈希表是unordered_map,底层是基于hashtable实现的
②采用拉链法来解决哈希冲突,当链表的大小超过8时,就自动转为红黑树进行组织
63. STL中unordered_map和map的区别
- ①unordered_map的底层实现是哈希表,占用的内存较多,查询速度是常数时间复杂度,其内部是序的
②map底层实现是红黑树,插入删除查询操作时间复杂度都是O(log(n)),其内部是根据key值进行排序的,可以重载<运算符改变排序规则
64. STL中vector的实现
- ①其内部维护的是一个动态的连续线性空间,当容量不够时,会扩充容量至原来的两倍
②扩容并不是在原空间后接续新空间,而是另外寻找一块是原空间2倍的新空间,将元素一一移动到新空间。然后释放原空间
③一旦引起空间重新配置(扩容),原vector的所有迭代器就都失效了(特别注意这一点)
④频繁插入时,最好先指定其容量大小,否则频繁的扩容消耗很大. ⑤vector::iterator重载了++,
⑥插入元素时,最坏情况下从头部插入,则需要移动后面的所有元素,复杂度为O(n)
65.STL中的list
- ①list是由双向链表实现的,其内存空间不是连续的
- ②由于是链表时间,所以没有浪费的内存空间,且其插入和删除时间复杂度都是常数时间复杂度
③只能通过指针查找元素,不能随机访问,访问时间复杂度为O(n) ④list::iterator重载了++
66. 什么情况下会vector迭代器失效或者改变?
- ①空间重新配置时,所有迭代器失效
- ②删除元素时,原先的end迭代器失效
③插入元素与者删除元素时,原先定义的插入位置与者删除位置处及其后面位置的迭代器都发生了改变
67.STL中的map和set
- ①其底层实现都是红黑树,因此插入删除等操作都是O(logn)时间复杂度
②所有元素都会根据键值自动排序,(红黑树能够实现这一功能,而且时间复杂度比较低)
③set中存放的元素只有一个值,key就是value,value就是key,不允许有两个相同的元素
④map中存放的是key-value键值对,不允许两个value有相同的key值
68.智能指针相关(后续补上)
69.空间配置相关(后续补上)
持续更新中,敬请期待....................................