智能指针(smart_ptr)
前备知识
- 工厂函数:是一个普通函数(或静态方法),通常是类外部的,用于返回类的实例。工厂函数可以访问类的私有构造函数。
- 将类的实例化的过程封装,直接向外返回一个对象的指针或者引用
- reset方法
reset
会删除当前智能指针所持有的对象(如果有),然后将智能指针置空(对于不带参数的reset
),或者管理一个新的对象(对于带参数的reset
)。但是在shared_ptr中如果引用计数大于1,只会给引用计数减一
一、shared_ptr共享指针
1.1情景与定义
- 多个裸指针指向同一块内存地址,即使被delete了,也有可能导致改内存空间不再被使用,从而引发内存泄漏。可以使用共享指针,当该地址的引用计数缩减为0的时候,就会重载该块地址
1.2内存占用
- 控制块、初始化内容对象在堆上,指针本身在栈上
- 控制块:
std::shared_ptr
会为每个被管理的对象创建一个控制块。控制块中包含了:- 引用计数(shared count)
- 自定义删除器(deleter)
- 自定义分配器(如果有的话)
- 创建的时候会先创建控制块,然后让指针本身指向控制块
-
需要避免控制块的重复创建,比如已经创建了指向一个地址的shared_ptr,如果该地址上的shared_ptr没有被释放,那么不可以再在这个地址上创建shared_Ptr。例如:从成员函数中生成
std::shared_ptr
实例和使用裸指针创建std::shared_ptr
的问题#include <iostream> #include <memory> void loggingDeleter(int* ptr) { std::cout << "Deleting pointer: " << ptr << std::endl; delete ptr; } int main() { int* rawPtr = new int(42); // 使用裸指针创建多个 std::shared_ptr 实例 std::shared_ptr<int> sp1(rawPtr, loggingDeleter); std::shared_ptr<int> sp2(rawPtr, loggingDeleter); // 错误:rawPtr 被多次管理 return 0; // 此时 rawPtr 被 delete 两次 } #include <iostream> #include <memory> #include <vector> class Widget : public std::enable_shared_from_this<Widget> { public: void process(std::vector<std::shared_ptr<Widget>>& processedWidgets) { processedWidgets.emplace_back(this); // 错误:this 被用来创建新的 std::shared_ptr 这里会直接构造一个函数,避免拷贝或者移动,导致一块地址多次初始化共享指针 } }; int main() { auto widget = std::make_shared<Widget>(); std::vector<std::shared_ptr<Widget>> processedWidgets; widget->process(processedWidgets); // 此时 Widget 对象的控制块可能会被创建多次 return 0; } //解决方案 void process(std::vector<std::shared_ptr<Widget>>& processedWidgets) { processedWidgets.emplace_back(shared_from_this()); // 正确:使用 shared_from_this 获取现有 std::shared_ptr }
1.3引用计数、make_shared、移动构造、移动赋值
-
对象本身不会再分配一个空间给引用计数,计数和对象的联动使用会加大内存消耗
std::shared_ptr<test> a(new test());
。而make_shared是一种工厂函数,会使得引用计数和对象连续分布,减少消耗std::shared_ptr a = make_shared<test>()
。 -
引用计数操作必须是原子性的,可能在线程A中
std::shared_ptr
实例正在执行析构,发试图将引用计数减少1,而在线程B中被执行了拷贝构造。两个线程引起对引用计数的竞争条件。 -
移动构造和移动赋值 通常比拷贝构造和拷贝赋值更高效,因为它们避免了引用计数的自增操作。移动操作只涉及指针的转移,不涉及对引用计数的修改,从而减少了性能开销。
拷贝构造和拷贝赋值 涉及引用计数的增加,这可能会带来性能开销,尤其是在引用计数操作频繁的情况下。
class MyClass { public: MyClass(MyClass&& other) noexcept; }; MyClass obj1; MyClass obj2 = std::move(obj1); // 通过移动构造创建 obj2 class MyClass { public: MyClass& operator=(MyClass&& other) noexcept; }; MyClass obj1; MyClass obj2; obj2 = std::move(obj1); // 通过移动赋值将 obj1 的资源转移到 obj2
1.4make_shared with new and make_shared
std::make_shared
内部使用单次内存分配来同时分配对象和控制块。它通过一次new
操作分配一大块内存,这块内存包含了对象和控制块。因此,控制块和对象都在堆上。new
加std::shared_ptr
: 需要两次内存分配,一次为对象,另一次为控制块。这可能导致额外的内存分配开销,并且对象和控制块不一定存储在连续的内存区域中。
1.5共享指针的创建和销毁
std::shared_ptr<Entity> e1(new Entity());
std::shared_ptr<Entity> e1 = std::make_shared<Entity>();
auto e1 = std::make_shared<Entity>();
std::shared_ptr<Entity> e2 = e1;
std::shared_ptr<Entity> e2 = std::move(e1);
foo(std::move(e1));
// 引用数量不变
foo(e1);
// 引用+1
#include <memory>
#include <iostream>
using namespace std;
class Ball {
public:
Ball() {
cout << "构造执行" << endl;
};
~Ball() {
cout << "析构执行" << endl;
};
void Bounce() {
cout << "A Ball Bounce" << endl;
}
};
int main() {
// shared_ptr<int> p = new int(100);
shared_ptr<int> p = make_shared<int>(100); //隐式转换,给共享指针类实例化,且效率更高。
// 裸指针进行的操作其也可以
cout << *p << endl;
// 使用共享指针
shared_ptr<Ball> p1 = make_shared<Ball>(); //构造执行
cout << p1.use_count() << endl; //1
shared_ptr<Ball> p2 = p1;
cout << p1.use_count() << " " << p2.use_count() << endl; //2 2
shared_ptr<Ball> p3 = p1;
cout << p1.use_count() << " " << p2.use_count() << " " << p3.use_count() << endl; //3 3 3
p1.reset();
p2.reset();
p3.reset(); //析构执行
return 0;
}
-
注意虽然可以裸指针和共享指针指向同一内存空间
Ball* p4 = p.get()
(get方法获取),但只要共享指针引用计数为0,对应内存空间会直接被销毁,会变为野指针。 -
生命周期,堆是每个线程公用的,每个线程的引用都消失了,最后才会消失
-
使用
std::make_shared()
:总是会创建一个新的控制块并返回一个指向该控制块的std::shared_ptr
实例。
1.6自定义deleter
- 见unique_ptr的deleter
1.7自定义reset和别名
#include <iostream>
#include <memory>
using namespace std;
struct Bar { int i = 123;};
struct Foo { Bar bar; };
void close_file(FILE* fp) {
if (fp == nullptr) {
return;
}
fclose(fp);
std::cout << "file closed" << std::endl;
}
int main() {
FILE* fp = fopen("../CMakeLists.txt", "r");
shared_ptr<FILE> sfp{fp, close_file};
cout << "file opened" << endl; //file opened
cout << sfp.use_count() << endl; //1
shared_ptr<Foo> f = make_shared<Foo>();cout << f.use_count()<< endl;// 1
shared_ptr<Bar> b(f,&(f->bar));cout << f.use_count()<< endl; // 2
cout << b->i << endl;// 123
return 0;
}
// file closed
-
可以不delete内存,而关闭文件。
-
f为共享指针的别名,可以通过这样的操作使得b指针可以访问f指针的属性,注意b和f都必须是共享指针
二、unique_ptr
2.1情景和使用场景
- 常用在工厂函数中,将unique_ptr作为工厂函数发返回值
2.2内存占用
std::unique_ptr
对象(即管理的指针和它自身的其他成员)是存储在栈上(如果它作为局部变量存在)。它只包含一个裸指针(或其他智能指针的内部数据结构),该指针指向堆上的对象。
2.3不可赋值,可以移动
std::unique_ptr<Entity> e1 = new Entity()
(错误),不是普通指针类型
std::unique_ptr<Entity> e1(new Entity())
(正确),不推荐
std::unique_ptr<Entity> e1 = make_uniuqe<entity>();
(正确)
auto e2 = std::unique_ptr<Entity>
(正确)
std::unique_ptr<Entity> e2 = e1
(错误),无法赋值
std::unique_ptr<Entity> e2 = std::move(e1)
(正确),转移
foo(std::move(e1))
(正确),转移后的指针
2.4unique_ptr生命周期
2.5自定义deleter
-
可以使用lamda表达式、普通函数、任意函数
auto dele = [](file* file){ make_log(file); delete dele; } template<typename Ts> std::unique_ptr<file, deceltype(dele)> makefile(Ts& param) { std::unique_ptr<param, deceltype(dele)> fileTest(nullptr, dele); if(/*应该分配一个Stock类型的对象*/){ fileTest.reset(new Stock(std::forward<Ts>(params))); } else if(/*应该分配一个Bond类型的对象*/){ fileTest.reset(new Bond(std::forward<Ts>(params))); } else if(/*应该分配一个RealEstate类型的对象*/){ fileTest.reset(new RealEstate(std::forward<Ts>(params))); } }
-
当使用一个自定义的
deleter
时,需要将deleter
的类型也传给std::unique_ptr
用于模板参数。记住这一点,这和std::shared_ptr
很不同!
2.6unique_ptr or shared_ptr
类型声明和灵活性:
- 对于
unique_ptr
,deleter 类型是类型的一部分。这意味着不同 deleter 的unique_ptr
是不同的类型,即使它们指向相同类型的对象。 - 对于
shared_ptr
,deleter 类型不是类型的一部分。所有shared_ptr<Widget>
都是相同的类型,无论它们使用什么 deleter。
绑定时机:
unique_ptr
的 deleter 在编译时绑定。这可能导致更高的性能,因为编译器可以进行更多优化。shared_ptr
的 deleter 在运行时绑定。这提供了更大的灵活性,但可能会有轻微的性能开销。- 总结:
shared_ptr
使用类型擦除来实现其灵活性,而unique_ptr
则依赖于编译时的类型信息。
多态性:
shared_ptr
的设计允许更多的多态行为。你可以在运行时决定使用哪个 deleter,而不会改变shared_ptr
的类型。unique_ptr
的设计更加静态,deleter 类型在编译时确定。
#include <memory>
#include <vector>
#include <iostream>
struct Widget {
virtual ~Widget() = default;
virtual void doSomething() = 0;
};
struct WidgetA : Widget {
void doSomething() override { std::cout << "WidgetA doing something\n"; }
};
struct WidgetB : Widget {
void doSomething() override { std::cout << "WidgetB doing something\n"; }
};
// 自定义删除器
auto deleterA = [](Widget* w) {
std::cout << "Deleting with deleterA\n";
delete w;
};
auto deleterB = [](Widget* w) {
std::cout << "Deleting with deleterB\n";
delete w;
};
int main() {
// unique_ptr例子
std::unique_ptr<Widget, decltype(deleterA)> upA(new WidgetA(), deleterA);
std::unique_ptr<Widget, decltype(deleterB)> upB(new WidgetB(), deleterB);
// 不能运行,A和B类型不一样
// std::vector<std::unique_ptr<Widget, decltype(deleterA)>> uniqueVec{std::move(upA), std::move(upB)};
// shared_ptr例子
std::shared_ptr<Widget> spA(new WidgetA(), deleterA);
std::shared_ptr<Widget> spB(new WidgetB(), deleterB);
// 可以运行,类型一样
std::vector<std::shared_ptr<Widget>> sharedVec{spA, spB};
upA->doSomething();
upB->doSomething();
for(const auto& sp : sharedVec) {
sp->doSomething();
}
return 0;
}
2.7类型擦除和虚函数类似,下面是对比和相似之处
- 运行时多态:
- 虚函数和类型擦除都实现了运行时多态,允许在运行时决定调用哪个函数。
- 动态分发:
- 两者都使用某种形式的动态分发机制来调用正确的函数。
- 额外的内存开销:
- 虚函数使用虚函数表(vtable)。
- 类型擦除通常使用某种形式的函数指针或函数对象。
区别:
-
实现机制:
- 虚函数:使用虚函数表和虚指针。
- 类型擦除:通常使用模板和运行时多态的组合。
-
灵活性:
- 虚函数:限于继承层次结构。
- 类型擦除:可以用于不相关的类型。
-
编译时要求:
- 虚函数:需要在基类中声明。
- 类型擦除:不需要预先声明,可以在使用时定义接口。
-
性能:
- 虚函数:通常有较小的运行时开销。
- 类型擦除:可能有更大的开销,取决于实现。
#include <iostream> #include <memory> #include <vector> // 使用虚函数的方式 class Animal { public: virtual void makeSound() const = 0; virtual ~Animal() = default; }; class Dog : public Animal { public: void makeSound() const override { std::cout << "Woof!" << std::endl; } }; class Cat : public Animal { public: void makeSound() const override { std::cout << "Meow!" << std::endl; } }; // 使用类型擦除的方式 class AnimalConcept { struct Concept { virtual void makeSound() const = 0; virtual ~Concept() = default; }; template<typename T> struct Model : Concept { T object; Model(T obj) : object(std::move(obj)) {} void makeSound() const override { object.makeSound(); } }; std::unique_ptr<Concept> pimpl; public: template<typename T> AnimalConcept(T obj) : pimpl(std::make_unique<Model<T>>(std::move(obj))) {} void makeSound() const { pimpl->makeSound(); } }; struct Cow { void makeSound() const { std::cout << "Moo!" << std::endl; } }; int main() { // 使用虚函数 std::vector<std::unique_ptr<Animal>> animals; animals.push_back(std::make_unique<Dog>()); animals.push_back(std::make_unique<Cat>()); for (const auto& animal : animals) { animal->makeSound(); } // 使用类型擦除 std::vector<AnimalConcept> animalConcepts; animalConcepts.emplace_back(Dog{}); animalConcepts.emplace_back(Cat{}); animalConcepts.emplace_back(Cow{}); // 注意:Cow 不需要继承任何基类 for (const auto& animal : animalConcepts) { animal.makeSound(); } return 0; }
2.8将unique_ptr转换为shared_ptr
std::unique_ptr<int> uptr = std::make_unique<int>(42);
std::shared_ptr<int> sptr = std::move(uptr); // 转换为 std::shared_ptr
- 转换
std::unique_ptr
到std::shared_ptr
:这会创建一个新的控制块,并将对象的所有权从std::unique_ptr
转移到std::shared_ptr
。
2.9get方法
尽管 std::shared_ptr
和 std::unique_ptr
都可以通过 get()
方法获取裸指针,但 不推荐 用这个方法在它们之间转换。直接使用裸指针来转换会失去智能指针的自动内存管理,导致严重的错误,例如双重删除或悬挂指针。
std::shared_ptr<int> sptr = std::make_shared<int>(42);
int* rawPtr = sptr.get();
std::unique_ptr<int> uptr(rawPtr); // 非常危险!可能导致双重删除
在上面的代码中,std::shared_ptr
和 std::unique_ptr
都会尝试删除 rawPtr
指向的对象,导致 未定义行为。
2.10总结:
- std::unique_ptr是一种小型,快速,只能移动的智能指针,管理资源时有独占式语义
- 默认情况下,资源的释放通过调用delete操作符实现,但也可以自定义deleter。有状态的deleter和函数指针都会引起std::unique_ptr实例的内存占用增加
- 从std::unique_ptr到std::shared_ptr的转换极为简单
三、weak_ptr
3.1特性
-
它是一种智能指针,与
std::shared_ptr
配合使用。它指向
shared_ptr
管理的对象,但不增加引用计数。它可以检测所指向的对象是否已被释放。
-
不能直接访问所指对象(没有 * 和 -> 操作符)。
可以转换为
shared_ptr
来访问对象。可以检查所指对象是否还存在。
3.2使用和特性
- 在shared_ptr的基础上使用weak_ptr,在使用的时候进行判断,如果shared_ptr消失了就不使用了,否则就继续使用
#include <iostream>
#include <memory>
class Entity {
public:
Entity() {
std::cout << "Created Entity!" << std::endl;
}
~Entity() {
std::cout << "Destroyed Entity!" << std::endl;
}
};
void observe(std::weak_ptr<Entity> ew) {
if(std::shared_ptr<Entity> spt = ew.lock()) {
std::cout << spt.use_count() << std::endl;
std::cout << "entity still alive" << std::endl;
} else {
std::cout << "entity was espried:(" << std::endl;
}
}
void ex4() {
puts("--------");
puts("Entering ex4");
std::weak_ptr<Entity> ew;
{
puts("Entering ex4::scope1");
auto e1 = std::make_shared<Entity>();
std::cout << e1.use_count() << std::endl;
ew = e1;
std::cout << e1.use_count() << std::endl;
observe(ew);
puts("Leaving ex4::scope1");
}
}
int main() {
ex4();
return 0;
}
--------
Entering ex4
Entering ex4::scope1
Created Entity!
1
1
2
entity still alive
Leaving ex4::scope1
Destroyed Entity!
3.3应用场景
#include <iostream>
#include <memory>
#include <vector>
#include <unordered_map>
// 1. 基本用法
void basic_usage() {
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
if (auto locked = wp.lock()) {
std::cout << "Value: " << *locked << std::endl;
} else {
std::cout << "Object no longer exists" << std::endl;
}
}
// 2. 检查对象是否存在
void check_existence() {
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(42);
wp = sp;
std::cout << "wp expired? " << std::boolalpha << wp.expired() << std::endl;
}
std::cout << "wp expired? " << std::boolalpha << wp.expired() << std::endl;
}
// 3. 缓存系统
class Widget {};
std::shared_ptr<Widget> loadWidget(int id) {
return std::make_shared<Widget>();
}
std::shared_ptr<Widget> getCachedWidget(int id) {
static std::unordered_map<int, std::weak_ptr<Widget>> cache;
if (auto sp = cache[id].lock()) {
return sp;
} else {
auto sp = loadWidget(id);
cache[id] = sp;
return sp;
}
}
// 4. 观察者模式
class Subject;
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void addObserver(std::shared_ptr<Observer> observer) {
observers.push_back(observer);
}
void notify() {
for (auto it = observers.begin(); it != observers.end();) {
if (auto sp = it->lock()) {
sp->update();
++it;
} else {
it = observers.erase(it);
}
}
}
};
// 5. 打破循环引用
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 而不是 shared_ptr
~B() { std::cout << "B destroyed" << std::endl; }
};
void cyclic_reference() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
}
int main() {
basic_usage();
check_existence();
getCachedWidget(1);
cyclic_reference();
return 0;
}
四、总结
- 能用unique_ptr就用unique_ptr,不行就使用shared_ptr,因为use_count占内存
- weak_ptr几乎不使用