C++11 智能指针高频面试题解析
目录
2. C++11 中有哪几种智能指针?它们的核心区别是什么?
3. 谈谈 std::unique_ptr 的实现原理和使用场景。
5. 为什么需要 std::weak_ptr?它是如何解决循环引用问题的?
1. 什么是智能指针?为什么需要智能指针?
答案:
智能指针是C++中一个模板类,它利用了RAII(Resource Acquisition Is Initialization,资源获取即初始化)技术来模拟指针的行为,同时能自动管理所指向对象的生命周期。
为什么需要智能指针?
在传统的C++编程中,我们使用 new 关键字在堆上分配内存,并需要手动使用 delete 来释放,这被称为“裸指针”或“原始指针”。手动管理内存有以下几个主要痛点:
-
内存泄漏(Memory Leaks): 忘记调用
delete释放内存,导致程序长时间运行后耗尽内存。 -
悬挂指针(Dangling Pointers): 内存已经被释放,但指针仍然指向该地址。此时通过该指针访问内存是未定义行为,可能导致程序崩溃。
-
重复释放(Double Free): 对同一块内存执行多次
delete,这也是未定义行为,通常会导致程序崩溃。 -
异常安全问题: 在
new和delete之间如果发生异常,delete语句可能不会被执行,从而导致内存泄漏。
智能指针的出现就是为了解决这些问题。它将对象的生命周期与一个栈上的智能指针对象绑定,当智能指针对象离开作用域时,其析构函数会自动被调用,并在析构函数中释放它所管理的堆内存。这极大地简化了资源管理,提高了代码的健壮性和异常安全性。
2. C++11 中有哪几种智能指针?它们的核心区别是什么?
答案:
C++11 在 <memory> 头文件中引入了三种主要的智能指针:
-
std::unique_ptr:独占所有权的智能指针。 -
std::shared_ptr:共享所有权的智能指针。 -
std::weak_ptr:一种非拥有型的智能指针,用于解决std::shared_ptr的循环引用问题。
核心区别总结:
|
特性 |
|
|
|
|---|---|---|---|
|
所有权 |
独占、唯一的 |
共享的 |
临时的、观察者 |
|
拷贝 |
禁止拷贝,只允许移动 |
允许拷贝 |
允许拷贝 |
|
引用计数 |
无 |
有(强引用计数) |
有(弱引用计数) |
|
核心用途 |
对资源进行独占管理 |
多个指针共享同一个资源 |
监视 |
|
资源释放 |
当 |
当最后一个 |
不直接管理资源生命周期 |
|
开销 |
极低,与裸指针几乎无差别 |
较大(包含一个控制块和原子操作) |
较小 |
3. 谈谈 std::unique_ptr 的实现原理和使用场景。
答案:
std::unique_ptr 体现了“独占所有权”的语义。在任何时候,只有一个 unique_ptr 可以指向一个给定的对象。
实现原理:
unique_ptr 内部封装了一个裸指针。它通过禁止拷贝构造函数和拷贝赋值运算符,只提供移动构造函数和移动赋值运算符来实现所有权的唯一性。当一个 unique_ptr 对象被销毁(例如离开作用域)时,它的析构函数会释放其管理的资源。
所有权的转移必须是显式的,通常通过 std::move() 来完成。
// 创建一个 unique_ptr
std::unique_ptr<int> p1(new int(10));
// std::unique_ptr<int> p2 = p1; // 编译错误!不允许拷贝
// 转移所有权
std::unique_ptr<int> p3 = std::move(p1);
// 此时 p1 变为空指针,p3 拥有资源
if (!p1) {
std::cout << "p1 is now empty." << std::endl;
}
std::cout << "p3 points to: " << *p3 << std::endl;
使用场景:
-
函数返回值: 当函数需要返回一个在堆上创建的对象时,返回
unique_ptr是一个非常好的选择。这清晰地表明了所有权的转移,并且效率很高。 -
作为类的成员变量: 如果一个类拥有某个资源的唯一所有权(例如,一个工厂类创建并拥有一个产品对象),使用
unique_ptr作为成员变量可以确保当类实例被销毁时,其拥有的资源也被自动释放。 -
管理动态数组:
unique_ptr有一个特化版本std::unique_ptr<T[]>,可以安全地管理动态数组,并在销毁时调用delete[]。
std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
arr[0] = 100; // 可以像数组一样使用
// 当 arr 离开作用域时,会自动调用 delete[]
优先选择 unique_ptr:除非你需要共享所有权,否则 unique_ptr 应该是你的首选,因为它更轻量,所有权模型更简单。
4. std::shared_ptr 的实现原理是什么?它是线程安全的吗?
答案:
std::shared_ptr 实现了“共享所有权”的语义。多个 shared_ptr 可以指向同一个对象。只有当最后一个指向该对象的 shared_ptr 被销毁时,对象才会被释放。
实现原理:
shared_ptr 的实现通常包含两个指针:
-
一个指向它所管理的对象。
-
一个指向一个控制块(Control Block)。
这个控制块是一个非常关键的数据结构,它在首次创建 shared_ptr 时被分配,并被所有指向同一对象的 shared_ptr 共享。控制块中至少包含:
-
强引用计数(Strong Reference Count): 记录有多少个
shared_ptr正在共享该对象。当这个计数变为0时,对象被删除。 -
弱引用计数(Weak Reference Count): 记录有多少个
weak_ptr正在观察该对象。当这个计数变为0时,控制块本身被删除。 -
(可选)自定义删除器、分配器等信息。
每次拷贝 shared_ptr 时(拷贝构造或赋值),强引用计数会原子性地加1。每次 shared_ptr 被销毁时,强引用计数会原子性地减1。
线程安全性:
这是一个经典的面试题,答案需要精确:
-
控制块是线程安全的:多个线程可以同时拷贝、赋值或销毁指向同一个对象的
shared_ptr实例,因为引用计数的增减操作是原子操作,不会导致数据竞争。 -
所管理的对象不是线程安全的:
shared_ptr只保证了引用计数的线程安全,但它并不保证所指向对象的线程安全。如果多个线程需要同时访问或修改shared_ptr所指向的对象,你仍然需要手动加锁(例如使用std::mutex)。
5. 为什么需要 std::weak_ptr?它是如何解决循环引用问题的?
答案:
std::weak_ptr 是一种非拥有型(non-owning)的智能指针。它指向一个由 shared_ptr 管理的对象,但它不会增加强引用计数。它的主要作用是“观察”一个对象,而不会影响其生命周期。
为什么需要 weak_ptr?
主要是为了解决 shared_ptr 可能导致的**循环引用(Circular Reference)**问题。
循环引用问题举例:
假设有两个类 A 和 B,它们都通过 shared_ptr 相互引用:
#include <iostream>
#include <memory>
struct B; // 前向声明
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destructor called" << std::endl; }
};
struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destructor called" << std::endl; }
};
int main() {
std::shared_ptr<A> pa = std::make_shared<A>();
std::shared_ptr<B> pb = std::make_shared<B>();
pa->b_ptr = pb;
pb->a_ptr = pa;
std::cout << "pa use_count: " << pa.use_count() << std::endl; // 输出 2
std::cout << "pb use_count: " << pb.use_count() << std::endl; // 输出 2
// main函数结束时,pa 和 pb 被销毁
// pa 的引用计数从 2 变为 1
// pb 的引用计数从 2 变为 1
// 它们的引用计数永远不会变为0,导致 A 和 B 的对象无法被释放,造成内存泄漏。
// A 和 B 的析构函数永远不会被调用。
return 0;
}
在这个例子中,A 对象内部有一个指向 B 的 shared_ptr,B 对象内部也有一个指向 A 的 shared_ptr。当 main 函数结束时,pa 和 pb 这两个栈上的智能指针被销毁,它们各自使其所指对象的引用计数减1。但此时,A 对象的引用计数仍然为1(被pb->a_ptr持有),B 对象的引用计数也为1(被pa->b_ptr持有)。这导致它们的引用计数永远无法归零,析构函数永远不会被调用,从而造成内存泄漏。
如何用 weak_ptr 解决:
解决方法是将其中一方的引用(通常是“子”指向“父”或相互依赖关系中较弱的一方)改为 std::weak_ptr。weak_ptr 不会增加强引用计数,从而打破了循环。
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destructor called" << std::endl; }
};
struct B {
std::weak_ptr<A> a_ptr; // <--- 改为 weak_ptr
~B() { std::cout << "B destructor called" << std::endl; }
};
// ... main 函数与之前相同
// 当 pa, pb 离开作用域后:
// pa 的引用计数变为 1 -> 0, A 对象被销毁。
// A 的析构函数中,其成员 b_ptr 被销毁,导致 B 对象的引用计数从 1 -> 0, B 对象被销毁。
// 内存泄漏问题解决!
如何使用 weak_ptr?
weak_ptr 不能直接访问所指向的对象,因为它不确定对象是否还存在。必须通过调用 lock() 方法来检查:
-
如果对象仍然存在,
lock()会返回一个指向该对象的有效的shared_ptr。 -
如果对象已经被销毁,
lock()会返回一个空的shared_ptr。
这种“检查再使用”的机制保证了访问的安全性。
6. std::make_shared 和 new 的方式创建 shared_ptr 有什么区别?为什么优先使用 std::make_shared?
答案:
创建 std::shared_ptr 有两种常见方式:
// 方式一:使用 std::make_shared
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
// 方式二:使用 new
std::shared_ptr<MyClass> ptr2(new MyClass());
虽然两者都能创建 shared_ptr,但强烈推荐优先使用 std::make_shared,原因有二:
1. 性能更高(效率优势)
-
std::shared_ptr<MyClass> ptr(new MyClass());这行代码会执行两次堆内存分配:-
new MyClass():为MyClass对象本身分配内存。 -
shared_ptr的构造函数:为控制块(包含引用计数等)分配内存。
-
-
std::make_shared<MyClass>()这行代码只会执行一次堆内存分配。它会一次性地分配一块足够大的内存,同时容纳MyClass对象和控制块。这减少了内存分配的开销,也提高了内存局部性,从而提升了性能。
2. 异常安全(健壮性优势)
考虑以下函数调用: process_data(std::shared_ptr<Data>(new Data()), compute_priority());
C++标准不保证函数参数的求值顺序。一个可能的执行顺序是:
-
new Data():成功分配Data对象。 -
compute_priority():调用此函数,但不幸地抛出了一个异常。 -
std::shared_ptr的构造函数没有机会被调用。
结果是,第一步中分配的 Data 对象的内存就泄漏了,因为没有任何智能指针能接管它。
如果使用 std::make_shared: process_data(std::make_shared<Data>(), compute_priority());
std::make_shared<Data>() 会原子性地完成对象的创建和智能指针的构造,即使 compute_priority() 抛出异常,Data 对象的内存也会被安全地管理和释放,不会发生泄漏。
什么情况下不能使用 std::make_shared?
| 场景 | make_shared 为何不行 |
解决方案 |
| 自定义删除器 | 函数接口没地方传删除器。 | 使用 shared_ptr 的构造函数:std::shared_ptr<T>(new T(), my_deleter); |
| 私有构造函数 | make_shared 是外部函数,没有访问权限。 |
在类的公开静态工厂函数内部使用 new 创建对象并包装成 shared_ptr 返回。 |
在这种情况下,我们就需要告诉 shared_ptr:“别用 delete,请用我指定的这个函数来释放资源。” 这个指定的函数就是“自定义删除器”。
(2) make_shared 为何不支持?
std::make_shared 的设计目标是效率,它通过一次性分配一块大内存(同时给对象和控制块使用)来实现这一点。它的函数模板签名是这样的:
std::make_shared<类型>(构造函数参数...);
您会发现,它的参数列表里只接受所创建对象的构造函数参数,根本没有地方让我们传入一个自定义删除器。这是设计上的一个权衡,为了保持其接口的简洁和高效的实现,它放弃了支持自定义删除器这个相对不那么常用的功能。
(3) 怎么做?
当需要自定义删除器时,我们必须放弃 make_shared,回到使用 new 的方式,因为 shared_ptr 的构造函数可以直接接收一个删除器作为参数。
示例代码:
#include <iostream>
#include <memory>
#include <cstdlib> // for malloc and free
// 这是一个自定义删除器,用于释放 malloc 分配的内存
void malloc_deleter(int* ptr) {
std::cout << "Using custom deleter to free memory!" << std::endl;
free(ptr);
}
int main() {
// 正确做法:使用 shared_ptr 的构造函数传入指针和删除器
std::shared_ptr<int> p1((int*)malloc(sizeof(int)), malloc_deleter);
// 错误做法:make_shared 无法指定删除器,下面这行代码无法编译
// std::shared_ptr<int> p2 = std::make_shared<int>(...); // 怎么传 deleter?没地方传!
return 0;
}
2. 为什么不能用于“私有/受保护构造函数”?
(1) 什么是私有/受保护构造函数?
这是一种常见的设计模式(例如工厂模式、单例模式)。类的作者不希望你直接创建这个类的对象,所以把构造函数声明为 private 或 protected。取而代之的是,它会提供一个公开的静态成员函数(通常叫 create() 或 getInstance())来作为创建对象的唯一入口。
(2) make_shared 为何不支持?
std::make_shared<MyClass>() 本质上是一个外部的辅助函数。当它尝试创建 MyClass 的对象时,它需要调用 MyClass 的构造函数。如果这个构造函数是 private 的,那么根据 C++ 的访问权限规则,一个外部函数是无权访问一个类的私有成员的,因此编译器会报错。
只有类的成员函数或者友元(friend)才有权限访问私有成员。
(3) 怎么做?
在这种情况下,只能在那个公开的静态工厂函数内部,使用 new 来调用私有构造函数(因为作为成员函数,它有这个权限),然后将 new 出来的裸指针包装成 shared_ptr 并返回。
示例代码:
#include <iostream>
#include <memory>
class MyClass {
private:
// 构造函数是私有的!
MyClass() {
std::cout << "MyClass private constructor called." << std::endl;
}
public:
// 提供一个公开的静态工厂函数来创建对象
static std::shared_ptr<MyClass> create() {
// 在类的内部,我们有权调用私有构造函数。
// 因为 make_shared 无法访问,所以这里必须用 new。
return std::shared_ptr<MyClass>(new MyClass());
}
};
int main() {
// 正确做法:通过静态工厂函数创建实例
std::shared_ptr<MyClass> p1 = MyClass::create();
// 错误做法:make_shared 是外部函数,无权访问私有构造函数,编译失败
// std::shared_ptr<MyClass> p2 = std::make_shared<MyClass>();
return 0;
}
-
当需要使用自定义删除器时。
make_shared不支持传递自定义删除器。 -
当你要管理的对象的构造函数是私有或受保护的,而你需要通过一个特殊的工厂函数来创建对象并包装成
shared_ptr时。 -
好的,没问题。这两个确实是
std::make_shared的经典局限场景,我们来逐一拆解,用具体的例子帮助您理解。1. 为什么不能用于“自定义删除器” (Custom Deleter)?
(1) 什么是自定义删除器?
默认情况下,
shared_ptr在释放内存时会调用delete。但有时我们管理的资源并非是通过new创建的,比如: -
用 C 语言的
malloc分配的内存,需要用free释放。 -
一个文件指针,需要用
fclose关闭。 -
一个数据库连接,需要调用特定的
close_connection函数。
更多推荐


所有评论(0)