今天我们聊聊在C++世界中一个不可避免的问题----内存管理。
想想那些年,你因为忘记delete而漏掉的内存,它们是不是在夜深人静时化作幽灵,在你的程序里飘荡,随着程序的持续运行,被泄漏的内存不断堆积,最终可能导致系统内存枯竭,程序无奈崩溃。所以如何避免资源的泄漏是我们必须面对的挑战。
原生指针的问题
在C++中,资源的申请与释放通常是手动管理的。比如new分配内存,delete释放内存。但因为一些疏忽或者意外可能会出现各种问题。
内存泄漏
- 你和你的心爱之人表白,他/她同意了。(你new了一个对象)
- 你去买水,你对象说在沙滩上等你。(对象在堆上)
- 但你买完水直接回家了。(忘记delete)
- 你把你的对象忘在了沙滩上。(未释放动态分配的内存,导致内存泄漏)
悬空指针
你把自己家的地址告诉了朋友,然后有天你想不开把自己家给拆了(delete),别人顺着你给的地址欢天喜地的来到你家,一进门...噗通一下掉坑里了。
简单的代码示例:
int main()
{
int* ptr1 = new int(5);
int* ptr2 = ptr1;
delete ptr1;
ptr1 = nullptr;
std::cout << *ptr2 << std::endl;
return 0;
}
上述代码中ptr1和ptr2指向同一块地址,然后这块地址被释放了,但ptr2还指向这块地址,导致指针指向已释放或未初始化的内存。
双重释放
int main()
{
int* ptr1 = new int(5);
int* ptr2 = ptr1; //p1和p2指向同一块内存
delete ptr1; //第一次释放
delete ptr2; //第二次释放,但p2指向的内存早已被释放
return 0;
}
多个原始指针共享同一块动态内存时,若其中一个执行了delete,其他指针仍指向原地址,若再对它们执行delete则导致双重释放。
智能指针
为了解决这些问题,C++11引入了智能指针。智能指针利用RAII(Resource Acquisition Is Initialization)机制,将内存的管理和对象的生命周期绑定在一起。智能指针在构造时获取内存,在析构时自动释放,从而避免手动调用delete。
C++中的智能指针均定义在头文件中:
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
std::unique_ptr
unique_ptr是C++11引入的智能指针,它是一种独占所有权的智能指针,保证同一时间内只能有一个 unique_ptr 实例拥有对某个对象的所有权。不能被拷贝,只能被移动,这是为了确保资源的独占性。
int main()
{
std::unique_ptr<int> u_ptr1(new int(5)); // 指向int类型的unique指针
std::unique_ptr<double> u_ptr2 = std::make_unique<double>(9.21); // 指向double类型的unique指针
// std::unique_ptr<int> u_ptr3 = u_ptr1; // 不支持拷贝
std::unique_ptr<int> u_ptr4 = std::move(u_ptr1); // 支持移动
return 0;
}
在这段代码中我们直接使用new来初始化u1,在复杂表达式中(例如函数参数)可能导致内存泄漏,使用make_unique初始化u2可以确保如果发生异常,资源会被正确清理,不会泄漏。
我们尝试将u1赋值给u3而导致编译器报错,但我们可以通过std::move将u1的所有权交给u4,u1变为空指针。
除了使用std::move移交所有权,我们还可以使用release或reset将指针的所有权移交给另一个unique指针。
int main()
{
std::unique_ptr<int> u_ptr1(new int(5)); // 指向int类型的unique指针
std::unique_ptr<int> u_ptr2(u_ptr1.release()); // release返回u1的内置指针,并将u1置空
std::unique_ptr<int> u_ptr3(new int(6));
u_ptr3.reset(u_ptr2.release()); // reset放弃原来的内置指针,指向u2的内置指针
return 0;
}
unique_ptr有一个成员方法release,能够返回其内置指针,并将其置空。上述代码中我们将u1的内置指针移交给了u2;同样的,我们先放弃u3自己的内置指针,然后指向u2的内置指针。
unique_ptr不能被拷贝,但是有一个例外情况:可以作为函数返回值,将资源所有权移交给调用者。
std::unique_ptr<int> copy_unique(int val)
{
return std::unique_ptr<int>(new int(val));
}
int main()
{
int value = 10;
std::unique_ptr<int> u_ptr1 = copy_unique(value);
std::cout << *u_ptr1 << std::endl;
return 0;
}
在上述代码中,copy_unique函数返回std::unique_ptr,将资源所有权移交给u1。
std::shared_ptr
shared_ptr是一种共享所有权的智能指针,允许多个shared_ptr实例共享对同一个对象的所有权。通过引用计数机制来管理资源的生命周期。
当一个shared_ptr指向对象时,引用计数加1;当shared_ptr离开作用域或被重新赋值时,引用计数减1,当引用计数为0时,对象会被自动释放。
int main()
{
std::shared_ptr<int> s_ptr1(new int(5));
std::shared_ptr<int> s_ptr2 = s_ptr1; // 将s1赋值给s2,引用计数+1
// 打印引用计数
std::cout << "s1 use_count : " << s_ptr1.use_count() << std::endl;
std::cout << "s2 use_count : " << s_ptr2.use_count() << std::endl;
s_ptr2 = std::shared_ptr<int>(new int(6)); // s2引用计数为1
std::cout << "s2 use_count : " << s_ptr2.use_count() << std::endl;
return 0;
}
程序输出:
s1 use_count : 2
s2 use_count : 2
s2 use_count : 1
在这段代码中s1和s2共享一个对象,引用计数为2,当s2指向新的对象后,引用计数减1。
上述代码都是通过new生成的内置指针初始化生成shared_ptr,还可以使用make_shared直接构造shared_ptr对象,性能更高,异常安全性更强。
struct Data
{
int m_age;
std::string m_name;
Data(int age, std::string name) :m_age(age), m_name(name) {}
~Data() { std::cout << "~Data()" << std::endl; }
};
int main()
{
std::shared_ptr<Data> s_ptr1 = std::make_shared<Data>(11, "xingxing");
std::cout << s_ptr1->m_name << std::endl;
return 0;
}
程序输出:
xingxing
~Data()
使用make_shared能够避免两次内存分配,构造时更安全。
std::weak_ptr
std::weak_ptr是一种不拥有对象所有权的智能指针,用于观察但不影响对象的生命周期。主要用于解决shared_ptr之间的循环引用问题。
我们将一个weak_ptr绑定到一个shared_ptr上时,不会改变shared_ptr的引用计数,weak_ptr就相当于一个观察者;并且当weak_ptr指定到一个对象时,那个对象依然会被释放掉,所以它具有弱共享对象的特点。
int main()
{
auto s_ptr1 = std::make_shared<int>(5);// 创建一个shared_ptr
std::weak_ptr<int> w_ptr(s_ptr1);// 用s1构造一个weak_ptr
std::cout << "s1 use_cout is: " << s_ptr1.use_count() << std::endl;
return 0;
}
程序输出:
s1 use_cout is: 1
上述代码中因为w不会改变s1的引用计数,所以引用计数为1。
weak_ptr不仅提供了reset(),use_count()等方法,还提供了expired()方法。该方法在use_count等于0时返回true,否则返回false。所以我们可以使用expired()方法来判断weak_ptr的内置指针是否被释放:
std::weak_ptr<int> create_weakptr(int num)
{
std::shared_ptr<int> s_ptr = std::make_shared<int>(num);
return std::weak_ptr<int>(s_ptr);
}
int main()
{
std::weak_ptr<int> w_ptr = create_weakptr(5);
if (w_ptr.expired())
std::cout << "The w_ptr constructed by s_ptr has been deleted." << std::endl;
else
std::cout << "s_ptr has not been deleted." << std::endl;
return 0;
}
最后的输出结果为“The w_ptr constructed by s_ptr has been deleted.”。因为create_weakptr函数返回的是一个局部变量,随着create_weakptr函数的结束而被释放,所以expired()为true。
我们使用weak_ptr时,因为其本身不控制对象的生命周期,可以通过lock()方法返回一个shared_ptr,这样就能安全的访问对象了。如果对象已经被释放,lock方法会返回一个空的shared_ptr。
int main()
{
std::weak_ptr<int> w_ptr;
{
auto s_ptr1 = std::make_shared<int>(42);
w_ptr = s_ptr1;
if (auto s_ptr2 = w_ptr.lock())// 对象存在,返回有效的 shared_ptr
{
std::cout << "s_ptr2 value: " << *s_ptr2 << std::endl;
std::cout << "s_ptr2 use_count is: " << s_ptr2.use_count() << std::endl;
}
else
{
std::cout << "object has been deleted.\n";
}
} // s1 离开作用域,对象被释放
std::cout << "s1 goes out of scope." << std::endl;
if (auto s_ptr2 = w_ptr.lock()) // 对象已释放,返回空shared_ptr
{
std::cout << "s_ptr2 value: " << *s_ptr2 << std::endl;
}
else
{
std::cout << "object has been deleted." << std::endl;
}
return 0;
}
程序输出:
s_ptr2 value: 42
s_ptr2 use_count is: 2
s1 goes out of scope.
object has been deleted.
在这段代码中,w_ptr是指向s1所管理对象的弱引用,我们通过w_ptr.lock()可以将其提升为shared_ptr,在s1未被释放时,lock函数返回的shared_ptr可以正常访问对象;当s1被释放后,lock函数返回的shared_ptr为空。
循环引用问题
shared_ptr通过引用计数来管理对象的生命周期:每当一个新的shared_ptr指向对象时,引用计数加1;每当一个shared_ptr被销毁或重置时,引用计数减1;当引用计数变为0时,对象被自动删除。
循环引用发生的情况是:对象 A 持有一个指向对象 B 的 shared_ptr,同时对象 B 也持有一个指向对象 A 的 shared_ptr(或者通过更多对象形成环状结构)。此时:
- A 的引用计数因为 B 持有它而至少为1。
- B 的引用计数因为 A 持有它而至少为1。
- 即使所有外部 shared_ptr 都已释放,A 和 B 仍然互相持有,引用计数无法降为0,它们的析构函数永远不会被调用,导致内存泄漏。
代码示例:
#include <iostream>
#include <memory>
class B; // 声明
class A
{
public:
std::shared_ptr<B> ptrB;
A() { std::cout << "A Construct" << std::endl; }
~A() { std::cout << "A Destruct" << std::endl; }
};
class B
{
public:
std::shared_ptr<A> ptrA;
B() { std::cout << "B Construct" << std::endl; }
~B() { std::cout << "B Destruct" << std::endl; }
};
int main()
{
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b;
b->ptrA = a;
std::cout << "a use_count is: " << a.use_count() << std::endl;
std::cout << "b use_count is: " << b.use_count() << std::endl;
}
std::cout << "main exit" << std::endl;
return 0;
}
程序输出:
A Construct
B Construct
a use_count is: 2
b use_count is: 2
main exit
在上述代码中,当a和b离开作用域后,A和B的析构函数没有被调用。这是因为A和B相互引用,形成了循环引用,导致a和b的引用计数永远不会为 0,对象无法被释放。
我们可以把A和B其中一方改为weak_ptr,就可以打破循环引用,使对象能够正常释放 。
#include <iostream>
#include <memory>
class B; // 声明
class A
{
public:
std::shared_ptr<B> ptrB;
A() { std::cout << "A Construct" << std::endl; }
~A() { std::cout << "A Destruct" << std::endl; }
};
class B
{
public:
std::weak_ptr<A> ptrA;
B() { std::cout << "B Construct" << std::endl; }
~B() { std::cout << "B Destruct" << std::endl; }
};
int main()
{
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b;
b->ptrA = a;
std::cout << "a use_count is: " << a.use_count() << std::endl;
std::cout << "b use_count is: " << b.use_count() << std::endl;
}
std::cout << "main exit" << std::endl;
return 0;
}
程序输出:
A Construct
B Construct
a use_count is: 1
b use_count is: 2
A Destruct
B Destruct
main exit
我们可以看到当a和b离开作用域后,析构函数正常被调用,这是因为B使用weak_ptr指向A,不会增加引用计数,所有当a和b离开作用域,引用计数为0,资源被释放。
自定义删除器
有时,默认的 delete 操作不适用于所有资源管理场景。此时,可以使用自定义删除器来指定资源释放的方式。例如,管理文件句柄、网络资源或自定义清理逻辑。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
#include <cstdio>
void fileCloser(FILE* file)
{
if (file)
{
std::cout << "Closing file." << std::endl;
fclose(file);
}
}
int main() {
std::shared_ptr<FILE> file_ptr(fopen("test.txt", "w"), fileCloser);
if (file_ptr)
{
std::cout << "File opened successfully." << std::endl;
fprintf(file_ptr.get(), "Hello, World!\n"); // 使用file_ptr进行文件操作
}
return 0;
}
当shared_ptr被销毁会,会调用fileCloser关闭文件。使用filePtr.get()获取原生FILE*指针进行文件操作。
总结
std::unique_ptr
特性:
- 独占所有权:同一时刻只有一个 unique_ptr 可以指向给定的对象,不允许拷贝,只允许移动。
- 轻量级:大小通常与原始指针相当(除非使用自定义删除器),无额外性能开销。
- 自动释放:当 unique_ptr 被销毁(如离开作用域)或通过 reset() 释放时,所管理的对象自动被删除。
适用场景:
- 需要独占资源的场景(如工厂函数返回对象、容器元素等)。
- 作为类的成员变量,表示独有所有权。
- 避免裸指针带来的资源泄漏风险。
std::shared_ptr
特性:
- 共享所有权:多个 shared_ptr 可以指向同一个对象,内部使用引用计数控制生命周期。最后一个指向对象的 shared_ptr 被销毁时,对象被删除。
- 引用计数线程安全:引用计数的增减是原子操作,但所管理对象的线程安全性需自己保证。
- 额外开销:包含指向控制块的指针,控制块存储引用计数、弱计数、删除器等,内存占用较大(约两个原始指针大小),且原子操作有一定性能成本。
- 支持自定义删除器:可用于管理非 new 分配的资源(如文件句柄)。
适用场景:
- 多个对象需要共享同一资源(如多个容器共享数据、图结构等)。
- 复杂的生命周期管理,无法明确唯一的拥有者。
- 与 weak_ptr 配合解决循环引用。
std::weak_ptr
特性:
- 弱引用:指向由 shared_ptr 管理的对象,但不增加引用计数,因此不影响对象的生命周期。
- 必须与 shared_ptr 配合使用:通过 shared_ptr 构造或赋值。
- 解决循环引用:在可能形成环状依赖的结构中,将其中一个方向改为 weak_ptr 可打破循环,避免内存泄漏。
- 线程安全:lock() 方法原子性地尝试获取一个 shared_ptr,安全地检查对象是否存活。
适用场景:
- 打破 shared_ptr 循环引用。
- 缓存或观察者模式:需要跟踪对象但不想延长其生命周期。
- 延迟资源获取:需要时通过 lock() 临时获得所有权。