【转自tonywearme】我为什么要为纯虚函数写出实现
2015-05-04 22:54
134 查看
在帮新同事进行代码审查的时候,常常会发现这样的问题:代码中原有基类B和派生类D1,现在新加一个派生类D2,它有一个函数f2()。由于经验不足,新同事并没有注意到D1也有类似的函数f1()。于是造成了类似的代码出现在了两个地方,代码冗余造成将来的维护工作异常困难。注意到f()实际上是一个通用的行为,我们可以把它抽出来放到基类中,如下所示。
class B
{
public:
virtual void f();
};
void B::f()
{
//f默认的实现方式..
}
class D1: public B {...};
class D2: public B {...};
这里有两点。基类里面的f()有函数定义是为了避免代码冗余,因为D1和D2关于f的实现都是相同的。f()是虚函数是考虑到将来万一有一个派生类需要f()有不同的实现方式时,它可以重新定义f。
到目前为止一切顺利。好了,假设现在另一个同事又加了一个派生类D3,它要求f()有不同的实现方式。但是,他忘记了重新定义f。。这是一个灾难。
class D3: public B {...};
B* b = new D3;
// f做的动作不是我希望的!
b->f();
注意,这个问题的本质并不是说基类不能有缺省的实现,而是说如果派生类需要使用基类缺省的实现,必须显式地表示出来。所以,我们需要一个能够强制派生类定义f的方式,答案就是纯虚函数。
class B
{
public:
virtual void f() = 0;
};
class D3: public B {...};
B* b = new D3; // 编译报错:抽象类无法实例化
b->f();
我们知道有纯虚函数的类是抽象类,而抽象类是无法实例化的。如果D3没有实现f,那么D3也无法实例化。这就要求每个要实例化的子类都必须显示地实现纯虚函数f。另一方面,f的缺省实现放哪里?
一种方法是把缺省实现作为另一个非虚函数放在基类里,当然它是保护的,这样外界无妨直接访问而派生类可以通过调用该函数来达到缺省的行为。
class B
{
public:
virtual void f() = 0;
protected:
void f_impl();
};
void B::f_impl()
{
// 缺省实现..
}
// D2类似
class D1: public B
{
public:
virtual void f()
};
void D1::f()
{
// 对于缺省实现已经够用的派生类来说,调用基类的缺省实现就可以了
f_impl();
}
class D3: public B
{
public:
virtual void f()
};
void D3::f()
{
// 特殊实现..
}
第二种方法是把缺省的实现放入基类纯虚函数的函数体。没错,纯虚函数可以有函数定义存在,它的作用就是提供虚函数缺省的实现方式。和一般虚函数不同,派生类必须重新定义纯虚函数。如果需要使用缺省实现,也必须显式地调用基类的相关函数。
class B
{
public:
virtual void f()
};
void B::f()
{
// 缺省实现..
}
// D2类似
class D1: public B
{
public:
virtual void f()
};
void D1::f()
{
// 显式调用基类的缺省实现
B::f();
}
这种方法的好处是少了一个需要维护的函数,缺点是客户代码可以直接调用基类的缺省实现。
B* b = new D1;
b->B::f();
普通的虚函数提供了接口与缺省实现。(公有继承的)派生类继承接口的同时可以自动继承接口的缺省实现(免费的午餐),或者选择重新定义该函数来特化自己的行为。风险是以后需要定制此行为的派生类可能因为忘记重定义而错误地使用缺省实现。
纯虚函数只提供了接口。但是具有函数体的纯虚函数同时还提供了缺省实现。派生类必须显式定义接口。可以通过显式调用基类函数来完成缺省行为。
为了完整,顺便补充一下,公有非虚函数提供了接口与强制实现。也就是说所有的派生类必须继承这个接口及其实现。
class B
{
public:
virtual void f();
};
void B::f()
{
//f默认的实现方式..
}
class D1: public B {...};
class D2: public B {...};
这里有两点。基类里面的f()有函数定义是为了避免代码冗余,因为D1和D2关于f的实现都是相同的。f()是虚函数是考虑到将来万一有一个派生类需要f()有不同的实现方式时,它可以重新定义f。
到目前为止一切顺利。好了,假设现在另一个同事又加了一个派生类D3,它要求f()有不同的实现方式。但是,他忘记了重新定义f。。这是一个灾难。
class D3: public B {...};
B* b = new D3;
// f做的动作不是我希望的!
b->f();
注意,这个问题的本质并不是说基类不能有缺省的实现,而是说如果派生类需要使用基类缺省的实现,必须显式地表示出来。所以,我们需要一个能够强制派生类定义f的方式,答案就是纯虚函数。
class B
{
public:
virtual void f() = 0;
};
class D3: public B {...};
B* b = new D3; // 编译报错:抽象类无法实例化
b->f();
我们知道有纯虚函数的类是抽象类,而抽象类是无法实例化的。如果D3没有实现f,那么D3也无法实例化。这就要求每个要实例化的子类都必须显示地实现纯虚函数f。另一方面,f的缺省实现放哪里?
一种方法是把缺省实现作为另一个非虚函数放在基类里,当然它是保护的,这样外界无妨直接访问而派生类可以通过调用该函数来达到缺省的行为。
class B
{
public:
virtual void f() = 0;
protected:
void f_impl();
};
void B::f_impl()
{
// 缺省实现..
}
// D2类似
class D1: public B
{
public:
virtual void f()
};
void D1::f()
{
// 对于缺省实现已经够用的派生类来说,调用基类的缺省实现就可以了
f_impl();
}
class D3: public B
{
public:
virtual void f()
};
void D3::f()
{
// 特殊实现..
}
第二种方法是把缺省的实现放入基类纯虚函数的函数体。没错,纯虚函数可以有函数定义存在,它的作用就是提供虚函数缺省的实现方式。和一般虚函数不同,派生类必须重新定义纯虚函数。如果需要使用缺省实现,也必须显式地调用基类的相关函数。
class B
{
public:
virtual void f()
};
void B::f()
{
// 缺省实现..
}
// D2类似
class D1: public B
{
public:
virtual void f()
};
void D1::f()
{
// 显式调用基类的缺省实现
B::f();
}
这种方法的好处是少了一个需要维护的函数,缺点是客户代码可以直接调用基类的缺省实现。
B* b = new D1;
b->B::f();
小结:
普通的虚函数提供了接口与缺省实现。(公有继承的)派生类继承接口的同时可以自动继承接口的缺省实现(免费的午餐),或者选择重新定义该函数来特化自己的行为。风险是以后需要定制此行为的派生类可能因为忘记重定义而错误地使用缺省实现。纯虚函数只提供了接口。但是具有函数体的纯虚函数同时还提供了缺省实现。派生类必须显式定义接口。可以通过显式调用基类函数来完成缺省行为。
为了完整,顺便补充一下,公有非虚函数提供了接口与强制实现。也就是说所有的派生类必须继承这个接口及其实现。
相关文章推荐
- [c++]为什么可以在基类中实现纯虚函数
- why pure virtual function has definition 为什么可以在基类中实现纯虚函数
- C++中纯虚函数的实现原理是什么,为什么该纯虚函数不能实例化?
- 使用Bufferd和缓冲数组,实现最快写入写出
- 写出一个你自己的MVC框架-基于对springMVC源码实现和理解(1):入口所在
- 为什么需要htons(), ntohl(), ntohs(),htons() 函数 .模拟htonl、ntohl、htons、ntohs函数实现
- 第13周 阅读程序,写出运行结果。 2.(3)交通工具类之纯虚函数
- 虚拟现实为什么这么难实现?
- 为什么objc_msgSend必须用汇编实现
- 实现一个简单的银行储蓄系统,承担活期用户的存款和取款业务 (只是初步的写出)
- nginx为什么自己要实现ngx_atoi函数等
- java为什么不直接实现Iterator接口,而是实现Iterable
- javaBean为什么要实现Serializable接口?
- 为什么我希望用C而不是C++来实现ZeroMQ
- 1、使用javascript代码写出一个函数:实现传入两个整数后弹出较大的整数
- 为什么需要htons(), ntohl(), ntohs(),htons() 函数 .模拟htonl、ntohl、htons、ntohs函数实现
- 关于Session(javax.servlet.HttpSession)持久化----为什么实体类需要实现序列化接口
- C++中为什么要用虚函数、指针或引用才能实现多态?
- WebSocket 是什么原理?为什么可以实现持久连接
- 为什么在集合框架中已经继承了抽象类还要实现接口