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 合起来叫做静态区或全局区)。

image-20231004104352606

  • 代码区(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()

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()

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()

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()

memcmp 是 C 语言标准库 <string.h> 中提供的一个函数,用于比较两个内存区域的内容。

memcmp 函数的原型如下:

1
int memcmp(const void *ptr1, const void *ptr2, size_t num);

参数说明:

  • ptr1:指向第一个内存区域的指针。
  • ptr2:指向第二个内存区域的指针。
  • num:要比较的字节数。

memcmp 函数会比较 ptr1ptr2 指向的内存区域的前 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 函数比较了两个字符串 str1str2 的前 5 个字符。根据比较结果,使用条件语句判断并打印了相应的结果。

运行程序后的输出结果如下:

1
str1 小于 str2

需要注意的是,memcmp 函数是以字节为单位进行比较的,因此适用于比较任意类型的数据。但是,当比较字符串时,建议使用字符串比较函数 strcmp 来代替 memcmp,因为 strcmp 能够根据字符串的终止符'\0'自动确定比较的长度,而不需要显式指定比较的字节数。

3. 堆内存的分配和释放

3.1 函数原型

在 C 语言中,使用标准库函数 <stdlib.h> 中的以下函数来进行堆内存的分配和释放:

  1. 分配内存有三种方式:

    • 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 是之前使用 malloccallocrealloc 函数分配的内存块的指针
        • size 表示需要重新分配的内存大小(以字节为单位)
      • 函数返回值:
        • 成功,返回一个指向重新分配后内存的指针
        • 失败,返回 NULL
  2. 释放内存:

    • free:用于释放之前通过 malloccallocrealloc 分配的内存。它接受一个指针作为参数,将该指针所指向的内存块释放,并使该指针不再有效。
      1
      2
      #include <stdlib.h>
      void free(void* ptr);

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()

以下是一个使用 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()

以下是一个使用 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 是一个有用的函数。