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

CPP后端研发常见面试题
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 本身的大小。CPP后端研发常见面试题
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 特性。

如果感觉有点意思,可以关注 HelloNebula

CPP后端研发常见面试题