《Effective C++:改善程序与设计的55个具体做法》阅读笔记 6——继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系

参考:链接

使用public继承时,要确保子类和父类是is-a关系。is-a关系:子类是一种父类。
函数形参类型为父类指针或引用,那么可以给函数传入子类指针或引用。(此结论只对public继承有效,private继承需要看条款39,作者没有说protected继承的时是否有效)

is-a关系被破坏的情况——父类中有子类不应该有的属性或方法。如果Penguin(企鹅)继承自Bird(鸟),但是Bird中有fly方法。解决方法:

  • (1)方法一:在Penguin的fly()方法里面抛出异常,一旦调用了p.fly(),那么就会在运行时捕捉到这个异常。这个方法不怎么好,因为它要在运行时才发现问题。
  • (2)方法二:去掉Bird的fly()方法,在中间加上一层FlyingBird类(有fly()方法)与NotFlyingBird类(没有fly()方法),然后让企鹅继承与NotFlyingBird类。这个方法也不好,因为会使注意力分散,继承的层次加深也会使代码难懂和难以维护。
  • (3)方法三:保留所有Bird一定会有的共性(比如生蛋和孵化),去掉Bird的fly()方法,只在其他可以飞的鸟的子类里面单独写这个方法。这是一种比较好的选择,因为根本没有定义fly()方法,所以Penguin对象调用fly()会在编译期报错。

条款33:避免遮掩继承而来的名称(避免在子类中重载函数,或定义和父类相同的成员变量)

参考:链接

子类中避免出现和父类同名的成员变量;子类中避免重载父类的成员函数,重写倒是没有问题。因为会出现下面那些麻烦的事情。

成员变量被遮掩:

#include<iostream>

//成员变量被遮掩
class Base
{
public:
    int x;
    Base(int _x):x(_x){}

    void show(){
        std::cout<< x << std::endl;
    }
};

class Derived: public Base
{
public:
    int x;
    Derived(int _x):Base(_x),x(_x + 1){}
};

int main()
{
    Derived d(3);
    std::cout<< d.x << std::endl; //输出4
    d.show(); //输出3
}
  • 对于std::cout<< d.x << std::endl;,因为定义的是子类的对象,所以会优先查找子类独有的作用域,这里已经找到了x,所以不会再查找父类的作用域,因此输出的是4,如果子类里没有另行声明x成员变量,那么才会去查找父类的作用域。
  • 对于d.show();,由于show的存在与父类的作用域里,所以使用的是父类作用域中的x。

子类中可以通过int GetBaseX() {return Base::x;}访问父类的x。

成员函数被遮掩

class Base
{
public:
    void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
    void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
    void virtual PureVirtualFunction() = 0;
};

class Derived: public Base
{
public:
    void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
    void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};

int main()
{
    Derived d;
    d.VirtualFunction(3); // ?
    return 0;
}

上述程序会编译出错,理由如下:编译器先查找子类独有的域,一旦发现了完全相同的函数名(VirtualFunction),它就已经不再往父类中找了!在核查函数参数时,发现了子类的VirtualFunction没有带整型形参,所以直接报编译错了。

结论:子类重载(函数名相同但形参不一样)父类的成员函数,会将父类所有同名的函数都隐藏掉。(不管有没有virtual关键字,结论都成立)

访问父类里面的方法:

  • 子类中使用using Base::VirtualFunction;,告诉编译器,把父类Base的VirtualFunction函数也纳入第一批查找范围里面。
  • 子类中重写父类函数:void virtual VirtualFunction(int x){Base::VirtualFunction(x)}; // 隐含inline

条款34:区分接口继承和实现继承(假定讨论的成员函数都是public的)

参考:链接

接口、实现和声明、定义是相关的,总结:

  • 纯虚函数只继承接口;
  • 虚函数既继承接口,也提供了一份默认实现;
  • 普通函数既继承接口,也强制继承实现。

纯虚函数、虚函数和普通函数:

  • 纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现)。所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。
    “基类中有纯虚函数的具体实现”的目的:告诉子类此函数必须实现(与虚函数不同),并且提供了此函数的默认实现(子类中可以通过类似BaseClass::PureVirtualFunction();的方式调用此函数的默认实现)。
  • 虚函数则必须有实现,否则会报链接错误。虚函数可以在基类和多个派生类中提供不同的版本,利用多态性质,在程序运行时动态决定执行哪一个版本的虚函数(机制是编译器生成的虚表)。virtual关键字在基类中必须显式指明,在派生类中不必指明,即使不写,也会被编译器认可为virtual函数,virtual函数存在的类可以定义实例对象。
  • 普通函数所代表的意义是不变性凌驾与特异性,所以它绝不该在派生类中被重新定义。当需要重写时,使用虚函数或者纯虚函数。当需要重载时,条款33告诉我们——子类中避免重载父类的成员函数。

书上提倡尽量用纯虚函数去替代虚函数(避免子类忘记实现自己的成员函数),因为虚函数提供了一个默认的实现,如果派生类的想要的行为与这个虚函数不一致,而又恰好忘记去覆盖虚函数,就会出现问题。但纯虚函数不会,因为它从语法上限定派生类必须要去实现它,否则将无法定义派生类的对象。

下面是三类成员函数的应用:

class BaseClass
{
public:
    void virtual PureVirtualFunction() = 0; // 纯虚函数
    void virtual ImpureVirtualFunction(); // 虚函数
    void CommonFunciton(); // 普通函数
};
void BaseClass::PureVirtualFunction()
{
    cout << "Base PureVirtualFunction" << endl;
}
void BaseClass::ImpureVirtualFunction()
{
    cout << "Base ImpureVirtualFunciton" << endl;
}

class DerivedClass1: public BaseClass
{
    void PureVirtualFunction()
    {
        cout << "DerivedClass1 PureVirturalFunction Called" << endl;
    }
};

class DerivedClass2: public BaseClass
{
    void PureVirtualFunction()
    {
        cout << "DerivedClass2 PureVirturalFunction Called" << endl;
    }
};

int main()
{
    BaseClass *b1 = new DerivedClass1();
    BaseClass *b2 = new DerivedClass2();
    b1->PureVirtualFunction(); // 调用的是DerivedClass1版本的PureVirtualFunction
    b2->PureVirtualFunction(); // 调用的是DerivedClass2版本析PureVirtualFunction
    b1->BaseClass::PureVirtualFunction(); // 当然也可以调用BaseClass版本的PureVirtualFucntion
    return 0;
}

条款35:考虑virtual函数以外的其他选择

参考:链接

virtual函数以外的其他选择:

  • 将虚函数DoA放入private,然后在public中的函数A调用DoA。这样向外提供的接口就不是一个虚函数。NVI(Non-Virutal Interface)的一个流派主张所有的虚函数都是private的,将父类与子类都会使用的前置方法与后置方法单独作一个non-virtual的函数
    【注】这里是将虚函数都置成了private的,但编译器生成的虚表指针则不是private的,否则会因private成员变量根本不被继承而无法实现多态。NVI的方式也不是绝对的,比如虚析构函数,它必须是public的,才能确保它的子类,以及子类的子类们能够顺利释放资源。
  • 定义一个函数指针作为成员变量,这样就可以给父类和派生类的对象指定不同的函数。可以使用tr1 : : function来构建函数指针,tr1 : : function构建的函数指针可以指向函数、函数对象、std::tr1::bind构造的函数。
  • 定义另一个类A的指针作为成员变量,然后将类A的对象传入父类B,将类A的派生类对象传入给B的子类。
posted @ 2022-11-11 20:00  好人~  阅读(23)  评论(0编辑  收藏  举报