c++面试八股文
https://blog.csdn.net/songbijian/article/details/132507421
1.讲一讲封装、继承、多态是什么?
封装:将具体实现过程和数据封装成一个类,只能通过接口进行访问,降低耦合性,使类成为一个具有内部数据的自我隐藏能力、功能独立的软件模块。意义:保护或防止代码在无意之中被破坏,保护类中的成员,不让类中以外的程序直接访问或者修改,只能通过提供的公共接口访问。
继承:子类继承父类的特征和行为,复用了基类的全体数据和成员函数,具有从基类复制而来的数据成员和成员函数(基类私有成员可被继承,但是无法被访问),其中构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承。基类中成员的访问方式只能决定派生类能否访问它们。增强了代码耦合性,当父类中的成员变量或者类本身被final关键字修饰时,修饰的类不能被继承,修饰的成员函数不能重写或修改。意义:基类的程序代码可以被派生类复用,提高了软件复用的效率,缩短了软件开发的周期
多态:不同继承类的对象对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现形式。意义:对已存在的代码具有可替代性,对代码具有可扩充性,新增子类不会影响已存在类的各种性质,在程序中体现了灵活多样的操作,提高了使用效率,简化了对应用代码的编写和修改过程。
2.多态的实现原理(实现方式)是什么?以及多态的优点(特点)?
实现方式:多态分为动态多态(动态多态是利用虚函数实现运行时的多态,即在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数。)和静态多态(又称编译期多态,即在系统编译期间就可以确定程序将要执行哪个函数),其中动态多态是通过虚函数实现的,虚函数是类的成员函数,存在存储虚函数指针的表叫做虚函数表,虚函数表是一个存储类成员虚函数的指针,每个指针都指向调用它的地方,当子类调用虚函数时,就会去虚表里面找自己对应的函数指针,从而实现“谁调用、实现谁”从而实现多态。而静态多态则是通过函数重载(函数名相同,参数不同,两个函数在同一作用域),运算符重载,和重定义(又叫隐藏,指的是在继承关系中,子类实现了一个和父类名字一样的函数,(只关注函数名,和参数与返回值无关)这样的话子类的函数就把父类的同名函数隐藏了。隐藏只与函数名有关,与参数没有关系.)来实现的。
优点:加强代码的可扩展性,可替换性,增强程序的灵活性,提高使用效率,简化对应用代码的编写和修改过程。
3.final标识符的作用是什么?
放在类的后面表示该类无法被继承,也就是阻止了该类的继承,放在虚函数后面该虚函数无法被重写,表示阻止虚函数的重载
4.虚函数是怎么实现的?它存放在哪里在内存的哪个区?什么时候生成的
在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针
虚函数表:每个包含虚函数的类都会生成一个虚函数表,其中存储着该类中所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。
虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。
当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。
在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针又存放在了虚表之中。
5.智能指针的本质是什么,它们的实现原理是什么?
智能指针本质是一个封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。
6.匿名函数的本质是什么?它的优点是什么?
匿名函数本质上是一个对象,在其定义的过程中会创建出一个栈对象,内部通过重载()符号实现函数调用的外表。
优点:使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。
7.右值引用是什么,为什么要引入右值引用?
右值引用是为一个临时变量取别名,它只能绑定到一个临时变量或表达式(将亡值)上。实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)而右值引用可以对右值进行修改。
为什么:
1.为了支持移动语义,右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
2.完美转发:右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转发”到其他函数中,从而实现完美转发。
3.拓展可变参数模板,实现更加灵活的模板编程。
8.左值引用和指针的区别?
是否初始化:指针可以不用初始化,引用必须初始化
性质不同:指针是一个变量,引用是对被引用的对象取一个别名
占用内存单元不同:指针有自己的空间地址,引用和被引用对象占同一个空间。
9.指针是什么?
指针全名为指针变量,计算机在存储数据是有序存放的,为了能够使用存放的地址,就需要一个地址来区别每个数据的位置,指针变量就是用来存放这些地址的变量。
10.weak_ptr真的不计数?是否有计数方式,在哪分配的空间。
计数,控制块中有强弱引用计数,如果是使用make_shared初始化的函数则它所在的控制块空间是在所引用的shared_ptr中同一块的空间,若是new则控制器所分配的内存与shared_ptr本身所在的空间不在同一块内存。
11.malloc的内存分配的方式,有什么缺点?
malloc并不是系统调用,而是C库中的函数,用于动态内存分配,在使用malloc分配内存的时候会有两种方式向操作系统申请堆内存
方式1:当用户分配的内存小于128KB时通过brk()系统调用从堆分配内存,实现方式:将堆顶指针向高地址移动,获取内存空间,如果使用free释放空间,并不会将内存归还给操作系统,而是会缓存在malloc的内存池中,待下次使用
方式2:当用户分配的内存大于128KB时通过mmap()系统调用在文件映射区域分配内存,实现方式为:使用私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区拿了一块内存,free释放内存的时候,会把内存归还给操作系统,内存得到真正释放
缺点:容易造成内存泄漏和过多的内存碎片,影响系统正常运行,还得注意判断内存是否分配成功,而且内存释放后(使用free函数之后指针变量p本身保存的地址并没有改变),需要将p的赋值为NULL拴住野指针。
11.1为什么不全部使用mmap来分配内存?
因为向操作系统申请内存的时候,是要通过系统调用的,执行系统调用要进入内核态,然后再回到用户态,状态的切换会耗费不少时间,所以申请内存的操作应该避免频繁的系统调用,如果都使用mmap来分配内存,等于每次都要执行系统调用。另外,因为mmap分配的内存每次释放的时候都会归还给操作系统,于是每次mmap分配的虚拟地址都是缺页状态,然后在第一次访问该虚拟地址的时候就会触发缺页中断。
11.2为什么不全部都用brk
如果全部使用brk申请内存那么随着程序频繁的调用malloc和free,尤其是小块内存,堆内将产生越来越多的不可用的内存碎片。
12.传入一个指针,它如何确定具体要清理多少空间呢?
我们在申请内存的时候,会多分配16字节的内存,里面保存了内存块的详细信息,free会对传入的内存地址向左偏移16字节,然后分析出当前内存块的大小,就知道要释放多大的内存空间了。
13.define和const的区别是什么?
编译阶段:define是在编译预处理阶段进行简单的文本替换,const是在编译阶段确定其值
安全性:define定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全检查;const定义的常量是有类型的,是要进行类型判断的
内存占用:define定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的内存;const定义常量占用静态存储区域的空间,程序运行过程中只有一份
调试:define定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const定义的常量是可以进行调试的。
14.程序运行的步骤是什么
预编译:将头文件编译,进行宏替换,输出.i文件
编译:将其转化为汇编语言文件,主要做词法分析,语义分析以及检查错误,检查无误后将代码翻译成汇编语言,生成.s文件
汇编:汇编器将汇编语言文件翻译成机器语言,生成.o文件
链接:将目标文件和库链接到一起,生成可执行文件.exe
15.锁的底层原理是什么?
锁的底层是通过CAS,atomic 机制实现。
CAS机制:全称为Compare And Swap(比较相同再交换)可以将比较和交换操作转换为原子操作,CAS操作依赖于三个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存在内存之中。(就是每一个线程从主内存复制一个变量副本后,进行操作,然后对其进行修改,修改完后,再刷新回主内存前。再取一次主内存的值,看拿到的主内存的新值与当初保存的快照值,是否一样,如果不一样,说明有其他线程修改,本次修改放弃,重试。)
atomic机制:如16问。
16.原子操作是什么?
原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程。
原理是:在X86的平台下,CPU提供了在指令执行期间对总线加锁的手段,CPU中有一根引线#HLOCK pin连接到北桥,如果汇编语言的程序在程序中的一条指令前面加上了前缀“LOCK”,经过汇编之后的机器码就使CPU在执行这条指令的时候把#HLOCKpin的电平拉低持续到这条指令结束的时候放开,从而把总线锁住,这样别的CPU就暂时不能够通过总线访问内存了,保证了多处理器环境中的原子性。
17.纯虚函数是否可以存在默认实现
答案是可以的,但是意义不是很大。
18.堆,栈都存什么内容
栈是一种先进后出(LIFO) 的内存区域,由编译器自动分配和释放,用于存储函数调用过程中的临时数据,其操作效率高。
- 函数参数
- 局部变量
- 返回地址
- 栈帧信息
- 非静态局部对象的成员变量
堆是一种动态分配的内存区域,由程序员手动申请(如 new/malloc)和释放(如 delete/free),其大小不固定,管理相对灵活但效率较低。
- 动态分配的变量 / 对象
- 容器中的动态数据
- 全局 / 静态变量中动态分配的部分
19.堆和栈的大小限制是多少
栈的默认大小由操作系统和编译器决定,通常较小: Windows 系统:默认栈大小一般为 1MB(可通过编译器设置调整,如 Visual Studio 中可修改项目配置)。 Linux 系统:默认栈大小通常为 8MB(可通过 ulimit -s 命令查看和临时调整)。 嵌入式系统或资源受限环境:栈大小可能更小(如几十 KB),需手动配置。
限制原因 栈的内存是连续分配的,且由系统自动管理(函数调用时分配,返回时释放),设计为较小的固定大小可避免内存碎片化,提升访问效率。
溢出风险 若函数递归层级过深、局部变量占用内存过大(如大型数组),可能导致栈溢出(Stack Overflow),程序会直接崩溃。
堆的最大可能大小取决于系统的地址空间: 32 位系统:地址空间上限为 4GB(实际可用堆通常远小于此,受操作系统内核占用、其他进程内存使用等影响,一般不超过 2GB)。 64 位系统:地址空间极大(理论上可达 2^64 字节),但实际受物理内存(RAM)和虚拟内存(交换分区 / 页面文件)总大小限制。
20、常量池
常量池(Constant Pool) 是用于存储程序中各种常量(如字符串、数字常量、类 / 方法信息等)的特殊内存区域,其核心作用是避免常量重复存储、节省内存空间,并提高访问效率。
C++ 标准未明确规定常量池,但编译器(如 GCC、MSVC)通常会对字符串常量等实现类似 “常量池” 的优化,称为字符串驻留(String Interning)。
- 相同的字符串字面量在编译时会被合并,仅存储一份(位于只读数据段.rodata)。
- 通过const char*定义的字符串指针会指向常量池中的同一份数据,而std::string是否使用常量池取决于编译器和版本(C++11 后std::string可能不依赖常量池,直接管理内存)。
基本类型常量(如const int a = 5)通常存储在栈或全局数据段,不进入 “常量池”,但编译器会进行值优化(如直接替换为常量值)。
常量池的核心作用 节省内存:相同常量只存储一份,避免重复占用空间。 提高效率:常量地址固定,访问时无需重复创建,加快引用速度。 保证唯一性:确保常量在程序中全局唯一,避免逻辑错误(如字符串比较时的地址一致性)。
21、类枚举和普通枚举区别
在 C++ 中,“类枚举”(enum class,也称为 “限定作用域枚举”)和 “普通枚举”(enum,也称为 “不限定作用域枚举”)是两种枚举类型,它们的核心区别体现在作用域、类型安全性和隐式转换三个方面。C++11 引入 enum class 正是为了解决普通枚举的一些缺陷。
作用域与命名冲突 普通枚举的枚举值会 “泄漏” 到所在的作用域中,可能导致命名冲突;而类枚举的枚举值被严格限定在自身作用域内,必须通过 枚举名::值 的方式访问,避免冲突。
类型安全性与隐式转换 普通枚举的值本质是整数,可与整数类型隐式转换,容易导致逻辑错误;类枚举是独立类型,禁止与整数或其他枚举类型隐式转换,类型安全性更高。
底层类型控制 普通枚举的底层整数类型由编译器决定(通常是 int),无法显式指定;类枚举可以显式指定底层类型(如 uint8_t、int32_t),便于控制内存占用和跨平台兼容性。
22、内联变量
inline constexpr bool kCgLimitDeviceRead = true;constexpr:编译期常量,确保不可变性 编译期计算:constexpr 表明该变量是编译期常量,其值在编译阶段就已确定,而非运行时初始化。这意味着编译器可将其值直接嵌入到使用它的代码中(类似宏替换,但类型安全),无需运行时内存分配。 不可修改:constexpr 隐含 const 语义,变量初始化后无法被修改(编译期检查,若尝试修改会直接报错),确保值的唯一性和稳定性。 类型安全:相比 #define 宏定义的常量,constexpr 变量有明确的 bool 类型,编译器会进行类型检查,避免隐式类型转换导致的错误。
inline:允许跨翻译单元重复定义,避免链接冲突 跨文件共享:在 C++ 中,若一个变量在多个 .cpp 文件(翻译单元)中声明,会导致链接冲突(多重定义错误)。inline 关键字允许该变量在多个翻译单元中重复定义(只要定义完全相同),编译器会在链接时自动合并为单一实例,适合作为全局共享的常量。 头文件安全:因此,这类变量通常放在头文件中,供多个源文件包含使用,无需担心 “重复定义” 问题(这是 inline 在变量上的核心作用,C++17 开始支持 inline 变量)。
命名风格:符合工程实践的常量命名 前缀 k 表示 “常量”(源自 Google C++ 风格指南,广泛用于工程实践);