面经 C++ 编译和底层
C++源文件从文本到可执行文件经历的过程
四个过程。
- 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译 .i 文件
- 编译阶段:对预编译文件进行语法分析(关键字、标识符),语义分析等,转换成特定汇编代码,生成.s汇编文件
- 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件 .o 文件
- 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件。连接过程主要解决源代码的相互依赖问题,分为静态连接和动态连接。
#include尖括号和双引号的区别
编译器预处理阶段查找头文件的路径不一样。
- 尖括号:直接从系统目录里查找 头文件。系统目录主要有:编译时 -I 参数指定的头文件搜索路径,系统变量
CPLUS_INCLUDE_PATH
/C_INCLUDE_PATH
指定的头文件路径 - 双引号:先从当前目录搜索头文件,然后从系统目录中搜索
include头文件的顺序
malloc原理,以及系统调用
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过显式链表,根据待分配内存大小在直接从合适的链表中寻找空闲块;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
C++的内存管理
对于32位CPU可寻址4G线性空间,其中0-3G是用户态空间,3-4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中,其逻辑地址其划分如下: (前三个为静态区 后 三个为动态区)
- 代码段:用于存放代码和字符常量
- 数据段:用于存放已经初始化的全局变量和局部静态变量
- bss段:用于存放未初始化的全局变量和局部静态变量,或初始化未0的全局局部静态变量
- 堆区:用于程序运行过程中,用户自行动态分配的管理的内存区域
- 栈区:用于函数调用时,存放函数的局部变量,函数返回地址,返回值等,由操作系统分配和管理。在创建进程时会有一个最大栈大小
- mmap映射区:用于存储动态库等文件映射,同时malloc也可以通过系统调用申请大内存
如何判断内存泄漏
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
内存泄漏的分类:
- 堆内存泄漏 (Heap leak)。堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
- 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
- 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
什么时候会发生段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
- 使用野指针
- 试图修改字符串常量的内容
说一下共享内存相关api
Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h
中。
1 | /* 新建共享内存: |
shmget 将键和共享内存 绑定,返回的是一个类似文件句柄的东西(只不过这个文件的存储载体是内存);shmat 将前面的文件映射至当前进程的 文件映射区,返回映射后的地址,这个地址是用户地址空间文件映射区的地址。 通过 shmctl 读写共享内存
new 和 malloc 的区别
- new 是 c++ 的一个关键字,而 malloc 是函数。
- new 会根据对象的大小自动分配相同大小的内存,并且会默认调用类的构造函数,而malloc只能自己使用sizeof设定需要分配大小
- new分配的后返回的是指向类的指针,而 malloc返回的是void指针,需要自行进行类型转换
- new分配使用delete释放,malloc与之对应的是free
- new如果内存申请失败会抛出异常,而malloc则返回空指针
- malloc分配的内存不够时,可以调用 relloc 进行扩容,而new没有这个操作
- 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
在默认情况下,new操作的自由存储区和堆等价,但是可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。
C++如何处理内存泄漏
- 首先可以 使用 编辑器的 查找功能查看是否使用了 成对的 new 和 delete 语句。这样能有个粗略的判断。
- 然后话可以在 类中增加一个静态成员变量,在构造函数中 加,在析构函数中减;最后程序结束输出该计数值,应该为0才对,但是这样只能解决自身编写的类来检查
- 有可能是 析构函数不是虚函数,就无法调用子类的析构函数释放子类申请的空间,还要注意子类中有没有调用父类的析构函数释放父类中申请空间。
- 认真检查代码逻辑,是否有提前跳出的位置。
- 可以使用智能指针代替指针
使用varglind,mtrace检测
C++函数编译
无论是全局函数,还是类中的成员函数,C++都是将函数名重新编码为一个新的函数名。如果在类中的函数,它会根据类的名字和函数名字共同编码生成一个新的名字,保证全局所有函数的名字唯一性。而类的成员函数在编译阶段如何和类以及成员变量关联的呢?方法就是给成员函数添加一个隐藏的对象指针,函数内部访问类成员时通过对象指针访问,这样就将成员函数和成员变量关联了起来。同理,我们常用的 bin 的绑定方法,绑定类成员函数的方法,可以指定不同的对象指针,他们的成员函数的值可能不同,就实现了不同对象的成员函数的绑定。