结构体、动态内存管理对于后面数据结构的学习是非常重要的,这次来看看动态内存管理,话都不说,正文开始。
在我们对当前变量的内存开辟是有局限性的,比如说
int val = 20;//在栈空间上开辟四个字节
这个内存空间就开辟好了,不能再修改。
如果觉得不够,刚开始就直接申请
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
较多的空间,当可能会造成浪费。
上述的开辟空间的方式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知
道,那数组的编译时开辟空间的方式就不能满足了。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较较灵活了。
malloc
是用来申请内存的
void* malloc (size_t size);
它有一个参数就是size,指定就是要申请多少个字节。返回类型是void*,返回的是申请好空间之后的起始位置,之后就通过这个指针来维护这块空间。
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
C语⾔提供了另外⼀个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数⽤来释放动态开辟的内存。
malloc
和free
都声明在 stdlib.h
头文件中。举个例子看看malloc和free的使用:
#include
#include
int main()
{
//假设申请10个整型的空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放内存空间
free(p);
p = NULL;
return 0;
}
free(p);
p = NULL;
为什么free之后还要置空呢?
会存在free之后不小心继续使用就这个地址,这时就存在野指针,为了避免,在free之后,再加一个置空,这样在之后不小心使用了,也没事,已经是空了。
那么会不会出现开辟内存过大,失败的情况?
当然也会有,来看一个例子:
int main()
{
int* p = (int*)malloc(INT_MAX*4);//开辟失败的
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
return 0;
}
因为malloc在堆区上申请内存空间,如果需要释放空间,需要使用free函数;如果没有使用free释放,在程序退出的时候,也会由操作系统来回收。
C语言还提供了⼀个函数叫 calloc
, calloc
函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
举个例子:
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用内存
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
就等同于下面这个代码:
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用内存
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
C语言还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小
的连续空间来使用。这样函数返回的是一个新的内存地址。
举个例子:
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;
}
int* ptr = (int*)realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
ptr = NULL;
}
else
{
perror("realloc");
return 1;
}
for (i = 5; i < 10; i++)
{
*(p + i) = i+1;
}
for (i = 0; i < 10; i++)
{
printf("%d\n", *(p + i));
}
free(p);
p = NULL;
return 0;
}
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
在这个代码中,我们如果开辟不成功,返回的就是NULL,那么后面解引用p的时候就会有问题。
所以说我们在使用malloc来开辟空间的时候多加一个判断,来看看是否内存开辟成功,失败代码就不继续往下执行。
加一个判断之后:
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
if (p == NULL)
{
return 1;
}
*p = 20;
free(p);
p=NULL;
}
int main()
{
int* p = (int*)malloc(20);//20个字节 - 5个整型
if (p == NULL)
{
return 1;
}
int i = 0;
//越界访问
for (i = 0; i < 20; i++)/20个字节 - 5个整型,不是20个整型
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
当在使用malloc开辟空间时,给的内存是有限的,超过就会越界访问。
所以在使用malloc开辟空间时,不能越界访问。
int main()
{
int a = 10;
int* p = &a;
//...
free(p);
p = NULL;
return 0;
}
这里a开辟的空间不是在堆上开辟的,只能是malloc
calloc
realloc
,开辟的空间才能free。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功
int i = 0;
//1 2 3 4 5 [] [] [] [] []
for (i = 0; i < 5; i++)
{
*p = i + 1;
p++;//p位置已经改变
}
//释放
free(p);
p = NULL;
return 0;
}
当要free的时候,一定要指向起始位置。
在使用一定要注意,free时不要把起始位置弄错了,不然就释放不了了。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//...
//释放
free(p);
//....
free(p);
return 0;
}
我们可能会忘记已经释放过开辟的空间。
为了避免这样,我们在free时候,把指针置为空,这样,当再次free时候,也不会出错。
void test()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return;
}
//使用
//....
if (1)//申请的空间还没来得及释放,就返回
{
return;
}
//释放
free(p);
p = NULL;
}
int main()
{
test();
//如果程序不退出,一直跑
return 0;
}
申请的空间还没来得及释放,就返回
切记:动态开辟的空间一定要释放,并且正确释放。
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这个代码能不能打印出hello world
?
这里有两个问题:1. 对NULL进行解引用,导致程序崩溃。2.malloc申请空间,没有free,导致内存泄漏。
所以这里要把地址传过去,用二级指针。
修改一下:
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;
}
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
执行结果为什么是这样?
这个是 str = GetMemory(),接受的就是char p[] = "hello world"中p的位置,这个数组是局部的,一旦返回p,这个空间就销毁了,这时在去访问,就是非法访问了。
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
这里忘记对申请的空间进行free,可能会造成内存泄漏。结束使用时,不要忘记free。
修改之后:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
free(str)之后,没有将str置空,这里str肯定不为空,str就是野指针,继续使用会继续产生非法访问。
加上str = NULL。
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
}
}
也许之前从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
例如:
struct st_type
{
int i;
int a[0];//柔性数组成员
};
有些编译器会报错无法编译可以改成:
struct st_type
{
int i;
int a[];//柔性数组成员
};
举个例子:
struct st_type
{
int i;
int a[0];//柔性数组成员
};
int main()
{
printf("%d\n", sizeof(struct st_type));
return 0;
}
直接看例子:
struct st_type
{
int i;
int a[0];//柔性数组成员
};
int main()
{
/* printf("%d\n", sizeof(struct st_type));*/
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 = (struct st_type*)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\n");
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指向的空间是15 个整型
//struct st_type* ptr = (struct st_type*)realloc(p->a, 15 * sizeof(int));//错误的
//应该改为下面代码:
int* ptr = (int*)realloc(p->a, 15 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
{
//p = ptr;//错误的
//应该改为:
p->a = ptr;
ptr = NULL;
}
//使用
//....
//释放
free(p->a);
p->a = NULL;
free(p);
p = NULL;
return 0;
}
上述 代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在⼀个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次次性分配好了,并返回给用户一个结构体指针,用户做⼀次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,也没多高了,反正跑不了要用做偏移量的加法来寻址)
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
有错误请指出,大家一起进步吧!