在计算机总线一文中介绍了内存地址空间,现在说明在C++中如何进行内存分配。

一个C/C++的程序占用的内存分为以下几个部分:

1.栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等,操作方式类似与数据结构的栈。

2.堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS释放。

3.全局区(static):也叫静态数据内存空间,存储全局变量和静态变量,两个存在一起,但是已经初始化的放一起,没有初始化的放一起。程序结束后由系统释放。

4.文字常量区:常量字符串就是放在这,程序结束后由系统释放。

5.程序代码区:存放函数体的二进制代码。

但是还有的版本是1.2.3相同,4.5是自由存储区和常量存储区

我查了整整一天,发现后面这种其实更符合官方文档。后文会再谈。我这先采用自己的视角探讨内存分配。

可执行程序的文件结构

C程序

我在Deepin中用gcc(6.2.0)编译一个hello程序,再用size命令查看其可执行程序的结构,如图:

C结构

可以看到,存储时(未加载到内存中)分为代码区(text),数据区(data),未初始化数据区(bss),后面两个是总和2192和2192的十六进制大小。

(1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

(2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
include<stdio.h>
int global=100;
static int x=50;
const pi=3.14;
int main()
{
......
}
void fun1(int x)
{
static int count=0;
}

全局变量global,x,全局静态变量x,局部静态变量count,常量piglobalx的区别在于global可以在该程序的其他文件中使用,而x只在这这个源代码中使用。static有限定只能在当前文件中被调用的作用。

(3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如声明一个数组,但没有赋值:

1
long sun[1000];

longlong int的简写。

C++程序

在Windows10中用g++(6.3.0)编译C++程序,其结构是和C的一样的

c++结构

运行时的结构

比较

从上图可以看出,内存空间被分为五个部分。

(1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。

代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。

(2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。

(3)未初始化数据区(BSS)。在运行时改变其值。

(4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。

(5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

之所以分成这么多个区域,主要基于以下考虑:

一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

堆区由用户自由分配,以便管理。

C/C++语言中典型的存储类型有auto, extern, register, static 这四种,但是是有区别的,C++还有其他两种。存储类型说明了该变量要在进程的哪一个段中分配内存空间,可以为变量分配内存存储空间的有数据区、BBS区、栈区、堆区。下面来一一举例看一下这几个存储类型:

1.auto存储类型

auto只能用来标识局部变量(’{}’中的部分)的存储类型,对于局部变量,auto是默认的存储类型,不需要显示的指定。因此,auto标识的变量存储在栈区中。示例如下:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>  

int main(void)
{
auto int i=1; //显示指定变量的存储类型
int j=2;

printf("i=%d\tj=%d\n",i,j);

return 0;
}

2.extern存储类型

extern用来声明在当前文件中引用在当前项目中的其它文件中定义的全局变量。如果全局变量未被初始化,那么将被存在BBS区中,且在编译时,自动将其值赋值为0,如果已经被初始化,那么就被存在数据区中。全局变量,不管是否被初始化,其生命周期都是整个程序运行过程中,为了节省内存空间,在当前文件中使用extern来声明其它文件中定义的全局变量时,就不会再为其分配内存空间。

示例如下:

1
2
3
4
5
6
7
8
#include <stdio.h>  

int i=5; //定义全局变量,并初始化

void test(void)
{
printf("in subfunction i=%d\n",i);
}
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>  

extern i; //声明引用全局变量i

int main(void)
{
printf("in main i=%d\n",i);
test();
return 0;
}
1
2
$ gcc -o test test.c file.c  #编译连接  
$ ./test #运行
1
2
3
4
结果:  

in main i=5
in subfunction i=5

3.register存储类型

声明为register的变量在由内存调入到CPU寄存器后,则常驻在CPU的寄存器中,因此访问register变量将在很大程度上提高效率,因为省去了变量由内存调入到寄存器过程中的好几个指令周期。如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>  

int main(void)
{
register int i,sum=0;

for(i=0;i<10;i++)
sum=sum+1;

printf("%d\n",sum);

return 0;
}

但是在 C++11之前,这个关键字在C++中的用法始终未变,只是随着硬件和编译器变得越来越复杂,这种
提示表明变量用得很多,编译器可对其做特殊处理。而且寄存器的数量是有限的!

4.static存储类型

被声明为静态类型的变量,无论是全局的还是局部的,都存储在数据区中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{}内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。静态变量只能够初始化一次。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>  

int sum(int a)
{
auto int c=0;
static int b=5;

c++;
b++;

printf("a=%d,\tc=%d,\tb=%d\t",a,c,b);

return (a+b+c);
}

int main()
{
int i;
int a=2;
for(i=0;i<5;i++)
printf("sum(a)=%d\n",sum(a));
return 0;
}
1
2
3
4
5
6
7
8
$ gcc -o test test.c  
$ ./test
a=2, c=1, b=6 sum(a)=9
a=2, c=1, b=7 sum(a)=10
a=2, c=1, b=8 sum(a)=11
a=2, c=1, b=9 sum(a)=12
a=2, c=1, b=10 sum(a)=13

5.字符串常量

字符串常量存储在数据区中,其生存期为整个程序运行时间,但作用域为当前文件,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>  

char *a="hello";

void test()
{
char *c="hello";

if(a==c)
printf("yes,a==c\n");
else
printf("no,a!=c\n");
}

int main()
{
char *b="hello";
char *d="hello2";

if(a==b)
printf("yes,a==b\n");
else
printf("no,a!=b\n");

test();

if(a==d)
printf("yes,a==d\n");
else
printf("no,a!=d\n");

return 0;
}
1
2
3
4
5
$ gcc -o test test.c  
$ ./test
yes,a==b
yes,a==c
no,a!=d

由于1~4的类型在C/C++中是一样的,所以我用的全是C的代码。

但是在C++11中,autoregister两种都有了新的含义!!!

C++11新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。为此,它重新定义了 auto 的含义。auto是一个C 语言关键字,但很少使用,有关其以前的含义,请参阅第9 章。在初始化声明中, 如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:

1
2
3
auto n = 100// n is int 
auto x = 1.5// x is double
auto y = 1.3el2L; // y is long double

然而,自动推断类型并非为这种简单情况而设计的;事实上,如果将其用于这种简单情形,甚至可能让您误入歧途。例如,假设您要将x、y 和 z 都指定为double类型,并编写了如下代码:

1
2
3
auto x = 0.0// ok, x is double because 0.0 is double 
double y = 0// ok, 0 automatically converted to 0.0
auto z = 0; // oops, z is int because 0 is int

显式地声明类型时,将变量初始化0 (而不是0.0)不会导致任何问题,但采用自动类型推断时,这却会导致问题。
处理复杂类型,如标准模块库(STL)中的类型时,自动类型推断的有时才能显现出来。例如,对于下述C++98代码:

1
2
std::vector<double> scores; 
std::vector<double>::iterator pv = scores.begin();

c++n允许您将其重写为下面这样:

1
2
std::vector<double> scores; 
auto pv = scores.begin();

本书后面讨论相关的主题时,将再次提到

在 C++11之前,这个关键字在C++中的用法始终未变,只是随着硬件和编译器变得越来越复杂,这种提示表明变量用得很多,编译器可对其做特殊处理。在 C++11中,这种提示作用也失去了,关键字register 只是显式地指出变量是自动的。鉴于关键字register只能用于原本就是自动的变量,使用它的唯一原因是, 指出程序员想使用一个自动变量,这个变量的名称可能与外部变量相同。*这与auto以前的用途完全相同。 然而,保留关键字register的重要原因是,避免使用了该关键字的现有代码非法。*

简而言之,新auto用于自动推断类型,新register的用法则与旧auto一样。

什么是栈?

由于自动变量的数目是随着函数的开始和结束而增减的(上面是main函数),因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,将其视为栈,以管理变量的增减。之所以叫栈,是因为新数据被象征性的放在旧数据的上面,即相邻的内存单元。当程序使用完后,将其从栈中删除。栈的默认长度取决于实现,但是可以通过编译器修改。程序使用两个指针来控制栈,一个指向栈底—栈开始的地方;另一个指向栈顶—下一个内存单元。当函数被调用时,其自动变量被加入栈,栈顶指针指向下一个可用的内存单元。函数结束后,栈顶指针被重置为函数被调用之前指向的内存单元,从而释放函数调用使用的自动变量的内存。

栈是LIFO(后进先出)的,即后加入栈中的变量先被弹出。这种设计简化了参数传递。函数调用将其参数的值放在栈顶,然后重新设置栈顶指针。被调用的函数根据其形参描述来确定每个参数的地址。例如,图9.3表明,函数fib()被调用时,传递一个2 字节的int和一个4 字节的long。这些值被加入到栈中。当 fib()开始执行时,它将名称real和 tell同这两个值关联起来。当 fib()结束时,栈顶指针重新指向以 前的位置。新值没有被删除,但不再被标记,它们所占据的空间将被下一个将值加入到栈中的函数调用所
使用(图9.3做了简化,因为函数调用可能传递其他信息,如返回地址)。

9.3

注意,栈是从高地址向低地址增长的,大小是有限的。

栈和堆的区别

前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)。栈和堆的主要区别有以下几点:

(1)管理方式不同。

栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

(2)空间大小不同。

栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

(3)是否产生碎片。

对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

(4)增长方向不同。

堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

(5)分配方式不同。

堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

(6)分配效率不同。

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。

自由存储区是什么?

内存划分 运行时结构
栈(stack)
堆(heap)
全局/静态存储区 未初始化数据(BSS)
自由存储区 数据区(data)
常量存储区 代码区

常量存储区和全局/静态存储区对应着数据区。BSS和代码区没有对应。而自由存储区在某种程度上可以视为堆的一部分。

“free store” VS “heap”

当我问你C++的内存布局时,你大概会回答:

“在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区”。

如果我接着问你自由存储区与堆有什么区别,你或许这样回答:

“malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在自由存储区上,使用delete来释放。”

这样听起来似乎也没错,但如果我接着问:

自由存储区与堆是两块不同的内存区域吗?它们有可能相同吗?

你可能就懵了。

事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是new/delete与malloc/free。然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么请问:借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。**基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。**但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

问题的来源

再回过头来来看看这个问题的起源在哪里。最先我们使用C语言的时候,并没有这样的争议,很明确地知道malloc/free是在堆上进行内存操作。直到我们在Bjarne Stroustrup的书籍中数次看到free store (自由存储区),说实话,我一直把自由存储区等价于堆。而在Herb Sutter的《exceptional C++》中,明确指出了free store(自由存储区) 与 heap(堆) 是有区别的。关于自由存储区与堆是否等价的问题讨论,大概就是从这里开始的:

Free Store
The free store is one of the two dynamic memory areas, allocated/freed by new/delete. Object lifetime can be less than the time the storage is allocated; that is, free store objects can have memory allocated without being immediately initialized, and can be destroyed without the memory being immediately deallocated. During the period when the storage is allocated but outside the object’s lifetime, the storage may be accessed and manipulated through a void but none of the proto-object’s nonstatic members or member functions may be accessed, have their addresses taken, or be otherwise manipulated.*

Heap
The heap is the other dynamic memory area, allocated/freed by malloc/free and their variants. Note that while the default global new and delete might be implemented in terms of malloc and free by a particular compiler, the heap is not the same as free store and memory allocated in one area cannot be safely deallocated in the other. Memory allocated from the heap can be used for objects of class type by placement-new construction and explicit destruction. If so used, the notes about free store object lifetime apply similarly here.

来源:http://www.gotw.ca/gotw/009.htm

作者也指出,之所以把堆与自由存储区要分开来,是因为在C++标准草案中关于这两种区域是否有联系的问题一直很谨慎地没有给予详细说明,而且特定情况下new和delete是按照malloc和free来实现,或者说是放过来malloc和free是按照new和delete来实现的也没有定论。这两种内存区域的运作方式不同、访问方式不同,所以应该被当成不一样的东西来使用。

结论

  • 自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。
  • new所申请的内存区域在C++中称为自由存储区。藉由堆实现的自由存储,可以说new所申请的内存区域在堆上。
  • 堆与自由存储区还是有区别的,它们并非等价。

假如你来自C语言,从没接触过C++;或者说你一开始就熟悉C++的自由储存概念,而从没听说过C语言的malloc,可能你就不会陷入“自由存储区与堆好像一样,好像又不同”这样的迷惑之中。这就像Bjarne Stroustrup所说的:

usually because they come from a different language background.

大概只是语言背景不同罢了。

总结

分配方式1 分配方式2 运行时结构
自由存储区 文字常量区(所有常量) BSS
全局/静态存储区 全局/静态存储区 Data
常量存储区(包括字符) 程序代码区 代码区

21相比就是把自由存储区加到了堆中,再加了个代码区。两种方式孰优孰劣?