chapter14_近乎自动地管理内存

本文探讨了使用指针时常见的问题,并提出了一种通用句柄类解决方案,包括内存管理和多态实现,以及引用计数思想的改进版句柄类,旨在简化对象管理并提高代码效率。
  1. 使用指针常常出现的问题

    1. 复制一个指针不会导致对指针所指对象的复制,但是会让两个指针指向同一个对象

    2. 删除一个指针不会释放指针所指对象的内存

    3. 删除一个对象但是没有删除指向该对象的指针,会产生一个空悬指针

    4. 定义一个指针却没有初始化

  2. 一个通用的句柄类

    1. 目标

      (1) 这个Handle类是通用的,因此应该使用template

      (2) Handle对象复制内存管理

      (3) 通过Handle类实现多态

    2. 示例

       template<class T>
       class Handle {
       public:
           // "三位一体"函数
           Handle() : p(nullptr) {}
           explicit Handle(T *t) : p(t) {}
           
           // 复制构造函数和赋值运算符函数使用的都是clone这个多态函数,委托给内部对象实现,由于使用了模板,所以在绑定模板时编译器
           Handle(const Handle &s) : p(nullptr) {
               if (s.p != nullptr) {
                   p = s.p->clone();
               }
           }
           Handle &operator=(const Handle &s) {
               if (this != &s) {
                   delete p;
                   p = nullptr;
                   if (s.p != nullptr) {
                       p = s.p->clone();
                   }
               }
               return *this;
           }
      
           ~Handle() { delete p; }
      
           // 重载了三个运算符
           operator bool() const { return p; }
      
           T& operator*() const {
               if (p != nullptr) {
                   return *p;
               } else {
                   throw std::runtime_error("unbound Handle");
               }
           }
      
           T* operator->() const {
               if (p != nullptr) {
                   return p;
               } else {
                   throw std::runtime_error("unbound Handle");
               }
           }
       private:
           T *p;
       };
      
    3. 有了通用句柄类工具以后,编写接口类变的简单,无需在接口类中控制指针的操作

       class Student_Info {
       public:
           // 不需要复制构造函数、赋值运算符函数、析构函数,因为 Handle 句柄类负责管理内存
           Student_Info() = default;
           explicit Student_Info(std::istream &is) { read(is); }
      
           // 这里的new出来的指针只被Handle捕获,不会被其他地方使用
           std::istream &read(std::istream &is) {
               char ch;
               is >> ch;
               if (ch == 'U') {
                   cp = Handle(new Core(is));
               } else {
                   cp = Handle(new Grad(is));
               }
               return is;
           }
      
           // 这里继续使用多态,套了几层:cp属于Handle<Core>指针,重载了->运算符,使用的还是内部T*,也就是Core*或者Grad*
           [[nodiscard]] std::string name() const {
               if (cp) {
                   return cp->name();
               } else {
                   throw std::runtime_error("uninitialized Student");
               }
           }
      
           [[nodiscard]] double grade() const {
               if (cp) {
                   return cp->grade();
               } else {
                   throw std::runtime_error("uninitialized Student");
               }
           }
      
           static bool compare(const Student_Info &s1, const Student_Info &s2) {
               return s1.name() < s2.name();
           }
      
       private:
           Handle<Core> cp;    // 通用句柄类对象管理底层细节,Student_info只需要关注接口
       };
      

      对比一下chapter13中的示例,发现有了通用句柄类Handle以后,编写Student_info得到了简化

  3. 使用引用计数思想的句柄类

    1. 2中的句柄类总是对底层对象进行复制,每次调用复制构造函数或者赋值运算符函数时,都会复制一次

    2. 引用计数的意思是,让不同的句柄类指向同一个底层对象,当计数值为0时再回收内存。这是需要一个计数器,这个计数器也是句柄类的一个成员,保存一个地址

    3. 示例

       template<class T>
       class Ref_handle {
       public:
           // "三位一体"
           Ref_handle() : p(nullptr), refptr(new size_t(1)) {}
           explicit Ref_handle(T *t) : p(t), refptr(new size_t(1)) {}
      
           // 复制构造函数,只需要对计数值+1,不复制底层
           Ref_handle(const Ref_handle &h) : p(h.p), refptr(h.refptr) {
               (*refptr)++;
           }
      
           // 赋值运算符函数,要先判断自我赋值情况,然后让原有位置的计数值-1,新位置的计数值+1,并且修改指针位置
           Ref_handle &operator=(const Ref_handle &h) {
               if (this != &h) {
                   if (--*refptr == 0) {
                       delete refptr;
                       delete p;
                   }
      
                   (*h.refptr)++;
                   refptr = h.refptr;
                   p = h.p;
               }
               return *this;
           }
      
           // 析构函数,判断计数值是否为0,决定是否释放内存
           ~Ref_handle() {
               if (--(*refptr) == 0) {
                   delete refptr;
                   delete p;
               }
           }
      
           // 三个重载运算符 bool, *, ->和之前的Handle版本一样
           ...
      
       private:
           T *p;
           size_t *refptr;  // 计数器的地址
       };
      
  4. 如何自己决定使用复制底层的句柄,还是引用计数的句柄?

    1. 只需要在引用计数的句柄类中加入一个 make_unique 成员函数

       template<class T>
       class Ref_handle {
       public:
           void make_unique() {
               if ((*refptr) == 1) {
                   return;
               }
      
               --(*refptr);
               refptr = new size_t(1);
               p = (p == nullptr ? p->clone() : nullptr);
       }
      

      make_unique()的目标是,把当前的底层对象独立出去。基本思路是:如果计数值为1,说明就是独立的;如果不是的话,调用底层对象的多态clone()方法,并让原计数值-1

    2. 但这样写其实有一个问题,就是模板类强制要求底层对象具有clone()方法,这个地方可以引入中间层,把clone函数定义为外部函数独立出来

       template<class T> 
       T* clone(const T* tp) {
           return tp->clone();
       }
      
       template<>
       Vec<char>* clone(const Vec<char>* vp) {
           return new Vec<char>(*vp);
       }
      

      这样在 make_unique() 方法中,使用的是clone§而非 p->clone():对于实现了clone()方法的底层对象,使用模板方法即可;对于未实现clone()方法的底层对象,例如Vec<char>,下面的是一个模板特化方法,编译器会去调用这个特化的方法用于clone()方法的实例化

  5. 软件工程中的一个基本定理

    所有的问题都可以通过引入一个额外的中间层来解决

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值