一、动态内存函数
a、栈上开辟空间该特点为:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
所以我们会产生如下思考:
有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了
关于static关键字修饰的局部变量:
普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
C/C++程序的内存开辟:
二、动态内存函数
1、malloc函数
void* malloc (size_t size);
#include<stdlib.h>
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
struct boo { char* name; int quantity; }g,g1; int main() { g.name = "小小"; g.quantity = 97; printf("%s %d\n", g.name, g.quantity); g1.name = (char*)malloc(4);//要先为name开辟一个空间才能存放拷贝对象 strcpy(g1.name, "大大"); g1.quantity = 17; //strcpy(g1.name, "大大");//error, printf("%s %d", g1.name, g1.quantity); g1.name=NULL; free(g1.name); return 0; } 第一种对name初始化的方法是将小小存放在常量区的地址存放到name指针 第二种是将原来创建name指针后随意存放的地址空间上去存放拷贝对象大大,是非法访问 我们要为name开辟一个可使用的空间这里使用了malloc函数
要注意malloc函数原型为void* malloc (size_t size);,
a、void代表该函数其实并不知道自己是为什么类型的数据开辟空间,但我们在使用的时候可以进行强制类型转换。
b、每次用完动态内存函数,记得要用free函数(专门用来做动态内存的释放和回收)
1.1、free函数(用来释放动态开辟的内存)
void free (void* ptr);
#include<stdlib.h>
注意:
a、如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
b、如果参数 ptr 是NULL指针,则函数什么事都不做。
c、free(p)只是代表将malloc等动态内存函数申请的内存空间释放,
这意味着仍存放着该空间的地址,并不会做到将p置为空(NULL),我们
要记得自己将其置为null(防止指针空间数据没有及时清空)
d、记得每次在函数中调用一个指针时可以先判断是否为空指针
If(p!=NULL)或者if(NULL==p)或者assert(p)等都可以
void Test(void) { char *str = (char *) malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } }
上述错误代码可以直到,每次free后要记得把开辟动态内存空间的指针指针置成空指针,因为free完后,指针还保留着原地址,但所指的那片空间已经被释放,会产生非法访问。
2、calloc函数
void* calloc (size_t num, size_t size);
和malloc函数一样用来动态内存分配,不同的是:
a、calloc函数为 num 个大小为 size 的元素开辟一块空间,形如:
int *p = (int*)calloc(10, sizeof(int));
b、 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
所以如果在对申请空间内容要求初始化的条件下我们可以使用这个函数
3、realloc函数
void* realloc (void* ptr, size_t size);
a、ptr 是要调整的内存地址
b、size 调整之后新大小
c、返回值为调整之后的内存起始位置。
d、这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。(所以要开辟空间的指针,当后面开辟空间足够时,为原来ptr所指向的起始地址,如果后面的空间不足再开辟,则会另找一个地址开始开辟)
如下请况可以更好的理解
realloc在调整内存空间时存在两种请况:
int main() { int *ptr = (int*)malloc(100); if(ptr != NULL) { ptr = (int*)realloc(ptr, 1000); //在第一种情况中,开辟后的地址仍为原地址(在原有内存之后直接追加空间) //在第二种情况中,因为往后追加空间不够,在堆空间上另找一个合适大小 //的连续空间来使用。这样函数返回的是一个新的内存地址。s这时ptr存放的是新地址 //虽然以上另种情况都适用,但是当申请空间太大时,则会返回0. //在这种情况下用原来的指针接收函数值则意味着指针指向的地址遗失 } free(ptr); ptr=NULL; return 0; } 所以正确应为: int main() { int *ptr = (int*)malloc(100); int*p=NIULL; p = (int*)realloc(ptr, 1000);//创建一个指针去接收,在判断其不为空指针时再赋值给原指针 if(p != NULL) { ptr=p; } free(ptr); ptr=NULL; return 0; }
如上使用该函数
补充:
int* p = (int*)realloc(NULL, 100);
该功能类似于malloc函数,直接在堆区开辟100个字节的空间大小
二、动态内存开辟常见错误
1、对NULL指针的解引用操作
void test() { int *p = (int *)malloc(100); *p = 20;//如果p的值是NULL,就会有问题(即p=NULL;*p=20) free(p); } 所以一般都会先判断其是否为空指针
应该用if判断一下,是否为空再进行赋值操作。
返回栈空间地址问题:
(静态开辟的空间是在栈上,栈上开辟的空间出了作用域就销毁了)
所以如果是在函数内部创建了动态内存则出了函数不会被销毁,因为其开辟的空间在堆上
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL;//没有为str开辟空间 str = GetMemory(); printf(str); } int main() { Test(); return 0; }
如上代码的问题:
函数内部创建的数组在栈区上创建的,出了函数数组空间还给了操作系统,返回的地址没有实际意义,如果通过返回的地址去访问内存就是非法访问内存的。
2、对动态开辟空间越界访问
void test() { int i = 0; int *p = (int *)malloc(10*sizeof(int)); if(NULL == p) { exit(EXIT_FAILURE);//表示没有成功的执行一个程序 } for(i=0; i<=10; i++) { *(p+i) = i;//当i是10的时候越界访问 } free(p); }
补充:(稍稍了解一下)exit( ),_exit( ),与return_Lapland Stark的博客-CSDN博客_return exit _exit
exit函数所在头文件:stdlib.h(如果是”VC6.0“的话头文件为:windows.h)
stdlib.h头文件中 定义了两个变量:
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1exit(EXIT_SUCCESS):表示安全退出
exit(EXIT_FAILURE):表示异常退出
但又有了解到,
exit(0)、exit(1)等说法,
联想起一直使用的return来看:
return是函数的退出(返回),exit是进程的退出(退出应用程序),并将程序的一个状态返回给OS(操作系统),这个状态标识了应用程序的一些运行信息,(一般0为正常退出,非0为非正常退出)
(实际上C标准只规定EXIT_FAILURE值表示程序执行失败。其他的status值以及含义都是由实现自行规定的)
来源于网上
3、使用free释放非动态开辟的空间
void test() { int a = 10; int *p = &a; free(p);//p不是由动态内存函数开辟的空间 }
非堆分配的内存是不需要free的
这个问题应该属于没有正确开辟内存空间吧:
void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str);//传值调用 strcpy(str, "hello world"); printf(str); } 问题在于: 但因为该函数形参p为实参的临时拷贝,在函数内部申请空间的地址,存放在p中,不会影响str,所以当函数返回之后,str依然是NULL,strcpy会失败。 且因为在函数中开辟后并没有回收那片空间,而形参p在函数返回之后被销毁,使得内存开辟的那片空间大小存在内存泄漏,无法释放。
a、指针初始化为NULL,在还没有分配内存的前提下用strcpy函数去放置拷贝内容,会报错。因为未分配内存就意味着没有一个合法指向的地址,随机指向的一个空间(形成野指针)是无法使用的
b、需要注意的是,没有初始化的指针是会随机指向一个地址的,相对于置于NULL的做法来看,只是更加确切的知道后者的情况是该指针指向的0x0这个地址,
c、所以在一个指针没有初始化时可以先指向NULL,(也可直接初始化分配内存指向一个地址)再分配内存,
我们可改成如下两种代码:
char* GetMemory(char* p) { return p = (char*)malloc(100); } void Test(void) { char* str = NULL; str= GetMemory(str); strcpy(str, "hello world"); printf(str); } //上面方法等价于: 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接收该地址后,便指向那片开辟的空间,并进行使用。
第二种形式:在传址调用的基础上去开辟str所需内存空间,那样可以直接使用。
补充:
指针作为实参时的传值和传址:
我们要知道形参i其实是对实参的临时拷贝,所以当我们把指针b传过去时,实质和一个不是指针变量的值传递的效果相同(传值效果)
void aaa(int* b) { *b = 17; b = b + 2; printf("%p\n", b); } int main() { int a = 1; int* b = &a; aaa(b); printf("%d %d\n", a, *b); printf("%p", b); return 0; }
如下图所示,当在aaa函数中对b指针所指地址进行改变后,在主函数中指针b所指地址的打印的结果和aaa函数的不同,其所存地址仍为原来的没有改变,所以本质上是传值的效果。
但是我们可以发现我们对指针所指的内容发生了改变,这就是和普通变量传值中的不同之处。
同理可得,当我们的实参为(&p)即传的是指针的地址时,则形参应用二级指针接收。
void aaa(int** b) { *b = *b + 1; **b = 17; printf("%p\n", *b); } int main() { int a[3] = { 11,22,77 }; int* b = a; printf("%p %p\n", b, &a);//这里打印一开始被赋予的数组a的地址,和下面的地址进行比较 aaa(&b);//传的是指针b的地址,传址调用 printf("%d %d\n", a[1], *b);//17 17 printf("%p", b);//和函数中的地址相同,说明对指针所指向的地址发生了改变 return 0; }
由此可以发现,指针传址调用既可以改变指针所传地址,又可以改变所指地址的内容,而指针传值,只能改变指向地址的内容,无法改变指向的地址
4、使用free释放一块动态开辟内存的一部分
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置,意味着只释放了后面的空间) }
带来的后果有:
a、释放后面部分的空间
b、如果不记得p指针是如何指向来的地址的,则会遗失起始地址
5、对同一块动态内存多次释放
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
虽然可能不像上述代码那么明显的重复释放了,也有可能是出了函数后忘记已经释放然后再此释放
6、动态开辟的空间忘记释放——可能会造成内存泄漏
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } }//开辟了一个动态空间后并没有释放 int main() { test(); while(1);//这是一个死循环,代码不再向下执行 } //或是这种情况,也是忘记释放: void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
补充:
while(1){}会重复执行{}中的代码,一般用于检测一部分代码的执行情况,防止后面的代码干扰执行结果。可以在{}的代码中设置跳出循环的条件并以break结束。
动态开辟空间,两种回收方式,
1、主动free
2、程序结束
三、柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
1、柔性数组的特点
a、结构中的柔性数组成员前面必须至少一个其他成员。
b、sizeof 返回的这种结构大小不包括柔性数组的内存。
c、包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct c//包含柔性数组成员的结构大小不包含柔性数组的大小 { int a; int b[0];//柔性数组之前有一个变量n }; int main() { struct c* p = (struct c*)malloc(sizeof(struct c) + 10 * sizeof(int));//sizeof(struct c)是给a开辟的空间,后面则是给柔性数组b开辟的空间 //为实现柔性数组空间大小可变,用动态函数对柔性数组开辟空间 if (p) { p->a = 10; int i = 0; for (i = 0; i < 10; i++) { p->b[i] = i; } } struct c* p1 = (struct c*)realloc(p, 20); if (p1)//p!=NULL { p = p1; } free(p); p = NULL; return 0; }
struct c { int a; int*b; }; int main() { struct c* p= (struct c*)malloc(sizeof(struct c));//为结构体成员a和b开辟空间 if (p == NULL) return 1; p->a = 10; p->b= (int*)malloc(10*sizeof(int));//为b开辟一块内存空空间 if (p->b == NULL) return 1; int i = 0; for (i = 0; i < 10; i++) { p->b[i] = i; } int*p1= (struct c*)realloc(p->b,10 * sizeof(int));//记得这里是为p->b扩展空间,其为int*型 if (p1)//每次开辟用动态内存开辟函数时都要再判断一下是否为空指针 { p->b = p1; } free(p->b);//释放时要先释放为p->b开辟的空间, p->b = NULL; free(p);//再释放为成员对象开辟的空间,因为如果先释放这个空间的话将找不到后面为p->b的空间 p = NULL; return 0; }
比较上面两种代码后可以看出柔性数组的以下好处:
第一个好处是:
方便内存释放 :如果我们的代码是在一个给别人用的函数中,我们在里面做了二次内存分配,并把整个结构体返回给 用户。
用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:
这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。(可能也没多高了但总归要用做偏移量的加法来寻址)