【C++】内存管理、底层


C/C++程序编译流程(预处理->编译->汇编->链接)

图片名称

1. 预处理

  预处理相当于根据预处理指令组装新的C/C ++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。

读取C/C ++源程序,对其中的伪指令(以#开头的指令)进行处理

  • ①将所有的“#define”删除,并且展开所有的宏定义
  • ②处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
  • ③处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
    (注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件)

删除所有的注释

添加行号和文件名标识

  • 以便于编译时编译器产生调试用的行号信息及用于编译时产生的编译错误或警告时能够显示行号

保留所有的#pragma编译器指令

2. 编译

将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码.s文件。

3. 汇编

将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可。

4. 链接

通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序.exe。

由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

类的对象存储空间?

  • 非静态成员变量的数据类型大小之和。
  • 编译器加入的额外成员变量(如指向虚函数表的指针)。
  • 为了边缘对齐优化加入的padding(填充)。

空类(无非静态成员变量)的对象的size为1, 当作为基类时, size为0.

C++的内存分区?

C++中的内存分区,分别是堆、栈、全局/静态存储区、常量存储区和代码区。

图片名称
  • :指那些由编译器在需要的时候分配,不需要时手动清除的变量所在的存储区,效率高,分配的内存空间有限,形参和局部变量分配在栈区,栈是向低地址生长的数据结构,是一块连续的内存
  • :就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0
  • 常量存储区:这是一块比较特殊的存储区,这里面存放的是字符串常量、onst修饰的全局变量存储在常量区,const修饰的局部变量在栈区,不允许修改
  • 代码区:存放函数体的二进制代码

C++中类的成员变量和成员函数内存分布情况?

  • C语言的结构体只有成员变量
  • 一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。(在定义类对象的同时这些成员变量也就被定义了)
  • 静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态成员变量。
  • 所有函数都存放在代码区,静态函数也不例外。有人一看到 static 这个单词就主观的认为是存放在全局存储区,那是不对的。

this指针

  • this指针是类的指针,指向对象的首地址。
  • this指针只能在非静态成员函数中使用,在全局函数、静态成员函数中都不能用this。
  • this指针只有在非静态成员函数中才有定义,你获得一个对象后,也不能通过对象使用this指针,且存储位置会因编译器不同而不同。可能是栈,也可能是寄存器,甚至全局变量。
  • this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。

内存泄漏的后果?如何监测?解决方法?

1) 内存泄漏

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制

2) 后果

只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种征兆:性能下降到内存逐渐用完,导致另一个程序失败;

3) 如何排除

使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误;
调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口),综合分析内存泄漏的原因,排除内存泄漏。

4) 解决方法

智能指针。

5) 检查、定位内存泄漏

  • 检查方法:在main函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:
  • 输出这样的格式{453}normal block at 0x02432CA8,868 bytes long
  • 被{}包围的453就是我们需要的内存泄漏定位值,868 bytes long就是说这个地方有868比特内存没有释放。
  • 定位代码位置
  • 在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include <crtdbg.h>

在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

  • 在类对象的内存空间中,只有成员变量和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码区中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作成员变量,调用虚函数等,就会出现不可预期的问题。
  • delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

空类的大小是多少?

  1. C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
  2. C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址
  3. 带有虚函数的C++类大小不为1,因为每一个对象会有一个虚函数表指针指向虚函数表,具体大小根据指针大小确定;
  4. C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
class A {};
int main(){
  cout<<sizeof(A)<<endl;// 输出 1;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 1;
  return 0;
}
  • 空类的大小是1, 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。当该空白类作为基类时,该类的大小就优化为0了,子类的大小就是子类本身的大小。这就是所谓的空白基类最优化。
  • 空类的实例大小就是类的大小,所以sizeof(a)=1字节,如果a是指针,则sizeof(a)就是指针的大小,即4字节。
class A { virtual Fun(){} };
int main(){
  cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器);
  A a; 
  cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器);
  return 0;
}
//因为有虚函数的类对象中都有一个虚函数表指针 __vptr,其大小是4字节


class A { static int a; int b; };;
int main(){
  cout<<sizeof(A)<<endl;// 输出 4;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 4;
  return 0;
}
//静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类大小

this指针调用成员变量时,堆栈会发生什么变化?

  • 当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给非静态成员函数作为隐含参数传递给函数,这个隐含参数就是this指针
  • 即使你并没有写this指针,编译器在链接时也会加上this的,对各成员的访问都是通过this

例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看this指针来查看具体是哪个对象在调用。this指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址值入栈

类对象的大小受哪些因素影响?

  1. 类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
  2. 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
  3. 虚函数的话,会在类对象插入虚函数表指针,加上指针大小;
  4. 当该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中,也会对派生类进行扩展。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇光_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值