Effective c++ 读书笔记六——条款32-37 重载,重写, 重定义

学习完本章,相信会对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{
        publicvirtual 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函数——你唯一应该覆写的东西——却是动态绑定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值