我们已经掌握的内存开辟方式有:
int val = 20; //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
【函数原型】:
void* malloc (size_t size);
malloc()这个函数,它会向内存申请一块连续可用的空间,并返回指向这块空间的指针【特点】:
NULL指针,因此malloc的返回值一定要做检查申请的内存过大就会开辟失败,所以就要判断是不是空指针,然后再进行使用
void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定在内存中的布局是这样的:
这里我们来举一个例子说明一下
malloc()函数去向内存申请大小为40的空间,由于其返回值是一个void*的指针,可以接收任何类型的指针,所以这里我去做了一个强转,将这块空间强制类型转换为int*#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 1.开辟空间
int* p = (int *)malloc(40);
// 2.异常判断
if (NULL == p)
{
perror("malloc fail");
exit(-1);
}
// 3.初始化空间
for (int i = 0; i < 10; ++i)
{
*(p + i) = i + 1;
}
// 4.打印观察
for (int i = 0; i < 10; ++i)
{
printf("%d ", *(p + i));
}
return 0;
}
size为0,malloc的行为是标准是未定义的,取决于编译器。malloc()函数传递进去一个size大小,它便会为我们开辟出指定的空间,但若是我们传递的参数为0的话,就显得很荒唐。malloc申请的空间是怎么释放的呢?
正常情况下,谁申请的空间,谁释放
万一自己不释放,也要交代给别人
所以下面我们就介绍
free
【函数原型】:
void free (void* ptr);
【特点】:
free(p)才行,但是这样真的就可以了吗?free(p);
1 ~ 10变成了一些随机值,这也就意味着我们一开始申请的这块空间还给操作系统了,所以里面所存放的这些内容都销毁了,不过从上面对于这个函数的解读中我们可以看出即使我们将这块空间还给操作系统了,但是这块申请空间的地址还是在的free(p)之后再将其置为NULL即可,此时就无法再找到之前的那块地址了【注意实现】:
stdlib.h头文件中,记得要引头文件【函数原型】:
void* calloc (size_t num, size_t size);
calloc,calloc函数也用来动态内存分配。原型如下:【特点】:
malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0malloc不同的地方在于当我们申请到10个大小为4字节的空间后,发现这10个数据均为0,即在申请的同时就已经为初始化好了,不需要我们自己再去初始化最后再来讲讲动态内存函数【realloc】
【函数原型】:
ptr是要调整的内存地址、size是调整之后新大小、返回值为调整之后的内存起始位置void* realloc (void* ptr, size_t size);
具体地我们来看一下要如何去使用这个
realloc()进行一个扩容
p,扩充后的容量便是10个整型数据int main()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (NULL == p)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
}
//扩容
p = (int*)realloc(p, 20 * sizeof(int));
//释放
free(p);
p = NULL;
return 0;
}
realloc到底是怎么进行扩容的,因为它有一个扩容机制,分为【本地扩容】和【异地扩容】【注意事项】:
realloc就会返回一个空指针p = (int*)realloc(p, sizeof(int) * 10);
tmp去指向这块空间,再扩容结束后再去判断一下这个指针是否NULL,若是为NULL的话代表扩容失败,此时应该打印错误信息然后结束程序,不要再往下执行了,而是当这个地址不为空的时候再将让原先的指针p指向它,让我们从头至尾都在维护同一个指针tmp指针置为空,防止其变为【野指针】int* tmp = (int*)realloc(p, sizeof(int) * 10);
if (tmp == NULL)
{
perror("fail realloc");
return 1;
}
p = tmp;
tmp = NULL;
然后是异地扩容,我们可以将需要扩充后的容量调大,这样后续的容量就会不够了,此时编译器便会在内存中再去找一块合适大小的空间,然后将原先的5个整型数据先拷贝过去,然后再在其后开辟出剩余的空间,最后再释放掉原先的那块空间
在介绍完几个动态内存函数之后,我们再来分析一下【常见的动态内存错误】
代码:
void test()
{
int *p = (int *)malloc(INT_MAX / 4);
*p = 20; //如果p的值是NULL,就会有问题
free(p);
}
分析:
INT_MAX是什么。它是一个宏定义,表示int类型(整型)能够表示的最大值,其值为21474837,那在上面讲malloc的时候我们有说到过,若是需要申请的空间过大的话可能就会导致申请失败的问题,所以这里很致命的一个错误就是在申请空间之后没有去及时判断是否申请成功
改进:
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
if (NULL == p)
{
perror("fail malloc");
exit(-1);
}
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
代码:
int main()
{
int* p = (int*)malloc(40);
if (NULL == p)
{
perror("malloc");
return 1;
}
int i = 0;
for (int i = 0; i <= 10; i++)
{
*(p + i) = 0; // 当i == 10时便会越界
}
free(p);
p = NULL;
return 0;
}
分析:
malloc向堆区申请了10个字节的空间,但是呢在下面对这块空间进行访问的时候却访问了11个整型的大小,此时一定会造成访问越界的问题i没有到100的话是不会出问题的,所以为了方便调试我们需要去设置一个【条件断点】,将i从【9】开始执行,这样我们很快就能观察到结果了i并没有到达11,而是直接跳出了当前循环,然后在free()的时候就出现了问题,一般我们在一些其他地方观察不到的问题就会在free()的地方显现出来,因为此时是要去释放掉我们的这块申请的空间了,便会引发一些异常*(p + i) = 0修改成p[i] = 0,利用[]操作符对某个下标进行访问,此时我们可以看到编译器就报出了警告说索引"10"超出了“0"至”9"的有效范围,因此合法的下标索引即为0 ~ 9改进:
int* p = (int*)malloc(100 * sizeof(int));
代码:
void test()
{
int a = 10;
int* p = &a;
free(p); //ok?
}
分析:
free()释放,那我们在介绍free()的时候说到它只能释放由【malloc】、【calloc】、【realloc】所开辟出来的空间,这些空间都是在堆区上进行申请的,但是我们在普通的函数中所创建的普通变量无非是栈区或者静态区的,它们的释放工作并不是由free()来完成的,因此强行去这样做的话就会造成了一个很大的问题改进:
free()普通栈区上的变量即可,或者按照常规去动态申请然后在进行free()代码:
void test()
{
int* p = (int*)malloc(100);
if (NULL == p)
{
perror("malloc fail");
return 1;
}
for (int i = 0; i < 10; i++)
{
p++;
}
free(p); //p不再指向动态内存的起始位置
}
分析:
free的时候其实就会出问题从下图可以看出,因为free()函数需要做到申请多少释放多少,所以当其释放了一部分之后,就不够了,便造成了访问内存错误的问题
free()的话就会出现警告,很明显这个debug_heap.cpp就是【堆】这一块出的问题改进:
free()一块动态开辟出来内存的一部分,而是要从起始地址开始释放,申请多少释放多少代码:
void test()
{
int* p = (int*)malloc(100);
//使用...
free(p);
//...
free(p); //重复释放
}
分析:
p做置空的操作,于是它还指向那块空间所在的地址,不过里面的内容已经是随机的了,那么这个指针就是一个【野指针】free()的操作,就会造成操作野指针的问题改进:
free后将指针p置为NULL即可,此刻若是后面再去free的话,就不会出现问题了,因为当我们传递NULL作为参数的时候,free(NULL)便不会去做任何的事情void test()
{
int* p = (int*)malloc(100);
//使用...
free(p);
p = NULL; // 将不使用的指针置为NULL
//...
free(p); //重复释放
}
代码:
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
分析:
test()函数,函数内部去申请了100个字节的数据,并为其做了一个初始化,此时main函数就正常地去调用它,但是呢这中间却没有任何地free()释放操作,就会存在【内存泄漏】的问题那么可以既然函数内部没有做释放的话我在调用结束后去free一下这个p不就好了?
free()释放这块空间的时候,是无法访问到这个指针p的。因此要释放的话只能在函数内部进行才可以改进:
free()之后要将指针置为NULL防止野指针void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
free(p);
p = NULL;
}
但是它们两个成对出现就一定不会出现问题吗?
if(1)的条件判断,我们知道这个条件是天然成立的,然后看到当这个条件成立后就会执行return语句,那么当前这个函数就会结束了,此时并没有运行到free(p)这句话void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
if (1)
return; // 因为某些条件中途return了, 没到free()
free(p);
}
代码:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world\n");
printf(str);
}
分析:
错误1: 非法访问内存
GetMemory()函数内部申请了空间后,地址是放在p中的,str依然是NULL。当GetMemory()函数返回之后,strcpy()在拷贝的时候便会形成非法访问内存错误2: 内存泄漏
free释放,会造成内存泄漏的问题str可能是“0”,那这个str它可是一个指针,那为0的话也就意味着它是一个【空指针】,那去访问空指针的话也是非常危险的一件事GetMemory()函数进行动态开辟内存后,虽然p指向了那块地址,但是与外界的str却毫无关系,因此即使我们将其作为参数传入,也无法改变其为NULL的事实,那么此时再将其作为参数传递进strcpy()和printf()函数后,便会造成【空指针异常】的问题改进:
那我们如何对这个代码去进行改进呢?因为我们想要使得函数内部指针的变化带动外部的变化,在C语言中我们可以使用【传址】的形式去进行
&str进行传递,然后在函数的形参部分使用二级的字符指针char**来进行接收,此时内部的在使用*p的时候就等同于是外部的str,它们便指向了同一块内存地址,此时再去使用strcpy()和printf()这两个函数的时候就不会引发【空指针异常】的问题了void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world\n");
printf(str);
// 释放
free(str);
str = NULL;
}
代码:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
分析:
p代表字符数组的首元素地址,外部的str接收到了这个地址,然后再去打印这块地址中的内容,但是字符数组p属于【局部变量】,局部变量的会在栈区开辟函数栈帧,当函数调用结束的时候就把这块空间的使用权还给操作系统了,虽然这块地址还是在的,但是里面的内容已经销毁了,再去访问的话就会造成非法访问的问题GetMemoy()函数后,str接收到了内部的hello world,但是在打印的时候出了问题,这个p除了函数的作用域后就销毁了,不过呢在销毁之前【return】了一下,所以外界的str可以接收到这个p所指向的地址,但是在打印str的时候,p所指向的那块空间就销毁了,此时再去打印的话就看到了[烫烫烫...]这些字样,因为这块空间已经变成了一块未分配的空间,那我们知道那些未分配的地址均是[ccccc],转变为中文字符即为[烫烫烫...]改进1:
那想要去解决上述的这个问题,其实很简单,我们只要在让这个p不要存放在栈区即可,要让其存放在【静态区】,那里面的东西是从程序开始到结束都会留存着的,具体的内存分布在下一节会详细展开
p就指向了一个常量字符串,而对于常量字符串来说是不可改变的,其也是存放在内存中的【静态区】char* p = "hello world";
p为一个常量指针,其所指向的内容是不可修改的const char* p = "hello world";
str指向了和指针p相同的那块空间,并且因为它们所指向的是一个常量字符串,它也存放在静态区,是不会消失的,因此我们在打印的时候就没有任何问题改进2:
static做修饰,此时它就是一个静态数组,那和常量字符串一样也是存在于【静态区】中static char p[] = "hello world";
hello world打印出来对比分析返回局部变量:
可能对于本题一开始的错误 — — 不可返回局部变量,有些同学还没有理解,我这里这里再举一个例子来对比分析一下
Test(),其返回了一个局部变量的地址,此时外面拿一个指针去接收了一下这块地址,并且将其里面的内容给打印了出来,可以注意到这里我在接收到值后立马就做了打印,那如果我在这中间做点其他事呢,例如再做一个其他的打印,此时发生的结果会不会不一样呢?int* Test()
{
int a = 10;
return &a; // 返回局部变量的地址
}
int main(void)
{
int* pa = Test();
printf("%d\n", *pa);
printf("haha\n"); // 先打印haha的话就看不到10了,printf()函数的栈帧覆盖了原来的pa
return 0;
}
但是呢,若我在返回后没有立马去进行打印的话,此时就可以很直观得观察到局部变量a在出了当前函数的栈帧后已经销毁了,所以我们打印出来的并不是【10】,而是【5】,仔细观察调试窗口中,指针pa所指向的那块空间中的值变成了【92491】,完全可以说是一个随机值
刚才我们可以获取到这个10的原因是编译器的问题,可能我们在Linux上去运行的话结果就不是这样了,因为有些编译器在当前函数结束后不会立即释放掉,而是会等待一会;不过有些编译器呢却会理解销毁掉当前所创建的函数栈帧,此时内部所创建的局部变量也就会随之消失了
代码:
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掉即可,最后别忘了将其置空void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
代码:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
分析:
错误:非法内存访问
hello这个字符串,接着立马进行了free()释放,那我们之前有说过一块动态申请的空间若是释放了的话,虽然空间销毁了,但是指针还是留存着那块空间的地址,此时这个str即为一个野指针,指向了一块未分配空间的地址,而且有着100个字节的大小,所以其是不为空的world这个字符串,这也就形成了【非法访问内存】,虽然去运行不存在问题,但是这块空间的使用权并不是我们的,这才有【非法】这么一说改进:
free(str)后将其置空即可,因为在将一块空间还给操作系统后,本身我们不再拥有这块空间的使用权了,后面的操作都是非法的,但若时间我们将其置为NULL之后,这个指针也就忘记了它之前所指向的地址,此时进不了下面的这个if分支了,那逻辑也就正确了void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
malloc向【堆区】申请了一块空间并试图往里写点东西的时候,此时堆区就会多出来一块已经分配了的空间。我们这里结合具体的代码来观察一下
int globalVar = 1;
static int staticGlobalVar = 2;
void test()
{
static int staticVar = 3;
int localVar = 4;
int num1[5] = { 1,2,3,4,5 };
char str[] = "abcd";
char* ps = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 8);
free(ptr1);
free(ptr3);
}
具体地我们可以通过下图来进行观察:
staticVar,虽然它是一个函数当中所创建的变量,但是因为前面加上了一个static作为修饰,所以它所存放的地址也是【数据段 / 静态区】malloc()、calloc()、realloc(),只要是所涉及的内存分配,都是在堆区中开辟的abcd,就是一个常量字符串,它就是不可修改的,因此是存放在【代码段】动态内存管理就到这里结束了!
在本文的最后呢,我们再来讲一下有关【柔性数组】的相关知识
【概念】:C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
a就被称作为是【柔性数组】typedef struct st_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
typedef struct st_type
{
int i;
int a[]; //柔性数组成员
}type_a;
1、sizeof 返回的这种结构大小不包括柔性数组的内存
2、结构中的柔性数组成员前面必须至少一个其他成员
typedef struct st_type
{
int a[]; //柔性数组成员
}type_a;
3、包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
struct st_type
{
int i;
int a[0]; //柔性数组成员
};
int main()
{
// 1.开辟空间
struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));//44
if (NULL == ps)
{
perror("fail malloc");
return 1;
}
// 2.初始化空间
ps->i = 1;
for (int i = 0; i < 10; i++)
{
ps->a[i] = i;
}
// 3.打印
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->a[i]);
}
// 4.释放
free(ps);
ps = NULL;
return 0;
}
**
当然,就上面这样还体现不出柔性数组的特征,我们要动态地去改变这个数组的大小
realloc()函数去进行操作即可type_a* tmp = (type_a*)realloc(s, sizeof(type_a) + 20 * sizeof(int));
if (NULL == tmp)
{
perror("fail realloc");
exit(-1);
}
struct st_type
{
int i;
int a[0]; //柔性数组成员
};
int main()
{
// 1.开辟空间
struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));//44
if (NULL == ps)
{
perror("fail malloc");
return 1;
}
// 2.初始化空间
ps->i = 1;
for (int i = 0; i < 10; i++)
{
ps->a[i] = i;
}
// 3.打印
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->a[i]);
}
struct st_type* tmp = (struct st_type*)realloc(ps, sizeof(struct st_type) + 20 * sizeof(int));
if (NULL == tmp)
{
perror("fail realloc");
exit(-1);
}
else
{
ps = tmp;
}
// 2.初始化空间
for (int i = 10; i < 20; i++)
{
ps->a[i] = 'W';
}
for (int i = 10; i < 20; i++)
{
printf("%c ", ps->a[i]);
}
// 4.释放
free(ps);
ps = NULL;
return 0;
}
既然结构体的中的数组大小都增大了,那么这个结构体的大小会发生改变吗?
其实对于上面的这一种柔性数组实现,还可以像下面这样去进行设计
typedef struct st_type
{
int i;
char* a;
}type_a;
malloc的形式去申请内存空间,不过这里结构体的空间和数组的空间是分开申请的,只有当结构体的内存空间申请完后,我们才去确立这个数组的大小type_a* s = (type_a*)malloc(sizeof(type_a));
if (NULL == s)
{
perror("fail malloc");
exit(-1);
}
s->i = 100;
char* tmp = (char*)malloc(sizeof(char) * 10);
if (NULL == tmp)
{
perror("fail malloc");
exit(-1);
}
s->a = tmp;
struct st_type
{
int i;
int* a; //柔性数组成员
};
int main()
{
// 1.开辟空间
struct st_type* s = (struct st_type*)malloc(sizeof(struct st_type)); // 分配结构体空间
if (NULL == s)
{
perror("fail malloc");
return 1;
}
s->a = (int*)malloc(sizeof(int) * 10); // 为柔性数组成员分配内存空间
if (NULL == s->a)
{
perror("fail malloc");
free(s);
return 1;
}
s->i = 100;
for (int i = 0; i < 10; i++)
{
s->a[i] = i;
}
// 3.打印
for (int i = 0; i < 10; i++)
{
printf("%d ", s->a[i]);
}
// 4.增加
int* tmp = (int*)realloc(s->a, sizeof(int) * 20); // 使用原指针进行扩展内存空间
if (NULL == tmp)
{
perror("fail malloc");
return 1;
}
else
{
s->a = tmp;
}
return 0;
}
i和a,其中后者是一个字符型指针,又指向了内存中的一块连续区域,它们都是在堆中的malloc出来的a就找不到了// 释放
free(s->a);
s->a = NULL;
free(s);
s = NULL;
最后我们再来对比分析一下这两种方法的区别
【对比分析】:
从下图来分析两种形式我们可以观察到三个不同点:
malloc一次free;malloc两次free;第一个好处是:方便内存释放
第二个好处是:这样有利于访问速度
malloc之后,内存中就会产生多个内存碎片,所以我们应该尽量减少malloc和free的此处。不过呢,我个人觉得也没多大区别了,反正你跑不了要用做偏移量的加法来寻址因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- gamedaodao.com 版权所有 湘ICP备2022005869号-6
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务