平常常用的开辟内存方式有:
int i = 20; 在栈区开辟一个4字节大小的空间
char ch[10] = {0}; 在栈区开辟一个连续的10字节大小空间
但是,在栈区开辟的内存是固定大小的,数组在申明的时候必须申明长度;而有时候当我在程序运行以后才知道具体需要多大的内存空间,比如实际只需要3个数字长度(3字节),那就造成了内存浪费,这时就可以用动态内存来分配
malloc
#include <stdlib.h>
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。 1.如果开辟成功,则返回一个指向开辟好空间的指针。 2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
请看代码:
int main()
{
申请空间
int* p = (int*)malloc(sizeof(int)*10);
在堆区,申请一块连续的40字节大小的内存空间,并把首地址返回
由于不知道使用者是什么数据类型,所以是void*,需要强制转为需要使用的数据类型
if(p == NULL)
有可能申请失败,那就会返回null,所以最好在申请完判断一下
{
perror("malloc"); 如果为空就打印错误信息
return 1;
}
使用这块空间
int i;
for(i = 0;i < 10;i++)
printf("%d\n",(p[i]=i));
return 0;
}
free
当程序结束时,系统会自动回收内存,但是当程序没结束时,总是申请内存,动态内存包括大部分的虚拟内存,而由于虚拟内存的操作是要读写磁盘,因此极大地影响系统的性能。你的系统很可能因此而崩溃那很容易就被占满,就会影响系统系能导致崩溃。 所以申请后使用完要及时的释放,是一种良好的使用习惯
void free (void* ptr);
注意: 1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。 2.如果参数 ptr 是NULL指针,则函数什么事都不做。 3.释放必须是申请内存的首地址传参
int main()
{
申请空间
int* p = (int*)malloc(sizeof(int)*10);
在堆区,申请一块连续的40字节大小的内存空间,并把首地址返回
由于不知道使用者是什么数据类型,所以是void*,需要强制转为需要使用的数据类型
if(p == NULL)
有可能申请失败,那就会返回null,所以最好在申请完判断一下
{
perror("malloc"); 如果为空就打印错误信息
return 1;
}
使用这块空间
int i;
for(i = 0;i < 10;i++)
printf("%d\n",(p[i]=i));
free(p); p是申请内存空间的首地址
释放完以后,申请的内存空间就会被收回,里面的内容就会被系统重置了
这时指针就会为一个野指针,只保留了地址,但不知道里面是什么内容
所以在释放完以后,需要把指针也置NULL
p = NULL;
return 0;
}
calloc
void* calloc(size_t num,size_t size);
1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。 2.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
请看代码:
int main()
{
int* p = (int*)calloc(10,sizeof(int));
申请 10块连续的,大小为4字节的空间,并把首地址返回给p
if(NULL == p)
{
perror("calloc");
return 1;
}
int i;
for(i = 0;i<10;i++)
printf("%d ",*(p+i));
free(p);
p = NULL;
return 0;
}
如果要求对申请的空间初始化,那么就可以用calloc
realloc
有时等我们发现开辟的内存不够或者多了,就可以使用realloc realloc函数的出现让动态内存管理更加灵活,可以做到对动态开辟内存大小的调整
void* realloc (void* ptr, size_t size);
1.ptr 是要调整的内存地址 2.size 调整之后新大小 3.当向后追加的内存被占用或不够了,就会重新找一块最够的内存,并把原先的内容复制到新的内存上,并把原先的内存free掉 4.返回值为调整之后的内存起始位置(原内存后续不够时)。 5.扩容失败时返回NULL
int main()
{
int* p = (int*)malloc(4*10);
if(p == NULL)
{
perror("malloc");
return 1;
}
int i;
for(i = 0;i < 10;i++)
printf("%d ",(p[i] = 1));
这时想保留20个内容,则使用 realloc
int* fp = (int*)realloc(p,20*4);
由于realloc也有扩容失败的风险,所以用新一个指针接收地址
若返回的不是空,则在把接收的地址赋给原来的指针
if(fp != NULL)
{
p = fp;
}
扩容成功
for(i=0;i<20;i++)
*(p+i) = i;
free(p);
p = NULL;
除了释放空间,把原指针置空以外,还要把接收扩容返回地址的指针也要置空
不然等于是,只存了一个地址,不知道地址里是什么的野指针
fp = NULL;
return 0;
}
常见错误
1.对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;
free(p);
}
正确做法是:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
申请完后,进行对空的判断
if(p == NULL)
{
perror("malloc");
return 1;
}
*p = 20;
free(p);
}
在申请动态内存后,一定要对指针接收的返回值进行判断
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的时候越界访问
}
或者 这里用 *p++ = i时
for(i=0; i<10; i++)
{
*p++ = i 当i是9的时候,p是先输出后自增,此时p就等于 p+10了
} 而且free(p)也是有问题的,这里已经不是首地址了
free(p);
}
在平时编程中一定要对循环边界仔细检查
3.对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);
}
a是在栈区开辟内存的,由编译器自动释放,调用结束就自动销毁了
4.使用free释放一块动态开辟内存的一部分
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是9的时候,p是先输出后自增,此时p就等于 p+10了
} 而且free(p)也是有问题的,这里已经不是首地址了
free(p);
}
必须记住起始地址 最好不要对指针本身存储的地址进行改变,应该让指针指向内存去访问,而不是让指针改变存的内存去访问
5.对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); error 重复释放
}
6.动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
test在调用结束后,里面的变量等都会被销毁(栈区),而在test函数体中申请的动态内存(堆区)不会
而调用结束后,没有返回申请的首地址,也没有释放,接收地址的指针变量随着调用结束销毁
等于是通往这个内存的入口丢失了,会导致无法再访问这个内存了,则造成内存泄漏
while(1);
}
在哪开辟的内存,就在哪释放,若后续还要使用,那就返回内存首地址
经典例题: 例1.
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
例1中,str是空指针,传参后,形参是实参的一份临时拷贝,等于p也是NULL;GetMemory函数体中申请完了动态内存,虽然让p接收保存首地址,但是他没有返回该地址,也没有释放该地址,GetMemory调用结束,p也随即销毁,“通往申请的动态内存入口丢失” ,造成内存泄漏,str没变还是空指针,所以也不会打印hello world
例2.
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
在GetMemory(void) 创建了一个数组并保存了一个字符串,虽然返回了指针p保留的 “字符串首地址” ,但由于GetMemory函数在栈区,调用结束后,内存被系统收回并重置成别的内容。hello world所在的地址也属于栈区,是临时创建的,调用结束后也会随着被销毁并重置;此时指针p等同于,保留了一个地址,但是地址里现在是什么内容不知道,成为了野指针
例3.
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这里是先释放,后使用。释放完后,内存已经还给系统了,虽然str存的是返回的首地址,但此时该地址存的是什么就不知道了,变成了野指针
柔性数组
结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct stu1
{
int num;
double sore;
int arr[]; 或int arr[0]
};
int main()
{
printf("%d",sizeof(struct stu1));
struct stu1* ps = (struct stu1*)malloc( sizeof(struct stu1) + 40 );
sizeof(struct stu1)是 int num和 double sore的空间大小共16,后面的40是给数组的空间,等于是int arr[10]
柔性体现在:动态开辟的内存后面多加的40内存空间是给数组用的,若不够用,还可以进行修改(可大可小)
这样就可以根据需求随时调整数组大小,而且这样开辟也是连续的,也不影响释放
}
如上述代码中,结构体中,最后一个成员是数组,且未知大小,就是柔性数组成员
柔性数组的特点: 1.结构中的柔性数组成员前面必须至少一个其他成员。 2.sizeof 返回的这种结构大小不包括柔性数组的内存。 3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
总结
1.malloc、calloc在申请完后 最好对返回值进行判断 2.realloc在扩容后,要新定义一个指针接收,并判断不为空后再赋给原来的指针 3.申请了动态内存要记得释放,给free的实参必须是申请内存空间的首地址 4.不可对非动态内存进行释放 5.不可对一个动态内存多次释放 6.free(0)属于未定义 7.要注意越界问题,在编写完后对边界要仔细检查 8.要避免出现野指针,不用的指针及时置空 9.要避免内存泄漏 10.在不同的函数中申请动态内存,要做到谁申请谁释放,或者返回该地址在不需要使用后及时释放 11.柔性数组的作用在于,动态申请内存时可大可小
|