effective c++:virtual函数在构造函数和析构函数中的注意事项
如不使用自动生成函数要明确拒绝
对于一个类,如果你没有声明,c++会自动生成一个构造函数,一个析构函数,一个copy构造函数和一个copy assignment操作符。
class Empty {
public:
Empty() { ... } // default constructor
Empty(const Empty& rhs) { ... } // copy constructor
~Empty() { ... } // destructor — see below
// for whether it’s virtual
Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};
一般来讲,如果你不想有某个功能,不定义对应函数即可,但对与自动生成的函数,当某些人试图使用它,编译器会自动的生成。
遇到这种情况我们就需要private帮忙,因为编译器产生的都是public函数,如果我们在private中声明,编译器就不会再次创建相同的函数,同时也阻止了别人的调用。但是当成员函数和friend函数还是可以调用private函数。所以如果要做彻底一点,我们可以不去定义,只是声明,如果不管是谁使用这些函数,都会得到一个链接错误。
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // declarations only
HomeForSale& operator=(const HomeForSale&);
};
为多态声明virtual析构函数
首先设计一个TimeKeeper base class 和derived class作为不同的计时方法:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper
返回一个指针指向一个TimeKeeper派生类动态分配对象,因此为了避免内存泄露,应该把创建的对象delete掉:
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy
... // use it
delete ptk; // release it to avoid resource leak
但是这样还是出现了内存泄露,问题出在基类的non-virtual析构函数,对象经过基类指针被删除,而实际上派生类的成分还没被销毁,派生类的析构函数没有能够执行起来。
我们可以在基类析构函数前加一个virtual 就会完整的销毁整个对象。
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly
虚函数的目的是重新实现基类的功能,任何class带有虚函数都需要一个虚析构函数。
当class中不存在虚函数,使其析构函数为virtual是错误的。一个虚函数的实现往往需要附带一些额外的信息来确定编译器在调用虚函数时找到正确的函数指针,这就使得这个由c++写成的类也不再和其他语言相同的声明有着同样的结构,因此也不再具备可移植性。
所以只有当class内含有至少一个虚函数时,我们才声明虚析构函数,而且当class设计不是为了当基类时(没有虚函数),或不是为了具备多态性,就不该声明虚析构函数。
别让异常逃离析构函数
试想,如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。
所以需要在析构函数中做一些额外的工作来避免这一问题,
方法之一在析构函数中抛出异常就abort,例如下面类DBConn调用的析构函数
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
std::abort();
}
}
或者吞下调用close而发生的异常。
如果客户需要对某个操作函数运行期间做出反应,那么class应该提供一个普通函数来执行该操作。
class DBConn {
public:
...
void close() // new function for
{ // client use
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try { // close the connection
db.close(); // if the client didn’t
}
catch (...) { // if closing fails,
make log entry that call to close failed; // note that and
... // terminate or swallow
}
}
}
private:
DBConnection db;
bool closed;
};
不要在构造和析构过程中调用virtual函数
先来看这样一段代码:
class Transaction { // base class for all
public: // transactions
Transaction();
virtual void logTransaction() const = ; // make type-dependent
// log entry
...
};
Transaction::Transaction() // implementation of
{ // base class ctor
...
logTransaction(); // as final action, log this
} // transaction
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
class SellTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
当我们执行BuyTransaction b;这条语句时会调用BuyTransaction 构造函数,但在这之前会首先调用Transaction构造函数,而首先调用的logTransaction也是Transaction版本,这个可以直观的理解为,在Transaction构造时BuyTransaction 尚未开始构造,从而BuyTransaction 中的成员变量也还没有初始化,而虚函数logTransaction几乎一定会调用BuyTransaction 中的成员变量,显而易见调用未经初始化的变量是不允许的,所以基类构造函数中的虚函数形同虚设,析构函数同理。而且如果基类虚函数有实体的话,程序运行期间很难发现原本要调用派生类版本的函数变成了基类版本函数。
唯一能解决 这一问题的方法是在构造和析构函数中不要使用虚函数。例如上例,logTransaction改为non-virtual,在BuyTransaction 构造时传递参数给Transaction
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters )) // pass log info
{ ... } // to base class
... // constructor
private:
static std::string createLogString( parameters );
};
effective c++:virtual函数在构造函数和析构函数中的注意事项的更多相关文章
- 【校招面试 之 C/C++】第10题 C++不在构造函数和析构函数中调用虚函数
1.不要在构造函数中调用虚函数的原因 在概念上,构造函数的工作是为对象进行初始化.在构造函数完成之前,被构造的对象被认为“未完全生成”.当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数, ...
- C++进阶--构造函数和析构函数中的虚函数
//############################################################################ /* 任何时候都不要在构造函数或析构函数中 ...
- 在构造函数和析构函数中调用虚函数------新标准c++程序设计
在构造函数和析构函数中调用虚函数不是多态,因为编译时即可确定调用的是哪个函数.如果本类有该函数,调用的就是本类的函数:如果本类没有,调用的就是直接基类的函数:如果基类没有,调用的就是间接基类的函数,以 ...
- c++有关构造函数和析构函数中调用虚函数问题
今天看了一道迅雷的笔试题目,然后引起一段思考,题目如下: 下列关于虚函数的说法正确的是()A.在构造函数中调用类自己的虚函数,虚函数的动态绑定机制还会生效.B.在析构函数中调用类自己的虚函数,虚函数的 ...
- 31.C++-虚函数之构造函数与析构函数分析
1.构造函数不能为虚函数 当我们将构造函数定义为虚函数时,会直接报错: 首先回忆下以前学的virtual虚函数概念: 如果类定义了虚函数,创建对象时,则会分配内存空间,并且为该父类以及其所有子类的内存 ...
- C++类中函数(构造函数、析构函数、拷贝构造函数、赋值构造函数)
[1]为什么空类可以创建对象呢? 示例代码如下: #include <iostream> using namespace std; class Empty { }; void main() ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...
- 09——绝不在构造和析构函数中调用virtual函数
在base class构造期间,virtual函数不是virtual函数. 构造函数.析构函数中不要调用virtual函数.
- Effective C++(9) 构造函数调用virtual函数会发生什么
问题聚焦: 不要在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果. 让我先来看一下在构造函数里调用一个virtual函数会发生什么结果 Demo class Trans ...
随机推荐
- 每个PHP开发者都应该看的书
PHP这几年口碑很差.关于它的“糟糕设计的汇总”和语法上的矛盾有着大量的讨论,但是主要的抱怨通常是安全.很多PHP站点分分钟被黑掉,甚至一些有经验的.有见识的程序员会说,这门语言本身是不安全的. 我总 ...
- php安装了扩展提示undefined
安装curl扩展后仍然提示如下错误: Call to undefined function curl_init() 使用一下语句 输出NO echo function_exists('curl_ini ...
- MongoDB 学习笔记(四)C# 操作MongoDB
C#驱动对mongodb的操作,目前驱动有两种:官方驱动和samus驱动,不过我个人还是喜欢后者, 因为提供了丰富的linq操作,相当方便. 官方驱动:https://github.com/mongo ...
- Gliffy Diagrams 好用的流程图工具
很好用!加上百度脑图!good!
- HTTP头学习汇总
在开发http请求的时候,对HTTP头部信息一知半解,各种百度谷歌汇总一下学习到的资料. http简介 HTTP(HyperTextTransferProtocol)是超文本传输协议的缩写,它用于 ...
- github概念和实战
fork: 通过fork操作,你将拥有了别人创建的repo的ownership,但是url却变成了/youraccount/repo,这时你将可以做git push操作 clone: 该命令是直接将r ...
- grub rescue修复引导项
1.需要先找到linux系统盘所在到目录 grub rescue > ls 然后依次 ls (hd0,msdosX)/ 假如我们到系统在msdos2 2.输入 set root=(hd0,msd ...
- visual studio 2015常用快捷键
常用快捷键 技巧 0.0 删除文件中的当前行: Home + Shife-End + Delete 技巧 1.1 避免意外复制一个空白行 工具->选项->文本编辑器->所有语言-&g ...
- kafka迁移与扩容
参考官网site: http://kafka.apache.org/documentation.html#basic_ops_cluster_expansion https://cwiki.apach ...
- C# 编写Windows Service(windows服务程序)【转载】
[转]http://www.cnblogs.com/bluestorm/p/3510398.html Windows Service简介: 一个Windows服务程序是在Windows操作系统下能完成 ...