作者:求一个demo
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
内容通俗易懂,没有废话,文章最后是面试常问内容(建议通过标题目录学习)
废话不多说,我们直接开始------>>>>>>
#include <memory>//智能指针头文件
一、独占指针(unique_ptrr)
1、特点:
①同一时间只能有一个unique_ptr指向某个对象(不能进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值)。
②不能使用其他unique_ptr对象的值初始化另一个unique_ptr,也不能将一个unique_ptr赋值给另一个unique_ptr。因为这样会导致两个独占指针共享相同的对象。
③可以使用move()方法,将对象的所有权从一个独占指针转移到另一个独占指针。
④不能动态分配(new)一个智能指针,而是应该像声明函数的局部变量那样声明智能指针(智能指针是自动管理内存,如果new手动管理内存容易内存泄漏)。当unique_ptr离开作用域时,它管理的对象也将被删除(unique_ptr有独占所有权的特性,unique_ptr离开作用域,析构函数被调用,自动释放内存,防止内存泄漏)。如果要删除unique_ptr管理的对象,又要保留智能指针在作用域中,则可以将其值设置为nullptr,或者调用reset()函数(置nullptr则释放unique_ptr管理对象的所有权并释放对象内存,之后unique_ptr不指向任何对象,仍在作用域中。reset()函数作用和置nullptr类似)。
⑤智能指针指向对象的内存一般是堆内存,因为智能指针是帮助我们管理动态分配的内存,避免手动管理出现的内存泄漏等问题。
2、底层及是否线程安全:
(1)底层:unique_ptr本质是裸指针+删除器(deleter),析构时调用delete或自定义删除器。
(2)是否线程安全:线程不安全(可能导致数据竞争和未定义行为。unique_ptr的独占性,unique_ptr不能进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值。在多线程环境中,多个线程访问一个unique_ptr对象,一个线程正使用unique_ptr访问其管理的对象,另一个线程试图移动该unique_ptr,导致结果不确定性)。
3、使用场景:
①替代new/delete,管理动态分配的单个对象或数组。
②明确资源独占的场景(如工厂模式返回的资源)。
4、示例:
#include <memory>
using namespace std;
// 分配一个 int
unique_ptr<int> p1 = make_unique<int>(42);
// 分配数组(C++14 支持 make_unique<T[]>)
unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 100;
// 转移所有权
unique_ptr<int> p2 = move(p1); // p1 变为 nullptr
二、共享指针(shared_ptr)
1、特点:
①多个shared_ptr可以指向同一个对象,通过(强)引用计数管理生命周期,自动管理内存。
②开销较大,因为需要维护引用计数和控制块。
③shared_ptr是强引用计数,每当有一个新的shared_ptr指向相同对象或拷贝构造、拷贝赋值运算符从一个已有的shared_ptr创建一个新的shared_ptr,强引用计数加1。每当一个shared_ptr被销毁(例如离开其作用域),或通过赋值操作让它指向其他对象时,强引用计数减1。强引用计数减为0时,shared_ptr会自动释放对象所占的内存,并调用对象的析构函数。
2、底层及是否线程安全:
(1)底层:包含两个指针,一个指向对象,一个指向控制块(含强弱引用计数、删除器等)。make_shared将对象和控制块分配在连续内存中,提高缓存命中率。
(2)是否线程安全:部分线程安全。
①安全的部分:引用计数的增减是原子的,因此多线程同时拷贝或销毁同一个shared_ptr是安全的(引用计数的变化不会引起竞争)。
②不安全的部分:shared_ptr指向的对象本身的访问需要用户自行同步。shared_ptr只保证引用计数的安全,不保证对象的线程安全。如果多个线程通过不同shared_ptr访问同一个对象,且至少有一个线程执行写操作,需要同步机制(如互斥锁)。
3、使用场景:
①需要多个指针共享同一对象的场景(如多线程缓存、图结构等)。
4、循环引用
(1)原理:当两个或多个shared_ptr相互引用时,每个对象的引用计数都不会减为0,即使这些对象都没有外部引用,他们的内存也无法被释放,造成内存泄漏。
(2)产生原因:
两个shared_ptr对象a和b分别指向A和B类型的对象,从此时A和B强引用计数都为1。通过让A对象中b_ptr指向B对象,B对象中a_ptr指向A对象,A和B强引用计数都变为2。当main函数结束(a、b超出作用域被销毁),它们对A和B的强引用计数变为1。但A和B对象相互引用,它们的计数仍为1,未降为0。因此A和B对象的内存不会被释放,析构函数也不会被调用。
class A {
public:
std::shared_ptr<B> b_ptr;
~A() {
std::cout << "A destructor" << std::endl;
}
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() {
std::cout << "B destructor" << std::endl;
}
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
(3)解决循环引用:weak_ptr是弱引用计数,不会增加对象的引用计数,不会影响对象的生命周期。将类A与B中shared_ptr改为weak_ptr,当A对象中b_ptr指向B对象,B对象中a_ptr指向A对象,A和B对象的强引用计数不会增加。当main函数结束后,a和b超出作用域被销毁,A和B对象的引用计数降为0,内存被释放,析构函数被调用。
class A {
public:
std::weak_ptr<B> b_ptr;
~A() {
std::cout << "A destructor" << std::endl;
}
};
class B {
public:
std::weak_ptr<A> a_ptr;
~B() {
std::cout << "B destructor" << std::endl;
}
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
5、示例:
#include <memory>
using namespace std;
auto p1 = make_shared<int>(42); // 引用计数=1
{
auto p2 = p1; // 引用计数=2
} // p2 析构,引用计数=1
// p1 析构时引用计数=0,自动释放内存
三、弱指针(weak_ptr)
1、特点:
①weak_ptr改变弱引用计数,观察shared_ptr管理的对象,但不影响其生命周期(类似shared_ptr的辅助)。
②当需要访问weak_ptr所管理的对象时,需要将其转换为shared_ptr,通常调用lock()函数来实现(weak_ptr是弱引用,不拥有对象的所有权,所以weak_ptr不能直接解引用 * 访问对象,因为对象可能已经被销毁了)。
③weak_ptr一般不单独使用,通常和shared_ptr一起使用,可以解决共享指针的循环引用问题。
④当weak_ptr和shared_ptr指向对象相同时,弱引用计数加1,当一个weak_ptr被销毁(离开作用域),若引用计数减1。但是,弱引用计数不影响对象内存空间的释放。
2、底层及是否线程安全:
(1)底层:依赖shared_ptr的控制块,但不增加引用计数。
(2)是否线程安全:部分线程安全。
①安全的部分:weak_ptr是原子的,多线程同时操作一个weak_ptr的拷贝是安全的。
②不安全的部分:通过weak_ptr的lock()函数升级为shared_ptr时,如果与其他的shared_ptr操作竞争,可能需要同步。
3、使用场景:
①解决shared_ptr的循环引用问题(如双向链表、观察者模式)。
②缓存场景(避免缓存对象长期未被释放)。
4、示例:
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
if (auto tmp = weak.lock()) { // 转换为 shared_ptr
// 对象仍存在
} else {
// 对象已被释放
}
四、对比(智能指针和裸指针)
①所有权管理:智能指针是自动(RAII)管理;裸指针需要手动管理。
②内存泄漏风险:智能指针风险低;裸指针风险高。
③线程安全:shared_ptr计数安全(对象访问方面需要同步);裸指针没有保障。
④开销:shared_ptr较高(要维护计数和控制块),unique_ptr较低;裸指针无额外开销。
⑤循环引用:需要weak_ptr解决;无此问题(但易泄漏)。
五、校招面试常问内容
1、为什么需要智能指针?
①解决指针内存泄漏问题(如忘记delete)。
②避免悬空指针(指向已释放的内存)。
③自动管理资源的生命周期,符合RAII原则。
2、如何转移unique_ptr的所有权?
①使用move()函数。
3、shared_ptr的线程安全性?
①引用计数是线程安全的(原子操作)。
②指向的对象的访问需要用户自行同步(如加锁)。
4、shared_ptr循环引用问题?如何解决?
(1)循环引用:多个共享指针互相引用,导致强引用计数永远不降为0,造成内存泄漏。
(2)解决:将其中一个共享指针改为weak_ptr。
5、weak_ptr的作用和底层原理?
(1)作用:
①解决shared_ptr循环引用问题。
②观察shared_ptr管理的对象是否存活(不增加强引用计数)。
(2)底层:依赖shared_ptr的控制块,通过若引用计数跟踪观察者的数量(看若引用计数的数量)。
6、shared_ptr的控制块何时创建?
①通过make_shared一次性分配内存(对象+控制块),更高效。
②直接构造(shared_ptr<T>(new T)):先分配对象,再分配控制块,可能抛出异常导致内存泄漏。
最后,如有不足和错误的地方,期待私信指正!