C++ Primer 学习笔记_66_面向对象编程 -定义基类跟派生类[续]

面向对象编程

--定义基类和派生类[续]

四、virtual与其他成员函数

C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:

1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。

2)必须通过基类类型的引用或指针进行函数调用。

1、从派生类到基类的转换

因为每个派生类对象都包含基类部分,所以可以将基类类型的引用绑定到派生类对象的基类部分可以用指向基类的指针指向派生类对象:

    void print_total(const Item_base &item,size_t n);
    Item_base item;
    print_total(item,10);

    Item_base *p = &item;
    Bulk_item bulk;
    print_total(bulk,10);

    p = &bulk;

无论实际对象具有哪种类型,编译器都将它当做基类类型对象。将派生类对象当做基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即:任何在基类对象上执行的操作也可以通过派生类对象使用。

【释疑】

基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。

2、可以在运行时确定virtual函数的调用

当通过指针或引用调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型向对应的函数:

void print_total(const Item_base &item,size_t n)
{
    cout << "ISBN: " << item.book()
         << "\t number sold: " << n << "\ttotal price: "
         << item.net_price(n) << endl;
}

因为 item形参是一个引用且net_price是虚函数,item.net_price(n)所调用的net_price版本取决于在运行时绑定到item形参的实参类型:

    Item_base base;
    Bulk_item derived;
    print_total(base,10);       //Item_base::net_price
    print_total(derived,10);    //Bulk_item::net_price

【关键概念:C++中的多态性】

引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态的基石!

通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。

如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。

【理解:】

只有通过引用或指针调用,虚函数才在运行时确定。

3、在编译时确定非virtual调用

非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。尽管item的类型是constItem_base 的引用,但是,无论在运行时item引用的实际对象是什么类型,调用该对象的非虚函数都将会调用Item_base中定义的版
本。

4、覆盖虚函数机制

如果希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,可以使用作用域操作符:

    Item_base *baseP = &derived;
    //显式调用Item_base中的版本,重载时确定
    double d = baseP -> Item_base::net_price(42);

【最佳实践】

只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。

覆盖虚函数机制常用在:派生类虚函数调用基类中的版本,在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作:

【小心地雷】

派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了作用域操作符,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归!

5、虚函数与默认实参

像其他任何函数一样,虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。

在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。

//P482 习题15.8
struct base
{
    base(string name = ""):baseName(name) {}

    string name()
    {
        return baseName;
    }

    virtual void print(ostream &os)
    {
        os << baseName;
    }

private:
    string baseName;
};

struct derived : public base
{
    derived(string name = "",int intMem = 0):base(name),mem(intMem) {}

    void print(ostream &os)
    {
        base::print(os);    //原来此处形成了无穷递归!
        os << "" << mem;
    }

private:
    int mem;
};
//习题15.9 理解下面这段程序
int main()
{
    base ba("xiaofang");

    base *p = &ba;
    p -> print(cout);
    cout << endl;

    derived de("xiaofang");
    p = &de;
    p -> print(cout);	//调用派生类print函数
}

五、公用、私有和受保护的继承

对类所继承的成员的访问由基类中的成员访问级别和派生类列表中使用的访问标号共同控制。每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对继承的成员的访问!

基类本身指定对自身成员的最小访问控制。基类中的private,只有基类和基类的友元可以访问该成员。派生类也不能访问其基类的private成员,当然也不能使自己的用户访问!

如果基类成员为public或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:

1)如果是公用继承public:基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。

2)如果是受保护继承protected:基类的public和protected成员在派生类中为protected成员。

3)如果是私有继承private:基类的所有成员在派生类中为private成员。

class Base
{
public:
    void baseMem();

protected:
    int i;
};

class Public_derived : public Base
{
    int use_base()
    {
        return i;   //OK
    }
};
class Private_derived : private Base
{
    int use_base()
    {
        return i;   //OK
    }
};

上例说明:无论派生列表中是什么访问标号,所有继承Base的类对Base的成员具有相同的访问权限;派生类访问标号将控制派生类的用户对从Base继承而来的成员的访问:

    Base b;
    Public_derived d1;
    Private_derived d2;

    b.baseMem();
    d1.baseMem();   //OK
    d2.baseMem();   //Error

派生类访问标号还控制来自非直接派生类的访问:

class Derived_from_Private : public Private_derived
{
    int use_base()
    {
        return i;   //Error
    }
};

class Derived_from_Public : public Public_derived
{
    int use_base()
    {
        return i;   //OK
    }
};

其实这也可以理解:因为在类Private_derived中它所继承来的所有东西都变成private的了,这就相当于派生类不能访问基类private成员一样了!而从Public_derived派生的类可以访问来自Base类的i,是因为该成员在Public_derived中仍为protected成员。

1、接口继承与实现继承

public派生类继承基类的接口:它具有与基类相同的接口。设计良好的类层次中,public派生类的对象可以用在任何需要基类对象的地方。【接口继承】

private和protected派生类继承基类的实现:它们“不继承”基类的接口[因为继承过来就相当于成为了派生了的内置实用函数了...],派生类在实现中被继承类但继承基类的部分并未成为其接口的一部分!【实现继承】

[迄今为止:最常见的继承形式是public!]

【关键概念:继承与组合】

定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(IsA)”关系。在书店例子中,基类表示按规定价格销售的书的概念,Bulk_item是一种书,但具有不同的定价策略。

类型之间另一种常见的关系是称为“有一个(HasA)”的关系。书店例子中的类具有价格和ISBN。通过“有一个”关系而相关的类型暗含有成员关系,因此,书店例子中的类由表示价格和ISBN的成员组成。

2、去除个别成员

如果进行private/protected继承,则基类成员的访问级别在派生类中比在基类中更受限:

class Base
{
public:
    std::size_t size() const
    {
        return n;
    }

protected:
    std::size_t n;
};

class Derived : private Base
{
    //...
};

//测试
int main()
{
    Derived de;
    std::size_t n = de.size();  //Error
}

【注解】

派生类可以恢复继承成员的访问级别,但不能使得访问级别比基类中原来指定的更严格(?)或更宽松!

在上例中,size在 Derived中为private。为了使size在 Derived中恢复往日的地位[public],可以在Derived的public部分增加一个using声明。如下这样改变Derived的定义,可以使size成员能够被用户访问,并使n能够被从Derived派生的类访问:

class Derived : private Base
{
public:
    using Base::size;

protected:
    using Base::n;
};

正如可以使用using声明从命名空间使用名字,也可以使用using声明访问基类中的名字。

此时:

    Derived de;
    std::size_t n = de.size();  //OK

3、默认继承保护级别

默认继承访问级别根据使用哪个保留关键字定义派生类也不相同:使用class定义的派生类默认具有private继承,而struct定义的类默认具有public继承:

class Base
{
    /* ... */
};
struct D1 : Base    //struct D1 : public Base
{
    /* ... */
};
class D2 : Base     //class D2 : private Base
{
    /* ... */
};

注意:class与struct默认继承的唯一区别只是默认的成员保护级别和默认的派生保护级别!

class D3 : public Base
{
public:
    /* ... */
};
// equivalent definition of D3
struct D3 : Base
{
    /* ... */
};

struct D4 : private Base
{
private:
    /* ... */
};
// equivalent definition of D4
class D4 : Base
{
    /* ... */
};

【最佳实践】

尽管私有继承在使用class保留字时是默认情况,但这在实践中相对罕见[所以建议最好不要使用,因为最终阅读你的源码的不只是计算机,还有程序员!]。因为私有继承是如此罕见,通常显式指定 private是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。

六、友元关系与继承

友元关系不能继承。基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

每个类控制对自己的成员的友元关系:

class Base
{
    friend class Frnd;

protected:
    int i;
};

//Frnd对D1没有特殊的访问权限
class D1 : public Base
{
protected:
    int j;
};

class Frnd
{
public:
    int mem(const Base &obj)
    {
        return obj.i;   //OK
    }
    int mem(const D1 &obj)
    {
        return obj.j;   //Error
    }
};
//D2对Base没有特殊的访问权限
class D2 : public Frnd
{
public:
    int mem(const Base &obj)
    {
        return obj.i;   //Error
    }
};

基类的友元对从该基类派生的类型没有特殊访问权限,同样,如果基类和派生类都需要访问一个类,那个类必须特定的将访问权限授予基类和每一个派生类。

七、继承与静态成员

如果基类定义了static成员,则整个继承层次只有一个这样的成员:无论从基类派生出多少个派生类,每个static成员只有一个实例。

static成员遵循常规访问控制:如果成员在基类中为private,则派生类不能访问它。假定可以访问该static成员[如public],则既可以通过基类访问static成员,也可以通过派生类访问static成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。

struct Base
{
    static void statMem();
};

struct Drived : public Base
{
    void f (const Drived &);
};

void Drived::f(const Drived &derived_obj)
{
    Base::statMem();
    Drived::statMem();
    derived_obj.statMem();
    statMem();
}
//P487 习题15.13
struct ConcreteBase
{
    static std::size_t object_count();

protected:
    static std::size_t obj_count;
};

struct C1 : public ConcreteBase
{
    void f(const ConcreteBase &obj)
    {
        ConcreteBase::object_count();
        ConcreteBase::obj_count;

        C1::object_count();
        C1::obj_count;

        obj.object_count();
        obj.obj_count;

        object_count();
        obj_count;
    }

};
struct C2 : public ConcreteBase
{

};

int main()
{
    C2 obj;
    obj.object_count();

    //obj不能直接访问obj_count成员,因为该成员是受保护成员,不能通过对象访问
    obj.obj_count;    //Error
    obj.ConcreteBase::object_count();
    obj.C2::object_count();
}
时间: 2024-10-11 18:25:33

C++ Primer 学习笔记_66_面向对象编程 -定义基类跟派生类[续]的相关文章

C++ Primer 学习笔记_66_面向对象编程 --定义基类和派生类[续]

算法旨在用尽可能简单的思路解决问题,理解算法也应该是一个越看越简单的过程,当你看到算法里的一串概念,或者一大坨代码,第一感觉是复杂,此时不妨从例子入手,通过一个简单的例子,并编程实现,这个过程其实就可以理解清楚算法里的最重要的思想,之后扩展,对算法的引理或者更复杂的情况,对算法进行改进.最后,再考虑时间和空间复杂度的问题. 了解这个算法是源于在Network Alignment问题中,图论算法用得比较多,而对于alignment,特别是pairwise alignment, 又经常遇到maxim

C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类

面向对象编程 --概述.定义基类和派生类 引言: 面向对象编程基于的三个基本概念:数据抽象.继承和动态绑定. 在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员.动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数. 继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序. 面向对象编程:概述 面向对象编程的关键思想是多态性(polymorphism)

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

C++ Primer 学习笔记_73_面向对象编程 --再谈文本查询示例

面向对象编程 --再谈文本查询示例 引言: 扩展第10.6节的文本查询应用程序,使我们的系统可以支持更复杂的查询. 为了说明问题,将用下面的简单小说来运行查询: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对象进行的调用的虚行为. 1.比較两个Sales_item对象 在编写函数计算销售总数之前,须要定义比較Sales_item对象的方法.要用Sales_item作为关联容器的keyword,必须能够比較它们.关联容器默认使用keyword类型的小于操作符,可是假设给Sales_item定义小于操作符,

C++ Primer 学习笔记_67_面向对象编程 --转换与继承、复制控制与继承

面向对象编程 --转换与继承.复制控制与继承 I.转换与继承 引言: 由于每一个派生类对象都包括一个基类部分,因此能够像使用基类对象一样在派生类对象上执行操作. 对于指针/引用,能够将派生类对象的指针/引用转换为基类子对象的指针/引用. 基类类型对象既能够作为独立对象存在,也能够作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,因此,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自己主动)转换. 关于对象类型,尽管一般能够使用派生类型的对象对基类

C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名

C++ Primer 学习笔记_70_面向对象编程 --纯虚函数、容器与继承

面向对象编程 --纯虚函数.容器与继承 I.纯虚函数 在函数形参后面写上 =0 以指定纯虚函数: class Disc_item : public Item_base { public: double net_price(size_t) const = 0; //指定纯虚函数 }; 将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类的版本绝不会调用.重要的是,用户将不能创建Disc_item类型的对象. Disc_item discount; //Error Bulk