C++资源管理是确保程序运行效率和稳定性的关键。资源管理涉及变量、参数的存储和生命周期控制,以及动态内存的分配和释放。C++通过一套内存管理机制来实现资源的有效分配和管理。
为适用不同场景,C++提供了多种内存管理方式,以适用不同的使用场景。
栈:自动分配和释放,存储静态局部变量、函数参数、返回值等,栈向下增长;
堆:手动分配和释放,用于程序运行时动态内存分配,堆向上增长;
数据段:用于存储全局数据和静态数据,即全局变量或static修饰的变量;
代码段:可执行的代码(以二进制形式存储)和只读常量;
内存映射段:是高效的I/O隐射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享内存,做进程间通信。
由栈管理的内存可进行自动内存管理,由编译器自动处理。如局部变量和函数参数等,当函数调用时被创建,其生命周期与函数执行时间相同,当函数返回时,这些变量自动销毁,内存自动释放。
由堆管理的内存需进行手动分配,由程序员进行控制。在C++中通过new和delete操作符进行动态内存管理。
new/delete操作内置类型
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。对用内置类型而言,用malloc和new,除了用法不同,没有什么区别。
new/delete操作自定义类型
申请空间时,malloc只开空间,new既开空间又调用构造函数初始化(先调用operator new开空间,再调用构造进行初始化);释放空间时,delete会调用析构函数(先调用析构函数,再调用operator delete),free不会
内存泄漏: 内存泄漏发生在分配了内存但未正确释放的情况下。
悬空指针: 野指针是指向已经释放或无效内存的指针,使用野指针可能导致程序奔溃或未定义行为。
内存碎片: 在堆上频繁的使用new和delete分配和释放内存可能会导致内存碎片,影响程序性能,使用内存池或大块内存分配策略可以减少碎片。
RALL是为高效管理内存资源提出的一种编程范式,利用对象的生命周期管理资源的获取和释放。RALL(Resource Acquisition is Initialization)资源获取即初始化。核心思想是将资源的获取与对象的初始化绑定,将资源的释放与对象的销毁绑定。这样做的目的是确保在任何情况下,一旦资源被成功获取,它最终都会被正确释放,从而避免资源泄漏和重复释放的问题。
在C++中,对象的生命周期是明确的:对象在创建时构造函数被调用,在对象生命周期结束时析构函数被调用。RAII利用这一特性,通过在构造函数中获取资源,在析构函数中释放资源,来管理资源的生命周期。其基本工作流程是:创建对象时,先开辟空间,再调用构造进行初始化,对象销毁时,先调用析构函数,再释放空间。
在浅拷贝中,只复制对象中的值,而不复制指向动态分配内存的指针。这意味着两个对象共享相同的内存资源。如果其中一个对象修改了共享的资源,另一个对象也会受到影响。
#include
class ShallowCopyExample {
public:
int *data;
ShallowCopyExample(const ShallowCopyExample &other) {
// 浅拷贝
data = other.data;
}
};
int main() {
ShallowCopyExample obj1;
obj1.data = new int(42);
ShallowCopyExample obj2 = obj1; // 浅拷贝
// 修改obj2的data,将影响obj1
*(obj2.data) = 84;
std::cout << *(obj1.data) << std::endl; // 输出 84
std::cout << *(obj2.data) << std::endl; // 输出 84
delete obj1.data;
return 0;
}
在深拷贝中,不仅复制对象的值,还复制指向动态分配内存的指针所指向的实际数据。这样,两个对象将拥有彼此独立的内存副本,修改一个对象不会影响另一个对象。
#include
class DeepCopyExample {
public:
int *data;
DeepCopyExample(const DeepCopyExample &other) {
// 深拷贝
data = new int(*(other.data));
}
~DeepCopyExample() {
delete data;
}
};
int main() {
DeepCopyExample obj1;
obj1.data = new int(42);
DeepCopyExample obj2 = obj1; // 深拷贝
// 修改obj2的data,不会影响obj1
*(obj2.data) = 84;
std::cout << *(obj1.data) << std::endl; // 输出 42
std::cout << *(obj2.data) << std::endl; // 输出 84
delete obj1.data;
delete obj2.data;
return 0;
}
#include
class MyClass {
public:
// 构造函数
MyClass(int value) {
data = new int(value);
std::cout << "Constructor" << std::endl;
}
// 复制构造函数
MyClass(const MyClass& other) {
// 深拷贝资源
data = new int(*(other.data));
std::cout << "Copy Constructor" << std::endl;
}
// 赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this != &other) {
// 释放已有资源
delete data;
// 深拷贝资源
data = new int(*(other.data));
}
std::cout << "Copy Assignment Operator" << std::endl;
return *this;
}
// 析构函数
~MyClass() {
// 释放资源
delete data;
std::cout << "Destructor" << std::endl;
}
// 显示数据
void displayData() const {
std::cout << "Data: " << *data << std::endl;
}
private:
int* data; // 示例成员,假设是一个动态分配的整数
};
int main() {
// 创建对象1
MyClass obj1(42);
obj1.displayData();
// 使用复制构造函数创建对象2
MyClass obj2 = obj1;
obj2.displayData();
// 使用赋值运算符创建对象3
MyClass obj3(100);
obj3 = obj1;
obj3.displayData();
return 0;
}
在C++11及以上的标准中,为了解决大数据对象拷贝时资源消耗的问题,引入了移动语义,通过转移资源所有权的方式,完成新对象的创建,这种情况下不在需要堆对象进行深拷贝。而移动语义主要是基于右值引用来实现的。
右值引用:
我们通常说的变量、解引用的指针,这种可以取地址,可以赋值,可以在赋值号左边或者右边的值称为左值,而右值,通常是字面常量,函数返回值、表达式的返回值,是一个即将被销毁的临时变量,不能进行取地址操作,且只能出现在赋值号右侧。
//左值
int* p=new int(0);
int b=1;
int a=b;
const int c=3;
//右值
int&& ri=10; //10不能出现在赋值号左侧
int&& ri1=x+y; //同理x+y也不行
int&& ri2=fun(x,y);
左值引用和右值引用就是分别给左值和右值取别名,如上述ri、ri1、ri2。左值和右值在引用的时候有如下特点:
int a=10;
int& ra1=a;//左值引用
//int& ra2=10; //10是右值,不能给左值引用
const int& ra3=10;//const左值引用右值
const int& ra4=a;//const左值引用左值
int&& ri1=10;//右值引用右值
int&& ri2=std::move(a);//右值引用move的左值
int&& ri3=20;
ri3++;
const int&& ri4=30;
//ri4++;//不可修改
说清楚右值和右值引用后,再来讨论右值引用和移动构造的关系。
对于函数需要返回操作结果的问题,通常有两种处理方式:设置返回值和通过左值引用设置输出型参数。
设置返回值的情况:返回值出了函数作用域就会被销毁,因此,需先有个临时变量去接受函数的返回值,然后再将返回值赋值给主函数的接受对象,这个过程涉及多次拷贝,销毁较大,效率较低。
设置输出型参数的情况:通过左值引用设置输出型参数的方式可以避免拷贝的问题,但是会导致函数参数过多,函数定义臃肿。(目前只知道这些,知道的可以补充)。
针对上述情况,右值引用就发挥了其价值,右值引用可以直接接受函数返回值,通过移动对象所有权的方式,避免中间的一系列拷贝操作,大大提高新能。对于自定义的类对象,通过定义拷贝构造函数来实现右值引用,实现性能优化的效果。
基于上述内容,又有了两个新的规则,将“三法则”扩展为“五法则”:
移动构造函数:移动构造用于转移对象资源的所有权,而不是复制,可以避免不必要的资源复制,定义一个移动构造函数,以支持资源的高效转移。
移动赋值运算符重载:定义一个移动赋值运算符,以支持资源的高效转移。
#include
class MyClass {
public:
// 构造函数
MyClass(int value) : data(new int(value)) {
std::cout << "Constructor" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 确保源对象处于有效但未指定的状态
std::cout << "Move Constructor" << std::endl;
}
// 移动赋值运算符重载
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data; // 释放已有资源
data = other.data;
other.data = nullptr; // 确保源对象处于有效但未指定的状态
}
std::cout << "Move Assignment Operator" << std::endl;
return *this;
}
// 析构函数
~MyClass() {
delete data;
std::cout << "Destructor" << std::endl;
}
// 显示数据
void displayData() const {
std::cout << "Data: " << (data ? *data : 0) << std::endl;
}
private:
int* data; // 示例成员,假设是一个动态分配的整数
};
int main() {
// 创建对象1
MyClass obj1(42);
obj1.displayData();
// 移动构造函数创建对象2
MyClass obj2 = std::move(obj1);
obj2.displayData();
// 移动赋值运算符创建对象3
MyClass obj3(100);
obj3 = std::move(obj2);
obj3.displayData();
return 0;
}
这五法则确保了对于拥有资源管理责任的类,它们能够正确地进行资源的获取、释放和转移,从而实现了更安全和更高效的编程。RAII的使用通常与智能指针、容器等C++标准库组件一起,以提供更好的资源管理。
[注:]noexcept
是一个C++11引入的关键字,用于指示一个函数是否可能抛出异常。noexcept
并不是完全禁止函数抛出异常,而是告诉编译器在异常发生时如何处理。如果函数确实抛出异常,而 noexcept
有一个 false
的参数,编译器会调用 std::terminate
来终止程序。
资源管理: 利用对象的生命周期自动管理资源,减少了手动管理资源的复杂性;
异常安全: 即使在代码执行过程中发生异常,由于析构函数总是会被调用,资源也能被正确释放。
RALL的典型应用是智能指针和内存池,用于安全的进行堆上内存的分配和释放。
智能指针是C++标准库提供的一种模板类,是RALL原则的具体实现,用于自动管理动态分配的内存,以防止内存泄漏和其他与动态内存分配相关的问题。C++标准库提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr。其中后三种是C++11支持的,并且第一个已经在C++11弃用。
auto_ptr采用管理权转移的方法进行赋值和拷贝构造,假设原先有一个auto_ptr对象p1,要通过p1构造p2,当拷贝构造完成后,用于拷贝构造传参的对象p1中管理资源的指针会被更改为nullptr,赋值也一样,假设p2=p1,p1中资源的管理权会转移给p2,p2原本的资源会被释放。转移管理权的方式容易出现悬空指针的问题,auto_ptr在C++11中已经被摒弃,很多公司命令禁止使用。
独占对象所有权,直接将拷贝构造和赋值禁止,不存在浅拷贝多次释放同一块空间的问题。相比于shared_ptr,由于没有引用计数,性能较好。虽然unique_ptr禁止拷贝和赋值,但可以利用move通过返回值的方式实现拷贝。
unique_ptr<T> ptr_a(new T);
unique_ptr<T> ptr_b=std::move(ptr_a);
共享对象的所有权,通过引用计数的方式较好的解决了拷贝和赋值的问题,相对于unique_ptr,性能较差。share_ptr指向同一个对象,当进行拷贝和赋值时,通过应用计数来实现,当最后一个shared_ptr释放时,资源才会释放。
//循环引用
struct ListNode
{
shared_ptr<ListNode> _pre;
shared_ptr<ListNode> _next;
};
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next=node2;
node2->_pre=node1;
cout<<"node1引用计数:"<<node1.use_count()<<endl;
cout<<"node2引用计数:"<<node2.use_count()<<endl;
}
不增加引用计数,为解决shared_ptr环形引用的问题而提出,weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,在weak_ptr中,也没有operator*函数和operator->成员函数,不具有一般指针的行为,因此,weak_ptr严格意义上并不是智能指针,weak_ptr的出现,就是为了解决shared_ptr的循环引用问题。
//weak_ptr不管资源的释放
struct ListNode
{
weak_ptr<ListNode> _pre;
weak_ptr<ListNode> _next;
};
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next=node2;
node2->_pre=node1;
cout<<"node1引用计数:"<<node1.use_count()<<endl;
cout<<"node2引用计数:"<<node2.use_count()<<endl;
}
内存池(Memory Pool)是一种用于优化动态内存分配和释放操作的技术。它的基本原理是在程序启动时或在明确的时机预先分配一大块内存,然后将这块内存分割成多个固定大小的小块,存储在自由列表(free list)中,当程序需要分配内存时,直接从自由列表中获取一个内存块,这样可以显著减少对操作系统内存分配器的调用次数,从而提高内存分配的效率。
内存池的工作原理可以分为以下几个步骤:
#include
#include
#include
class MemoryPool {
public:
MemoryPool(size_t blockSize, size_t blockCount)
: blockSize(blockSize), blockCount(blockCount), freeBlocks(blockCount) {
// 分配内存池
pool = ::operator new(blockSize * blockCount);
char* current = static_cast<char*>(pool);
// 初始化空闲块列表
for (size_t i = 0; i < blockCount; ++i) {
freeBlocks[i] = current + i * blockSize;
}
}
~MemoryPool() {
// 释放内存池
::operator delete(pool);
}
void* allocate() {
if (freeBlocks.empty()) {
return nullptr; // 没有可用的块
}
// 从空闲块列表中取出一个块
char* block = freeBlocks.back();
freeBlocks.pop_back();
return block;
}
void deallocate(void* block) {
assert(block != nullptr); // 确保要释放的块不是nullptr
// 添加到空闲块列表
freeBlocks.push_back(static_cast<char*>(block));
}
size_t getBlockSize() const {
return blockSize;
}
size_t getBlockCount() const {
return blockCount;
}
size_t getFreeBlockCount() const {
return freeBlocks.size();
}
private:
size_t blockSize; // 每个内存块的大小
size_t blockCount; // 内存池中的块数量
void* pool; // 内存池的起始地址
std::vector<char*> freeBlocks; // 存储空闲块的列表
};
// 示例使用
int main() {
const size_t BLOCK_SIZE = 32; // 每个块32字节
const size_t BLOCK_COUNT = 10; // 总共10个块
MemoryPool pool(BLOCK_SIZE, BLOCK_COUNT); // 创建内存池
// 分配内存块
void* block1 = pool.allocate();
void* block2 = pool.allocate();
std::cout << "Allocated blocks: " << block1 << ", " << block2 << std::endl;
std::cout << "Free blocks: " << pool.getFreeBlockCount() << std::endl;
// 释放内存块
pool.deallocate(block1);
pool.deallocate(block2);
std::cout << "Free blocks after deallocation: " << pool.getFreeBlockCount() << std::endl;
return 0;
}
参考资料
1.C / C++ 内存管理_c和c++的动态管理内存方法-CSDN博客
2.【C进阶】动态内存管理(2)_(char *)malloc(100)-CSDN博客
3.【C++】右值引用(极详细版)-CSDN博客
4.C++:智能指针_c++智能指针-CSDN博客
5.C++ — 智能指针 - 流水灯 - 博客园 (cnblogs.com)
6.【c++复习笔记】——智能指针详细解析(智能指针的使用,原理分析)-CSDN博客
7.c++深拷贝和浅拷贝-CSDN博客