C++面经八股文
- 1. 编译内存相关
- 1.1. C++ 程序编译过程
- 1.2. C++ 内存管理
- 1.3. 栈和堆的区别
- 1.4. 变量的区别
- 1.5. 全局变量定义在头文件中有什么问题?
- 1.6. 内存对齐
- 1.7. 什么是内存泄露
- 1.8. 怎么防止内存泄漏?内存泄漏检测工具的原理?
- 1.9. 智能指针有哪几种?智能指针的实现原理?
- 1.10 智能指针应用举例
- 1.11 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?
- 1.12 使用智能指针会出现什么问题?怎么解决?
- 1.13 VS检测内存泄漏,定位泄漏代码位置方法
- 1.14 深拷贝与浅拷贝
- 1.15 虚拟内存
- 2. 语言对比
- 2.1 C++ 11 新特性
- 2.2 C 和 C++ 的区别
- 2.3 Python 和 C++ 的区别
- 3. 面向对象
- 3.1 什么是面向对象?面向对象的三大特性
- 3.2 重载、重写、隐藏的区别
- 3.3 如何理解 C++ 是面向对象编程
- 3.4 什么是多态?多态如何实现?
- 3.4.1 静态多态与动态多态:
- 4. 类相关
- 4.1 什么是虚函数?什么是纯虚函数?
- 4.2 虚函数和纯虚函数的区别?
- 4.3 虚函数的实现机制
- 4.4 单继承和多继承的虚函数表结构
- 4.5 为什么构造函数不能为虚函数?
- 4.6 为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?
- 4.7 .不能声明为虚函数的有哪些
- 5. 关键字库函数
- 5.1 sizeof 和 strlen 的区别
- 5.2 lambda 表达式(匿名函数)的具体应用和使用场景
- 5.3 explicit 的作用(如何避免编译器进行隐式类型转换)
- 5.4 C 和 C++ static 的区别
- 5.5 static 的作用
- 5.6 static 在类中使用的注意事项(定义、初始化和使用)
- 5.7 static 全局变量和普通全局变量的异同
- 5.8 const 作用及用法
- 5.9 define 和 const 的区别
- 5.10 define 和 typedef 的区别
- 5.11 用宏实现比较大小,以及两个数中的最小值
- 5.12 inline 作用及使用方法
- 5.13 inline 函数工作原理
- 5.14 宏定义(define)和内联函数(inline)的区别
- 5.15 new 的作用?
- 5.16 new 和 malloc 如何判断是否申请到内存?
- 5.17 delete 实现原理?delete 和 delete[] 的区别?
- 5.18 new 和 malloc 的区别,delete 和 free 的区别
- 5.19 malloc 的原理?malloc 的底层实现?
- 5.20 C 和 C++ struct 的区别?
- 5.21 为什么有了 class 还保留 struct?
- 5.22 struct 和 union 的区别
- 5.23 class 和 struct 的异同
- 5.24 volatile 的作用?是否具有原子性,对编译器有什么影响?
- 5.25 什么情况下一定要用 volatile, 能否和 const 一起使用?
- 5.26 返回函数中静态变量的地址会发生什么?
- 5.27 extern C 的作用?
- 5.28 sizeof(1==1) 在 C 和 C++ 中分别是什么结果?
- 5.29 memcpy 函数的底层原理?
- 5.30 strcpy 函数有什么缺陷?
- 5.31 auto 类型推导的原理
- 5.32 malloc一次性最大能申请多大内存空间
- 5.33 public、protected、private的区别
- 6. 语言特性相关
- 6.1 左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?
- 6.2 std::move() 函数的实现原理
- 6.3 什么是指针?指针的大小及用法?
- 6.5 C++ 11 nullptr 比 NULL 优势
- 6.6 指针和引用的区别?
- 6.7 常量指针和指针常量的区别
- 6.8 函数指针和指针函数的区别
- 6.9 强制类型转换有哪几种?
- 6.10 如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?
- 6.11 参数传递时,值传递、引用传递、指针传递的区别?
- 6.12 什么是模板?如何实现?
- 6.13 函数模板和类模板的区别?
- 6.14 什么是可变参数模板?
- 6.15 什么是模板特化?为什么特化?
- 6.16 include " " 和 的区别
- 6.17 泛型编程如何实现?
- 6.18 C++命名空间
- 6.19 C++ STL六大组件
- 7 git 分布式版本控制系统
- 7.1 简单说一下大端、小端。
- 7.2 什么是git?
- 7.3 为什么要用git?在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。
- 7.4 简述集中式版本控制库和分布式版本控制库的区别
- 8. 补充内容
- 8.1 mmap基本原理和分类
- 8.2 RAII机制介绍
- 8.3 使用RAII机制的原因
- 8.4 RAII机制的使用方法
参考资料:
面试背诵版—C++
C++语言堆栈的详细讲解
代码鹿のC++八股面试题总结
1. 编译内存相关
1.1. C++ 程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
编译预处理:处理以 # 开头的指令,产生 .i 文件;
主要的处理操作如下:
-
对全部的#define进行宏展开。
-
处理全部的条件编译指令,比方#if、#ifdef、#elif、#else、#endif;
-
处理 #include 指令,这个过程是递归的,也就是说被包括的文件可能还包括其它文件;
-
删除全部的注释 // 和 /**/
-
加入行号和文件标识
-
保留全部的 #pragma 编译器指令
ps:经过预处理后的 .i 文件不包括任何宏定义,由于全部的宏已经被展开。而且包括的文件也已经被插入到 .i 文件里。
编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
-
词法分析:将源代码的字符序列分割成一系列的记号。
-
语法分析:对记号进行语法分析,产生语法树。
-
语义分析:判断表达式是否有意义。
-
代码优化:
-
目标代码生成:生成汇编代码。
-
目标代码优化:
编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后的.s是ASCII码文件。
汇编:将汇编代码 .s 翻译成机器指令的 .o 或.obj 目标文件;
- 汇编过程调用汇编器AS来完成,是用于将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
- 汇编后的.o文件是纯二进制文件。
链接:产生 .out 或 .exe 可运行文件
汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。
详细来说,链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows的.exe文件或Linux的.out文件)等。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向。
最基本的链接叫做静态链接,就是将每个模块的源代码文件编译、汇编成目标文件(Linux:.o 文件;Windows:.obj文件),然后将目标文件和库一起链接形成最后的可执行文件(.exe或.out等)。库其实就是一组目标文件的包,就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。
链接分为两种:
静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
把目标程序运行时需要调用的函数代码直接链接到了生成的可执行文件中,程序在运行的时候不需要其他额外的库文件,且就算你去静态库把程序执行需要的库删掉也不会影响程序的运行,因为所需要的所有东西已经被链接到了链接阶段生成的可执行文件中。
Windows下以.lib为后缀,Linux下以.a为后缀。
动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
动态 “动” 在了程序在执行阶段需要去寻找相应的函数代码,即在程序运行时才会将程序安装模块链接在一起
具体来说,动态链接就是把调⽤的函数所在⽂件模块(DLL )和调⽤函数在⽂件中的位置等信息链接进目标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码,因此需要相应 DLL ⽂件的⽀持 。(Windows)
包含函数重定位信息的文件,在Windows下以.dll为后缀,Linux下以.so为后缀。
二者的区别:
静态链接是 将各个模块的obj和库链接成一个完整的可执行程序;而动态链接是程序在运行的时候寻找动态库的函数符号(重定位),即DLL不必被包含在最终的exe文件中;
链接使用工具不同:
-
静态链接由称为“链接器”的工具完成;
-
动态链接由操作系统在程序运行时完成链接;
库包含限制:
- 静态链接库中不能再包含其他的动态链接库或者静态库;
- 动态链接库中还可以再包含其他的动态或静态链接库。
运行速度:
-
静态链接运行速度快(因为执行过程中不用重定位),可独立运行
-
动态链接运行速度慢、不可独立运行
二者的优缺点:
静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
1.2. C++ 内存管理
C++的内存分布模型:
从高地址到低地址,一个程序由 内核空间、栈区、堆区、BSS段、数据段(data)、代码区组成。
常说的C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
可执行程序在运行时会多出两个区域:
-
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。栈从高地址向低地址增长。是一块连续的空间。栈一般分配几M大小的内存。
-
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。堆从低地址向高地址增长。一般可以分配几个G大小的内存。
-
在堆栈之间有一个 共享区(文件映射区)。
-
全局区/静态存储区(.BSS 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,程序中未初始化的全局变量和静态变量存放在.BSS 段中,已初始化的全局变量和静态变量存放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
-
代码区(.text 段):存放程序执行代码的一块内存区域。只读,不允许修改,但可以执行。编译后的二进制文件存放在这里。代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)
在linux下size命令可以查看一个可执行二进制文件基本情况:
1.3. 栈和堆的区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请。
- 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
- 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
- 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
- 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
此题总结:
1、申请方式的不同。 栈由系统自动分配,而堆是人为申请开辟;
2、申请大小的不同。 栈获得的空间较小,而堆获得的空间较大;
3、申请效率的不同。 栈由系统自动分配,速度较快,而堆一般速度比较慢;
4、 存储的内容不同。
栈在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
1.4. 变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别:
全局变量就是定义在函数外的变量。
局部变量就是函数内定义的变量。
静态变量就是加了static的变量。 例如:static int value = 1
各自存储的位置:
-
全局变量,存储在常量区(静态存储区)。
-
静态变量,存储在常量区(静态存储区)。
-
局部变量, 存储在栈区。
注意: 因为静态变量都在静态存储区(常量区),所以下次调用函数的时候还是能取到原来的值。
各自初始化的值:
-
局部变量, 存储在栈区。局部变量一般是不初始化的,
-
局部变量, 存储在栈区。全局变量和静态变量,都是初始化为0的,有一个初始值。
-
局部变量, 存储在栈区。如果是类变量,会调用默认构造函数初始化。
从作用域看:
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
- 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。会一直存在到程序结束。
- 静态全局变量:全局作用域+文件作用域,所以无法在其他文件中使用。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
- 局部变量:具有局部作用域。比如函数的参数,函数内的局部变量等等;它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被销毁,其所占用的内存也被收回。
- 静态局部变量:具有局部作用域。它只被初始化一次, 直到程序结束。自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:
- 静态存储区:全局变量,静态局部变量,静态全局变量。
- 栈:局部变量。
各自的应用场景:
-
局部变量就是我们经常用的,进入函数,逐个构造,最后统一销毁。
-
全局变量主要是用来给不同的文件之间进行通信。
-
静态变量:只在本文件中使用,局部静态变量在函数内起作用,可以作为一个计数器。
例子:
void func(){ static int count; count ++; } int main(int argc, char** argv){ for(int i = 0; i
说说静态变量在代码执行的什么阶段进行初始化?
static int value //静态变量初始化语句
对于C语言: 静态变量和全局变量均在编译期进行初始化,即初始化发生在任何代码执行之前。
对于C++: 静态变量和全局变量仅当首次被使用的时候才进行初始化。
助记: 如果你使用过C/C++你会发现,C语言要求在程序的最开头声明全部的变量,而C++则可以随时使用随时声明;这个规律是不是和答案类似呢?
1.5. 全局变量定义在头文件中有什么问题?
如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。
1.6. 内存对齐
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
- 结构体每个成员相对于结构体首地址的偏移量 (offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
- 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
进行内存对齐的原因:(主要是硬件设备方面的问题)
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap);
- 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
内存对齐的优点:
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
1.7. 什么是内存泄露
内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释:
- 并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。
- 常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。
- 使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete释放内存,否则这块内存就会造成内存泄漏。
- 指针重新赋值
char *p = (char *)malloc(10); char *p1 = (char *)malloc(10); p = np;
开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。
1.8. 怎么防止内存泄漏?内存泄漏检测工具的原理?
防止内存泄漏的方法:
- 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况)
- 智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。
VS下内存泄漏的检测方法(CRT):
在debug模式下以F5运行:
#define CRTDBG_MAP_ALLOC #include #include //在入口函数中包含 _CrtDumpMemoryLeaks(); //即可检测到内存泄露 //以如下测试函数为例: int main(){ char* pChars = new char[10]; _CrtDumpMemoryLeaks(); return 0; }
1.9. 智能指针有哪几种?智能指针的实现原理?
智能指针是为了解决动态内存分配时忘记释放内存导致的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 #include 头文件中。
C++11 引入了 3 个智能指针类型:
std::unique_ptr :独占资源所有权的指针。
std::shared_ptr :共享资源所有权的指针。
std::weak_ptr :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。
注:std::auto_ptr 已被废弃。
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
弱指针(weak_ptr):指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。
智能指针的实现原理: 计数原理。
1.10 智能指针应用举例
unique_ptr
unique_ptr 的使用比较简单,也是用得比较多的智能指针。当我们独占资源的所有权的时候,可以使用 unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。
- 自动管理内存
使用裸指针时,要记得释放内存。
{ int* p = new int(100); // ... delete p; // 要记得释放内存 }
使用 unique_ptr 自动管理内存。
{ std::unique_ptr uptr = std::make_unique(200); //... // 离开 uptr 的作用域的时候自动释放内存 }
- unique_ptr 是 move-only 的,也是实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象的方法
{ std::unique_ptr uptr = std::make_unique(200); std::unique_ptr uptr1 = uptr; // 编译错误,std::unique_ptr 是 move-only 的 std::unique_ptr uptr2 = std::move(uptr); assert(uptr == nullptr); }
- unique_ptr 可以指向一个数组
{ std::unique_ptr uptr = std::make_unique(10); for (int i = 0; i
- unique_ptr 可以指向一个数组
- unique_ptr 是 move-only 的,也是实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象的方法
-
-
-
-
-
-
-
-