目录
一、动态内存分配与传统开辟内存的比较
二.动态内存函数
2.1 malloc 和 free
?2.2 malloc和free具体使用
?2.3 calloc
2.4 realloc
三、常见的动态内存错误
3.1 对NULL指针的解引用操作
3.2 对动态开辟空间的越界访问
3.3 对非动态开辟内存使用free释放
3.4 使用free释放动态开辟内存的一部分
?3.5 对同一块动态内存多次释放
3.6 动态开辟内存忘记释放(内存泄露)
四、关于动态内存管理比较经典的题目
4.1 题目1:
4.2 题目2:
4.3 题目3:
4.4 题目4:
?五、C/C++程序的内存开辟
六、柔性数组
6.1 柔性数组的概念
?6.2柔性数组的特点
6.3 柔性数组的使用
一、动态内存分配与传统开辟内存的比较
我们一致的内存开辟方式有:
int val =20;//在栈空间上开辟4个字节。
char arr[10]={0};//在栈空间上开辟10个字节。
?上述开辟空间的方式有两个特点:
1.空间开辟大小是固定的。
2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
?但是有时空间的大小在运行时才能获知,那么上述开辟空间的方式就不能满足了,于是就产生了动态内存开辟。
二.动态内存函数
2.1 malloc 和 free
void* malloc (size_t size);
?这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,就返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体使用的时候使用者自己决定强转哪种类型。
如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
?另外在使用malloc的时候,要和它的好搭档free搭配使用,free是专门用来做动态内存的释放和回收的。
void free (void*? ptr);
?free函数用来释放动态开辟的内存。
具体使用:
1.如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。(错误的)
2.如果参数ptr是NULL,则函数无操作。
?2.2 malloc和free具体使用
#include<stdio.h> #include<errno.h> int main() { ?? ?int num; ?? ?scanf("%d", &num); ?? ?int* ptr = NULL; ?? ?ptr = (int*)malloc(num * sizeof(int)); ?? ?if (num == NULL)//判断是否为空 ?? ?{ ?? ??? ?printf("%s\n", strerror(errno)); ?? ?} ?? ?for (int i = 0; i < num; i++) ?? ?{ ?? ??? ?*(ptr + i) = 0; ?? ?} ?? ?free(ptr);//释放ptr所指向的动态内存 ?? ?ptr = NULL; ?? ?return 0; }
?这里要申明两件事:
1.开辟空间后做检查是否为NULL指针是非常重要的。
2.注意误区:free动态开辟的内存后,该动态开辟的空间虽然被释放,但是地址仍旧存在,指针不为NULL,可以访问,但会出错,所以养成良好习惯,要置为NULL指针。(务必记住)
3.free函数只能释放动态开辟的内存或者空指针,释放其他内存会出错。
?2.3 calloc
C语言还提供了一个函数叫calloc,calloc函数也用来动态内存分配,原型如下:
void* calloc(size_t num, size_t size);
1.函数的功能:为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
2.calloc函数与malloc的区别在于calloc会在返回地址之前把申请的空间的每个字节初始化为0.
?例如:
?所以如果要求我们对初始开辟的空间进行初始化时,我们可以很使用calloc函数很轻松解决这个问题。
2.4 realloc
realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型:
?void* realloc (void* ptr,size_t size);
函数解析:
· ptr是要调整的内存地址
· size 调整之后新大小
· 返回值为调整之后的内存起始位置
· 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间时存在两种情况:
1.原有空间之后有足够大的空间。
?对于情况一,要扩展内存就直接原有内存之后直接追加空间。
2.原有空间之后没有足够大的空间。
?
?当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。 由于上述的两种情况,realloc函数的使用就要注意一些,我们通常会再创建一个变量来充当桥梁的作用用于过渡。
譬如:
?
int main()
{
int* ptr = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
}
int* p = (int*)realloc(ptr, 100);
if (p != NULL)
{
ptr = p;
p=NULL;
}
free(ptr);
ptr = NULL;
return 0;
}
?
通过对上述函数的介绍,我们对于动态开辟内存有了一定的了解,简单总结一下需要注意:
1.开辟空间后要记住做检查是否为NULL指针。
2.注意free动态开辟的内存后,指针不为NULL,可以访问,但会出错,所以养成良好习惯,要置为NULL指针。(务必记住)
3.free函数只能释放动态开辟的内存或者空指针,释放其他内存会出错。
4.在使用realloc函数时要记住创建一个中间变量来过渡,使用过后,记得中间变量置为NULL空指针,防止有两个地址指向同一块空间,防止因为情况一空间不够导致开辟失败。
三、常见的动态内存错误
3.1 对NULL指针的解引用操作
? 这个问题也就是我在上面提到的,动态开辟后要进行检查,防止为NULL指针。
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
//要进行检查
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
3.2 对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//访问
int i = 0;
for (i = 0; i <= 10; i++)//越界访问
{
p[i] = i;
}
free(p);
p = NULL;
return 0;
}
这个问题比较常见,在常规开辟内存时也是应该注意的问题。
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int* p = &a;
free(p);//ok?
}
答案肯定是不ok的,free只能释放动态开辟的空间或者是NULL空指针(无操作).
3.4 使用free释放动态开辟内存的一部分
这个问题比较不容易发现,很多时候调试出现错误可能就是因为这里出了错。
大家可以看一下以下两组代码,例如:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//释放
free(p);//仅释放一部分,不合理,应该释放指向起始位置的。
p = NULL;
return 0;
}
代码二:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//释放
free(p);//仅释放一部分,不合理,应该释放指向起始位置的。
p = NULL;
return 0;
}
这两段代码的不同之处就在于,malloc开辟空间初始化的方式,在这里第一种不会出错,而第二种则会出错。
希望大家要注意++是有副作用的,这里第一个代码p指针所指向的地址始终是开辟空间的初始地址,而第二个代码p的地址被++给更改,这样free就会出错。
?3.5 对同一块动态内存多次释放
例如:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
这个问题比较初级,一般也不会有人犯这样的低级错误。
3.6 动态开辟内存忘记释放(内存泄露)
内存泄露是非常严重的问题,虽然说在一个进程结束之后堆开辟的空间也就是动态开辟的空间会被销毁,但是很多时候比如游戏服务器它是不关机一直运行的,如果发生内存泄露这是非常严重的问题。
尽管你可能非常注意避免内存泄漏,但是也可能会出现这样的失误,比如:
void test()
{
int* p = (int*)malloc(40);
//...
int flag = 0;
scanf("%d", &flag);
if (flag == 5)
return;//这里如果return 就会出现内存泄露
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
这段代码就是如此,尽管你注意最后free掉了p,但是在中间如果满足条件return的话还是会出现内存泄露。
四、关于动态内存管理比较经典的题目
4.1 题目1:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
运行Test函数会有怎样的结果,会出现打印"hello world"吗?
答案肯定不会,首先我们Test函数开始分析,首先GetMemory函数传过去的是值,不是地址,也就是说,在GetMemory函数内对str的任何操作都不会影响到str,所以str还是NULL指针,所以下一步对str也就是NULL指针解引用会出错,也就打印不出来。再来看GetMemory函数内部,开辟之后没有进行是否为NULL指针进行检查,也没进行释放,造成内存泄露。
4.2 题目2:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
运行Test函数会有怎样的结果?
这段代码同样有问题,虽然这回把地址传给了str,但是形参出了函数就会被销毁,所以造成的后果就是str是野指针。
4.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);
}
运行Test函数会有怎样的结果呢?
这段代码的问题就是没有对动态开辟的内存释放造成内存泄漏。
4.4 题目4:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这段代码的问题就是
1.编码习惯不好,free完之后,没有置为NULL指针。
2.free之后动态开辟的空间就被释放,无法访问,有地址没空间,str是野指针。
?五、C/C++程序的内存开辟
?C/C++程序内存分配的几个区域:
1.栈区(stack):在执行函数时,函数内局部变量的存储单位都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
2.堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。 4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
六、柔性数组
6.1 柔性数组的概念
在C99标准中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
?譬如:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
?6.2柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。 2.sizeof 返回的这种结构大小不包括柔性数组的内存。 3.包含柔性数组成员的结构用,malloc()函数进行内存的动态分配,并且分配的 的内存应该大于结构的大小,以适应柔性数组的预期大小。
?对于第二点我们可以代码验证一下:
6.3 柔性数组的使用
我们通常在给包含柔性数组的结构体开辟动态内存时,习惯柔性数组需要多大我们就给多大空间。
譬如:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
int main()
{
type_a* ptr = (type_a*)malloc(sizeof(type_a) + 10*sizeof(int));//后面40个字节是给arr提供的
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
}
ptr->i = 100;
int j = 0;
for (j = 0; j < 10; j++)
{
ptr->a[j] = j;
printf("%d ", ptr->a[j]);
}
//柔性数组是malloc(动态内存开辟)的,可以realloc调整大小,体现了柔性
type_a* ps = (type_a*)realloc(ptr, sizeof(type_a) + 20*sizeof(int));
if (ps != NULL)
{
ptr = ps;
ps = NULL;
}
//
free(ptr);
ptr = NULL;
return 0;
}
通过上图我们可以发现柔性数组使得我们对于结构体使用的更加方便。
当然各位同学可能还是不太体会到柔性数组的好处,我们对于上述代码同样,再使用普通方式开辟空间。
struct S { ?? ?int n; ?? ?int* arr;//采用这样的结构,不是柔性数组 };
int main() { ?? ?printf("%d\n", sizeof(struct S));//大小为8 ?? ?struct S* ps=(struct S*)malloc(sizeof(struct S));// ?? ?if (ps == NULL) ?? ?{ ?? ??? ?printf("%s\n", strerror(errno)); ?? ?} ?? ?ps->n = 100; ?? ?ps->arr = (int*)malloc(40); ?? ?if (ps->arr == NULL) ?? ?{ ?? ??? ?printf("%s\n", strerror(errno)); ?? ??? ?return 1; ?? ?} ?? ?//使用了 ?? ?int i = 0; ?? ?for (i = 0; i < 10; i++) ?? ?{ ?? ??? ?ps->arr[i] = i; ?? ?} ?? ?for (i = 0; i < 10; i++) ?? ?{ ?? ??? ?printf("%d ", ps->arr[i]); ?? ?} ?? ?//释放 ?? ?//释放两次 ?? ?//注意先释放数组,再释放结构体 ?? ?//扩容 ?? ?int* ptr = (int*)realloc(ps->arr, 80); ?? ?if (ptr == NULL) ?? ?{ ?? ??? ?return 1; ?? ?}//两次malloc,还要注意free顺序,比较麻烦,还容易出现内存泄露 ?? ?//还有malloc次数越多,内存碎片越多,使得程序变得繁杂 ?? ?free(ps->arr); ?? ?free(ps); ?? ?ps = NULL; ?? ?return 0; }
?通过对比我们发现第二种方式的坏处:
1、两次malloc开辟空间,产生较多的内存碎片,使得程序的运行效率受到影响,不如方法一采用柔性数组。
2、free两次,还要注意顺序,先free掉结构体里面的数组,不能先free掉结构体,这样会导致丢失动态开辟的数组,造成内存泄露,所以不仅要free两次,还要注意先后,比较繁琐还容易出错。
?
?
|