CPP后端研发常见面试题
C/C++ 语法问题
关键字
const
- 声明常量。
- 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
- 修饰成员变量,说明不可被修改。被const修饰的成员变量只能在构造函数的初始化列表中进行初始化。
- const 修饰成员函数,函数只能被 const this 调用。不能修改成员变量。除非成员变量被 mutable 修饰过。
const 的指针与引用
- 指针
- 指向常量的指针(pointer to const)
- 自身是常量的指针(常量指针,const pointer)
- 引用
- 指向常量的引用(reference to const)
- 没有 const reference,因为引用本身就是 const pointer
this 指针
- this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。
- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
- 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
- this 指针被隐含地声明为一个常量,即 className * const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,即不能修改 this 指针指向的数据;
- this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
inline 内联函数
- 相当于把内联函数里面的代码写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,但是又具有函数的特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
编译器处理 inline 函数的步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
inline 函数优缺点
-
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数方便调试,而宏定义比较麻烦。
-
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
virtual 与 inline
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。
- inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
sizeof()
- sizeof 对数组,得到整个数组所占空间大小。
- sizeof 对指针,得到指针本身所占空间大小。
总结一下,sizeof(A) 返回的是变量 A 本身的大小。
explicit(显式)关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
friend 友元类和友元函数
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
引用
- 左值引用:常规引用,一般表示对象的身份。
- 右值引用:必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
引用折叠
- 为便于阅读,例子中用下划线代替空格。
- 所有右值引用折叠到右值引用上仍然是一个右值引用。(X&&_&& 可折叠成 X&&)
- 所有的其他引用类型之间的折叠都将变成左值引用。(X&_&、X&_&&、X&&_& 可折叠成 X&)
成员初始化列表
有些场合必须要用初始化列表:
- 常量成员。因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型的成员变量。引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型成员变量。
面向对象
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。
- public:类外函数,友元,派生类均可访问。
- private: 友元可访问。
- protected:派生类的成员函数或友元可以通过派生类对象访问基类的 protected 成员。
访问控制与继承
某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没有影响。派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
- 如果继承是 public 的,则继承自基类的成员在派生类内仍遵循其原有的访问说明符。
- 如果继承是 private 的,则继承自基类的成员在派生类内都是private的。
- 如果继承是 protected 的,则继承自基类的public成员在派生类中是 protected 的,其他成员不变。
多态
C++ 多态分类及实现:
- 编译期多态:函数重载
- 运行期多态:虚函数
关于虚函数
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象的问题。上述情况中,如果析构函数不是虚函数,则派生类的析构函数逻辑不会被执行。
虚函数、纯虚函数
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。声明虚函数的目的在于,使派生类继承函数的接口和缺省实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
智能指针
- shared_ptr:多个智能指针可以共享同一个对象,对象的最后一个拥有者有责任销毁对象,并清理与该对象相关的所有资源。
- 支持定制型删除器(custom deleter)
- 可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)
- shared_ptr 本身是线程安全的。
- unique_ptr:采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有者被销毁或指向 nullptr,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
- weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的shared_ptr失去了所有权,所有关联的 weak_ptr 都会自动成空。
- 可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题。
强制转换
static_cast
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int)
- 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
dynamic_cast
- 用于多态类型的转换
- 执行运行时类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast
- 用于删除 const 特性,如将 const int 转化为 int。
reinterpret_cast
- 用于位的简单重新解释
- 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
- 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
- reinterpret_cast 运算符不能丢掉 const 特性。