目录
一、常用的智能指针:
1、shared_ptr(共享所有权的智能指针)
std::shared_ptr
允许多个指针共享同一个对象的所有权。当最后一个拥有该对象的 shared_ptr
离开作用域或被重置时,对象会被自动删除,实现自动资源管理。
shared_ptr
内部维护一个引用计数,每次拷贝构造或赋值增加计数,对象被销毁时减少计数,计数归零时自动释放内存。由于需要维护引用计数,shared_ptr
相比 unique_ptr
有轻微的性能开销。(代码详情请看例子1)
2、unique_ptr(独占所有权的智能指针)
std::unique_ptr
确保同一时间内只有一个指针拥有对象的所有权。它不允许拷贝,但可以转移所有权(通过移动语义)。
如同 shared_ptr
,当 unique_ptr
离开作用域或被重置时,它所管理的对象会被删除。相比 shared_ptr
,unique_ptr
更轻量级,因为它不需要维护引用计数
3、weak_ptr
与shared_ptr
协同使用,提供一种非拥有(弱)引用到由shared_ptr
管理的对象的方式。weak_ptr
不会增加对象的引用计数,因此不会阻止对象被销毁。在需要检查对象是否仍然存在时非常有用,通常与lock()
函数一起使用来尝试提升为临时的shared_ptr(代码详情请看例子2)
。
为什么需要需要临时转化为shared_ptr,lock()函数的作用是啥?
-
访问控制:
std::weak_ptr
不增加它所指向对象的引用计数,因此,直接使用std::weak_ptr
不能保证当你尝试访问对象时,对象仍然存在。它主要用于监控对象是否存在而不影响对象的生命周期管理。 -
安全访问:当需要实际访问(读取或修改)由
std::shared_ptr
管理的对象时,必须确保该对象仍然有效,即没有被销毁。通过调用lock()
方法,std::weak_ptr
尝试提升为std::shared_ptr
。如果对象仍然存在,lock()
会返回一个指向该对象的新的std::shared_ptr
实例,同时增加对象的引用计数,从而确保在接下来的使用过程中对象不会被意外释放。 -
避免悬挂引用(例子4):如果不使用
lock()
转换而直接使用std::weak_ptr
访问对象,就有可能遇到悬挂引用的问题,即引用指向的内存已经被释放。通过转换成std::shared_ptr
,可以在访问前确认对象的有效性,避免了这种风险。
二、智能指针的创建方法
1.make_shared
std::make_shared
是一个用于创建 shared_ptr
的函数,它在同一个内存块中同时分配控制块和对象,这通常能提高性能并减少内存碎片。
通过一次内存分配操作完成智能指针及其管理对象的创建,这比单独分配内存和使用构造 shared_ptr
更高效。对于 shared_ptr
的创建,通常推荐使用 make_shared
,因为它更高效且更安全。
代码示例:
// 法1:使用 shared_ptr 构造函数,先动态分配 MyClass 对象,然后传递指针
MyClass* rawPtr = new MyClass();
std::shared_ptr<MyClass> sptr(rawPtr);
// 注意:通常建议使用 make_shared,因为这里存在两步操作,可能导致异常安全问题
// 法2:使用 make_shared 创建 shared_ptr,同时分配 MyClass 对象的内存和管理控制块
std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();
2.make_unique
类似于 make_shared
,std::make_unique
是创建 unique_ptr
的函数,它简化了 unique_ptr
的初始化过程。
代码示例:
// 法1:使用 unique_ptr 构造函数直接管理 MyClass 对象的生命周期
std::unique_ptr<MyClass> uptr(new MyClass());
// 法2:使用 make_unique 创建 unique_ptr,简化初始化过程
std::unique_ptr<MyClass> uptr = std::make_unique<MyClass>();
思考: make_unique、make_shared创建一个指针变量和new创建指针变量的区别?
三、示例
1.例子(引用计数):
std::shared_ptr
如何通过引用计数来管理内存的生命周期,赋值和拷贝构造的方式
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed." << std::endl; }
~MyClass() { std::cout << "MyClass destructed." << std::endl; }
};
void displayRefCount(std::shared_ptr<MyClass> ptr) {
std::cout << "Reference count in function: " << ptr.use_count() << std::endl;
}
int main() {
// 创建一个shared_ptr,引用计数初始化为1
std::shared_ptr<MyClass> sptr1 = std::make_shared<MyClass>();
std::cout << "Initial reference count: " << sptr1.use_count() << std::endl; // 输出: 1
// 赋值:复制sptr1到sptr2,引用计数增加到2
std::shared_ptr<MyClass> sptr2 = sptr1;
std::cout << "Reference count after copy: " << sptr2.use_count() << std::endl; // 输出: 2
// 拷贝构造:使用拷贝构造函数创建一个新的shared_ptr实例,这会增加原始对象的引用计数
std::shared_ptr<MyClass> sptr3(sptr1);
// 将sptr1传递给函数,引用计数不变(传递的是引用)
displayRefCount(sptr1); // 输出: Reference count in function: 2
// sptr1生命周期结束,引用计数减1
sptr1.reset();
sptr2.reset();
std::cout << "Reference count after sptr1 and sptr2 reset: " << sptr3.use_count() << std::endl; // 输出: 1
// sptr3生命周期结束,引用计数减到0,自动释放内存
sptr3 = nullptr;
// 此时MyClass对象的析构函数会被调用,输出"MyClass destructed."
return 0;
}
2.例子(std::weak_ptr: 与shared_ptr协同使用 ):
假设我们设计一个简单的日志系统,其中日志条目(LogEntry
)被多个地方共享查看,但我们也希望有个日志索引(LogIndex
)来快速查找日志,但这个索引不应当阻止日志条目因无人使用而被释放。在这个例子中,LogIndex
类使用std::weak_ptr
存储对LogEntry
的引用,这样即使所有的std::shared_ptr
(直接指向日志条目的)都被销毁,也不会阻止日志条目被释放。当我们尝试通过索引查找日志条目时,使用lock()
函数尝试将weak_ptr
转换为shared_ptr
,如果转换成功则说明日志条目仍然存在,否则说明对象已经被销毁。这样,std::weak_ptr
既实现了对共享资源的非拥有引用,又避免了潜在的循环引用问题(例子3
),提高了内存管理的安全性和效率。
#include <iostream>
#include <memory>
#include <map>
class LogEntry {
public:
explicit LogEntry(const std::string& message) : message_(message) {
std::cout << "LogEntry created: " << message_ << std::endl;
}
~LogEntry() {
std::cout << "LogEntry destructed: " << message_ << std::endl;
}
const std::string& GetMessage() const { return message_; }
private:
std::string message_;
};
class LogIndex {
public:
void AddEntry(std::shared_ptr<LogEntry> entry) {
index_[entry->GetMessage()] = std::weak_ptr<LogEntry>(entry);
}
std::shared_ptr<LogEntry> FindEntry(const std::string& message) {
auto it = index_.find(message);
if (it != index_.end()) {
// 尝试将weak_ptr升级为shared_ptr以检查对象是否还存在
std::shared_ptr<LogEntry> sharedEntry = it->second.lock();
if (sharedEntry) {
std::cout << "Found LogEntry: " << message << std::endl;
return sharedEntry;
} else {
std::cout << "LogEntry not found: " << message << " (already destructed)" << std::endl;
}
}
return nullptr;
}
private:
std::map<std::string, std::weak_ptr<LogEntry>> index_;
};
int main() {
std::shared_ptr<LogEntry> entry1 = std::make_shared<LogEntry>("Hello");
std::shared_ptr<LogEntry> entry2 = std::make_shared<LogEntry>("World");
LogIndex logIndex;
logIndex.AddEntry(entry1);
logIndex.AddEntry(entry2);
// 使用FindEntry尝试访问日志条目
std::shared_ptr<LogEntry> foundEntry = logIndex.FindEntry("Hello");
if (foundEntry) {
std::cout << "Accessing LogEntry: " << foundEntry->GetMessage() << std::endl;
}
// entry1和entry2生命周期结束,引用计数归零,日志条目被销毁
entry1.reset();
entry2.reset();
// 尝试再次访问已销毁的日志条目
foundEntry = logIndex.FindEntry("Hello");
return 0;
}
注:
index_
是一个std::map<std::string, std::weak_ptr<LogEntry>>
类型的成员变量,它存储了日志条目消息到其std::weak_ptr
的映射。
-
index_.find(message)
:这一行代码会在index_
这个映射中查找键为message
的元素。find
方法返回一个迭代器,指向找到的元素,如果找不到则返回index_.end()
。 -
if (it != index_.end())
:这行代码检查find
操作的结果。如果找到了对应的条目(即message
存在于index_
中),it
将不等于index_.end()
,因此条件为真,执行大括号内的代码块。这意味着正在尝试访问的message
在日志索引中确实有一个关联的std::weak_ptr
。
3.例子(循环引用问题)
循环引用问题通常发生在两个或更多的对象相互持有对方的智能指针,形成一个闭环,导致即使所有外部引用都消失,这些对象的引用计数也无法降为0,进而无法自动释放内存。使用std::weak_ptr
可以打破这种循环引用,下面是一个简化示例:
假设有两个类,Parent
和Child
,每个Parent
对象可以有多个Child
,同时每个Child
知道其所属的Parent
。如果直接使用std::shared_ptr
互相持有,就会形成循环引用。
// 错误使用方法:未使用std::weak_ptr的错误示例:
class Child;
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
// ...
};
class Child {
public:
std::shared_ptr<Parent> parent;
// ...
};
// 正确使用方法:使用std::weak_ptr解决问题:
class Child;
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
// ...
};
class Child {
public:
std::weak_ptr<Parent> parent; // 改为使用weak_ptr
// ...
};
4.例子(悬空引用)
假设一个图书馆管理系统,其中书籍(Book
)的信息可能被多个地方引用,但也需要一个不增加引用计数的索引来快速查找书籍。书籍条目可能因为无人借阅而从系统中移除(模拟对象被释放)。
#include <iostream>
#include <memory>
#include <unordered_map>
class Book {
public:
Book(const std::string& title) : title_(title) {
std::cout << "Book created: " << title_ << std::endl;
}
~Book() {
std::cout << "Book destructed: " << title_ << std::endl;
}
std::string getTitle() const { return title_; }
private:
std::string title_;
};
class LibraryCatalog {
public:
void addBook(const std::shared_ptr<Book>& book) {
books_[book->getTitle()] = std::weak_ptr<Book>(book);
}
// 不安全的访问示例:直接使用weak_ptr
void unsafeAccess(const std::string& title) {
auto it = books_.find(title);
if (it != books_.end()) {
// 直接解引用weak_ptr可能会导致悬挂引用错误
std::cout << "Book title from weak_ptr: " << it->second.lock()->getTitle() << std::endl;
}
}
// 安全的访问示例:通过lock()转换为shared_ptr
void safeAccess(const std::string& title) {
auto it = books_.find(title);
if (it != books_.end()) {
// 先通过lock()尝试获取有效的shared_ptr
std::shared_ptr<Book> sharedBook = it->second.lock();
if (sharedBook) {
std::cout << "Book title from safe access: " << sharedBook->getTitle() << std::endl;
} else {
std::cout << "Book already destructed or not found." << std::endl;
}
}
}
private:
std::unordered_map<std::string, std::weak_ptr<Book>> books_;
};
int main() {
std::shared_ptr<Book> book = std::make_shared<Book>("The Catcher in the Rye");
LibraryCatalog catalog;
catalog.addBook(book);
// 使book的shared_ptr失效,模拟对象可能被释放的情况
book.reset();
// 不安全的访问尝试
catalog.unsafeAccess("The Catcher in the Rye"); // 这里可能产生悬挂引用错误或未定义行为
// 安全的访问尝试
catalog.safeAccess("The Catcher in the Rye"); // 正确处理对象可能已被释放的情况
return 0;
}
此处结束。。。。。。
参考文献
[1] 钱能. C++程序设计教程(第二版)[M]. 北京: 清华大学出版社, 2005.9.