开辟内存的方式
目前已知的是使用变量类型来开辟内存空间:
int a = 10;//在栈区开辟4个字节
char arr[10] = {0};//在栈区开辟10个字节
但这样有些问题:
- 空间的大小固定
- 数组声明后长度不变。
当需要的内存空间不定时,以上的方式就没法满足了。因此引入了动态内存开辟,让程序员可以自己申请和释放空间。
malloc和free
malloc
看一下malloc
的形式:
void* malloc(size_t size)
malloc
有以下几个要点:
malloc
会向内存申请一块连续可用的空间,返回这块空间的指针;- 如果开辟失败,返回一个
NULL
,因此malloc
一定要做返回值检查; - 返回类型是
void*
,依据使用者的需求来决定具体是什么类型的; - 如果
size = 0
,malloc行为是没有定义的,取决于编译器。
free
free用于释放动态内存。形式如下:
void free(void* ptr);
free
有以下几个要点:
- 如果
ptr
指向的空间不是动态开辟的,free
函数的行为未定义; - 如果
ptr
是空指针,函数什么都不做。
使用举例(malloc & free)
include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
稍微分析一下代码在做什么:
首先是建立了一个变长数组arr,然后通过malloc为其分配空间,判断malloc返回值之后对这个数组赋值,使用完成后释放掉ptr,然后把ptr置为NULL。
calloc和realloc
calloc
calloc的原型如下:
void* calloc(size_t num, size_t size);
函数功能是为num个大小为size的元素开辟空间,并且把空间的每个字节都初始化为0。它与malloc的区别仅在于calloc会把申请的空间初始化为0。
看个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i));
}
}
free(p);
p = NULL;
return 0;
}
最终会打印10个0。
realloc
realloc的原型如下:
void* realloc (void* ptr, size_t size);
它的功能是调整已经申请的内存空间的大小。使用时注意:
ptr
是需要调整的内存地址;size
是调整后的新大小;- 返回值是调整后内存的起始位置;
- 这个函数调整大小后,还会将原来内存中的数据移动到新的空间;
realloc
调整时有两种情况。当原有空间之后有足够的空间时,返回原地址。如果没有,返回一个新的地址;- 扩容失败还是会返回一个
NULL
,但旧的空间可用。
举个例子:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 5; i++)
{
printf("%d ",*(p + i));
}
printf("\n");
int* ptr = (int*)relloc(p, 10 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
ptr = NULL;//不要free(ptr),这里等价与free(p)
}
else
{
perror("realloc:");
return 1;
}
//使用
for (i = 5; i < 10; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
看一下这段代码做了什么事情:
- 使用malloc申请了20个字节的空间给5个整型;
- 给这5个整型赋值;
- 打印这5个整型;
- 扩大内存空间,调整大小为40个字节;
- 给新的整型赋值
- 打印这块空间内所有的整型;
- 释放
常见动态内存的错误
对NULL的解引用
int main()
{
int*p = (int*)malloc(20);
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
return 0;
}
以上代码中,p是有可能为NULL的。
对动态开辟空间的越界访问
int main()
{
int*p = (int*)malloc(20);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 20; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
上面的代码中,malloc
申请了20
个字节,但赋值的时候赋值了20个整型,越界了。
对非动态开辟的内存使用free释放
int main()
{
int a = 10;
int* p = &a;
//...
free(p);
p = NULL;
return 0;
}
这里p指向的空间不是动态开辟的。
使用free释放一块动态开辟内存的一部分
int main()
{
int *p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc:");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i + 1;
p++;
}
//释放
free(p);//必须是起始位置,这里释放了一部分,error
p = NULL;
return 0;
}
这里p指向的位置不是开辟空间的起始位置了。
对同一个空间多次释放
int main()
{
int *p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc:");
return 1;
}
free(p);
//....
free(p);//error,如果没有 p = NULL;
return 0;
}
第一个free之后没有将p置NULL,因此会报错。
动态开辟内存忘记释放(内存泄露)
void test()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return;
}
//使用
if (5 == 2 + 3)
{
return;//这里返回的时候,没有机会free了
}
//释放、
free(p);
p = NULL;
}
int main()
{
test();
//程序不退出
//7*24一直在跑
return 0;
}
这里由于函数中提前返回了,因此p是没有释放的。造成了内存泄露。
动态内存管理经典笔试题
Prob 1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
运行Test
会有什么问题?
分析一下Test
的行为。调用GetMemory
的时候,实际是传值调用。那么*p
得到的就是str
的一份拷贝,因此p得到的空间仅在GetMemory
内部,当函数执行结束后,函数栈帧会销毁,所以其实str
并没有被开辟空间,因此在strcpy
的时候,实际是在往NULL
里拷贝。并且GetMemory
并没有释放空间。那么应该如何修改这两个函数呢?
传值调用不行,那么就传址调用。
void GetMemory(char** p)
{
*p = (char*)malloc(100);
if (*p == NULL)
{
return;
}
}
void Test(void)
{
char* str = NULL; //
GetMemory(&str);//
strcpy(str, "hello world");
printf(str);//打印没问题
free(str);
str = NULL;
}
另一种改造方法:
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL; //
str = GetMemory();//传参没什么意义
strcpy(str, "hello world");
printf(str);//打印没问题
free(str);
str = NULL;
}
Prob 2
char* Getmemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = Getmemory();
printf(str);//非法访问了,p已经被销毁
}
这里的问题时返回了栈空间的地址。栈空间在函数执行结束之后就会销毁,这时候p就成了野指针(指向了已经销毁的空间)。
Prob 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);
}
这个代码的问题很简单,就是str
使用结束之后没有释放。
Prob 4
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这里代码的问题是当str被释放后,没有置NULL。free之后的指针就是野指针,那么就一定要记住加一句str = NULL
。虽然可以打印,但仍然属于非法访问。
柔性数组
C99中,结构体的最后一个元素允许是未知大小的,这就是柔性数组的成员。柔性数组有下面几个要点:
- 结构体
- 最后一个成员(前面必须有别的成员)
- 未知大小
柔性数组一般和动态内存管理的几个函数一起使用。
柔性数组的声明
struct st_type
{
int i;
int a[0];//0可以不写
};
柔性数组的大小
使用sizeof来确认柔性数组大小的时候,其中的柔性数组时不包含在内的。sizeof仅返回除柔性数组之外的大小。例如:
struct st_type
{
int i;
int a[0];
};
int main()
{
printf("%d\n", sizeof(struct st_type));
}
这个结果是4。
柔性数组的使用
包含柔性数组成员的结构需要使用malloc分配内存,而且分配的内存应该大于结构体的大小,以适应柔性数组的大小。
看个例子:
struct st_type
{
int i;
int a[0];
};
int main()
{
struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
if (p == NULL)
{
perror("malloc:");
return 1;
}
int i = 0;
p->i = 100;
for (i = 0; i < 10; i++)
{
p->a[i] = i;
}
}
这里就是一个应用。程序首先使用malloc创建了44个字节的空间,然后给结构体内的成员都赋值。
如果希望修改a的大小:
struct st_type
{
int i;
int a[0];//柔性数组成员//0可以不写
};
int main()
{
struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
if (p == NULL)
{
perror("malloc:");
return 1;
}
int i = 0;
p->i = 100;
for (i = 0; i < 10; i++)
{
p->a[i] = i;
}
//希望a数组变成60个字节
struct st_type* ptr = realloc(p,sizeof(struct st_type) + 15 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
ptr = NULL;
}
else
{
perror("realloc:");
return 1;
}
//使用
//..
//释放
free(p);
p = NULL;
return 0;
}
柔性数组的优势
先看一下代码:
struct st_type
{
int i;
int* a;
};
int main()
{
struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type));
if (p == NULL)
{
perror("malloc:");
return 1;
}
p->i = 100;
p->a = malloc(10*sizeof(int));
if (p->a == NULL)
{
perror("malloc:");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p->a[i] = i;
}
//希望a指向的空间变为60个字节:
struct st_type* ptr = (struct st_type*)realloc(p->a, sizeof(struct st_type) + 15 * sizeof(int));
if (ptr == NULL)
{
perror("realloc:");
return 1;
}
else
{
p = ptr;
ptr = NULL;
}
//使用
//释放
free(p->a);//必须先释放p->a
p->a = NULL;
free(p);
p = NULL;
return 0;
}
这里使用指针实现了和柔性数组类似的功能。但对比一下不难发现:
- 柔性数组方便内存释放
- 柔性数组有利于提高运行速度(虽然不大)