一、引言
在软件开发与系统运维的广阔领域中,内存安全是基石亦是挑战。随着软件复杂度的增加,内存泄露、缓冲区溢出、越界访问等安全问题频发,不仅影响程序稳定性,更可能成为黑客攻击的入口。确保内存安全,意味着守护数据完整、预防系统崩溃,更是保障用户体验与业务连续性的关键。因此,深入探讨内存安全机制,采用先进防护技术,构建安全的内存使用环境,是每一位开发者与运维人员不可或缺的技能与责任。
C/C++语言可以方便的操作内存(通过指针等),因此能得到较高的运行效率。而指针是一把双刃剑,因为指针导致的问题也层出不穷,成为一个不可以绕过的问题。关于解决这个问题,C++也一直在努力,今天我们就来看看C++11中新增的智能指针。
二、简单的原理演示
C++中的智能指针其实是应用了“类对象出作用域后会自动调用其析构函数”的特点,将内存资源回收放在智能指针的析构函数中。以下是一个简单的自定义智能指针,旨在为了帮助大家理解其基本原理。
// 简单的智能指针
#include <iostream>
template <typename T>
class MyPointer{
private:
T* pointer_;
public:
MyPointer(T *p): pointer_(p){
}
~MyPointer(){
delete pointer_;
std::cout << "pointer_ destroy.";
}
};
int main(){
MyPointer<int> p(new int(10));
} // p对象会在这里析构,自动调用析构函数
三、unique_ptr智能指针
std::unique_ptr 是 C++11 引入的一种智能指针,用于自动管理具有唯一所有权的动态分配的对象。所有的智能指针都在<memory>头文件中声明。
std::unique_ptr 保证其指向的对象在 unique_ptr 被销毁时自动被删除,从而避免了内存泄漏。与 std::shared_ptr 不同,std::unique_ptr 不可被复制(即,不能有两个 unique_ptr 指向同一个对象),但可以被移动(move),这允许 unique_ptr 的所有权在不同作用域之间转移。
基本用法
创建 std::unique_ptr
#include <memory>
int main() {
// 使用 new 分配的内存初始化 unique_ptr
std::unique_ptr<int> ptr(new int(10));
// 使用 std::make_unique (C++14 及以后)
auto ptr2 = std::make_unique<int>(20);
return 0;
}
访问 std::unique_ptr 指向的对象
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl; // 输出 10
return 0;
}
转移 std::unique_ptr 的所有权
#include <iostream>
#include <memory>
void take_ownership(std::unique_ptr<int> ptr) {
std::cout << *ptr << std::endl; // 原始 ptr 将不再拥有对象
}
int main() {
std::unique_ptr<int> ptr(new int(10));
take_ownership(std::move(ptr)); // 将所有权转移给函数
// 此时 ptr 为空,访问 *ptr 将是未定义行为
return 0;
}
注意事项
- std::unique_ptr 不能被复制(即没有拷贝构造函数和拷贝赋值运算符),但可以被移动(通过移动构造函数和移动赋值运算符)。
- 当 std::unique_ptr 被销毁时,它指向的对象也会被删除。因此,确保 std::unique_ptr 指向的对象支持删除操作(即对象是通过 new 分配的)。
- 使用 std::make_unique 是创建 std::unique_ptr 的推荐方式,因为它更安全且更简洁(不需要显式调用 new,也避免了与 new 相关的异常安全性问题)。
- std::unique_ptr 是管理动态分配资源(特别是那些具有唯一所有权的资源)的强大工具,它帮助减少了内存泄漏的风险,并使代码更加简洁和安全。
四、shared_ptr智能指针
std::shared_ptr 是 C++11 引入的另一种智能指针,用于自动管理具有共享所有权的动态分配的对象。与 std::unique_ptr 不同,std::shared_ptr 允许多个 shared_ptr 实例共享对同一对象的所有权。当最后一个 shared_ptr 被销毁或重置时,指向的对象才会被删除。这种机制通过内部使用一个控制块(通常是一个引用计数)来实现,该控制块跟踪有多少个 shared_ptr 实例指向某个对象。
基本用法
创建 std::shared_ptr
#include <memory>
#include <iostream>
int main() {
// 使用 new 分配的内存和 std::shared_ptr 构造函数
std::shared_ptr<int> ptr1(new int(10));
// 使用 std::make_shared(推荐方式)
auto ptr2 = std::make_shared<int>(20);
std::cout << *ptr1 << std::endl; // 输出 10
std::cout << *ptr2 << std::endl; // 输出 20
return 0;
}
访问 std::shared_ptr 指向的对象
与 std::unique_ptr 类似,你可以通过解引用 shared_ptr 来访问其指向的对象。
std::cout << *ptr1 << std::endl; // 访问 ptr1 指向的对象
复制 std::shared_ptr
与 std::unique_ptr 不同,std::shared_ptr 支持复制操作,这会增加引用计数。
std::shared_ptr<int> ptr3 = ptr1; // 复制 ptr1,引用计数增加
转移 std::shared_ptr 的所有权(实际上是复制,因为所有权是共享的)
虽然 std::shared_ptr 可以通过移动语义进行转移,但在大多数情况下,这更像是复制操作,因为所有权是共享的。然而,你可以使用 std::move 来提高效率(尽管在 shared_ptr 的情况下,这种效率提升可能很小或不存在)。
auto ptr4 = std::move(ptr1); // 移动语义,但所有权仍然是共享的
注意事项
- 当最后一个 std::shared_ptr 被销毁或重置时,指向的对象才会被删除。
- std::shared_ptr 允许复制,因此可以安全地在多个地方共享对同一对象的所有权。
- 使用 std::make_shared 是创建 std::shared_ptr 的推荐方式,因为它比直接使用 new 和 std::shared_ptr 构造函数更高效(因为它可以在单个内存分配中同时分配控制块和对象)。
- 循环引用是 std::shared_ptr 使用时需要避免的一个常见问题。当两个或多个 shared_ptr 实例相互持有对方的引用时,它们会导致引用计数永远不会降至零,从而阻止对象被删除。解决循环引用的常见方法是使用 std::weak_ptr。
五、weak_ptr智能指针
std::weak_ptr 是 C++11 引入的一种智能指针,用于解决 std::shared_ptr 可能导致的循环引用问题。std::weak_ptr 不拥有其所指向对象的所有权,即它不会增加对象的引用计数。相反,它提供了一种方式来访问一个由 std::shared_ptr 管理的对象,但不参与该对象的生命周期管理。
基本用法
创建 std::weak_ptr
std::weak_ptr 通常是通过一个 std::shared_ptr 来创建的,这样它就指向了由 std::shared_ptr 管理的同一个对象。
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
std::weak_ptr<int> weakPtr = sharedPtr; // weakPtr 现在指向 sharedPtr 管理的对象
// 不能直接解引用 weakPtr,因为它不拥有对象
// std::cout << *weakPtr << std::endl; // 这是错误的
// 但可以通过 lock 方法尝试获取一个指向对象的 shared_ptr
if (auto lockedPtr = weakPtr.lock()) {
std::cout << *lockedPtr << std::endl; // 输出 10
}
return 0;
}
使用 lock 方法
std::weak_ptr 的 lock 方法尝试返回一个指向其所指对象的 std::shared_ptr。如果原始 shared_ptr 仍然存在(即它指向的对象尚未被删除),则 lock 方法会成功并返回一个指向该对象的 shared_ptr。如果原始 shared_ptr 已被销毁(即它指向的对象已被删除),则 lock 方法会返回一个空的 shared_ptr。
注意事项
- std::weak_ptr 不拥有它所指向的对象的所有权,因此不会增加对象的引用计数。
- 当最后一个 std::shared_ptr 被销毁时,它所管理的对象也会被删除,即使还有 std::weak_ptr 指向该对象。
- 使用 std::weak_ptr 可以解决 std::shared_ptr 之间的循环引用问题,因为它允许你保持对对象的引用,但不阻止对象的删除。
- 不能直接解引用 std::weak_ptr,因为它不保证它所指向的对象仍然存在。相反,应该使用 lock 方法来获取一个临时的 std::shared_ptr,然后对该 shared_ptr 进行操作。
- 由于 std::weak_ptr 不拥有对象的所有权,因此它通常与 std::shared_ptr 一起使用,以提供对共享对象的非拥有性访问。
六、使用weak智能指针避免循环引用
#include <iostream>
#include <memory>
// 定义两个类,它们会相互引用对方
class B; // 前向声明
class A {
public:
std::shared_ptr<B> bPtr; // 使用shared_ptr来持有B的引用
~A() {
std::cout << "A is being destroyed\n";
}
};
class B {
public:
std::weak_ptr<A> aWeakPtr; // 使用weak_ptr来避免循环引用
~B() {
std::cout << "B is being destroyed\n";
}
};
int main() {
// 创建两个shared_ptr,分别指向A和B的实例
auto aPtr = std::make_shared<A>();
auto bPtr = std::make_shared<B>();
// 让A持有B的shared_ptr
aPtr->bPtr = bPtr;
// 让B持有A的weak_ptr,从而避免循环引用
bPtr->aWeakPtr = aPtr;
// 此时,没有直接的循环引用,因为B持有的是A的weak_ptr
// 离开main函数的作用域时,aPtr和bPtr会被销毁
// 由于bPtr持有的是aPtr的weak_ptr,它不会阻止aPtr的销毁
// 当aPtr被销毁时,它会销毁其持有的bPtr(因为bPtr是一个shared_ptr)
// 然后bPtr的销毁会触发B的析构函数,接着是A的析构函数(如果它还没有被销毁的话)
return 0;
}
// 输出结果应该是:
// B is being destroyed
// A is being destroyed
// 注意:输出的顺序可能因编译器和运行时环境的不同而有所变化
在这个示例中,A类持有一个指向B的std::shared_ptr,而B类持有一个指向A的std::weak_ptr。由于B不持有A的shared_ptr,因此不会增加A的引用计数,从而避免了循环引用。当main函数结束时,aPtr和bPtr(作为shared_ptr)会被销毁。由于bPtr持有aPtr的weak_ptr,它不会阻止aPtr的销毁。接着,aPtr的销毁会触发A的析构函数,并导致A持有的bPtr(A中的bPtr成员)也被销毁,进而触发B的析构函数。这样,两个对象都被正确地销毁了,没有发生内存泄漏。
七、小结
智能指针是C++为了防止内存泄露的一种可行的手段,它解决了编程人员在内存分配和回收上的纠结。因此,在不极致追求效率的情况下(智能指针会增加一定的开销),建议使用智能指针。