C/C++内存管理
这期我们来介绍一下C/C++中的内存管理
文章目录
- C/C++内存管理
-
- C/C++内存分布
- C语言内存管理方式
- C++内存管理方式
-
- new/delete操作内置类型
- new/delete操作自定义类型
- [operator new和operator delete函数](#operator new和operator delete函数)
- new和delete的实现原理
- 定位new表达式
- new/delete和malloc/free的区别(面试题
- 内存泄漏
C/C++内存分布
cpp
int globalval = 1;
static int staticGlobalvar = 1;
void Test() {
static int staticvar = 1;
int localval = 1;
int num1[10] = { 1,2,3,4, };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
根据以上代码, 提出以下几个问题
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?____
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
看到这里的同学肯定是一脸懵逼,哪里有一上来就写题目的,待看到下面的图后,大部分的填空题都迎刃而解
- 栈又叫堆栈------存储非静态局部变量/函数参数/返回值等等,栈是向下增长的
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可以使用系统接口创建共享内存,做进程间通信
- 堆是用于程序运行时动态内存分配,堆是可以向上增长的
- 数据段------存储全局数据和静态数据
- 代码段------存储可执行代码、只读常量、字面量等
在回顾前面的问题
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?
C
staticGlobalVar在哪里?
C
staticVar在哪里?
C
localVar在哪里?
A
num1 在哪里?
A
char2在哪里?
A
前面几道是轻而易举的,到了后面几道就有点懵逼了。可能有的同学会把*char2
解引用成"abcd"
因而就选择了D
这个是错误的,因为"abcd"
是拷贝给char2
的,而char2
在栈区,因此*char2
解引用是在栈区的,因此*char2
在栈区
pchar3
毋庸置疑是在栈区,*pchar3
解引用后是"acbd"
,但是前面有一个const
修饰*pchar3
所指向的内容(注意是修饰指针所指向的内容,不是修饰指针,因为修饰指针需要这样子写:const char* pChar3 const = "abcd";
,在指针后面写const
才是修饰指针,因此*pchar3
所指向的内容是数据段,也就是静态区
ptr1
在栈区,*ptr1
使用了malloc
函数在堆上开取空间,因此在堆区
这里重新回顾一下C语言的知识
c
sizeof(num1) = ____;
sizeof(char2) = ____;
strlen(char2) = ____;
sizeof(pChar3) = ____;
strlen(pChar3) = ____;
sizeof(ptr1) = ____;
答案是
sizeof(num1)=40
sizeof计算所有的大小:也就是int:4字节 * 数组大小:10=40字节
sizeof(char2)=5
char2是字符串,结尾包括\0
,因此sizeof的结果是"abcd"+'\0'
=5字节
strlen(char2)=4
strlen遇到\0
就停下来,因此只包括前面的字符串,也就是"abcd"
=4字节
sizeof(pChar3)=4/8
,因为pChar3是指针,在32位平台下是4字节,64位平台下是8字节
strlen(pChar3)=4
strlen遇到\0
就停下来,因此只包括前面的字符串,也就是"abcd"
=4字节
sizeof(ptr1)=4/8
因为ptr1是指针,在32位平台下是4字节,64位平台下是8字节
C语言内存管理方式
c
int main() {
int* p1 = (int*)malloc(sizeof(int) * 4);
free(p1);
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int));
free(p3);
//free(p2);
return 0;
}
其中有malloc
、calloc
、realloc
、free
函数
可以在c语言的参考手册上可以看见
为什么不需要free(p2)
呢,因为realloc
是空间不够时,扩容用到的,这里还没有用到对应的内存,因此不需要free
C++内存管理方式
可以看见C语言在内存管理方面,有点繁琐,并且在某些方面是无能为力的,因此C++推出了属于他的内存管理方式:new
和delete
。当然,C++是兼容C的,因此C语言的内存管理在C++中也能用
new/delete操作内置类型
cpp
int main() {
//动态申请一个int类型的空间
int* ptr1 = new int;
//动态申请一个int类型的空间,并且初始化为10
int* ptr2 = new int(5);
//动态申请5个int类型的空间
int* ptr3 = new int[5];
//动态申请5个int类型的空间,并且全部初始化为0
int* ptr4 = new int[5] {0};
//动态申请5个int类型的空间,并且部分初始化,其余全为0,与c语言的部分初始化类似
int* ptr5 = new int[5] {1, 2, 3};
//回收资源
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
delete[] ptr5;
return 0;
}
调试结果(未delete
之前)
delete
后
可以看见delete
后,编译器对内置类型,就把所对应的内存给释放掉(释放地址),并没有把已经初始化的值给清理掉(和malloc
、free
是没有区别的)
有了new和delete之后,动态申请内存就没有C语言这么麻烦了,不需要手动计算大小,操作便捷。正所谓学编程的人的对象,都是可以new的
new/delete操作自定义类型
cpp
int main() {
A* p1 = new A;
A* p2 = new A(5,6);
A* p3 = new A[5];
A* p4 = new A[5]{ {1,2},{3,4},{5,6} };
//malloc和new的区别就在于,new首先会创建空间,然后去调用类的默认构造函数初始化,而malloc不会调用默认构造
A* m1 = (A*)malloc(sizeof(A));
int* p = (int*)malloc(sizeof(int) * 5);
//同时free和delete的区别于,delete会调用析构函数,而free不会
//malloc/free在内置类型上,和new/delete没有区别
delete p1;
delete p2;
delete[] p3;
delete[] p4;
free(p);
free(m1);
return 0;
}
调试(未delete前)
delete后
运行结果
对于自定义类型,new
会去调用类的默认构造,上面代码中,new
12个对象,就调用了12次默认构造,同样delete
也是调用了12次析构函数
因此,C++推出new
和delete
是为了更好的支持自定义类型,符合面向对象编程的逻辑
operator new和operator delete函数
在cpluscplus的参考手册上是这样写的
operator new
can be called explicitly as a regular function, but in C++,new
is an operator with a very specific behavior: An expression with thenew
operator, first calls functionoperator new
(i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.[operator new - C++ Reference](https://legacy.cplusplus.com/reference/new/operator new/)
new
和delete
是用户进行动态管理内存申请和释放的操作符,operator new/operator delete
是cpp提供的全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete
来释放空间
cpp/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。 */ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } /* operator delete: 该函数最终是通过free来释放空间的 */ void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg(pUserData, pHead->nBlockUse); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; } /* free的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator new 和operator delete底层代码
new和delete的实现原理
内置类型
new
和delete
在内置类型上,基本和malloc
和free
的功能一致,不同的地方:new/delete
申请和释放的是单个的内存空间,new[]/delete[]
申请和释放的是连续的内存空间,new
失败时返回异常,malloc
失败返回NULL
自定义类型
new
的原理
- 先调用
operator new
开辟空间 - 在开辟的空间执行构造函数
delete
的原理
- 先调用
operator delete
- 在调用自定义类的析构函数
new[]
的原理
- 先调用
operator new[]
开辟N个空间 - 在开辟的空间执行N次构造函数
delete[]
的原理
- 先调用
operator delete[]
- 在调用N次自定义类的析构函数
定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
格式
cpp
ptr = (type*)operator new(ptr) type;
ptr = (type*)operator new(ptr) type(initializer_list);
其中type*
是类型指针,ptr
是指针,initializer_list
是初始化列表,type
是类型
应用场景
定位new主要搭配内存池技术使用
示例:
cpp
class A {
public:
A(int x = 0, int y = 0)
:_a(x),
_b(y)
{
//查看是否运行构造函数
cout << "A():" << endl;
}
~A() {
//查看是否运行析构函数
cout << "~A()" << endl;
}
private:
int _a;
int _b;
};
int main() {
A* p1 = (A*)operator new(sizeof(A));
new(p1)A(10);
p1 ->~A();
operator delete(p1);
return 0;
}
在C++中,能不能通过指针的方式调用构造函数和析构函数呢,显然,构造函数是没办法实例化的,因为是实例化对象时自动调用的,是编译器处理的,但是析构函数可以通过定位new
的方式来调用
new/delete和malloc/free的区别(面试题
相同点
都是从堆上申请空间,都需要用户手动释放
不同点
malloc/free
是函数,new/delete
是操作符malloc
申请的空间不会初始化,new
会初始化(有构造函数)malloc
在申请空间时,还需要sizeof
来手动计算类型大小,new
只需要在后面跟上类型即可,如果需要new
多个,在new []
中指定数量即可malloc
申请空间失败时,返回的是NULL
(在学数据结构时,肯定会有一个判断if(ptr==NULL)
,就输出或返回fail
),而new
不需要判空,只需要捕获异常:try catch
语句malloc
的返回值是void*
,在使用时必须强转((int*)malloc(sizeof(int))
),而new
不需要,因为new
后面紧跟着的是对应的数据类型- 在申请自定义类型的空间时,
malloc/free
只申请空间/释放空间,不能调用构造函数和析构函数,而new/delete
可以
内存泄漏
**定义:**什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
**内存泄漏的危害:**长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
分类
C/C++程序中,我们最主要关心两种方面的内存泄漏
堆内存泄漏(Heap Leak)
堆内存指的是程序执行中依据须要分配通过malloc/calloc/realloc/new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生HeapLeak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测
在VS下,可以使用Windous下提供的_CrtCheckMemory()
函数查看大概泄漏了多少内存,仅仅简单检查,并不能准确查找泄漏位置
cpp
int main() {
//1.忘记释放空间
int* p1 = new int[10];
int* p2 = new int[100];
_CrtDumpMemoryLeaks();
return 0;
}
调试结果
因此,在写代码过程中,一定要注意内存释放,这也是cpp最容易出问题的地方。创建空间一定要记得释放。
如果工程过大,泄漏位置多,可以使用以下工具
Linux:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
Windows:【VS2019】C/C++内存泄漏检测工具:Visual Leak Detector超详细安装教程(for windows)_windows leaks detector安装使用-CSDN博客
Other:内存泄露检测工具比较 - 默默淡然 - 博客园
如何避免
1.工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2.采用RAII思想或者智能指针来管理资源。
3.有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4.出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。