-
使用指针常常出现的问题
-
复制一个指针不会导致对指针所指对象的复制,但是会让两个指针指向同一个对象
-
删除一个指针不会释放指针所指对象的内存
-
删除一个对象但是没有删除指向该对象的指针,会产生一个空悬指针
-
定义一个指针却没有初始化
-
-
一个通用的句柄类
-
目标
(1) 这个Handle类是通用的,因此应该使用template
(2) Handle对象复制内存管理
(3) 通过Handle类实现多态
-
示例
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; }; -
有了通用句柄类工具以后,编写接口类变的简单,无需在接口类中控制指针的操作
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得到了简化
-
-
使用引用计数思想的句柄类
-
2中的句柄类总是对底层对象进行复制,每次调用复制构造函数或者赋值运算符函数时,都会复制一次
-
引用计数的意思是,让不同的句柄类指向同一个底层对象,当计数值为0时再回收内存。这是需要一个计数器,这个计数器也是句柄类的一个成员,保存一个地址
-
示例
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; // 计数器的地址 };
-
-
如何自己决定使用复制底层的句柄,还是引用计数的句柄?
-
只需要在引用计数的句柄类中加入一个 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
-
但这样写其实有一个问题,就是模板类强制要求底层对象具有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()方法的实例化
-
-
软件工程中的一个基本定理:
所有的问题都可以通过引入一个额外的中间层来解决
chapter14_近乎自动地管理内存
最新推荐文章于 2025-10-08 20:03:05 发布
本文探讨了使用指针时常见的问题,并提出了一种通用句柄类解决方案,包括内存管理和多态实现,以及引用计数思想的改进版句柄类,旨在简化对象管理并提高代码效率。
2043

被折叠的 条评论
为什么被折叠?



