C++学习之路: 智能指针入门
引言: 编写智能指针的要点:
a) 构造函数接收堆内存
b) 析构函数释放内存
c) 必要时要禁止值语义。
d) 重载*与->两个操作符
1. 简易的智能指针 。
1 #ifndef START_PTR_H 2 #define START_PTR_H 3 4 #include <iostream> 5 using namespace std; 6 7 class Animal 8 { 9 public: 10 Animal() { cout << "Animal" << endl ;} 11 ~Animal() { cout << "~Animal" << endl; } 12 13 void run() { cout << "Animal is running....." << endl; } 14 15 }; 16 17 18 19 20 class SmartPtr 21 { 22 public: 23 SmartPtr(Animal * ptr = NULL) //采用缺省函数 24 :ptr_(ptr) 25 { 26 27 } 28 29 ~SmartPtr() 30 { 31 delete ptr_ ; 32 } 33 34 Animal &operator*() //需要重载一个const 版本,否则const SmartPtr 无法解引用 35 { 36 return *ptr_ ; 37 } 38 const Animal &operator*() const 39 { 40 return *ptr_ ; 41 } 42 43 Animal *operator->() 44 { 45 return ptr_ ; 46 } 47 const Animal *operator->() const 48 { 49 return ptr_ ; 50 } 51 private: 52 SmartPtr(const SmartPtr &other) 53 :ptr_(other.ptr_) 54 { 55 56 } 57 SmartPtr &operator=(const SmartPtr &other) 58 { 59 if(this != &other) 60 { 61 ptr_ = other.ptr_ ; 62 } 63 return *this ; 64 } 65 66 Animal *ptr_ ; 67 } ; 68 69 #endif /*START_PTR_H*/
测试代码:
1 #include "smartPtr.h" 2 #include <iostream> 3 using namespace std; 4 5 int main(int argc, const char *argv[]) 6 { 7 { 8 SmartPtr ptr(new Animal) ; //这个智能指针的生命周期仅限于这个花括号内部, 那么它 持有的对象 的生命 也只存在于这对括号中 9 ptr->run() ; //因为智能指针的构造函数中对持有对象进行定义, 在智能指针析构 函数中对持有对象进行释放, 实现自动化管理获取的资源 10 } 11 return 0; 12 }
Animal Animal is running..... ~Animal
打印结果, 智能指针实现了内存的自动管理
总结:
其实
智能指针是个类对象,但是行为表现的像一个指针。它有三种操作符
a) . 调用的是智能指针这个对象本身的方法。
b) * 调用的是 解引用出持有的对象
c) -> 调用的是 调用持有对象内部的成员
2. 上例代码十分的笨拙, 对于每一类都需要都要手写一个智能指针, 重复的代码十分冗余, 所以我们采用模板技术重新改写;
对于指向每个对象(例如string s1,s2)只产生 对应的 一类 智能指针
先理清一个概念 CountPtr ptr1(s1), 和 CountPtr ptr(s2) 两个智能指针 指向的类型相同,但是对象不同, 所以它们的计数也不同
我们在 用CountPtr ptr3(ptr2),当我们构造拷贝 ptr2 给ptr3 时, ptr2和ptr3 会共享一个计数器,计数为2.
但是ptr1的计数还是1.
当计数为0时, 再执行析构。
1 #ifndef COUNTER_HPP 2 #define COUNTER_HPP 3 4 template <typename T> 5 class CounterPtr 6 { 7 public: 8 typedef T value_type; 9 typedef T *pointer; 10 typedef const T *const_pointer; 11 typedef T &reference; 12 typedef const T &const_reference; 13 14 explicit CounterPtr(T *p = NULL); 15 CounterPtr(const CounterPtr<T> &other); 16 ~CounterPtr(); 17 18 CounterPtr<T> &operator=(const CounterPtr<T> &other); 19 20 reference operator*() const throw() 21 { 22 return *ptr_; 23 } 24 25 pointer operator->() const throw() 26 { 27 return ptr_; 28 } 29 30 size_t count() const { return *count_; } 31 32 void swap(CounterPtr<T> &other) throw() 33 { 34 std::swap(ptr_, other.ptr_); 35 std::swap(count_, other.count_); 36 } 37 38 void reset() throw() 39 { 40 dispose(); 41 } 42 43 pointer get() const throw() 44 { 45 return ptr_; 46 } 47 48 bool unique() const throw() 49 { 50 return *count_ == 1; 51 } 52 53 operator bool() 54 { 55 return ptr_ != NULL; 56 } 57 58 private: 59 60 void dispose() 61 { 62 if(--*count_ == 0) 63 { 64 delete ptr_; 65 delete count_; 66 } 67 } 68 69 70 T *ptr_; 71 size_t *count_; //引用计数 思考为什么采用指针 同样也不可以使用 static个对象,否则 对于一种类 我们智能指向它一个对象,例如string s1,s2 假如我们指向了 s1, 那么再指向 s2时 共用了 计数, 出现逻辑错误 72 }; 73 74 template <typename T> 75 CounterPtr<T>::CounterPtr(T *p) 76 :ptr_(p), count_(new size_t(1)) 77 { 78 79 } 80 81 template <typename T> 82 CounterPtr<T>::CounterPtr(const CounterPtr<T> &other) 83 :ptr_(other.ptr_), count_(other.count_) 84 { 85 ++*count_; //引用计数+1 86 } 87 88 template <typename T> 89 CounterPtr<T>::~CounterPtr() 90 { 91 dispose(); 92 } 93 94 template <typename T> 95 CounterPtr<T> &CounterPtr<T>::operator=(const CounterPtr<T> &other) 96 { 97 ++*other.count_; //先对other进行+1,这样不用处理自身赋值 98 dispose(); 99 ptr_ = other.ptr_; 100 count_ = other.count_; 101 102 return *this; 103 } 104 105 106 107 #endif //COUNTER_HPP
以上代码 不过是例1的升级版, 用模板计数实现, 不过再此基础上又增加了一个多对象共用的 计数器, 为了防止其中一个 智能指针删除 持有对象后, 其他智能指针变成悬垂指针, 所以只有当计数器为0时我们才释放 资源(即持有资源)。
这个模板 有一个错误使用的例子。
1 #include "CountPtr.hpp" 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 6 using namespace std; 7 8 int main(int argc, const char *argv[]) 9 { 10 string s1("hello"); 11 string s2("world") ; 12 CountPtr ptr1(s1) ; //ptr1 ->s1 13 CountPtr ptr2(ptr1) ; //利用ptr1 的拷贝构造函数 构造了ptr2 ,计数++ 14 15 //错误使用 16 CountPtr ptr3(s1) ; //此处我们又创建一套指针系统,因为我们是手动调用构造函数 创建新的 智能指针去指向 s1, 新的计数器 初始化计数为1; 17 //ptr3 是独立于 ptr1 和 ptr2 的新的一套指针, 它的存在也没有让ptr1 和ptr2得知, 所以ptr2 和 ptr1 计数也没有增加; 18 //当 ptr3 释放掉 s1, 那么ptr1 和ptr2 这整套指针系统全部都悬空, 导致逻辑错误 切记此例 19 20 return 0; 21 }
上例不是一种标准 实现, C++是一种复杂的 语言, 有很多的坑, 可能单独实现某功能是对的, 但是它不能作为 一套系统的 工具, 会出现许多 难以察觉的 bug,
为了避免这些bug, 我们追求一种标准实现
1 #include "CountPtr.hpp" 2 3 using namespace std; 4 5 int main(int argc, const char *argv[]) 6 { 7 CountPtr ptr1(new string("hello world")) ; //智能指针指向的内存都是堆上内存, 8 //我们上例的智能指针指向的 string 是在main中定义的,在栈里,是一种错误 9 CountPtr ptr2(ptr1) ; //当智能指针析构时会delete string, 这是未定义行为, 是错误的, 所以智能指针必须指向处于堆内存的对象 10 11 CountPtr ptr3(make_ptr(string)) ; 12 return 0; 13 }
上述 6,9,12 都是标准实现, 除此之外, 不要自己写出奇葩的实现。
3. 了解了智能指针的实现以后, 我们看下强大的C++11标准给我们提供了哪些智能指针,及接口的使用和注意事项
a) scoped_ptr
的实现并不复杂, 和我们例2的的实现原理相同, 不过加了很多的异常处理和接口保护, 它是一款真正的工业级代码, 并不是我们写出来的玩具
有兴趣可以去boost库一探究竟
1 1 #include <iostream> 2 2 #include <boost/scoped_ptr.hpp> 3 3 using namespace std; 4 4 using namespace boost; 5 5 6 6 7 7 class Test 8 8 { 9 9 public: 10 10 Test() { cout << "Test" << endl;} 11 11 ~Test() { cout << "~Test" << endl;} 12 12 }; 13 13 14 14 int main(int argc, char const *argv[]) 15 15 { 16 16 scoped_ptr<Test> ptr(new Test); 17 17 return 0; 18 18 }
结果打印:
Test
~Test
scoped_ptr是一种最简单的智能指针, 它不可复制也不可赋值。
b)Unique_ptr
下面介绍一下scoped_ptr的加强版, C++11标准中提供了一种更加强大的智能指针,它和scoped_ptr的区别在于 添加了 右值引用 和(move)语义;
它也是不可复制和不可赋值类。 但是拥有移动复制, 和移动赋值的能力
#include <iostream> #include <string> #include <vector> #include <memory> using namespace std; class Test { public: Test() { cout << "Test" << endl;} ~Test() { cout << "~Test" << endl;} }; int main(int argc, const char *argv[]) { unique_ptr<Test> ptr(new Test); //unique_ptr<Test> ptr2(ptr); //没有拷贝构造 //unique_ptr<Test> ptr2; //ptr2 = ptr; unique_ptr<Test> ptr2(std::move(ptr)); unique_ptr<Test> ptr3; ptr3 = std::move(ptr2); return 0; }
同样打印正确, 这里不在演示。
编译时记得 加-std=c++0x
下面验证一下确实Unique 拥有移动赋值的能力, 看下它的机制。
1 #include <iostream> 2 #include <memory> 3 #include <vector> 4 using namespace std; 5 6 class Test 7 { 8 public: 9 Test() { cout << "Test" << endl;} 10 ~Test() { cout << "~Test" << endl;} 11 12 Test(Test &&t) { cout << "move" << endl; } 13 Test &operator=(Test &&t) 14 { 15 16 } 17 18 private: 19 Test(const Test &); 20 void operator=(const Test &); 21 }; 22 23 int main(int argc, char const *argv[]) 24 { 25 vector<Test> coll; 26 coll.push_back(Test()); 27 return 0; 28 }
结果打印:
Test move ~Test ~Test
coll.push_back(Test()), 这一行调用构造函数构造了一个temp零时变量,然后用右值移动构造函数 把 temp变量的值 移动给 coll中的元素。 然后零时变量失效,无所谓,temp也只能在26行存活。
这样便节省了 复制temp的开销。
如果是旧标准的话:
temp 变量的构造以及 复制temp 将会打印出 两次Test ;但是实验证明我们只构造了一次Test即达到了目的,节约了一次开销。
但是temp变量的 析构却在所难免。
c) unique_ptr正是拥有右值传递的能力,即使它不可复制和不可赋值 我们还可以用vector容器来装很多unique_ptr对象
1 #include <iostream> 2 #include <memory> 3 #include <vector> 4 using namespace std; 5 6 class Test 7 { 8 public: 9 Test() { cout << "Test" << endl;} 10 ~Test() { cout << "~Test" << endl;} 11 12 private: 13 Test(const Test &); 14 void operator=(const Test &); 15 }; 16 17 int main(int argc, char const *argv[]) 18 { 19 vector<unique_ptr<Test> > coll; 20 coll.push_back(unique_ptr<Test>(new Test)); 21 return 0; 22 }
打印结果:正确
1 Test 2 ~Test
我们可以看到, Test对象并没有析构两次, 在以前的久标准中,情况如下
在20行我们创建了一个unique_ptr的temp变量, 它指向一个在堆上的Test的对象, 这时 coll复制了 unique_ptr, 然后temp销毁,Test 被复制版的unique_ptr(存在于coll中)重新指向,所以没有析构
在新标准中添加了move 右值移动赋值以后:
unique_ptr构建temp变量以后, 直接被move到 college中, 省去了复制和析构temp的开销。