C/C++内存分布
我们先来复习一下C/C++的内存分布
数据的存储和用途分类
在C/C++程序中,从数据的存储和用途的角度来看,程序中的数据主要分为以下几类:
- 局部数据:
- 定义在函数内部或代码块内部的数据。
- 每次函数调用时创建,函数返回时销毁。
- 静态数据:
- 使用
static
关键字修饰的数据。 - 只在一个局部域(如函数内部)或当前文件内使用,生命周期贯穿整个程序运行期间。
- 使用
- 全局数据:
- 定义在所有函数外部的数据。
- 在整个程序中都可以访问。
- 常量数据:
- 使用
const
关键字修饰的数据。 - 在程序运行期间值不变。
- 使用
- 动态申请的数据:
- 使用动态内存分配函数(如
malloc
,calloc
,new
等)申请的数据。 - 需要程序员手动管理内存(分配和释放)。
- 使用动态内存分配函数(如
程序的存储与运行
- 程序存储:
- 我们平时写的代码以
.c
、.cpp
等源文件形式存在磁盘上。 - 编译后生成
.exe
可执行程序文件,该文件也存储在磁盘上。
- 我们平时写的代码以
- 程序运行:
- 将可执行文件加载到内存中,以进程的形式在操作系统上运行。
虚拟进程地址空间划分
当程序运行起来后,操作系统会为其分配一个虚拟进程地址空间,该空间划分为以下几个区域:
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。(注意:栈是自动管理的,不会出现空间碎片的情况。栈可以通过函数_alloca进行动态分配,但是所分配的空间不能通过frew或delete释放。)
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量。
C/C++动态内存管理方式
回顾:C语言中动态内存管理方式
我们先来看一段代码:
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
free(p3 );
}
我们来回顾一下:
1.malloc/calloc/realloc的区别是什么?
malloc
:
- 用于分配指定字节数的内存空间。
- 内存空间的内容是未初始化的(即内存中的值是不确定的)。
calloc
:
- 用于分配指定数量(
num
)的元素,每个元素的大小为size
字节的内存空间。 - 内存空间的内容被初始化为零。
- 实际上,
calloc(4, sizeof(int))
等价于malloc(4 * sizeof(int))
,但前者会将分配的内存全部初始化为0。
realloc
:
- 用于调整之前通过
malloc
或calloc
分配的内存块的大小。 - 如果新大小大于旧大小,则扩展内存区域(可能会也可能不会移动内存块的位置)。
- 如果新大小小于旧大小,则收缩内存区域,并可能释放多余的空间。
- 注意:如果
realloc
失败,它将返回NULL
,并且原来的内存块(ptr
指向的)仍然有效。因此,在使用realloc
时,通常的做法是将原始指针保存在一个临时变量中,以便在realloc
失败时能够释放它。
2.这里需要free(p2)吗?
p2
是通过calloc
分配的内存,然后p2
被传递给realloc
以调整大小。在这种情况下,你不需要(也不应该)在调用realloc
之后立即释放p2
,因为realloc
可能会返回p2
指向的同一个内存地址(如果内存块没有移动的话),或者它可能会返回一个新的地址(如果内存块被移动了)。无论哪种情况,你都应该使用realloc
返回的指针(p3
)来访问重新分配的内存,并在不再需要时释放它。
C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
1.new/delete操作内置类型
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]
2.new和delete操作自定义类型
注意:在申请自定义类型的空间时,new和delete会调用构造函数初始化、析构函数清理,而malloc与free不会。
class A{
public:
A(int a = 0): _a(a){
cout << "A():" << this << endl;
}
~A(){
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main() {
cout << "new / delete 和 malloc / free最大区别是 new / delete对于自定义类型除了开空间," << endl;
cout << "还会调用构造函数和析构函数:" << endl;
A* p1 = (A*)malloc(sizeof(A)); // C
A* p2 = new A(1);
free(p1);
delete p2;
cout << "内置类型是几乎是一样的!" << endl;
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
cout << "申请开辟连续的空间:" << endl;
A* p5 = (A*)malloc(sizeof(A) * 10); // C
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
输出:
接下来,我们来看一下C语言版本的申请链表节点和C++版本的申请链表节点的代码:
C语言版本的:
struct ListNode
{
int _val;
struct ListNode* _next;
};
struct ListNode* BuyListNode(int x)
{
// 单纯开空间
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
// 检查
newnode->_next = NULL;
newnode->_val = x;
return newnode;
}
int main()
{
struct ListNode* n1 = BuyListNode(1);
struct ListNode* n2 = BuyListNode(2);
struct ListNode* n3 = BuyListNode(3);
return 0;
}
C++版本的:
struct ListNode
{
int _val;
struct ListNode* _next;
ListNode(int x)
:_val(x)
,_next(NULL)
{}
};
int main()
{
// 开空间+调用构造函数初始化
ListNode* nn1 = new ListNode(1);
ListNode* nn2 = new ListNode(2);
ListNode* nn3 = new ListNode(3);
return 0;
}
我们发现,C++版本的确实方便和简洁很多。
总结:malloc/free和new/delete的区别
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
new和delete实现原理
1.operator new与operator delete函数(重点)
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。它们与C语言中的 malloc
和 free
类似,但有一些重要的区别,它们并不是让我们直接去使用的。
/*
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)
2.new和delete的实现原理
(1)内置类型
(2)自定义类型
讲解
我们前面说过,malloc主要做的事是开空间,new在开空间的基础上还要调用构造函数。在处理开空间失败的情况,而面向对象语言处理失败不喜欢用返回值,而是使用抛异常(通过上面的代码可见),我们new一个对象的时候,它首先调用 operator new
来分配内存,这背后会调用 malloc,
然后调用对象的构造函数来初始化该内存区域中的对象。当使用 delete
释放一个对象时,它首先调用对象的析构函数来清理对象占用的资源,然后调用 operator delete
来释放内存,这背后会调用 free
。
例子
接下来,我们通过一个Stack的部分实现代码来理解一下new和delete的实现原理:
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack(size_t capacity = 3)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
// 需要申请一个堆上的栈对象
Stack* p1 = new Stack;
delete p1;
return 0;
}
使用 new 时:
当执行 Stack* p1 = new Stack; 时,涉及以下步骤和函数调用:
1.operator new:
- 作用:分配足够的内存空间以存储 Stack 对象。
- 执行顺序:首先被调用。
- 作用对象:为 Stack 对象分配内存。
- 注意:operator new 最终会调用 malloc 来分配内存,但这一步在标准C++中通常是隐藏的,用户代码不会直接调用 malloc。然而,由于 Stack 的构造函数中显式调用了 malloc,这里区分了构造函数内部的 malloc 和 operator new 调用的内存分配。
2.构造函数 Stack(size_t capacity = 3):
- 作用:初始化 Stack 对象。
- 执行顺序:在 operator new 之后调用。
- 作用对象:对分配的内存进行初始化,设置 _array、_capacity 和 _size 成员变量。
- 注意:构造函数内部调用了 malloc 来为 _array 分配内存。
使用 delete 时:
当执行 delete p1; 时,涉及以下步骤和函数调用:
1.析构函数 ~Stack():
- 作用:清理 Stack 对象,释放资源。
- 执行顺序:首先被调用。
- 作用对象:释放 _array 指向的内存,并重置 _array、_capacity 和 _size 成员变量。
2.operator delete:
- 作用:释放 Stack 对象所占用的内存。
- 执行顺序:在析构函数之后调用。
- 作用对象:释放 Stack 对象所占用的内存空间。
- 注意:operator delete 最终会调用 free 来释放内存,但这一步在标准C++中通常是隐藏的。由于 Stack 的析构函数已经显式调用了 free 来释放 _array,这里的 operator delete 负责释放整个 Stack 对象所占用的内存(即 operator new 分配的内存)。
定位new表达式(placement-new)(了解)
作用
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,通常在显示调用构造函数和析构函数中应用的比较多。
使用格式
new (place_address) type;
//或者
new (place_address) type(initializer-list);
//place_address必须是一个指针,initializer-list是类型的初始化列表
应用场景:池化技术
池化技术概述
池化技术是一种资源管理技术,用于提高资源使用的效率和性能。常见的池化技术包括内存池、线程池、连接池等。
内存池与定位new的应用
我们会以内存池为例进行讲解:
内存池主要是用于管理内存资源的分配和释放,以减少频繁向操作系统申请和释放内存带来的开销。想象这样一个场景,如果现在有一个应用,需要频繁地申请和释放内存,这会十分麻烦。为了解决这种麻烦,我们可以提前申请一个比较大的内存,将这个比较大的内存放在内存池,我们在申请内存的时候就不用找堆,而是找内存池要内存,这样效率会高些(补充一句:内存池中的内存用完了,还是会找堆要内存)。由于内存池中的内存不是通过new操作符分配的,因此不会自动调用构造函数进行初始化。同样地,释放内存时也不会自动调用析构函数。为了解决这个问题,我们可以使用定位new来在内存池中分配对象,并显式调用析构函数来销毁对象。
#include <iostream>
using namespace std;
class MyClass {
public:
int value;
// 默认构造函数
MyClass() : value(0) {
cout << "Default constructor called, value = " << value << endl;
}
// 带参数的构造函数
MyClass(int v) : value(v) {
cout << "Parameterized constructor called, value = " << value << endl;
}
// 析构函数
~MyClass() {
cout << "Destructor called, value = " << value << endl;
}
};
int main() {
// 分配一块未初始化的内存(假设足够大以容纳MyClass对象)
char buffer[sizeof(MyClass)];
// 使用定位new在buffer上调用默认构造函数
MyClass* obj1 = new (buffer) MyClass();
// 使用定位new在buffer上调用带参数的构造函数
// 注意:这里需要重新分配一块未初始化的内存,因为buffer已经被obj1使用了
// 在实际使用中,你可能会维护一个内存池来管理这些内存块
char buffer2[sizeof(MyClass)];
MyClass* obj2 = new (buffer2) MyClass(42);
// ... 使用obj1和obj2 ...
// 显式调用析构函数
obj1->~MyClass();
obj2->~MyClass();
// 注意:这里没有使用delete,因为内存是通过定位new在预先分配的内存上分配的
// 也不应该使用delete,因为这会导致未定义行为(尝试释放非堆内存)
return 0;
}
内存泄漏
什么是内存泄漏
内存泄漏的危害
在我们平时写代码时,如果我们申请了内存却不去释放它,进程正常结束后操作系统会帮我们释放资源。但是,在服务器上的服务程序,如果不去释放申请的内存会有很大的危害,因为服务器的服务程序是长期运行的(例如:游戏服务、电商服务……),它们在非升级期间是不能停机的。最怕的是每天泄露一点点内存,这不会影响平时的正常使用,但是用久了服务器就会十分卡。
所以,长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会
如何避免内存泄漏
- 事前预防型。如智能指针等。
- 事后查错型。如泄漏检测工具。