文章目录
一. 相同不同点概述
相同点:
unique_ptr,shared_ptr和weak_ptr都是C++11中引入的智能指针,用于管理动态分配的内存(auto_ptr是C++98引入的智能指针,并在C++11中被弃用)。
区别:
unique_ptr是独占的智能指针,只能有一个指针指向内存,它的所有权不能被复制或转移。当它超出作用域,或被显式销毁时,所指向的对象会被自动释放。
shared_ptr允许多个智能指针指向同一块内存,它使用引用计数来追踪有多少个指针指向内存。当最后一个指向此内存的shared_ptr离开作用域或显式被销毁时,内存会被释放。
weak_ptr是一个弱引用的智能指针,它可以从一个shared_ptr或另一个weak_ptr对象构造,可以通过调用lock()方法获得一个指向所管理对象的shared_ptr。weak_ptr不会影响所指向对象的引用计数,所以可以用来防止互相引用时的死锁。当weak_ptr指向的shared_ptr被销毁时,weak_ptr会自动失效。weak_ptr一般和shared_ptr配合一起使用,用在那些可能出现互相引用的场景。
二. shared_ptr用法总结
智能指针 std::shared_ptr 在代码中的用法涉及示例如下:
- 创建 std::shared_ptr 指向动态分配的内存:
std::shared_ptr<int> ptr = std::make_shared<int>(42);
- 共享对象的所有权, 持有多个指向相同对象的 shared_ptr:
auto ptr1 = std::make_shared<int>(10);
auto ptr2 = ptr1; // 多个 shared_ptr 指向同一块内存
- 传递给函数:
void someFunction(std::shared_ptr<int> ptr) {
// 函数中使用 shared_ptr
}
// 传递 shared_ptr 到函数
someFunction(ptr);
- 获取引用计数:
std::shared_ptr<int> ptr = std::make_shared<int>(42);
int count = ptr.use_count(); // 获取引用计数
- 重置为nullptr,释放当前指向的对象并将智能指针置为空:
ptr.reset();
或者shared_ptr,使其指向另一个动态分配的int对象,如下:
std::shared_ptr<int> ptr1(new int(10));
ptr1.reset(new int(20)); // 重置shared_ptr,使其指向另一个动态分配的int对象
- 使用自定义删除器:
std::shared_ptr<int> ptr(new int, myDeleter); // 使用自定义删除器
- 检查引用是否有效:
std::shared_ptr<int> ptr = std::make_shared<int>(42);
if (ptr) {
// ptr 指向对象有效
}
总的来说, shared_ptr 提供了通用的内存管理功能,允许多个指针共享对象的所有权,而不需要手动进行内存管理。此外还能方便地查看引用计数,并可以选择是否使用自定义删除器。
而unique_ptr基本和shared_ptr一致,但是unique_ptr没有拷贝功能,也没有引用计数。所以在C++11中没有make_unique()函数,需要用new来进行内存分配,但是在C++14中引入了make_unique。
weak_ptr 的出现是为了防止shared_ptr互相引用,不能销毁的死锁问题。并且weak_ptr提供了一种获取 shared_ptr 的安全方式,以避免因为 shared_ptr 指向的对象已经被释放而导致访问悬空指针的问题。这正是 weak_ptr 类似于一个"观察者"的角色,它可以监视一个 shared_ptr 的生命周期。
三. 三类指针用法示例。
#include <iostream>
#include <memory>
class TestClass {
public:
TestClass() {
std::cout << "Constructor called" << std::endl;
}
~TestClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
// C++11中没有make_unique, C++14中才有。在11中只能用new的方式给unique_ptr申请内存
// 使用 unique_ptr 和 new
std::unique_ptr<TestClass> uniquePtr(new TestClass());//正确,调用unique_ptr的构造函数
//std::unique_ptr<TestClass> uniquePtr = new TestClass();//错误,unique_ptr不支持从裸指针进行初始化,
//即不支持拷贝构造函数和赋值运算符。
// 使用 shared_ptr 和 make_shared
std::shared_ptr<TestClass> sharedPtr1 = std::make_shared<TestClass>();
std::shared_ptr<TestClass> sharedPtr2 = sharedPtr1; // sharedPtr2和sharedPtr1指向相同的内存
// 使用 weak_ptr
std::weak_ptr<TestClass> weakPtr = sharedPtr1;
std::cout << "use_count of sharedPtr1: " << sharedPtr1.use_count() << std::endl; // 输出引用计数
// weak_ptr 检查所指向的 shared_ptr 是否存活
if (auto temp = weakPtr.lock()) {
std::cout << "Pointer is still alive" << std::endl;
} else {
std::cout << "Pointer is expired" << std::endl;
}
return 0;
}
输出结果是:
Constructor called
Constructor called
use_count of sharedPtr1: 2
Pointer is still alive
Destructor called
Destructor called
其中,引用计数为2,说明是sharedPtr1的1次+sharedPtr2的1次,而weakPtr不会增加引用计数的次数。
四. shared_ptr引用计数详解
shared_ptr 的引用计数是通过一种称为“控制块”(control block)的结构来实现的。控制块通常会存储引用计数以及指向堆上分配的对象的指针。每当创建一个新的 shared_ptr 时,实际上是在管理这个共享的控制块,而不是直接管理所指向的对象。
以下是一个简化的示例,展示了 shared_ptr 引用计数的概念:
#include <iostream>
template <typename T>
class SharedPtr {
private:
struct ControlBlock {
T* data;
int referenceCount;
ControlBlock(T* ptr) : data(ptr), referenceCount(1) {}
};
ControlBlock* controlBlock;
public:
SharedPtr(T* ptr) : controlBlock(new ControlBlock(ptr)) {}
SharedPtr(const SharedPtr& other) : controlBlock(other.controlBlock) {
controlBlock->referenceCount++;
}
~SharedPtr() {
if (--controlBlock->referenceCount == 0) {
delete controlBlock->data;
delete controlBlock;
}
}
};
int main() {
SharedPtr<int> ptr1(new int(42));
SharedPtr<int> ptr2 = ptr1;
return 0;
}
在上述示例中,当创建 SharedPtr 对象时,会同时创建一个 ControlBlock 对象来管理引用计数。在拷贝构造函数中,引用计数会增加,当对象超出作用域时,引用计数减少。当引用计数为0时,ControlBlock 会负责释放内存。
这仅仅是一个简化的示例,真正的 shared_ptr 实现可能会更加复杂,并且还会处理多线程下的安全性等额外的问题。
五. auto_ptr和unique_ptr
auto_ptr是C++98标准引入的智能指针,用于管理动态分配的对象。它包含在头文件中,并位于std命名空间中。auto_ptr可以做到RAII(资源获取就是初始化),但是它具有潜在的安全和所有权转移问题,它不支持对资源的拷贝,所以容器在进行元素拷贝时可能会引起资源的不正确释放,从而导致悬垂指针问题。
使用C++11编译器编译auto_ptr代码可能会导致一些警告或错误。所以在C++11中将其弃用了。
C++11标准引入了更为安全和灵活的unique_ptr。unique_ptr引入了移动语义和禁止拷贝的特性,提供更好的语义和安全性。
移动语义是C++11引入的重要特性,它允许可移动对象的资源在不产生副本的情况下进行转移,从而提高性能并减少不必要的内存开销。
移动语义通过引入右值引用和移动构造函数来实现。右值引用是使用双 && 符号声明的引用类型,可以绑定到临时对象或表达式的结果,而移动构造函数允许将资源从一个对象转移到另一个对象,而不进行深层拷贝。移动语义的引入大大改善了在分配和管理资源时的性能和内存开销。
六. weak_ptr的lock()功能和用法
weak_ptr 用于解决 shared_ptr 循环引用问题。使用shared_ptr时可能会出现循环引用,这将导致资源无法正确释放。而weak_ptr则不会有这个问题。
weak_ptr 指向一个由 shared_ptr 管理的资源,但它并不增加引用计数,也不影响资源的生命周期。
那weak_ptr如何知道当前对象是否已经被释放呢?用lock()方法。
weak_ptr 可以通过调用 lock() 方法获得一个指向所管理对象的 shared_ptr。若被 weak_ptr 管理的对象已经被释放,lock() 会返回一个空的 shared_ptr。如果对象还存在,lock() 则会返回一个有效的 shared_ptr。
以下是一个简单的例子来说明 lock() 的用法:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// 使用 lock 获取 shared_ptr
if (auto locked = weak.lock()) {
// locked 是一个有效的 shared_ptr
std::cout << "shared_ptr有效, 内容为" << *locked << std::endl;
} else {
// shared_ptr 已经释放
std::cout << "shared_ptr 已经释放" << std::endl;
}
// 离开作用域后,shared_ptr 释放
shared.reset();
// 再次使用 lock 获取 shared_ptr
if (auto locked = weak.lock()) {
// locked 是一个有效的 shared_ptr
std::cout << "shared_ptr有效, 内容为" << *locked << std::endl;
} else {
// shared_ptr 已经释放
std::cout << "shared_ptr 已经释放" << std::endl;
}
return 0;
}
结果是:
shared_ptr有效, 内容为42
shared_ptr 已经释放
七、智能指针来管理动态分配的数组
《effective C++》里说,shared_ptr等智能指针在析构函数里做的是delete,而不是delete[]。这会导致“首尾不呼应”。所以不应该在动态分配的array上使用shared_ptr等智能指针。如:
std::shared_ptr<int> spi(new int[1024]); //馊主意!会用上错误的delete形式。
但《effective C++》比较老,可能说的不够正确。
其实我们可以自定义智能指针的删除器,自己实现delete[],就保证了“首尾呼应”。如下:
std::shared_ptr<int> spi(new int[1024], [](int* spi){ delete[] spi; });