变量去哪了

262 阅读4分钟

前言:当你敲下一个变量的时候,程序会将它放到哪?当你粗暴地使用着指针的时候,为何会导致内存泄露?为了解开上述疑惑,本文便总结了 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;
};

参考资料

详解操作系统分配内存

C++内存分配机制

C++中的字节对齐