前言:当你敲下一个变量的时候,程序会将它放到哪?当你粗暴地使用着指针的时候,为何会导致内存泄露?为了解开上述疑惑,本文便总结了 C++ 内存管理的相关知识。
何为内存
在了解内存管理的相关知识前,我们首先需要知道什么是内存?
内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。(摘自百度百科)
系统与内存的互动
从操作系统层次来看,内存的最小访问单位是字节 ,也就是8bit。
通常我们所说的计算机系统是32位的总线,所谓的32位总线就是说一次读写可以从内存当中读或者写32位(也就是4字节)。
程序有关的内存区域
当我们运行一个 cpp 程序时,系统会给程序分配一块内存,这块内存分成了五个区域:
栈
分配给函数局部变量的存储单元,函数结束后,该变量的存储单元自动释放,效率高,分配的空间有限。
堆
程序中由 new 创建,由 delete 释放的动态内存单元。如果用户不释放该内存,程序结束时,系统会自动回收。
自由存储区
由 malloc 创建,由free释放的动态内存单元,与堆类似。
全局(静态存储区)
全局变量和静态变量占一块内存空间。
常量存储区
存储常量,内容不允许更改。
局部变量的内存分配
一个例子供大家参考,由图可以知道,macOS 的编译系统,变量的内存分配是从高到低。

系统字节序
首先,看完上图的内存分配以后,我们可以知道的是,0x7ffeecf12740 到 0x7ffeecf12743 这四个字节的内存分配给了 int 类型的变量 c,那我又是怎么知道 0x05 是存在了 740 而不是 743?
这里涉及到的概念为字节序,内存中存储这四个字节有两种方法:一种是将低序字节存储在起始位置,这称为小端字节序,另一种方法是将高序字节存储在起始地址,这称为大端字节序(网络字节序通常使用大端字节序)。

知道大小端字节序后,我们可以对我们系统进行测试,结果我们的系统(macOS)存储方式属于小端字节序,由此,0x05 存在 740 是正确的。

常见类型变量所占内存大小


字节对齐
当我们理解了上面的内容以后,兴冲冲地拿出一道内存相关的面试题来进行小试牛刀的时候,却又发现怎么都算不对答案!当然不是上面讲的东西不对,而是因为面试题最喜欢考的是字节对齐问题。
计算机存储系统中以 Byte 为单位存储数据,不同数据类型所占的空间不同,如:整型(int)数据占4个字节,字符型(char)数据占一个字节,短整型(short)数据占两个字节,等等。计算机为了快速的读写数据,默认情况下将数据存放在某个地址的起始位置,如:整型数据(int)默认存储在地址能被4整除的起始位置,字符型数据(char)可以存放在任何地址位置(被1整除),短整型(short)数据存储在地址能被2整除的起始位置。这就是默认字节对齐方式。
很显然默认对齐方式会浪费很多空间,例如如下结构:
struct student
{
char name[5];
int num;
short score;
}
这个结构本来只用了 11 个字节的空间,但是由于 int 型默认4字节对齐,存放在地址能被 4 整除的起始位置,即:如果 name[5] 从0开始存放,它占 5bytes,而 num 则从第8(偏移量)个字节开始存放。所以 sizeof(student) = 16。于是中间空出几个字节闲置着。但这样便于计算机快速读写数据,是一种以空间换取时间的方式。其数据对齐如下:
|char|char|char|char|char|----|----|----|--------int--------|--short--|----|----|
动态内存管理
在我们使用动态内存时最最最最容易出现错误的地方,是在类的构造函数中为成员指针变量申请了内存,而在对象销毁时却忘了释放这个成员指针变量的内存。
正确的思路应该这么写:
class A() {
public:
A() {
p = new B(); // 在构造函数中向堆申请内存
}
~A() {
delete p; // 在析构函数中应该主动释放内存
}
private:
B *p;
};