C和C++的动态内存管理
内存分区
-
栈区(stack):存放函数形参和局部变量(auto类型)和返回值,由编译器自动分配和释放
-
堆区(heap):用于动态内存分配该区由程序员申请后使用,需要手动释放否则会造成内存泄漏。如果程序员没有手动释放,那么程序结束时可能由OS回收
-
全局/静态存储区:存放全局变量和静态变量(包括静态全局变量与静态局部变量),初始化的全局变量和静态局部变量放在一块,未初始化的放在另一块
-
文字常量区:常量在统一运行被创建,常量区的内存是只读的,程序结束后由系统释放
-
程序代码区:存放程序的二进制代码,内存由系统管理
其中未初始化数据区(bss)、初始化数据(data)以及文字常量区(data)统称为数据段(data)
而实际上C/C++内存有六个区域 ,我们经常听到的有栈、堆、数据段和代码段。还有两个分别是内核空间和内存映射段。
下面是几个说明:
栈:向下增长,非静态局部变量,函数返回值,参数列表,函数栈帧等(一般8M)
堆:向上增长,动态内存分配,手动申请
数据段:存储静态数据和全局数据
代码段:可执行代码,只读常量
举个例子:
内存处理函数
- memset()
#include<string.h>
void *memset(void *s,int c,size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
s:需要操作内存s的首地址
c:填充的字符,c虽然参数为int,但必须是unsigned char,范围为0~255
n:指定需要设置的大小
返回值:s的首地址
说明:memset是对一个一个字节进行修改,每个要修改自己的内容都会被修改成c的内容
- memcpy()
#include<string.h>
void *memcpy(void *dest,const void *src,size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上
参数:
dest:目的内存首地址
src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
n:需要拷贝的字节数
返回值:dest的首地址
注意:现在memcpy函数已经优化,如果拷贝的地址有重叠依旧可以处理,并且执行效率高
- memcmp()
#include<string.h>
int memcmp(const void *s1, const void *s2,size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
s1:内存首地址1
s2:内存首地址2
n:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0
char num1[ ] = {1,0,3,4,5,6,7};
char num2[ ] = {1,0,3,6,5,6,7};
char str1[ ] = "dbdadkadadsad";
char str2[ ] = "dbdafaadadsad";
printf("%d\n",memcmp(num1,num2,7*sizoef(char)));
printf("%d\n",strncmp(num1,num2,7*sizoef(char)));
printf("%d\n",memcmp(str1,str2,sizoef(str1)));
printf("%d\n",strncmp(str1,str2,sizoef(str1)));
这里对内存的操作和之前所学的字符串操作函数不一样,因为对字符串操作的函数遇到\0就会停止,但是内存操作函数不会停止。
存储类型
1.static静态局部变量:在main函数运行之前就已经开辟了空间,在程序结束之后释放空间,作用域在{}内,static局部变量定义使用后值会存储下来。所以使用static局部变量定义只需要一次赋值。静态局部变量的作用域仅限于所定义的函数。但函数结束后变量的值会保留。直到整个程序运行结束。全局变量从定义开始作用于整个文件直至程序运行结束
2.static静态全局变量:在函数外定义,作用范围被限制在所定义的文件中。不同文件静态全局变量可以重名,但作用域不冲突
3.全局变量:全局变量在任何地方定义都可以,在不同的分文件使用的时候只要声明一下就可以了(extern声明),因为全局变量在main开始之前就已经开辟了存储空间
4.函数只有全局函数,没有局部函数,所以函数只有全局函数和静态全局函数,静态全局函数只能当前分文件可以调用,别的.c文件无法调用静态全局函数
为什么存在动态内存分配
在此之前,我们基本都是在栈上开辟空间且开辟的空间大小也都是要明确指定的。
例如:
int val = 10;
这个变量的大小是在栈上开辟的,大小是4个字节。缺点大小是固定的。
int arr[10] = {0};
这个数组也是在栈上开辟的,大小是40个字节。缺点是数组大小要明确指定。这样会导致空间不能够按需所取。
显然,用数组开辟空间大小已经不能满足我们的需求了,这时候就产生了动态内存开辟这一说了。
C语言
动态内存管理
malloc
#include<stdio.h>
void *malloc(size_t size);
功能:
在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
size:需要分配内存大小(字节)
返回值:
成功:分配空间的起始地址
失败:NULL
- malloc向堆区申请一块连续的内存空间
- malloc返回的是void*类型,需要对void类型进行类型转化
- 堆区申请的空间,在局部函数结束后不会被释放,区分栈区
- malloc申请的空间不可以释放两次,一一对应,申请一次释放一次。free释放的地址必须是上一次申请的地址,不能改变地址,free只能释放申请的地址,不能随意释放其他的地址
free
#include<stdio.h>
void free( void *memblock);
参数:
memblock:memblock指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。
无返回值
注意:
- 当参数是NULL时,这个函数什么都不做
- 参数中指针必须指向动态内存开辟的空间,如果参数 memblock 指向的空间不是动态开辟的,那free函数的行为是未定义的
实例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
scanf("%d", &n);
int *ptr = (int*)malloc(sizeof(int)* n);//开辟4*n个字节大小的连续空间
//检查空间是否申请成功
if (ptr == NULL)
{
perror("malloc fail:");
exit(-1);
}
int i = 0;
for (i = 0; i < n; i++)
{
*(ptr + i) = i;
printf("%d ", *(ptr + i));
}
printf("\n");
//释放和回收空间
free(ptr);
ptr = NULL;
return 0;
}
运行结果如下:
第一个正常运行:
第二个:
这个是空间申请过大,然后报错,程序提前结束。
calloc
- 这个函数的第一个参数是申请空间的个数,第二个参数是每个空间的大小,单位是字节。
- 这个函数比mallo函数多了会将开辟好的内存空间中每个字节初始化为0。
void *calloc(size_t nmemb,size_t size);
功能:
在内存动态存储区(堆)中分配rmemb块长度为size字节的连续区域。calloc自动将分配的内存置为0
参数:
nmemb:所需内存单元数量
size:每个内存单元的大小(字节)
返回值:
成功:分配空间的起始地址
失败:NULL
和malloc唯一的区别就是calloc会把申请的空间全部自动置为0
realloc
void *realloc(void *ptr, size_t size);
功能:
重新分配用malloc和calloc函数在堆中分配内存空间的大小。
relloc不会自动清理增加的内容,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存
参数:
ptr:为之前用的malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
size:为重新分配内存的大小(字节)
返回值:
成功:新分配的堆内存地址
失败:NULL
- 函数第一个参数是要调整的内存地址,第二个参数是调整后空间的大小。
- 返回值为调整之后的内存起始位置。
- 这个函数会将原来内存中的数据移动到新的空间上。
realloc调整内存空间一般会有两种情况:
第一种情况:
原有空间之后有足够的大的空间。
第二种情况:
原有空间之后没有足够大的空间。
图解:
所以realloc函数在使用时,我们要注意其返回值不能直接用原指针接收,如果动态内存空间申请失败,那么原来那块空间也就找不到了,所以我们要创建一个新的指针变量来接收并检查指针是否为空,这样就能保证空间申请失败时元原空间不丢失,下面我们来看一个实例:
int main()
{
int *ptr = (int*)malloc(10*sizeof(int));//开辟40个字节大小的连续空间
//检查空间是否申请成功
if (ptr == NULL)
{
perror("malloc fail:");
exit(-1);
}
//扩展40个字节的空间
//创建一个临时指针变量接收新的空间地址
int* tmp = (int*)realloc(ptr, 10 * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail:");
exit(-1);
}
ptr = tmp; //扩展成功就把新的空间地址给指向旧的空间的指针变量
//释放和回收空间
free(ptr);
ptr = NULL;
return 0;
}
常见的动态内存的错误
对NULL进行解引用操作
#include <limits.h>
int main()
{
int* ptr = (int*)malloc(INT_MAX);
*ptr = 10;
free(ptr);
ptr = NULL;
return 0;
}
指针ptr未检查是否为空,直接使用造成对NULL进行解引用操作程序直接崩溃了。
对动态开辟空间的越界访问
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL)
{
perror("malloc fail");
exit(-1);
}
int i = 0;
for (i = 0; i <= 10; i++)
{
*(ptr + i) = i;//当i = 10时,指针越界访问,程序崩溃
}
free(ptr);
ptr = NULL;
return 0;
}
对非动态开辟的空间free释放
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
free(arr);
return 0;
}
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
对同一块动态内存进行多次free释放
这个错误多发生在类的浅拷贝(只是对变量的值进行字节的简单拷贝)当中,当成员变量有在堆区开辟空间的情况,拷贝构造函数中必须重新开辟空间重载拷贝构造函数,否则就会发生浅拷贝现象,同一块空间释放两次
int main()
{
int *p = (int *)malloc(100);
if (p == NULL)
{
perror("malloc fail");
}
free(p);
free(p);
return 0;
}
柔性数组
概念:
结构体最后一个元素是一个未知大小的数组,这就叫做柔性数组成员
。
//第一种
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
//第二种
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
以上两种哪一种不报错就使用哪一种,因为这是根据编译器来选择的。
柔性数组的特点
- 柔性数组成员前面至少包含一个其他成员。
- sizeof返回这种结构体大小不包含柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
柔性数组的使用
typedef struct st
{
int i;
int arr[];//柔性数组成员
}st;
int main()
{
st* p = (st*)malloc(sizeof(st)+sizeof(int)* 10);
if (p == NULL)
{
perror("malloc fail");
exit(-1);
}
//开始使用
p->i = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
p->arr[i] = i;
}
//如果不够,继续扩展
st* tmp = (st*)realloc(p, sizeof(st)+sizeof(int)* 20);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
else
{
p = tmp;
}
for (i = 10; i < 20; i++)
{
p->arr[i] = i;
}
//释放回收空间
free(p);
p = NULL;
return 0;
}
图解:
typedef struct st
{
int i;
int* ptr;
}st;
int main()
{
st* p = (st*)malloc(sizeof(st));//先开辟一个结构体大小的空间
if (p == NULL)
{
perror("malloc fail");
exit(-1);
}
p->i = 100;
p->ptr = (int*)malloc(sizeof(int)* p->i);
if (p->ptr == NULL)
{
perror("malloc fail");
exit(-1);
}
int i = 0;
//使用
for (i = 0; i < p->i; i++)
{
p->ptr[i] = i;
}
//扩展空间
int* tmp = (int*)realloc(p->ptr, sizeof(int)* (p->i+10));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
else
{
p->ptr = tmp;
}
for (i = 10; i < 20; i++)
{
p->ptr[i] = i;
}
//释放空间
free(p->ptr);
p->ptr = NULL;
free(p);
p = NULL;
return 0;
}
图解:
柔性数组的优势
对比上面两种方法,柔性数组有两个优势:
第一个优势:第一种方法只需要释放一次内存空间,方便内存释放,但第二种方法要进行两次free,不方便释放。
第二个优势:由于空间连续,有利于内存访问,所以访问速度快。连续的内存有益于提高访问速度,也有益于减少内存碎片。内存碎片化会导致右下空间不可用。所以柔性数组还是比较有优势的。
C++
C++内存管理方式
方式: 通过new和delete操作符进行动态内存管理。C++可以继续使用C的内存管理方式,也可以用自己的内存管理方式。
下面用这两个操作符来处理自定义类型和内置类型,并且和C的内存管理方式的效果进行对比。
例1:内置类型
int main()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)malloc(5 * sizeof(int));
// 对于内置类型,malloc和new没有本质的区别,只是用法不同
// new 和 delete都是操作符
int* p3 = new int;
// int* p3 = new int(3);
int* p4 = new int[5];
// int* p4 = new int[5]{ 1,2,3,4,5 }; C++98不支持初始化new的数组,C++11支持
free(p1);
free(p2);
delete p3;
delete[] p4;
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
p4 = nullptr;
return 0;
}
对于内置类型,malloc和new没有本质的区别,只是用法不同
通过调试发现,对于内存类型,两者都不会对空间初始化,但是new可以通过一定的手段来进行初始化。
int* p3 = new int(3);//创建一个int对象,并且进行初始化,调用有参构造
int* p4 = new int[5]{ 1,2,3,4,5 };//,创建一个Int类型的数组, C++98不支持初始化new的数组,C++11支持
上面两种方式都是可以在开空间时,给它们来进行初始化。但是C语言中的malloc不可以。
例2:自定义类型
class A
{
public:
A(int a = 10)
:_a{ a }
{
cout << "A(int a = 0)" << endl;
}
private:
int _a;
};
void test()
{
// 对于自定义类型,new会调用构造函数,delete会调用析构函数,而malloc和free不会
A* p1 = (A*)malloc(sizeof(A));
A* p2 = (A*)malloc(sizeof(A) * 5);
// new在堆上申请空间,然后调用构造函数初始化
// delete先调用析构函数,然后释放空间给堆上
A* p3 = new A;
A* p4 = new A[5];
// A* p4 = new A[5]{ 1,2,3,4,5 };
free(p1);
free(p2);
delete p3;
delete[] p4;
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
p4 = nullptr;
}
对于自定义类型,new会先开空间,然后调用构造函数进行初始化,delete会调用析构函数来清理,然后释放空间,而malloc和free不会。观察下面的图片也可以发现。
总结:
- 对于内置类型,malloc和new没有本质的区别,只是用法不同
- C和C++的动态内存申请都是在堆区申请的空间
- 对于自定义类型,new会调用构造函数,delete会调用析构函数,而malloc和free不会
- 切记C和C++的申请和释放堆区空间不要混用
new和delete的实现原理
1.内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL
2.自定义类型
- new的原理
先调用operator new函数申请空间,然后调用构造函数进行初始化 - delete原理
先调用析构函数清理资源,然后调用operator delete函数释放空间 - new T[]原理
先调用operator new[]函数完成N个对象的空间的申请,然后调用N次构造函数进行初始化 - delete[] 原理
先调用N次析构函数完成N个对象中资源的清理,然后调用operator delete[]函数释放空间
malloc/free和new/delete的区别
从三个角度分析如下:
1.概念性质: malloc/free是函数,new/delete是操作符
2.使用方法: malloc需要手动计数申请空间的大小且需要将void*类型强转为对应类型,new后面跟着的是类型,只需要按类型申请即可。
3.使用效果: malloc申请的空间不会进行初始化,且申请失败是返回NULL,new申请的空间可以初始化,对应自定义类型会调用它的构造函数进行初始化,delete在释放空间之前会调用析构函数来清理资源。
内存泄漏和内存污染
内存泄漏:只申请,不释放,导致程序使用的内存空间一直增长,只有程序退出,程序使用的所有内存就会释放
所以在使用malloc或者new在堆区申请完空间之后,必须释放分别调用free和delete释放申请的空间,
否则就会造成内存泄漏
内存污染:向没有申请过的内存空间写入数据,可能会覆盖掉原本有用的数据,造成程序崩溃,是坚决不允许的情况