文章目录
一、C/C++内存分布
说明:
- 栈:又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射区(共享区):是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆:用于程序运行时动态内存分配,堆是可以上增长的。
- 数据区:存储全局数据和静态数据。
- 代码区:可执行的代码/只读常量。
对于这几个区域,有如下特点:
比如32位操作系统下,其虚拟内存(进程地址空间)只有4G(注:即使物理内存有8G,也是用不完的):
- 堆是很大的,内核空间大概1G,剩余的几个区域共3G,其中大部分都是堆的。
- 栈是很小的,Linux下一般只有8M左右。(所以递归调用太深,会导致栈溢出)
- 数据区和代码区也不是很大,因为。
👉请看下面这道题,理解不同的数据分别存放在内存的哪个区域?
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
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在哪里?C staticGlobalVar在哪里?C
staticVar在哪里?C localVar在哪里?A
num1 在哪里?A
char2在哪里?A *char2在哪里?A
pChar3在哪里?A *pChar3在哪里?D
ptr1在哪里?A *ptr1在哪里?B
二、C++内存管理方式
2.1 C语言 的动态内存管理方式
通过 malloc / calloc / realloc & free 库函数进行动态内存管理。
#include<stdlib>
int main()
{
// malloc开辟一块sizeof(int)个字节的空间
int* p1 = (int*)malloc(sizeof(int));
assert(p1);
free(p1);
p1 = nullptr;
// calloc可以对申请的内存空间的每个字节初始化为0
// calloc 等价于 malloc + memset(0)
int* p2 = (int*)calloc(4, sizeof(int)); // 为4个大小为sizeof(int)的元素开辟一块空间
assert(p2);
// realloc是对malloc和calloc开的空间进行扩容
int* p3 = (int*)realloc(p2, sizeof(int) * 4);
if (p3 != nullptr)
{
p2 = p3;
}
free(p2);
p2 = nullptr;
return 0;
}
2.2 C++ 的内存管理方式
C语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
思考:malloc / free 和 new / delete,有什么区别呢?
👉区别1:
- 如果动态申请的是内置类型,它们没有区别。
- 如果动态申请的是「自定义类型」,它们有区别。
int main()
{
// 库函数 - malloc/free
int* p1 = (int*)malloc(sizeof(int));
free(p1);
// 操作符 - new/delete是关键字
int* p2 = new int;
delete p2;
}
new / delete 操作内置类型:
void Test1()
{
int* p1 = new int; // 动态申请一个int类型的空间
delete p1;
int* p2 = new int(10); // 动态申请一个int类型的空间并初始化为10
delete p2;
int* p3 = new int[5]; // 动态申请10个int类型的空间
delete[] p3;
}
new / delete 操作自定义类型:
void Test2()
{
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
private:
int _a;
};
A* p1 = (A*)malloc(sizeof(A));
free(p1);
A* p2 = new A;
delete p2;
}
结论:
-
观察运行结果,new / delete 操作「自定义类型」,不仅仅会开空间 / 释放空间,还会自动调用构造函数和析构函数。
汇编代码可以看的更清楚:
-
尽量使用 new / delete
注意:
- 申请和释放单个元素的空间,使用
new
和delete
操作符,申请和释放连续的空间,使用new[]
和delete[]
,一定要匹配使用,否则可能会导致程序崩溃。
👉区别2:
malloc 和 new 出错之后的处理方式不一样(C++ 提供一种新的处理错误的方式:抛异常)
malloc 在申请空间失败时会返回 NULL,new 会抛异常。
void Test3()
{
// malloc申请空间失败,返回NULL
char* p1 = (char*)malloc((size_t)2 * 1024 * 1024 * 1024); // 开2G空间
if (p1 == NULL)
printf("malloc fail\n");
else
printf("malloc success\n");
// new跟malloc不一样,申请空间失败了,会抛异常
try
{
char* p2 = new char[0x7fffffff];
// 如果抛了异常,这后面的语句就不会执行,会直接跳到捕获异常的位置
printf("xxxxx\n");
}
catch (const exception& e) // 如果没有捕获异常的位置,会终止程序
{
cout << e.what() << endl;
}
}
三、new & delete 底层
3.1 operator new 与 operator delete 函数(重点)
new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数(不是对 new 和 delete 运算符的重载),new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
operator new 和 operator delete 函数底层代码:
/*
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 函数就是对 malloc 的封装,申请内存失败了,会抛出异常。
那为什么不直接用 malloc 来申请空间呢?-- 因为 malloc 申请失败了会返回 NULL,就达不到抛异常的机制了,所以产生了 operator new 全局函数。
-
operator delete 函数就是对 free 的封装。
思考:我们可以直接用 operator new 和 operator delete 函数来开辟/释放空间吗?-- 可以的。
operator new 和 operator delete 函数跟 malloc 和 free 函数的使用没啥区别,唯一的区别就是会抛异常。
void Test4()
{
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
private:
int _a = 10;
};
A* p1 = (A*)malloc(sizeof(A));
free(p1);
// 调试发现,operator new和operator delete函数不会调用构造和析构函数
// 跟malloc和free的唯一区别就是,它会抛异常
A* p2 = (A*)operator new(sizeof(A));
operator delete(p2);
A* p3 = new A;
delete p3;
}
3.2 总结一下
通过上述代码和测试,我们发现,new 和 delete 其实没啥神奇的,就是一层一层的封装。
-
new = operator new( = malloc + 抛异常) + 构造函数
-
delete = 析构函数 + operator delete( = free)
3.3 重载类专属的 operator new 与 operator delete 函数(了解)
重载一个类专属的 operator new 与 operator delete 函数,那么实例化对象申请空间时就会调用专属的 operator new / operator delete 函数(比如在其中进行内存池的申请和释放,提高效率),了解一下即可,这个语法,实际中价值也不大,很少用。
四、new & delete 的原理(重要)
对于内置类型:
如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本类似,不同的地方是:
-
new / delete 申请/释放的是单个元素的空间,new[] 和 delete[] 申请/释放的是连续空间,
-
而且 new 在申请空间失败时会抛异常,malloc 会返回 NULL。
对于自定义类型:
new A,做了哪些事情呢?
-
先调用
operator new(size)
申请空间(底层其实是调用malloc
来申请)。 -
再调用 A 的构造函数对申请的空间初始化。
delete ptr,做了哪些事情呢?
-
先调用析构函数完成 ptr 指向的对象中资源的清理。
-
再调用
operator delete(ptr)
函数释放 ptr 所指向的空间(底层其实是调用free
来释放)。
new A[n],做了哪些事情呢?
-
先调用
operator new[](n)
函数申请空间,在operator new[]()
中实际调用operator new()
函数来完成 n 个对象空间的申请。 -
再调用 n 次 A 的构造函数对申请的空间初始化。
delete[] ptr,做了哪些事情呢?
-
先调用 n 次析构函数完成 ptr 指向的 n 个对象中资源的清理。
-
再调用
operator delete[](ptr)
函数释放 ptr 指向的空间,在operator delete[]()
中实际调用operator delete()
函数来释放空间。
五、定位 new 表达式(placement-new)
👉思考:构造函数在定义对象时自动调用的,但现在我这里有一块已分配的空间,想要调用构造函数对这块空间初始化,有没有什么办法呢?
定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
语法格式:new(place_address) type
或者 new(place_address) type(initializer-list)
注意:place_address
必须是一个指针,initializer-list
是该类型的初始化列表。
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
private:
int _a = 10;
};
int main()
{
// 比如我用malloc申请了一块空间,想要显式调用构造函数对其初始化
// p现在指向的只不过是与A对象相同大小的一块空间,还不能算是一个对象,因为构造函数没有执行
A* p = (A*)malloc(sizeof(A));
// 定位new表达式:显式的对一块空间调用构造函数初始化
new(p) A(); // 注意:如果A类的构造函数有参数时,此处需要传参
// 析构函数可以直接显式调用
p->~A();
free(p);
return 0;
}
使用场景:
-
定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。
-
复制一份 a 数组到另外一块空间中去。
传统做法:
int main() { A a[5]; // 调用5次构造函数,再调用5次拷贝构造函数 A* p = new A[5]; for (int i = 0; i < 5; i++) { p[i] = a[i]; } return 0; }
使用定位 new 表达式做法:
int main() { A a[5]; A* p = (A*)malloc(sizeof(A) * 5); for (int i = 0; i < 5; i++) { new(p + i) A(a[i]); // 直接调用5次拷贝构造就行了 } return 0; }
六、常见面试题
malloc / free 和 new / delete 的区别
malloc / free 和 new / delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同点从三个方面去比较:
1)特点和用法
- malloc 和 free 是函数,new 和 delete 是操作符。
- malloc 申请空间不会初始化,new 申请内置类型不会初始化,申请自定义类型会调用构造函数初始化。
- malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可。
- malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型。
2)底层原理的区别
- 申请自定义类型对象时,malloc / free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间
后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理。
3)处理错误的方式
- malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常。
七、内存泄漏
7.1 内存泄漏及其内存泄漏的危害
在堆上申请的空间,在我们不用了以后也没有释放,就存在内存泄漏
什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
int main()
{
char* p = new char[1024 * 1024 * 1024];
return 0;
}
思考:上述程序存在内存泄漏,一次泄漏 1G,但是对我们系统好像也没有什么影响,这是为什么呢?
因为一个进程正常结束后,会把映射的内存都会释放掉,对于上述程序,虽然我们没有主动释放,但是进程结束后也会释放这些内存。
思考:那么内存泄漏真的没啥影响吗?因为进程正常结束后,这些空间都会释放。
-
但是进程没有正常结束呢?就会变成僵尸进程,可能存在一些资源没有释放。
-
还有一些需要长期运行的服务器程序,比如网络游戏的后台服务,内存泄漏会导致可用内存越来越少,从而影响到其它进程的工作。
内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
C++ 我们需要主动释放内存,Java 不需要主动释放内存,Java 后台有垃圾回收器,接管了内存释放。
7.2 内存泄漏的分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
-
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
-
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
7.3 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
证。 - 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结:内存泄漏非常常见,解决方案一般分为两种。
-
事前预防型。如智能指针等。
-
事后查错型。如泄漏检测工具。
7.4 拓展
如何一次性在堆上申请4G的内存?
因为 32 位的进程,虚拟内存(进程地址空间)只有4G,不可能申请成功。
而 64 位的进程,虚拟内存(进程地址空间)有 264 G,所以可以申请成功。
// 将程序编译成 x64 的进程
int main()
{
try
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}