我们以往学过的内存开辟无非是
int a=10;//在栈上开辟4个字节 char arr[10]={0};//在栈上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1、空间开辟的大小是固定的
2、数组在声明时必须指定数组长度,它所需要的内存在编译的时候分配
但是对于空间的需求,我们不仅仅满足上述情况。有时我们需要的空间大小在程序运行时才能知道,那数组在编译时开辟空间的方式就不能满足了,这时就只能试试动态内存开辟了。
malloc
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
free
void free (void* ptr);
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的
int main() { int* p = (int*)malloc(10 * sizeof(int)); //开辟失败 if (p == NULL) { printf("malloc fail\n"); exit(-1); } //开辟成功 for (int i = 0; i < 10; i++) { *(p + i) = i; printf("%d ", p[i]); } printf("\n"); free(p); p = NULL; return 0; }
1、如果你开辟的空间过大有可能会开辟失败,所以必须检查。
2、开辟成功了,因为是连续的空间,所以p相当于一个数组。
3、最后记得释放动态开辟的空间,你拿的就要还回去,防止内存泄漏。
4、最后要把该指针置空NULL,有什么必要?
指针被free后,指针指向的还是原来的区域,但是这片区域已经不归自己使用了,这片区域可以被别人用,被别人覆盖了,所以你已经变成野指针了,如果你不置为空,你去访问这个地方了,就造成非法访问了,会有不安全因素。且可以防止对一个已经释放的指针多次释放,造成程序崩溃,但我们可以对NULL指针多次释放。
举个例子:
比如你有一个女朋友,有一天你和她分手啦,这里相当于free,她已经不属于你了,你们之间已经没有关系了,她现在可以成为别人的女友了,但是你脑子里还记者她,还记着她的联系方式,这里相当于p指针还指向原来的内容,这样对她是不好的,因为你还能根据联系方式去打扰她的生活,置为NULL,就是清除你对她的记忆,喝下忘情水。(当然祝大家都幸福哦)
calloc
void* calloc (size_t num, size_t size);
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
realloc
void* realloc (void* ptr, size_t size);
情况1:原有空间之后有足够大的空间
要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化
情况2:原有空间之后没有足够大的空间
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,前面的数据会拷贝下来,这样函数返回的是一个新的内存地址,之前的realloc会自己free掉
1、对NULL指针的解引用操作
void test() { int *p = (int *)malloc(4); *p = 10;//如果p的值是NULL,就会有问题 free(p); p=NULL; }
所以使用动态内存分配,需要判断是否开辟成功,如果成功再使用,否则不能使用,返回NULL,但是不能对NULL解引用
2、对动态开辟空间的越界访问
int*p=(int*)malloc(200); for(int i=0;i<80;i++) { //.... }
总共申请了200÷4=50个元素,而你的for循环的判断条件到80了,所以当大于50的时候,会出现越界访问。
3、对非动态开辟内存使用free释放
void test() { int a = 10; int *p = &a; free(p);//可以吗? }
显然是不可以的,因为a是在栈上开辟的空间,不是堆上
free只能释放堆上动态开辟的空间!
4、使用free释放一块动态开辟内存的一部分
void test() { int *p = (int *)malloc(100); p++; free(p);//可以吗? }
当然是不可以的,因为自增后,p指向的位置改变了,而free释放必须是释放全部的动态开辟的空间(起始位置),不能是部分。
5、对同一块动态内存多次释放
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
不可以对一个同一块动态内存重复释放,但是可以这样
free(p); p=NULL; free(p); p=NULL;
6、动态开辟内存忘记释放(内存泄漏)
首先在堆上申请的空间有两种回收方式
1、free
2、程序退出时,申请空间自动回收
如果不对开辟的空间进行释放,则会造成内存泄漏,你的电脑就会越来越卡
所以当使用完动态开辟的内存,一定要记得释放
看几个笔试题
题一:
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; }
该程序结果是什么?为什么会有这样的结果?
结果是:程序崩溃,什么也不打印
原因:
str传给p的时,是值传递,p是str的临时拷贝,当malloc开辟空间的起始位置放到p中时,str并没有改变还是NULL;
当str是NULL,strcpy要把hello world拷贝到str指向的空间时,因为str是NULL,所以不知道拷到哪里,程序崩溃。
那我们应该怎么更改呢?
很简单,我们应该传str地址,这样*p就是str
题二:
char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }
结果是啥?
为啥是随机值?
这里需要一点函数栈帧的知识
函数在栈上开辟栈帧,函数返回,销毁栈帧,当栈帧销毁后,里面的东西都还给操作系统了,return p,这里的返回值p是通过寄存器(eax)传回来的,把p的地址通过寄存器赋给str,虽然str拿到了p的地址,但是栈帧已经销毁,p地址指向的空间已经不属于p了,这时候p其实已经算是野指针了,如果你再去访问这个空间,就造成了非法访问了。如果空间内容没有被覆盖,还有可能打印出来,如果被别人使用了,就打印随机值了。
题目三:
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; }
这个代码结果是什么?有什么问题?
看似打印出来了,其实已经出现了问题
char* str = (char*)malloc(100);
strcpy(str, "hello");
这两句没有问题,str指向malloc出来的空间,strcpy把hello拷贝到这片空间
free(str);
if (str != NULL)
{
strcpy(str, “world”);
printf(str);
}
str被free,则malloc出来的空间还给操作系统,不属于自己了,但是str指向的地址没有变,只是变成野指针了,if判断进去,strcpy(str, “world”);这里就出现问题了,因为这片空间已经不属于自己了,你又使用了,所以造成非法访问了,虽然最后打印出来了了,但是早已出错了。
我们应该在free完之后就应该把str置空,养成好习惯,才不容易出错
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
现在我们是否就理解了static关键字修饰局部变量的例子了,为啥生命周期会改变
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁 所以生命周期变长
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例如:
struct S { int i; int a[0];//柔性数组成员 };
有些编译器会报错,无法编译可以改成下面
struct S { int i; int a[];//柔性数组成员 };
柔性数组的特点:
struct S { int i; int a[];//柔性数组成员 }; printf("%d\n",sizeof(struct S));//结果是4
柔性数组的使用
//代码1 struct S { int i; int a[]; }; int i = 0; struct S*ps= (struct S*)malloc(sizeof(struct S)+100*sizeof(int)); p->i = 100; for(i=0; i<100; i++) { p->a[i] = i; } free(p); p=NULL;
这样柔性数组成员a,相当于获得了100个整型元素的连续空间
柔性数组的优势
//代码2 struct S { int i; int *a; }; struct S*p = (struct S*)malloc(sizeof(struct S)); p->i = 100; p->a = (int *)malloc(100*sizeof(int)); for(i=0; i<100; i++) { p->a[i] = i; } free(p->a); p->a = NULL; free(p); p = NULL;
上述 代码1 和 代码2 可以完成同样的功能,但是谁更好呢?代码一更好!
原因:
1、方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户free一次可以释放结构体,但是他不知道结构体里成员也是分配出来的,也需要free释放,我们不能指望用户自己发现这个事。如果我们用代码一这种,结构体内存和成员内存一起只分配一次,返回给用户一个指针,那么用户free一次就可以释放所有内存。
2、有利于访问效率
连续的内存有利于提高访问速度,且减少内存碎片(这点效率没有提升很高,都要用偏移量的加法来寻址)
这期就到这里啦,感谢阅读,我们下期再见
如有错 欢迎提出一起交流
关注周周汪哦
关注三连么么么哒