动态内存分配

在创建变量的时候实际上就是在开辟分配空间

1
2
int val = 0; // 开辟了四个字节的空间
int arr[5] = { 0 }; // 在栈空间上开辟了10个字节的连续空间

创建变量的开辟空间方式有两个特点:

  • 空间开辟大小是有限的

  • 数组在声明的时,必须指明数组长度,一旦确定了不能调整

某些情况,需要的空间大小在程序运行时才知道

C语言提供了动态内存开辟,让开发者自行申请和释放空间

malloc 和 free

malloc

一个用于动态开辟内存空间的函数,包含在头文件<stdlib.h>中,语法格式如下

1
void* malloc (size_t size);
  • 开辟成功,返回一个指向开辟好空间的指针

  • 开辟失败,返回 NULL

  • 返回值类型是 void* , 使用时需操作者根据需求决定怎么使用

  • size 如果为 0, malloc 的行为是标准未定义的,由编译器决定

1
2
3
4
5
6
7
8
9
10
11
12
// 20个字节 放5个整型
int* p = (int*)malloc(20);
if (!p)
{
perror("malloc");
return 1;
}

for (int i = 1;i <= 5;i++)
{
*(p + i) = i;
}

可以看到申请成功了

成功赋值

free

向C语言申请了内存空间,在使用完成后就需要释放和回收

C语言提供了 free 函数用于动态空间的释放和回收,也包含在头文件<stdlib.h>中

1
void free (void* ptr);
  • 如果 ptr 指向的空间不是动态开辟的, 那么 free 函数的行为是未定义的

  • 如果 ptr 是 NULL, 则函数啥都不干

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     ```c
    int* p = (int*)malloc(20);
    if (!p)
    {
    perror("malloc");
    return 1;
    }

    for (int i = 0;i < 5;i++)
    {
    *(p + i) = i;
    }

    free(p);

需要注意的是,传给 free 的是动态开辟的内存空间的起始地址

1
2
3
4
5
6
7
8
9
10
11
12
13
int* p = (int*)malloc(20);
if (!p)
{
perror("malloc");
return 1;
}

for (int i = 0;i < 5;i++)
{
*p++ = i;
}

free(p); // 需要注意的是,传给 free 的是 malloc 开辟的内存空间的起始地址

这里 p 自加后已经越过了 malloc 动态开辟的空间所以报错了

calloc 和 realloc

calloc

包含在头文件<stdlib.h>中,语法格式如下

1
void* calloc (size_t num, size_t size);
  • 为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0
1
2
3
4
5
int* p = (int*)calloc(5, sizeof(int));
for (int i = 0;i < 5;i++)
{
printf("%d ", *(p + i));
}

如果要求申请的空间初始化,就可以用 calloc

realloc

包含在头文件<stdlib.h>中,语法格式如下

1
void* realloc (void* ptr, size_t size);
  • 对动态开辟内存空间的大小进行调整

  • ptr 是要调整的动态开辟内存空间的起始地址

  • size 调整后的内存空间的大小

  • 返回值是调整之后内存空间的起始地址

  • 调整后会将原空间的数据移动到新的空间中

1
2
3
4
5
6
7
8
9
void* p = malloc(5 * sizeof(int));
if (!p)
{
perror("malloc");
return 1;
}

// 希望调整成四十个字节大小的空间
p = realloc(p, 10 * sizeof(int));

realloc 调整空间大小有三种情况:

若原有空间后有足够大的空间,就在原有空间上向后进行扩展



若原有空间不够大,就找一块尚未分配且足够大的空间,将原空间的数据拷贝过去,

返回这一块新的空间的起始地址



调整失败,返回 NULL

需要注意的是, 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
int* p = (int*)calloc(5, sizeof(int));
if (!p)
{
perror("malloc");
return 1;
}

// 希望调整成四十个字节大小的空间
int* p2 = (int*)realloc(p, 10 * sizeof(int));
if (!p2)
{
perror("realloc");
return 1;
}


for (int i = 0;i < 10;i++)
{
*(p2 + i) = i;
}

for (int i = 0;i < 10;i++)
{
printf("%d ", *(p2 + i));
}


// 释放空间
free(p);
p = NULL;


realloc 也可以实现和 malloc 一样的功能,给 ptr 传 NULL 就可以了

常见的动态内存错误

对 NULL 指针解引用操作

1
2
3
int* p = (int*)malloc(INT_MAX );
*p = 20;//如果p的值是NULL,就会有问题
free(p);

对动态开辟内存空间的越界访问

1
2
3
4
5
6
int* p = (int*)malloc(5 * sizeof(int));
for (int i = 0;i <= 5; i++)
{
*(p + i) = i;
}
free(p);

对非动态开辟内存进行释放

1
2
3
int a = 0;
int* p = &a;
free(p);

使用 free 释放一块动态开辟内存的一部分

1
2
int* p = (int*)calloc(4,sizeof(int));
free(p + 1);

对同一块动态内存多次释放

1
2
3
int* p = (int*)malloc(4);
free(p);
free(p);

动态内存开辟忘记释放(内存泄漏)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}

int main()
{
test();
while (1);
return 0;
}

!
在这串代码中, test() 中开辟的空间没有释放,就造成了内存泄漏

动态开辟的内存空间一定要释放,并且正确的释放


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

int main()
{
Test();
return 0;
}

这段代码会运行崩溃

这里是传值调用,数是 char* 类型,传的参数也是 char* 类型,属于是传值调用,形参

的操作影响不了实参,str 仍然是 NULL , 这里对 NULL 解引用了
同时,也没有对动态

开辟的内存进行释放

解决方法就是使用传址调用,而保存 char* 这个一级指针变量的变量就是 char** 这

个二级指针,再加一部释放内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
}

int main()
{
Test();
return 0;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}

int main()
{
Test();
return 0;
}

函数执行完后,局部数组 p 所在的栈空间被系统自动回收,指针 str 指向了被回

收的空间,因此成为了野指针

解决方法是可以在函数 GetMemory()使用动态开辟内存,动态开辟内存在未手动释

放的情况下只会在程序结束运行后由系统自动释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char* GetMemory(void)
{
// 动态开辟内存在未手动释放的情况下只会在程序结束运行后由系统自动释放
// 所以这里局部变量 p所指向的空间不会在函数执行完成后被系统回收
char* p = (char*)malloc(12);
strcpy(p, "hello world");
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}

int main()
{
// p 是局部数组
// 函数执行完成 p 申请的空间也就被系统收回了
// str 在接受 GetMemory() 的返回值后成了野指针
Test();
return 0;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}

void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

int main()
{
Test();
return 0;
}

这段代码的能够正常执行函数,但是在 Test() 中,使用完动态开辟内存后应该进行释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}

void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}

int main()
{
Test();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

int main()
{
Test();
return 0;
}

这一段代码虽然能执行,但本质上属于非法访问内存, str 所指向的内存空间被释放

了,逻辑上讲已经没有对 str 指向内存空间的使用权了, 但这块内存空间仍然存

在,str 并未置空,对他进行使用属于非法访问

在释放动态内存空间后要将指向这片空间的指针变量置空

1
2
3
4
5
6
7
8
9
10
11
12
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

柔性数组

C99中,结构中的最后一个元素允许是未知大小的数组,这叫柔性数组

1
2
3
4
5
struct S
{
char c;
int arr[]; // 柔性数组
};

柔性数组的特点

  • 结构体中柔性数组成员前至少有一个其他成员

  • sizeof 返回的结构大小不包含柔性数组的内存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct S
    {
    int c;
    int arr[]; // 柔性数组
    };


    int main()
    {
    printf("%zd\n", sizeof(struct S));
    return 0;
    }

  • 包含柔性数组成员的结构体用 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
32
33
34
35
36
37
38
39
40
41
42
// 柔性数组动态开辟空间
// sizeof(struct S) 表示除柔性数组之外这个结构体其他成员变量所占的空间
// num * sizeof(type) 表示这个柔性数组动态开辟的空间大小
struct S* p = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
if (!p)
{
// 判空
perror("malloc");
return 1;
}

for (int i = 0;i < 5;i++)
{
p->arr[i] = i;
}

for (int i = 0;i < 5;i++)
{
printf("%d ", p->arr[i]);
}
printf("\n");
// 大小不合适可以用 realloc 调整
// 这样数组就不是固定的一块儿空间,而是可以动态变化的空间
// 可以拉伸和压缩,拥有了柔性
struct S* ps = (struct S*)realloc(p, sizeof(struct S) + 10 * sizeof(int));
if (!ps)
{
//判空
perror("realloc");
return 1;
}
p = ps;
for (int i = 0;i < 10;i++)
{
ps->arr[i] = i;
}

for (int i = 0;i < 10;i++)
{
printf("%d ", ps->arr[i]);
}
free(p);


指针的方式实现柔性数组

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct S2
{
int c;
int* arr; // 指针变量实现柔性数组
};

struct S2* p =(struct S2*)malloc(sizeof(struct S2));
if (!p)
{
//判空
perror("malloc");
return 1;
}

p->arr = (int*)malloc(5 * sizeof(int));
if (!p->arr)
{
//判空

perror("malloc");
return 1;
}

for (int i = 0;i < 5;i++)
{
p->arr[i] = i;
}

for (int i = 0;i < 5;i++)
{
printf("%d ",p->arr[i]);
}
printf("\n");

// 大小不合适也可以通过 realloc 进行调整
int* ps = (int*)realloc(p->arr,10 * sizeof(int));
if (!ps)
{
// 判空
perror("realloc");
return 1;
}

p->arr = ps;
for (int i = 0;i < 10;i++)
{
p->arr[i] = i;
}

for (int i = 0;i < 10;i++)
{
printf("%d ", p->arr[i]);
}
// 这种方式需要释放两次内存空间
// 容易出错
// 且多次使用 malloc 进行空间的开辟会在成内存空间中存在多出内存碎片
free(p->arr);
free(p);