目录
1、为什么要有动态内存分配
我们已经掌握的内存开辟方式有:
int a = 10;
char arr[10] = { 0 };
上述的开辟空间的方式有两个特点:
1·空间开辟的大小是固定的。
2·数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小就不能调整。
但是对于空间的需求,不仅仅是上述情况,有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
C语言引入了动态内存开辟,使得程序员自己可以申请和释放空间,这样就变得相对灵活了。
2、malloc函数和free函数
2.1、malloc函数
函数原型为:
void *malloc(size_t size)
这个函数向内存申请一块连续可用的size大小的空间,并返回这块内存的指针。
如果开辟成功则返回一个指向开辟的空间的指针。
如果失败则返回NULL,因此这个函数的返回值要检查。
这个函数的返回值类型为void*,所以在具体使用的时候使用者自己来决定开辟空间的类型。
如果参数size为零,函数的行为是未定义的,取决于编译器。
size的单位是字节。使用这个函数时要包含stdlib.h这个头文件。
这个函数申请到的空间是不会初始化空间内容的。
代码实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = malloc(sizeof(int) * 10);
if (arr == NULL)
{
perror("fail malloc");
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(arr + i));
}
free(arr);
arr = NULL;
return 0;
}
运行结果为:
2.2、free函数
C语言中这个函数是专门用来做动态内存的释放和回收的。函数原型为:
void free(void *ptr)
如果参数ptr指向的空间不是动态开辟的,那么该函数的行为是未定义的。
如果参数ptr为空指针,则该函数什么都不做。使用这个函数时要包含stdlib.h这个头文件。
代码实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[10] = {0};
int* ptr = (int*)malloc(sizeof(int) * 10);
if (ptr == NULL)
{
perror("fail malloc");
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = 0;
}
free(ptr);
ptr = NULL;
return 0;
}
通过监视我们可以看到:
可见free后ptr所指向的内容被释放掉了。
3、calloc函数和realloc函数
3.1、calloc函数
函数原型为:
void *calloc(size_t nitems, size_t size)
这个函数的功能是为nitems个大小为size的元素开辟空间,并且将空间的每个字节初始化为0,这一点与上面的malloc函数不同。空间开辟失败也会返回空指针。
代码实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
运行结果为:
3.2、realloc函数
有时候我们会发现空间开辟大了,或者是小了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整,realloc函数就可以做到对动态内存开辟大小的调整。
函数原型为:
void *realloc(void *ptr, size_t size)
ptr是要调整的地址,size为调整之后大小。
调整失败会返回空指针。
参数ptr为空指针,但size不为0,那么行为就等于malloc函数。
参数size为0,则realloc函数的行为free函数,此时,原有的指针已经被free掉了,不能再使用了,此时函数的返回值为NULL;
函数的返回值返回调整后的空间的内存地址,但要注意的是,这个返回的地址可能与调整前的地址相同也可能不同。
realloc函数调整空间是有两种情况的
情况1·原有空间之后有足够大的空间。
情况2·原有空间之后没有足够大的空间。
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
当是情况2 的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩ 的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。会将原有空间的内容拷贝到新内存中,释放旧的内存空间。
realloc函数的使用要注意一下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(sizeof(int)*10);
if (ptr == NULL)
{
perror("fail malloc");
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr+i));
}
int* p = NULL;
p = (int*)realloc(ptr, sizeof(int) * 250);
//先将realloc函数的返回值放在p中,不为NULL,在放ptr中
if (p != NULL)
{
ptr = p;
}
free(ptr);
return 0;
}
4、柔性数组
在c99中结构体中最后一个元素允许是大小未知的数组,这个数组就是柔性数组成员
例如:
#include<stdio.h>
struct type
{
int i;
int arr[];//柔性数组成员
};
有些编译器会报错⽆法编译可以改成:
struct type2
{
int j;
int arr2[0];//柔性数组成员
};
4.1、柔性数组的特点
1·结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
2·sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。
3·包含柔性数组的结构体使用malloc函数进行动态内存分配,并且分配的内存空间应大于结构体的大小以适应柔性数组的大小。
例如:
#include<stdio.h>
struct type1
{
int i;
int arr1[];//柔性数组成员
};
struct type2
{
int j;
int arr2[0];//柔性数组成员
};
int main()
{
printf("%d %d", sizeof(struct type1), sizeof(struct type2));
return 0;
}
运行结果为:
4.2、柔性数组的使用
#include<stdio.h>
#include<stdlib.h>
typedef struct type
{
int i;
int arr[];
}Type;
int main()
{
Type* pf = malloc(sizeof(int) + sizeof(int) * 10);
if (pf == NULL)
{
perror("fail malloc");
return -1;
}
pf->i = 10;
for (int i = 0; i < 10; i++)
{
pf->arr[i] = i + 1;
}
free(pf);
pf = NULL;
return 0;
}
这样相当于柔性数组arr获得了10个整形的连续空间
4.3、柔性数组的优势
上面的Type也可以设计成下面的结构,也能完成同样的效果:
#include<stdio.h>
#include<stdlib.h>
typedef struct type
{
int i;
int* arr;
}Type;
int main()
{
Type test = { 0 };
test.i = 10;
test.arr = (int*)malloc(sizeof(int) * 10);
if (test.arr == NULL)
{
perror("fail malloc");
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
test.arr[i] = i + 1;
}
free(test.arr);
test.arr = NULL;
return 0;
}
上述 代码1 和 代码2 可以完成同样的功能,但是 代码1 的实现有两个好处:
第⼀个好处是:⽅便内存释放 如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤ ⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能 指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返 回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度. 连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你 跑不了要⽤做偏移量的加法来寻址)
5、总结C/C++中程序内存区域划分
内存分配的几个区域:
5.1、栈区
在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
5.2、堆区
⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅ 式类似于链表。
5.3、(数据段)静态区
存放全局变量、静态数据。程序结束后由系统释放。
5.4、代码段
存放函数体(类成员函数和全局函数)的⼆进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。
6、常见的动态内存的错误
6.1、对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题,所以在使用前应该进行判断
free(p);
}
6.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的时候越界访问
}
free(p);
}
6.3、对⾮动态开辟内存使⽤free释放
void test()
{
int a = 10;
int* p = &a;
free(p);//不可对非动态开辟的内存使用free
}
6.4、使⽤free释放⼀块动态开辟内存的⼀部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
6.5、对同⼀块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
6.6、动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
以上代码仅仅只是介绍一些常见的错误,并非完整的代码。
7、动态内存经典笔试题分析
7.1、题目1
void GetMemory(char* p)
{
p = (char*)malloc(100);//开辟的空间未能释放。
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//这里本质上为传值调用,函数结束后str仍为空指针。
strcpy(str, "hello world");
printf(str);
}
7.2、题目2
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();//出函数后p所指向的空间已经被释放了,所以str也就成野指针了。
printf(str);
}
7.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);
}
//未去释放动态开辟的内存空间
7.4、题目4
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str); //释放完空间后,要把str给置为空指针,否则可能会出现问题。
if (str != NULL)
{
strcpy(str, "world");//str已经在前面释放了,所以这里是非法访问。
printf(str);
}
}
到这里基本上就说完了,欢迎在评论区留言。