学习完本章,相信会对OOP有更深的理解!
首先区别下重载(overload),重写(override,也称覆盖), 重定义(redefine)
一、重载(overload)
指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。
(1)相同的范围(在同一个作用域中) ;
(2)函数名字相同;
(3)参数必须不同;
(4)virtual 关键字可有可无。
(5)返回值可以不同;
二、重写(也称为覆盖 override,继承关系中存在)
是指派生类重新定义基类的函数,特征是:
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
(5)返回值相同(或是协变),否则报错;<—-协变这个概念我也是第一次才知道…
(6)重写函数的访问修饰符可以不同。尽管 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的
三、重定义(也称隐藏,子类重新定义父类中的函数,屏蔽了父类的同名函数)见条款33,只适用于子类对象
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)返回值可以不同;
(4)参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆) 。
(5)子类和父类的函数名称相同,参数也相同,但是父类函数不是virtual函数,父类的函数将被隐藏。(这里会有特殊情况,若父类指针指向子类对象,非虚成员函数不会被隐藏,而是调用父类成员函数。原因:普通函数是静态绑定的)
#include <iostream>
#include <complex>
using namespace std;
class Base
{
public:
virtual void fun1(){cout<<"base fun1"<<endl;}
virtual void fun2(){cout<<"base fun2"<<endl;}
void fun3(){cout<<"base fun3"<<endl;}
void fun4(){cout<<"base fun4"<<endl;}
virtual void a(int x) { cout << "Base::a(int)" << endl; }
// overload the Base::a(int) function
virtual void a(double x) { cout << "Base::a(double)" << endl; }
virtual void b(int x) { cout << "Base::b(int)" << endl; }
void c() { cout << "Base::c(int)" << endl; }
void d() { cout << "Base::d" << endl;}
};
class Derived : public Base
{
public:
virtual void fun1(){cout<<"derived fun1"<<endl;}
void fun2(int x){cout<<"derived fun2"<<endl;}
virtual void fun3(){cout<<"derived fun3"<<endl;}
void fun4(){cout<<"derived fun4"<<endl;}
// redefine the Base::a() function
void a(double x) { cout << "Derived::a(complex)" << endl; }
// override the Base::b(int) function
void b(int x) { cout << "Derived::b(int)" << endl; }
// redefine the Base::c() function
void c(int x) { cout << "Derived::c(int)" << endl; }
void d() { cout << "Derived::d" << endl;}
};
int main()
{
Base ba;
Base *pb1;
Derived de;
pb1 = &de;
pb1->fun1(); //Derived fun1
pb1->fun2(); //base fun2 此处不会隐藏基类函数,不太明白,与基类指针有关、
pb1->fun3(); //base fun3
pb1->fun4(); //base fun4
de.fun2(10); //derived fun2
//de.a(10);
pb1->a(10); //"Base::a(int)"
//de.c(); //掩盖,出错
//pb1->c(10);//出错
//de.c(); //基类函数被隐藏,没有不带参数的c函数,会编译出错
de.c(10); //Derived::c(int)
pb1->c(); //Base::c(int)
//子类和父类的函数名称相同,参数也相同,但是父类函数不是virtual函数,父类的函数将被隐藏。
de.d(); //"Derived::d"
pb1->d(); //"Base::d"
//de.fun2(); //隐藏,出错
de.fun2(10); //"derived fun2"
//pb1->fun2(10);//出错
////////////////////////
Base base;
Derived der;
Base* pb = new Derived;
// ----------------------------------- //
base.a(1.0); // Base::a(double)
der.a(1.0); // Derived::a(complex) //只有子类对象调用重定义函数,基类函数才会被隐藏。
pb->a(1.0); //"Base::a(double)" This is redefine the Base::a() function
// pb->a(complex<double>(1.0, 2.0)); // clear the annotation and have a try
// ----------------------------------- //
base.b(10); // Base::b(int) 只有基类对象调用重写函数,基类函数才不会被覆盖。
der.b(10); // Derived::b(int)
pb->b(10); // Derived::b(int), This is the virtual function
// ----------------------------------- //
delete pb;
pb->c(10); //Base::c(int) 重定义。调用基类的函数
return 0;
}
1.Base类中的第二个函数a是对第一个的重载
2.Derived类中的函数b是对Base类中函数b的重写,即使用了虚函数特性。
3.Derived类中的函数a是对Base类中函数a的隐藏,即重定义了。
4.pb指针是一个指向Base类型的指针,但是它实际指向了一个Derived的空间,这里对pd调用函数的处理(多态性)取决于是否重写(虚函数特性)了函数,若没有,则依然调用基类。
5.只有在通过基类指针或基类引用 间接指向派生类类型时多态性才会起作用。
6.因为Base类的函数c没有定义为virtual虚函数,所以Derived类的函数c是对Base::c()的重定义。
条款32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。
如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。这个论点只对public 继承才成立。private继承意义见条款39.
class Person {……};
class Student: public Person {……};
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);//正确
eat(s);//正确
study(s);//正确
study(p);//错误 p不是学生,学习行为是不对的。
有时public和is-a之间的关系会误导我们。例如,企鹅(penguin)是一种鸟,这是事实;鸟可以飞,这也是事实。如果以C++描述这层关系:
class Bird{
public:
virtual void fly();
……
};
class Penguin: public Bird{
……
};
企鹅不能飞是事实。所以此程序不是我们想要的。为了表现“企鹅不会飞”的限制,我们可以不定义fly函数,
class Bird{
public:
……
};
class Penguin: public Bird{
……
};
这样,如果 Penguin p; p.fly();编译就会出错。而不是让它在运行期间才发现错误。
好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
is-a只是存在class继承关系中的一种,还有两个继承关系式has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。这些关系将在条款**38和条款**39讨论。在设计类时,应该了解这些classes之间的相互关系和相互差异,在去塑模类之间的关系。
请记住:
♦“public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。
条款33:避免遮掩继承而来的名称
C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否是相同或不同的类型,并不重要。即,只要名称相同就覆盖基类相应的成员,不管是类型,参数个数,都无关紧要。派生类的作用域嵌套在基类的作用域内。
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。
C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
……
};
class Derived: public Base{
public:
virtual void mf1();
void mf3();
void mf4();
……
};
因为以作用域为基础的“名称遮掩规则”,base class内所有名称为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3都不再被Derived继承。
Derived d;
int x;
d.mf1();//正确,调用Derived::mf1
d.mf1(x);//错误,此处含参,因为Derived::mf1遮掩了Base::mf1
d.mf2();//正确,调用Base::mf2
d.mf3();//正确,调用Derived::mf3
d.mf3(x);//错误,因为Derived::mf3遮掩了Base::mf3
如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式,引入基类的成员函数。如下:在子类中
//让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见,且为public
using Base::mf1;
using Base::mf3;
如果你继承base class,且加上重载函数(子类函数名和基类相同);你又希望重新定义或覆写其中一部分,那么要把被遮掩的每个名称引入一个using声明。
请记住:
♦ derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
♦ 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。
条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。
所谓接口继承,就是派生类只继承函数的接口,也就是声明;而实现继承,就是派生类同时继承函数的接口和实现。
我们都很清楚C++中有几个基本的概念,虚函数、纯虚函数、非虚函数。
虚函数:
虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。
虚函数用来表现基类和派生类的成员函数之间的一种关系. 虚函数的定义在基类中进行,在需要定义为虚函数的成员函数的声明前冠以关键字 virtual. 基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义.
在派生类中重新定义时,其函数原型,包括返回类型,函数名,参数个数,参数类型及参数的先后顺序,都必须与基类中的原型完全相同. 虚函数是重载的一种表现形式,是一种动态的重载方式.
纯虚函数:
纯虚函数在基类中没有定义,它们被初始化为0。 任何用纯虚函数派生的类,都要自己提供该函数的具体实现。 定义纯虚函数 virtual void fun(void) = 0;
非虚函数:
一般成员函数,无virtual关键字修饰。 至于为什么要定义这些函数,我们可以将虚函数、纯虚函数和非虚函数的功能与接口继承与实现继承联系起来:
声明一个纯虚函数(pure virtual)的目的是为了让派生类只继承函数接口,也就是上面说的接口继承。
纯虚函数一般是在不方便具体实现此函数的情况下使用。也就是说基类无法为继承类规定一个统一的缺省操作,但继承类又必须含有这个函数接口,并对其分别实现。但是,在C++中,我们是可以为纯虚函数提供定义的,只不过这种定义对继承类来说没有特定的意义。因为继承类仍然要根据各自需要实现函数。
通俗说,纯虚函数就是要求其继承类必须含有该函数接口,并对其进行实现。是对继承类的一种接口实现要求,但并不提供缺省操作,各个继承类必须分别实现自己的操作。
声明非纯虚函数(impure virtual)的目的是让继承类继承该函数的接口和缺省实现。 与纯虚函数唯一的不同就是其为继承类提供了缺省操作,继承类可以不实现自己的操作而采用基类提供的默认操作。
声明非虚函数(non-virtual)的目的是为了令继承类继承函数接口及一份强制性实现。 相对于虚函数来说,非虚函数对继承类要求的更为严格,继承类不仅要继承函数接口,而且也要继承函数实现。也就是为继承类定义了一种行为。
总结: 纯虚函数:要求继承类必须含有某个接口,并对接口函数实现。 虚函数:继承类必须含有某个接口,可以自己实现,也可以不实现,而采用基类定义的缺省实现。 非虚函数:继承类必须含有某个接口,必须使用基类的实现。
请记住:
◆接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
◆pure virtual函数只具体制定接口继承。
◆简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
◆non-virtual函数具体制定接口继承以及强制性实现继承。
条款35:考虑virtual函数以外的其它选择*
当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。
请记住:
◆使用non-virtual interfance (NVI)手法,NVI手法自身是一个特殊形式的Template Method设计模式。
◆将virtual函数替换为“函数指针成员变量”,这是strategy设计模式的一种分解表现方式。
◆将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
◆tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
条款36:绝不重新定义继承而来的non-virtual函数
以一个例子来展开本条款阐述内容。假设class D是class B的派生类,class B中有一个public成员函数mf:
class B{
public:
void mf();
……
};
class D: public B {……};
由一下方式调用
D x;
B* pB=&x;
pB->mf();
D* pD=&x;
pD->mf();
上面的两次调用函数mf得到的行为相同吗?虽然mf是个non-virtual函数,但是如果class D中有自己定义的mf版本,那就行为真的不同。
class D: public B {
public:
void mf();//遮掩了B::mf。见条款33
……};
pB->mf();//调用B::mf
pD->mf();//调用D::mf。
之所以行为不一致,是因为non-virtual函数是静态绑定的(statically bound,条款 37)。pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本。
但是virtual函数是动态绑定(dynamically bound,条款 37),所以virtual函数不受这个约束,即通过指针调用,实际调用的函数是指针真正指向对象的那个函数。这里就是真正指向的是类型为D的对象。
如果你打算在class D中重新定义继承自class B的non-virtual函数,D对象很可能会出现行为不一致行径。更明确一点,即任何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初声明类型。References也会展现出和指针一样难以理解的行径。
前面已经说过,public继承是is-a 关系(条款 32)。**条款**34说过,class内声明一个non-virtual函数会为该class建立一个不变性(invariant),它凌驾其特异性(specialization)。将这两个观点施行到class B和class D上以及non-virtual函数B::mf上,那么:
■ 适用于B对象的每一件事,也适用于D对象。(is-a 关系)
■ B的derived classes一定会继承mf的接口和实现,因为mf是一个non-virtual函数。
现在在D中重新定义mf,就会有矛盾。1、如果D真的有必要重新实现mf(不同于B的),那么is-a 关系就不成立,因为每个D都是B不再为真;既然这样,就不应该以public形式继承。2、如果D必须以public方式继承B,且D有需求实现不同的mf,那么久不能反映出不变性凌驾特异性;既然这样就应该声明为virtual函数。3、如果每个D是一个B为真,且mf真的可以反映出不变性凌驾特异性的性质,那么D久不需要重新定义mf了。
不论上面那个观点,结论都相同:任何情况下都不应该重新定义一个基础而来的non-virtual函数。
在条款 7已经知道,base class内的析构函数应该是virtual;如果你违反了条款 7,你也就违反了本条款,因为析构函数每个class都有,即使你没有自己编写。
请记住:
◆绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
关键词:静态类型 动态类型
在继承中,只能继承两种函数:virtual和non-virtual。在条款 36中我们学到,不能重新定义一个继承而来的non-virtual函数。本条款讨论的是继承virtual函数问题,再具体一点:继承一个带有缺省参数值的virtual函数。
我们应该知道,virtual函数是动态绑定(dynamically bound),缺省参数值却是静态绑定(statically bound)。
对象的静态类型(static type)是它在程序中被声明时采用的类型,例如
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
virtual void draw(ShapeColor color=Red) const=0;
……
};
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color=Green) const;//不同缺省参数值,很糟糕
……
};
class Circle: public Shape{
public:
virtual void draw(ShapeColor color) const;
/*客户调用上面函数时,如果使用对象调用,必须指定参数值,因为静态绑定下这个函数不从base继承缺省值。*/
/*如果使用指针或引用调用,可以不指定缺省参数值,动态绑定会从base继承缺省参数值*/
……
};
这个继承很简单。现在这样使用
Shape* ps;
Shape* pc=new Circle;
Shape* pr=new Rectangle;
这些指针类型都是pointer-to-Shape类型,都是静态类型Shape*。
对象的动态类型是指“目前所指对象类型”。动态类型可以表现出一个对象将会有什么行为。pc动态类型是Circle*,pr动态类型是Rectangle*,ps没有动态类型(它没有指向任何对象)。动态类型可以在执行过程中改变,重新赋值可以改变动态类型。
virtual函数是动态绑定的,调用哪一份函数实现的代码,取决于调用的那个对象的动态类型。
pc->draw(Shape::Red);//调用circle::draw(shape::red)
pr->draw(Shape::Red);//调用rectangle::draw(shape::red)
这样调用无可非议,都带有参数值。但是如果不带参数值呢
pr->draw();//调用Rectangle::draw(Shape::Red)
上面调用中,pr动态类型是Rectangle*,所以调用Rectangle的virtual函数。Rectangle::draw函数缺省值是GREEN,但是pr是静态类型Shape*,所以这个调用的缺省参数值来自Shape class,不是Rectangle class。这次调用两个函数各出了一半的力。
C++之所以使用这么怪异的运作方式,是因为效率问题。如果缺省参数值动态绑定,编译器必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢且更复杂。为了执行速度和编译器实现上的简易度,C++做了这样的取舍。
我们尝试遵守这个规则,给base class和derived class提供相同参数值
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
virtual void draw(ShapeColor color=Red) const=0;
……
};
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color=Red) const;
……
};
这样问题又来了,代码重复且带着相依性(with dependencies):如果Shape内缺省参数值改变了,那么derived classes的缺省参数值也要改变,否则就会导致重复定义一个继承而来的缺省参数值。
当时如果的确需要derived classes的缺省参数值,那么就需要替代方法。条款 35列出了一些virtual函数的替代方法,例如NVI手法:
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
void draw(ShapeColor=Red) const
{
doDraw(color);
}
……
private:
virtual void doDraw(ShapeColor color) const=0;//真正在这里完成工作
};
class Rectangle: public Shape{
public:
……
private:
virtual void draw(ShapeColor color) const;//注意不需指定缺省参数值
……
};
因为non-virtual函数不会被derived覆写(条款 36),这个设计很清楚的使得draw函数的color缺省参数值总是Red。
请记住:
◆ 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。