Tal*_*yed 149 c c++ memory memory-management terminology
在C和C++等编程语言中,人们经常提到静态和动态内存分配.我理解这个概念,但短语"所有内存在编译期间被分配(保留)"总是让我感到困惑.
据我所知,编译将高级C/C++代码转换为机器语言并输出可执行文件.如何在编译文件中"分配"内存?是不是内存总是在RAM中分配所有虚拟内存管理的东西?
根据定义,内存分配不是运行时概念吗?
如果我在我的C/C++代码中创建一个1KB静态分配的变量,那么这会增加可执行文件的大小吗?
这是在"静态分配"标题下使用该短语的页面之一.
Man*_*726 177
在编译时分配的内存意味着编译器在编译时解析,其中某些内容将在进程内存映射中分配.
例如,考虑一个全局数组:
int array[100];
Run Code Online (Sandbox Code Playgroud)
编译器在编译时知道数组的大小和大小int,因此它在编译时知道数组的整个大小.默认情况下,全局变量也具有静态存储持续时间:它分配在进程内存空间的静态内存区域(.data/.bss部分).给定该信息,编译器在编译期间决定该阵列将在该静态存储器区域的哪个地址.
当然,内存地址是虚拟地址.该程序假定它有自己的整个存储空间(例如,从0x00000000到0xFFFFFFFF).这就是为什么编译器可以做一些假设,比如"好吧,数组将在地址0x00A33211".在运行时,MMU和OS将地址转换为实际/硬件地址.
值初始化静态存储的东西有点不同.例如:
int array[] = { 1 , 2 , 3 , 4 };
Run Code Online (Sandbox Code Playgroud)
在我们的第一个示例中,编译器仅确定将分配数组的位置,将该信息存储在可执行文件中.
在值初始化的东西的情况下,编译器还将数组的初始值注入到可执行文件中,并添加代码,告诉程序加载器在程序启动时的数组分配之后,应该用这些值填充数组.
以下是编译器生成的程序集的两个示例(带有x86目标的GCC4.8.1):
C++代码:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
Run Code Online (Sandbox Code Playgroud)
输出组件:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
Run Code Online (Sandbox Code Playgroud)
如您所见,这些值直接注入到程序集中.在数组中a,编译器生成16个字节的零初始化,因为标准表示静态存储的东西应该默认初始化为零:
8.5.9(初始化程序)[注意]:
在进行任何其他初始化之前,每个静态存储持续时间的对象在程序启动时都是零初始化的.在某些情况下,稍后会进行额外的初始化.
我总是建议人们反汇编他们的代码,看看编译器对C++代码的真正作用.这适用于存储类/持续时间(如此问题)到高级编译器优化.您可以指示编译器生成程序集,但有一些很好的工具可以在Internet上以友好的方式执行此操作.我最喜欢的是GCC Explorer.
mah*_*mah 26
在编译时分配的内存只是意味着在运行时不会进一步分配 - 不调用malloc,new或其他动态分配方法.即使您不需要所有内存,也会有固定的内存使用量.
根据定义,内存分配不是运行时概念吗?
在运行时之前没有使用内存,但是在执行之前立即开始分配由系统处理.
如果我在我的C/C++代码中创建一个1KB静态分配的变量,那么这会增加可执行文件的大小吗?
简单地声明静态不会增加可执行文件的大小超过几个字节.使用非零的初始值声明它将(为了保持该初始值).相反,链接器只是将这个1KB的数量添加到系统的加载器在执行之前立即为您创建的内存需求.
fed*_*024 23
在编译时分配的内存意味着当您加载程序时,将立即分配内存的某些部分,并在编译时确定此分配的大小和(相对)位置.
char a[32];
char b;
char c;
Run Code Online (Sandbox Code Playgroud)
这三个变量"在编译时分配",这意味着编译器在编译时计算它们的大小(固定).该变量a将是内存中的偏移量,比方说,指向地址0,b将指向地址33和c34(假设没有对齐优化).因此,分配1Kb的静态数据不会增加代码的大小,因为它只会改变其中的偏移量.实际空间将在加载时分配.
实内存分配总是在运行时发生,因为内核需要跟踪它并更新其内部数据结构(为每个进程分配多少内存,页面等).不同之处在于编译器已经知道您要使用的每个数据的大小,并且只要程序执行就会分配.
还要记住,我们正在讨论相对地址.变量所在的实际地址将不同.在加载时,内核将为进程保留一些内存,比如在地址处x,并且可执行文件中包含的所有硬编码地址将按x字节递增,因此a示例中的变量将位于地址x,b处于地址x+33和等等.
Eli*_*gem 17
在堆栈上添加占用N个字节的变量不会(必然)将bin的大小增加N个字节.事实上,它在大多数情况下会增加几个字节.
让我们从一个示例开始,例如如何在代码中添加1000个字符将以线性方式增加bin的大小.
如果1k是一个字符串,一千个字符,这是声明的
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
Run Code Online (Sandbox Code Playgroud)
然后你就到了vim your_compiled_bin,你实际上能够在某个地方看到那个字符串.在这种情况下,是的:可执行文件将大1 k,因为它包含完整的字符串.
但是,如果您在堆栈上分配ints,chars或longs 数组并将其分配给循环,则沿着这些行分配
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
Run Code Online (Sandbox Code Playgroud)
然后,不:它不会增加bin ... 1000*sizeof(int)
在编译时通过Allocation意味着你现在理解它意味着(基于你的注释):编译的bin包含系统需要知道多少内存的信息执行时需要什么功能/块,以及应用程序所需的堆栈大小信息.这就是系统在执行你的bin时会分配的内容,你的程序就变成了一个进程(好吧,你的bin的执行就是这个过程......好吧,你得到了我所说的).
当然,我不是在这里绘制全部图片:bin包含有关bin实际需要多大堆栈的信息.根据这些信息(除其他事项外),系统将保留一块称为堆栈的内存,程序可以自由统治.当启动进程(正在执行bin的结果)时,堆栈内存仍由系统分配.然后,该过程为您管理堆栈内存.当调用/执行函数或循环(任何类型的块)时,该块的本地变量被推送到堆栈,并且它们被移除(堆栈存储器被"释放"可以这么说)以供其他人使用功能/块.因此声明int some_array[100]只会向bin添加几个字节的附加信息,告诉系统函数X将需要100*sizeof(int)额外的一些簿记空间.
sup*_*cat 15
在许多平台上,每个模块中的所有全局或静态分配将由编译器合并为三个或更少的合并分配(一个用于未初始化的数据(通常称为"bss"),一个用于初始化的可写数据(通常称为"数据") ),一个用于常量数据("const")),并且程序中每种类型的所有全局或静态分配将由链接器合并为每种类型的一个全局.例如,假设int是四个字节,模块将以下作为其唯一的静态分配:
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
Run Code Online (Sandbox Code Playgroud)
它会告诉链接器它需要208字节用于bss,16字节用于"数据",28字节用于"const".此外,对变量的任何引用都将替换为区域选择器和偏移量,因此a,b,c,d和e将被bss + 0,const + 0,bss + 4,const + 24,数据替换分别为+0或bss + 204.
当一个程序被链接时,所有模块中的所有bss区域都被连接在一起; 同样是数据和常量区域.对于每个模块,任何bss相对变量的地址将增加所有前面模块的bss区域的大小(同样,数据和const也是如此).因此,当链接器完成时,任何程序将具有一个bss分配,一个数据分配和一个const分配.
加载程序时,通常会发生以下四种情况之一,具体取决于平台:
可执行文件将指示每种数据需要多少字节,并且 - 对于初始化数据区域,可以找到初始内容.它还将包括使用bss,data或const相对地址的所有指令的列表.操作系统或加载程序将为每个区域分配适当的空间量,然后将该区域的起始地址添加到需要它的每条指令中.
操作系统将分配一块内存来保存所有三种数据,并为应用程序提供指向该内存块的指针.任何使用静态或全局数据的代码都会相对于该指针取消引用它(在许多情况下,指针将在应用程序的生命周期中存储在寄存器中).
操作系统最初不会将任何内存分配给应用程序,除了保存其二进制代码的内容,但应用程序所做的第一件事就是从操作系统请求合适的分配,它将永远保存在寄存器中.
操作系统最初不会为应用程序分配空间,但应用程序将在启动时请求合适的分配(如上所述).该应用程序将包含一个指令列表,其中包含需要更新以反映内存分配位置的地址(与第一种样式一样),但是应用程序将包含足够的代码来修补自身,而不是通过OS加载程序修补应用程序. .
所有四种方法都有优点和缺点.但是,在每种情况下,编译器都会将任意数量的静态变量合并到固定的少量内存请求中,并且链接器会将所有这些变量合并为少量的合并分配.即使应用程序必须从操作系统或加载器接收一块内存,编译器和链接器也负责将大块中的各个块分配给需要它的所有单个变量.
Jul*_*les 13
您的问题的核心是:"如何在编译文件中分配"内存?是不是内存总是在RAM中分配所有虚拟内存管理内容?根据定义,内存分配不是运行时概念吗?"
我认为问题在于内存分配涉及两个不同的概念.从根本上说,内存分配是我们说"这个数据项存储在这个特定的内存块中"的过程.在现代计算机系统中,这涉及两个步骤:
后一个过程纯粹是运行时,但前者可以在编译时完成,如果数据具有已知大小并且需要固定数量的数据.这基本上是它的工作原理:
编译器看到一个源文件,其中包含一行看起来像这样的行:
int c;
Run Code Online (Sandbox Code Playgroud)它为汇编程序生成输出,指示它为变量"c"保留内存.这可能如下所示:
global _c
section .bss
_c: resb 4
Run Code Online (Sandbox Code Playgroud)当汇编程序运行时,它会保留一个计数器,用于跟踪每个项目从内存"段"(或"段")开始的偏移量.这就像一个非常大的'struct'的部分,它包含整个文件中的所有内容,此时它没有分配给它的任何实际内存,并且可以在任何地方.它在一个表中注明了_c具有特定偏移(比如从段的起始处开始的510个字节),然后将其计数器增加4,因此下一个这样的变量将是(例如)514个字节.对于需要地址的任何代码_c,它只是将510放在输出文件中,并添加一条注释,表示输出需要包含以后_c添加的段的地址.
链接器获取所有汇编器的输出文件,并检查它们.它确定每个段的地址,以便它们不会重叠,并添加必要的偏移量,以便指令仍然引用正确的数据项.对于未被初始化的内存(如被占用的那样)c(汇编程序被告知内存将由于编译器将其放入'.bss'段而未初始化,这是为未初始化的内存保留的名称),它包含一个输出中的头字段告诉操作系统需要保留多少.它可能被重新定位(通常是),但通常被设计为在一个特定的内存地址处更有效地加载,并且OS将尝试在该地址加载它.此时,我们非常清楚虚拟地址将被使用的内容c.
在程序运行之前,实际上不会确定物理地址.然而,从程序员的角度来看,物理地址实际上是无关紧要的 - 我们甚至从未发现它是什么,因为操作系统通常不会告诉任何人,它可以经常更改(即使在程序运行时),以及无论如何,操作系统的主要目的是将其抽象出来.
可执行文件描述了为静态变量分配的空间.当您运行可执行文件时,此分配由系统完成.所以你的1kB静态变量不会增加1kB的可执行文件的大小:
static char[1024];
Run Code Online (Sandbox Code Playgroud)
除非您指定初始化程序:
static char[1024] = { 1, 2, 3, 4, ... };
Run Code Online (Sandbox Code Playgroud)
因此,除了"机器语言"(即CPU指令)之外,可执行文件还包含所需内存布局的描述.
内存可以通过多种方式分配:
现在你的问题是什么是"在编译时分配的内存".肯定这只是一个错误的措辞说法,它应该指代二进制段分配或堆栈分配,或者在某些情况下甚至指向堆分配,但在这种情况下,分配是由程序员眼睛通过隐形构造函数调用隐藏的.或者可能是那个说只是想说内存没有在堆上分配但是不知道堆栈或段分配的人.(或者不想进入那种细节).
但在大多数情况下,人们只是想说在编译时已知分配的内存量.
只有在应用程序的代码或数据段中保留内存时,二进制大小才会更改.