C++ Primer阅读心得(第十五章)

1.继承:C++中的类通过继承形成一种层次关系,处在上层的类被称为基类(base class),处在下层的类被称为派生类(derived class)。基类中定义在派生类中共用的代码,提高代码的复用度;派生类在继承了基类代码的基础之上,定制自身特定的代码,增强代码的灵活性。

2.类派生列表:派生类通过使用类派生列表指明它是与哪些基类存在继承关系。

class Base {
};
class Derived : public Base { //Derived继承自Base
};

其中每个基类之前都有一个访问说明符:public、protected或者private,它们对于继承而来的基类内容影响如下表所示:

继承\访问publicprotectedprivate
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateprivateprivateprivate

在日常的编程过程中,主要用到的就是public继承,基本上用不到protected继承和private继承,那么它们有什么作用?它们俩主要的用途在于表达“基于基类来实现功能”的语义(表示软件内实现细节,例如:基于list实现一个set等)。Protected继承和private继承使得基类所有接口变得外界不可访问,它们不再是is-a关系。因此,编译器不再支持将派生类指针转换为基类指针。所以,protected继承和private继承就是“把基类的内容引入,但是不给外面看到,只能用来实现自己的逻辑”这样一个行为。但是实际上,“基于基类来实现功能”这样的行为同样可以通过给派生类内部添加一个基类的对象/指针的“组合”来做到,因此protected继承和private继承非常鸡肋,除非基类被设计为只有继承了它才能使用它实现功能(某种编程框架)这一种情况,几乎没人用。

3.继承中的构造与析构:基类的成员会被派生类全部继承(包括private部分,只不过派生类对象无法访问)。派生类的对象中包含了基类对象,而基类的对象中又包含了它的基类的对象,一直包含到继承层次的最上层的基类为止。所以,基类指针或引用指向一个派生类的对象是默认类型转换(注意必须是public继承),不需要额外的声明;而反过来则不行,基类指针或引用转化为派生类指针或引用必须显示转换(static_cast或dynamic_cast,推荐后者因为系统会帮助你做检查)。注意不要将派生类对象转化为基类对象,这会产生截断(slice down),丢弃派生类中的数据成员。

class Base {
};
class Derived : public Base {
};
Base *bp = new Derived(); //ok
Derived d;
Base &br = d;  //ok
Derived *dp = bp; //错误,必须显式转换
Derived *dp = dynamic_cast<Derived *>bp; //ok
Base b(d); //错误,d中的数据将丢失

在构造(拷贝、移动)派生类的对象时,会从上至下依次构造各个层次的基类对象,最后构造派生类对象;而在析构派生类对象时正好相反,从下到上依次析构各个层次的对象。注意:如果基类没有默认构造函数,派生类必须在构造函数的初始化列表(只有这个位置可以,其他地方都晚了)中显式调用基类的构造函数。同理,在派生类的拷贝构造函数(操作符等)复制控制成员中也不要忘记调用基类的复制控制成员。

class Base2 {
public:
    Base (int i) {...}
    Base2 (const Base2& b) {} //拷贝构造
};
class Derived2 : public Base2 {
public:
    Derived2 (int i, int j) : Base2(i) {...} //必须在初始化列表中
    Derived2 (const Derived2& d) : Base2(d) {} // 用派生类参数调用基类拷贝构造
}; 

注意不要在基类的构造函数中调用virtual函数,因为这么做不会产生你想要的效果(子类构造的时候也自动调用自己的版本)。因为此时子类对象尚未构造完成,所以最终在基类中被调用到的只会是基类的版本(virtual完全没有生效)。

4.虚函数与动态绑定:基类将希望派生类覆盖(override)的函数使用virtual关键字标记为虚函数,派生类则根据自身的需要覆盖继承而来的虚函数中的一部分或者全部。当使用基类指针或引用调用虚函数的时候,程序可以根据基类指针或引用指向的实际类型而调用对应版本的函数,这种行为被称为动态绑定(dynamic bind)。

class Base {
public:
    virtual void myfunc(int i) {...}
};
class Derived : public Base {
public:
    virtual void myfunc(int i) {...}
};

Base *pb = new Base();
pb->myfunc1(1); //调用Base版本
delete pb;
pb = new Derived();
pd->myfunc1(1); //调用Derived版本

动态绑定的虚函数覆盖要求必须返回类型和参数列表都一致,否则只是单纯的函数覆盖,而不能够提供动态绑定的特性。为了帮助发现这一问题,c++11新增了一个关键字override来让编译器帮助检查这种错误。

class Derived2 : public Base {
public:
    virtual void myfunc1(double d) override {...} //错误
};

对虚函数调用的查找从指针或引用指向的实际对象开始,沿着继承链依次向上,在第一个函数名对应上的层次上查找匹配函数(注意只根据名称)。所以:

  • 如果派生类没有覆盖每个虚函数,将会调用基类版本或者基类的基类版本
  • 如果基类中有一系列重载(overload)的虚函数,而派生类只覆盖了其中一部分,那么未覆盖部分将不会出现在派生类中(因为不会查找到那个层次)。这个问题的解决办法是使用using base_class::function_name来把基类的重载的系列函数全部引入派生类当中。
class Base2 {
public:
    virtual void not_override() {...}
    virtual void overload1(int i) {...}
    virtual void overload1(double d) {...}
    virtual void overload2(int i) {...}
    virtual void overload2(double d) {...}
};
class Derived2 : public Base2 {
public:
    virtual void overload1(int i) {...} //覆盖了一个,但是屏蔽了另外一个
    using Base2::overload2;
    virtual void overload2(int i) {...} 
};
Base2 bp2 = new Derived();
bp2->not_override(); //ok,调用基类版本
bp2->overload1(0.01); //错误,Derived2中没有这个函数
bp2->overload2(0.01); //ok,调用基类版本

基类中的析构函数推荐为virtual的。因为如果析构函数不是虚函数,那么就无法利用动态绑定来释放基类指针指向的派生类对象了。

class Base3 {
public:
    ~Base3() {...}
};
class Derived3 : public Base3 {
public:
    ~Derived3() {...}
};
Base3 *bp = new Derived3();
delete bp; //错误,内存没有完全释放

protected、private中的函数也可以是virtual的,它们为友元提供动态绑定。

class Base4;
void myfriend(Base4 *);
class Base4 {
friend void myfriend(Base4 *);
private:
    virtual do_sth() {...}
};
class Derived4 : public Base4 {
friend void myfriend(Base4 *);
private:
    virtual do_sth() {...}
};
void myfriend(Base4 *p) {
    p->do_sth();
}
Base4 b;
Derived4 d;
myfriend(&b); //调用基类版本
myfriend(&d); //调用派生类版本

还有一点值得注意的是,如果虚函数的参数带有默认值,那么当进行动态绑定时,虽然行为是派生类的行为,但是默认值用的是基类的默认值。这是因为虚函数的调用采用的是动态绑定的机制,而函数的默认值采用的是静态绑定的机制,所以基类指针/引用在编译时就将基类默认参数值绑定好了,共同作用的结果是你用基类的默认值调用了派生类的函数。所以基类和派生类中虚函数的实参最好保持一致,否则结果往往出人意料。

5.静态绑定:在类中的非虚函数,对它们的调用在编译期确定,这被称为静态绑定(static bind)。

6.静态类型与动态类型:变量声明时的类型被称为它的静态类型,在程序运行期间它所的类型被称为动态类型。因为有从派生类指针或引用向基类指针或引用的默认转换,所以基类指针或引用的静态类型与动态类型可以是不一致的。根据动态类型来决定调用哪个版本的函数,这就是动态绑定。

7.回避动态绑定:有时候我们只想调用虚函数的指定版本,而不用动态绑定,这时就可以使用域操作符制定函数调用的版本。这一点在派生类函数调用基类版本时特别有用。

class Base {
public:
    virtual void init() {...}
};
class Derived : public Base {
public:
    virtual void init() override {
        Base::init(); //调用基类版本
        ...
    }
};

8.final关键字:在C++11中,新增了final关键字以明确阻止基类被继承或基类函数被派生类覆盖(Java赢了!)。

class Base1 final {
};
class Base2 {
public:
    void init() final {...}
};
class Derived1 : public Base1 { //错误,Base1不可被继承
};
class Derived2 : public Base2 {
public:
    void init() {...} //错误,init不可被覆盖
}

9.static与继承:static成员属于类,所以它们不仅在基类对象中是唯一的,在派生类对象中也是唯一的。

10.友元与继承:友元(friend)关系不能被继承,基类的友元不能直接访问基类的私有成员。

11.智能指针与动态绑定:C++11中新增的智能指针也支持从派生类指针到基类指针的默认转换,使用它们调用虚函数时也可以获得动态绑定的特性。

12.神奇的using:上面说过,在覆盖一组重载的虚函数中的一部分时,可以使用using Base::func来避免未被覆盖的虚函数被屏蔽的情况。除此之外,使用using还可以改变基类中单个成员的访问权限。注意:using语句只能修改派生类可见的基类函数的访问权限。

class Base {
public:
    void do_sth() {...}
private:
    void do_sth2() {....}
};
class Derived : private Base {
public:
    using Base::do_sth(); //ok,虽然是private继承,但是do_sth被using修改为了public权限,可以被外部访问
    using Base::do_sth2(); //错误,private成员对派生类不可见
};

在C++11中,可以使用using继承基类的构造函数。注意:1.继承来的构造函数无论放在哪里都是它在基类中的访问权限;2.因为using语句中无法添加explicit和constexpr,继承来的构造函数在这方面只能与原来一样;3.基类中的默认参数无法被继承;4.如果using和相同参数的构造函数同时存在,则后者覆盖前者,using语句等于没起作用;5.默认构造函数(移动、拷贝)无法被继承,它们只能被合成。

class Base2 {
public:
    explicit Base2(int i) {...}
    Base2(double d, bool b=false) {...}
};
class Derived2 : public Base2 {
private:
    using Base2::Base2; //ok,继承构造函数,它们都是public的,b=false这个默认参数被丢掉了
public:
    Derived2(double d, bool b) {...} //继承来的构造函数被覆盖
};

13.纯虚函数与抽象类:在继承层次中,有一些类我们不希望它们被实例化,这时可以将这种类中的虚函数定义为纯虚函数(pure virtual function),让它成为抽象类(abstract class),无法被实例化。注意,如果一个派生类没有覆盖所有的纯虚函数,那么它也是抽象类,无法被实例化。

class AbstractBase { //抽象类
public:
    virtual void func1() = 0; //纯虚函数
    virtual void func2() = 0; //纯虚函数
};
void AbstractBase::func1() {...} //为纯虚函数提供一个备选实现

class Derived1 : public AbstractBase {
public:
    virtual void func1() {
        AbstractBase::func1(); //选用备选实现
    }
};

class Derived2 : public AbstractBase {
public:
    virtual void func1() {...} //提供自己的实现
    virtual void func2() {...}
};

AbstractBase b; //错误,抽象类无法被实例化
Derived1 d1; //错误,Derived1中含有纯虚函数,无法被实例化
Derive2 d2; //ok

让人惊讶的是,C++居然允许你为纯虚函数提供一个实现,派生类可以通过“基类::纯虚函数名”的方式调用这个版本的实现。为纯虚函数提供实现的目的在于实现基类接口与实现的分离,该方法刚好与虚函数提供了相反的语义:要求其派生类继承一个接口和一个备选的实现,派生类通常应该提供一个自己的实现,但是也可以选择偷懒调用这个备选的实现。

14.struct与class:之前说过,struct与class的区别在于内部成员的默认访问权限,一个是public另外一个是private。在继承这一点上也是这样,struct的默认继承是public的,而class的默认继承是private的。

15.继承与包含:类之间的继承关系是一种IS-A关系,而类之间的包含是一种HAS-A关系。这两种关系在OOP中最为常用,在程序设计的时候需要仔细考虑。

16.基类中纯虚函数(pure virtual)、虚函数(impure virtual)和一般成员函数(non-virtual)在继承中的含义:

  1. 纯虚函数意味着基类设计者要求其派生类继承这个接口,并且必须提供自己版本的实现
  2. 虚函数意味着基类设计者要求其派生类继承这个接口和一个默认实现,派生类可以选择提供自定义实现或者复用默认实现
  3. 一般成员函数意味着基类设计者要求其派生类继承这个接口和一个强制的实现(不希望修改),派生类不该覆盖这个函数
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值