[CPP] 智能指针
介绍 C++ 的智能指针 (Smart Pointers) 相关 API。
C++ 中的智能指针是为了解决内存泄漏、重复释放等问题而提出的,它基于 RAII (Resource Acquisition Is Initialization),也称为“资源获取即初始化” 的思想实现。智能指针实质上是一个类,但经过封装之后,在行为语义上的表现像指针。
参考资料:
- [1] https://en.cppreference.com/w/cpp/memory
- [2] https://docs.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-160
shared_ptr
shared_ptr
能够记录多少个 shared_ptr
共同指向一个对象,从而消除显式调用 delete
,当引用计数变为零的时候就会将对象自动删除。
注意,这里使用 shared_ptr
能够实现自动 delete
,但是如果使用之前仍需要 new
的话,代码风格就会变得很「奇怪」,因为 new/delete
总是需要成对出现的,所以尽可能使用封装后的 make_shared
来「代替」new
。
shared_ptr
基于引用计数实现,每一个 shared_ptr
的拷贝均指向相同的内存。如果某个 shared_ptr
被析构(生命周期结束),那么引用计数减 1 ,当引用计数为 0 时,自动释放指向的内存。
shared
的所有成员函数,包括拷贝构造函数 (Copy Constructor) 和拷贝赋值运算 (Copy Assignment Operator),都是线程安全的,即使这些 shared_ptr
指向同一对象。但如果是多线程访问同一个 non-const 的 shared_ptr
,那有可能发生资源竞争 (Data Race) 的情况,比如改变这个 shared_ptr
的指向,因此这种情况需要实现多线程同步机制。当然,可以使用 shared_ptr overloads of atomic functions 来防止 Data Race 的发生。
内部实现
如下图所示,shared_ptr
内部仅包括 2 个指针,一个指针指向共享对象,另外一个指针指向 Control block .
初始化
- 通过构造函数初始化(废话)
下面是正确的方式。
void func1()
{
int *a = new int[10];
shared_ptr<int[]> p(a);
// a is same as p.get()
cout << a << endl;
cout << p.get() << endl;
for (int i = 0; i < 10; i++) p[i] = i;
for (int i = 0; i < 10; i++) cout << a[i] << ' ';
}
// Output: 1-9
下面是错误的方式,因为 ptr
析构时会释放 &a
这个地址,但这个地址在栈上(而不是堆),因此会发生运行时错误。
int main()
{
int a = 10;
shared_ptr<int> ptr(&a);
// a is same as p.get(), but runs fail
cout << &a << endl;
cout << ptr.get() << endl;
}
- 如果通过
nullptr
初始化,那么引用计数的初始值为 0 而不是 1 。
shared_ptr<void *> p(nullptr);
cout << p.use_count() << endl;
- 不允许通过一个原始指针初始化多个
shared_ptr
。
int main()
{
int *p = new int[10];
shared_ptr<int> ptr1(p);
shared_ptr<int> ptr2(p);
cout << p << endl;
cout << ptr1.get() << endl;
cout << ptr2.get() << endl;
}
上述方式是错误的。可以通过编译,三行 cout
也能正常输出,但会发生运行时错误,因为 ptr2
会先执行析构函数,释放 p
,然后 ptr1
进行析构的时候,就会对无效指针 p
进行重复释放。
0x7feefd405a10
0x7feefd405a10
0x7feefd405a10
a.out(6286,0x113edde00) malloc: *** error for object 0x7feefd405a10: pointer being freed was not allocated
a.out(6286,0x113edde00) malloc: *** set a breakpoint in malloc_error_break to debug
- 通过
make_shared
初始化
make_shared
的参数可以时一个对象,也可以是跟该类的构造函数匹配的参数列表。
auto ptr1 = make_shared<vector<int>>(10, -1);
auto ptr2 = make_shared<vector<int>>(vector<int>(10, -1));
与通过构造函数初始化不同的是,make_shared
允许传入一个临时对象,如以下代码:
int main()
{
vector<int> v = {1, 2, 3};
auto ptr = make_shared<vector<int>>(v);
// &v = 0x7ffeef698690
// ptr.get() = 0x7fc03ec05a18
cout << &v << endl;
cout << ptr.get() << endl;
// v[0] is still 1
ptr.get()->resize(3, -1);
cout << v[0] << endl;
}
通过 ptr.get()
获取指针并修改指向的内存,并不会影响局部变量 v
的内容。
自定义 deleter
在初始化时传入一个函数指针,shared_ptr
在释放指向的对象时,会调用自定义的 deleter
处理释放行为。
int main()
{
int *p = new int[10];
auto func = [](int *p) {
delete[] p;
cout << "Delete memory at " << p << endl;
};
shared_ptr<int> ptr(p, func);
}
那么 deleter
有什么用呢?假如我们有这么一段代码:
class Basic
{
public:
Basic() { cout << "Basic" << endl; }
~Basic() { cout << "~Basic" << endl; }
};
int main()
{
Basic *p = new Basic[3];
shared_ptr<Basic> ptr(p);
}
这段代码会发生运行时错误。因为 shared_ptr
默认是使用 delete
去释放指向的对象,但定义了析构函数的对象数组,必须要通过 delete[]
析构,否则产生内存错误。
因此,为了使上述代码正常工作,需要自定义 delete
函数:
shared_ptr<Basic> ptr(p, [](Basic *p){ delete[] p; });
或者(C++17 及其之后的标准支持):
shared_ptr<Base[]> ptr(p);
指向一个函数
根据参考资料 [1] ,shared_ptr
指向一个函数,有时用于保持动态库或插件加载,只要其任何函数被 shared_ptr
引用:
void func() { cout << "hello" << endl; }
int main()
{
shared_ptr<void()> ptr(func, [](void (*)()) {});
(*ptr)();
}
注意,这里自定义的 deleter
是必不可少的,否则不能通过编译。
例子
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;
class Base
{
public:
Base() { cout << "Base" << endl; }
~Base() { cout << "~Base" << endl; }
};
class Derived : public Base
{
public:
Derived() { cout << " Derived" << endl; }
~Derived() { cout << " ~Derived" << endl; }
};
void worker(shared_ptr<Base> ptr)
{
this_thread::sleep_for(std::chrono::seconds(1));
shared_ptr<Base> lp = ptr;
{
static std::mutex io_mutex;
lock_guard<mutex> lock(io_mutex);
cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get() << ", "
<< " lp.use_count() = " << lp.use_count() << "\n";
}
}
int main()
{
shared_ptr<Base> ptr = make_shared<Derived>();
cout << "Created a shared Derived (as a pointer to Base)\n"
<< "ptr.get() = " << ptr.get() << ", "
<< "ptr.use_count() = " << ptr.use_count() << '\n';
thread t1(worker, ptr), t2(worker, ptr), t3(worker, ptr);
this_thread::sleep_for(std::chrono::seconds(2));
ptr.reset();
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << ptr.get()
<< ", p.use_count() = " << ptr.use_count() << '\n';
t1.join(), t2.join(), t3.join();
}
输出:
Base
Derived
Created a shared Derived (as a pointer to Base)
ptr.get() = 0x7fcabc405a08, ptr.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
p.get() = 0x0, p.use_count() = 0
local pointer in a thread:
lp.get() = 0x7fcabc405a08, lp.use_count() = 6
local pointer in a thread:
lp.get() = 0x7fcabc405a08, lp.use_count() = 4
local pointer in a thread:
lp.get() = 0x7fcabc405a08, lp.use_count() = 2
~Derived
~Base
lp.use_count
也可能是 {5,3,2}
这样的序列。在 worker
传入参数过程中,ptr
被拷贝了 3 次,并且在进入 worker
后,三个线程的局部变量 lp
又把 ptr
拷贝了 3 次,因此 user_count
的最大值是 7 。
unique_ptr
unique_ptr
保证同一时刻只能有一个 unique_ptr
指向给定对象。发生下列情况之一时,指定对象就会被释放:
unique_ptr
被销毁(生命周期消亡,被delete
等情况)unique_ptr
调用reset
或者进行ptr1 = move(ptr2)
操作
基于这 2 个特点,non-const 的 unique_ptr
可以把管理对象的所有权转移给另外一个 unique_ptr
:
示例代码:
class Base
{
public:
Base() { cout << "Base" << endl; }
~Base() { cout << "~Base" << endl; }
};
int main()
{
auto p = new Base();
cout << p << endl;
unique_ptr<Base> ptr(p);
unique_ptr<Base> ptr2 = std::move(ptr);
cout << ptr.get() << endl;
cout << ptr2.get() << endl;
}
/* Output is :
Base
0x7fd81fc059f0
0x0
0x7fd81fc059f0
~Base
*/
在上述代码中,存在 U = move(V)
,当执行该语句时,会发生两件事情。首先,当前 U 所拥有的任何对象都将被删除;其次,指针 V 放弃了原有的对象所有权,被置为空,而 U 则获得转移的所有权,继续控制之前由 V 所拥有的对象。
如果是 const unique_ptr
,那么其指向的对象的作用域 (Scope) 只能局限在这个 const unique_ptr
的作用域当中。
此外,unique_ptr
不能通过 pass by value 的方式传递参数,只能通过 pass by reference 或者 std::move
。
初始化
与 shared_ptr
类似。但由于 unique_ptr
的特点,它没有拷贝构造函数,因此不允许 unique_ptr<int> ptr2 = ptr
这样的操作。
下面是 unique_ptr
正确初始化的例子。
- 指向对象
class Base
{
public:
Base() { cout << "Base" << endl; }
~Base() { cout << "~Base" << endl; }
void printThis() { cout << this << endl; }
};
int main()
{
auto p = new Base();
unique_ptr<Base> ptr(p);
ptr->printThis();
}
/* Output is:
Base
0x7fbe0a4059f0
~Base
*/
- 指向数组
int main()
{
auto p = new Base[3];
unique_ptr<Base[]> ptr(p);
for (int i = 0; i < 3; i++)
ptr[i].printThis();
}
/* Output is:
Base * 3
0xc18c28 0xc18c29 0xc18c2a
~Base * 3
*/
make_unique
与 make_shared
类似,允许向 make_unique
传入一个临时变量。
void func3()
{
auto ptr = make_unique<vector<int>>(5, 0);
for (int i = 0; i < 5;i++) (*ptr)[i] = i;
for (int x : *ptr) cout << x << ' ';
}
// Output: 0 1 2 3 4
自定义 deleter
unique_ptr
的 deleter
与 shared_ptr
不同,它是基于模版参数实现的。
使用仿函数
struct MyDeleter
{
void operator()(Base *p)
{
cout << "Delete memory[] at " << p << endl;
delete[] p;
}
};
unique_ptr<Base[], MyDeleter> ptr(new Base[3]);
// unique_ptr<Base, MyDeleter> ptr(new Base[3]);
// both of them is okay
使用普通函数
unique_ptr<Base[], void (*)(Base * p)> ptr(new Base[3], [](Base *p) {
cout << "Delete memory[] at " << p << endl;
delete[] p;
});
使用 std::function
unique_ptr<Base[], function<void(Base *)>> ptr(new Base[3], [](Base *p) { delete[] p; });
注意到,使用普通函数时,模版参数为 void (*)(Base *p)
,这是一种数据类型,该类型是一个指针,指向一个返回值为 void
, 参数列表为 (Base *p)
的函数,而 void *(Base *p)
则是在声明一个函数(看不懂可以忽略)。
作为函数参数或返回值
unique_ptr
作为函数参数,只能通过引用,或者 move
操作实现。
下列操作无法通过编译:
void func5(unique_ptr<Base> ptr) {}
int main()
{
unique_ptr<Base> ptr(new Base());
func5(ptr);
}
需要改成:
void func5(unique_ptr<Base> &ptr) {}
func(ptr);
或者通过 move
转换为右值引用:
void func5(unique_ptr<Base> ptr)
{
cout << "ptr in function: " << ptr.get() << endl;
}
int main()
{
auto p = new Base();
cout << "p = " << p << endl;
unique_ptr<Base> ptr(p);
func5(move(ptr));
cout << "ptr in main: " << ptr.get() << endl;
}
/* Output is:
Base
p = 0xa66c20
ptr in function: 0xa66c20
~Base
ptr in main: 0
*/
把 unique_ptr
作为函数返回值,会自动发生 U = move(V)
的操作(转换为右值引用):
unique_ptr<Base> func6()
{
auto p = new Base();
unique_ptr<Base> ptr(p);
cout << "In function: " << ptr.get() << endl;
return ptr;
}
int main()
{
auto ptr = func6();
cout << "In main: " << ptr.get() << endl;
}
成员函数
函数 | 作用 |
---|---|
release | returns a pointer to the managed object and releases the ownership (will not delete the object) |
reset | replaces the managed object (it will delete the object) |
swap | swaps the managed objects |
get | returns a pointer to the managed object |
get_deleter | returns the deleter that is used for destruction of the managed object |
operator bool | checks if there is an associated managed object (more details) |
operator = | assigns the unique_ptr , support U = move(V) , U will delete its own object |
例子
#include <vector>
#include <memory>
#include <iostream>
#include <fstream>
#include <functional>
#include <cassert>
#include <cstdio>
using namespace std;
// helper class for runtime polymorphism demo
class B
{
public:
virtual void bar() { cout << "B::bar\n"; }
virtual ~B() = default;
};
class D : public B
{
public:
D() { cout << "D::D\n"; }
~D() { cout << "D::~D\n"; }
void bar() override { cout << "D::bar\n"; }
};
// a function consuming a unique_ptr can take it by value or by rvalue reference
unique_ptr<D> passThrough(unique_ptr<D> p)
{
p->bar();
return p;
}
// helper function for the custom deleter demo below
void close_file(FILE *fp) { std::fclose(fp); }
// unique_ptr-base linked list demo
class List
{
public:
struct Node
{
int data;
unique_ptr<Node> next;
Node(int val) : data(val), next(nullptr) {}
};
List() : head(nullptr) {}
~List() { while (head) head = move(head->next); }
void push(int x)
{
auto t = make_unique<Node>(x);
if (head) t->next = move(head);
head = move(t);
}
private:
unique_ptr<Node> head;
};
int main()
{
cout << "unique ownership semantics demo\n";
{
auto p = make_unique<D>();
auto q = passThrough(move(p));
assert(!p), assert(q);
}
cout << "Runtime polymorphism demo\n";
{
unique_ptr<B> p = make_unique<D>();
p->bar();
cout << "----\n";
vector<unique_ptr<B>> v;
v.push_back(make_unique<D>());
v.push_back(move(p));
v.emplace_back(new D());
for (auto &p : v) p->bar();
}
cout << "Custom deleter demo\n";
ofstream("demo.txt") << "x";
{
unique_ptr<FILE, decltype(&close_file)> fp(fopen("demo.txt", "r"), &close_file);
if (fp) cout << (char)fgetc(fp.get()) << '\n';
}
cout << "Linked list demo\n";
{
List list;
for (long n = 0; n != 1000000; ++n) list.push(n);
cout << "Pass!\n";
}
}
weak_ptr
weak_ptr
指针通常不单独使用(因为没有实际用处),只能和 shared_ptr
类型指针搭配使用。
当 weak_ptr
类型指针的指向和某一 shared_ptr
指针相同时,weak_ptr
指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr
指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr
类型指针并不会影响所指堆内存空间的引用计数。
此外,weak_ptr
没有重载 *
和 ->
运算符,因此 weak_ptr
只能访问所指的堆内存,而无法修改它。
weak_ptr
作为一个 Observer 的角色存在,可以获取 shared_ptr
的引用计数,可以读取 shared_ptr
指向的对象。
成员函数:
函数 | 作用 |
---|---|
operator = | weak_ptr 可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值 |
swap | 与另外一个 weak_ptr 交换 own objetc |
reset | 置为 nullptr |
use_count | 查看与 weak_ptr 指向相同对象的 shared_ptr 的数量 |
expired | 判断当前 weak_ptr 是否失效(指针为空,或者指向的堆内存已经被释放) |
lock | 如果 weak_ptr 失效,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。 |
例子:
#include <memory>
#include <iostream>
using namespace std;
// global weak ptr
weak_ptr<int> gw;
void observe()
{
cout << "use count = " << gw.use_count() << ": ";
if (auto spt = gw.lock()) cout << *spt << "\n";
else cout << "gw is expired\n";
}
int main()
{
{
auto sp = make_shared<int>(233);
gw = sp;
observe();
}
observe();
}
// Output:
// use count = 1: 233
// use count = 0: gw is expired
总结
使用智能指针的几个重要原则是:
- 永远不要试图去动态分配一个智能指针,相反,应该像声明函数的局部变量那样去声明智能指针。
- 使用
shared_ptr
要注意避免循环引用
[CPP] 智能指针的更多相关文章
- cpp智能指针
weak_ptr<Cls1> wp1; { shared_ptr<Cls1> ptr1(new Cls1);//共享指针 wp1 = ptr1;//临时共享指针 std::co ...
- Android智能指针sp wp详解
研究Android的时候,经常会遇到sp.wp的东西,网上一搜,原来是android封装了c++中对象回收机制.说明:1. 如果一个类想使用智能指针,那么必须满足下面两个条件: a. 该类是虚基 ...
- 根据OSG中的ref_ptr和Reference简化的智能指针
main.cpp测试代码 #include "TestSmartPointer" void fun() { SP<TestSmartPointer> sp1=new T ...
- C++中智能指针的设计和使用
转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7561235 智能指针(smart pointer)是存储指向动态分配(堆 ...
- auto_ptr,shared_ptr 智能指针的使用
Q: 那个auto_ptr是什么东东啊?为什么没有auto_array?A: 哦,auto_ptr是一个很简单的资源封装类,是在<memory>头文件中定义的.它使用“资源分配即初始化”技 ...
- Android系统的智能指针(轻量级指针、强指针和弱指针)的实现原理分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6786239 Android 系统的运行时库层代 ...
- 关于智能指针auto_ptr
智能指针auto_ptr和shared_ptr也是面试中经常被问到的一个 感觉看auto_ptr的源码反而更加容易理解一些,因为源码的代码量并不大,而且比较容易理解. 本篇主要介绍auto_ptr 其 ...
- STL模板_智能指针概念
一.智能指针1.类类型对象,在其内部封装了一个普通指针.当智能指针对象因离开作用域而被析构时,其析构函数被执行,通过其内部封装的普通指针,销毁该指针的目标对象,避免内存泄露.2.为了表现出和普通指针一 ...
- Android系统智能指针的设计思路(轻量级指针、强指针、弱指针)
本博客为原创,转载请注明出处,谢谢. 参考博文:Android系统的智能指针(轻量级指针.强指针和弱指针)的实现原理分析 C++中最容易出错的地方莫过于指针了,指针问题主要有两类,一是内存泄露,二是无 ...
随机推荐
- sqoop进行将Hive 词频统计的结果数据传输到Mysql中
使用sqoop进行将Hive 词频统计的结果数据传输到Mysql中. mysql准备接受数据的数据库与表 hive准备待传输的数据 sqoop进行数据传输 mysql查看传输结果 二:电子书 ...
- JS怎么把for循环出来的东西放到一个数组里
var students=[ {name: "vehicleTravelLicenseCopyBack", id: "a1"}, {name: "ve ...
- Spring框架之jms源码完全解析
Spring框架之jms源码完全解析 我们在前两篇文章中介绍了Spring两大核心IOC(Inversion of Control控制反转)和AOP(Aspect Oriented Programmi ...
- 我用go-zero开发了第一个线上项目
作者:结冰 前言 说在最前面,我是一个外表谦让,内心狂热,外表斯文,内心贪玩的一个普通人.我的职业是程序员,是一个golang语言爱好者,一半是因为golang好用,一半是因为其他语言学不好.我是 ...
- create-react-app 基于TS的项目
写在前面 最近在用React,发现百度了很多都没有找到基于TS的React项目搭建,很多是老的方法已经属于不成功的了,今天我把最新的搭建基于ts的React的项目分享出来 create-react-a ...
- Python高级语法-私有化-私有化理解(4.3.1)
@ 目录 1.说明 2.代码 关于作者 1.说明 __a(私有):只能在类的内部使用,对象使用,但是子类不允许使用,不能导入到其他包 a(protected):可以在子类使用,可以通过对象访问,不能导 ...
- Python高级语法-深浅拷贝-总结(4.2.1)
@ 目录 1.说明 2.代码 关于作者 1.说明 任何可变数据类型都牵扯到深浅拷贝 但是元组,常数等,不可变数据类型,无论浅拷贝,深拷贝都是指向 不管如何嵌套,一旦牵扯到可变数据类型,都会有深浅区别 ...
- Hibernate实现对数据的CRUD
今天主要去看公司的老框架, CRUD用的较多,所以总结一下步骤,以免忘记的时候温习 回顾 JDBC 工作过程: 加载驱动 建立连接 定义sql,发生sql语句 执行sql语句获得执行结果 处理返回结果 ...
- BF,BM,KMP,就这?
为保证代码严谨性,文中所有代码均在 leetcode 刷题网站 AC ,大家可以放心食用. 皇上生辰之际,举国同庆,袁记菜馆作为天下第一饭店,所以被选为这次庆典的菜品供应方,这次庆典对于袁记菜馆是一项 ...
- java 多线程40个问题汇总(转)
java 多线程40个问题汇总,自己也记录一份,如有侵权,联系删除 ref from :http://www.cnblogs.com/xrq730/p/5060921.html 1.多线程作用 - 利 ...