C++内存管理
目录
4.operator new和operator delete函数
(图像由AI生成)
0.前言
在前面的博客中,我们了解到C++作为一门面向对象的语言的核心特色之一——类与对象。除此之外,内存管理也是一个绝不能忽视的核心主题。良好的内存管理不仅可以提高程序的效率,还能避免许多由资源管理不当引起的问题。本博客旨在全面解析C++内存管理的各个方面,帮助读者构建坚实的理论基础,并学会实践中的关键技能。
1.C++内存布局
在C++中,程序的内存通常分为几个主要部分,各自承担着不同的功能和责任:
-
栈(Stack):
- 用于自动存储局部变量和函数调用的管理(如返回地址和局部变量)。
- 栈是有序的,先进后出的结构。当函数调用发生时,新的帧被推入栈中,函数返回时帧被弹出。
-
堆(Heap):
- 用于动态内存分配,程序中使用
new
、delete
、malloc
、free
等操作进行管理。 - 堆的管理更为复杂,涉及到内存的申请和释放,容易出现内存泄漏、碎片等问题。
- 用于动态内存分配,程序中使用
-
内存映射段(Memory Mapped Regions):
- 通常用于映射外部设备(如映射硬件设备状态)或文件(通过mmap等系统调用)。
-
数据段:
- 初始化数据段(.data):存储初始化的全局变量和静态变量。
- 未初始化数据段(.bss):用于存储未初始化的全局变量和静态变量。
-
代码段(Text Segment):
- 存储程序的机器语言指令。这部分内存通常是只读的,以防止程序代码被意外修改。
以下是一段C++代码示例,其中定义了不同类型的变量,并注释说明了它们各自的内存存储区域。
#include <iostream>
#include <string>
#include <vector>
int globalVar = 100; // 存储在初始化数据段 (.data)
static int staticGlobalVar; // 存储在未初始化数据段 (.bss)
const char* constText = "Hello World"; // 存储在代码段 (.text)
void exampleFunction() {
int localVar = 5; // 存储在栈 (stack)
static int staticVar = 5; // 存储在数据段 (.data 或 .bss,取决于是否被初始化)
int* heapVar = new int(10); // 指针存储在栈上,指向的数据存储在堆 (heap)
std::string str = "Hello"; // "Hello"存储在代码段,str对象本身存储在栈上
std::vector<int> vec = {1, 2, 3, 4}; // vec对象本身存储在栈上,其数据部分存储在堆上
}
int main() {
exampleFunction();
return 0;
}
2.C语言动态内存管理
在C语言中,动态内存管理是通过一组标准库函数实现的,主要包括 malloc
, calloc
, realloc
, 和 free
。这些函数提供了在运行时从堆区分配和释放内存的能力,使得程序能够根据需要动态调整内存使用量。以下是这些函数的主要特性和用途:
-
malloc
(Memory Allocation)- 功能:分配指定大小的内存块。
- 用法:
void* malloc(size_t size);
- 返回值:成功时返回指向分配的内存的指针,失败时返回
NULL
。 - 特点:分配的内存不会被初始化,可能含有垃圾数据。
-
calloc
(Contiguous Allocation)- 功能:分配指定数量、指定大小的内存,并自动初始化所有位为零。
- 用法:
void* calloc(size_t num, size_t size);
- 返回值:成功时返回指向分配的内存的指针,失败时返回
NULL
。 - 特点:适合于需要分配多个相同大小对象的情况,且需要内存内容清零。
-
realloc
(Re-Allocation)- 功能:重新分配先前通过
malloc
或calloc
分配的内存块的大小。 - 用法:
void* realloc(void* ptr, size_t newSize);
- 返回值:成功时返回指向新内存的指针(可能与原来的指针不同),失败时返回
NULL
且原内存块不受影响。 - 特点:当需要扩大或缩小已分配内存的大小时使用,避免了额外的复制操作和提高内存使用效率。
- 功能:重新分配先前通过
-
free
- 功能:释放之前通过
malloc
,calloc
, 或realloc
分配的内存块。 - 用法:
void free(void* ptr);
- 特点:释放的内存块之后可以被再次分配;如果传入
NULL
指针,free
函数无效果。
- 功能:释放之前通过
这一部分我们在之前的博客中已经详细介绍过。详见C语言——动态内存管理-CSDN博客。
3.C++内存管理方式
C++提供了一套相对于C更高级的动态内存管理工具,主要体现在new
和delete
操作符的使用上。这些操作符不仅封装了内存的分配和释放,还处理了对象的构造和析构,提供了更为安全和便捷的内存管理方式。
3.1new/delete操作内置类型
对于内置类型,如int
、double
、char
等,new
和delete
主要用于分配和释放内存,同时保持类型安全和初始化的控制。以下是如何使用new
和delete
以及它们的特性:
-
使用
new
分配内置类型- 语法:
TypeName* ptr = new TypeName;
- 功能:分配足够的内存以存储
TypeName
类型的一个对象,并返回指向该内存的指针。 - 初始化:对于内置类型,
new
会默认初始化内存(如果是内置的基本数据类型,如int
或double
,其值未定义,除非另行指定)。 - 例子:
int* p = new int(10);
分配一个int
类型的内存,并初始化为10。
- 语法:
-
使用
delete
释放内置类型- 语法:
delete ptr;
- 功能:释放
new
操作符分配的内存,并将指针ptr
所指向的对象所占用的内存返回给堆。 - 注意:使用
delete
释放内存后,指针变量本身并未清空或删除,它依然存在。适当的做法是将指针设置为nullptr
,避免悬挂指针。
- 语法:
下面是一段示例代码:
#include <iostream>
int main() {
// 分配单个整数
int* p = new int(10); // 分配并初始化
std::cout << "Value: " << *p << std::endl; // 输出: Value: 10
// 释放内存
delete p;
p = nullptr; // 避免悬挂指针
// 分配数组
int* array = new int[5] {1, 2, 3, 4, 5}; // 分配并初始化一个整数数组
for (int i = 0; i < 5; ++i) {
std::cout << array[i] << " "; // 输出数组内容
}
std::cout << std::endl;
// 释放数组
delete[] array;
array = nullptr; // 避免悬挂指针
return 0;
}
注意事项
- 类型安全:
new
和delete
提供了类型安全,确保分配的内存与对象类型匹配。 - 内存泄漏:必须确保每次
new
操作都对应一个delete
操作,否则会导致内存泄漏。 - 数组注意事项:分配数组时使用
new[]
,释放时使用delete[]
。不遵循这一规则可能导致资源泄露或程序崩溃。
3.2new/delete操作自定义类型
对于C++中的自定义类型,如类和结构体,使用new
和delete
不仅涉及内存的分配和释放,还包括对象的构造和析构。这使得new
和delete
在处理复杂对象时显得更为强大和灵活。
使用new
分配自定义类型
当使用new
为自定义类型分配内存时,new
操作符会先分配足够的内存来存储对象,然后在分配的内存上调用对象的构造函数来初始化对象。
- 语法:
TypeName* ptr = new TypeName(arguments);
- 例子:为自定义的类
MyClass
分配内存,并调用其构造函数。
class MyClass {
public:
int a;
MyClass(int x) : a(x) {
std::cout << "MyClass constructed with value " << a << std::endl;
}
~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
};
int main() {
MyClass* myObject = new MyClass(10); // 调用构造函数,输出构造信息
}
使用delete
释放自定义类型
对于使用new
创建的自定义类型的对象,使用delete
来释放这些对象时,delete
操作符会首先调用对象的析构函数,然后释放对象所占用的内存。
- 语法:
delete ptr;
- 行为:首先调用
ptr
指向对象的析构函数,然后释放内存。 - 例子:释放上面创建的
MyClass
对象,并触发析构函数。
int main() {
MyClass* myObject = new MyClass(10);
delete myObject; // 调用析构函数,输出析构信息
myObject = nullptr; // 避免悬挂指针
}
注意事项
- 构造和析构:
new
和delete
自动处理对象的构造和析构,这是与内置类型最大的区别。 - 异常安全:如果在构造过程中发生异常,已经分配的内存会被自动释放,防止内存泄漏。
- 资源管理:
delete
确保释放对象所持有的任何资源,例如,如果对象包含动态分配的内存或文件句柄,它们也应在析构函数中被释放。
4.operator new和operator delete函数
在C++中,operator new
和operator delete
提供了一种机制,允许程序员自定义对象的内存分配和释放过程。这两个操作符函数可以被重载,以适应特定的内存管理需求,如实现内存池、跟踪内存使用情况或增加特殊的内存分配逻辑。
基本概念
operator new
和operator delete
是全局函数,也可以为特定类进行重载。这些函数不仅仅是简单地分配和释放内存,它们在C++的内存管理中扮演了构造和析构对象之外的角色。
1. operator new
operator new
函数负责分配足够的内存以容纳特定类型的对象。它是new
表达式的一部分,当你使用new
关键字时,C++编译器会调用operator new
来分配内存,然后在分配的内存上调用构造函数来构造对象。
-
原型:
void* operator new(size_t size);
-
参数:
size
:需要分配的内存大小,通常由编译器根据对象类型自动提供。
-
返回值:
- 返回一个指向分配的内存块的指针。如果无法分配内存,标准的
operator new
会抛出一个std::bad_alloc
异常,除非使用了nothrow
版本。
- 返回一个指向分配的内存块的指针。如果无法分配内存,标准的
-
例子:重载一个类的
operator new
class MyClass {
public:
static void* operator new(size_t size) {
std::cout << "Custom new operator for MyClass. Size: " << size << std::endl;
return ::operator new(size);
}
};
2. operator delete
operator delete
负责释放由operator new
分配的内存,并在对象生命周期结束后由delete
表达式调用。在对象的析构函数执行后,operator delete
被调用来处理内存的释放。
-
原型:
void operator delete(void* pointer);
-
参数:
pointer
:指向要释放内存的指针。
-
功能:
- 释放之前通过
operator new
分配的内存。如果指针是nullptr
,则operator delete
不执行任何操作。
- 释放之前通过
-
例子:重载一个类的
operator delete
class MyClass {
public:
static void operator delete(void* pointer) {
std::cout << "Custom delete operator for MyClass" << std::endl;
::operator delete(pointer);
}
};
注意事项
- 匹配使用:为类重载
operator new
时,也应该重载operator delete
。这样可以确保内存分配和释放的一致性。 - 异常安全:如果在构造对象后但在返回指针之前发生异常,应当在
operator new
内部正确处理,以避免内存泄漏。
5.new和delete的实现原理
在C++中,new
和delete
不仅是关键字,它们背后还有复杂的实现机制,负责管理内存的分配和释放。理解这些实现细节可以帮助开发者写出更高效和更稳定的程序。
5.1对内置类型
对于内置类型(如int
、double
、char
等),new
和delete
的实现比较直接,主要涉及内存分配和释放,不涉及复杂的对象构造和析构。
new
的实现原理
-
内存分配:
new
首先调用operator new
函数来分配内存。这是一个全局函数,其标准实现通常调用底层的内存管理API(如C的malloc
函数)来获取足够的内存。 -
初始化:对于内置类型,标准的
new
通常不进行任何初始化,除非显式提供了初始化值(例如new int(42)
)。如果没有提供初始化值,则分配的内存中的内容是未定义的。
int* p = new int; // 内存未初始化
int* q = new int(10); // 内存初始化为10
delete
的实现原理
-
内存释放:
delete
关键字对应地调用operator delete
函数来释放内存。这个函数通常会调用底层的内存释放API(如C的free
函数),将之前由operator new
分配的内存返回给系统。 -
资源管理:对于内置类型,由于没有复杂的构造和析构过程,
delete
的操作相对简单。主要任务是确保内存被正确释放,避免内存泄漏。
delete p; // 释放p指向的内存
p = nullptr; // 避免产生悬挂指针
示例代码
这里是一个简单的例子,演示了如何使用new
和delete
操作内置类型的内存:
#include <iostream>
int main() {
// 使用new分配内存
int* p = new int(10); // 分配并初始化为10
std::cout << "Value: " << *p << std::endl; // 输出: Value: 10
// 使用delete释放内存
delete p;
p = nullptr; // 推荐做法,避免悬挂指针
return 0;
}
在这个示例中,new
分配了一个整数的内存并进行了初始化,而delete
则释放了这块内存并清空了指针。这种简单的用法隐藏了new
和delete
背后的复杂实现,但对于内置类型来说,这种直接的内存操作是足够的。这样的实现确保了内存管理的基本安全性和效率。
5.2对自定义类型
对于自定义类型,如类或结构体,new
和delete
不仅涉及内存分配和释放,还包括对象的构造和析构。这增加了实现的复杂性,但同时提供了更多控制和灵活性,使得内存管理能够与对象的生命周期紧密结合。
new
的实现原理
当new
用于自定义类型时,其工作流程大致如下:
-
内存分配:与内置类型类似,
new
首先调用operator new
函数来分配足够的内存。这通常通过调用底层的内存管理系统(如malloc
)来完成。 -
对象构造:在分配好的内存上,
new
接着调用对象的构造函数来初始化对象。这个步骤是自定义类型和内置类型操作的主要区别。构造函数的调用确保了对象的数据成员根据定义进行初始化,并执行任何其他必要的设置。
例如,给定一个类Person
,使用new
创建其对象的过程会涉及到以下操作:
class Person {
public:
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Person created: " << name << ", " << age << std::endl;
}
~Person() {
std::cout << "Person destroyed: " << name << std::endl;
}
};
int main() {
Person* p = new Person("John", 30); // 分配内存并构造对象
}
在这个例子中,new
不仅分配了内存,还调用了Person
的构造函数来初始化对象的name
和age
字段。
delete
的实现原理
使用delete
释放自定义类型对象时,操作包括:
-
对象析构:
delete
首先调用对象的析构函数。析构函数负责进行清理工作,如释放对象可能持有的资源(比如动态分配的内存、文件句柄等)。 -
内存释放:析构函数执行完毕后,
delete
调用operator delete
函数释放对象占用的内存。这通常涉及到调用底层的内存释放函数(如free
)。
在前面的Person
类示例中,删除p
指向的对象的过程会调用Person
的析构函数,并输出析构信息,然后释放内存:
int main() {
Person* p = new Person("John", 30);
delete p; // 调用析构函数,然后释放内存
}
注意事项
- 异常安全:在构造过程中如果发生异常,已分配的内存需要被正确释放以防内存泄漏。C++的异常处理机制确保了在构造函数抛出异常时,分配给对象的内存会被自动释放。
- 配对使用:每个通过
new
分配的对象都必须通过相应的delete
来释放,确保每个对象的生命周期得到正确管理。 - 复杂对象:对于包含其他资源的对象,如指向其他动态分配内存的指针,必须在析构函数中正确处理这些资源的释放。
6.定位new表达式(placement-new)
定位new表达式,通常称为“placement new”,是C++中一种特殊的内存管理技术,它允许在已经分配的内存上构造对象。这种方式主要用于需要对内存使用进行精细控制的场合,如在特定的内存位置创建对象,或重用已经分配的内存以避免额外的内存分配开销。
6.1基本概念
定位new的基本语法和普通的new
类似,但它需要一个额外的参数——一个指向已经分配的内存的指针。这允许定位new在这块预先存在的内存上直接构造对象,而不是像普通的new
那样首先分配内存。
6.2使用场景
- 内存池:定位new可以在预先分配的内存池中构造对象,提高内存分配效率和减少碎片化。
- 缓冲区重用:在同一块内存区域多次构造、析构不同类型的对象,避免反复的内存分配和释放。
- 硬件指定位置:在特定物理内存地址构造对象,常用于嵌入式系统或操作系统的底层编程。
6.3语法
#include <new> // 必须包含头文件 <new>
void* place = ::operator new(sizeof(YourClass)); // 分配内存
YourClass* p = new(place) YourClass(arguments); // 在place指向的地址构造对象
这里,place
是一个指针,指向已分配的内存,YourClass(arguments)
是构造函数的参数。
示例代码
以下示例演示了如何使用定位new:
#include <iostream>
#include <new> // For std::nothrow and placement new
class Widget {
public:
Widget() { std::cout << "Widget constructed\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
};
int main() {
// 分配足够的内存
void* buffer = ::operator new(sizeof(Widget));
try {
// 在buffer指定的内存上构造Widget对象
Widget* w = new(buffer) Widget;
// 使用对象...
// 手动调用析构函数
w->~Widget();
} catch (...) {
::operator delete(buffer);
throw; // 重新抛出异常
}
// 释放内存
::operator delete(buffer);
return 0;
}
在这个例子中,Widget
类的对象被构造在通过::operator new
手动分配的内存上。使用完毕后,需要手动调用析构函数来销毁对象,并且手动释放内存。
6.4注意事项
- 内存管理:使用定位new时,必须自行管理内存的分配和释放,这增加了错误的风险。
- 析构函数:定位new不会自动调用析构函数,因此需要手动调用析构函数来正确管理对象的生命周期。
- 异常处理:构造函数中如果发生异常,需要适当处理已分配的内存,避免泄漏。
7.常见概念辨析
7.1malloc/free和new/delete的区别
在C++程序设计中,malloc
/free
和new
/delete
是两组用于内存管理的函数和操作符。虽然它们的基本目的相同——分配和释放内存——但它们之间存在几个重要的区别,这些区别影响了它们在实际编程中的使用和功能。
-
构造和析构:
new
/delete
:不仅负责内存的分配和释放,还自动调用对象的构造函数和析构函数。这确保了对象的成员被正确初始化和清理。malloc
/free
:仅仅分配和释放内存,不调用任何构造或析构函数。使用malloc
分配内存后,内存中的内容是未初始化的。
-
类型安全:
new
/delete
:是类型安全的,返回的是类型指定的指针,不需要类型转换。malloc
/free
:malloc
返回一个void*
类型的指针,需要显式转换为需要的类型。
-
异常处理:
new
:默认情况下,如果内存分配失败,new
会抛出std::bad_alloc
异常。malloc
:分配内存失败时返回NULL
,不抛出异常。这要求程序员必须检查返回值以确定内存分配是否成功。
-
内存分配方式:
new
:可以重载new
操作符来改变对象的分配行为。malloc
:是一个库函数,不能被重载,行为不变。
-
便利性和功能:
new
/delete
:可以更加方便地用于构造和析构类的对象,支持重载以提供自定义行为。malloc
/free
:更适合用于分配一块原始内存区域,如分配一个大的字节数组。
示例代码
下面的代码清晰地展示了new
/delete
和malloc
/free
的使用差异:
#include <iostream>
class Widget {
public:
Widget() { std::cout << "Widget constructed\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
};
int main() {
// 使用new/delete
Widget* w = new Widget;
delete w;
// 使用malloc/free
Widget* w2 = (Widget*)malloc(sizeof(Widget)); // 只分配内存,不调用构造函数
w2->Widget::Widget(); // 需要显式调用构造函数
w2->~Widget(); // 需要显式调用析构函数
free(w2);
}
在这个示例中,使用new
和delete
自动调用了Widget
类的构造函数和析构函数。而使用malloc
和free
时,必须显式调用构造函数和析构函数,这增加了代码的复杂度并且容易出错。
7.2内存泄漏
内存泄漏是程序设计中常见的一种问题,特别是在使用手动内存管理的语言如C和C++中。内存泄漏指的是程序未能释放不再使用的内存,导致有效内存逐渐减少,最终可能影响程序性能或导致程序崩溃。
7.2.1内存泄漏的含义与危害
含义:
- 内存泄漏发生在已分配的内存不再被任何部分的程序代码引用,但由于某些原因未被释放或无法释放,从而造成系统内存的浪费。
危害:
- 性能下降:内存泄漏会逐渐消耗系统的内存资源,导致可用内存减少,影响系统性能。
- 系统稳定性:长时间的内存泄漏可能导致内存不足,引发程序或系统崩溃。
- 资源浪费:内存资源被无效占用,不能被其他程序或同一程序的其他部分利用。
7.2.2内存泄漏的分类
1. 显式泄漏:
- 发生在明确的动态内存分配和释放之间。例如,使用
new
或malloc
分配的内存没有相应的delete
或free
来释放。
2. 隐式泄漏:
- 指内存仍被程序的活动部分所引用,但实际上已无用处,无法被再次利用。这通常涉及到数据结构错误或逻辑错误。
3. 资源泄漏:
- 涉及到非内存资源如文件句柄、数据库连接等,这些资源未被适时释放,虽不直接消耗物理内存,但也会导致资源耗尽。
7.2.3如何检测内存泄漏
1. 代码审查:
- 人工检查代码,寻找没有配对的
new
/delete
或malloc
/free
。
2. 运行时工具:
- 使用专门的工具如 Valgrind,Dr. Memory,Visual Leak Detector 等来监测运行时的内存分配和释放。
- 开发环境和编译器内置工具,如Visual Studio的内存检测工具。
3. 日志和监控:
- 在代码中加入日志记录内存分配和释放的活动,帮助追踪可能的泄漏点。
7.2.4如何避免内存泄漏
1. 使用智能指针:
- 在C++中使用
std::unique_ptr
,std::shared_ptr
等智能指针自动管理内存。智能指针在销毁时自动释放其指向的内存。我们将在后续的博客中对智能指针做详细介绍。
2. 资源获取即初始化 (RAII):
- 将资源的生命周期绑定到对象的生命周期,确保通过对象的构造和析构自动管理资源。
3. 定期代码审查和重构:
- 定期进行代码审查,消除潜在的内存泄漏风险点。使用更安全的编程实践和模式。
4. 测试和验证:
- 实施严格的测试流程,包括单元测试、集成测试和系统测试,使用专门的工具定期检查内存泄漏。
8.结语
在本博客中,我们全面探讨了C++中的内存管理机制,从基本的内存布局到高级的内存操作技术,包括new
和delete
的使用、内存泄漏的问题及其防治措施。通过深入了解这些概念,开发者可以更有效地控制程序的资源使用,提高程序的性能和稳定性。希望本文的内容能帮助你避免常见的内存管理错误,编写出更加健壮和高效的代码。
原文地址:https://blog.csdn.net/wxk2227814847/article/details/137970639
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!