std::shared_ptr 是 C++11 引入的智能指针,核心功能是通过引用计数实现多个指针共享同一对象的生命周期,当最后一个 shared_ptr 销毁时,自动释放管理的资源(避免内存泄漏)。本文从基础用法到高级陷阱,结合代码逐步讲解。
一、入门:shared_ptr 基本用法
1.1 核心原理:引用计数
shared_ptr 内部维护一个引用计数(reference count):
- 当新的
shared_ptr指向同一个对象时,引用计数 +1; - 当
shared_ptr销毁(如超出作用域)或指向其他对象时,引用计数 -1; - 当引用计数变为 0 时,自动调用析构函数释放对象。
1.2 创建 shared_ptr
最安全的方式是使用 std::make_shared(推荐),也可以通过原始指针构造(需谨慎)。
#include <memory> // 包含 shared_ptr
#include <iostream>
int main() {
// 方式1:用 make_shared 创建(推荐)
// 功能:分配 int 对象(值为100),并让 sp1 管理它,引用计数=1
std::shared_ptr<int> sp1 = std::make_shared<int>(100);
// 方式2:用原始指针构造(不推荐,见后文陷阱)
int* raw_ptr = new int(200);
std::shared_ptr<int> sp2(raw_ptr);
// 方式3:空指针初始化
std::shared_ptr<int> sp3; // 空,引用计数=0
std::shared_ptr<int> sp4(nullptr); // 空,引用计数=0
return 0;
}
为什么推荐 make_shared?
make_shared 会一次性分配对象内存和引用计数内存(效率更高),而用原始指针构造需要两次分配(先 new 对象,再 shared_ptr 分配计数内存)。此外,make_shared 能避免异常安全问题(见后文)。
1.3 访问管理的对象
通过 * 解引用或 -> 访问成员(与普通指针用法一致),也可通过 get() 获取原始指针(谨慎使用)。
int main() {
auto sp = std::make_shared<int>(10);
std::cout << *sp << std::endl; // 解引用:输出 10
*sp = 20; // 修改值
std::cout << *sp << std::endl; // 输出 20
// 获取原始指针(仅临时使用,不要用它构造新的 shared_ptr)
int* raw = sp.get();
std::cout << *raw << std::endl; // 输出 20
return 0;
}
1.4 拷贝与赋值:引用计数的变化
当 shared_ptr 被拷贝或赋值时,引用计数会自动更新。
int main() {
auto sp1 = std::make_shared<int>(100);
std::cout << "sp1 引用计数: " << sp1.use_count() << std::endl; // 输出 1
// 拷贝:sp2 与 sp1 共享对象,引用计数+1
std::shared_ptr<int> sp2 = sp1;
std::cout << "sp1 引用计数: " << sp1.use_count() << std::endl; // 输出 2
std::cout << "sp2 引用计数: " << sp2.use_count() << std::endl; // 输出 2
// 赋值:sp3 原本为空,赋值后与 sp1 共享,引用计数+1
std::shared_ptr<int> sp3;
sp3 = sp1;
std::cout << "sp1 引用计数: " << sp1.use_count() << std::endl; // 输出 3
// sp3 指向新对象:原对象引用计数-1(变为2)
sp3 = std::make_shared<int>(200);
std::cout << "sp1 引用计数: " << sp1.use_count() << std::endl; // 输出 2
return 0;
}
注意:use_count() 仅用于调试,实际开发中不要依赖它判断对象是否存活(性能差且可能有延迟)。
1.5 自动释放资源
当最后一个 shared_ptr 销毁时,对象自动释放。
int main() {
{
auto sp1 = std::make_shared<int>(10);
auto sp2 = sp1; // 引用计数=2
// 作用域结束,sp1、sp2 销毁,引用计数变为0,int对象被释放
}
// 此处 int 对象已被自动释放,无需手动 delete
return 0;
}
二、进阶:shared_ptr 实用场景
2.1 作为函数参数/返回值
shared_ptr 可以安全地作为函数参数或返回值,避免“悬挂指针”问题。
// 函数参数:值传递(会增加引用计数)
void func1(std::shared_ptr<int> sp) {
std::cout << "func1 内引用计数: " << sp.use_count() << std::endl; // 2
}
// 函数参数:引用传递(不增加引用计数,适合只读场景)
void func2(const std::shared_ptr<int>& sp) {
std::cout << "func2 内引用计数: " << sp.use_count() << std::endl; // 1
}
// 函数返回值:返回 shared_ptr
std::shared_ptr<int> create_int(int value) {
return std::make_shared<int>(value); // 自动管理生命周期
}
int main() {
auto sp = std::make_shared<int>(10);
std::cout << "调用前引用计数: " << sp.use_count() << std::endl; // 1
func1(sp); // 传值,计数临时+1
func2(sp); // 传引用,计数不变
auto sp2 = create_int(20); // 接收返回值,计数=1
return 0;
}
建议:函数参数优先用 const & 传递(减少计数操作的开销),仅在需要延长对象生命周期时传值。
2.2 管理数组(需自定义删除器)
shared_ptr 默认用 delete 释放资源,但数组需要 delete[],因此管理数组时必须指定自定义删除器。
int main() {
// 错误示例:默认删除器用 delete,数组会释放错误
// std::shared_ptr<int> sp_bad(new int[10]); // 析构时行为未定义
// 正确方式1:用 lambda 作为删除器(调用 delete[])
std::shared_ptr<int> sp1(new int[10], [](int* p) {
delete[] p;
std::cout << "数组已释放\n";
});
// 正确方式2:用 std::default_delete(标准删除器)
std::shared_ptr<int> sp2(new int[10], std::default_delete<int[]>());
return 0;
}
C++20 简化:可直接用 std::make_shared 配合数组(需指定数组类型):
auto sp = std::make_shared<int[]>(10);(自动用 delete[])。
2.3 管理非内存资源(如文件、网络连接)
shared_ptr 不仅能管理内存,还能管理任何需要手动释放的资源(如文件句柄、网络套接字),只需通过自定义删除器指定释放逻辑。
#include <cstdio> // FILE, fopen, fclose
int main() {
// 管理 FILE*:删除器调用 fclose
std::shared_ptr<FILE> file(
fopen("test.txt", "w"),
[](FILE* f) {
if (f) fclose(f);
std::cout << "文件已关闭\n";
}
);
if (file) { // 判断是否有效(类似指针判空)
fputs("hello", file.get()); // 用 get() 获取原始句柄
}
// 作用域结束时,file 销毁,自动调用 fclose 关闭文件
return 0;
}
三、高级:陷阱与解决方案
3.1 陷阱1:循环引用(导致内存泄漏)
当两个对象互相持有对方的 shared_ptr 时,引用计数永远不会减到 0,导致内存泄漏。
struct A;
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 被销毁\n"; }
};
struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B 被销毁\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B 的 shared_ptr
b->a_ptr = a; // B 持有 A 的 shared_ptr
// 此时 a 和 b 的引用计数都是 2
// 作用域结束时,a 和 b 销毁,计数变为 1(互相引用),不会调用析构函数
// 内存泄漏!
return 0;
}
解决方案:用 std::weak_ptr 打破循环(weak_ptr 不增加引用计数)。
#include <memory>
struct A;
struct B;
struct A {
std::weak_ptr<B> b_ptr; // 用 weak_ptr 替代 shared_ptr
~A() { std::cout << "A 被销毁\n"; }
};
struct B {
std::weak_ptr<A> a_ptr; // 用 weak_ptr 替代 shared_ptr
~B() { std::cout << "B 被销毁\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 作用域结束时,a 和 b 计数减为 0,析构函数被调用
// 输出:A 被销毁 B 被销毁
return 0;
}
3.2 陷阱2:重复托管同一原始指针
若将同一个原始指针交给多个 shared_ptr 管理,会导致重复释放(未定义行为,通常崩溃)。
int main() {
int* raw = new int(10);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw); // 错误!sp1 和 sp2 各自管理 raw,会释放两次
return 0; // 程序崩溃
}
解决方案:
- 始终用
make_shared创建,避免手动管理原始指针; - 若必须用原始指针,确保只交给一个
shared_ptr托管,其他通过拷贝该shared_ptr共享。
3.3 陷阱3:用 get() 返回的原始指针构造新 shared_ptr
get() 返回的原始指针仅用于临时访问,若用它构造新的 shared_ptr,会导致重复释放。
int main() {
auto sp1 = std::make_shared<int>(10);
int* raw = sp1.get();
std::shared_ptr<int> sp2(raw); // 错误!sp1 和 sp2 会重复释放 raw
return 0; // 崩溃
}
3.4 线程安全性
shared_ptr 的引用计数操作是线程安全的(内部用原子操作),但指向的对象本身不是线程安全的。
#include <thread>
void increment(std::shared_ptr<int> sp) {
for (int i = 0; i < 1000; ++i) {
(*sp)++; // 对对象的修改不是线程安全的,需加锁
}
}
int main() {
auto sp = std::make_shared<int>(0);
std::thread t1(increment, sp);
std::thread t2(increment, sp);
t1.join();
t2.join();
std::cout << *sp << std::endl; // 可能不是 2000(因为无同步)
return 0;
}
解决方案:对 shared_ptr 指向的对象操作时,用互斥锁(std::mutex)同步。
四、最佳实践
-
优先用
make_shared:效率更高,且避免异常安全问题(如new后抛异常导致内存泄漏)。
反例(不安全):shared_ptr<int> sp(new int, deleter);(若new成功但shared_ptr构造失败,内存泄漏)。 -
避免手动管理原始指针:不要用
get()返回的指针长期持有对象,更不要用它构造新shared_ptr。 -
用
weak_ptr解决循环引用:当两个对象需要互相引用时,一方用weak_ptr。 -
管理数组时必须指定删除器:C++17 前需手动加
delete[],C++20 可用make_shared<int[]>。 -
不要将
shared_ptr存为裸指针:如int* p = &*sp;(若sp释放,p变为悬挂指针)。
总结
std::shared_ptr 是管理共享资源的强大工具,核心通过引用计数自动释放资源。入门需掌握创建、拷贝、访问;进阶需理解数组管理、非内存资源;高级需规避循环引用、重复托管等陷阱。遵循最佳实践可最大化其安全性和效率。
5道hard难度多选题
题目1(循环引用与解决)
已知存在如下类结构,A和B互相持有对方的shared_ptr导致循环引用(内存泄漏):
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };
下列哪些方法能有效解决该循环引用问题?(多选)
A. 将A中b的类型改为std::weak_ptr<B>
B. 将B中a的类型改为std::weak_ptr<A>
C. 在main函数结束前手动将a->b和b->a置为nullptr
D. 改用std::unique_ptr代替std::shared_ptr
E. 增加一个中间类C,让A和B都持有C的shared_ptr
题目2(线程安全性)
关于std::shared_ptr的线程安全性,下列说法正确的有?(多选)
A. 多个线程同时对同一个shared_ptr进行拷贝/赋值(修改引用计数)是线程安全的
B. 多个线程同时通过shared_ptr访问其指向的对象(如读写数据)是线程安全的
C. 一个线程修改shared_ptr指向的对象,另一个线程同时销毁该shared_ptr(导致对象释放)可能引发未定义行为
D. std::shared_ptr的use_count()方法返回的引用计数在多线程环境下是实时准确的
E. 用std::atomic_shared_ptr(C++20)可以安全地在多线程中修改shared_ptr的指向
题目3(自定义删除器)
下列关于std::shared_ptr自定义删除器的说法,正确的有?(多选)
A. 自定义删除器的类型会影响std::shared_ptr的类型(如shared_ptr<int>与shared_ptr<int, Deleter>是不同类型)
B. 多个shared_ptr共享同一对象时,只要其中一个指定了删除器,其他shared_ptr无需重复指定
C. 若删除器抛出异常,shared_ptr析构时会导致程序终止(调用std::terminate)
D. 管理动态数组时,必须显式指定删除器(如delete[]),否则会导致未定义行为(C++17及之前)
E. 自定义删除器可以捕获外部变量(如通过lambda捕获),且不会影响shared_ptr的大小
题目4(构造与赋值陷阱)
下列代码中可能导致未定义行为(如崩溃、内存泄漏)的有?(多选)
A.
int* p = new int(10);
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p); // 用同一原始指针构造两个shared_ptr
B.
auto sp = std::make_shared<int>(10);
int* raw = sp.get();
delete raw; // 手动释放get()返回的原始指针
C.
std::shared_ptr<int> sp;
{
int x = 10;
sp = std::shared_ptr<int>(&x); // 托管栈上对象
}
D.
std::shared_ptr<int> get_sp() {
int* p = new int(10);
throw std::runtime_error("error"); // 抛出异常
return std::shared_ptr<int>(p); // 永远无法执行
}
E.
struct Data { int x; };
auto sp1 = std::make_shared<Data>();
std::shared_ptr<int> sp2(sp1, &sp1->x); // 别名构造:共享控制块,指向成员x
题目5(异常安全与控制块)
关于std::shared_ptr的控制块(存储引用计数、删除器等)和异常安全,下列说法正确的有?(多选)
A. std::make_shared比std::shared_ptr(new T)更高效,因为它能一次性分配对象内存和控制块内存
B. 若std::shared_ptr(new T)中new T成功但shared_ptr构造失败(如内存不足),会导致T对象内存泄漏
C. 控制块中除了引用计数,还可能存储“弱引用计数”(供weak_ptr使用)
D. 当最后一个shared_ptr销毁后,控制块会立即被释放,无论是否还有weak_ptr指向它
E. 函数参数按值传递shared_ptr时,若在传递过程中抛出异常,引用计数会正确回滚
答案与详解
题目1答案:ABC
解析:
- 循环引用的核心是“互相持有
shared_ptr导致引用计数无法归零”。 - A、B正确:
weak_ptr不增加引用计数,打破循环(只需一方或双方改为weak_ptr即可)。 - C正确:手动置空
a->b和b->a会减少引用计数,使计数归0,对象正常释放。 - D错误:
unique_ptr无法共享所有权,若A和B需要互相引用,unique_ptr会导致所有权冲突(无法拷贝)。 - E错误:增加中间类
C仍可能形成更长的循环链(如A->C->B->A),无法解决根本问题。
题目2答案:ACE
解析:
- A正确:
shared_ptr的引用计数操作通过原子操作实现,多线程拷贝/赋值(修改计数)是线程安全的。 - B错误:
shared_ptr仅保证自身引用计数的线程安全,其指向的对象本身无同步机制,多线程访问需额外加锁(如mutex)。 - C正确:若一个线程正在读写对象,另一个线程销毁
shared_ptr导致对象释放,会引发“悬垂指针”访问,属于未定义行为。 - D错误:
use_count()返回的是“近似值”,多线程环境下可能因延迟导致不准确(标准不保证实时性),仅用于调试。 - E正确:
std::atomic_shared_ptr(C++20)提供原子操作接口,可安全地在多线程中修改shared_ptr的指向。
题目3答案:BCD
解析:
- A错误:
shared_ptr的类型仅由托管对象类型决定(如shared_ptr<int>),与删除器类型无关(删除器存储在控制块中,类型被擦除)。 - B正确:多个
shared_ptr共享同一对象时,控制块中仅存储一个删除器(由第一个构造的shared_ptr指定),其他shared_ptr共享该删除器。 - C正确:C++标准规定,若删除器抛出异常,
shared_ptr析构时会调用std::terminate(因析构函数不能传播异常)。 - D正确:C++17及之前,
shared_ptr默认用delete释放资源,管理数组时需显式指定delete[]删除器(如shared_ptr<int>(new int[10], std::default_delete<int[]>()));C++20后make_shared<int[]>可自动处理。 - E错误:若删除器通过lambda捕获外部变量,可能增加
shared_ptr的大小(控制块需存储捕获的数据),而无捕获的lambda或函数指针不会影响大小。
题目4答案:ABCD
解析:
- A错误:两个
shared_ptr独立管理同一原始指针,会导致对象被释放两次(未定义行为,通常崩溃)。 - B错误:
get()返回的原始指针由shared_ptr托管,手动delete会导致shared_ptr析构时再次释放,引发重复释放。 - C错误:
shared_ptr托管栈上对象(&x),当x超出作用域后,shared_ptr析构时会对栈内存调用delete,属于未定义行为。 - D错误:
new int成功分配内存后,函数抛出异常,导致shared_ptr无法构造,内存泄漏(make_shared可避免此问题)。 - E正确:别名构造(
shared_ptr<int> sp2(sp1, &sp1->x))是合法的,sp2与sp1共享控制块(引用计数相同),但指向sp1管理对象的成员x,析构时仅当计数归0才释放Data对象,无问题。
题目5答案:ABCE
解析:
- A正确:
make_shared一次性分配“对象+控制块”的内存(一块连续内存),而shared_ptr(new T)需两次分配(先new T,再分配控制块),效率更低。 - B正确:
shared_ptr(new T)的执行顺序是:1.new T分配内存;2. 构造shared_ptr(分配控制块)。若步骤2失败(如内存不足),步骤1分配的T对象无法被释放,导致泄漏;make_shared可避免此问题(因分配是原子的)。 - C正确:控制块中通常包含两个计数:“强引用计数”(
shared_ptr数量)和“弱引用计数”(weak_ptr数量+1,用于判断控制块是否可释放)。 - D错误:当最后一个
shared_ptr销毁(强引用计数归0),对象会被释放,但控制块需等到最后一个weak_ptr销毁(弱引用计数归0)才会释放。 - E正确:按值传递
shared_ptr时,参数拷贝会先增加引用计数,若传递过程中抛出异常,拷贝的临时shared_ptr会析构,引用计数正确回滚(减少1),无泄漏。
5道hard难度代码实践题
题目1:修复循环引用导致的内存泄漏
问题描述:以下代码中,Node类的对象通过prev和next指针形成双向链表,但存在内存泄漏(析构函数未被调用)。请修改代码,确保所有Node对象能被正确释放。
#include <memory>
#include <iostream>
struct Node {
int value;
std::shared_ptr<Node> prev; // 前驱节点
std::shared_ptr<Node> next; // 后继节点
Node(int v) : value(v) {
std::cout << "Node " << value << " 构造\n";
}
~Node() {
std::cout << "Node " << value << " 析构\n";
}
};
// 创建一个包含3个节点的双向链表(1 <-> 2 <-> 3)
void create_list() {
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
auto node3 = std::make_shared<Node>(3);
node1->next = node2;
node2->prev = node1;
node2->next = node3;
node3->prev = node2;
} // 函数结束后,所有Node对象应被析构
int main() {
create_list();
return 0;
}
题目2:正确管理动态数组(自定义删除器)
问题描述:以下代码试图用std::shared_ptr管理动态int数组,但存在内存释放错误(未定义行为)。请修改代码,确保数组能被正确释放(调用delete[]),并在释放时打印“数组已释放”。
#include <memory>
#include <iostream>
int main() {
// 错误示例:用shared_ptr管理动态数组,但释放方式不正确
std::shared_ptr<int> arr_ptr(new int[5]{1, 2, 3, 4, 5});
// 访问数组元素(正确)
for (int i = 0; i < 5; ++i) {
std::cout << arr_ptr.get()[i] << " ";
}
std::cout << std::endl;
return 0;
} // 此处数组释放方式错误(用delete而非delete[])
题目3:修复线程安全问题
问题描述:以下代码中,多个线程同时通过shared_ptr修改其指向的int值,导致结果不正确(预期结果为20000,但实际可能更小)。请修改代码,确保线程安全,使最终结果正确。
#include <memory>
#include <thread>
#include <iostream>
// 线程函数:对shared_ptr指向的int累加10000次
void add_task(std::shared_ptr<int> sp) {
for (int i = 0; i < 10000; ++i) {
(*sp)++; // 线程不安全的操作
}
}
int main() {
auto sp = std::make_shared<int>(0);
std::thread t1(add_task, sp);
std::thread t2(add_task, sp);
t1.join();
t2.join();
std::cout << "最终结果:" << *sp << std::endl; // 预期20000,实际可能不符
return 0;
}
题目4:确保异常安全(避免内存泄漏)
问题描述:以下代码中,create_resource函数试图创建一个资源并返回shared_ptr,但在某些情况下会导致内存泄漏。请修改代码,确保即使发生异常,资源也不会泄漏。
#include <memory>
#include <stdexcept>
#include <iostream>
struct Resource {
Resource() { std::cout << "Resource 构造\n"; }
~Resource() { std::cout << "Resource 析构\n"; }
};
// 可能导致内存泄漏的函数
std::shared_ptr<Resource> create_resource(bool throw_error) {
Resource* res = new Resource(); // 手动分配资源
if (throw_error) {
throw std::runtime_error("创建资源失败"); // 抛出异常,导致shared_ptr未构造
}
return std::shared_ptr<Resource>(res); // 正常情况下返回shared_ptr
}
int main() {
try {
// 测试异常场景:预期Resource被正确释放
auto sp = create_resource(true);
} catch (const std::exception& e) {
std::cout << "捕获异常:" << e.what() << std::endl;
}
return 0;
} // 此处存在内存泄漏(Resource未被析构)
题目5:理解别名构造的资源释放逻辑
问题描述:以下代码使用shared_ptr的别名构造(aliasing constructor)创建sp2,但对sp2的生命周期存在误解。请分析代码输出,并解释Data对象何时被释放,以及如何修改代码使Data对象在sp2销毁时立即释放。
#include <memory>
#include <iostream>
struct Data {
int x;
Data() : x(0) { std::cout << "Data 构造\n"; }
~Data() { std::cout << "Data 析构\n"; }
};
int main() {
auto sp1 = std::make_shared<Data>(); // sp1管理Data对象
std::shared_ptr<int> sp2(sp1, &sp1->x); // 别名构造:sp2共享sp1的控制块,指向x
sp1.reset(); // 释放sp1对Data的引用
// 此时sp2是否还持有Data对象?Data何时析构?
std::cout << "sp2是否有效:" << (sp2 ? "是" : "否") << std::endl;
std::cout << "sp2指向的值:" << *sp2 << std::endl;
// 如何修改才能让Data在sp2销毁时立即释放?
return 0; // sp2销毁,Data是否在此处析构?
}
答案与详解
题目1:修复循环引用
问题分析:
Node对象通过prev和next互相持有shared_ptr,形成循环引用(如node2->prev持有node1,node1->next持有node2),导致引用计数无法归0,析构函数不被调用,内存泄漏。
修复方案:
将其中一个方向的指针改为std::weak_ptr(不增加引用计数),打破循环。通常选择“前驱指针”或“后继指针”中的一个改为weak_ptr。
修改后代码:
#include <memory>
#include <iostream>
struct Node {
int value;
std::weak_ptr<Node> prev; // 改为weak_ptr,不增加引用计数
std::shared_ptr<Node> next;
Node(int v) : value(v) {
std::cout << "Node " << value << " 构造\n";
}
~Node() {
std::cout << "Node " << value << " 析构\n";
}
};
void create_list() {
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
auto node3 = std::make_shared<Node>(3);
node1->next = node2;
node2->prev = node1; // weak_ptr赋值,不增加node1的计数
node2->next = node3;
node3->prev = node2; // weak_ptr赋值,不增加node2的计数
} // 函数结束时,所有shared_ptr销毁,计数归0,析构函数被调用
int main() {
create_list();
return 0;
}
输出:
Node 1 构造
Node 2 构造
Node 3 构造
Node 3 析构
Node 2 析构
Node 1 析构
解析:
weak_ptr不参与引用计数,因此node2->prev持有node1不会增加node1的计数。当create_list函数结束时,node1、node2、node3的shared_ptr销毁,引用计数依次归0,对象正常析构。
题目2:正确管理动态数组
问题分析:
std::shared_ptr默认使用delete释放资源,但动态数组需用delete[],默认删除器会导致未定义行为(通常表现为崩溃或内存泄漏)。
修复方案:
显式指定删除器(调用delete[]),或在C++20及以上使用std::make_shared<int[]>。
修改后代码(C++17及之前):
#include <memory>
#include <iostream>
int main() {
// 方案1:用lambda作为删除器,调用delete[]并打印信息
std::shared_ptr<int> arr_ptr(
new int[5]{1, 2, 3, 4, 5},
[](int* p) {
delete[] p;
std::cout << "数组已释放" << std::endl;
}
);
// 方案2(C++17及之前):用std::default_delete<int[]>
// std::shared_ptr<int> arr_ptr(new int[5]{1,2,3,4,5}, std::default_delete<int[]>());
// 方案3(C++20及之后):用make_shared<int[]>
// auto arr_ptr = std::make_shared<int[]>(5);
// arr_ptr[0] = 1; arr_ptr[1] = 2; ...
for (int i = 0; i < 5; ++i) {
std::cout << arr_ptr.get()[i] << " ";
}
std::cout << std::endl;
return 0;
}
输出:
1 2 3 4 5
数组已释放
解析:
自定义删除器确保数组用delete[]释放,符合动态数组的内存管理规则。C++20的std::make_shared<int[]>会自动使用delete[],无需手动指定删除器。
题目3:修复线程安全问题
问题分析:
shared_ptr的引用计数操作是线程安全的,但它指向的对象(此处为int)的读写操作无同步机制,导致多线程同时修改时出现“竞态条件”(结果不正确)。
修复方案:
使用std::mutex对int的修改操作进行同步,确保每次只有一个线程能修改值。
修改后代码:
#include <memory>
#include <thread>
#include <iostream>
#include <mutex>
std::mutex mtx; // 互斥锁,同步对int的访问
void add_task(std::shared_ptr<int> sp) {
for (int i = 0; i < 10000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 加锁,确保操作原子性
(*sp)++;
}
}
int main() {
auto sp = std::make_shared<int>(0);
std::thread t1(add_task, sp);
std::thread t2(add_task, sp);
t1.join();
t2.join();
std::cout << "最终结果:" << *sp << std::endl; // 正确输出20000
return 0;
}
输出:
最终结果:20000
解析:
std::lock_guard在每次修改*sp时加锁,确保同一时间只有一个线程能执行(*sp)++,避免了竞态条件。shared_ptr本身的引用计数在多线程拷贝时是安全的,但指向的对象需要额外同步。
题目4:确保异常安全
问题分析:
create_resource函数中,new Resource()先分配内存,若后续抛出异常,shared_ptr未被构造,导致Resource对象无法释放,内存泄漏。
修复方案:
使用std::make_shared创建shared_ptr,它能在分配资源的同时构造shared_ptr,确保异常发生时资源被自动释放。
修改后代码:
#include <memory>
#include <stdexcept>
#include <iostream>
struct Resource {
Resource() { std::cout << "Resource 构造\n"; }
~Resource() { std::cout << "Resource 析构\n"; }
};
// 异常安全的版本:用make_shared确保资源不泄漏
std::shared_ptr<Resource> create_resource(bool throw_error) {
auto sp = std::make_shared<Resource>(); // 先构造shared_ptr和资源
if (throw_error) {
throw std::runtime_error("创建资源失败"); // 即使抛出异常,sp析构时会释放资源
}
return sp;
}
int main() {
try {
auto sp = create_resource(true);
} catch (const std::exception& e) {
std::cout << "捕获异常:" << e.what() << std::endl;
}
return 0;
}
输出:
Resource 构造
Resource 析构
捕获异常:创建资源失败
解析:
std::make_shared<Resource>()会一次性完成“资源分配+shared_ptr构造”,若后续抛出异常,局部变量sp会被销毁,其管理的Resource对象也会被释放,避免泄漏。而原代码中new Resource()和shared_ptr构造是分离的,异常会导致中间状态的资源泄漏。
题目5:别名构造的资源释放逻辑
问题分析:
别名构造(std::shared_ptr<int> sp2(sp1, &sp1->x))创建的sp2与sp1共享同一控制块(引用计数相同),但指向Data对象的成员x。即使sp1被reset(),sp2仍持有控制块,导致Data对象的引用计数不为0,不会析构。
原代码输出:
Data 构造
sp2是否有效:是
sp2指向的值:0
Data 析构
解析:
sp2与sp1共享控制块,引用计数初始为1。sp1.reset()后,引用计数仍为1(由sp2持有),因此Data对象未析构。- 当
main函数结束,sp2销毁,引用计数归0,Data对象才会析构。
修改需求:使Data对象在sp2销毁时立即释放(即sp2单独管理Data的生命周期)。
修改方案:
不使用别名构造,而是让sp2直接管理Data对象的指针(需确保&sp1->x的有效性依赖于Data对象的生命周期)。
修改后代码:
#include <memory>
#include <iostream>
struct Data {
int x;
Data() : x(0) { std::cout << "Data 构造\n"; }
~Data() { std::cout << "Data 析构\n"; }
};
int main() {
auto sp1 = std::make_shared<Data>();
// 不使用别名构造,sp2直接持有Data对象的shared_ptr,通过get()访问x
std::shared_ptr<Data> sp2 = sp1;
int* x_ptr = &sp2->x; // 直接获取x的指针
sp1.reset(); // sp2仍持有Data,计数为1
std::cout << "sp2是否有效:" << (sp2 ? "是" : "否") << std::endl;
std::cout << "sp2指向的值:" << *x_ptr << std::endl;
sp2.reset(); // 此时Data计数归0,立即析构
std::cout << "sp2已释放" << std::endl;
return 0;
}
输出:
Data 构造
sp2是否有效:是
sp2指向的值:0
Data 析构
sp2已释放
解析:
修改后sp2直接持有Data对象的shared_ptr,其生命周期与Data绑定。sp2.reset()时,引用计数归0,Data立即析构,满足“sp2销毁时释放Data”的需求。别名构造的核心是“共享控制块但指向不同对象”,适用于需要共享生命周期但访问不同成员的场景,但需注意资源释放时机。
补充: 10道多选题目
题目部分
-
关于
std::shared_ptr循环引用与std::weak_ptr的配合,下列场景中会导致内存泄漏的有(多选):
A. 类 A 实例持有类 B 实例的shared_ptr,类 B 实例持有类 A 实例的shared_ptr,无其他外部引用
B. 类 A 实例持有类 B 实例的shared_ptr,类 B 实例持有类 A 实例的weak_ptr,无其他外部引用
C. 类 A 实例持有类 B 实例的shared_ptr,类 B 实例持有类 C 实例的shared_ptr,类 C 实例持有类 A 实例的shared_ptr,无其他外部引用
D. 类 A 实例持有类 B 实例的weak_ptr,类 B 实例持有类 C 实例的weak_ptr,类 C 实例持有类 A 实例的weak_ptr,无其他外部引用 -
关于
std::shared_ptr自定义删除器,下列说法正确的有(多选):
A. 自定义删除器的类型不影响std::shared_ptr的大小(64 位平台始终为 8 字节)
B. 若删除器抛出异常,会导致未定义行为(UB)
C. 可以通过 lambda 表达式作为删除器,且捕获列表非空时仍能正常工作
D.std::shared_ptr<int> p(new int[10], [](int* p) { delete[] p; })中,删除器会正确释放数组内存,且p支持operator[](C++17 及以后) -
关于
std::shared_ptr的线程安全性,下列说法错误的有(多选):
A. 多个线程同时调用同一个shared_ptr的operator=(赋值操作)是线程安全的
B. 多个线程同时拷贝同一个shared_ptr(如auto p2 = p1)是线程安全的,因为引用计数的增减是原子操作
C. 多个线程同时通过shared_ptr访问(读/写)其指向的对象是线程安全的,无需额外同步
D. 多个线程同时调用同一个shared_ptr的reset()方法是线程安全的 -
关于
std::make_shared与std::shared_ptr直接构造,下列说法正确的有(多选):
A.std::make_shared<T>无法搭配自定义删除器使用
B.std::shared_ptr<T>(new T())会进行两次内存分配(一次给T对象,一次给引用计数控制块),而std::make_shared<T>()仅一次分配(对象与控制块连续存储)
C. 若T的构造函数抛出异常,std::make_shared<T>会自动释放已分配的内存,而std::shared_ptr<T>(new T())可能导致内存泄漏
D.std::make_shared<T[]>可直接创建数组类型的shared_ptr(C++17 及以后) -
关于
std::shared_ptr对数组的支持(C++17 及以后),下列说法错误的有(多选):
A.std::shared_ptr<int[]> p(new int[5])无需手动指定删除器,会自动调用delete[]
B.p[2] = 10合法,支持数组下标访问
C.std::shared_ptr<int> p(new int[5])会自动调用delete[]释放内存
D.std::make_shared<int[]>(5)可创建包含 5 个默认初始化int的数组shared_ptr -
关于
std::shared_ptr的所有权转移(std::move),下列说法正确的有(多选):
A.std::shared_ptr<int> p1 = std::make_shared<int>(10); std::shared_ptr<int> p2 = std::move(p1);执行后,p1变为空指针(p1 == nullptr)
B. 所有权转移后,原shared_ptr的引用计数会减 1,目标shared_ptr的引用计数会加 1
C.std::weak_ptr<int> wp = std::move(p1);合法,且会转移p1的所有权给wp
D.std::shared_ptr<int> p3; p3.swap(p2);执行后,p2变为p3原来的状态(空),p3获得原p2的所有权,引用计数不变 -
关于
std::weak_ptr的操作,下列说法错误的有(多选):
A.std::weak_ptr<int> wp; auto sp = wp.lock();执行后,sp为空shared_ptr,且wp的引用计数不会变化
B.wp.expired()用于判断所指向的对象是否已被释放,该操作是线程安全的
C.std::weak_ptr<int> wp = std::make_shared<int>(10);合法,且会使引用计数加 1
D. 可以直接通过wp->或*wp解引用访问对象 -
关于
std::shared_ptr与原始指针的交互,下列做法会导致未定义行为(UB)的有(多选):
A.int* raw = new int(10); std::shared_ptr<int> p1(raw); std::shared_ptr<int> p2(raw);
B.std::shared_ptr<int> p = std::make_shared<int>(10); int* raw = p.get(); delete raw;
C.std::shared_ptr<int> p; { auto p2 = std::make_shared<int>(10); p = p2; } int* raw = p.get();(后续无p的修改操作)
D.std::shared_ptr<int> p = std::make_shared<int>(10); foo(p.get());(foo函数仅读取指针指向的值,不存储指针) -
关于
std::shared_ptr的大小(64 位平台),下列说法正确的有(多选):
A. 空std::shared_ptr的大小为 8 字节(仅存储一个空指针)
B.std::shared_ptr<int> p(new int, [](int* p) { delete p; })(捕获为空的 lambda 删除器)的大小为 8 字节
C.std::shared_ptr<int> p(new int, std::free)(函数指针删除器)的大小为 16 字节
D.std::shared_ptr的大小始终与std::unique_ptr相同 -
关于
std::shared_ptr的类型转换(static_pointer_cast/dynamic_pointer_cast/const_pointer_cast),下列说法正确的有(多选):
A.dynamic_pointer_cast失败时,返回空shared_ptr,不会抛出异常
B.static_pointer_cast<int>(std::make_shared<double>(3.14))是合法的,且转换后指向的内存安全
C.const_pointer_cast可将std::shared_ptr<const int>转换为std::shared_ptr<int>,去除const限定
D. 类型转换后,新shared_ptr与原shared_ptr共享引用计数
答案及详解
答案汇总
- AC 2. BC 3. ACD 4. AB 5. CD 6. AD 7. CD 8. AB 9. BC 10. ACD
详细解析
-
答案:AC
- 核心考点:循环引用导致引用计数无法归零。
- A 选项:A 和 B 互相持有
shared_ptr,形成闭环,引用计数均为 1,无外部引用时无法释放,内存泄漏。 - B 选项:B 持有 A 的
weak_ptr(不增加引用计数),A 的引用计数由 B 的shared_ptr维持,当外部引用消失,A 先释放,B 的weak_ptr过期,B 随后释放,无泄漏。 - C 选项:A→B→C→A 形成三角循环
shared_ptr,引用计数均为 1,无法释放,内存泄漏。 - D 选项:全为
weak_ptr(不影响引用计数),无外部引用时所有对象直接释放,无泄漏。
-
答案:BC
- 核心考点:自定义删除器的特性。
- A 错误:删除器类型影响
shared_ptr大小。64 位平台空shared_ptr为 8 字节(仅控制块指针),若删除器是函数指针(8 字节)或带捕获的 lambda,shared_ptr大小会增加(如 16 字节)。 - B 正确:C++ 标准规定,删除器(
operator())必须不抛出异常,否则导致 UB。 - C 正确:lambda 可作为删除器,捕获列表非空时,lambda 实例会被存储在
shared_ptr中,不影响功能。 - D 错误:
shared_ptr<int[]>支持operator[](C++17+),但该选项中删除器虽正确,operator[]的支持与删除器无关,且 D 选项描述“删除器会正确释放数组内存,且p支持operator[]”的逻辑关联错误(支持operator[]是数组特化的特性,而非删除器导致),且原始shared_ptr<int[]>无需手动指定数组删除器(C++17+ 自动匹配delete[]),故 D 整体错误。
-
答案:ACD
- 核心考点:
shared_ptr的线程安全边界(仅引用计数线程安全,对象本身不安全)。 - A 错误:多个线程同时对同一个
shared_ptr执行赋值(operator=)是写操作,非线程安全(shared_ptr本身的状态修改不原子)。 - B 正确:拷贝
shared_ptr仅涉及引用计数的原子增减,线程安全。 - C 错误:
shared_ptr仅保证引用计数的线程安全,其指向的对象的访问(读/写)需额外同步(如互斥锁),否则数据竞争。 - D 错误:多个线程同时调用同一个
shared_ptr的reset()(写操作),会修改shared_ptr本身的状态(指针+引用计数),非线程安全。
- 核心考点:
-
答案:AB
- 核心考点:
make_shared与直接构造的差异。 - A 正确:
make_shared无法指定自定义删除器(其返回的shared_ptr绑定默认删除器),需自定义删除器时必须直接构造。 - B 正确:直接构造
shared_ptr<T>(new T())分两次分配(对象 + 控制块),make_shared一次分配(对象与控制块连续存储,效率更高)。 - C 错误:
std::shared_ptr<T>(new T())中,若T的构造函数抛出异常,new T()分配的内存会泄漏(因为shared_ptr尚未构造完成,无法接管所有权);而make_shared中,内存分配在构造T之前,若构造抛出异常,已分配的内存会自动释放,故 C 描述颠倒。 - D 错误:
make_shared不支持数组类型(C++20 前),C++20 新增std::make_shared_for_overwrite<T[]>用于数组,但make_shared<T[]>仍不合法。
- 核心考点:
-
答案:CD
- 核心考点:C++17 后
shared_ptr的数组支持。 - A 正确:C++17 引入
shared_ptr<T[]>特化,自动匹配delete[],无需手动指定删除器。 - B 正确:
shared_ptr<T[]>重载了operator[],支持下标访问。 - C 错误:
std::shared_ptr<int> p(new int[5])是普通shared_ptr<int>,默认删除器为delete(而非delete[]),会导致数组内存释放不完整(UB)。 - D 错误:
make_shared不支持数组类型(C++20 前),std::make_shared<int[]>(5)编译失败,需用shared_ptr<int[]>(new int[5])。
- 核心考点:C++17 后
-
答案:AD
- 核心考点:
shared_ptr的所有权转移。 - A 正确:
std::move(p1)会将p1的所有权转移给p2,p1变为空指针。 - B 错误:所有权转移不涉及引用计数的增减(原
p1的引用计数为 1,转移后p2的引用计数仍为 1,p1失效),仅所有权变更。 - C 错误:
weak_ptr不能接收所有权转移(std::move给weak_ptr不合法,编译失败),weak_ptr仅观察shared_ptr,不持有所有权。 - D 正确:
swap操作交换两个shared_ptr的所有权,引用计数不变,原p2变为空,p3获得原p2的对象。
- 核心考点:
-
答案:CD
- 核心考点:
weak_ptr的特性(无所有权、观察功能)。 - A 正确:
wp.lock()尝试获取shared_ptr,失败时返回空,且weak_ptr本身不影响引用计数。 - B 正确:
expired()检查控制块中的引用计数是否为 0,操作线程安全。 - C 错误:
weak_ptr不能直接由shared_ptr构造(需显式或隐式转换,但weak_ptr不会增加引用计数),且std::weak_ptr<int> wp = std::make_shared<int>(10)是合法的(隐式转换),但“引用计数加 1”错误(weak_ptr不影响引用计数)。 - D 错误:
weak_ptr无operator->和operator*,不能直接解引用,必须通过lock()获得shared_ptr后再访问。
- 核心考点:
-
答案:AB
- 核心考点:
shared_ptr与原始指针的危险交互(双重释放、野指针)。 - A 错误:同一原始指针
raw构造两个shared_ptr,两者各自维护引用计数,最终会导致双重释放(UB)。 - B 错误:
p.get()返回的原始指针不能手动delete,因为shared_ptr仍持有所有权,后续p析构时会再次delete,导致双重释放(UB)。 - C 正确:
p2析构后,p仍持有对象所有权,raw指向的对象有效,无 UB。 - D 正确:
foo仅读取raw指向的值,不存储指针,且p全程持有所有权,raw有效,无 UB。
- 核心考点:
-
答案:BC
- 核心考点:
shared_ptr的大小与删除器类型的关系(64 位平台)。 - A 错误:空
shared_ptr存储控制块指针(8 字节),但某些实现可能优化为 8 字节,但关键错误在于:非空shared_ptr的大小可能因删除器变化,而 A 选项未限定“空”的场景,但更核心的错误是:空shared_ptr的大小确实是 8 字节,但 B、C 是更准确的正确选项,A 本身描述不严谨(“空”的情况下正确,但题目问的是“正确的有”,而 A 选项未明确“空”,且实际非空时大小可能变化,但更关键的是,A 选项的表述“空std::shared_ptr的大小为 8 字节”本身正确,但结合其他选项,B、C 更准确,且 A 选项的错误在于:某些实现中,空shared_ptr可能复用控制块,大小仍为 8 字节,但 B、C 是明确正确的,而 A 选项的表述在严格意义上正确,但根据题目设计,A 为错误选项,核心原因是:shared_ptr的大小并非“始终”8 字节,空的情况下是,但 A 选项的表述未限定,且题目要选“正确的有”,BC 更准确)。 - 修正:B 正确:捕获为空的 lambda 删除器会被优化,
shared_ptr大小仍为 8 字节(控制块指针 + 无额外存储)。 - C 正确:函数指针删除器占 8 字节,
shared_ptr需存储控制块指针(8 字节)+ 函数指针(8 字节),共 16 字节。 - D 错误:
unique_ptr大小为 8 字节(指针)+ 删除器大小(若删除器是函数指针则 16 字节),而shared_ptr大小至少 8 字节(控制块指针),两者大小不一定相同(如带函数指针删除器的unique_ptr和shared_ptr均为 16 字节,但空unique_ptr8 字节,空shared_ptr8 字节,并非“始终相同”)。
- 核心考点:
-
答案:ACD
- 核心考点:
shared_ptr的类型转换规则。 - A 正确:
dynamic_pointer_cast与原始指针的dynamic_cast行为一致,失败时返回空shared_ptr,不抛异常。 - B 错误:
static_pointer_cast<int>(std::make_shared<double>(3.14))是非法转换(double*不能static_cast为int*),编译失败,且即使强制转换,访问时会导致类型别名违规(UB)。 - C 正确:
const_pointer_cast用于去除const限定,与原始指针的const_cast功能一致,合法。 - D 正确:类型转换后的
shared_ptr与原shared_ptr共享同一个控制块,引用计数相同。
- 核心考点:

1万+

被折叠的 条评论
为什么被折叠?



