本章内容包括:
- is-a关系的继承
- 如何以公有方式从一个类派生出另一个类
- 保护访问
- 构造函数成员初始化列表
- 向上和向下强制转换
- 虚成员函数
- 早期(静态)联编与晚期(动态)联编
- 抽象基类
- 纯虚函数
- 何时及如何使用公有继承
面向对象编程的主要目的之一是提供可重用的代码.目前,很多厂商提供了类库,类库由类声明和实现构成.因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包.
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,程序将根据引用或指针指向的对象的类型来选择方法.
- 注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的.这样,程序将根据对象类型而不是应用或指针的类型来选择方法版本.为基类声明一个虚析构函数也是一种惯例.
- 关键字virtual.虚方法
- 1.类实现
- 关键字virtual只用于类声明的方法原型中,而没有用于类实现的方法定义中.
- 程序清单13.8 brass.cpp
- 派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法.非构造函数不能使用成员初始化列表语法.
- 在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法.
- 2.使用Brass和BrassPlus类
- 程序清单13.9 usebrass1.cpp
- 请注意为何Hogg受透支限制,而Pigg没有
- 程序清单13.9 usebrass1.cpp
- 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 有关虚函数注意事项
- 构造函数
- 构造函数不能是虚函数.将类构造函数声明为虚的没什么意义.
- 析构函数
- 析构函数应当是虚函数,除非类不用做基类.
- 提示:通常应给基类提供一个虚析构函数,即使它并不需要析构函数.
- 友元
- 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数.
- 如果由于这个原因引起了设计问题,可以通过让友元函数使用虚函数成员函数来解决.
- 没有重新定义
- 如果派生类没有重新定义函数,将使用该函数的基类版本.如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的(稍后将介绍).
- 重新定义将隐藏方法.
- 总之,重新定义继承的方法并不是重载.如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何.
- 这引出两条经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的).这种特性被称为返回类型协变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; -
}
本文深入探讨C++中的继承机制,包括公有、保护和私有继承的区别,以及如何正确使用构造函数成员初始化列表。同时,文章详细讲解了多态的概念,包括虚函数的使用、静态与动态联编的差异,以及抽象基类和纯虚函数的作用。通过实例代码,读者可以更好地理解这些概念的实际应用。

被折叠的 条评论
为什么被折叠?



