0%

内存申请,初始化与释放

在实现业务代码的时候,总不可避免地要动态申请内存。在C语言中,没有自动回收内存的机制,所以必须手动释放内存。如果没有正确释放内存,就会出现内存泄露。如果是在服务器端的程序出现内存泄露,那必定会导致服务不稳定,长期运行总会耗尽内存而导致无法新申请出内存,影响业务。

在C语言中,申请内存调用的是alloc类函数(之所以说类,是因为有malloc calloc realloc三种),释放内存使用free,这些个函数都定义在stdlib.h中。简单说一下alloc函数之间的区别,网上也有很多资料。

1
2
3
4
void* malloc (size_t size);//申请size bytes的一片连续地址空间,不初始化
void* calloc (size_t num, size_t size);//申请num*size bytes的一片连续地址空间,初始化为0
void* realloc (void* ptr, size_t size);//改变已经申请内存空间的大小(可扩充可收缩),ptr也应该指向一片动态申请的内存
//realloc会保留原有的数据。如果是收缩,则新内存为原来内存内容的一部分截取,如果是扩展,则会拷贝原来所有内容并且新增空间,新增的空间不会初始化

问题1:申请的内存是否需要初始化?

这个要根据具体的业务逻辑来分析。理论上来说,保证每一块内存都初始化是最保险的,但是会涉及性能的开销。因为调用memset也是很消耗资源的。一般的客户端软件对性能不敏感,但是在服务器上就不一样了。例如通信基站上的程序,对性能异常的敏感,有时候多1ms的延迟都会导致出错。如果对于大片的内存,全部都初始化太浪费性能。

首先要分析,这片内存的使用场景:

  • 如果申请出内存后的第一步操作就是覆写内存,那大可不必初始化。
  • 如果申请的内存有可能先被读,那就必须初始化保证逻辑的正确。

不需要初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例如业务的逻辑是做数据的拷贝

#include <stdio.h>

char * copyString(char str[])
{
int len = strlen(str) + 1; //末尾空字符
char *ret = malloc(len * sizeof(char));
strcpy(ret, str); //申请的内存空间马上就会被覆写,不需要初始化
return ret;
}

int main ()
{
char str[] = "hello world";
char *copy = copyString(str);
return 0;
}

需要初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//业务的逻辑收到报文,即把对应标志位置1,默认状态肯定都是没收到

#define NOT_RCV 0
#define RCV 1

// 读
bool checkIsRcv(char *packetStatus, int index)
{
return packetStatus[index] == RCV
}

// 写
void setPacketFlag((char *packetStatus, int index, char flag)
{
packetStatus[index] = flag;
}

int main ()
{
int n;
sacnf("%d", &n); //报文数量
char *packetStatus = malloc(n * sizeof(char));
memset(status, 0, n); //如果不初始化,里面的值是不确定的,后面读取出来就会导致逻辑出错
}

问题2:如何正确释放内存?

要做到完全避免内存泄露,在大型的C项目上是个难题,特别是业务逻辑很复杂时。这里只说一下简单的场景:

free一个指针前,需要注意释放这个指针指向的结构体内部的指针指向的内存

1
2
3
4
5
6
7
8
9
10
11
struct node
{
struct node *next;
void *value //结构体内部的指针,指向动态申请的内存
}

void freeNode(struct node *p)
{
free(p->value); //一定要把value指向的内存先释放,再释放p,不然会导致再也无法释放value指向的内存
free(p);
}

但是这样也还是不安全,因为value是void *类型指针,可能value还是指向一个结构体,里面也有动态申请的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct myArray //一个动态长度的int数组
{
int size;
int *array;
}

struct node *p = malloc(sizeof(struct node));
p->value = (void *)malloc(sizeof(struct myArray));

struct myArrayp *q = (struct myArrayp *)p->value;
q->array = malloc(q->size * sizeof(int));

// 为这种结构体封装一个freeXXX函数,保证这个函数可以完全释放内存空间。
void freeArray(struct myArray *p)
{
free(p->array);
free(p);
}

void freeNode(struct node *p)
{
freeArray(p->value); //不要直接调用free,而是调用封装的freeXXX函数
free(p);
}

其实解决问题的核心还是在于封装,某种类型的结构体,需要封装一个创建函数,和释放函数。保证释放函数可以把创建函数申请的内存和代码运行期间申请的内存全部释放掉。