智能指针
- 一、内存泄露
- 1.1 内存泄露常见原因
- 1.2 如何避免内存泄露
- 二、实例Demo
- 2.1 文件结构
- 2.2 Dog.h
- 2.3 Dog.cpp
- 2.3 mian.cpp
- 三、独占式智能指针:unique _ptr
- 3.1 创建方式
- 3.1.1 ⭐从原始(裸)指针转换:
- 3.1.2 ⭐⭐使用 new 关键字直接创建:
- 3.1.3 ⭐⭐⭐使用 std::make_unique(推荐)
- 3.1.4 对比:"使用 std::make_unique" 和 "使用 new 关键字直接创建"
- 3.2 unique的特性
- 3.2.1 独占所有权:
- 3.2.2 不可复制、可移动:
- 3.2.3 自动内存管理:
- 3.2.4 自定义删除器:
- 3.2.5 支持数组:
- 3.2.6 与标准库的兼容性:
- 四、共享计数指针:shared_ptr
- 4.1 实例
- 4.2 shared_ptr 的特性
- 4.2.1 共享所有权
- 4.2.2 引用计数:
- 4.2.3自动内存管理:
- 4.2.4 线程安全的引用计数:
- 4.2.5 使用方便:
- 4.2.6 支持自定义删除器:
- 4.2.7std::weak_ptr 结合使用:
- 五、弱引用智能指针:weak_ptr
- 5.1 循环依赖
- 5.2 weak_ptr 的特性
- 5.2.1 不增加引用计数:
- 5.2.2 防止循环引用:
- 5.2.3 可以转换为 shared_ptr:
- 5.2.4 允许检查资源状态:
- 5.2.5 与 shared_ptr 共享同一控制块:
- 5.2.6 线程安全:
前言:参加工作两年来,感觉还没咋使用过智能指针,今天还是来总结学习下,以后总有天会用到的
一、内存泄露
在C++
中,内存泄露指的是程序在运行过程中动态分配内存(使用new
或malloc
)后,没有释放相应的内存(使用delete
或free
),导致这些内存空间无法被重新使用,从而造成系统资源的浪费,最终可能导致程序在长时间运行后耗尽可用内存。
1.1 内存泄露常见原因
🎈①忘记释放内存: 在使用动态内存分配后,程序的某些路径可能会遗漏释放内存的代码。
void example() {
int* arr = new int[10]; // 动态分配内存
// ... 使用 arr
// delete[] arr; // 忘记释放内存
}
🎈②异常处理: 在分配内存之后如果发生异常,且没有正确处理异常,会导致内存未释放。
void example() {
int* arr = new int[10];
// 假设这里发生异常
throw std::runtime_error("Error");
delete[] arr; // 这一行永远不会被执行
}
🎈③指针丢失: 将指针赋值为其他指针时,如果没有先释放原有指针指向的内存,就会造成内存泄漏。
void example() {
int* ptr = new int(42);
ptr = new int(24); // 原来的内存没有释放,造成泄漏
delete ptr; // 只释放了新的内存
}
容器管理:在使用标准库容器如std::vector
,std::map
等时,注意对存储的指针管理,不要在容器中存储动态分配的原始指针,建议使用智能指针。
1.2 如何避免内存泄露
🎈①使用智能指针:C++11
引入了std::unique_ptr
和std::shared_ptr
,它们自动管理内存,减少内存泄露的风险。
#include <memory>
void example() {
std::unique_ptr<int[]> arr(new int[10]); // 使用智能指针
// 不需要手动释放内存,超出作用域时自动释放
}
🎈②确保配对:每次使用new
或malloc
时,确保有相应的delete
或free
。
🎈③RAII
(资源获取即初始化):将资源的生命周期与对象的生命周期绑定,避免手动管理内存。
🎈④使用内存检查工具:使用工具如Valgrind
、AddressSanitizer
等进行内存错误检测,帮助发现内存泄露和其它内存管理问题。
通过以上措施,可以大大减少C++程序中的内存泄露问题
二、实例Demo
2.1 文件结构
2.2 Dog.h
#ifndef DOG_H
#define DOG_H
/*这里使用条件编译指令防止 重复引用头文件 达到目的:"头文件保护"或"包含保护"也可以用:#pragma once 但是使用该方式更具备兼容性*/
#include<string>
#include<iostream>
class Dog
{
public:
Dog(const std::string& name);
Dog() = default;/* 这里用了关键字defalut 保留编译器原有的构造函数*/
~Dog();
void dog_info() const/*使用const 关键字确保该函数只读该类成员*/
{
std::cout << "U Dog name is:" << this->m_name << std::endl;
}
std::string get_name () const
{
return m_name;
}
void set_name(const std::string& u_name) /*使用const+引用 确保u_name 只读且通过引用方式传值提高效率,避免非必要的拷贝*/
{
m_name = u_name;
}
private:
std::string m_name ="bigYellow";
};
#endif
2.3 Dog.cpp
#include "Dog.h"
Dog::Dog(const std::string& name):m_name(name)
{
std::cout << "构造一根狗子,它叫:" <<m_name <<std::endl;
}
Dog::~Dog()
{
std::cout << "析构一根狗子,它叫:" << m_name << std::endl;
}
2.3 mian.cpp
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
return 0;
}
三、独占式智能指针:unique _ptr
在C++
中,智能指针是用来管理动态分配内存的对象,以减少内存泄漏和资源管理的复杂性。独占式指针(std::unique_ptr
)是C++11
标准库中引入的一种智能指针,它的主要特点是保证对其所管理的资源具有唯一的所有权。
3.1 创建方式
3.1.1 ⭐从原始(裸)指针转换:
通过 std::unique_ptr
的构造函数或 std::unique_ptr
的 reset
方法可以将一个现有的原始(裸)指针转换为 unique_ptr
。
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
/*方式①:通过原始(裸)指针*/
Dog* gua = new Dog("GUA");
unique_ptr<Dog> dogUniquePtr(gua);// 将已有的裸指针分配给unique_ptr
gua->set_name("XI");
gua->dog_info();
dogUniquePtr->dog_info();
return 0;
}
运行结果:
我们不妨将这两个指针打印出来:
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
/*方式①:通过原始(裸)指针*/
Dog* gua = new Dog("GUA");
unique_ptr<Dog> dogUniquePtr(gua);// 将已有的裸指针分配给unique_ptr
gua->set_name("XI");
gua->dog_info();
dogUniquePtr->dog_info();
cout << "gua的地址:" << gua << endl;
cout << "dogUniquePtr的地址:" << dogUniquePtr.get() << endl;
//delete gua;
return 0;
}
运行结果:
两个指针其实都指向了同一块地址,但是我们发现,这块地址任然可以被两个指针操作,这也能叫独占智能指针吗?安全吗这?如果我释放了gua
的地址会怎么样?解开上述实例代码最后一行执行:
运行结果:
程序崩溃
结论: 这种方式虽然会自动释放内存地址,但是原始(裸)指针仍然可以操作该地址。
如果尝试使用已被 std::unique_ptr
释放的内存,会导致未定义行为。
如果你在裸指针上 delete
了内存,而后又让 std::unique_ptr
执行析构,就会发生双重释放的问题,这将会导致程序崩溃。
所以,尽管 std::unique_ptr
是设计为独占式的,但如果需要在你的代码中使用裸指针,必须非常小心,确保不会在裸指针和 std::unique_ptr
之间发生冲突。通常情况下,最佳实践是尽量减少对于裸指针的使用,而是使用智能指针进行内存管理,以确保资源的安全和有效释放。
3.1.2 ⭐⭐使用 new 关键字直接创建:
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
/*方式②:通过new关键字创建*/
unique_ptr<Dog> jl{ new Dog("jl") };
jl->dog_info();
return 0;
}
运行结果:
3.1.3 ⭐⭐⭐使用 std::make_unique(推荐)
C++14
添加了 std::make_unique
函数,能够更安全地创建 unique_ptr
。
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
/*方式③:通过td::make_unique(推荐)创建*/
unique_ptr<Dog> zzy = std::make_unique<Dog>("zzy");
zzy->dog_info();
zzy->set_name("bigZZY");
zzy->dog_info();
return 0;
}
运行结果:
3.1.4 对比:“使用 std::make_unique” 和 “使用 new 关键字直接创建”
① 可读性和简洁性
- new 关键字:
std::unique_ptr<int> ptr(new int(42));
- std::make_unique:
auto ptr = std::make_unique<int>(42);
使用 std::make_unique
更加简洁、可读,将所有权的转移和对象的构造合并为一行代码,同时也更加简洁,减少了冗余的 new
关键字。
② 安全性
new
关键字:在使用 new
时,可能发生内存分配失败的情况,此时new
会抛出 std::bad_alloc
异常。创建 std::unique_ptr
的时候,如果 new
返回 nullptr
,则需要开发者在代码中处理这些异常。
std::make_unique:std::make_unique
不会返回 nullptr
,而是直接抛出异常,因此代码更加安全并且没有风险(因为 std::unique_ptr
会自动管理内存,即使在构造失败的情况下也不会泄漏内存)。
③ 内存泄漏风险
new
关键字:如果使用 new
创建一个对象,并且由于某种错误(例如异常抛出),没有成功将其指针传递给 std::unique_ptr
,则会发生内存泄漏。
std::unique_ptr<int> ptr;
if (someCondition) {
int* rawPtr = new int(42); // 如果在此之后抛出异常,将会泄漏
ptr.reset(rawPtr);
} // 可能遇到内存泄漏
std::make_unique
:通过 std::make_unique
创建对象的过程中,确保了不会有内存泄漏的风险,因为std::make_unique
会处理所有权的转移并直接返回 std::unique_ptr
。
④. 性能
在性能方面,std::make_unique
一般情况下是更优的选择,尽管对于大多数应用程序,它们之间的性能差异可以忽略不计。使用 std::make_unique
可以避免可能的额外操作。
总结:
推荐使用 std::make_unique:由于它提高了可读性、安全性,并且避免了内存泄漏的风险,现代 C++ 的最佳实践是使用 std::make_unique 来创建和管理动态分配的对象。
仅在特定情况下使用 new:当你需要直接获取裸指针或者从其他 API 中传递裸指针时,才可能临时使用 new ,但也要小心管理内存,确保避免内存泄漏。
3.2 unique的特性
3.2.1 独占所有权:
std::unique_ptr
提供独占性所有权语义,这意味着每个 unique_ptr
对象都可以唯一拥有一个动态分配的资源。这样的设计确保了资源不会被多个指针管理,从而避免了双重释放(double free)
等问题
结合实例:
/*特性①:独占所有权*/
Dog* gua = new Dog("GUA");
unique_ptr<Dog> dogUniquePtr1(gua);
//编译错误:不能复制 unique_ptr,因为它是独占的
//unique_ptr<Dog> dogUniquePtr2 = dogUniquePtr1;
3.2.2 不可复制、可移动:
std::unique_ptr
不支持复制(copy
),即不能使用拷贝构造函数或拷贝赋值运算符,因为这会导致所有权的混淆。相反,它支持移动语义(move
),可以通过移动构造和移动赋值将资源的所有权从一个 unique_ptr
转移到另一个:
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1 现在为空,ptr2 拥有资源
结合实例:
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
void do_with_pass_dog(unique_ptr<Dog> u_dog)
{
cout<<"u_dog 地址是:"<<u_dog.get()<<endl;
u_dog->dog_info();
}
int main(int argc,char** argv)
{
/*特性②:不可复制、可移动:*/
unique_ptr<Dog> zzy = std::make_unique<Dog>("zzy");
//do_with_pass_dog(zzy);
cout << "zzy 地址是:" << zzy.get() << endl;
do_with_pass_dog(std::move(zzy));
cout << "zzy 地址是:" << zzy.get() << endl;
return 0;
}
运行结果:
不难发现 通过move
,将该智能指针管理的地址,交接给了宁外一个智能指针,这种特性保证了,只有一个智能指针管理一块地址,满足独占指针的特性。
3.2.3 自动内存管理:
当 std::unique_ptr
的生命周期结束时,它所持有的资源会自动被释放。这种自动释放机制减少了手动管理内存的需要,降低了内存泄漏风险:
{
std::unique_ptr<int> ptr(new int(10)); // ptr 指向新分配的整数
} //ptr 到达作用域末尾,自动释放内存
3.2.4 自定义删除器:
std::unique_ptr
可以接受一个自定义的删除器,这使得它不仅局限于标准类型的资源管理。例如,可以在资源释放时执行特定的逻辑:
std::unique_ptr<FILE, decltype(&fclose)> filePtr(fopen("example.txt", "r"), fclose);
3.2.5 支持数组:
std::unique_ptr
可以用来管理动态数组。使用 std::unique_ptr<T[]>
来确保数组的正确释放:
std::unique_ptr<int[]> arrPtr(new int[10]); // 管理动态分配的整型数组
3.2.6 与标准库的兼容性:
std::unique_ptr
可以与 C++
标准库中的其他组件良好协作,例如可以用在 STL
容器(如 std::vector
)中,从而构建复杂的数据结构:
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10));
四、共享计数指针:shared_ptr
std::shared_ptr
是 C++11
引入的智能指针之一,属于C++
标准库中的 <memory>
头文件。它实现了共享所有权的内存管理方式,允许多个 std::shared_ptr
实例共同拥有同一个对象。shared_ptr
创建了一个计数器与类对象所指的内存相关联 Copy
则计数器加一,销毁则计数器减一api
为use_count()
注意:
weak ptr
并不拥有所有权
并不能调用->
和解引用*
4.1 实例
#include <iostream>
#include "Dog.h"
#include <memory>/*使用智能指针的头文件*/
using namespace std;
int main(int argc,char** argv)
{
/*计数指针*/
// 创建一个 shared_ptr,指向 MyClass 对象
std::shared_ptr<Dog> ptr1 = std::make_shared<Dog>();
ptr1->dog_info();
// 创建另一个 shared_ptr,指向同一个对象
std::shared_ptr<Dog> ptr2 = ptr1;
std::cout << "Reference Count: " << ptr1.use_count() << std::endl; // 输出: 2
return 0;
}
运行结果:
①初始化 :std::shared_ptr
:使用 std::make_shared
创建一个 shared_ptr ptr1
,指向 MyClass
的对象。
②共享所有权:ptr2
是通过拷贝 ptr1
创建的,它们共同拥有同一个 MyClass
对象。此时,引用计数变为。
③引用计数: 使用 use_count()
方法可以查看有多少个shared_ptr
在共享同一个对象。
④自动释放资源: 当 ptr1
和ptr2
在作用域结束后,引用计数降为零,MyClass
对象的内存将自动被释放,调用其析构函数。
4.2 shared_ptr 的特性
4.2.1 共享所有权
多个 std::shared_ptr 实例可以共同管理同一个动态分配的对象,每个指针都有对这个对象的所有权。
#include <iostream>
#include <memory>
struct Object {
int value;
Object(int v) : value(v) {}
};
int main() {
std::shared_ptr<Object> ptr1 = std::make_shared<Object>(10);
std::shared_ptr<Object> ptr2 = ptr1; // ptr2 共享 ptr1 的所有权
std::cout << "Value from ptr1: " << ptr1->value << std::endl;
std::cout << "Value from ptr2: " << ptr2->value << std::endl;
return 0;
}
4.2.2 引用计数:
每个 std::shared_ptr
都维护一个引用计数,用于跟踪有多少个 shared_ptr
实例指向同一个对象。当引用计数降为零时,指向的对象会被自动释放。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> countPtr = std::make_shared<int>(42);
std::cout << "Reference Count: " << countPtr.use_count() << std::endl; // 输出 1
{
std::shared_ptr<int> anotherPtr = countPtr;
std::cout << "Reference Count: " << countPtr.use_count() << std::endl; // 输出 2
} // anotherPtr 超出作用域
std::cout << "Reference Count: " << countPtr.use_count() << std::endl; // 输出 1
return 0;
}
4.2.3自动内存管理:
在 std::shared_ptr
超出作用域或被重置时,会自动释放关联的对象,减少内存泄漏的风险。
#include <iostream>
#include <memory>
struct Resource {
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
};
int main() {
{
std::shared_ptr<Resource> resourcePtr = std::make_shared<Resource>();
// 资源在这里管理
} // resourcePtr 超出作用域,资源被自动释放
return 0;
}
4.2.4 线程安全的引用计数:
对引用计数的操作是线程安全的。但需要注意,指向的对象内容本身不是线程安全的。
#include <iostream>
#include <memory>
#include <thread>
void threadFunction(std::shared_ptr<int> ptr) {
std::cout << "Thread value: " << *ptr << std::endl;
}
int main() {
std::shared_ptr<int> sharedData = std::make_shared<int>(20);
std::thread t1(threadFunction, sharedData);
std::thread t2(threadFunction, sharedData);
t1.join();
t2.join();
return 0;
}
4.2.5 使用方便:
可以通过 std::make_shared
来简化 shared_ptr
的创建,同时提高性能(减少内存分配次数)。
#include <iostream>
#include <memory>
struct Simple {
Simple() { std::cout << "Simple constructed" << std::endl; }
~Simple() { std::cout << "Simple destructed" << std::endl; }
};
int main() {
auto simplePtr = std::make_shared<Simple>(); // 自动创建 shared_ptr
return 0; // 自动清理
}
4.2.6 支持自定义删除器:
可以通过构造函数提供自定义删除器,以定义对象如何被释放,适用于特殊的资源管理需求。
#include <iostream>
#include <memory>
struct CustomDeleter {
void operator()(int* p) {
std::cout << "Custom delete for " << *p << std::endl;
delete p;
}
};
int main() {
std::shared_ptr<int> ptr(new int(42), CustomDeleter());
return 0; // 会调用 CustomDeleter
}
4.2.7std::weak_ptr 结合使用:
可与 std::weak_ptr
一起使用,以打破循环引用的问题,weak_ptr
不增加引用计数。
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next;
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
std::shared_ptr<Node> head = std::make_shared<Node>();
std::weak_ptr<Node> weakHead = head; // weak_ptr 不增加引用计数
head->next = std::make_shared<Node>(); // 创建另一个 Node
std::cout << "Reference count of head: " << head.use_count() << std::endl; // 输出 2
head.reset(); // 释放 shared_ptr
if (auto temp = weakHead.lock()) {
std::cout << "Node is alive." << std::endl;
} else {
std::cout << "Node has been deleted." << std::endl; // 输出
}
return 0;
}
五、弱引用智能指针:weak_ptr
std::weak_ptr
是 C++
标准库中的一个智能指针,旨在解决与 std::shared_ptr
相关的循环依赖问题。它提供了一种方式来观察共享对象,但不会增加其引用计数,从而避免了循环引用造成的内存泄漏。
5.1 循环依赖
循环依赖问题通常发生在两个或多个类相互持有对方的引用,这会导致它们之间的引用计数无法归零,从而引发内存泄漏。如下实例:我们创建了两个类 A
和 B
,它们互相指向对方的实例。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b; // 拥有者指针
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a; // 拥有者指针
B() { std::cout << "B created" << std::endl; }
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b; // A 拥有 B
b->a = a; // B 拥有 A
return 0; // 当程序结束时,A 和 B 永远无法被销毁
}
在上述代码中,A
类拥有 B
的一个 shared_ptr
,而 B
类也拥有 A
的一个 shared_ptr
。这样就形成了一种循环依赖关系:
a
的引用计数为 1(指向 A
),但因为它持有 b
(B
的引用),所以 B
的引用计数也为 1。
b
的引用计数为 1(指向 B
),但因为它持有 a
(A
的引用),所以 A
的引用计数也为 1。
由于这两个类互相持有对方的 shared_ptr
,引用计数永远不会归零,导致内存无法释放。
了解决这个问题,我们可以将其中一个 shared_ptr
改为 weak_ptr
。通常,持有较少使用的引用或者想要避免循环引用的类会使用 weak_ptr
。以下是修改后的代码示例:
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b; // 拥有者指针
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a; // 使用 weak_ptr 避免循环引用
B() { std::cout << "B created" << std::endl; }
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b; // A 拥有 B
b->a = a; // B 使用 weak_ptr 指向 A
return 0; // 当程序结束时,A 和 B 将会被正确销毁
}
5.2 weak_ptr 的特性
5.2.1 不增加引用计数:
std::weak_ptr
拥有一个指向 std::shared_ptr
管理的对象的弱引用。与 std::shared_ptr
不同,它不会增加对象的引用计数。这意味着,std::weak_ptr
仅仅作为对对象的观察者,使用它不会阻止对象的销毁。
#include <iostream>
#include <memory>
class A {
public:
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>(); // 创建一个 shared_ptr
std::weak_ptr<A> p2 = p1; // 从 shared_ptr 创建一个 weak_ptr
std::cout << "p1 use count: " << p1.use_count() << std::endl; // 输出: 1
std::cout << "p2 expired: " << p2.expired() << std::endl; // 输出: 0 (false)
return 0;
}
5.2.2 防止循环引用:
std::weak_ptr
是解决循环引用问题的关键。通过将某个类的某些指针(通常是指向负责管理资源的类的指针)定义为 weak_ptr
,可以打破这种相互依赖关系,允许资源被正确释放。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> bPtr; // 使用 shared_ptr
};
class B {
public:
std::weak_ptr<A> aPtr; // 使用 weak_ptr 防止循环引用
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->bPtr = b;
b->aPtr = a; // 不会引起循环引用
return 0; // 程序结束时 A 和 B 的资源会被正确释放
}
5.2.3 可以转换为 shared_ptr:
std::weak_ptr
提供了一个 lock()
方法,可以将其转换为 std::shared_ptr
,如果原始对象仍然存在(即其引用计数大于零),lock()
方法会返回一个有效的 std::shared_ptr
。如果对象已被销毁,则返回一个空的 shared_ptr
。
#include <iostream>
#include <memory>
class A {
public:
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>();
std::weak_ptr<A> p2 = p1;
std::shared_ptr<A> p3 = p2.lock(); // 尝试获取 shared_ptr
if (p3) {
std::cout << "p3 acquired" << std::endl; // 将会输出
}
p1.reset(); // 释放 p1 的资源
std::shared_ptr<A> p4 = p2.lock(); // 尝试再次获取 shared_ptr
if (!p4) {
std::cout << "p4 is null" << std::endl; // 将会输出
}
return 0;
}
5.2.4 允许检查资源状态:
通过使用 std::weak_ptr
,可以检查资源对象的状态。可以使用 expired()
方法来检查指向的对象是否已经被销毁。如果返回 true
,则表明对象已不再存在。
#include <iostream>
#include <memory>
class A {
public:
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::weak_ptr<A> p1;
{
std::shared_ptr<A> p2 = std::make_shared<A>();
p1 = p2; // p1 指向 p2
std::cout << "p1 expired: " << p1.expired() << std::endl; // 输出: 0 (false)
}
std::cout << "p1 expired after exiting scope: " << p1.expired() << std::endl; // 输出: 1 (true)
return 0;
}
5.2.5 与 shared_ptr 共享同一控制块:
std::weak_ptr
与其对应的 std::shared_ptr
共享同一个控制块,这个控制块保存着引用计数和其他状态信息。这允许 weak_ptr 检查原始对象的状态。
#include <iostream>
#include <memory>
class A {
public:
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>();
std::weak_ptr<A> p2 = p1; // p2 和 p1 共享同一控制块
std::cout << "p1 use count: " << p1.use_count() << std::endl; // 输出: 1
std::cout << "p2 use count (via shared_ptr): " << p2.lock().use_count() << std::endl; // 输出: 1
return 0;
}
5.2.6 线程安全:
std::weak_ptr
和 std::shared_ptr
的操作是线程安全的。可以在多个线程中安全地访问和管理这些智能指针。
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <chrono>
class A {
public:
A() { std::cout << "A created" << std::endl; }
~A() { std::cout << "A destroyed" << std::endl; }
};
void threadFunction(std::weak_ptr<A> weakPtr) {
// 尝试从 weak_ptr 获取 shared_ptr
std::shared_ptr<A> sharedPtr = weakPtr.lock();
if (sharedPtr) {
std::cout << "Thread accessing object" << std::endl;
} else {
std::cout << "Object already destroyed" << std::endl;
}
}
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>();
// 创建一个 weak_ptr 来观察 p1
std::weak_ptr<A> p2 = p1;
// 启动多个线程来访问 p2
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(threadFunction, p2);
}
// 等待一段时间然后重置 p1,模拟对象的销毁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
p1.reset(); // 释放 p1 的资源
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
🎈①代码说明:
class A
:定义了一个简单的类 A
,其构造和析构函数会打印消息,以便我们跟踪对象的创建与销毁过程。
threadFunction
:每个线程执行的函数,尝试通过调用 weakPtr.lock()
获取对象的 shared_ptr
:
如果成功获取 shared_ptr
,表示对象仍然存在,线程可以安全访问该对象。
如果对象已经被销毁,lock()
返回空指针,线程会打印相关消息。
🎈②主函数中的逻辑:
创建一个 std::shared_ptr<A>
实例 p1,并从中获得一个 std::weak_ptr<A>
实例 p2
。
启动多个线程,每个线程都尝试访问同一个 weak_ptr
。
主线程等待一段时间,然后重置 p1
,模拟对象的销毁。
等待所有线程结束执行。
🎈③线程安全注意事项:
使用 std::weak_ptr
来访问 std::shared_ptr
,避免了因对象销毁而引起的悬挂指针。
weak_ptr.lock()
是线程安全的,可以安全地用于多个线程同时访问的场景。
在整个程序运行过程中,你将看到某些线程能够访问对象,而在对象被销毁后,其他线程则会看到对象已经被销毁。