- 内存分区 {#1-内存分区} =================
C代码经过预处理、编译、汇编、链接4步后生成一个二进制可执行程序。
在 Linux 下,可以在命令行中通过size
命令查看二进制文件(可执行文件、静态库、动态库等)的大小和节(section)信息。
size
命令的基本语法是:
|-----------|-------------------------|
| 1
| size [选项] [文件名]
|
示例使用:
|-----------|---------------------------|
| 1
| $ size my_program
|
上述名为 my_program
的可执行文件的大小信息输出如下:
|-------------|--------------------------------------------------------------------------------------------------------|
| 1 2
| text data bss dec hex filename 10024 464 24 10512 2900 my_program
|
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 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 34 35
| #include <stdio.h> #include <stdlib.h> int e; static int f; int g = 10; static int h = 10; int main() { int a; int b = 10; static int c; static int d = 10; char* i = "test"; char* k = NULL; printf("&a\t %p\t // 局部未初始化变量\n", &a); printf("&b\t %p\t // 局部初始化变量\n", &b); printf("&c\t %p\t // 静态局部未初始化变量\n", &c); printf("&d\t %p\t // 静态局部初始化变量\n", &d); printf("&e\t %p\t // 全局未初始化变量\n", &e); printf("&f\t %p\t // 全局静态未初始化变量\n", &f); printf("&g\t %p\t // 全局初始化变量\n", &g); printf("&h\t %p\t // 全局静态初始化变量\n", &h); printf("i\t %p\t // 只读数据(文字常量区)\n", i); k = (char*)malloc(10); printf("k\t %p\t // 动态分配的内存\n", k); return 0; }
|
- 内存操作函数 {#2-内存操作函数} =====================
2.1 memset() {#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 2 3 4 5 6 7 8 9 10 11 12
| #include <stdio.h> #include <string.h> int main() { char str[15]; // 填充 str 数组的前 15 个字节为字符 'A' memset(str, 'A', 15); printf("str 内容为:%s\n", str); return 0; }
|
在上述示例中,我们定义了一个 char
类型的数组 str
,长度为 15。使用 memset
函数将数组的前 15 个字节设置为字符 'A'
。最后使用 printf
函数打印数组内容。
运行程序后的输出结果如下所示:
|-----------|---------------------------------|
| 1
| str 内容为:AAAAAAAAAAAAAAA
|
需要注意的是,memset
函数逐字节设置指定内存区域,适用于字符类型 (char) 或无符号字节类型 (unsigned char) 的数据。对于其他数据类型,如整数或自定义结构体,如果需要设置为其他具体的值,可以使用其他方法,如循环逐个赋值。此外,在使用 memset
时需要确保目标内存区域的大小不超过其分配的大小,以避免访问越界。
2.2 memcpy() {#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 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h> #include <string.h> int main() { char src[] = "Hello, Dabing!"; char dest[20]; // 将 src 数组的内容复制到 dest 数组 memcpy(dest, src, strlen(src) + 1); printf("dest 内容为:%s\n", dest); return 0; }
|
在上述示例中,我们定义了一个 src
数组,内容为 "Hello, Dabing!"
,然后定义了一个 dest
数组作为目标。使用 memcpy
函数将 src
数组的内容复制到 dest
数组中。最后使用 printf
打印 dest
数组的内容。
运行程序后的输出结果如下所示:
|-----------|---------------------------------|
| 1
| dest 内容为:Hello, Dabing!
|
需要注意的是,memcpy
函数逐字节复制源内存区域的内容到目标内存区域,适用于任意类型的数据
。但需要确保目标内存区域的大小足够容纳源数据,以避免访问越界。同时,为了防止内存重叠导致不确定的结果,建议源内存区域和目标内存区域不能重叠。
2.3 memmove() {#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 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h> #include <string.h> int main() { char str[] = "Hello, Dabing!"; char buffer[20]; // 将 str 数组的内容移动到 buffer 数组 memmove(buffer, str, strlen(str) + 1); printf("buffer 内容为:%s\n", buffer); return 0; }
|
在上述示例中,我们定义了一个 str
数组,内容为 "Hello, Dabing!"
,然后定义了一个 buffer
数组作为目标。使用 memmove
函数将 str
数组的内容移动到 buffer
数组中。最后使用 printf
打印 buffer
数组的内容。运行程序后的输出结果如下所示:
|-----------|-----------------------------------|
| 1
| buffer 内容为:Hello, Dabing!
|
需要注意的是,与 memcpy
不同的是,memmove 函数能够处理源内存区域和目标内存区域重叠的情况,以确保正确的数据移动。因此,当需要在内存中进行数据移动时,并且源内存区域和目标内存区域可能重叠时,建议使用 memmove
函数而不是 memcpy
函数。
2.4 memcmp() {#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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <stdio.h> #include <string.h> int main() { char str1[] = "Hello"; char str2[] = "World"; int result = memcmp(str1, str2, 5); if (result < 0) { printf("str1 小于 str2\n"); } else if (result == 0) { printf("str1 等于 str2\n"); } else { printf("str1 大于 str2\n"); } return 0; }
|
在上述示例中,我们使用 memcmp
函数比较了两个字符串 str1
和 str2
的前 5 个字符。根据比较结果,使用条件语句判断并打印了相应的结果。
运行程序后的输出结果如下:
|-----------|----------------------|
| 1
| str1 小于 str2
|
需要注意的是,memcmp
函数是以字节为单位进行比较的,因此适用于比较任意类型的数据。但是,当比较字符串时,建议使用字符串比较函数 strcmp
来代替 memcmp
,因为 strcmp
能够根据字符串的终止符'\0'
自动确定比较的长度,而不需要显式指定比较的字节数。
- 堆内存的分配和释放 {#3-堆内存的分配和释放} ===========================
3.1 函数原型 {#3-1-函数原型}
在 C 语言中,使用标准库函数 <stdlib.h>
中的以下函数来进行堆内存的分配和释放:
-
分配内存有三种方式:
-
malloc
:分配一块新的内存|-------------|--------------------------------------------------------| |
1 2
|#include <stdlib.h> void* malloc(size_t size);
|- 参数
size
表示欲分配的内存大小,以字节为单位。 - 返回值:
- 成功,返回一个指向分配内存的指针
- 失败,则返回
NULL
。
- 参数
-
calloc
:与malloc
类似,用于分配指定数量的内存块。不同之处在于,calloc
还会将分配的内存块初始化为0。|-------------|--------------------------------------------------------------------| |
1 2
|#include <stdlib.h> void* calloc(size_t num, size_t size);
|- 函数参数
num
表示欲分配的内存块数目size
表示每个内存块的大小(以字节为单位)
- 函数返回值:
- 成功,返回一个指向分配内存的指针
- 失败,则返回
NULL
。
- 函数参数
-
realloc
:用于重新分配已分配内存的大小。|-------------|--------------------------------------------------------------------| |
1 2
|#include <stdlib.h> void* realloc(void* ptr, size_t size);
|- 函数参数
ptr
是之前使用malloc
、calloc
或realloc
函数分配的内存块的指针size
表示需要重新分配的内存大小(以字节为单位)
- 函数返回值:
- 成功,返回一个指向重新分配后内存的指针
- 失败,返回
NULL
。
- 函数参数
-
-
释放内存:
-
free
:用于释放之前通过malloc
、calloc
或realloc
分配的内存。它接受一个指针作为参数,将该指针所指向的内存块释放,并使该指针不再有效。|-------------|---------------------------------------------------| |
1 2
|#include <stdlib.h> void free(void* ptr);
|
-
3.2 malloc() {#3-2-malloc}
以下是一个使用 malloc
分配内存的示例:
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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
| #include <stdio.h> #include <stdlib.h> int main() { int* ptr; // 分配可以存储 5 个 int 值的内存块, 内存中的值是随机的 ptr = (int*)malloc(5 * sizeof(int)); if (ptr == NULL) { printf("内存分配失败\n"); return 1; } // 使用分配的内存块 for (int i = 0; i < 5; i++) { ptr[i] = i + 1; } // 打印内存中的值 for (int i = 0; i < 5; i++) { printf("%d ", ptr[i]); } printf("\n"); // 释放内存 free(ptr); return 0; }
|
在上述示例中,我们首先定义了一个指向 int
类型的指针 ptr
,然后使用 malloc
分配了可以存储 5 个 int
值的内存块。如果分配成功,则返回的指针被赋给 ptr
。我们可以使用这块分配的内存来存储和访问数据。最后,使用 free
函数释放了分配的内存。
需要注意以下几点:
- 使用
malloc
分配的内存需要在使用完毕后调用free
函数进行释放,以避免内存泄漏。 - 分配的内存大小应根据实际需要进行调整,大小不足可能导致程序运行错误,而过大可能浪费内存资源。
- 当使用
malloc
分配内存失败时,返回的指针为NULL
,需要对其进行检查。
在实际开发中,使用 malloc
分配动态内存可以灵活地管理和利用内存资源,但也需要注意内存的释放和错误处理,以确保程序的正确性和健壮性。
3.3 calloc() {#3-3-calloc}
以下是一个使用 calloc
分配内存的示例:
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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
| #include <stdio.h> #include <stdlib.h> int main() { int* ptr; // 分配可以存储 5 个 int 值的内存块,并初始化为 0 ptr = (int*)calloc(5, sizeof(int)); if (ptr == NULL) { printf("内存分配失败\n"); return 1; } // 打印内存中的值 for (int i = 0; i < 5; i++) { printf("%d ", ptr[i]); } printf("\n"); // 释放内存 free(ptr); return 0; }
|
在上述示例中,我们使用 calloc
分配了可以存储 5 个 int
值的内存块,并将所有元素初始化为 0。然后,我们通过循环打印了分配的内存块中的值。需要注意以下几点:
- 与
malloc
类似,使用calloc
分配的内存需要在使用完毕后调用free
函数进行释放,以避免内存泄漏。 - 分配的内存大小应根据实际需要进行调整,大小不足可能导致程序运行错误,而过大可能浪费内存资源。
- 当使用
calloc
分配内存失败时,返回的指针为NULL
,需要对其进行检查。
使用 calloc
可以方便地分配内存,并初始化为零。在需要使用零初始化的情况下,calloc
是一个很有用的函数。
3.4 realloc() {#3-4-realloc}
以下是一个使用 realloc
重新分配内存大小的示例:
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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> #include <stdlib.h> int main() { // 分配可以存储 5 个 int 值的内存块 int* ptr = (int*)malloc(5 * sizeof(int)); if (ptr == NULL) { printf("内存分配失败\n"); return 1; } // 重新分配内存为可以存储 10 个 int 值的内存块 ptr = (int*)realloc(ptr, 10 * sizeof(int)); if (ptr == NULL) { printf("内存重新分配失败\n"); return 1; } // 打印重新分配内存后的值 for (int i = 0; i < 10; i++) { ptr[i] = i + 100; printf("%d ", ptr[i]); } printf("\n"); // 释放内存 free(ptr); return 0; }
|
在上述示例中,我们首先使用 malloc
分配了可以存储 5 个 int
值的内存块,然后使用 realloc
将内存重新分配为可以存储 10 个 int
值的内存块。注意,当调用 realloc
后,我们需要将返回的指针重新赋值给原来的指针变量 ptr
,以确保使用的是重新分配后的内存。
需要注意以下几点:
- 使用
realloc
重新分配内存后,返回的指针可能是原有内存的地址,也可能是新分配内存的地址,因此在重新分配内存后要基于返回的地址进行后续的处理。 - 当使用
realloc
重新分配内存失败时,返回的指针为NULL
,需要对其进行检查。 - 在使用
realloc
时,尽量提供尽可能准确的新大小,避免频繁地重新分配内存,以提高效率。
使用 realloc
可以动态地调整已分配内存的大小,使其更适应程序的需求。在需要动态修改内存大小的情况下,realloc
是一个有用的函数。