目录
[一、C/C++ 内存分布](#一、C/C++ 内存分布)
[1.new 操作符](#1.new 操作符)
[2.delete 操作符](#2.delete 操作符)
[3.new 和 delete 的优势](#3.new 和 delete 的优势)
[四、operator new 与 operator delete 函数](#四、operator new 与 operator delete 函数)
[1.operator new 的实现原理](#1.operator new 的实现原理)
[2.operator delete 的实现原理](#2.operator delete 的实现原理)
[3.operator new[] 与 operator delete[]](#3.operator new[] 与 operator delete[])
[五、new 和 delete 的实现原理](#五、new 和 delete 的实现原理)
[1. 内置类型的 new 和 delete 实现原理](#1. 内置类型的 new 和 delete 实现原理)
[2. 自定义类型的 new 和 delete 实现原理](#2. 自定义类型的 new 和 delete 实现原理)
[六、定位 new 表达式(placement-new)](#六、定位 new 表达式(placement-new))
[七、malloc/free 与 new/delete 的区别总结](#七、malloc/free 与 new/delete 的区别总结)
引言
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
一、C/C++ 内存分布
1.内存分布
在编译和执行C++程序时,内存划分为几个不同的区域,各个区域承担不同的任务。以下是C++内存的基本分布:
1.栈(Stack):
用途:用于存储局部变量、数参数、返回地址等。
特性:栈内存的分配和释放由系统自动管理,遵循LIFO(Last In, First Out)顺序。每当函数调用时,栈帧(Stack Frame)被压入栈中,函数返回时栈帧弹出。
适用场景:适合小规模的临时变量和函数参数的存储。
2.堆(Heap):
用途:用于程序运行时的动态内存分配。堆内存由程序员手动管理(申请和释放),例如通过new或malloc申请的内存。
特性:堆内存是向上增长的(从低地址到高地址),且不同于栈,堆内存不会自动释放。程序员需要显式调用delete或free来释放内存,否则会导致内存泄漏。
适用场景:适合需要动态分配或较大内存的数据结构,例如链表、树等。
3.数据段(Data Segment):
用途:用于存储全局变量、静态变量等,程序开始时分配,程序结束时释放。
特性:数据段分为已初始化的数据段和未初始化的数据段。已初始化的数据段(如定义为int globalVar = 1;的变量)在程序加载时会直接初始化为指定值;未初始化的数据段(如定义为int globalVar;的变量)则会被初始化为零。
适用场景:适合存储在整个程序生命周期内都需保持的变量。
4.代码段(Text Segment):
用途:存储程序的可执行代码,包括函数体、常量字符串等。
特性:代码段通常是只读的,防止代码在运行时被意外修改,提高了程序的安全性。
适用场景:适合存储程序中的可执行指令以及常量字符串等。
2.代码示例
cpp
#include <cstdlib> // 包含 malloc, calloc, realloc, free
#include <iostream>
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"; // 字符数组,存储在栈中(字符串内容复制到栈中)
const char* pChar3 = "abcd"; // 指针变量在栈中,指向的字符串常量"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); // 释放堆上的内存
}
int main() {
Test();
return 0;
}
3.详细解释
-
globalVar 和 staticGlobalVar:
- 这两个变量分别是全局变量和静态全局变量,它们都存储在 已初始化数据段 中。已初始化数据段用于存储在程序启动时分配的全局变量和静态变量,并且在程序的整个生命周期内一直存在。也就是说,程序结束前,它们的值会一直保留。
-
staticVar:
staticVar
是一个静态局部变量,虽然它是局部变量,但因为是static
类型,它的存储位置与普通局部变量不同。staticVar
存储在 已初始化数据段 ,即使函数退出,变量也不会被释放,而是保留其值,直到程序结束。如果函数再次调用,staticVar
不会重新分配内存,而是继续使用上一次保留的值。
-
localVar 和 num1:
localVar
是一个局部变量,num1
是一个局部数组,它们都存储在 栈 中。栈用于存储局部变量、函数参数等,栈内存是随着函数的调用自动分配的,函数返回时栈上的内存会自动释放。局部变量的生命周期仅限于函数执行期间,函数返回后它们会被销毁。
-
char2:
char2
是一个字符数组,存储在 栈 中。编译器会将字符串"abcd"
复制到栈中,这意味着"abcd"
的内容实际上存在栈内存中。由于char2[]
是局部变量,因此它的存储空间(包括字符串内容)在函数调用时分配,在函数返回时释放。
-
pChar3:
pChar3
是一个指针变量,存储在 栈 中。它指向一个字符串常量"abcd"
,该字符串常量存储在 只读数据段(常量区)。常量区用于存储字符串字面量等只读数据,因此该字符串在程序的整个生命周期内都存在,且不能被修改。
-
ptr1、ptr2、ptr3:
-
ptr1
、ptr2
和ptr3
这三个指针变量本身存储在 栈 中,指向的内存则存储在 堆 中。这些指针通过动态内存分配函数malloc
、calloc
和realloc
分配了堆内存。堆内存用于程序运行时动态分配的数据,需要手动释放。如果这些堆内存未通过free()
释放,则会导致内存泄漏。 -
ptr1 :通过
malloc
动态分配了内存,分配的内存位于堆中。 -
ptr2 :通过
calloc
动态分配了内存,分配的内存位于堆中,且会初始化为零。 -
ptr3 :通过
realloc
调整了ptr2
的内存大小,新的内存分配在堆中。
-
二、C语言中的动态内存管理
详见前面博客:C语言动态内存管理
在C语言中,动态内存管理通过以下几个函数实现:
1.malloc
-
malloc(memory allocation) :分配指定大小的内存块,内存中的数据未被初始化。返回值是
void*
类型指针,需要手动强制转换为具体的类型。示例:
cppint* ptr = (int*)malloc(sizeof(int) * 5); // 申请5个int的内存
2.calloc
- calloc(contiguous allocation) :分配一块连续的内存,并将所有字节初始化为零。返回值同样为
void*
类型。
示例:
cpp
int* ptr = (int*)calloc(5, sizeof(int)); // 申请5个int的内存并初始化为0
3.realloc
-
realloc(reallocation) :用于调整已经分配的内存块大小。传入新大小后,
realloc
会尝试扩大或缩小原有的内存块,如果扩展失败,它会在新的位置申请内存并拷贝原内容。示例:
cppint* newPtr = (int*)realloc(ptr, sizeof(int) * 10); // 将原来的内存扩展到10个int
4.free
-
free :用于释放通过
malloc
、calloc
、realloc
申请的内存。内存释放后,不再受程序管理,避免内存泄漏。示例:
cppfree(ptr); // 释放动态分配的内存
三、C++中的内存管理方式
C++继承了C语言的malloc、calloc、realloc和free,但提供了更加灵活的内存管理方式,即new
和delete
操作符:
1.new 操作符
-
用途:用于动态分配内存,并对基本类型或自定义对象进行初始化。
-
特性:分配内存失败时,new会抛出异常std::bad_alloc,而不会像malloc返回NULL。
-
语法:new 类型用于分配单个对象;new 类型[数量]用于分配数组。
示例:
cpp
// 动态申请一个int类型的空间,未初始化,值未定义
int* ptr1 = new int;
// 动态申请一个int类型的空间,并初始化为10
int* ptr2 = new int(10);
// 动态申请3个int类型的连续空间,未初始化,值未定义
int* ptr3 = new int[3];
2.delete 操作符
-
用途:用于释放通过new分配的内存,避免内存泄漏。
-
特性:delete用于释放单个对象,delete[]用于释放数组。
-
语法:delete 指针用于释放单个对象;delete[] 指针用于释放数组。
注意:1. 申请和释放单个元素的空间,使用new和delete操作符
申请和释放连续的空间,使用 new[]和delete[],
2.new和delete要匹配起来使用。
3.new 和 delete 的优势
new和delete不仅仅是分配和释放内存,还会自动调用构造函数和析构函数,非常适合面向对象编程中的自定义类型管理。
代码示例:new和delete****操作自定义类型
cpp
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
using namespace std;
class A {
public:
// 构造函数
A(int a = 0) : _a(a) {
cout << "A() constructor called, object address: " << this << endl;
}
// 析构函数
~A() {
cout << "~A() destructor called, object address: " << this << endl;
}
private:
int _a; // 成员变量
};
int main() {
// 使用 malloc 申请内存,但不会调用构造函数
A* p1 = (A*)malloc(sizeof(A)); // 只分配内存,未调用构造函数
A* p2 = new A(1); // 分配内存并调用构造函数
// 释放通过 malloc 申请的内存,不会调用析构函数
free(p1);
// 使用 delete 释放内存,会调用析构函数
delete p2;
// 内置类型的操作:malloc 和 new 对内置类型的行为几乎相同
int* p3 = (int*)malloc(sizeof(int)); // 只分配内存,不会初始化
int* p4 = new int; // 分配内存但未初始化
free(p3); // 释放 malloc 分配的内存
delete p4; // 释放 new 分配的内存
// 动态申请数组
A* p5 = (A*)malloc(sizeof(A) * 10); // 只分配内存,未调用构造函数
A* p6 = new A[10]; // 分配内存并调用构造函数
// 释放内存
free(p5); // 只释放内存,不调用析构函数
delete[] p6; // 释放数组并调用每个对象的析构函数
return 0;
}
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
四、operator new 与 operator delete 函数
在C++中, new和delete操作符用于动态内存管理,并且会在创建和销毁对象时自动调用构造函数和析构函数。然而 new和delete在底层是依赖于全局的 operator new 和operator delete 函数进行 实际的内存分配和释放操作。通过自定义这些函数,开发者可以控制内存的分配策略,尤其是在需要自定义内存管理(如内存池)时。
1.operator new
的实现原理
operator new
是C++的全局函数,它负责分配内存。默认情况下,它会调用 malloc
来分配指定大小的内存。如果内存分配失败,它会抛出 std::bad_alloc
异常。
代码示例:
cpp
#include <new> // 包含 bad_alloc
#include <cstdlib> // 包含 malloc
#include <iostream>
using namespace std;
// operator new: 内存分配函数
void* operator new(size_t size) _THROW1(std::bad_alloc) {
void* p;
// 使用 malloc 分配内存,如果分配失败则尝试调用处理函数
while ((p = malloc(size)) == 0) {
// 如果处理函数返回 0,抛出 bad_alloc 异常
if (_callnewh(size) == 0) {
static const std::bad_alloc nomem;
_RAISE(nomem); // 抛出内存不足异常
}
}
return p; // 如果成功分配内存,返回指向该内存的指针
}
2.operator delete
的实现原理
operator delete
是 new
的对应函数,用于释放内存。它会调用 free
来释放由 new
分配的内存。和 new
类似,delete
也可以被用户自定义,以实现特定的内存管理需求。
代码示例:
cpp
#include <cstdlib> // 包含 free
#include <iostream>
using namespace std;
// operator delete: 内存释放函数
void operator delete(void* p) noexcept {
// 如果传入的指针为空,则不执行释放操作
if (p == NULL) return;
// 通过 free 函数释放内存
free(p);
}
3.operator new[]
与 operator delete[]
与 operator new 和 operator delete 类似,C++ 中还提供了 operator new[] 和 operator delete[] 用于分配和释放数组。这些函数与单个对象的 operator new
和 operator delete
在功能上类似,只是针对的是数组。
代码示例:
cpp
// operator new[]: 分配数组所需的内存
void* operator new[](size_t size) {
cout << "Allocating array of size: " << size << endl;
return malloc(size); // 使用 malloc 分配内存
}
// operator delete[]: 释放数组的内存
void operator delete[](void* p) noexcept {
cout << "Freeing array memory" << endl;
free(p); // 使用 free 释放内存
}
五、new 和 delete 的实现原理
1. 内置类型的 new
和 delete
实现原理
对于内置类型(例如 int
、double
等),new
和 malloc
,delete
和 free
的行为非常相似。它们之间的主要区别在于:
- 单个元素与数组 :
new/delete
申请和释放单个对象,而new[]/delete[]
申请和释放数组,即连续的多个元素。 - 异常处理 :
new
在内存分配失败时会抛出std::bad_alloc
异常。malloc
在分配失败时返回NULL
,所以使用malloc
时需要手动检查返回值是否为空。
- 自动初始化 :
new
可以初始化内存。例如,new int(5)
会为新分配的int
空间初始化为 5,而malloc
只分配内存,不做初始化。
代码示例:
cpp
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
using namespace std;
int main() {
// 使用 malloc 分配内存,不会初始化
int* p1 = (int*)malloc(sizeof(int));
// 使用 new 分配内存,并初始化为 10
int* p2 = new int(10);
// 释放内存,malloc 使用 free 释放
free(p1);
// 释放内存,new 使用 delete 释放
delete p2;
return 0;
}
总结:
- 内置类型 的
new
和malloc
在申请内存的行为上相似,但new
提供了异常处理机制,而malloc
返回NULL
。- delete/free 在释放内存的行为上基本一致,都是简单的释放操作。
2. 自定义类型的 new
和 delete
实现原理
对于自定义类型(例如类对象),new
和 delete
的行为比内置类型更复杂,因为它们不仅需要分配和释放内存,还必须调用构造函数和析构函数。这是 new/delete
和 malloc/free
的核心区别。
自定义类型
new
的工作流程:
调用
operator new
分配内存 :
operator new
通过malloc
或其他内存分配函数为对象分配内存空间。调用构造函数初始化对象 :
- 在分配的内存上,
new
调用对象的构造函数,完成对象的初始化。- 这一步确保了对象的成员变量得到正确的初始化。
自定义类型delete
的工作流程:调用析构函数:
delete
在释放对象之前,首先会调用对象的析构函数,用于释放对象中的资源(例如释放对象成员变量中动态分配的内存,关闭文件等)。调用
operator delete
释放内存:
- 在调用完析构函数后,
operator delete
函数被调用,使用free
来释放该对象占用的内存空间。
代码示例:
cpp
#include <iostream>
using namespace std;
class A {
public:
// 构造函数
A(int a = 0) : _a(a) {
cout << "A() constructor called, value: " << _a << endl;
}
// 析构函数
~A() {
cout << "~A() destructor called, value: " << _a << endl;
}
private:
int _a; // 成员变量
};
int main() {
// 使用 new 分配 A 类对象,调用构造函数
A* p1 = new A(10);
// 使用 delete 释放 A 类对象,调用析构函数
delete p1;
return 0;
}
总结:
new
和delete
:不仅分配和释放内存,还负责调用构造函数和析构函数,确保自定义类型对象的正确初始化和清理。malloc/free
:对于自定义类型来说,只会分配和释放内存,不会调用构造和析构函数,因此不适用于需要自动管理对象生命周期的情况。
六、定位 new 表达式(placement-new)
placement-new
是一种特殊的new
语法,允许在指定的内存地址上构造对象。该特性在高效内存分配的场景(如内存池)中非常有用。
示例:
cpp
#include <new> // 必须包含<new>头文件
class Example {
public:
Example() { std::cout << "Example Constructor" << std::endl; }
~Example() { std::cout << "Example Destructor" << std::endl; }
};
int main() {
char buffer[sizeof(Example)]; // 分配足够大的缓冲区
Example* p = new(buffer) Example; // 在缓冲区上构造对象
p->~Example(); // 显式调用析构函数
return 0;
}
七、malloc/free 与 new/delete 的区别总结
在 C++ 中,malloc/free
和 new/delete
都可以用于从堆上申请和释放内存。它们的共同点是都用于动态内存分配,并且需要用户手动释放内存。但它们之间有一些重要的区别:
- 函数 vs 操作符
-
malloc/free
:这是 C 语言中的函数,用于分配和释放内存。 -
new/delete
:这是 C++ 中的操作符,用于分配和释放内存,并且可以调用构造函数和析构函数。
- 内存初始化
-
malloc
:只分配内存,不会对分配的内存进行初始化,内存中的数据是未定义的。 -
new
:分配内存的同时可以初始化对象,尤其是对于自定义类型时,new
会调用构造函数对对象进行初始化。
- 内存大小计算
-
malloc
:用户需要手动计算需要分配的内存大小并传递给malloc
,例如malloc(sizeof(int))
。 -
new
:用户不需要手动计算内存大小,new
会根据类型自动计算。例如new int
自动分配int
类型的空间。
- 返回类型
-
malloc
:返回void*
类型的指针,使用时必须进行强制类型转换,例如(int*)malloc(sizeof(int))
。 -
new
:返回具体类型的指针,不需要强制类型转换,例如new int
返回int*
类型的指针。
- 错误处理
-
malloc
:内存分配失败时返回NULL
,因此需要在使用时手动检查返回值是否为NULL
。 -
new
:内存分配失败时会抛出std::bad_alloc
异常,因此使用new
时需要捕获异常。
- 构造函数与析构函数
-
malloc/free
:只负责内存的分配与释放,不会调用构造函数和析构函数。因此,malloc/free
不能正确处理自定义类型的对象。 -
new/delete
:在分配内存时会调用对象的构造函数完成初始化,在释放内存时会调用对象的析构函数完成资源的清理和释放。因此,new/delete
更适合管理自定义类型的对象。
掌握内存管理是编写高效C++程序的基础。通过熟悉栈、堆和各类动态内存管理方法,可以更好地理解C++底层机制,实现高效的内存管理。