前言
这是我两年前第一个CSDN博客文章, 文章中我犯了所有初学者都会犯的问题, 博客文章没有起因, 没有说明解决什么问题, 弄得人一头雾水, 现在回过头, 需要从头梳理, 是的, 我自己都不知道当时自己在说什么.
一、习题内容
c++ primer 15.41习题, 重新实现你的类,这次使用指向Query_base
的内置指针而非shared_ptr
。
请注意,做出上述改动后将不能再使用合成的拷贝控制成员。
二、坑的地方
现在回顾起来, 坑的地方是如何实现share_ptr
的功能, 原程序设计, 使用share_ptr
, 就不必过于在意所谓深拷贝和浅拷贝问题, 智能指针直接接管, 没有浅拷贝问题, 以及后续的内存泄漏或多次delete
问题.
还有我当时并没注意一个细节, 导致后续乌龙, 就是面向对象, 一定要把析构函数做virtual
函数, 否则会导致内存泄漏.
另外, 类的书写顺序也很重要, 先声明, 后定义, 互相有依赖的, 被依赖类要最先声明定义, 没有完整声明的类, 可以成为另一个类的成员, 但需要是指针形式, 否则, 就是使用不完整类报错.
总结
以下是原文内容:
c++ primer 15.41 坑,大坑。
github 的答案是掠过的😱。这真是太不幸了,没有提示做这道题,简直自虐。
参考13章的代码,需要自己计算指针引用数量,但这还不够。因为15.41这个结构和13章结构不同。此题结构是指针嵌套!直接粗鲁的delete,会导致嵌套指针内存泄漏!!!
对付的办法也比较简单,就是在指针引用为0时,强制进行~QueryBase()析构。
问题是QueryBase在Query之后定义,需要用个友元函数返回一下。
强制QueryBase析构的妙处是,进行Query的递归析构。这样就可以一层一层的析构,不会删了外层指针,把里面的指针丢失导致内存泄漏。
#ifndef DELETEPTR
#define DELETEPTR
#include <string>
#include <iostream>
struct Base;
struct wordBase;
struct deletePtr
{
deletePtr(const deletePtr &rhs)
: basePtr(rhs.basePtr), cnt(rhs.cnt) { ++(*cnt); }
deletePtr(const std::string &rhs);
~deletePtr();
private:
friend deletePtr operator~(const deletePtr &);
friend deletePtr operator&(const deletePtr &, const deletePtr &);
deletePtr(Base *iter) : basePtr(iter), cnt(new int(1)) {}
Base *basePtr = nullptr;
int *cnt = nullptr;
};
struct Base
{
virtual ~Base()
{
std::cout << "virtual ~Base()" << std::endl;
};
};
deletePtr::~deletePtr()
{
--(*cnt);
if (*cnt == 0)
{
delete basePtr;
delete cnt;
std::cout << "~deletePtr()" << std::endl;
}
}
struct wordBase : public Base
{
wordBase(const std::string &rhs) : word(rhs) {}
~wordBase()
{
std::cout << "~wordBase()" << std::endl;
}
private:
std::string word;
};
deletePtr::deletePtr(const std::string &rhs)
: basePtr(new wordBase(rhs)), cnt(new int(1)){};
struct notDeletePtr : public Base
{
~notDeletePtr()
{
std::cout << "~notDeletePtr()" << std::endl;
}
private:
friend deletePtr operator~(const deletePtr &);
notDeletePtr(const deletePtr &rhs) : delPtr(rhs) {}
deletePtr delPtr;
};
deletePtr operator~(const deletePtr &rhs)
{
Base *Ptr = new notDeletePtr(rhs);
return Ptr;
}
struct binBase : public Base
{
~binBase()
{
std::cout << "~binBase()" << std::endl;
}
protected:
binBase(const deletePtr &rhsOne, const deletePtr &rhsTwo, std::string s)
: lhs(rhsOne), rhs(rhsTwo), op(s) {}
deletePtr lhs;
deletePtr rhs;
std::string op;
};
struct andDeletePtr : public binBase
{
andDeletePtr(const deletePtr &lhs, const deletePtr &rhs)
: binBase(lhs, rhs, "&") {}
~andDeletePtr()
{
std::cout << "~andDeletePtr()" << std::endl;
}
private:
friend deletePtr operator&(const deletePtr &, const deletePtr &);
};
deletePtr operator&(const deletePtr &lhs, const deletePtr &rhs)
{
Base *ptr = new andDeletePtr(lhs, rhs);
return ptr;
}
#endif
以上是示例代码,我后来发现,析构delete 一个类指针的时候,如果要删除的类没有完成定义,析构函数一定要将定义放在这个类完成定义之后!!!
否则不能自动的进行递归析构,也就是不能真正的删除这个指针。
所以现在可以说,可以把这个坑填一填了,总结如下:
1,请务必理解RAII的设计原理,争取做到谁开出的资源谁删除,用智能指针,或引用计数追踪,在析构函数delete。
2,类的构建规则要服从:
类声明1,2,3.
类定义实现1,2,3.
类的成员函数在类的定义中声明1,2,3.
在所有相关引用类的定义实现完成后,定义实现类成员函数1,2,3
否则很可能出现因使用不完整类导致的错误,比如内存泄漏。