为什么要动态内存分配?
之前,我们向内存申请空间,有两种方式,一种是定义变量,一种是建立数组;但是,这两种方式都有缺陷,创建的空间大小是固定的,在程序的运行过程中,不能随着我们的需要改变而改变,这就需要我们申请动态内存了
1. 动态内存函数
1.1 malloc和free
void* malloc (size_t size);
函数功能:
- 开辟一块size字节大小的空间
- 如果开辟成功,返回开辟空间首地址
- 如果开辟失败,返回NULL
- 如果size是0,标准未定义,取决于编译器
- 由于返回值是void*类型,因此返回的地址需要我们另做处理
void free (void* ptr);
函数功能:
- 释放ptr指向的空间,前提是ptr指向的空间是动态开辟的;如果ptr指向的空间不是动态开辟的,编译器会报错
- 如果ptr为NULL,则什么都不做
- 另外,释放完空间,ptr此时是一个野指针,需要置空
动态内存函数要和free一同使用,动态开辟的空间有两种方式释放:
- free主动释放
- 程序结束,操作系统会帮我们回收
虽然程序结束,申请的动态空间也会被回收,但如果程序在退出之前,开辟了多处动态内存而没有释放,又去开辟动态内存,很可能会导致内存泄漏,因此,每次申请的动态内存都要记得free释放
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
//输出:0 1 2 3 4 5 6 7 8 9
内存非为三大部分,动态内存函数申请的空间是在堆上开辟的
1.2 calloc函数
void* calloc (size_t num, size_t size);
函数功能:
- num是开辟元素的个数,size是每个元素的大小,开辟num*size字节的空间
- calloc开辟空间后,会将空间自动初始化为0
1.3 realloc函数
void* realloc (void* ptr, size_t size);
动态开辟的内存用完了,想进行增容,这时就可以考虑使用realloc
函数功能:
- ptr是要进行扩容的地址,size为新的空间大小
- 返回新空间的地址
- 如果ptr为空,此时就相当于malloc函数
开辟空间有两种情况:
- ptr后面的空间够存放新空间的大小:此时直接在ptr后面的空间扩容
- ptr后面的空间不够存放新空间的大小:此时会另开辟一块size大小的空间,并把原数据拷贝到新空间,释放掉旧空间,返回新空间的地址
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//...
//想扩容到100个int的大小
//写法1
p = realloc(p, 100 * sizeof(int));
//写法2
int* ptr = (int*)realloc(p, 100 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
p = ptr;
//...
free(p);
p = NULL;
return 0;
}
用realloc开辟完空间,更推荐使用写法2,因为realloc开辟空间可能会失败,此时返回NULL,不仅没有扩容成功,还把原来的空间给弄没了
2.动态内存常见的错误
2.1对空指针进行解引用
这种情况通常是因为使用完动态内存函数没有对返回值进行检查
2.2对动态内存的越界访问
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
i=10的内存已经不属于我们的了;这种情况编译器是不会报错的,需要我们自己擦亮眼睛
2.3对非动态内存使用free
int main()
{
int p[10] = { 0 };
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
free(p);
return 0;
}
p指向的空间不是动态开辟的,不能进行free释放
2.4使用free释放一部分动态内存
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
p最终指向动态内存的一部分,free§只释放了一部分,最终仍有可能造成内存泄漏
2.5对一块动态内存多次释放
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//...
free(p);
//...
free(p);
p = NULL;
return 0;
}
前面说过,free指向的空间必须是动态内存,第二次free时,p指向的空间已经不是动态的了
2.6忘记释放动态内存
void Print(int n)
{
int* p = (int*)malloc(n * sizeof(int));
if (p == NULL)
{
perror("malloc");
return;
}
if (n == 3)
return;
free(p);
p = NULL;
}
int main()
{
Print(3);
return 0;
}
上面的代码写了free且置空,看似没有问题,但在执行free之前,函数已经返回,不会执行free,因此没有释放成功
内存泄漏是非常严重的问题,在日常写代码的过程中,一定要注意,动态开辟的内存要记得释放
3.笔试题讲解
题目1:
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;
}
上述代码的运行结果是什么?
- 由于p是str的一份临时拷贝,出了GetMemory函数就销毁了,malloc开辟出来的空间就找不到了,导致内存泄漏
- 执行完GetMemory函数,str仍然是NULL,在strcpy函数中,会对str解引用,对空指针进行解引用,最终导致程序崩溃
怎么修改上述代码,让它达到我们想要的功能?
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
题目2:
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是GetMemory函数的局部变量,出了函数就销毁了,此时返回的p被外面的str接受,str就变成了野指针,打印的是一堆乱码,这是一种返回栈空间地址的问题
上面的代码也可以简化为:
int* Test()
{
int a = 10;
return &a;
}
int main()
{
int* p = Test();
printf("%d\n", *p);//10
return 0;
}
同样是犯了返回栈空间地址的错误,我们发现该代码输出是正常的,这是为什么?
虽然结果正确,但这并不代表代码没有问题,结果正确的原因是Test函数即使销毁了,p位置处的值仍没有被修改,因此误打误撞,结果是对的
题目3:
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;
}
该代码正常打印,唯一的缺点就是少了释放内存,存在内存泄漏的问题
题目4:
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此时是野指针,对野指针进行了访问,非法访问内存空间
题目5:
int* Test()
{
int* p;
*p = 10;
return p;
}
创建p时没有对其初始化,p为随机值,随机指向一块空间,是野指针,对野指针进行了操作
4.C/C++的内存区域
- 栈区:执行函数时,函数内的局部变量都是在该区域创建的,当函数结束时,自动销毁创建的区域
- 堆区:动态开辟的空间在该区域创建,通常由程序员自己释放,当程序结束时,也会由操作系统自动回收
- 数据段:存放全局变量,静态数据,程序结束由系统释放
- 代码段:存放函数体的二进制代码
6.柔性数组
定义:在结构体中,最后一名成员是数据,且数组的大小未知,我们把该数组叫做柔性数组
struct S
{
char c;
int i;
int arr[];
};
6.1柔性数组的特点
- 柔性数组必须在结构体当中
- 必须是最后一名成员
- 柔性数组前面至少有一名成员
- 该结构体的大小不包括数组的大小
- 必须用动态内存函数对结构体开辟空间,且开辟空间的大小要大于结构体的大小,确保柔性数组有有一定的空间
6.2柔性数组的使用
//代码1
struct S
{
char c;
int i;
int arr[];
};
int main()
{
struct S* p;
p = (struct S*)malloc(sizeof(struct S) + 20);
if (p == NULL)
{
perror("malloc");
return 1;
}
p->c = 'a';
p->i = 5;
for (int i = 0; i < 5; i++)
{
p->arr[i] = i;
}
//空间不够了,进行增容
struct S* ptr = (struct S*)realloc(p, sizeof(struct S) + 40);
if (ptr == NULL)
{
perror("realloc");
return 1;
}
p = ptr;
ptr = NULL;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i;
}
free(p);
p = NULL;
return 0;
}
实际上,不用柔性数组也能完成上面的操作
//代码2
struct S
{
char c;
int i;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc");
return 1;
}
int* ptr = (int*)malloc(sizeof(int) * 5);
if(ptr == NULL)
{
perror("malloc");
return 1;
}
ps->arr = ptr;
ptr = NULL;
for (int i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
//增容
int* p = (int*)realloc(ps->arr, sizeof(int) * 10);
if (p == NULL)
{
perror("malloc");
return 1;
}
ps->arr = p;
p = NULL;
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
}
那么,使用柔性数组的代码1相较于代码2,有什么优势呢?
- 方便内存的释放,使用柔性数组只需要释放一次内存空间;而代码2你必须先将结构体成员开辟的空间释放后,才能释放结构体,多释放意味着风险越多
动态内存的内容就到这!