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的开销。



posted @ 2014-10-10 00:42  tilly_chang  阅读(389)  评论(0编辑  收藏  举报