• C++Primer笔记-----继承


    ==========================================================================
    day11 面向对象程序设计
    ==========================================================================

    1.面向对象程序设计的核心思想是数据抽象、继承、动态绑定(也叫封装、继承、多态)

    数据抽象:将类的接口和实现分离。
    继 承:可以定义相似的类型并对其相似关系建模。
    动态绑定:可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

    2.基类将它的成员函数分为两类,一种是希望派生类直接继承而不需要改变的函数;另一种是希望派生类进行覆盖的函数。
    对于后者,基类通常将其定义为虚函数(virtual)。任何构造函数之外的非静态函数都可以是虚函数。
    当我们使用指针或引用调用虚函数时,该调用将被动态绑定,根据指针或引用所绑定的对象的不同,该调用可能执行基类版本,也可能执行某个派生类版本。

    3.如果基类把一个函数声明为virtual,则该函数在派生类中隐式地也是虚函数。

    4.派生类能访问基类的共有成员,但不能访问私有成员。但是某些时候,基类中还有这样一些成员,基类希望它的派生类可以访问,同时禁止其他用户访问。于是产生了protected成员。

    5.【派生类继承基类所有成员,除了构造函数和拷贝控制成员。】

    6.派生列表中用到的访问说明符的作用是,控制派生类从基类继承来的成员是否对派生类的用户可见。
    class Father{
    ...
    };

    class Son : private Father{ // 私有继承,继承来的基类成员对派生类的用户不可见
    ...
    };

    7.我们可以将基类的指针或引用绑定到派生类对象上。 但如果基类的对象既不是指针,又不是引用就不可以。
    编译器也会隐式地执行派生类到基类的转换。这种隐式特性意味着我们可以在需要基类指针或引用的地方使用派生类来代替。

    【需要记住的一点是,即使基类的引用或指针知道自己实际上是一个子类,也不能调用基类中未定义的子类方法或者成员。】
    【这是因为,当调用非虚函数时,不会发生动态绑定,实际调用的函数版本由指针或引用的静态类型决定!】
    eg:
    class Super{
    public:
    Super();
    virtual void someMethod();
    ...
    };

    class Sub : public Super{
    public:
    Sub();

    virtual void someMethod(); // 覆盖了基类的该方法
    virtual void someOtherMethod(); // 子类新增的方法
    };
    考虑如下:

    Sub mySub;
    Super &ref = mySub;

    mySub.someOtherMethod(); // 正确
    ref.someOtherMethod(); // 错误 当调用非虚函数时,不会发生动态绑定,实际调用的函数版本由指针或引用的静态类型决定
    // 引用的静态类型是基类类型,但基类中没有此方法,所以错误!

    如果非引用、非指针的对象,则不具备这个特性。
    eg: Sub mySub;
    Super obj = mySub; // 将派生类对象直接赋值或强制转换给基类,然而这样一来,对象就丢失了子类的一些知识。也就是导致了切割(slicing)
    // 切割也就是覆盖的方法和子类数据的丢失

    obj.someMethod(); // 调用的是基类版本的此方法,而非派生类版本


    不存在基类向派生类的隐式转换。(但可以强制转换,使用dynamic_cast可以将基类指针/引用安全地转换为派生类的指针/引用)
    更特殊的是:
    Bulk_quote bulk;
    Quote *itemp = &bulk; // 正确,基类对象指针指向子类对象
    Bulk_quote *bulkp = itemp; // 错误,不能将基类转换为派生类,即使基类实际上指向的是派生类也不行

    8.派生类继承了基类后,它必须用基类的构造函数来初始化它的基类部分。
    【这是C++中的一个关键概念:每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分。】

    class Quote{
    public:
    ...
    Quote(const string &book,double sales_price):bookNo(book),price(sales_price){}
    ..
    private:
    string bookNo;
    protected:
    double price = 0.0;
    };

    class Bulk_quote : public Quote{ //Bulk_quote继承了Quote
    public:
    ...
    Bulk_quote(const string &,double,size_t,double);
    ...
    private:
    size_t min_qty = 0;
    double discount = 0.0;
    };

    eg: Bulk_quote(const string &book,double p,size_t qty,d disc):
    Quote(book,p),min_qty(qty),discount(disc){} // 使用基类构造函数初始化它的基类部分

    9.防止继承 (C++新标准)
    有时候我们不希望一个类被其他类继承。为了实现这个目的,在类名后面加关键字 final
    eg: class NoDerived final {...}

    ==================================================================================

    10.dynamic_cast运算符。用于将基类的指针或引用安全地转换为派生类的指针或引用
    特别适用于:我们想使用基类的指针或引用执行某个派生类的操作并且该操作并不是虚函数。
    dynamic_cast<type*>(e);
    dynamic_cast<type&>(e);
    e必须满足:e必须是目标type的公有派生类或公有基类或本身就是type类型。
    如果符合条件,则转换成功,否则转换失败,要转换的类型为指针则返回0,为引用则抛出bad_cast异常
    用法:
    对于指针:
    if(Derived *d = dynamic_cast<Derived*>(b)){
    // 使用d指向的Derived对象
    }else{ // 转换失败
    // 使用b指向的Base对象
    }
    对于引用:
    void f(const Base &b){
    try{
    const Derived &d = dynamic_cast<Derived&>(b);
    // 使用b引用的Derived对象
    }catch(bad_cast){
    //处理类型转换失败的情况
    }
    }


    typeid运算符。typeid(e),e可以是任何表达式或类型的名字,返回的是一个常量对象的引用。(type_info类型的)
    注意点:typeid忽略顶层const,typeid一个数组,返回数组类型,而不是指针类型。

    typeid一般用来比较两个表达式的类型是否一样:if(typeid(*b) == typeid(*d)) ... // typeid应该作用于对象,因此我们使用*b而不是b

    【使用RTTI】 p734
    某些情况下,RTTI非常有用,比如当我们想为具有继承关系的类实现相等运算符时。


    11. 通常情况下,如果我们不使用某个函数,就无须为该函数提供定义。
    但我们必须为虚函数提供定义,不管它有没有被使用到。

    12.一个派生类的函数如果覆盖了某个继承来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
    返回类型也必须一致。但有一个例外:当类的虚函数返回类型是类本身的指针或引用时。
    也就是说如果D由B派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型【要求从D到B的类型转换是可访问的】。

    13.override说明符的使用。
    实际上,派生类如果定义了一个函数与基类中虚函数名字相同但形参列表不同,这仍然是合法的行为。编译器会认为这两个函数是独立的,也就是派生类的
    函数并没有覆盖掉基类中的版本。 这种声明一般意味着我们原本想覆盖基类中的虚函数,但一不小心形参列表弄错了。而编译器并不会发现这样的错误,
    于是override产生了,它被用来防止这种错误。如果我们使用override标记了某个函数,但该函数并没有覆盖虚函数,此时编译器就会报错。

    14.final的使用
    除了阻止类被继承之外,我们还可以通过在形参列表后面加final来阻止函数被覆盖。

    15.如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
    这是因为:如果虚函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
    也就是说,如果我们通过基类的指针或引用调用虚函数,即使实际运行的是派生类中的函数版本,也依旧使用的是基类中定义的默认实参。

    16.派生类可以访问基类中的protected成员,但有一点必须明确:派生类的成员或友元只能通过派生类对象(而不能通过基类对象)来访问基类的受保护成员。 p543

    17.类的对象不能访问类的私有成员,只能通过类成员和友元函数访问。
    类的对象也不能访问类的保护成员,只能通过类的成员函数或派生类的成员函数访问。以前理解错了。。。

    18.派生类向基类转换的可访问性:
    1)如果D公有继承B,【用户代码】才能使用派生类向基类的转换。如果是protected或private则不行。 用户代码我理解为函数体外的地方
    2)不论D以什么方式继承B,【D的成员函数和友元】都可以使用派生类向基类的转换。
    3)如果D继承B的方式是公有的或保护的,则【D的派生类的成员和友元】可以使用派生类向基类的转换。如果是私有的则不行。

    19.默认继承
    和struct成员默认为公有,class成员默认为私有一样,默认继承也是如此:
    struct D1 : Base{...} // 默认为公有继承
    class D2 : Base{...} // 默认为私有继承

    20.改变派生类继承的某个名字的访问级别
    class Base{
    public:
    size_t size() const {return n;}
    protected:
    size_t n;
    };

    class Derived : private Base{
    public:
    using Base::size();
    protected:
    using Base::n;
    };

    考虑以上代码,Derived是私有继承,它继承来的成员对它的用户来说是私有。但我们通过using声明改变了这些成员的访问级别,它们不再是私有的了。
    改变之后,Derived的用户可以使用size()成员,而Derived的派生类可以使用n

    以上需要注意的一点是:派生类只能为那些它可以访问的基类成员提供using声明。 也就是基类中的private成员不可以被改变访问级别

    21.派生类的成员将隐藏同名的基类成员,可以使用作用域运算符来使用被隐藏的基类成员。
    struct Base{
    Base():mem(0){}
    protected:
    int mem;
    };

    struct Derived:Base{
    Derived(int i):mem(i){}
    int get_Dmem(){return mem;}
    int get_Bmem(){return Base::mem;} // 0
    protected:
    int mem; // 隐藏了基类中的同名成员
    };

    Derived d(42);
    cout<<d.get_Dmem()<<endl; // 42

    22.通过函数调用的解析过程来理解C++继承:
    假定我们调用p->mem()
    1)首先确定p的静态类型,由于我们调用的是一个成员,p必须是类类型。
    2)在p的静态类型对应的类中查找mem。如果找不到,依次在直接基类中不断查找直到继承链的顶部。还找不到,则报错。
    3)一旦找到mem,进行常规类型检查,看调用是否合法。
    4)如果合法,则编译器根据调用的是否是虚函数产生不同的代码。即动态调用还是普通调用

    23.派生类的作用域是嵌套在基类的作用域的,也可以说基类作用域是外层作用域,派生类是内层作用域。
    【在C++中,名字查找发生在类型检查之前。】 重载是根据函数名、参数的个数、参数的类型
    在不同的作用域,我们无法重载函数名。如果我们在内层作用域声明名字,它将隐藏外层作用域中声明的同名实体。

    24.虚析构函数
    当我们delete一个动态分配的对象的指针时,将执行析构函数。
    如果我们delelte一个基类的指针,而该指针实际上指向的是一个派生类对象,就会产生未定义的行为。
    这时应该将基类的析构函数声明为虚函数,动态地决定调用基类版本的析构函数还是派生类版本的。

    25.如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。 p556

    26.多重继承是指从多个直接基类中产生派生类。多重继承的派生类继承了所有父类的属性。

    多重继承的二义性问题

    当一个派生类同时继承了两个基类,而两个基类中又包含了同名的成员,则派生类调用该成员时会出现二义性问题。
    我们知道,在C++中名字查找先于类型检查。所以即使派生类从两个基类继承的两个同名函数的形参列表不同,也会导致二义性错误。此外,
    即使这个同名函数在一个基类中是私有的,在另一个基类中是公有或保护的,同样会产生错误。


    “倒三角” 和 “恐怖菱形”
    倒三角:当一个派生类同时继承了两个基类,而两个基类中又包含了同名的成员。
    可以通过作用域运算符::来规避这种二义性错误。
    要避免二义性错误,最好的办法是:在派生类中为该函数定义一个新版本。

    FA FB
    /
    C

    恐怖菱形:如下图所示,iostream类(间接)继承了base_io两次。

    base_io
    /
    istream ostream
    /
    iostream

    C++中我们通过虚继承的机制来解决这个问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。
    在这种机制下,不管虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

    假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,而D又继承了D1和D2. 如果我们通过D对象来使用x,有三种可能:
    1)如果x在D1和D2中都没有定义,因为虚继承的缘故,不存在二义性问题。
    2)如果x是B的成员,同时是D1和D2中某一个的成员,则同样不存在二义性。派生类的x比虚基类B的x优先级更高。
    3)如果D1和D2中都有x的定义,则直接访问x将产生二义性问题。 最好的解决办法是:在派生类中为成员定义新的实例。

    27.调用虚函数的指针也可以是this指针,当通过子类对象调用基类中的成员函数时,该函数里面的this指针将是一个指向子类对象的基类指针,这时再通过this去调用虚函数也可以表现多态的语法特性.

    eg:QT多线程实现
    class QThread{//QT官方写好的类
    public:
    void start(void){ this->run(); }
    protected:
    virtual void run(void){线程入口函数}
    };
    class myThread:public QThread{//自己写的类
    protected:
    //重写线程入口函数
    void run(void){需要放在线程中执行的代码..}
    };
    myThread thread;
    //子类重写的线程入口函数将被执行
    thread.start();

  • 相关阅读:
    浅谈MapReduce
    Redis源码分析(三十五)--- redis.c服务端的实现分析(2)
    Redis源码分析(三十五)--- redis.c服务端的实现分析(2)
    Redis源码分析(三十五)--- redis.c服务端的实现分析(2)
    Confluence 6 手动安装语言包和找到更多语言包
    Confluence 6 安装一个语言组件
    Confluence 6 启用主题评论
    Confluence 6 启用远程 API
    Confluence 6 配置时间和日期格式
    Confluence 6 创建-使用-删除快捷链接
  • 原文地址:https://www.cnblogs.com/ll-10/p/9935165.html
Copyright © 2020-2023  润新知