【读书笔记:C++ primer plus 第六版 中文版】第13章 类继承

转载请注明出处:http://blog.csdn.net/enyusmile/article/details/46779399

20150627,以此纪念一个特别的日子

本章内容包括:

  • is-a关系的继承
  • 如何以公有方式从一个类派生出另一个类
  • 保护访问
  • 构造函数成员初始化列表
  • 向上和向下强制转换
  • 虚成员函数
  • 早期(静态)联编与晚期(动态)联编
  • 抽象基类
  • 纯虚函数
  • 何时及如何使用公有继承

面向对象编程的主要目的之一是提供可重用的代码.目前,很多厂商提供了类库,类库由类声明和实现构成.因为类组合了数据表示和类方法,因此提供ing了比函数库更加完整的程序包.

13.1 一个简单的基类

  • 程序清单13.1 tabtenn0.h
  • 程序清单13.2 tabtenn0.cpp
  • 程序清单13.3 usett0.cpp

13.1.1 派生一个类

  • 使用公有派生,基类的公有成员将称为派生类的公有成员.基类的私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问.

13.1.2 构造函数:访问权限的考虑

  • 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问.具体地说,派生类构造函数必须使用基类构造函数.
  • 创建派生类对象时,程序首先创建基类对象.从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建.C++使用成员初始化列表语法类完成这种工作.
  • 派生类构造函数的要点如下:
    • 首先创建基类对象;
    • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数.
    • 派生类构造函数应初始化派生类新增的数据成员
  • 释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数.
  • 注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数.基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员.派生类的构造函数总是调用一个基类构造函数.可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数.派生类对象过期时,程序将首先调用派生类析构函数,然后在调用基类析构函数.
  • 成员初始化列表:派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数.除虚基类外(参见第14章),类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,一次类推.如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数.成员初始化列表只能用于构造函数.

13.1.3 使用派生类

  • 程序清单13.4 tabtenn1.h
  • 程序清单13.5 tabtenn1.cpp
  • 程序清单13.6 usett1.cpp

13.1.4 派生类和基类之间的特殊关系

  • 派生类对象可以使用基类的方法,条件是方法不是私有的.
  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象;
  • 基类引用可以在不进行显式类型转换的情况下引用派生类对象.
  • 然而,基类指针或引用只能用于调用基类方法.
  • 通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外.然而,这种例外知识单向的,不可以将基类对象和地址赋给派生类引用和指针.

13.2 继承:is-a关系

  • C++有3种继承方式:公有继承,保护继承和私有继承.
  • 因为派生类可以添加特性,所以,将这种关系称为is-a-kind-of(是一种)关系可能更准确,但是通常使用术语is-a
  • 公有继承部监理has-a关系.
  • 公有继承不能建立is-like-a关系,也就是说,它不采用明喻.
  • 公有继承不建立is-implemented-as-a(作为…来实现)关系.
  • 公有继承不建立uses-a关系.
  • 在C++中,完全可以使用公有继承来建立has-a,is-implemented-as-a或uses-a关系,然而,这样做通常会导致编程方面的问题.因此,还是坚持使用is-a关系吧.

13.3 多态公有继承

  • 方法的行为应取决于调用该方法的对象.这种较复杂的行为称为多态—具有多种形态,即同一个方法的行为随上下文而异.
  • 有两种重要的机制可用于实现多态公有继承:
    • 在派生类中重新定义基类的方法
    • 使用虚方法

13.3.1 开发Brass类和BrassPlus类

  • 程序清单13.7 brass.h
    • 关键字virtual.虚方法
      • 如果方法是通过应用或指针而不是对象调用的,它将确定使用哪一种方法.如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据应用或指针指向的对象的类型来选择方法.
      • 注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的.这样,程序将根据对象类型而不是应用或指针的类型来选择方法版本.为基类声明一个虚析构函数也是一种惯例.
  • 1.类实现
    • 关键字virtual志勇于类声明的方法原型中,而没有用于类实现的方法定义中.
    • 程序清单13.8 brass.cpp
      • 派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法.非构造函数不能使用成员初始化列表语法.
      • 在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法.
  • 2.使用Brass和BrassPlus类
    • 程序清单13.9 usebrass1.cpp
      • 请注意为何Hogg受透支限制,而Pigg没有
  • 3.演示虚方法的行为
    • 程序清单13.10 usebrass2.cpp
  • 4.为何需要虚析构函数
    • 使用虚析构函数可以确保正确的析构函数序列被调用.

13.4 静态联编和动态联编

  • 将源代码中的函数调用解释为执行耳钉的函数代码块被称为函数名联编binding.C/C++比啊你起可以在编译过程完成这种联编.
  • 在编译过程中进行联编被称为静态联编static binding,又称为早期联编early binding.然而,虚函数使这项工作变得更困难.使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象.所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编dynamic binding,又称为晚期联编late binding.

13.4.1 指针和引用类型的兼容性

  • 在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的.
  • 将派生类应用或指针转换为基类引用或指针被称为向上强制转换upcasting,这使公有继承不需要进行显式类型转换.该规则是is-a关系的一部分.
  • 相反的过程—将基类指针或引用转换为派生类指针或引用—称为向下强制转换downcasting.如果不使用显式类型转换,则向下强制转换是不允许的.
  • 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编.C++使用虚成员函数来满足这种需求.

13.4.2 虚成员函数和动态联编

  • 总之,编译器对非虚方法使用静态联编.
  • 总之,编译器对虚方法使用动态联编.
  • 1.为什么有两种类型的联编以及为什么默认为静态联编
    • 如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个—效率和概念模型
      • 效率:Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间).仅当程序设计确实需要虚函数时,才使用它们.
      • 概念模型:仅将那些预期将被重新定义的方法声明为虚的.
    • 提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法.
  • 2.虚函数的工作原理
    • C++规定了虚函数的行为,但将实现方法留给了编译器作者.
    • 通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针.这种数组称为虚函数表virtual function table ,vtbl.虚函数表中存储了为类对象进行声明的虚函数的地址.例如:基类对象包含一个指针,该指针指向基类中所有虚函数的地址表.派生类对象将包含一个指向独立低指标的指针.如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址.如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中.注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已.调用虚函数时,程序将查看存储在对象中的vtbl地址,然后专项相应的函数地址表.如果使用类声明中定义的第一个虚函数,则程序将使用数组汇总的第一个函数地址,并执行具有该地址的函数.如果使用类声明中第三个虚函数,程序将使用地址为数组中第三个元素的函数.
    • 总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
      • 每个对象都将增大,增大量为存储地址的空间.
      • 对于每个类,编译器都创建一个虚函数地址表(数组);
      • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址.

13.4.3 有关虚函数注意事项

  1. 构造函数
    • 构造函数不能是虚函数.将类构造函数声明为虚的没什么意义.
  2. 析构函数
    • 析构函数应当是虚函数,除非类不用做基类.
    • 提示:通常应给基类提供一个虚析构函数,即使它并不需要析构函数.
  3. 友元
    • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数.
    • 如果由于这个原因引起了设计问题,可以通过让友元函数使用虚函数成员函数来解决.
  4. 没有重新定义
    • 如果派生类没有重新定义函数,将使用该函数的基类版本.如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的(稍后将介绍).
  5. 重新定义将隐藏方法.
    • 总之,重新定义继承的方法并不是重载.如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何.
    • 这引出两条经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的).这种特性被称为返回类型协变covariance of return type),因为允许返回类型随类类型的变化而变化.注意这种例外只适用于返回值,而不适用于参数.第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本.

13.5 访问控制:protected

  • private和protected之间的区别只有在基类派生的类中才会表现出来.
  • 警告:最好对数据成员采用私有访问控制,不要适用保护访问控制;同时通过基类方法使派生类能够访问基类数据.

13.6 抽象基类

  • 抽象基类abstract base class,ABC
  • C++通过使用纯虚函数pure virtual function提供未实现的函数.纯虚函数声明的结尾处为=0.当类声明中包含纯虚函数时,则不能创建该类的对象.
  • 这里的理念是,包含纯虚函数的类只用作基类.要称为真正的ABC,必须至少包含一个纯虚函数.原型中的=0使虚函数称为纯虚函数.

13.6.1 应用ABC概念

  • 程序清单13.11 acctabc.h
  • 程序清单13.12 acctabc.cpp
  • 程序清单13.13 usebrass3.cpp

13.6.2 ABC理念

  • 设计ABC之前,首先应开发一个模型—指出变成问题所需的类以及它们之间相互关系.一种学院派思想认为,如果要设计类继承层次,则只能将哪些不会被用作基类的类设计为具体的类.这种方法的设计更清晰,复杂程度更低.

13.7 继承和动态内存分配
13.7.1 第一种情况:派生类不使用new

  • 如果派生类未包含其他一些不常用的,需要特殊处理的设计特性,是否需要为派生类定义显式析构函数,复制构造函数和赋值运算符呢?不需要.(此内容是择选拼凑的)

13.7.2 第二种情况:派生类使用new

  • 在这种情况下,必须为派生类定义显式析构函数,复制构造函数和赋值运算符.
  • 总之,当基类和派生类都采用动态内存分配时,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类元素.这种要求是通过三种不同的方式来满足的.对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数.对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的.

13.7.3 使用动态内存分配和友元的继承示例

  • 程序清单13.14 dma.h
// dma.h  -- inheritance and dynamic memory allocation
#ifndef DMA_H_
#define DMA_H_
#include <iostream>
//  Base Class Using DMA
class baseDMA
{
private:
    char * label;
    int rating;

public:
    baseDMA(const char * l = "null", int r = 0);
    baseDMA(const baseDMA & rs);
    virtual ~baseDMA();
    baseDMA & operator=(const baseDMA & rs);
    friend std::ostream & operator<<(std::ostream & os, 
                                     const baseDMA & rs);
};
// derived class without DMA
// no destructor needed
// uses implicit copy constructor
// uses implicit assignment operator
class lacksDMA :public baseDMA
{
private:
    enum { COL_LEN = 40};
    char color[COL_LEN];
public:
    lacksDMA(const char * c = "blank", const char * l = "null",
              int r = 0);
    lacksDMA(const char * c, const baseDMA & rs);
    friend std::ostream & operator<<(std::ostream & os, 
                                     const lacksDMA & rs);
};
// derived class with DMA
class hasDMA :public baseDMA
{
private:
    char * style;
public:
    hasDMA(const char * s = "none", const char * l = "null",
              int r = 0);
    hasDMA(const char * s, const baseDMA & rs);
    hasDMA(const hasDMA & hs);
    ~hasDMA();
    hasDMA & operator=(const hasDMA & rs);  
    friend std::ostream & operator<<(std::ostream & os, 
                                     const hasDMA & rs);
};
#endif
  • 程序清单13.15 dma.cpp
// dma.cpp --dma class methods
#include "dma.h"
#include <cstring>
// baseDMA methods
baseDMA::baseDMA(const char * l, int r)
{
    label = new char[std::strlen(l) + 1];
    std::strcpy(label, l);
    rating = r;
}
baseDMA::baseDMA(const baseDMA & rs)
{
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
}
baseDMA::~baseDMA()
{
    delete [] label;
}
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
    if (this == &rs)
        return *this;
    delete [] label;
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
    return *this;
}

std::ostream & operator<<(std::ostream & os, const baseDMA & rs)
{
    os << "Label: " << rs.label << std::endl;
    os << "Rating: " << rs.rating << std::endl;
    return os;
}
// lacksDMA methods
lacksDMA::lacksDMA(const char * c, const char * l, int r)
    : baseDMA(l, r)
{
    std::strncpy(color, c, 39);
    color[39] = '\0';
}
lacksDMA::lacksDMA(const char * c, const baseDMA & rs)
    : baseDMA(rs)
{
    std::strncpy(color, c, COL_LEN - 1);
    color[COL_LEN - 1] = '\0';
}
std::ostream & operator<<(std::ostream & os, const lacksDMA & ls)
{
    os << (const baseDMA &) ls;
    os << "Color: " << ls.color << std::endl;
    return os;
}
// hasDMA methods
hasDMA::hasDMA(const char * s, const char * l, int r)
         : baseDMA(l, r)
{
    style = new char[std::strlen(s) + 1];
    std::strcpy(style, s);
}
hasDMA::hasDMA(const char * s, const baseDMA & rs)
         : baseDMA(rs)
{
    style = new char[std::strlen(s) + 1];
    std::strcpy(style, s);
}
hasDMA::hasDMA(const hasDMA & hs)
         : baseDMA(hs)  // invoke base class copy constructor
{
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
}
hasDMA::~hasDMA()
{
    delete [] style;
}
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
    if (this == &hs)
        return *this;
    baseDMA::operator=(hs);  // copy base portion
    delete [] style;         // prepare for new style
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
}

std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
    os << (const baseDMA &) hs;
    os << "Style: " << hs.style << std::endl;
    return os;
}
  • 程序清单13.16 usedma.cpp
// usedma.cpp -- inheritance, friends, and DMA
// compile with dma.cpp
#include <iostream>
#include "dma.h"
int main()
{
    using std::cout;
    using std::endl;
    baseDMA shirt("Portabelly", 8);
    lacksDMA balloon("red", "Blimpo", 4);
    hasDMA map("Mercator", "Buffalo Keys", 5);
    cout << "Displaying baseDMA object:\n";
    cout << shirt << endl;
    cout << "Displaying lacksDMA object:\n";
    cout << balloon << endl;
    cout << "Displaying hasDMA object:\n";
    cout << map << endl;
    lacksDMA balloon2(balloon);
    cout << "Result of lacksDMA copy:\n";
    cout << balloon2 << endl;
    hasDMA map2;
    map2 = map;
    cout << "Result of hasDMA assignment:\n";
    cout << map2 << endl;
    // std::cin.get();
    return 0; 
}

13.8 类设计回顾
13.8.1 编译器生成的成员函数

  1. 默认构造函数
    • 默认构造函数要么没有参数,要么所有的参数都有默认值.
    • 最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值.
  2. 复制构造函数
    • 复制构造函数接收其所属类的对象作为参数.
    • 在下述情况下,将使用复制构造函数:
      • 将新对象初始化为一个同类对象;
      • 按值将对象传递给函数;
      • 函数按值返回对象;
      • 编译器生成临时对象.
    • 如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数.也就是说,新对象的每个成员都被初始化为原始对象相应成员的值.如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数.
    • 在某些情况下,成员初始化是不合适的.例如,使用new初始化的成员指针通常要求执行深复制,或者类可能包含需要修改的静态变量.在上述情况下,需要定义自己的复制构造函数.
  3. 赋值运算符
    • 如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符.
    • 编译器不会生成将一种类型赋给另一种类型的赋值运算符.

13.8.2 其他的类方法

  1. 构造函数
  2. 析构函数
    • 一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作.对于基类,即使它不需要析构函数,也应提供一个虚析构函数.
  3. 转换
    • 使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换.将可转换的类型传递给以类为参数的函数时,将调用转换构造函数.
    • 在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换.
    • 要将类对象转换为其他类型,应定义转换函数.
  4. 按值传递对象与传递引用
    • 通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象.另一个原因是,在继承使用虚函数时,被定义为接收基类引用参数的函数可以接受派生类.
  5. 返回对象和返回引用
    • 首先,在编码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头.
    • 其次,应返回引用而不是返回对象的原因在于,返回对象设计生成返回对象的临时副本,这是调用函数的程序可以使用的副本.返回引用可节省时间和内存.
    • 然而,并不总是可以返回引用.函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的.
    • 通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用.如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象.
  6. 使用const
    • 使用const时应特别注意.可以用它来确保方法不修改参数.
    • 可以使用const来确保方法修改调用它的对象.
    • 通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象.但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据.

13.8.3 公有继承的考虑因素

  1. is-a关系
  2. 什么不能被继承
    • 构造函数
    • 析构函数
    • 赋值运算符
  3. 赋值运算符
    • 如果类构造函数使用new来初始化指针,则需要提供一个显式赋值运算符.
  4. 私有成员与保护成员
  5. 虚方法
    • 设计基类时,必须确定是否将类方法声明为虚的.
  6. 析构函数
  7. 友元函数
    • 由于友元函数并非类成员,因此不能继承.
    • 然而,您可能希望派生类的幽咽函数能够使用基类的友元函数,为此,可以通过强制类型转换将,派生类应用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数.
  8. 有关使用基类方法的说明

13.8.4 类函数小结

  • 表13.1(摘自《The annotated C++ Reference Manual》)
    • C++类函数有很多不同的变体,其中有些可以继承,有些不可以.有些运算符函数即可以是成员函数,也可以是友元,而有些运算符函数只能是成员函数.

13.9 总结
13.10 复习题
13.11 编程练习

附件:本章源代码下载地址

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值