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;
test(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量
7 简述#ifdef、#else、#endif和#ifndef的作用
- 利用#ifdef,#endif做一些特定模块的添加
- 利用#ifdef,#endif做调试输出调试信息
8 float、double与0比较
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON))
9 sizeof 和strlen 的区别
- sizeof是一个操作符,strlen是库函数。
- sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数。
- 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
- 数组做sizeof的参数不退化,传递给strlen就退化为指针了
10 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。
C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
11 volatile有什么作用
易变关键字,告诉编译器不要对这个变量进行优化,也就是说,每次都要去绝对地址上取值,而不能从寄存器上取值。
volatile 指出变量是随时可能发生变化的,所以说 volatile 可以保证对特殊地址的稳定访问
12 一个参数可以既是const又是volatile吗
可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。
13 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?
- 全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);
- 而局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。
- 操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。
14 对于一个频繁使用的短小函数,应该使用什么来实现?有什么优缺点?
应该使用inline内联函数,即编译器将inline内联函数内的代码替换到函数被调用的地方。
-
优点:
- 在内联函数被调用的地方进行代码展开,省去函数调用的时间,从而提高程序运行效率;
- 相比于宏函数,内联函数在代码展开时,编译器会进行语法安全检查或数据类型转换,使用更加安全;
-
缺点:
- 代码膨胀,产生更多的开销;
- 如果内联函数内代码块的执行时间比调用时间长得多,那么效率的提升并没有那么大;
- 如果修改内联函数,那么所有调用该函数的代码文件都需要重新编译;
内联声明只是建议,是否内联由编译器决定,所以实际并不可控。
15 什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?
智能指针是一个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,但不会增加对象的引用计数。
16 shared_ptr是如何实现的?
- 构造函数中计数初始化为1;
- 拷贝构造函数中计数值加1;
- 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;
- 析构函数中引用计数减1;
- 在赋值运算符和析构函数中,如果减1后为0,则调用delete释放对象。
17 右值引用及其作用
右值引用
右值引用的主要目的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷贝,也能够更加简洁明确地定义泛型函数
18 悬挂指针与野指针有什么区别?
- 悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针;
- 野指针:未初始化的指针被称为野指针。
19 指针和引用的区别
- 指针有自己的一块空间,而引用只是一个别名;
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 可以有const指针,但是没有const引用;(具体解释看评论区)
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
- 指针可以有多级指针(**p),而引用止于一级;
- 指针和引用使用++运算符的意义不一样;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
20 typedef 和define 有什么区别
- 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。
- 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
- 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。
- 对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
21 简述队列和栈的异同
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。
「注意」:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
22 结构体struct和类class的区别
struct默认访问权限为public,class默认访问权限为private
23 简述指针常量与常量指针的区别
- 指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
- 指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
/*指针常量*/
int a = 0;
int *const b = &a;
/*常量指针*/
const int *p;
int const *p;
24 如何避免“野指针”
- 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
- 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
- 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
- 使用智能指针
25 句柄和指针的区别和联系是什么?
句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念。
26 extern“C”
extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。
能更好的兼容以前的底层C库函数
- C++代码调用C语言代码
- 在C++的头文件中使用
- 在多个人协同开发时可能使用
27 C++的顶层const和底层const ?
- 顶层const:代表指针的值是一个常量,而指针的值(即对象的地址)的内容可以改变(指向的不可改变),也就是指针常量
- 底层const:代表对象本身是一个常量(不可改变),也就是常量(的)指针
28 拷贝初始化和直接初始化,初始化和赋值的区别?
- 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。所以当复制构造函数变为私有时,该语句不能编译通过。
要点就是拷贝初始化和直接初始化调用的构造函数是不一样的,但是当类进行复制时,类会自动生成一个临时的对象,然后再进行拷贝初始化。
29 智能指针详解
30 面向对象的三大特征是哪些?
- 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。
- 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行拓展;
- 多态:一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。
31 C++中类成员的访问权限
- public:公有,类内外都可以访问
- protected:保护,仅类内可访问
- private:私有,仅雷内可访问
32 多态的实现有哪几种?
- 静态多态:通过重载和模板技术实现的,在编译期间确定
- 动态多态:通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。
33 动态绑定是如何实现的?
当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
34 动态多态有什么作用?有哪些必要条件?
作用:
- 隐藏实现细节,使代码模块化,提高代码的可复用性;
- 接口重用,使派生类的功能可以被基类的指针/引用所调用,即向后兼容,提高代码的可扩充性和可维护性。
必要条件:
- 需要有继承;
- 需要有虚函数覆盖;
- 需要有基类指针/引用指向子类对象
35 纯虚函数有什么作用?如何实现?
定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。
实现方式是在虚函数声明的结尾加上= 0即可。
36 虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。
37 为什么基类的构造函数不能定义为虚函数?
虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
38 为什么基类的析构函数需要定义为虚函数?
为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据,所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而销毁派生类对象中的所有数据。
如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象。
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。
39 构造函数和析构函数能抛出异常吗?
- 从语法的角度来说,构造函数可以抛出异常,但从逻辑和风险控制的角度来说,尽量不要抛出异常,否则可能导致内存泄漏。
- 析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄露的问题;而且当异常发生时,C++通常会调用对象的析构函数来释放资源,如果此时析构函数也抛出异常,即前一个异常未处理又出现了新的异常,从而造成程序崩溃的问题。
40 如何让一个类不能实例化?
将类定义为抽象类(也就是存在纯虚函数)或者将构造函数声明为private。
41 多继承存在什么问题?如何消除多继承中的二义性?
- 增加程序的复杂度,使得程序的编写和维护比较困难,容易出错;
- 在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性;
消除同名二义性的方法:
利用作用域运算符::,用于限定派生类使用的是哪个基类的成员;
在派生类中定义同名成员,覆盖基类中的相关成员;
- 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类的成员时,将产生另一种不确定性,即路径二义性;
消除路径二义性的方法:
消除同名二义性的两种方法都可以;
使用虚继承,使得不同路径继承来的同名成员在内存中只有一份拷贝。
42 如果类A是一个空类,那么sizeof(A)的值为多少?
sizeof(A)的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。
43 重载和重写之间有什么区别?
-
范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
-
参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
-
virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
44 拷贝构造函数和赋值运算符重载之间有什么区别?
-
拷贝构造函数用于构造新的对象;
-
赋值运算符重载用于将源对象的内容拷贝到目标对象中,而且若源对象中包含未释放的内存需要先将其释放;
一般情况下,类中包含指针变量时需要重载拷贝构造函数、赋值运算符和析构函数。
45 对虚函数和多态的理解
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
46 C++的四种强制类型转换运算符
-
reinterpret_cast
reinterpret_cast< type-id > (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。 -
const_cast
const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下: -
static_cast
static_cast < type-id > (expression)
dynamic_cast 主要用于类层次间的上行转换或下行转换。在进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的,但在下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。
4、dynamic_cast
dynamic_cast<type>(expression)
该运算符用来修改 expression 的 const 或 volatile 属性。这里需要注意:expression 和 type 的类型一样的。
比如下面的代码,指针 px 由于有 const 修饰,无法直接通过其修改 x 的值,但又期望能修改 x 的值时,怎么办呢?这时就需要用到 const_cast。
综上,在使用强制类型转换时,需要首先考虑清楚使用目的,总结如下:
- static_cast:基本类型转换,低风险;
- dynamic_cast:类层次间的上行转换或下行转换,低风险;
- const_cast:去 const 属性,低风险;
- reinterpret_cast:转换不相关的类型,高风险。
47 简述类成员函数的重写、重载和隐藏的区别
(1)重写和重载主要有以下几点不同。
- 范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
- 参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一 定不同。
- virtual 的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被 virtual 修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。 - 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
- 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的函数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。
48 RTTI是什么?其原理是什么?
RTTI即运行时类型识别,其功能由两个运算符实现:
- typeid运算符,用于返回表达式的类型,可以通过基类的指针获取派生类的数据类型;
- dynamic_cast运算符,具有类型检查的功能,用于将基类的指针或引用安全地转换成派生类的指针或引用。
49 C++的空类有哪些成员函数
- 缺省构造函数
- 缺省拷贝构造函数
- 默认析构函数
- 赋值运算符
- 取址运算符
- 取址运算符 const
「注意」:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是 空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
50 模板函数和模板类的特例化
编写单一的模板,它能适应多种类型的需求,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化
51 为什么析构函数一般写成虚函数
因为如果子类有数据开辟在堆区,则必须使用虚析构函数,否则会出现重复释放数据的错误。
52 基类和派生类的构造和析构顺序
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
即 :父构造 -> 子构造-> 子析构 -> 父析构
53 深拷贝与浅拷贝区别,浅拷贝可能会带来的问题
-
浅拷贝:简单的赋值拷贝操作
-
深拷贝:在堆区重新申请空间,进行拷贝操作
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
54 当类中类成员时的构造和析构顺序
当类中成员是其他类对象时,我们称该成员为 对象成员
- 对象成员的构造
- 本类的构造
- 本类的析构
- 对象成员的析构
55 简述类的this指针,以及可能的使用场景
-
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。函数不占对象空间,所有函数共享一个函数实例
-
this指针概念
- c++通过提供特殊的对象指针,this指针。解决代码是如何区分那个对象调用自己的问题。this指针指向被调用的成员函数所属的对象,this指向的是对象。
-
this指针的用途
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
56 空指针访问成员函数
- 空指针,可以调用成员函数
- 如果成员函数中用到了this指针,不能调用成员函数
57 继承的简述
继承方式 | 基础语法 | 父类public权限 | 父类protected权限 | 父类privite权限 |
---|---|---|---|---|
公共继承 | class Son1:public Base1 |
可访问 public权限 | 可访问 protected权限 | 不可访问 |
保护继承 | class Son2:protected Base2 |
可访问 protected权限 | 可访问 protected权限 | 不可访问 |
私有继承 | class Son3:private Base3 |
可访问 private权限 | 可访问 private权限 | 不可访问 |
58 继承同名成员处理方式
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
59 菱形继承是什么,有什么危害,怎么避免
-
菱形继承概念:
当两个派生类继承同一个基类,又有某个类同时继承者两个派生类时这种继承被称为菱形继承,或者钻石继承 -
菱形继承产生问题:是共同的子类继承两份相同的数据,导致资源浪费以及毫无意义
-
解决方法:利用虚继承可以解决菱形继承问题
60 抽象类的定义和特点
virtual void func() = 0; //纯虚函数
当类中有了纯虚函数,这个类也称为抽象类,如上述代码中的Base类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
61 hash_map与map
hash_map(std::unordered_map)底层是哈希表,map(std::map,std::multimap)底层是红黑树
总体来说,hash_map 查找速度会比 map 快,而且查找速度基本和数据数据量大小无关,属于常数级别;而 map 的查找速度是 log(n) 级别。
并不一定常数就比 log(n) 小,hash 还有 hash 函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑 hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map 可能会让你陷入尴尬,特别是当你的 hash_map 对象特别多时,你就更无法控制了。而且 hash_map 的构造速度较慢。
现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用 。
62 std::vector 相关
- 底层原理:
vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。
当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。
-
vector中的reserve和resize的区别
- vector的reserve增加了vector的capacity,但是它的size没有改变!而resize改变了vector的capacity同时也增加了它的size
- reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。加入新的元素时,要调用push_back()/insert()函数。
- resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。此时再调用push_back()函数,是加在这个新的空间后面的。
- 两个函数的参数形式也有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。
-
vector中的size和capacity的区别
size表示当前vector中有多少个元素(finish – start),而capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage – start)。 -
vector的元素类型可以是引用吗?
vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。 -
vector迭代器失效的情况
当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。
-
正确释放vector的内存(clear(), swap(), shrink_to_fit())
- vec.clear():清空内容,但是不释放内存。
- vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
- vec.shrink_to_fit():请求容器降低其capacity和size匹配。
- vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
-
vector 扩容为什么要以1.5倍或者2倍扩容?
根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2倍的方式扩容,或者以1.5倍的方式扩容。
以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间: -
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) 删除元素
63 std::list相关
- list的底层原理
list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
list不支持随机存取,适合需要大量的插入和删除,而不关心随即存取的应用场景。
-
list与vector的区别
- list不支持随机存取;
- 在list的任何位置执行插入和移除都非常快.插入和删除动作不影响指向其它元素的指针,引用,迭代器,不会造成失效;
- list不支持随机存取,不提供下标操作符和at()函数;
- list没有提供容量,空间重新分配等操作函数,每个元素都有自己的内存;
- list也提供了特殊成员函数,专门用于移动元素
-
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) 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
64 std::deque相关
-
底层原理:
deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。 -
std::vector,std::list和std::deque的使用场景
vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
需要从首尾两端进行插入或删除操作的时候需要选择deque。 -
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。
65 std::map、std::set、std::multiset、multimap相关
-
底层原理
map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。红黑树的特性:
(1)每个结点或是红色或是黑色;
(2)根结点是黑色;
(3)每个叶结点是黑的(并且都为NULL);
(4)如果一个结点是红的,则它的两个儿子均是黑色;
(5)每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。-
为什么要红黑树?
普通的二叉搜索树在极端情况下可退化成链表,此时的增删查效率都会比较低下。 -
红黑树能保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍
对于STL里的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0统计的是key出现的次数,因此只能为0/1,而mp.find(key) != mp.end()则表示key存在。
-
-
特点
set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。
map和set的增删改查速度为都是logn,是比较高效的。 -
为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?
- 因为存储的是结点,不需要内存拷贝和内存移动。
- 因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。
-
为何map和set不能像vector一样有个reserve函数来预分配数据?
因为在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>
声明的时候从参数中传入的Alloc。
66 std::unordered_map、unordered_set
-
底层原理:
unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。 -
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 的构造速度较慢。
67 Vector如何释放空间?
利用C++11引入的shrink_to_fit()函数,会自动调整现有的vector的capacity的大小
68 插入map方式
// 用insert函数插入pair数据,最常用
mapStudent.insert(pair<int, string>(1, "student_one"));
// 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type (1, "student_one"));
// 在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, "student_one"));
mapStudent[1] = "student_one";
69 标准库中的容器主要分为三类:顺序容器、关联容器、容器适配器。
- 顺序容器包括五种类型:
- array<T, N>
- vector
- deque
- list
- forward_list
- 关联容器包含两种类型:
- map:map<K, T>、multimap<K, T>、unordered_map<K, T>、unordered_multimap<K, T>
- set:set
、multiset 、unordered_set 、unordered_multiset
- 容器适配器:底层用双向队列deque构成
- stack
- queue
- priority_queue
- stack
70 map中[ ]与find的区别?
-
map的下标运算符[ ]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
-
map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。
71 new/delete和malloc/free之间有什么关系?
int *p = new int[2];
int *q = (int *)malloc(2*sizeof(int));
- new与delete直接带具体类型的指针,malloc和free返回void类型的指针。
- new类型是安全的,而malloc不是。例如int *p = new float[2];就会报错;而int p = malloc(2sizeof(int))编译时编译器就无法指出错误来。
- new一般分为两步:new操作和构造。new操作对应与malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。
- new调用构造函数,malloc不能;delete调用析构函数,而free不能
- malloc/free需要库文件stdlib.h的支持,new/delete则不需要
注意:delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该讲该指针指向NULL。
72 delete与delete []有什么区别?
-
对于自定义类型来说,就需要对于单个对象使用delete,对于对象数组使用delete [],逐个调用数组中对象的析构函数,从而释放所有内存;
如果反过来使用,即对于单个对象使用delete [],对于对象数组使用delete,其行为是未定义的; -
所以,最恰当的方式就是如果用了new,就用delete;如果用了new [],就用delete []。
73 内存泄漏的场景有哪些?
- malloc和free未成对出现;new/new []和delete/delete []未成对出现;
- 在堆中创建对象分配内存,但未显式释放内存;比如,通过局部分配的内存,未在调用者函数体内释放;
- 在构造函数中动态分配内存,但未在析构函数中正确释放内存;
- 没有将基类的析构函数定义为虚函数。
- 未定义拷贝构造函数或未重载赋值运算符,从而造成两次释放相同内存的做法;比如,类中包含指针成员变量,在未定义拷贝构造函数或未重载赋值运算符的情况下,编译器会调用默认的拷贝构造函数或赋值运算符,以逐个成员拷贝的方式来复制指针成员变量,使得两个对象包含指向同一内存空间的指针,那么在释放第一个对象时,析构函数释放该指针指向的内存空间,在释放第二个对象时,析构函数就会释放同一内存空间,这样的行为是错误的;
74 堆和栈有什么区别?
- 分配和管理方式不同:
- 堆是动态分配的,其空间的分配和释放都由程序员控制;
- 栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成,比如局部变量的分配;动态分配由alloca()函数进行分配,但是会由编译器释放;
- 产生碎片不同:
- 对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,产生大量碎片,是程序效率降低;
- 对栈来说,不存在碎片问题,因为栈具有先进后出的特性;
- 生长方向不同:
- 堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长;
- 栈是向着内存地址减小的方向增长的,从内存的高地址向低地址方向增长;
- 申请大小限制不同:
- 栈顶和栈底是预设好的,大小固定;
- 堆是不连续的内存区域,其大小可以灵活调整
75 静态内存分配和动态内存分配有什么区别?
- 静态内存分配是在编译时期完成的,不占用CPU资源;动态内存分配是在运行时期完成的,分配和释放需要占用CPU资源;
- 静态内存分配是在栈上分配的;动态内存分配是在堆上分配的;
- 静态内存分配不需要指针或引用类型的支持;动态内存分配需要;
- 静态内存分配是按计划分配的,在编译前确定内存块的大小;动态内存分配是按需要分配的;
- 静态内存分配是把内存的控制权交给了编译器;动态内存分配是把内存的控制权给了程序员;
- 静态内存分配的运行效率比动态内存分配高,动态内存分配不当可能造成内存泄漏。
76 如何构造一个类,使得只能在堆上或只能在栈上分配内存?
- 只能在堆上分配内存:将析构函数声明为private;
- 只能在栈上生成对象:将new和delete重载为private。
77 为什么需要字节对齐?
主要是为了访存效率,因为对齐的字节访存效率更高。计算机底层存储硬件比如内存、CPU cache、寄存器等的访问都不是一次一个字节而是一次一批/一组
C/C++ struct通常的对齐原则如下:
- 保序:C/C++不允许对struct成员进行重排序,即成员的内存排列顺序一定是定义顺序。
- 在保序的基础上保证每个成员是自然对齐的,如果前一个成员紧接着的地址不满足对齐要求,则增加填充。
- 最后一个成员后面也可能会增加填充,这样能保证创建struct数组时,数组中的每个struct仍然是对齐的。
- 结构体嵌套时,递归的按照结构体中最大的那个成员对齐。(注意不是最大的结构体,而是结构体总最大的那个成员)
78 STL的对比总结
迭代器 | 迭代器失效 | 插入 | 删除 | 查找 | 场景 | |
---|---|---|---|---|---|---|
vector | 顺序、随机访问 | 插入和删除都会失效 | 尾端O(1)非尾端P:O(N-P) | 尾端O(1)非尾端P:O(N-P) | O(1) | 需要快速查找,不需要频繁插入/删除 |
string | 顺序、随机访问 | 插入失效,删除不会 | 尾端O(1)非尾端P:O(N-P) | 尾端O(1)非尾端P:O(N-P) | O(1) | 类似vector,但是string删除元素不会释放空间(为了下次操作方便) |
array | 顺序 | 固定长度 | 无 | 无 | O(1) | 类似vector,比数组更安全(不担心越界),但是内容在栈上,且属于定长容器 |
deque | 顺序、随机访问 | 插入失效。删除头和尾元素,指向被删除节点迭代器失效,而删除中间元素会使所有迭代器失效 | 首尾端:O(1)非首尾P:O(min(p, N-P) | 首尾端:O(1)非首尾P:O(min(p, N-P)) | O(1) | 头尾增删元素很快,随机访问比vector慢一点,因为内部处理堆跳转。中间插入和删除效率交较高。因为他是list和vector综合。使用较少 |
forward_list | 顺序、单向 | 插入不失效,被删除节点自身失效 | O(1) | O(1) | O(N) | 需要list的优势,但只要向前迭代 |
list | 顺序、双向 | 插入不失效,被删除节点自身失效 | O(1) | O(1) | O(N) | 需要频繁插入/删除,不需要快速查找 |
queue | 无 | 不支持迭代器 | 只能尾端入:O(1) | 只能首端删除:O(1) | 不支持 | FIFO(先进先出)底层容器可以是list或deque |
priority_queue | 无 | 不支持迭代器 | 只能尾端入:O(1) | 只能首端删除:O(1) | 不支持 | FIFO(先进先出)底层容器可以是vector或deque |
stack | 无 | 不支持迭代器 | 只能尾端入:O(1) | 只能尾端删除:O(1) | 不支持 | FILO(先进后出)底层容器可以是list或vector或deque |
set/multiset | 双向 | 插入不失效。删除时只是被删除节点的迭代器失效,但迭代器返回void,所以需要保存删除前迭代器位置。 | O(logN) | O(logN) | O(logN) | 需要元素有序,查找/删除/插入性能一样。红黑树效率都是O(logN)。即使是几个亿的内容,最多也查几十次 |
map/multimap | 双向 | 同上 | O(logN) | O(logN) | O(logN) | 需要key有序将值关联到key,查找/删除/插入性能一样 |
unordered_map /multimap | 单向 | 插入删除失效 | 平均情况:O(1)最差情况:O(N) | 平均情况:O(1)最差情况:O(N) | 平均情况:O(1)最差情况:O(N) | 内存使用比有序的高一些,但是查找速度更快。只有哈希函数太差或者发生哈希重建才会退化为O(N)。但是一般很少发生,均摊还是O(1) |
unordered_set /multiset | 单向 | 插入删除失效 | 平均情况:O(1)最差情况:O(N) | 平均情况:O(1)最差情况:O(N) | 平均情况:O(1)最差情况:O(N) | 内存使用比有序的高一下,但是查找速度更快。只有哈希函数太差或者发生哈希重建才会退化为O(N)。但是一般很少发生,均摊还是O(1) |
79 指针函数和函数指针区别
指针函数本质是一个函数,其返回值为指针。 函数指针本质是一个指针,其指向一个函数。
int *fun(int x,int y); //指针函数
int (*fun)(int x,int y); //函数指针
80 说说内联函数和函数的区别
-
内联函数比普通函数多了关键字inline
-
内联函数避免了函数调用的开销;普通函数有调用的开销
-
普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
-
内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
81 C++传值方式?区别?
值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
引用传递:形参在函数体内值发生变化,会影响实参的值;
指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
做函数形参时,最好不使用值传递,因为会拷贝一个副本,效率很低,使用引用传递和指针传递更高效,不发生拷贝行为,但是两者间引用传递更加安全。
82 讲一下内存分区
- 代码区:存放函数体的二进制代码,由操作系统进行管理;
- 全局区:存放全局变量和静态(全局、局部)变量和字符串常量;
- 栈区(stack):由编译器自动分配释放, 存放函数的参数值,局部变量等;
- 堆区(heap):由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
内存分区意义:不同区域存放不同的数据,赋予数据不同的生命周期, 更大限度的灵活编程。
83函数指针
- 定义:函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
- 应用场景:回调(callback)