选题原则:1.不考察C++20及以上版本,一是大部分招聘都只要求掌握到C++17,学习和总结C++20回报率低,二是C++20不是必备技能,应该不会只因为不了解C++20就被PASS;2.为使得题目更具有针对性,剔除操作系统、网络、数据结构相关的内容,只保留纯粹的C++内容;3.内容较多,模板、STL、并发与同步相关的面试内容另行总结。

目录
  1. final和override的作用?final为什么能提高代码执行效率?
  2. static的三种作用?
  3. thread_local变量作用?存放位置?生命周期?
  4. 一个变量可以既是const又是volatile吗?
  5. NULL和nullptr区别?为什么要引入nullptr?
  6. 为什么noexcept能提高性能?
  7. delete[]是怎样知道数组长度的?
  8. new,placement new,operator new的区别?怎么在把对象new在栈上?
  9. __cdecl和__stdcall区别?
  10. 重载类的delete运算符,delete这个对象的时候会发生什么?
  11. 函数调用压栈流程?
  12. 声明和定义的作用,从编译角度说明?
  13. 现代大部分编程语言都没有头文件,c++为什么有头文件?头文件和模块的优劣比较?#include和前置声明的区别?
  14. c++11为什么引入枚举类?
  15. 程序是从main 函数开始执行的吗?
  16. 虚函数怎么实现的,性能?真的更慢吗?
  17. 构造函数、析构函数、重载运算符函数可以是虚函数吗?类成员函数模板可以是虚函数吗?
  18. 成员函数指针和普通函数指针区别?
  19. 各种变量存放在虚拟内存的哪个分区?
  20. 对象的内存模型?发生继承时候的对象内存模型?
  21. 什么是标准布局类型和trivial类型?有什么用?
  22. 什么是类型擦除?有哪些实现方式?
  23. 什么是多态?静态和动态多态的实现原理?
  24. inline的作用?优劣?为什么不会引发符号重定义错误?
  25. inline用作内联展开这层含义的时候,构造函数、析构函数、虚函数可以被inline修饰吗?可以获取inline函数的指针吗?static inline和extern inline含义?
  26. malloc和new区别?malloc实现原理?free后,内存被立即释放了吗?
  27. 谈谈lambda函数?
  28. union和struct和class的区别?
  29. 什么是零三五原则?
  30. C++可调用类型有哪些?
  31. 为什么需要把析构函数定义为虚函数?
  32. 具有类成员和基类的类,在实例化和销毁的时候,构造函数和析构函数的调用顺序?
  33. 指针和引用的区别?
  34. 符号重定义的解决方法有哪些?
  35. 四种指针类型转换的区别?
  36. 知道什么是RVO吗?
  37. RTTI的实现原理?
  38. extern C的作用?
  39. 可以在运行时访问private成员吗?
  40. C++编译流程?
  41. 动态库和静态库的区别?知道动态库延迟加载优化吗?
  42. 智能指针是什么?几种智能指针的区别?
  43. 四种智能指针的简单实现?
  44. 什么是左值和右值?它们是C++11才有的吗?string literal是左值还是右值?i++和++i是左值还是右值?
  45. 什么是左右值引用?和左右值有关系吗?右值引用适合什么场景下用?
  46. 基本类型的长度?
  47. 内存对齐规则?为什么要内存对齐?应用场景?
  48. 通过指针访问数组,系统是如何知道指针越界的?
  49. 断言是什么?断言和条件语句的优劣?
  50. 继承和组合的优劣?
题目
(1)final和override的作用?final为什么能提高代码执行效率?

override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名,作用就是用于编译期代码检查。
final:阻止类的进一步派生和虚函数的进一步重写,同时也是一种名为去虚拟化的优化技巧,相当于把运行期多态转换为了编译期多态,提高了执行效率。

(2)static的3种作用?

修饰局部变量,这意味着该变量只被初始化一次,并保留其值直到程序结束。
修饰全局变量,这意味着该变量具有文件作用域。
修饰成员变量,这意味着变量不会绑定到对象上。

(3)thread_local作用和原理?

thread_local用来定义一个线程本地变量,每个线程都拥有自己的thread_local对象副本,这些副本放在各个线程自己的TLS空间。thread_local描述的对象在thread开始时分配,而在thread结束时释放。

(4)一个变量可以既是const又是volatile吗?

可以。const的作用是告诉编译器,编译期间不允许对变量进行修改,编译器在编译期间往往会对const变量执行一种名为字符替换的优化。volatile的作用是告诉编译器,第一,编译期间不要对该变量进行优化;第二,运行期间,每次必须从内存中加载变量的值。const volatile表示一个变量在程序编译期不能被修改且不能被优化;在程序运行期,每次必须从内存中加载变量的值。

(5)NULL和nullptr区别?为什么要引入nullptr?

主流编译器中,NULL 实际上是一个整数常量,被定义为 0,在 C++11 之前,当我们想要将一个指针初始化为空时,我们通常使用 NULL;nullptr 是 C++11 中引入的新的关键字,专门用于表示空指针,它不是整数类型,而是特殊的指针类型nullptr_t。之所以引入nullptr,第一,NULL是整数类型,用户调用foo(NULL)的时候,不能区分调用的是foo(int)还是foo(int*)函数;第二,主流编译器中NULL值为0,通过0表示一个无效地址,但是有的架构下,0地址有特定用途,而nullptr指向的永远是一个无效地址。

(6)为什么noexcept能提高性能?

使用 noexcept 可以让编译器对代码进行优化,从而提高代码的性能。具体来说,为实现异常捕获的功能,c++引入了“栈回退”机制,编译器在编译函数的时候,会为函数生成额外的叫做“栈回退”的代码,使用noexcept 可以避免生成额外的代码来处理异常情况,这样可以减少代码量和执行时间。关闭异常捕获是比较危险的行为,一般只建议用在构造函数。

(7)delete[]是怎样知道数组长度的?

没有标准实现,一种常见的实现方法是,申请内存时,会在返回的指针前面存放这段内存的大小,调用delete[]的时候,就可以知道数组长度了。

(8)new,placement new,operator new的区别?怎么在把对象new在栈上?

operator new作用是分配一块内存,placement new作用是在已分配内存地址处,创建一个对象,new的作用则等于operator new + placement new。先在栈上声明一个数组,然后通过placement new 在这段地址处创建对象,这就实现了在栈上new一个对象。

(9)__cdecl和__stdcall区别?

都是 Microsoft Visual C++ 中用于声明函数调用约定的关键字。__cdecl 是 C/C++ 默认的调用约定,在 __cdecl 调用约定下,参数从右往左入栈,由调用方负责清理堆栈;在__stdcall 调用约定下,函数参数从右向左压入堆栈中,函数堆栈的清除工作由被调用方负责。这些关键字主要用于跨语言调用,以确保参数传递和堆栈清理的一致性。

(10)重载类的delete运算符,delete的时候会发生什么?

new的默认行为是先分配一段内存,然后调用对象的构造函数,把对象创建在这段内存上;delete的默认行为是先调用析构函数,然后释放内存。重载全局new和delete运算符号,会修改所有的new和delete内存行为,重载类的new和delete运算符,会修改针对这个类的new和delete内存行为。

(11)函数调用压栈流程?

不同系统下压栈的具体操作不同,但大致都有这么个过程:函数调用的时候,把被调用函数参数压栈,把预留的返回值存放位置压栈,把当前函数上下文,比如栈地址相关的寄存器和指令地址相关的寄存器内容压栈,函数返回的时候,弹出函数参数和返回值,弹出函数上下文内容到寄存器,恢复现场。

(12)声明和定义的作用,从编译角度说明?

声明的作用主要两点,第一,提供链接时需要的符号信息,这些信息存储在目标文件的重定位表和符号表当中;第二,提供类型大小信息,c++采取的是单文件编译策略,当不知道某个符号对应类型的定义的时候,需要在链接前预留出合适大小的内存空间,供链接时填充。

(13)现代大部分编程语言都没有头文件,c++为什么有头文件?头文件和模块的优劣比较?#include和前置声明的区别?

c++和采取模块机制的编程语言的一个重大区别在于,c++把函数和变量签名这部分信息保存在头文件内,而采取模块机制的编程语言把这部分信息保存在库内。头文件和模块相比,会拷贝很多无用的声明信息到当前文件内,从而导致编译非常慢,另一个缺点就是头文件机制很容易引发符号重定义错误。c++之所以采用头文件机制是因为,早期计算机的内存资源非常珍贵,如果把函数和变量签名信息都保存到二进制库中,会浪费更多的内存资源。

#include和前置声明本质上都是声明,区别在于#include在预处理期间做了一次拷贝声明的操作,前置声明的优势在于可以按需导入函数,而且可以解决循环依赖问题。

(14)C++11为什么引入枚举类?

传统的 C++ 枚举类型会将枚举值暴露在命名空间中,容易造成命名冲突,而枚举类则通过引入了作用域限定符来解决这个问题。其次,传统的 C++ 枚举类型是基于整数的,可以进行隐式的类型转换和比较操作,这可能会导致一些意想不到的错误,而枚举类则可以避免这个问题,因为它们只能进行显式的类型转换和比较操作。

(15)程序是从main 函数开始执行的吗?

不是,程序在执行前,会经历一个从磁盘加载程序到内存的过程,这个过程会执行全局变量的初始化。

(16)虚函数怎么实现的?真的更慢吗?

虚函数是通过虚函数表实现的,每个类都有自己的虚表,对象的首地址处存放有指向虚表的指针。当具体调用哪个虚函数可以在编译期间确定的时候,虚函数不一定更慢。

(17)构造函数、析构函数、重载运算符函数可以是虚函数吗?类成员函数模板可以是虚函数吗?

析构函数和重载运算符函数都可以是虚函数,而构造函数不能是虚函数,首先C++编译器层面不允许这种操作,第二构造函数不需要动态多态,C++引入虚函数的目的就是为了解决编译期间无法确定调用对象的问题,而对于构造函数这类特殊函数,编译期间就已经明确知道需要创建的对象类型。类成员函数模板不能是虚函数,因为C++在链接前是不知道成员函数模板被实例化多少次的,这就会导致编译器无法在编译期间确定虚表的大小。

(18)成员函数指针和普通函数指针区别?

普通函数指针属于指针类型,成员函数指针不是指针类型。通常来说,函数指针的长度等于机器字长,而成员函数指针长度比函数指针更长,其内部存放了对象地址和成员函数地址信息。在没有给出对象地址的情况下,调用成员函数指针会报错。

(19)各种变量存放在虚拟内存的哪个分区?

直接声明的变量、函数实参存储在栈区;new创建的对象,较小的对象存放在堆 区,较大的对象存放在共享内存区;常量和静态变量存放在静态存储区中的非代码区;所有函数存放在静态存储区中的代码区;字符常量也存放在代码区。

(20)对象的内存模型?发生继承时候的对象内存模型?

成员函数存放在代码区;静态成员变量存放在静态存储区;普通成员变量存放在对象内,且按照声明顺序依次存放;如果类声明了虚函数,那么对象的首地址处往往会存放一个指向虚表的指针,另外访问权限关键字可能会影响对象的内存布局,至于怎么影响,标准没有规定,不同编译器的实现可能不同。发生继承的时候,基类对象怎样存放,标准也没有规定,一般是按照继承顺序依次存放在内存当中,每个对象都可以有自己的虚表。

(21)什么是标准布局类型和trivial类型?有什么用?

引入标准布局类型是为了向C语言兼容,使得用户能够通过对象第一个成员的指针类型指向对象;引入trivial类型是为了提高对象初始化效率,memcpy比构造函数初始化效率效率更高。不考虑继承,一个类没有虚函数、所有非静态变量的访问权限相同,则是标准布局类型;不考虑继承,一个类没有自定义构造、自定义析构函数,没有虚函数,则是trivial类型。

(22)什么是类型擦除?实现方式?

类型擦除是一种,使得不同类型变量能够得到统一处理的技术。实现方式上可分为静态类型擦除了动态类型擦除,静态类型擦除通过模板或者宏实现,动态类型擦除可通过继承虚函数或者void类型实现。

(23)什么是多态?实现方式?

多态指的是一种相同的形式表现出不同行为的概念,分为静态多态和动态多态。代码层面,静态多态通过重载(overload)实现,动态多态通过覆盖(override)实现;原理层面,静态多态通过name mangling实现,动态多态通过虚表实现。

(24)inline的作用和原理?

c++17以前,inline关键字主要有两个作用:第一,作为内联优化建议,告诉编译器在调用处展开函数,只不过是否展开函数还是由编译器决定;第二,解决符号重定义问题,不同文件内定义了同签名的函数,若被inline关键字修饰,则不会引发符号重定义错误。c++17开始,inline只保留第二个作用,若用户希望函数内联展开,则可以使用__attribute((always_inline))__ 关键字,它是 GCC 和 Clang 中的一个扩展,用于强制内联函数。。

原理上,第一,内联展开相比于普通函数调用,少了函数上下文压栈的过程,因此效率更高,缺点就是容易引起代码膨胀。第二,被inline关键字修饰的函数名,编译期间会被标记为weak符号,链接目标文件的时候,多个同签名weak符号不会引发编译器报错,运行期间,会选取其中一个函数进行调用。

(25)inline用作内联展开这层含义的时候,构造函数、析构函数、虚函数可以被inline修饰吗?可以获取inline函数的指针吗?static inline和extern inline含义?

任何函数都可以被inline修饰,包括构造函数、析构函数、虚函数。这里提一下为什么虚函数可以内联,inline函数涉及到的是编译期解析,虚函数地址大多数情况下在运行期解析,但是某些情况下,具体调用哪个虚函数可以在编译期间确定,这个时候虚函数就能内联展开了。

inline只作为内联建议,是否展开由编译器决定,因此是可以获取inline函数指针的。

static inline指的是具有文件作用域的inline函数;extern inline作用比较特殊,外部单元把它当作普通函数进行调用,同单元内把它当作inline函数调用。

(26)malloc和new区别?malloc实现原理?free后,内存被释放了吗?

malloc只分配一段内存,new会先分配一段内存,然后在这段内存上创建对象。malloc实现上,先从用户态切换到内核态,分配一段空闲物理内存,接着在虚拟内存堆空间或者共享内存空间分配一段虚拟内存,然后填充页表,把虚拟内存映射到物理内存,最后返回用户态。free后,内存没有被立即释放,而是保留在内存当中,作为内存池的一部分供下次使用。

(27)谈谈lambda函数?

lambda函数可以看作是函数对象的语法糖,可以随地定义和调用。可通过lambda和智能指针实现闭包,c++17以前,lambda不支持this捕获,c++17开始支持this捕获,即非静态成员函数内部定义的lambda函数不需要通过显式指定this,就可以访问对象成员。

(28)union和struct和class的区别?

struct和class都可以用来定义类,struct成员默认public,class成员默认private,只不过从语义上来说,建议用struct定义数据块,class定义类。struct 每个成员变量都有自己的内存地址;union 内存占用大小为其成员中需要空间最大者,每个成员变量都占用相同的内存单元。

(29)什么是零三五原则?

零之法则:对于不需要通过析构函数回收资源的类,只定义普通构造函数。

三之法则:如果某个类需要用户定义析构函数回收资源,那么这个类除了要定义普通构造函数外,也一定要定义复制构造函数、赋值运算函数。

五之法则:因为用户定义的析构函数、复制构造函数或复制赋值运算符的存在会阻止移动构造函数和移动赋值运算符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数

(30)C++可调用类型有哪些?

函数指针、函数对象、lambda表达式、成员函数指针。

(31)为什么把析构函数定义为虚函数?

解决delete 指向子类对象的基类指针的时候,只析构基类、不析构子类的问题。

(32)构造函数和析构函数的调用顺序?

创建对象过程,先调用基类的构造函数,然后依次调用类非静态成员的构造函数,最后调用自己的构造函数;销毁对象过程,先调用自己的析构函数,然后依次调用非静态成员的析构函数,最后调用基类的析构函数。

(33)指针和引用的区别?

引用和指针在汇编层面都是内存地址,引用可以看作是指针常量,只能在声明的时候初始化,相比于指针,引用的优势在于编译器帮我们检查地址是否初始化。

(34)符号重定义的解决方法?

通过extern,static,inline,const关键字都可以解决符号重定义问题,也可以通过命名空间、前置声明、#ifndef和#pragma once宏解决这个问题。

(35)四种指针类型转换的区别?

reinterpret_cast用于任意指针(引用)类型之间的转换,不进行类型检查。
static_cast用于基类和子类指针(引用)之间的转换,编译期进行类型检查。
dynamic_cast用于基类和子类指针(引用)之间的转换,运行期进行类型检查。
const_cast用于指针(引用)类型,用于删除限定符,不进行类型检查。

(36)知道什么是RVO吗?

RVO是一种返回值优化手段,它通过避免创建临时对象来提高代码性能。当一个函数返回一个非引用类型的变量时,编译器会尝试将该对象直接构造在调用者的栈帧空间中,而不是为该对象分配新的内存并在函数返回后再将其拷贝到调用者的栈帧空间中。

(37)RTTI的实现原理?

RTTI指的是运行时类型识别,通过虚表实现,指向类型信息的指针存放在虚表上。

(38)extern C的作用?

extern "C" 是 C++ 提供的一个关键字,用于指示编译器将某个函数或变量的名称按照 C 语言的方式进行处理,以便与C语言进行交互。其原理上就是关闭编译器的name mangling。

(39)可以在运行时访问private成员吗?

可以,访问权限关键字只在编译期有效,运行期是没有访问权限关键字这些概念的,可以在运行时访问对象内的任何成员。

(40)C++的编译流程?

先预处理,然后编译成目标文件,接着把目标文件链接成库文件或者可执行文件。

(41)动态库和静态库的区别?知道动态库延迟加载优化吗?

链接动态库和静态库的时候,静态库会被复制到可执行程序当中,而动态库不会。相比动态库,静态库的执行效率更高,但占用磁盘空间更多,不方便更新。动态库的延迟加载指的是,在运行时按需加载动态链接库中的函数和数据,而不是在启动的时候加载库函数和数据,从而降低启动时间,在linux系统下,延迟加载是通过PLT表和GOT表配合实现的。

(42)智能指针是什么?几种智能指针的区别?

智能指针是RAII思想的一种应用,shared_ptr是最常用的智能指针,但是,第一,效率低,可以通过在特定场合使用unique_ptr弥补这点;第二,有循环引用的问题,故引入weak_ptr;第三,不能直接封装this并返回,否则会引起引用计数错误,故引入enable_shared_from_this

(43)四种智能指针的简单实现?

不考虑删除器的实现,unique_ptr内部封装一个指针,在构造函数内把地址传给指针,析构函数内销毁指针指向的对象;shared_ptr内部封装一个指针,和一个存放在堆空间的引用计数,重新实现构造、拷贝构造、赋值构造、析构函数,每次调用构造函数、赋值构造函数、拷贝构造函数的时候,通过原子操作,对引用计数加1,每次调用析构函数,通过原子操作对引用计数减1,计数为0则销毁对象;weak_ptr实现和shared_ptr类似,不同在于它不影响引用计数;enable_shared_from_this通过CRTP实现。

(44)什么是左值和右值?它们是C++11才有的吗?string literal是左值还是右值?i++++i是左值还是右值

左值是可以取地址的值,右值是不可取地址的值,右值之所以不能取地址,往往是因为这些值可能在寄存器上、可能是指令的一部分、可能是栈上的匿名变量。左右值是C语言出现开始,一直都有的概念,只是没有给他们明确下定义。string literal是左值,++i是返回值是i本身,是左值,i++会返回一个临时变量,是右值。

(45)什么是左右值引用?和左右值有关系吗?右值引用适合什么场景下用?

左值引用和右值引用在汇编层面都是地址,右值引用的出现是伴随着移动构造函数出现的,之所以引入右值引用类型的语法,是为了区分拷贝构造函数和移动构造函数,更准确地来说是为了区分深拷贝和浅拷贝。只有右值才可以赋值给右值引用,但是右值和右值引用没有严格意义上的关系,把右值赋值给右值引用往往是不合理的,反而会降低运行效率,不要把字面值赋值给右边值引用,不要以右值引用的方式返回函数返回值。右值引用仅仅适用于把将亡值传递给函数参数这类场景。

(46)基本类型的长度?

这些长度可能会因编译器、操作系统和计算机体系结构的不同而有所变化。char长度是1字节;short长度至少2字节,大多情况下2字节;int长度至少2字节,大多数情况下4字节;long int长度大于等于int长度;float长度4字节;double长度8字节。所以为了移植性,一般不建议直接使用这些类型,建议使用int8_tint16_tint32_t等类型。

(47)内存对齐规则?为什么要内存对齐?

内存对齐有两个要求,第一,C++中有对齐系数这个概念,任何类型在内存中的首地址必须是自身对齐系数的整数倍,基本类型的对齐系数等于自身大小,结构体类型的内存对齐系数等于内存占用最大的基本类型成员的大小;第二,结构体内类型,相对于结构体首地址的偏移必须等于自身对齐系数的整数倍。引入内存对齐,是为了减少CPU访问内存数据的次数,提高取数据的效率。

(48)通过指针访问数组,系统是如何知道指针越界的?

编译器编译代码期间会增加额外的代码用于检测数组是否越界。生成下标越界检查代码,C语言默认关闭;C++默认开启。

(49)断言是什么?断言和条件语句的优劣?

断言用于在代码编译或者执行期间检查特定条件是否成立,不成立则报错终止。静态断言和动态断言是两种不同类型的断言。静态断言在代码编译期间进行验证,并在发现问题时引发编译时错误;动态断言在代码运行期间进行验证,并在发现问题时引发异常或错误。C++内,动态断言通常只在调试模式下启用,而在发布模式下会被忽略。断言相比于条件语句,效率更高,但降低了程序安全性。

(50)继承和组合的优劣?

继承和组合都是代码复用的方案,继承的耦合性更高,但提供了更多复用特性,比如public和private复用、比如多态。