目录
引入:
一、C++中的内存布局
1.内存区域
2.示例变量存储位置说明
二、C语言中动态内存管理
三、C++内存管理方式
1.new/delete操作内置类型
2.new和delete操作自定义类型
四、operator new与operator delete函数(重要点进行讲解)
五、new和delete的实现原理
六、定位 new 和定位 delete
七、面试题
1.malloc/free和new/delete的区别?
2.内存泄漏
引入:
以下代码分别存储在哪个区域
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);
}
图解:
一、C++中的内存布局
在了解具体的内存管理操作之前,先得清楚 C++ 程序的内存布局。一般来说,一个 C++ 程序的内存可以大致分为以下几个区域:
1.内存区域
-
内核空间:
- 用户代码不能读写。
- 操作系统内核运行在此空间,负责管理系统资源和执行关键任务。
-
代码区(Text Segment):
- 存放程序的可执行代码,即机器指令。
- 通常是只读的,防止程序在运行过程中意外修改自身的代码。
-
全局 / 静态存储区(Data Segment):
- 存储全局变量和静态变量。
- 全局变量在整个程序的生命周期内都存在,而静态变量(包括局部静态变量)根据其定义的作用域有不同的可见性,但它们的生命周期都是从程序开始到结束。
-
栈区(Stack):
- 用于存储函数调用时的局部变量、函数参数以及返回地址等信息。
- 遵循后进先出(LIFO)的原则,每当一个函数被调用时,相关的信息就会被压入栈中,函数执行完毕后,这些信息又会被弹出栈。
- 栈的大小通常是在程序启动时就确定好的,相对有限,如果在栈上分配过多的内存,可能会导致栈溢出的错误。
-
堆区(Heap):
- 是一块相对较大且比较灵活的内存区域,用于动态分配内存。
- 程序员可以在程序运行期间根据需要随时在堆上申请和释放内存。
- 与栈不同,堆上的内存分配和释放需要程序员手动进行管理,这也正是内存管理容易出现问题的地方之一。如果忘记释放堆内存,会导致内存泄漏。
-
常量存储区:
- 存放常量数据,如字符串常量等。
- 这些数据在程序运行期间不能被修改。
-
内存映射段:
- 可以用于文件映射、动态库加载、匿名映射等。
- 提供了一种将文件或其他资源映射到进程内存空间的方式,以便更高效地访问这些资源。
2.示例变量存储位置说明
1、globalVar在哪里?
根据上面的代码可知,glovalVar是在main函数外创建的变量,即在全局创建的变量,全局变量存放在数据段(静态区)中。
2、staticGlobalVar在哪里?
staticGlobalVar是在main函数外创建的静态变量,即在全局创建的静态变量,全局静态变量存放在数据段(静态区)中。
3、staticVar在哪里?staticVar是在main函数内部创建的静态变量,即在局部创建的静态变量,局部静态变量存放在数据段(静态区)中。
4、localVar在哪里?
localVar是在main函数内部创建的变量,即在局部创建的普通变量,局部创建的普通变量存放在栈区。
5、char2在哪里?char2是在main函数内部创建的数组的数组名,即在局部创建的多个普通变量,局部创建的普通变量存放在栈区。
6、* char2在哪里?
*char2是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*char2的则存放在哪个区域,*char2是数组的第一个字符,即字符变量中的第一个元素,字符变量存放在栈区,因此*char2存放在栈区。
7、pChar3在哪里?pChar3是在main函数内部创建的const修饰的常指针变量,实质还是一个局部创建的变量,只是该变量的值不能修改,因此pChar3存放在栈区。
8、* pChar3在哪里?
*pChar3是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*pChar3的则存放在哪个区域,*pChar3是常量字符串的第一个字符,字符常量存放在代码段(常量区),因此*pChar3存放在代码段(常量区)。
9、ptr1在哪里?ptr1是在main函数内部创建的指针变量,实质还是一个局部创建的变量,因此pChar3存放在栈区。(ptr2、ptr3同理)
10、* ptr1在哪里?
*ptr1是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*ptr1的则存放在哪个区域,*ptr1是通过动态开辟的空间,动态开辟的空间存放在堆区,因此*ptr1存放在堆区。(ptr2、ptr3同理)
顺便提一下:为什么说栈是向下增长的,而堆是向上增长的?
简单来说,在一般情况下,在栈区开辟空间,先开辟的空间地址较高,而在堆区开辟空间,先开辟的空间地址较低。
例如,下面代码中,变量a和变量b存储在栈区,指针c和指针d指向堆区的内存空间:
#include <iostream>
using namespace std;
int main()
{
//栈区开辟空间,先开辟的空间地址高
int a = 10;
int b = 20;
cout << &a << endl;
cout << &b << endl;
//堆区开辟空间,先开辟的空间地址低
int* c = (int*)malloc(sizeof(int)* 10);
int* d = (int*)malloc(sizeof(int)* 10);
cout << c << endl;
cout << d << endl;
return 0;
}
因为在栈区开辟空间,先开辟的空间地址较高,所以打印出来a的地址大于b的地址;在堆区开辟空间,先开辟的空间地址较低,所以c指向的空间地址小于d指向的空间地址。
注意:在堆区开辟空间,后开辟的空间地址不一定比先开辟的空间地址高。因为在堆区,后开辟的空间也有可能位于前面某一被释放的空间位置。
二、C语言中动态内存管理
1.malloc:
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。传参时只需传入需要开辟的字节个数。
2.calloc
calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。calloc函数传参时需要传入开辟的内存用于存放的元素个数和每个元素的大小。calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。
3.realloc
realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小。realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL。
4.free
free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小。
这里只做简单的概述,若还想进一步了解malloc、calloc、realloc和free,请阅读动态内存管理。
三、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
1.new/delete操作内置类型
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
//动态申请10个int类型的空间并初始化为0到9
int* p7 = new int[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //申请 + 赋值
delete ptr4;//销毁
delete ptr5;
delete[] ptr6;
delete[] p7;
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。
2.new和delete操作自定义类型
对于以下自定义类型:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// 动态申请单个类的空间
A* ptr4 = new A;
// 动态申请一个A类的空间并初始化为10
A* ptr5 = new A(10);
// 动态申请10个A类的空间,创建 10 个对象
A* ptr6 = new A[10];
//动态申请10个A类的空间并初始化0到9
A* ptr7 = new A[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //申请 + 赋值
delete ptr4;//销毁
delete ptr5;
delete[] ptr6;
delete[] ptr7;
}
给一个malloc版本
int main()
{
// 动态申请单个类的空间
A* ptr4 = (A*)malloc(sizeof(A));
// 动态申请一个 A 类的空间并初始化为 10
A* ptr5 = (A*)malloc(sizeof(A));
// 动态申请 10 个 A 类的空间
A* ptr6 = (A*)malloc(sizeof(A) * 10);
// 动态申请 10 个 A 类的空间并初始化 0 到 9
A* ptr7 = (A*)malloc(sizeof(A) * 10);
free(ptr4);
free(ptr5);
free(ptr6);
free(ptr7);
return 0;
}
可以自己进行调试一下,会发现malloc,free和new,delete的区别。
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc和free不会。
总结一下:
1、C++中如果是申请内置类型的对象或是数组,用new/delete和malloc/free没有什么区别。
2、如果是自定义类型,区别很大,new和delete分别是开空间+构造函数、析构函数+释放空间,而malloc和free仅仅是开空间和释放空间。
3、建议在C++中无论是内置类型还是自定义类型的申请和释放,尽量都使用new和delete。
四、operator new与operator delete函数(重要点进行讲解)
new和delete在底层上就是调用operator new
和operator delete的。
operator new
和operator delete
是 C++ 中用于动态内存分配和释放的操作符函数。它们可以被重载以实现自定义的内存分配策略。默认情况下,operator new
会调用底层的操作系统函数来分配内存,而operator delete
会释放由operator new
分配的内存。
operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。
int* p1 = (int*)operator new(sizeof(int)* 10); //申请
operator delete(p1); //销毁
//-------------等价-----------------//
int* p2 = (int*)malloc(sizeof(int)* 10); //申请
free(p2); //销毁
默认行为:在 C++ 中,当我们使用new
关键字来分配内存时,实际上会调用operator new
函数。这个函数会尝试从堆中分配足够的内存来满足请求。如果分配成功,它会返回一个指向分配的内存的指针;如果分配失败,它会抛出一个std::bad_alloc
异常。
实际上,operator new的底层是通过调用malloc函数来申请空间的,当malloc申请空间成功时直接返回;若申请空间失败,则尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。而operator delete的底层是通过调用free函数来释放空间的。
注意:虽然说operator new和operator delete是系统提供的全局函数,但是我们也可以针对某个类,重载其专属的operator new和operator delete函数,进而提高效率。
例如,我们可以实现一个简单的内存池来提高内存分配的效率。
以下是一个简单的示例:
class MyClass
{
public:
static void* operator new(size_t size)
{
cout << "Custom operator new called." << endl;
return malloc(size);
}
static void operator delete(void* ptr)
{
cout << "Custom operator delete called." << endl;
free(ptr);
}
};
int main()
{
MyClass* obj = new MyClass();
delete obj;
return 0;
}
在这里,我们重载了MyClass
类的operator new
和operator delete
函数。当我们创建一个MyClass
对象时,会调用自定义的operator new
函数,该函数使用malloc
来分配内存。当我们释放一个MyClass
对象时,会调用自定义的operator delete
函数,该函数使用free
来释放内存。
五、new和delete的实现原理
内置类型
如果申请的是内置类型的空间,new/delete和malloc/free基本类似,不同的是,new/delete申请释放的是单个元素的空间,new[ ]/delete [ ]申请释放的是连续的空间,此外,malloc申请失败会返回NULL,而new申请失败会抛异常。
自定义类型
new的原理
1、调用operator new函数申请空间。
2、在申请的空间上执行构造函数,完成对象的构造。
delete的原理
1、在空间上执行析构函数,完成对象中资源的清理工作。
2、调用operator delete函数释放对象的空间。
new T[N]的原理
1、调用operator new[ ]函数,在operator new[ ]函数中实际调用operator new函数完成N个对象空间的申请。
2、在申请的空间上执行N次构造函数。
delete[ ] 的原理
1、在空间上执行N次析构函数,完成N个对象中资源的清理。
2、调用operator delete[ ]函数,在operator delete[ ]函数中实际调用operator delete函数完成N个对象空间的释放。
六、定位 new 和定位 delete
除了普通的operator new
和operator delete
,C++ 还提供了定位new
和定位delete
。定位new
允许我们在已经分配的内存上构造对象,而定位delete
允许我们在已经构造的对象上显式地调用析构函数并释放内存。
定位new
(placement new)
定位new
允许在已经分配好的内存地址上构造对象,而不是像普通的new
操作符那样从堆上动态分配新的内存。它的语法形式是new (place_address) type或者
或new(place_address)type(initializer-list),其中place_address
是一个已经分配好的内存地址,type
是要构造的对象类型,initializer-list是类型的初始化列表
定位delete
(placement delete)
定位delete
通常与定位new
配合使用,它允许在已经构造的对象上显式地调用析构函数并释放内存。一般情况下,不需要显式调用定位delete
,只有在特定的情况下(比如异常处理中需要确保正确的析构函数调用)才可能会用到。
使用场景:
定位new表达式在实际中一般是配合内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,就需要使用定位new表达式进行显示调用构造函数进行初始化。
#include <iostream>
#include <new>
class MyClass
{
public:
MyClass()
{
std::cout << "MyClass constructor called." << std::endl;
}
~MyClass()
{
std::cout << "MyClass destructor called." << std::endl;
}
};
int main()
{
//此时只是开辟空间,没有创建对象,因为构造函数没有调用
char* buffer = new char[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass(); //new(place_address)type 形式
obj->~MyClass();
delete[] buffer;
return 0;
}
代码讲解:
在这个例子中,首先分配了一块足够大的内存空间(通过char* buffer = new char[sizeof(MyClass)];
),然后使用定位new
在这个已经分配好的内存地址buffer
上构造了一个MyClass
对象。这里先显式调用对象的析构函数(obj->~MyClass();
),然后释放分配的内存块(delete[] buffer
)。注意,这里并没有直接使用定位delete
,而是通过先调用析构函数再释放内存块的方式来模拟定位delete
的行为。
总的来说,定位new
和定位delete
提供了一种在特定内存位置上构造和销毁对象的机制,在一些特定的场景下可以提供更灵活的内存管理方式。但使用时需要非常小心,确保内存的正确分配和释放,以避免出现内存泄漏和未定义行为等问题。
七、面试题
1.malloc/free和new/delete的区别?
共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
1、malloc和free是函数,new和delete是操作符。
2、malloc申请的空间不会初始化,new申请的空间会初始化。
3、malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即 可。
4、malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类 型。
5、malloc申请失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要 捕获异常。
6、申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数,而 new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函 数完成空间中资源的清理。
2.内存泄漏
什么是内存泄漏,内存泄漏的危害?
内存泄漏:
内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
内存泄漏分类
在C/C++中我们一般关心两种方面的内存泄漏:
1、堆内存泄漏(Heap Leak)
堆内存指的是程序执行中通过malloc、calloc、realloc、new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete释放。假设程序的设计错误导致这部分内容没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap
Leak。
2、系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄漏?
1、工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记住匹配的去释放。
2、采用RAII思想或者智能指针来管理资源。
3、有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
4、出问题了使用内存泄漏工具检测。
内存泄漏非常常见,解决方案分为两种:
1、事前预防型。如智能指针等。
2、事后查错型。如泄漏检测工具。