C++面试题
本文主要记录之前曾经见过的面试问题,做一个简单的汇总,便于复习。
C++常见面试问题汇总
- 非static变量和static变量
- 全局变量
- 非static全局变量表示这个变量存在于这个程序执行的整个生命周期,同时可以被本源码文件以外的其他文件访问到。
- static全局变量表示这个变量存在于程序执行的整个生命周期,但是只能被本源码文件的函数访问到
- 局部变量
- 非static局部变量表示,这个变量旨在本变量所执行的函数的执行上下文中存在
- static局部变量存在于整个程序的生命周期中,但是作用域被局限在定义这个变量的代码块中
- 全局函数
- 非static的函数定义表明这是一个全局函数,可以被源码文件以外的其他文件访问到。
- static函数限制了本函数只能被本源码文件的函数调用
- 类成员函数
- static成员函数代表整个类所有实例共有的函数
- static成员函数:
- 不能使用非static变量/函数(包括this)
- 不能是虚函数
- 必须在类定义的时候进行定义
- 类成员变量
- static成员变量代表整个类共有的变量
- 必须在类定义的时候进行定义
- 全局变量
-
指针和引用的区别
- 指针是一个变量,存储的是一个地址,指向一个内存的存储单元;而引用和原来的变量实质上是一个东西,只不过是原变量的一个别名而已,在内存上占着同一个存储单元。
- 对于指针和引用,都有底层const,但是指针有顶层const而引用没有。
- 指针可以有多级,但是引用只有一级。
- 指针的值可以为空,但引用的值不能为NULL;在类成员内部可以声明未初始化的引用变量,但是在构造函数中必须采用列表初始化的方式进行初始化。
- 指针指向的对象在初始化后可以改变,但是引用的对象在初始化之后就不会再改变了。
- sizeof引用 得出的是引用变量的大小,而 sizeof指针 得到的是指针本身的大小。
-
指针和引用的自增运算意义不一样。
- 从编译的角度看:编译时指针和引用都被添加到符号表上,符号表中记录的是变量名以及变量对应的地址。
- 指针变量记录的就是指针变量的地址,当我们试图更改指针指向对象时,实际上是更改的指针变量本身的值,因此是可以更改的。
- 引用变量记录的是引用对象的地址,如果我们试图去更改引用对象,实际上就是试图在运行期去更改编译期确定下来的符号表,自然是不可能的。因此引用不能重新更改对象。
传指针和传引用
-
指针传递本质是值传递,传的是指针本身的值
传引用实际上是传实参变量的地址,被调函数对形参的任何操作都被处理成间接寻址。即通过栈中存放的地址访问主调函数中的实参变量。
-
堆和栈的区别
- 管理方式不同:栈是通过编译器自动管理,无需程序员手动控制。而堆的申请和释放都是需要程序员手动管理的。
- 空间大小不同:栈的大小一般在1MB左右,我们也可以通过手动进行调整。在32位系统下,堆的理论大小可以达到4G。
- 能否产生碎片:栈是不会产生碎片的,因为栈是先进后出,和程序运行的顺序是一一对应的,在一个内存块弹出之前,其上面的栈内容已经被弹出。而对于堆,频繁的new/delete会造成内存空间的不连续,尤其当我们采取简单的内存管理算法的时候,势必会造成程序效率降低。
- 生长方向不同:堆是由低到高,栈是由高到低。
- 分配方式不同:堆是动态分配的。栈可以是静态分配也可以是动态分配的。
- 分配效率:栈的分配有计算机底层的支持:有专门的寄存器存放栈的地址,压栈和出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,其运算包括,搜索堆内存中可用空间,如果无可用空间需要进行碎片收集,然后进行返回。显然,堆的效率要比栈低。
-
new和delete是如何实现的,new 与 malloc的异同处
- new 和delete的实现
- new:调用operator new函数申请空间,并在申请的空间上执行构造函数
- delete:在空间上执行析构函数,完成对象中资源的清理工作。
- operator new 和 operator delete
- 实际上也是通过malloc和free进行申请和释放内存空间的,只是当失败的时候会抛出错误。
- new 和 malloc的区别
- 申请内存位置
- 返回类型安全性
- new直接返回对应类型指针,而malloc需要通过强转
- new 和delete的实现
-
C和C++的区别
-
C++、Java、C#的联系与区别,包括语言特性、垃圾回收、应用场景等(java的垃圾回收机制)
-
Struct和class的区别
- 默认的继承访问权:class默认private,struct默认public
- 默认访问权限:class默认private,struct默认public
- class这个关键字还用于定义模板参数,而struct不用于此
- class和struct在使用大括号初始化的区别
- 当class没有定义构造函数,且所有成员变量全是public的情况下可以用花括号初始化。
- 当struct没有定义构造函数,可以用花括号初始化
-
define 和const的区别(编译阶段、安全性、内存占用等)
- define是在编译时期就起作用的,而const则是在运行期才起作用的。
- 安全性:const常量有数据类型,有安全性检查,但是define仅仅是简单的字符替换,没有类型,也没有类型安全检查,在字符替换的过程中因为忽视优先级,还会产生意想不到的结果。
- define宏仅仅是展开,占用的是代码段空间,而const变量则是占用数据段空间。
- define可以通过undef来重新定义
- const变量可以进行调试,而define不可以进行调试
-
在C++中const和static的用法(定义,用途)
const
-
const对象一经赋值就不能再改变
- const对象必须初始化;可以是编译时初始化(赋值一个constexpr),或者是运行时初始化。(特例:用const修饰类内成员变量时,可以到构造函数的时候再进行初始化)
- 默认状态下,const对象仅在文件内有效,如果想在多个文件中共享const对象,必须在变量的定义之前添加extern关键字。
- 引用:
- 对const修饰变量的引用,必须也采用const进行修饰,不能用没有const修饰的引用去引用const变量(这样可能会导致const变量被修改)。
- 允许对变量进行常量引用。不允许通过常量引用对原变量进行修改,但允许通过其他方式进行修改。
- 指针:
- 指向常量的指针;和常量引用类似,指向常量的指针不要求指向的必须是一个常量。但我们不能通过该指针修改该变量。
- const指针:将指针本身定为常量,不允许指针指向其他地方。
- 顶层const(指针本身是个常量
int * const p
) - 底层const(指针指向的是一个常量
const int * p
)
static
static的整体目的
如果我们定义一个变量,并且在退出当前作用域之后,还想保留这个变量,并在下次进入这个作用域时访问它,我们应该如何实现?
简单的方法就是在当前作用域上次定义一个变量,比如全局作用域之于局部作用域,比如所有文件可见作用域之于当前文件作用域。但这样做虽然实现了目的,但破坏了这个变量的访问范围。我们需要做的是,让这个变量的生命周期延续的同时,将这个变量的作用域限制下来,因此我们发明了static关键字。
同时,对于类的实现,我们也可以利用static关键字保护类的封装性。这个数据对象将为整个类而不是某个对象服务。
静态数据的存储
全局存储区分为DATA段和BSS(Block Started by Symbol)段,DATA段存放已初始化的全局变量和静态变量,而BSS段存放未初始化的仅有符号的全局变量和静态变量。
BSS段会在程序执行之前被清零,因此所有未初始化的全局变量和静态变量在程序运行前已经为0。存储在静态数据区的变量会在程序刚开始的时候就完成初始化。
静态全局变量
特点:
- 该变量在全局数据区分配内存
- 如果不显式初始化,那么将被隐式初始化为0
静态局部变量
特点:
- 该变量在全局数据区分配内存
- 如果不显式初始化,那么将被隐式初始化为0
- 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块快结束时,其作用域随之结束。
static函数
当一个源程序由多个源文件组成时,根据函数能否被其他源文件函数调用,分为内部函数和外部函数。
-
内部函数
只能在源文件内部被调用的函数。前面有static关键字修饰。
-
外部函数
在定义函数的时候,没有加static,或者加上了extern,表示此函数是外部函数。
调用外部函数时,需要对其进行声明。
extern T func_name(U input1, ...);
-
-
const和static在类中使用的注意事项(定义、初始化和使用)
const
const成员函数
const成员函数不会修改调用它的对象的内容(指针除外)。
const成员函数可以被非const成员函数重载。
mutable关键字:如果将一个数据成员用mutable进行修饰,那么任何成员函数,无论是否是const函数,都可以对该数据成员进行修改。
const类对象
非const类对象既可以访问const成员函数也可以访问非const成员函数,const类对象只能访问const成员函数。
定义:const成员函数必须通过类构造函数对所有成员数据进行初始化。
const类成员数据
const类成员数据无法在声明时直接初始化,而只能在之后的构造函数中通过列表初始化的方式进行初始化。
const类成员对于某个实例是常量,但是对于整个类来说确实可变的(由构造函数决定)。
class A{ static int aa; // Declaration of one static member variable static const int count; // Constant static variable const int bb; // Constant variable public: A(int a); }; int A::aa = 0;// Definition + initialization of one static member variable const int A::count = 20; // Defn + init of one const static member variable A::A(int a):bb(a) // Init of one constant member variable { }
-
C++中的const类成员函数(用法和意义),以及和非const成员函数的区别
-
C++的顶层const和底层const
- 直接限制变量本身的是顶层const
- 限制的是变量所指向的变量内容的是底层const
const int i = 2; // 顶层const const int * pi = &i; //底层const,因为限制的是pi指向的变量内容,但不限制pi指针本身 int * const pi2 = &i; //顶层const,因为限制的是pi本身,而不是pi所指向的内容
- 其他区别主要体现在指针的拷贝上
- 顶层const基本不受影响
- 但是底层const会要求,要求拷入对象也必须是const
-
final和override关键字
- override明确地表示一个函数是对基类中一个虚函数的重载。更重要的是,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配的话,编译器会发出错误信息。
- final关键字有两个用途:一、阻止从类继承。二、阻止一个虚函数的重载。
-
拷贝初始化和直接初始化,初始化和赋值的区别
- 如果用“=”去初始化,实际上进行的是拷贝初始化。编译器把等号右侧的初始值拷贝到新创建的对象中。
- 与之相反,如果不使用等号,则执行的是直接初始化。
-
extern “C”的用法
-
告诉编译器,之后代码块内部的代码不进行名字重整。
-
名字重整(function name mangling):在C++中,为了支持函数重载,将编译之后的函数名做了重整,比如函数
int add(int a, int b)
,在C中编译完的函数名就是add
,而在C++中编译完之后的函数名就可能是add_int_int
(因编译器而异),在函数类型之后加上参数类型,就可以区分不同的重载函数了,比如add_float_float
等等。 -
但是问题也随之而来,因为这样的区别,当我们需要在C++程序中引用来自C的函数时,它会按照声明进行函数名字重整,然后按照重整之后的函数名去目标文件中寻找对应的函数,然而目标文件中存放的确实C版本的,没有经过重整的函数。因此我们无法正常引用该函数。
-
这就是
extern "C"
存在的一个原因了。它告诉C++编译器,之后代码块中的内容是C版本的,你不要进行名字重整。 -
如下例
#define _cplusplus #ifdef _cplusplus extern "C"{ #endif #include "CClass.h" // One header file implemented in C #ifdef _cplusplus } #endif ...
- 另一个需要
extern "C"
的场合是当C程序需要调用C++的代码时。 - 我们需要在头文件声明的时候用
extern "C"
代码块包裹,之后可以在.cpp文件中正常对函数进行定义。
-
-
模板函数和模板类的特例化
-
C++的STL源码(这个系列也很重要,建议侯捷老师的STL源码剖析书籍与视频),其中包括内存池机制,各种容器的底层实现机制,算法的实现原理等)
-
STL源码中的hashtable的实现
-
STL中unordered_map和map的区别和应用场景
-
STL中vector的实现
- STL容器的几种迭代器以及对应的容器(输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器)
顺序容器:vector,deque是随机访问迭代器;list是双向迭代器
容器适配器:stack,queue,priority_queue没有迭代器
关联容器:set,map,multiset,multimap是双向迭代器
unordered_set,unordered_map,unordered_multiset,unordered_multimap是前向迭代器
- STL中的traits技法
type_traits
iterator_traits
char traits/
allocator_traits
pointer_traits
array_traits
- vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
-
C++中的重载和重写的区别
重载:函数名相同,但是形参列表个数、顺序或者类型不同。但是不能仅仅是返回值不同(违背函数定义)
- 同一个作用域中
- 参数不同
- virtual可有可无
- 返回值可以不同(当形参列表不同的时候)
重写:指派生类重新定义基类的虚函数,也称覆盖
- 不在同一个作用域(分别位于派生类和基类);
- 函数名字相同
- 参数相同
- 基类函数必须有virtual关键字,不能有static
- 返回值相同(或是返回当前类的可转变的指针),否则报错
- 重写函数的访问修饰符可以不同
重定义:或称隐藏
- 不在同一个作用域;
- 函数名字相同
- 返回值可以不同
- 参数不同
- 此时不论有无virtual关键字,基类的函数都将被隐藏
- 参数相同,但是基类的函数没有virtual关键字。此时基类的函数将被隐藏(否则就是重写)
-
C++内存管理,内存池技术(热门问题),与csapp中几种内存分配方式对比学习加深理解
-
介绍面向对象的三大特性,并且举例说明每一个
三大特性
- 封装
- 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 一个类就是封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。
- 多态
- 让某个类型的对象获得另一个类型的对象的属性或方法。
- 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下堆这些功能进行扩展。
- 继承的实现方式有两种:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅仅使用属性和方法的名称、但是子类必须提供实现的能力。
- 继承
- 是指一个类实例的相同方法在不同情形有不同的表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
- 封装
-
C++多态的实现
-
C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(包括单一继承,多重继承等)(拓展问题:为什么基类指针指向派生类对象时可以调用派生类成员函数,基类的虚函数存放在内存的什么区,虚函数表指针vptr的初始化时间)
-
C++中类的数据成员和成员函数内存分布情况
-
this指针
-
this指针是类的指针,指向对象的首地址
-
T* const this
-
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this
-
this指针只有在成员函数中才有定义,存储位置会因编译器不同而有差异
-
this指针的用处
- 一个对象的this指针并不是对象的一部分,不会影响sizeof的结果
- this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员时,编译器会自动将对象本身的地址作为一个隐含参数传给函数。也就是说,即使你没有写this,编译器在编译时也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行
-
this指针的使用
- 一种情况是,在非静态成员函数需要返回类对象本身时,直接使用
return * this;
(比如在写赋值运算符函数时,为了兼容连等的情况) - 类的this指针有以下特点
- this指针只能在成员函数中使用,全局函数、静态函数都不能使用this
- 另一种情况是当形参和成员变量重名时。
- 一种情况是,在非静态成员函数需要返回类对象本身时,直接使用
-
举例说明
class A{ public: int func(int p); }; int main(){ A a; a.func(10); // 实际上,编译器会认为是: // A::func(&a,10); }
-
-
析构函数一般写成虚函数的原因
-
构造函数、拷贝构造函数和赋值操作符的区别
构造函数:对象不存在,没用别的对象初始化
拷贝构造函数:对象不存在,用别的对象初始化
赋值运算符:对象存在,用别的对象给它赋值
- 构造函数声明为explicit
- 构造函数为什么一般不定义为虚函数
- 构造函数的几种关键字(default delete 0)
= default:将拷贝控制成员定义为=default显式要求编译器生成合成的版本
= delete:将拷贝构造函数和拷贝赋值运算符定义删除的函数,阻止拷贝(析构函数不能是删除的函数 C++Primer P450)
= 0:将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)
-
构造函数或者析构函数中调用虚函数会怎样
不要这么做,尽管符合语法,但是不符合我们使用虚函数的动态绑定本意,在构造/析构函数调用时,可能子对象还没有完成构造/已经完成析构,造成当前虚指针指向的是父对象的虚表。
-
纯虚函数
-
静态类型和动态类型,静态绑定和动态绑定的介绍
静态类型
对象在声明时采用的类型,编译期确定
动态类型
一个指针或者一个引用目前所指对象的类型,运行期决定
静态绑定
绑定的是静态类型,所对应的函数或者属性依赖于对象的静态类型,发生在编译期;
动态绑定
绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
- 如果我们调用的函数是一个non-virtual函数,那么不论当前指针指向哪个子类对象(甚至指向一个空对象),函数都是被调用的(因此判空很重要)
- 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是有性能损失的,但是却能够实现多态特性;
- 建议:
- 不要重定义一个non-virtual函数,这会造成函数调用由声明时的静态类型决定,而和对象本身脱离关系,违背了多态特性。同时也会给程序留下不可预知的隐患和BUG。
- 同时注意子类对于虚函数定义的缺省参数,可能会导致虚函数定义出现问题
-
引用是否能实现动态绑定,为什么引用可以实现
可以,因为引用也是在运行时确定变量类型的,和指针相同,因此我们可以用引用实现动态绑定
-
深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
在C++中使用=运算符进行对象初始化时,我们会默认调用合成拷贝构造函数,也就是浅拷贝,会依次复制当前对象值到被拷贝对象上。
当当前对象成员变量没有指针(或者成员变量本身的成员变量…)时,这种浅拷贝是安全的,一旦其中有一个相关的成员变量含有指针,那么浅拷贝就不再是安全的,会造成空悬指针的问题。这种时候就需要我们去手动构造深拷贝函数,为当前被拷贝对象手动开辟一块新的内存,复制一个新的指针
-
对象复用的了解,零拷贝的了解
对象复用:对象池的一种说法。对象池通过对象复用来避免重复创建特定对象。
-
介绍C++所有的构造函数
-
什么情况下会调用拷贝构造函数(三种情况)
-
采用一个类对象去初始化另一个类对象时
-
值传递
-
返回值
-
-
结构体内存对齐方式和为什么要进行内存对齐?
-
内存泄露的定义,如何检测与避免?
-
手写智能指针的实现(shared_ptr和weak_ptr实现的区别)
-
智能指针的循环引用
两个对象中含有对方的一个智能指针,并且两者的智能指针互相指向对方。
解决方案:类成员变量采用weak_ptr
-
遇到coredump要怎么调试
-
内存检查工具的了解
-
模板的用法与适用场景
-
成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
-
用过C++ 11吗,知道C++ 11哪些新特性?
-
C++的调用惯例(简单一点C++函数调用的压栈过程)
-
右值引用和移动语义
简单来说,左值引用指的是变量,而右值引用指的是值。
在C++11之前,当我们使用一个临时变量,或者对一个变量进行赋值的时候,都会声明一个值,这个值也是会存在内存上的。最大限度就是常量引用去绑定一个右值。而实际上,一个右值是可以被修改的。我们可以对一个临时生成的对象去调用一个修改内部成员变量的函数。
T().set();
,这里的T()
本质上是一个右值,但是我们却可以通过成员函数对其进行修改。证明右值本身并非不可修改。在C++11之后,我们可以通过右值引用更加方便地解决实际问题。
一个临时变量当被调用时,会被当做一个右值,但是在被调用之后,又会被转换成左值。
void foo(int && i){ cout << "RValue" << endl;// 在函数内部时,i为左值 } void foo(int & i){ cout << "LValue" << endl; } int main(){ foo(1); // 输出为RValue int a = 1; foo(a); // 输出为LValue }
-
转移语义
转移语义可以将资源从一个对象转移到另一个对象上,减少不必要的对象创建、拷贝和销毁,能够大幅提高C++程序性能。
-
-
C++的四种强制转换
- dynamic_cast只能够用在指向类的指针或者引用上。
- 这种转换允许upcast(从派生类向基类的转换)
- 但dynamic_cast也支持downcast(从基类指针到派生类指针)当且仅当指针指向的目标对象有效且完整
- 如果是指针转换失败,会返回空指针,如果是引用转换失败,会抛出bad_cast
- dynamic_cast支持空指针到任意类型指针转换,以及任意指针类型到void*转换
- dynamic_cast通过RTTI进行运行时类型检查
- static_cast
- 能够完成相关类指针转换,同样支持upcast以及downcast。
- 但是并不会有运行时检查,因此可能会导致转换时出现运行时错误,这就依赖开发者来确保转换的安全性。
- 反过来说,这也不会增加额外检查的开销。
- 同时,static_cast支持隐式转换以及相反转换。
- void到任何指针以及任何指针到void
- 整数、浮点以及枚举之间的互相转换。
- reinterpret_cast
- 能够完成任意指针类型向任意指针类型的转换,即使它们之间毫无关联。该转换的操作结果是出现一份完全相同的二进制复制品,既不会有内容检查,也不会有类型检查
- const_cast
- 可以用来设置或者移除指针所指对象的const(更改底层const)
static_cast
dynamic_cast
const_cast
reinterpret_cast
-
C++中将临时变量作为返回值的时候的处理过程(栈上的内存分配、拷贝过程)
返回值的时候,临时变量本身已经被销毁,不过变量已经被保存在寄存器中了。
但是,不能返回一个指向局部变量的指针,因为函数退出当前作用域的时候已经清除了栈顶分配的内存,因此这是指针将会指向一块没有意义的内存。
返回的指针只能指向:
- 静态变量
- 手动分配的内存(new/malloc)
- 全局变量
- 程序文本区(比如指向函数的指针)
-
C++的异常处理
-
volatile关键字
一般用于告知编译器不要对该变量进行优化
一般用于多线程情况下
-
优化程序的几种方法
-
public,protected和private访问权限和继承
-
class和struct的区别
-
decltype()和auto
-
inline和宏定义的区别
- 内联函数在编译时展开,而宏在预编译时展开
- 在编译时,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
- 内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
- 宏不是函数,而inline是函数。
- 宏在定义是要小心处理宏函数,否则可能产生二义性。而内联函数则不会。
-
C++和C的类型安全
-
C++ 面向对象k
-
栈对象和堆对象限制
-
栈对象:限制手动分配内存
protected: operator new(){} operator delete(){}
-
堆对象:限制程序自行构造
protected: T(){} ~T(){}
-
-
构造函数详解
构造函数的种类:
class A{ public: // 直接构造函数 A(){}; A(const string & str){} // 拷贝构造函数 A(const A& a){} // 移动构造函数 A(const A&& a){} // 析构函数 ~A(){} //其他相关(不属于构造函数!) // 赋值运算符重载 A & operator=(const A& other){} };
-
直接构造函数
-
拷贝构造函数
-
什么时候调用拷贝构造,什么时候调用赋值运算符
-
当我们有一个新的对象需要初始化时调用拷贝构造,否则调用赋值运算符。
A a1; A a2 = a1; //调用拷贝构造函数 a1 = a2; //调用赋值运算符
-
-
移动构造函数
通过构造函数进行的初始化
- 直接初始化
- 拷贝初始化
string str = "Hello"; //拷贝初始化,相当于程序首先创建一个值为"Hello"的临时对象,然后调用拷贝初始化函数
-
虚函数[https://blog.csdn.net/hackbuteer1/article/details/7883531. https://blog.csdn.net/yusiguyuan/article/details/38764661]