回顾:
1.为什么要有动态内存管理?
2.动态内存管理是在堆区上进行内存空间的开辟,开辟到是一块连续的空间
- malloc -- 不初始化
- realloc -- 初始化为0
- realloc -- 调整内存空间的大小,如果第一个参数是NULL,那么它也具有malloc函数的功能
这里补充一下
int main()
{
int * p = ( int* ) realloc ( NULL,40 );
//等价于malloc ( 40 ) ;
return 0 ;
}
- free -- 释放动态内存开辟的内存空间的
1.常见的动态内存错误
1.1对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
正确做法:
1.3对非动态开辟内存使用free释放
void test()
{
int a = 10;
int* p = &a;
free(p);
p=NULL;
}
局部变量进作用域范围创建,出作用域范围销毁
1.4使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//此时的p不指向动态内存的起始位置
}
对于free函数的正确使用:
//正常p的释放是指向动态内存开辟空间的起始地址的
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
return 1;
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
return 0;
}
1.5对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
1.6动态开辟内存忘记释放(内存泄露)
//正常的使用,开辟完,在函数的结尾要释放掉这块空间
void get_memory()
{
int* p = (int*)malloc(40);
//使用...
free(p);
p = NULL;
}
//函数会返回动态开辟空间的地址,记得在使用之后释放
int* get_memory()
{
int* p = (int*)malloc(40);
//...
return p;
//此时p开辟的空间没有释放掉
}
int main()
{
int* ptr = get_memory();
//释放
free(ptr);
ptr = NULL;//现在申请的40个字节空间回收了
//如果没有释放这申请的40个字节的空间就造成了内存泄露
return 0;
}
2.几个经典的笔试题
2.1 题目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;
}
- 现在str内部放NULL,GetMemory函数调用将str传过取,p是指针变量得到str的内容,p得到NULL,然后为p开辟100个字节的空间,但是这些操作对p而言的,对str没有什么影响(p是一个变量,str是另外一个变量),调用完之后,到strcpy函数这里,此时ptr还是空指针,所以不能将Hello world放到str中(核心问题)
- 调用GetMemory函数的时候,str的传参为值传递,p是str临时拷贝,所以GetMemory函数内部将动态开辟空间的地址存放在p中的时候,不会影响str,所以GetMemory函数返回之后,str中依然是NULL指针。strcpy函数就会调用失败,原因是对NULL解引用操作,程序会崩溃
- 存在内存泄露,没有释放p所开辟的空间(并且p在Test函数中并不能释放,因为p出了作用域的范围就被销毁,所以并不能在Test函数中释放)
- 还没有对malloc函数的返回值进行合理的判断
上面代码的修改
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "Hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
//此时就能打印出Hello world了
2.2 题目2:
char* GetMemory(void)
{
char p[]="hello world";
return p;
}
void Test(void)
{
char* str=NULL;
str=GetMemory();
printf(str);
}
并不能打印出hello world,p是局部的数组,p出了作用域就被销毁了,p这块空间的内容换给了操作系统
返回栈空间地址的问题,数组是局部变量在栈中开辟,而栈上的空间出了作用域就被销毁
GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给str,但是数组的内存出了GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str,str就是野指针
注意下面这段代码是没有问题的
a这块空间在销毁之前会找一个寄存器把a的内容放进去,a出了作用域范围被销毁,然后将寄存器中的内容放到ret中
此时这样写又变成了栈空间地址的问题,但是此时我们还能打印出10,这是为什么?
因为此时没有其他变量使用这块空间,所以这块空间的内容放的还是10,当有其他变量时,就会将这块空间覆盖掉,这时就不能打印出10
2.3 题目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;
}
这里能打印出hello,但是存在内存泄露的问题,在Test函数内部free(str),并将str=NULL;
2.4 题目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;
}
这段代码打印出world,虽然打印结果,但本质还是错误的;这段代码存在非法访问,给str开辟了100个字节的空间然后又释放掉,此时str没有置为NULL,还记得这段已经被释放空间的起始地址,str确实不等于NULL(这里str时野指针),接下的操作(strcpy)就是非法访问
修改:
1.在开辟好空间之后判断是否开辟成功,if(str==NULL)return 1;
2.在free函数后面,将str置为空(str=NULL;)
3.C/C++程序的内存开辟
局部变量和函数参数是放到栈区上的,动态内存管理放到堆区,静态变量和全局变量放到静态区中(数据段)
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量,函数参数、返回数据、返回地址等
- 堆区(heap):一般又程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。分配方式类似于链表。
- 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码
实际上普通的局部变量是在栈区分配空间的,栈区的特点是创建的变量出了作用域就销毁,但是被static修饰的变量存放在数据段(静态区),数据段的特点是创建的变量直到程序结束才销毁,所以生命周期变长
4.柔性数组
在C99中,结构中的最后一个元素允许是未知的大小的数组,这就叫做柔性数组成员
eg:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
4.1柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的大小
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小--->在创建结构体变量的时候,这个变量中是没有这个柔性数组的,只有n和s(下面这张图片的代码),所以包含柔性数组时,整个结构体(不仅仅给n和s开辟空间,还要给柔性数组开辟空间)应该使用malloc函数来开辟空间
4.2柔性数组的使用
struct S
{
int n;
float s;
int arr[];//[柔性]数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
//既为n、s开辟空间,也为arr开辟了空间
if (ps == NULL)
{
return 1;
}
//使用
ps->n = 100;
ps->s = 5.5f;
int i = 0;
for (i = 0; i < 4; i++)
{
ps->arr[i] = i;
}
printf("%d %lf\n", ps->n, ps->s);
for (i = 0; i < 4; i++)
{
printf("%d ", ps->arr[i]);
}
//调整,使用realloc函数进行调整
struct S*ptr=(struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 10);
if (ptr == NULL)
{
return 1;
}
else
{
ps = ptr;
}
//释放
free(ps);
ps = NULL;
return 0;
}
4.3柔性数组的优势
struct S
{
int n;
float s;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
return 1;
ps->n = 100;
ps->s = 5.5f;
int* ptr = (int*)malloc(4 * sizeof(int));
if (ptr == NULL)
{
return 1;
}
else
{
ps->arr = ptr;
}
//使用
printf("%d %lf\n", ps->n, ps->s);
int i = 0;
for (i = 0; i < 4; i++)
{
scanf("%d",&(ps->arr[i]));
printf("%d ", ps->arr[i]);
}
//调整
//调整的时候只需要调整arr
realloc(ps->arr, 10 * sizeof(int));
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
上面是不使用柔性数组的方法,这种方法使用两次malloc,很有可能这次malloc开辟的空间不是一块连续的空间,有可能是分开的两段空间,造成内存碎片(内存和内存之间存在缝隙,这个内存碎片空间也不好使用),malloc次数越多,出现内存碎片的可能性越大,内存的利用率下降,并且需要两次释放,如果忘记释放就会造成内存泄露,所以还是使用柔性数组更好一些
柔性数组的优点:
- 第一个好处是:方便内存释放
- 如果我们的代码是在一个给别人用的函数中那个,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉
- 第二个好处是:这样有利于访问速度
- 连续的内存有益于提高访问速度,也有益于减少内存碎片。