C/C++程序的内存结构

  学习C/C++程序的内存结构,我们可以从两个方面来理解,首先对于一个可执行文件,在运行前程序还没有加载到内存中,内部已经分为3段:代码区(text)、数据区(data)和未初始化数据区(bss)。有时候,也会把数据区和未初始化数据区合起来称为静态区或全局区。运行时,程序被加载到内存中,除了文件分出的区域外,还会增加栈区和堆区。

运行前

可执行程序内部分段

  运行前,我们可以使用linux命令来查看可执行文件的内部分段。
  有以下的C++源码进行编译生成可执行文件main,源码如下所示:

#include<bits/stdc++.h>
using namespace std;
int main(){
    cout<<"Hello world!"<<endl;
    return 0;
}

  查看结果如下所示:

ubuntu@VM-4-8-ubuntu:~/mycode/learncpp/bin$ size main
   text    data     bss     dec     hex filename
  14854     864     280   15998    3e7e main

运行时

  运行时,程序被加载到内存中,执行时会用到栈区和堆区,包括局部变量或者函数调用过程中使用栈以及动态申请内存使用堆。

可执行程序在内存中的划分

  可执行程序在内存中的划分结果如下所示,包括了代码区、全局/静态区、堆区、栈区:

代码区

  代码区,存放的时CPU执行的机器指令。通常代码区是可以共享的,目的是为了频繁执行的程序在内存中只有一份;代码区通常是只读的,目的是为了防止程序以外地修改了指令。另外,代码区还包含了局部变量的相关信息。

全局/静态区

  全局区又包含了被初始化的全局变量、静态变量、常量数据和未初始化的全局变量、静态变量。一般为未初始化的数据区在程序开始之前会被初始化成0或者空(NULL)。

栈区

  栈区是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数、返回值、局部变量等。栈区在程序运行过程中实时加载和释放,因此局部变量的生存周期为在栈空间上存在的时间(从申请到释放)。

堆区

  堆区容量远远大于栈,但是没有栈那样先进后出的顺序。用于动态内存分配,通常情况下由程序员分配和释放,如果没有释放,那么程序结束时由操作系统回收。常用的动态内存分配有malloc/free和new/delete,两组动态内存分配操作使用时一定时成对使用的,即申请和释放有成对存在,不然可能会导致内存泄露。

自由存储区

  自由存储区(Free Store)是C++中引入的一个抽象概念,引用《exceptional C++》中的描述“The free store is one of the two dynamic memory areas, allocated/freed by new/delete.”。也就是说,new/delete进行的动态内存分配是在自由存储区上进行的。栈和堆属于计算机硬件范畴,是内存中具体的区域;自由存储区是一个抽象概念,其位置和实现由使用new/delete时的具体细节决定。new/delete默认是在堆上申请和释放内存,但是new可以定位申请内存,即可以指定一块内存区域申请内存。但是,定位new指定的内存可以是堆上的,也可以是栈上的,具体区别可以看后续代码实现。

编码实例分析

编码环境

  操作系统:Ubuntu 20.04 LTS
  编译器版本:g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

内存区域验证

  首先,验证全局变量、局部变量(栈)、堆内存空间的内存分布情况。

代码实现

  代码定义了全局变量、全局静态变量、全局const常量、全局字符串字面量,然后在主函数中输出这些变量或者常量的地址。另外,主函数中会调用两个函数:其中一个为localFunction,函数内部定义了局部变量、局部静态变量、局部const常量、局部字符串字面量,然后在函数内部输出这些量的地址;另一个为testHeap,函数内部定义了两个int类型的指针,分别使用new在堆上申请空间,然后输出两个指针的值。代码如下所示:

#include<bits/stdc++.h>
using namespace std;
//全局变量
int global_int_a = 0;
int global_int_b;
//全局静态变量
static int global_static_int_a = 0;
static int global_static_int_b;
//全局常量
const int global_const_int_a = 0;
const int global_const_int_b = -1;
char global_str[] = "hello world";
//函数
void localFunction(){
    //局部变量
    int local_int_a;
    int local_int_b;
    //局部静态变量
    static int local_static_int_a = 0;
    static int local_static_int_b;
    //局部常量
    const int local_const_int_a = 0;
    const int local_const_int_b = -1;
    char local_str[] = "hello world";
    //输出局部变量地址
    cout << "local_int_a: " << &local_int_a << endl;
    cout << "local_int_b: " << &local_int_b << endl;
    //输出局部静态变量地址
    cout << "local_static_int_a: " << &local_static_int_a << endl;
    cout << "local_static_int_b: " << &local_static_int_b << endl;
    //输出局部常量地址
    cout << "local_const_int_a: " << &local_const_int_a << endl;
    cout << "local_const_int_b: " << &local_const_int_b << endl;
    //输出局部字符串地址
    cout << "local_str: " << (void*)local_str << endl;
}

//测试堆的地址
void testHeap(){
    int *p = new int;
    int *q = new int;
    cout<<"address of p in testHeap: "<<p<<endl;
    cout<<"address of q in testHeap: "<<q<<endl;
    delete p;
    delete q;
}
//测试在栈上new
void testStack(){
    char buffer[1024];
    int *p = new(buffer) int;
    int *q = new(buffer+sizeof(int)) int;
    cout<<"address of buffer in testStack: "<<(void*)buffer<<endl;
    cout<<"address of p in testStack: "<<p<<endl;
    cout<<"address of q in testStack: "<<q<<endl;
}

//测试栈空间
void callFunction(int num){
    //输出num的地址
    cout<<"address of num: "<<&num<<endl;
    int a;
    int b;
    cout<<"address of a in callFunction: "<<&a<<endl;
    cout<<"address of b in callFunction: "<<&b<<endl;
}
void testStackSpace(int num){
    //输出num的地址
    cout<<"address of num: "<<&num<<endl;
    int a;
    int b;
    cout<<"address of a in testStackSpace: "<<&a<<endl;
    cout<<"address of b in testStackSpace: "<<&b<<endl;
    cout<<endl;
    callFunction(num);
}

int main(){
    //输出全局变量地址
    cout << "global_int_a: " << &global_int_a << endl;
    cout << "global_int_b: " << &global_int_b << endl;
    cout << endl;
    //输出全局静态变量地址
    cout << "global_static_int_a: " << &global_static_int_a << endl;
    cout << "global_static_int_b: " << &global_static_int_b << endl;
    cout << endl;
    //输出全局常量地址
    cout << "global_const_int_a: " << &global_const_int_a << endl;
    cout << "global_const_int_b: " << &global_const_int_b << endl;
    //输出全局字符串地址
    cout << "global_str: " << (void*)global_str << endl;
    cout << endl;
    localFunction();
    cout << endl;
    testHeap();
    cout << endl;
    return 0;
}

  运行结果:

global_int_a: 0x555555558154
global_int_b: 0x555555558158

global_static_int_a: 0x555555558160
global_static_int_b: 0x555555558164

global_const_int_a: 0x5555555560b0
global_const_int_b: 0x5555555560b4
global_str: 0x555555558010

local_int_a: 0x7fffffffdfec
local_int_b: 0x7fffffffdff0
local_static_int_a: 0x555555558168
local_static_int_b: 0x55555555816c
local_const_int_a: 0x7fffffffdff4
local_const_int_b: 0x7fffffffdff8
local_str: 0x7fffffffdffc

address of p in testHeap: 0x55555556b2c0
address of q in testHeap: 0x55555556b2e0

结果分析

  将运行结果中每个变量及其对应的地址使用表格进行汇总,然后按照从高地址到低地址降序排列如下图所示:

  可以看出,全局/静态区存放了所有的全局变量和静态变量;调用的函数内部的局部变量则是在栈区,地址段明显和全局/静态区有区别;而堆区在全局区和栈区中间。其中静态变量包含了全局静态变量和局部静态变量,由此可以理解,局部静态变量的生存期是整个程序运行期间都会保持的,只不过其访问权限局限在其所在的域中。

使用new分别在堆和栈上申请

  为了直观理解new在自由存储区上进行内存分配的概念,我们尝试使用new分别在栈和堆上申请内存空间。其中在堆上申请空间是使用new时候的默认操作,直接使用new关键字即可,此时申请的内存区域在堆上,需要程序员自己使用delete进行释放。但是在栈上申请空间则有一些不同,我们需要使用局部变量在栈上创建一片连续的内存空间,即直接定义一个局部的数组空间,在这片空间上使用new进行操作,此时申请内存的操作需要程序员自己进行地址偏移计算。不过在栈上使用new时,创建这片内存的时候是用过定义数组的方式实现的,当函数调用结束后,程序自动会从栈上释放这段空间,因此不需要手动释放。
  为什么能在堆上申请,还要麻烦在栈上使用new操作呢?
  首先,栈是程序编译过程中就已经分配好的空间,所以运行时几乎不需要额外的时间花销,而在堆上进行内存申请的时候,操作系统需要找到一块空间的大小合适的区域,最终释放回收时候还可能涉及到动态的合并。其次,栈的空间是连续的,而堆不一定是,寻址速度上栈比堆更快。另外,cpu有专门的寄存器来操作栈,而堆使用的是间接寻址。综上,栈能够达到比堆更快的速度,但是需要对内存的申请有更好的控制,可能会更复杂一些。
  使用new分别在堆上和栈上申请空间的代码如下所示,相关的函数已经在上一段代码中写好了,这里直接调用即可:

代码实现

//  省略重复的代码
// ...
int main(){
    //输出全局变量地址
    cout << "global_int_a: " << &global_int_a << endl;
    cout << "global_int_b: " << &global_int_b << endl;
    cout << endl;
    //输出全局静态变量地址
    cout << "global_static_int_a: " << &global_static_int_a << endl;
    cout << "global_static_int_b: " << &global_static_int_b << endl;
    cout << endl;
    //输出全局常量地址
    cout << "global_const_int_a: " << &global_const_int_a << endl;
    cout << "global_const_int_b: " << &global_const_int_b << endl;
    //输出全局字符串地址
    cout << "global_str: " << (void*)global_str << endl;
    cout << endl;
    testHeap();
    cout << endl;
    testStack();
    cout << endl;
    return 0;
}

  运行结果:

global_int_a: 0x555555558154
global_int_b: 0x555555558158

global_static_int_a: 0x555555558160
global_static_int_b: 0x555555558164

global_const_int_a: 0x5555555560b0
global_const_int_b: 0x5555555560b4
global_str: 0x555555558010

address of p in testHeap: 0x55555556b2c0
address of q in testHeap: 0x55555556b2e0

address of buffer in testStack: 0x7fffffffdc00
address of p in testStack: 0x7fffffffdc00
address of q in testStack: 0x7fffffffdc04

结果分析

  同样,将运行结果中每个变量及其对应的地址使用表格进行汇总排序如下所示:

  显而易见,在堆上申请的内存区域地址段和之前的测试一致,而使用定位new在栈区上申请的内存于上一个代码段中的栈区地址段一致。这也说明了new/delete在申请内存的时候并不一定是在堆或者栈上,而是和具体的使用方式有关。

栈区地址增长顺序分析

  根据之前的测试结果可以看出,在全局/静态区中地址的增长都是向从低到高,堆中的地址增长方向也是从低到高。一般来说栈区的地址增长方向应该是从高到低的,但是我们在第一段代码的运行结果中看到的局部变量a和b的增长方向确实从低到高。对于栈区的增长方向,我们再做一点深入的测试。这里使用两个函数, 第一个函数名字为testStackSpace,该函数运行过程中会接收一个参数,然后定义两个局部变量,输出参数和两个局部变量的地址,最后调用另外一个函数callFunction,此函数的操作类似,接收一个参数,然后定义两个局部变量,输出参数和两个局部变量的地址。
  两个函数的实现在第一段代码中,这里只在main函数中调用即可:

代码实现

//  省略重复的代码
// ...
int main(){
    //输出全局变量地址
    cout << "global_int_a: " << &global_int_a << endl;
    cout << "global_int_b: " << &global_int_b << endl;
    cout << endl;
    //输出全局静态变量地址
    cout << "global_static_int_a: " << &global_static_int_a << endl;
    cout << "global_static_int_b: " << &global_static_int_b << endl;
    cout << endl;
    //输出全局常量地址
    cout << "global_const_int_a: " << &global_const_int_a << endl;
    cout << "global_const_int_b: " << &global_const_int_b << endl;
    //输出全局字符串地址
    cout << "global_str: " << (void*)global_str << endl;
    cout << endl;
    testStackSpace(1);
    return 0;
}

  运行结果:

global_int_a: 0x555555558154
global_int_b: 0x555555558158

global_static_int_a: 0x555555558160
global_static_int_b: 0x555555558164

global_const_int_a: 0x5555555560b0
global_const_int_b: 0x5555555560b4
global_str: 0x555555558010

address of num: 0x7fffffffdffc
address of a in testStackSpace: 0x7fffffffe000
address of b in testStackSpace: 0x7fffffffe004

address of num: 0x7fffffffdfcc
address of a in callFunction: 0x7fffffffdfd0
address of b in callFunction: 0x7fffffffdfd4

结果分析

  将运行结果中每个变量及其对应的地址汇总排序如下所示:

  我们可以发现由于testStackSpace调用了callFunction,也就是说操作系统应该先在栈区给testStackSpace分配空间,然后才是callFunction。通过表格,我们可以看到,testStackSpace的参数和局部变量地址比callFunciton的更大,也就是说在函数调用过程中的栈区地址增长方向是从高到低的,但是在某一个函数调用的内部,我们注意到局部变量的地址增长方向则是从低到高(有博客说这种顺序和不同编译器采用的策略不同,暂时不验证是否如此)。


当珍惜每一片时光~