内存布局
内存布局
苏丙榅1. 内存分区
C代码经过预处理、编译、汇编、链接4步后生成一个二进制可执行程序。
在 Linux 下,可以在命令行中通过size
命令查看二进制文件(可执行文件、静态库、动态库等)的大小和节(section)信息。
size
命令的基本语法是:
1 | size [选项] [文件名] |
示例使用:
1 | size my_program |
上述名为 my_program
的可执行文件的大小信息输出如下:
1 | text data bss dec hex filename |
text
:代码段(可执行文件)或只读数据段(库文件)的大小。data
:已初始化数据段的大小。bss
:未初始化数据段(bss)的大小。dec
:代码段、数据段和bss段的总大小。hex
:十六进制表示的dec
的大小。filename
:文件的名称。
通过命令输出的信息可以得知,在没有运行程序前,也就是说程序没有加载到内存前
,可执行程序内部已经分好3段信息,分别为代码区(text)
、数据区(data)
和未初始化数据区(bss)
3 个部分(有些人直接把 data 和 bss 合起来叫做静态区或全局区)。
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
已初始化数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
栈区(stack)
栈是一种
先进后出
的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
下表是对变量类型、作用域、生命周期、存储位置的总结:
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
局部变量 | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
在下面的示例程序中打印了不同类型的变量对应的数据存储位置:
1 |
|
2. 内存操作函数
2.1 memset()
memset
是 C 语言标准库 <string.h>
中提供的一个函数,用于将一块内存区域填充为指定的值。
memset
函数的原型如下:
1 | void *memset(void *ptr, int value, size_t num); |
参数说明:
ptr
:指向要填充的内存区域的指针。value
:要填充的值,以整数形式传递。num
:要填充的字节数。
memset
函数将指定内存区域 ptr
开始的 num
个字节设置为 value
。
以下是一个使用 memset
的示例:
1 |
|
在上述示例中,我们定义了一个 char
类型的数组 str
,长度为 15。使用 memset
函数将数组的前 15 个字节设置为字符 'A'
。最后使用 printf
函数打印数组内容。
运行程序后的输出结果如下所示:
1 | str 内容为:AAAAAAAAAAAAAAA |
需要注意的是,memset
函数逐字节设置指定内存区域,适用于字符类型 (char) 或无符号字节类型 (unsigned char) 的数据。对于其他数据类型,如整数或自定义结构体,如果需要设置为其他具体的值,可以使用其他方法,如循环逐个赋值。此外,在使用 memset
时需要确保目标内存区域的大小不超过其分配的大小,以避免访问越界。
2.2 memcpy()
memcpy
是 C 语言标准库 <string.h>
中提供的一个函数,用于将源内存区域的内容复制到目标内存区域。
memcpy
函数的原型如下:
1 | void *memcpy(void *dest, const void *src, size_t n); |
参数说明:
dest
:指向目标内存区域的指针,也就是要将源数据复制到的地方。src
:指向源内存区域的指针,也就是要复制的数据来源。n
:要复制的字节数。
memcpy
函数会将源内存区域 src
的前 n
个字节的内容复制到目标内存区域 dest
中。
以下是一个使用 memcpy
的示例:
1 |
|
在上述示例中,我们定义了一个 src
数组,内容为 "Hello, Dabing!"
,然后定义了一个 dest
数组作为目标。使用 memcpy
函数将 src
数组的内容复制到 dest
数组中。最后使用 printf
打印 dest
数组的内容。
运行程序后的输出结果如下所示:
1 | dest 内容为:Hello, Dabing! |
需要注意的是,memcpy
函数逐字节复制源内存区域的内容到目标内存区域,适用于任意类型的数据
。但需要确保目标内存区域的大小足够容纳源数据,以避免访问越界。同时,为了防止内存重叠导致不确定的结果,建议源内存区域和目标内存区域不能重叠。
2.3 memmove()
memmove
是 C 语言标准库 <string.h>
中提供的一个函数,用于在内存中移动一块数据。
memmove
函数的原型如下:
1 | void *memmove(void *dest, const void *src, size_t n); |
参数说明:
dest
:指向目标内存区域的指针,也就是要将数据移动到的地方。src
:指向源内存区域的指针,也就是要移动的数据来源。n
:要移动的字节数。
memmove
函数会将源内存区域 src
的内容移动到目标内存区域 dest
中,并且可以处理内存重叠的情况。
以下是一个使用 memmove
的示例:
1 |
|
在上述示例中,我们定义了一个 str
数组,内容为 "Hello, Dabing!"
,然后定义了一个 buffer
数组作为目标。使用 memmove
函数将 str
数组的内容移动到 buffer
数组中。最后使用 printf
打印 buffer
数组的内容。运行程序后的输出结果如下所示:
1 | buffer 内容为:Hello, Dabing! |
需要注意的是,与 memcpy
不同的是,memmove 函数能够处理源内存区域和目标内存区域重叠的情况,以确保正确的数据移动。因此,当需要在内存中进行数据移动时,并且源内存区域和目标内存区域可能重叠时,建议使用 memmove
函数而不是 memcpy
函数。
2.4 memcmp()
memcmp
是 C 语言标准库 <string.h>
中提供的一个函数,用于比较两个内存区域的内容。
memcmp
函数的原型如下:
1 | int memcmp(const void *ptr1, const void *ptr2, size_t num); |
参数说明:
ptr1
:指向第一个内存区域的指针。ptr2
:指向第二个内存区域的指针。num
:要比较的字节数。
memcmp
函数会比较 ptr1
和 ptr2
指向的内存区域的前 num
个字节的内容,并根据比较结果返回一个整数值。返回值的含义如下:
- 如果
ptr1
的内容小于ptr2
的内容,返回一个负整数。 - 如果
ptr1
的内容等于ptr2
的内容,返回 0。 - 如果
ptr1
的内容大于ptr2
的内容,返回一个正整数。
以下是一个使用 memcmp
的示例:
1 |
|
在上述示例中,我们使用 memcmp
函数比较了两个字符串 str1
和 str2
的前 5 个字符。根据比较结果,使用条件语句判断并打印了相应的结果。
运行程序后的输出结果如下:
1 | str1 小于 str2 |
需要注意的是,memcmp
函数是以字节为单位进行比较的,因此适用于比较任意类型的数据。但是,当比较字符串时,建议使用字符串比较函数 strcmp
来代替 memcmp
,因为 strcmp
能够根据字符串的终止符'\0'
自动确定比较的长度,而不需要显式指定比较的字节数。
3. 堆内存的分配和释放
3.1 函数原型
在 C 语言中,使用标准库函数 <stdlib.h>
中的以下函数来进行堆内存的分配和释放:
分配内存有三种方式:
malloc
:分配一块新的内存1
2
void* malloc(size_t size);- 参数
size
表示欲分配的内存大小,以字节为单位。 - 返回值:
- 成功,返回一个指向分配内存的指针
- 失败,则返回
NULL
。
- 参数
calloc
:与malloc
类似,用于分配指定数量的内存块。不同之处在于,calloc
还会将分配的内存块初始化为0。1
2
void* calloc(size_t num, size_t size);- 函数参数
num
表示欲分配的内存块数目size
表示每个内存块的大小(以字节为单位)
- 函数返回值:
- 成功,返回一个指向分配内存的指针
- 失败,则返回
NULL
。
- 函数参数
realloc
:用于重新分配已分配内存的大小。1
2
void* realloc(void* ptr, size_t size);- 函数参数
ptr
是之前使用malloc
、calloc
或realloc
函数分配的内存块的指针size
表示需要重新分配的内存大小(以字节为单位)
- 函数返回值:
- 成功,返回一个指向重新分配后内存的指针
- 失败,返回
NULL
。
- 函数参数
释放内存:
free
:用于释放之前通过malloc
、calloc
或realloc
分配的内存。它接受一个指针作为参数,将该指针所指向的内存块释放,并使该指针不再有效。1
2
void free(void* ptr);
3.2 malloc()
以下是一个使用 malloc
分配内存的示例:
1 |
|
在上述示例中,我们首先定义了一个指向 int
类型的指针 ptr
,然后使用 malloc
分配了可以存储 5 个 int
值的内存块。如果分配成功,则返回的指针被赋给 ptr
。我们可以使用这块分配的内存来存储和访问数据。最后,使用 free
函数释放了分配的内存。
需要注意以下几点:
- 使用
malloc
分配的内存需要在使用完毕后调用free
函数进行释放,以避免内存泄漏。 - 分配的内存大小应根据实际需要进行调整,大小不足可能导致程序运行错误,而过大可能浪费内存资源。
- 当使用
malloc
分配内存失败时,返回的指针为NULL
,需要对其进行检查。
在实际开发中,使用 malloc
分配动态内存可以灵活地管理和利用内存资源,但也需要注意内存的释放和错误处理,以确保程序的正确性和健壮性。
3.3 calloc()
以下是一个使用 calloc
分配内存的示例:
1 |
|
在上述示例中,我们使用 calloc
分配了可以存储 5 个 int
值的内存块,并将所有元素初始化为 0。然后,我们通过循环打印了分配的内存块中的值。需要注意以下几点:
- 与
malloc
类似,使用calloc
分配的内存需要在使用完毕后调用free
函数进行释放,以避免内存泄漏。 - 分配的内存大小应根据实际需要进行调整,大小不足可能导致程序运行错误,而过大可能浪费内存资源。
- 当使用
calloc
分配内存失败时,返回的指针为NULL
,需要对其进行检查。
使用 calloc
可以方便地分配内存,并初始化为零。在需要使用零初始化的情况下,calloc
是一个很有用的函数。
3.4 realloc()
以下是一个使用 realloc
重新分配内存大小的示例:
1 |
|
在上述示例中,我们首先使用 malloc
分配了可以存储 5 个 int
值的内存块,然后使用 realloc
将内存重新分配为可以存储 10 个 int
值的内存块。注意,当调用 realloc
后,我们需要将返回的指针重新赋值给原来的指针变量 ptr
,以确保使用的是重新分配后的内存。
需要注意以下几点:
- 使用
realloc
重新分配内存后,返回的指针可能是原有内存的地址,也可能是新分配内存的地址,因此在重新分配内存后要基于返回的地址进行后续的处理。 - 当使用
realloc
重新分配内存失败时,返回的指针为NULL
,需要对其进行检查。 - 在使用
realloc
时,尽量提供尽可能准确的新大小,避免频繁地重新分配内存,以提高效率。
使用 realloc
可以动态地调整已分配内存的大小,使其更适应程序的需求。在需要动态修改内存大小的情况下,realloc
是一个有用的函数。