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

面向对象编程

--概述、定义基类和派生类

引言:

面向对象编程基于的三个基本概念:数据抽象、继承和动态绑定。

在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数

继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序

面向对象编程:概述

面向对象编程的关键思想是多态性(polymorphism)。之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。正如我们将看到的,在C++中,多态性仅用于通过继承而相关联的类型的引用或指针

1、继承

派生类(derivedclass)能够继承基类(baseclass)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。最后,除了从基类继承的成员之外,派生类还可以定义更多的成员。

我们经常称因继承而相关联的类为构成了一个继承层次。其中有一个类称为根,所以其他类直接或间接继承根类。如:

class Item_base
{
public:
    Item_base(const std::string &book = "",
              double sales_price = 0.0):isbn(book),price(sales_price) {}

    std::string book() const
    {
        return isbn;
    }

    virtual double net_price(std::size_t n) const
    {
        return n * price;
    }
    virtual ~Item_base() {}

private:
    std::string isbn;

protected:
    double price;
};

Item_base的派生类将无须改变地继承book函数:派生类不需要重新定义获取ISBN的含义。另一方面,每个派生类需要定义自己的net_price函数版本,以实现适当的折扣价格策略。

在C++中,基类必须指出希望派生类重写哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。

讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:

1)非虚函数std::stringbook(),返回ISBN。由Item_base定义,Bulk_item 继承。

2)虚函数doublenet_price(size_t) 的两个版本(其中一个已经定义出),返回给定数目的某书的总价。Item_base类和Bulk_item类将定义该函数自己的版本。

2、动态绑定

通过动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无需关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。例如可以编写print_total函数:

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

【注意:】

第一,虽然这个函数的第二形参是Item_base的引用但可以将Item_base对象或Bulk_item对象传给它。

第二,因为形参是引用且net_price是虚函数,所以对net_price的调用将在运行时确定。调用哪个版本的net_price将依赖于传给print_total的实参。

【小结】

在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。

定义基类和派生类

一、定义基类

像其他类一样,基类也有其接口和实现的数据和函数成员:

class Item_base
{
public:
    Item_base(const std::string &book = "",
              double sales_price = 0.0):isbn(book),price(sales_price) {}

    std::string book() const
    {
        return isbn;
    }

    virtual double net_price(std::size_t n) const
    {
        return n * price;
    }

    //继承层次的根类一般都要定义虚析构函数
    virtual ~Item_base() {}

private:
    std::string isbn;

protected:
    double price;
};

1、基类成员函数

虚函数:保留字virtual的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定,为了指明函数为虚函数,必须加上virtual关键字:

    virtual double net_price(std::size_t n) const;

除了构造函数之外,任意非static成员都可以使虚函数。保留字virtual只在类内部的成员函数声明中出现,不能在类定义体外部出现在类定义体上。

【最佳实践】

基类通常应将派生类需要重定义的任意函数定义为虚函数

2、访问控制和继承

在基类中,public和 private标号具有普通含义:用户代码可以访问类的public成员而不能访问private成员,private成员只能由基类的成员和友元访问。派生类对基类的public和 private成员的访问权限与程序中任意其他部分一样:它可以访问public成员而不能访问private成员。

protected成员可以被派生类对象访问不能被该类型的普通用户访问

二、protected成员

可以认为protected访问标号是private和public的混合:

1)像private成员一样,protected成员不能被类的用户访问

2)像public成员一样,protected成员可被该类的派生类访问

此外,protected还有另一重要性质:

派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象protected成员没有特殊访问权限。

void Bulk_item::memfcn(const Bulk_item &d,const Item_base &b)
{
    double ret = price;
    ret = d.price;
    ret = b.price;  //Error
}

【关键概念:类设计与受保护成员】

派生类的提供者通常(但并不总是)需要访问(一般为private的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的protected访问标号。

定义类充当基类时,将成员设计为public的标准并没有改变:仍然是接口函数应该为 public而数据一般应为private。被继承的类必须决定实现的哪些部分声明为protected而哪些部分声明为private希望禁止派生类访问的成员应该设为private,提供派生类实现所需操作或数据的成员应设为 protected。换句话说,提供给派生类的接口是protected成员和public成员的组合。

三、派生类

为了定义派生类,使用派生类列表指定基类。派生类列表指定了一个或多个基类以及访问权限:

class ClassName: access-label base-class

其中,以继承单个基类最为常见。然后访问标号[public,private,protected]决定了对继承成员的访问权限。如果想要继承基类的接口,则应该进行public派生。

派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。一般而言,派生类只()定义那些与基类不同或扩展基类行为的方面

1、定义派生类

从Item_base类派生Bulk_item类,Bulk_item类将继承book、isbn和price成员。Bulk_item类必须重定义net_price函数并定义该操作所需要的数据成员:

class Bulk_item:public Item_base
{
public:
    double net_price(std::size_t ) const;

private:
    std::size_t min_qty;
    double discount;
};

每个Bulk_item对象包含四个数据成员Item_base继承的isbnprice,自己定义的min_qtydiscount

2、派生类和虚函数

派生类一般会重定义所继承的虚函数,如果派生类没有重定义某个虚函数,则使用基类中定义的版本。

派生类型必须对想要重定义的每个继承成员进行声明。虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。

【注释】

一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做[但是建议这么做,可以提醒类的使用者该函数为virtual函数]。

3、派生类对象包含基类对象作为子对象

派生类对象由多个部分组成:派生类本身定义的(非static)成员+由基类(非static)成员组成的子对象。

【注解:C++语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列,因此,图中是关于类如何工作的概念表示而不是物理表示。】

4、派生类中的函数可以使用基类的成员

double Bulk_item::net_price(std::size_t cnt) const
{
    if (cnt >= min_qty)
    {
        return cnt * (1 - discount) * price;
    }

    return cnt * price;
}

因为每个派生类对象都有基类部分,类可以访问其基类的public和protected成员,就好像那些成员是自己的一样[不拿自己当外人O(∩_∩)O哈!]!

5、用作基类的类必须是已经定义的

class Item_base;	//仅仅声明了
//Error:Item_base没定义
class Bulk_item : public Item_base {};

每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类。

6、用派生类做基类

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

每个类继承其基类所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。最底层的派生类对象包含其每个直接基类间接基类的子对象。

7、派生类的声明

派生类的声明包含类名,而不包含派生类列表。

class Bulk_item : public Item_base; //Error
class Bulk_item;	//OK

//P479 习题15.6
class Bulk_item:public Item_base
{
public:
    double net_price(std::size_t ) const;

private:
    std::size_t min_qty;
    double discount;
};

double Bulk_item::net_price(std::size_t cnt) const
{
    if (cnt >= min_qty)
    {
        return cnt * (1 - discount) * price;
    }

    return cnt * price;
}

//习题15.7
class Item_derivd : public Item_base
{
public:
    double net_price(std::size_t ) const;

private:
    std::size_t count;
    double discount;
};

double Item_derivd::net_price(std::size_t cnt) const
{
    if (cnt <= count)
    {
        return cnt * (1 - discount) * price;
    }

    return cnt * price;
}

未完待续...

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

时间: 2024-08-02 02:45:34

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

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

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

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 学习笔记_74_面向对象编程 --再谈文本查询示例[续/习题]

面向对象编程 --再谈文本查询示例[续/习题] //P522 习题15.41 //1 in TextQuery.h #ifndef TEXTQUERY_H_INCLUDED #define TEXTQUERY_H_INCLUDED #include <iostream> #include <fstream> #include <sstream> #include <vector> #include <set> #include <map&g

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

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

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

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

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

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

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

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