目录
1. 为什么要有动态内存分配
我们已经掌握了开辟内存的方式有两种。
//1.在栈上开辟四个字节的空间
int a= 0;
//2.在栈上开辟十个字节的连续空间
char arr[10] = {'\0'};
但是上述的开辟空间的⽅式有两个缺陷:
空间开辟大小是固定的,比如a申请的是四个字节,以后我想变成八个字节,是做不到的。第二行代码,向内存申请了十个字节,我将来想变成二十个字节那也是做不到的,开辟的大小是不可改变的,比如我第二行代码arr数组,可以放是个元素,我放5个,那么5个字节的空间就会浪费,放10个的话就刚刚好,但是放15个的话又会不够。
数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知 道,那数组的编译时开辟空间的方式就不能满足了。
C语⾔引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
2. malloc和free
2.1 malloc
malloc全写memory allocation(内存分配)
C语言提供了⼀个动态内存开辟的函数:
void* malloc (size_t size);
上面的代码我们用数组的方式申请10个char类型的空间,现在我们可以使用malloc函数申请10个字节的空间:
#include <stdio.h>
#include <stdlib.h>//malloc函数头文件
int main()
{
//申请10个字符类型的空间
//void* p = malloc(10 * sizeof(char));
/*
开辟10个char类型的空间,返回的是void*的指针,放到p指针变量里面
但是我是申请了10个字符类型的空间,未来我要以字符的视角来看待这块内存,
所以我们可以强转为char*类型的指针,然后放到char*类型的指针变量里面。
*/
char* p = (char*)malloc(10 * sizeof(char));//malloc所申请的空间是连续的
if (p == NULL)//对于malloc返回的值是需要判断的
{
//空间开辟失败
perror("malloc");//打印失败原因
return 1;//空间开辟失败就之间返回,非0表示异常返回
}
//开辟成功.可以使用这10个字节的空间
for (int i = 0; i < 10; i++)
{
*(p + i) = 'a' + i;//往里面循环放入字符
}
for (int i = 0; i < 10; i++)
{
printf("%c ", *(p + i));//通过地址打印放进去的字符
}
return 0;//0表示正常返回
}
输出结果:
malloc申请的空间和数组的空间有什么区别呢?
1.malloc申请的空间叫动态内存,动态内存的大小是可以调整的。
2.开辟空间的位置不一样,但是使用上是一样的。
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
如果开辟成功,则返回⼀个指向开辟好空间的指针。
如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者自己来决定。
如果参数size为0,malloc的⾏为是标准是未定义的,取决于编译器。
当我们将INT_MAX(2147483647)传给malloc作为参数的时候, 该函数返回的是空指针,并且屏幕上打印没有足够的空间,所以不能开辟成功,任何内存都是资源,不能无截止的开辟。
2.2 free
起始我们刚刚写的代码还没有写完,malloc是用来申请空间的,申请空间,使用完之后还是需要释放空间的,malloc申请的空间是需要释放的。
C语⾔提供了另外⼀个函数free,专门是用来做动态内存的释放和回收的。
void free (void* ptr);
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请10个字符型空间
char* p = (char*)malloc(10 * sizeof(char));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用这10个字节的空间
for (int i = 0; i < 10; i++)
{
*(p + i) = 'a' + i;
}
for (int i = 0; i < 10; i++)
{
printf("%c ", *(p + i));
}
//释放这10个字节的空间
free(p);//这10个字节的地址在p里面,所以传p就可以
p = NULL;//将p置为空指针,否则会变成野指针
return 0;
}
前面我们提到malloc开辟的空间和我们创建的连续数组有两种区别,那么还有没有别的区别呢?
free函数⽤来释放动态开辟的内存。
如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数ptr是NULL指针,则函数什么事都不做。
将来对于malloc函数和free函数需要成对使用,有来有回,有申请有释放。
3. calloc和realloc
3.1 calloc
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。
void* calloc (size_t num, size_t size);
函数的功能是为num个大小为size的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
示例代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请10个整型的空间
//malloc(10*sizeof(int)); //对于malloc来说是一次性算好的
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)//calloc也是一样的,一旦空间申请失败就返回空指针,需要判断
{
perror("calloc");
return 1;
}
//使用空间
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);//p[i] <==> *(p + i)
}
//释放空间
free(p);//calloc申请的空间也是在堆上申请的,所以释放空间也是free释放
p = NULL;
return 0;
}
3.2 realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们⼜会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
void* realloc (void* ptr, size_t size);
ptr是要调整的内存地址。
size是调整之后新大小。
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况1:
当是情况1的时候就会比较简单,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化,返回的是旧地址。
情况2:
当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用,意思就是在别的地方直接开辟80个字节的空间,将旧的空间的数据拷贝到新的空间,然后释放旧的空间,这样函数返回的是⼀个新的内存地址。
除了上面的两种情况,还有可能会返回NULL。
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请10个整型的空间
int* p = (int*)malloc(10*sizeof(int)); //对于malloc来说是一次性算好的
//int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)//calloc也是一样的,一旦空间申请失败就返回空指针,需要判断
{
perror("calloc");
return 1;
}
//使用10个整型的空间
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);//p[i] <==> *(p + i)
}
//调整空间 - 希望变成20个整型
//这里不要用p来接收,如果realloc开辟失败的话就会返回NULL,用p接收会报p修改为空
//指针,原来的10个整型的空间也会找不到,所以这里只能用临时变量来接收
//如果缩小的话realloc函数第二个参数变小就可以了
int * tem = (int*)realloc(p, 80*sizeof(int));
if (tem == NULL)
{
//如果临时变量是空指针就说明扩容失败,打印错误信息退出就可以了
perror("realloc");
return 1;
}
//如果临时变量不等于空指针,就可以将临时变量的地址赋值给p,然后将p置为NULL
p = tem;
tem = NULL;
//使用扩容后的20个整型的空间
//释放空间
free(p);//calloc申请的空间也是在堆上申请的,所以释放空间也是free释放
p = NULL;
return 0;
}
realloc函数的参数不能乱传,不能随便传一个地址,想调整哪个空间把哪个空间的起始地址传进去,并且这块空间必须是动态开辟的。
4.常见的动态内存的错误
4.1 对NULL指针的解引用操作
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请空间
int* p = (int*)malloc(1000 * sizeof(int));
//使用
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
//销毁
free(p);
p = NULL;
return 0;
}
未来对malloc的返回值一定要进行判断。
4.2 对动态开辟空间的越界访问
动态内存开辟的空间也是有大小的,并不是想怎么用就怎么用。
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请空间
int* p = (int*)malloc(10 * sizeof(int));//申请可10个整型的空间 - 40个字节
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
for (int i = 0; i < 40; i++)//循环了40次,肯定会越界访问 - 整型的形式访问而不是字节
{
p[i] = i;
}
//销毁
free(p);
p = NULL;
return 0;
}
4.3 对⾮动态开辟内存使用free释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10;
int* p = &a;
//.......
free(p);
p = NULL;
return 0;
}
指针变量p不是动态内存开辟的,所以不能使用free来释放。
4.4 使用free释放⼀块动态开辟内存的⼀部分
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请空间
int* p = (int*)malloc(100 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
for (int i = 0; i < 5; i++)
{
p[i] = i;//p[i] = *(p+i)
/**p = i;
p++;*/
}
//销毁
free(p);
p = NULL;
return 0;
}
4.5 对同⼀块动态内存多次释放
4.6 动态开辟内存忘记释放(内存泄漏)
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
malloc和calloc是用来开辟内存的,那么realloc只能用来调整空间吗?
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)realloc(NULL, 40);//==malloc(40);
/*
realloc第一个参数传空指针,第二个参数传40,就等价于malloc(40),直接申请40个
字节的空间,因为它没法调整,传空指针没法调整,
*/
if (p == NULL)
{
perror("realloc");
return 1;
}
//使用
//释放
free(p);
p = NULL;
return 0;
}
其实并不是的,realloc函数也是可以用来开辟内存空间的。
5. 动态内存经典笔试题分析
5.1 题目1:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
解析:
上面的代码,printf这样传参也是可以的。
#include <stdio.h>
int main()
{
printf("haha\n");//给printf的不是字符串本身,而是字符串首字符的地址
//那么
char* p = "haha\n";//常量字符串赋给p的时候也是给首字符的地址
//所以printf接收到字符串的首字符的地址的时候是可以实现打印的
printf(p);
return 0;
}
那么,上面的代码修改正确应该怎么写呢?
从代码可以看出,malloc申请的100个字节的空间是放到p里面的,实际上它是想放到str里面,然后将hello world拷贝到这100个字节的空间中,然后实现打印,但是以值传递的时候,将str的值传递给p的时候,p是一块独立的空间,把地址放到p里面,不会影响str,我们知道,函数传参,值传递的时候,形成是实参的一份临时拷贝,修改形参不会影响实参,所以str以后为空指针,拷贝的时候就会失败,那我们就可以传地址。
#include <stdio.h>
#incluide <stdlib.h>
#include <string.h>
//写法1
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;
}
//写法2
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;
}
int main()
{
Test();
return 0;
}
运行:
5.2 题目2:
#include <stdio.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
5.3 题目3:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
解析:
5.4 题目4:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
解析:
6. 柔性数组
柔性数组(flexible array),C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
什么是柔性数组呢?
1.在结构体中
2.最后一个成员
3.位置大小的数组
//写法1
struct S
{
int n;
char c;
double d;
int arr[];//未知大小的数组 - arr就是柔性数组的成员
};
//写法2
struct S2
{
int n;
char c;
double d;
int arr[0];//写成0的时候也是未知大小的数组 - arr就是柔性数组的成员
};
有的编译器支持第一种写法,有的编译器支持第二种写法,就像vs编译器,两种写法都支持。
6.1 柔性数组的特点:
1.结构中的柔性数组成员前面必须至少一个其它成员。
如果前面没有其它成员,而且柔性数组的大小是未知的,那么结构的大小就没法算。
2.sizeof返回的这种结果大小不包括柔性数组的内存。
如果前面没有其它成员,而且不包含柔性数组,那这个结构体大小是0吗,很明显是不可能的。
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以使用柔性数组的预期大小。
6.2 柔性数组的使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct S
{
int n;
int arr[];
};
int main()
{
//struct S s;//包含柔性数组的结构不会这样创建变量,这样创建只有n的4个字节
//柔性数组是没有大小的
// n 4字节 arr 80字节
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
//站在ps的角度,这边分配了84个字节的空间,并且是struct S类型的数据,那么
//就可以访问里面的变量n和数组arr
if (ps == NULL)
{
perror("malloc");
return 1;
}
//使用
ps->n = 110;
for (int i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//销毁
free(ps);
ps = NULL;
return 0;
}
那既然是柔性数组,那么柔性体现在哪里呢?
因为前面的n和arr都是malloc来的,那么就可以通过realloc来调整这块空间,一旦通过realloc来调整这块空间的时候,那么数组的空间就可大可小,后面这块空间在柔性的变长变短。
#include
struct S
{
int n;
int arr[];
};
int main()
{
//struct S s;//包含柔性数组的结构不会这样创建变量,这样创建只有n的4个字节
//柔性数组是没有大小的
// n 4字节 arr 80字节
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
//站在ps的角度,这边分配了84个字节的空间,并且是struct S类型的数据,那么
//就可以访问里面的变量n和数组arr
if (ps == NULL)
{
perror("malloc");
return 1;
}
//使用
ps->n = 110;
for (int i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//调整ps指向空间的大小
struct S* tem = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
if (tem == NULL)
{
perror("realloc");
return 1;
}
ps = tem;
tem = NULL;
//使用调整后的空间
ps->n = 0;
for (int i = 0; i < 40; i++)
{
printf("%d\n", ps->arr[i]);
}
//销毁
free(ps);
ps = NULL;
return 0;
}
6.3 柔性数组的优势
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc");
return 1;
}
int* tmp = (int*)malloc(20 * sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp;
tmp = NULL;
}
else
{
return 1;
}
//使用
ps->n = 100;
for (int i = 0; i < 20; i++)//赋值为1~20
{
ps->arr[i] = i + 1;
}
//调整空间为40个字节
tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp;
tmp = NULL;
}
else
{
perror("malloc");
return 1;
}
//使用调整后的空间
ps->n = 200;
for (int i = 0; i < 40; i++)
{
printf("%x ", ps->arr[i]);
}
//销毁
//先释放ps->arr,如果先释放ps的话就找不到ps->arr
free(ps->arr);
ps->arr = NULL;//其实这句不写也可以,随后ps也会被释放的
free(ps);
ps = NULL;
return 0;
}
6.2中的代码我们使用了一次malloc.而上面的代码和我们6.2中的代码实现的效果一样,但是使用了两次malloc,那就得用两次free,那么维护起来就容易出错,所以我觉得6.2中的代码更好一些。
第⼀个好处是:方便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给用户,用户调⽤free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,用户做⼀次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个⼈觉得也没多⾼了,反正你 跑不了要用做偏移量的加法来寻址)
C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell
7. 总结C/C++中程序内存区域划分
在学校计算机语言的时候是这样讨论的,学习操作系统的时候会变。
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限, 栈区主要存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。
《函数栈帧的创建和销毁》
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3. 数据段(静态区):(static)存放全局变量,静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码,代码段的数据是不能被修改的。