闲话C++智能指针
提到指针, 大家就会想到memory leak
。 C/C++ 使用堆内存都需要程序员手动搭配使用 malloc/new
申请, free/delete
释放。
void func()
{
string* str = new string("example"); // 这里如果new 失败, 不会有问题, 编译器保证
foo(str); // 可能抛异常
delete str; // 可能人为忘记
}
1. RAII
如上两个原因, 如何解决呢? C++ 首先想到用 RAII
的思想, 将 new
出来的内存地址用对象进行封装。 即: 利用对象的构造函数对 new
返回的内存地址进行存储和封装使用,使用}
自动在对象的析构函数中 执行 delete
。
// smart_ptr.h
class smart_ptr {
public:
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {}
~smart_ptr() {delete ptr_;}
private:
string * ptr_;
};
// main.cpp
int main()
{
smart_ptr ptr{new string("example")}; // -> smart_ptr(string* ptr) : ptr_(ptr) {}
return 1;
} // -> ~smart_ptr() {delete ptr_;}
如上, 就解决了 memory leak
的问题, 但是RAII还是无法避免 指针的如下两个问题
// smart_ptr.h
class smart_ptr {
public:
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {}
~smart_ptr() {delete ptr_;}
// 仿指针行为
string& operator*() {return *ptr_;};
string* operator->() {return ptr_;};
private:
string * ptr_;
};
// main.cpp
void func1(smart_ptr ptr)
{
cout << *ptr << endl;
} // 1. -> ~smart_ptr() {delete ptr_;}
int main()
{
smart_ptr ptr{new string("example")};
smart_ptr ptr2 = ptr;
func1(ptr);
cout << *ptr2 << endl; // dangling pointer
return 1;
} // 2. -> ~smart_ptr() {delete ptr_;}
如上, 如果该指针对象发生 拷贝行为
, 那么RAII机制的}
势必会多次释放该指针, 即 double free
; 其次如果该指针释放后仍被使用,就会造成访问无效内存。即 dangling pointer
迷途指针或者叫悬挂指针。
解决上述两个问题, 我们可以直接把 拷贝行为
禁用。
// smart_ptr.h
class smart_ptr {
public:
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {}
~smart_ptr() {delete ptr_;}
// 仿指针行为
string& operator*() {return *ptr_;};
string* operator->() {return ptr_;};
// 禁用拷贝行为
smart_ptr(smart_ptr& ptr) = delete;
smart_ptr& operator=(smart_ptr& ptr) = delete;
private:
string * ptr_;
};
// main.cpp
int main()
{
smart_ptr ptr{new string("example")};
smart_ptr ptr2 = ptr; // 编译失败 use of deleted function 'smart_ptr::smart_ptr(smart_ptr&)'
func1(ptr); // 编译失败 use of deleted function 'smart_ptr::smart_ptr(smart_ptr&)'
cout << *ptr2 << endl;
return 1;
}
unique_ptr
显然这方法属于 ”解决提出问题的人 而并非解决问题“,拷贝 根本上是 资源的所有权发生变化, 那资源的释放权自然就会从一份变成多份。 我们这里需要改造用户的拷贝行为
.这里就可以分为2个场景, 该资源是单一使用还是共享使用。
如果是单一所有, 那么我们可以移动资源
.
// smart_ptr.cpp
class smart_ptr {
public:
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {}
~smart_ptr() {delete ptr_;}
// 仿指针行为
string& operator*() {return *ptr_;};
string* operator->() {return ptr_;};
// 禁用拷贝行为
smart_ptr(smart_ptr& ptr) = delete;
smart_ptr& operator=(smart_ptr& ptr) = delete;
// 资源自动移动
smart_ptr(smart_ptr&& ptr) {
ptr_ = ptr.release();
}
smart_ptr& operator=(smart_ptr rhs) // 拷贝和移动都兼容
{
rhs.swap(*this);
return *this;
}
private:
string * ptr_;
string* release()
{ // 新弄一个指针地址, 删除老指针地址
string* newptr = ptr_;
ptr_ = nullptr;
return newptr;
}
void swap(smart_ptr& rhs)
{ // 交换指针地址内容
using std::swap;
swap(ptr_, rhs.ptr_);
}
};
//main.cpp
int main()
{
smart_ptr ptr{new string("example")};
smart_ptr ptr2{move(ptr)}; // 移动构造函数
cout << *ptr2 << endl;
smart_ptr ptr3{nullptr};
ptr3 = move(ptr2); // 移动赋值
cout << *ptr3 << endl;
return 1;
}
以上就是 std::unique_ptr
的 简易实现啦。
shared_ptr
那如果该资源是共享资源呢? 这就涉及到RC技术了reference count
引用计数。这是已经常见的垃圾回收技术。
要实现RC, 首先就要有个单例的对象存储该资源的count. 这样多份资源在共享该资源是, count 保证只有一份。
因此 我们要对
private:
string * ptr_;
share_count* share_count_; // 增加共享计数
进行单例对象改造。
首先 1. 构造和析构函数要适配share_count, 即 构造的时候如果该指针没有share_count 需要new 一个, 如果析构时share_count_为1, 那需要释放资源。
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {
share_count_ = new share_count();
share_count_->add_count();
}
~smart_ptr() {
if (share_count_->reduce_count()) {
delete ptr_;
delete share_count_;
}
}
其次 2. 修改拷贝。实现对share_count 的增减。
// 拷贝
smart_ptr(smart_ptr& rhs) {
ptr_ = rhs.ptr_;
share_count_ = rhs.share_count_;
share_count_->add_count();
};
smart_ptr& operator=(smart_ptr& rhs) {
// 用拷贝构造把 右值构造好 然后swap 给 左值, 返回左值。
// 原本的this对象自己会析构。
smart_ptr(rhs).swap(*this);
return *this;
};
移动构造和赋值同unique_ptr
的实现,这里就不赘述啦。 移动不需要动share_count 因为并没有新增资源使用者,最终 share_ptr
简易实现如下:
// smart_ptr
class share_count {
public:
share_count() : count_(0) {}
bool reduce_count()
{
return --count_ == 0;
}
void add_count()
{
count_++;
}
int use_count() {return count_;};
private:
int count_;
};
class smart_ptr {
public:
// RAII
smart_ptr(string* ptr) : ptr_(ptr) {
share_count_ = new share_count();
share_count_->add_count();
}
~smart_ptr() {
if (share_count_->reduce_count()) {
delete ptr_;
delete share_count_;
}
}
// 仿指针行为
string& operator*() {return *ptr_;};
string* operator->() {return ptr_;};
// 拷贝
smart_ptr(smart_ptr& rhs) {
ptr_ = rhs.ptr_;
share_count_ = rhs.share_count_;
share_count_->add_count();
};
smart_ptr& operator=(smart_ptr& rhs) {
// 用拷贝构造把 右值构造好 然后swap 给 左值, 返回左值。
// 原本的this对象自己会析构。
smart_ptr(rhs).swap(*this);
return *this;
};
int use_count() {return share_count_->use_count();};
private:
string* ptr_;
share_count* share_count_;
string* release()
{
string *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(share_count_, rhs.share_count_);
}
};
// main.cpp
int main()
{
smart_ptr ptr1{new string("example")};
cout << ptr1.use_count() << endl; // use_count = 1
smart_ptr ptr2(ptr1);
cout << ptr1.use_count() << endl; // // use_count = 2
{
auto ptr3 = ptr1;
cout << ptr1.use_count() << endl; // // use_count = 3
}
cout << ptr1.use_count() << endl; // // use_count = 2
{
smart_ptr ptr5{nullptr};
ptr5 = ptr1;
cout << ptr1.use_count() << endl; // use_count = 3
}
cout << ptr1.use_count() << endl; // use_count = 2
return 1;
}
以上就是shared_ptr
的简易实现啦。
std::unique_ptr
了解原理,最终还是服务于更好的使用标准库。标准库实现自然要基于模板, 只需要将实际类型替换成 T
, 那先前代码替换string为T后如下
template <typename T>
class smart_ptr {
public:
smart_ptr(T* ptr) : ptr_(ptr) {}
T* Get() const { return ptr_;}
~smart_ptr() { delete ptr_;};
// * / ->
T& operator*() { return *ptr_; }
T* operator->() { return ptr_; }
// 指针禁止拷贝
smart_ptr(const smart_ptr &) = delete;
smart_ptr& operator=(const smart_ptr &) = delete;
// 使用移动拷贝构造, 如果要传值, 可以传右值
smart_ptr(smart_ptr&& other) {
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs) {
rhs.swap(*this);
return *this;
}
private:
T* ptr_;
T* release()
{
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr & rhs) {
using std::swap;
swap(ptr_, rhs.ptr_);
}
};
// main.cpp
int main()
{
smart_ptr<string> up1(new string("string"));
cout << *up1 << endl;
unique_ptr<string> stdup1(new string("string"));
cout << *stdup1 << endl;
// smart_ptr<string> up2(up1); use of deleted function 'smart_ptr<T>::smart_ptr(const smart_ptr<T>&) [with T = std::__cxx11::basic_string<char>]'
// unique_ptr<string> stdup2(stdup1); use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]'
// smart_ptr<string> up3 = up1; use of deleted function 'smart_ptr<T>::smart_ptr(const smart_ptr<T>&) [with T = std::__cxx11::basic_string<char>]'
// unique_ptr<string> stdup3 = stdup1; use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]'
return 1;
}
如上, 标准库也是直接简单粗暴的将拷贝赋值和拷贝构造给delete了。unique_ptr
实现简单, 使用也简单, 可以说他就做三件事。
void print_addr(unique_ptr<string>&& ptr)
{
cout << &*ptr << endl; // // 0x1b1b40
}
int main()
{
// 推荐用法
// 1. 定义
auto up = make_unique<string>("example");
cout << &*up << endl; // 0x1b1b40
// 2. 移动构造
unique_ptr<string> up2 = std::move(up);
cout << &*up2 << endl; // 0x1b1b40
// 3. 传参
print_addr(move(up2)); // 0x1b1b40
return 1;
}
了解原理, 记住make_unique
,unique_ptr
,std::move()
, 这三个关键词,unique_ptr
就掌握啦。
std::share_ptr
同理, share_ptr
对比unique_ptr
, 原理上只是多了可拷贝功能(使用了RC)。
int main()
{
// 1. 定义
auto sp = make_shared<string>("example");
// 2. 拷贝赋值
shared_ptr<string> sp2 = sp;
cout << sp.use_count(); // 2
// 3. 拷贝构造
shared_ptr<string> sp3(sp);
cout << sp.use_count(); // 3
// 4. 移动赋值
shared_ptr<string> sp4 = std::move(sp);
cout << sp4.use_count(); // 3
// 5. 移动构造, 转移所有权, 不新增使用者
shared_ptr<string> sp5(move(sp4));
cout << sp5.use_count() << endl; // 3
// 6. 新增一个使用者
print_addr(sp5); // 4
// 7. 移动所有权, 未新增使用者
print_addr(move(sp5)); // 3
}
如上share_ptr
又能拷贝又能移动。使用起来非常丝滑。
智能指针作为返回值
- 编译器对于这种用called func 返回智能指针, 对临时的unique_ptr, 他自己会确保ownership 传递给P. 编译器不会傻乎乎的先释放在重新构造一个新的给调用者的, 具体可以搜搜RVO。
// unique_ptr<string> BuildString()
smart_ptr<string> BuildString()
{
// return make_unique<string>("example");
return smart_ptr<string>(new string("example"));
}; // 并不会调用析构函数释放 string("example")
int main()
{
// 1. 定义
// unique_ptr<string> sp = BuildString();
smart_ptr<string> sp = BuildString();
cout << *sp << endl;
}
当然必须传值不可以返回引用或者右值。
// unique_ptr<string>& BuildString() // 编译不过
// unique_ptr<string>&& BuildString() // BUG
unique_ptr<string> BuildString()
{
return make_unique<string>("example");
}; // 并不会调用析构函数释放 string("example")
int main()
{
// 1. 定义
unique_ptr<string> sp = BuildString();
cout << *sp << endl;
}
shared_ptr
也同理, 因为函数结束的}
势必会减少引用计数或者直接释放该资源, 因此智能按值往外传。
// shared_ptr<string>& BuildString() // 编译不过
// shared_ptr<string>&& BuildString() // BUG
string* BuildString()
// shared_ptr<string> BuildString()
{
return make_shared<string>("example").get();
}; // 并不会调用析构函数释放 string("example")
int main()
{
// 1. 定义
string* sp = BuildString(); // BUG
cout << *sp << endl; // 无效内存
}