这一章在干嘛?
面向对象编程的一大目标是代码重用。C 函数库靠”给你一堆现成函数”来重用,但函数没法扩展;C++ 的类继承更进一步:你可以从旧类派生出新类,新类自动拥有旧类的数据和方法,再按需添加或修改。本章讲的就是公有继承的完整玩法——怎么派生、构造函数怎么传递、什么是虚方法与动态绑定、什么是抽象基类,以及继承遇到动态内存分配时的注意事项。
13.1 一个简单的基类
继承中,被继承的类叫基类,继承别人的类叫派生类。Webtown 俱乐部要记录乒乓球会员,先写一个基类 TableTennisPlayer,只记姓名和是否有球台:
// tabtenn0.h -- 乒乓球基类
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none",
const string & ln = "none", bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; }
void ResetTable(bool v) { hasTable = v; }
};
#endif// tabtenn0.cpp -- 简单基类方法
#include "tabtenn0.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer(const string & fn,
const string & ln, bool ht) : firstname(fn),
lastname(ln), hasTable(ht) {}
void TableTennisPlayer::Name() const
{
std::cout << lastname << ", " << firstname;
}两个细节值得注意:第一,存名字用的是标准 string 类而不是字符数组,更安全也更省心;第二,构造函数用了成员初始化列表语法。如果改用在函数体内赋值,firstname 会先调用 string 默认构造函数、再调用赋值 operator 重置,多走一步;初始化列表直接用拷贝构造函数一步到位。
// usett0.cpp -- 使用基类
#include <iostream>
#include "tabtenn0.h"
int main(void)
{
using std::cout;
TableTennisPlayer player1("Chuck", "Blizzard", true);
TableTennisPlayer player2("Tara", "Boomdea", false);
player1.Name();
if (player1.HasTable())
cout << ": has a table.\n";
else
cout << ": hasn't a table.\n";
player2.Name();
if (player2.HasTable())
cout << ": has a table.\n";
else
cout << ": hasn't a table.\n";
return 0;
}输出:Blizzard, Chuck: has a table. / Boomdea, Tara: hasn't a table.
传的是 C 风格字符串 "Chuck",形参却是 const string &——类型不匹配?没关系,string 类有一个 const char * 参数的构造函数,会自动把 C 字符串转成 string 对象。
13.2 派生一个类:RatedPlayer
俱乐部里有人参加比赛了,需要记录积分。不必从零写起,从 TableTennisPlayer 派生一个 RatedPlayer:
// RatedPlayer 从 TableTennisPlayer 公有派生
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating; // 新增数据成员
public:
RatedPlayer(unsigned int r = 0, const string & fn = "none",
const string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const { return rating; }
void ResetRating(unsigned int r) { rating = r; }
};冒号表示 RatedPlayer 基于基类 TableTennisPlayer,public 表示公有派生。派生对象的两大特征:
- 派生对象中存储着基类的数据成员(继承实现);
- 派生对象可以使用基类的方法(继承接口)。
所以 RatedPlayer 对象能存姓名、能用 Name() 和 HasTable()。派生类需要自己补充的是:必须有自己的构造函数,并可以随意添加新成员。
构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,必须通过基类方法。创建派生类对象时,程序先创建基类对象——概念上应在进入派生类构造函数体之前完成。C++ 用成员初始化列表做到这一点:
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}: TableTennisPlayer(fn, ln, ht) 就是初始化列表,它调用基类构造函数,把 "Mallory"、"Duck"、true 存进内嵌的基类对象,然后才执行 rating = r;。

▲ 图 13.1 基类对象与派生类对象:派生对象中内嵌着一个基类对象

▲ 图 13.2 派生类构造函数把参数”接力”传给基类构造函数
如果省略初始化列表呢?基类对象反正要先创建,程序会自动调用基类的默认构造函数。所以除非你正好想要默认版本,否则应显式写出基类构造函数调用。
第二个构造函数接收一个现成的 TableTennisPlayer 对象:
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}tp 是基类引用,调用的是基类拷贝构造函数(编译器自动生成的逐成员拷贝版本,这里够用)。
构造与析构顺序
创建派生对象:先基类构造函数,后派生类构造函数。销毁派生对象:先派生类析构函数,后基类析构函数——顺序正好相反。
13.3 派生类与基类的特殊关系
派生类对象能调用基类的公有方法;此外还有两条更重要的规则——基类指针和引用可以在不显式转换的情况下指向/引用派生类对象:
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer1; // 基类引用派生对象:OK
TableTennisPlayer * pt = &rplayer1; // 基类指针指向派生对象:OK
rt.Name(); // 用引用调用
pt->Name(); // 用指针调用但基类指针/引用只能调用基类方法,不能调用派生类新增的 Rating()。反过来不成立:不能把基类对象赋给派生类引用/指针——否则就能对基类对象调用 Rating(),而它根本没有 rating 成员。
这条”引用兼容”规则带来几个实用推论:
- 函数形参是基类引用/指针时,既能接收基类对象也能接收派生类对象;
- 可以用派生对象初始化基类对象(只拷贝基类部分);
- 可以把派生对象赋值给基类对象(同样只涉及基类成员)。
void Show(const TableTennisPlayer & rt); // 一个函数通吃两种对象
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Show(player1); // OK
Show(rplayer1); // OK——派生对象自动"向上"匹配基类引用13.4 继承:is-a 关系
公有继承建模的是 is-a 关系:派生类对象也应当是一种基类对象。香蕉是一种水果,所以 Banana 可以从 Fruit 派生;但关系不必对称——水果不是香蕉,基类对象不具备派生类的全部能力。
同样重要的是认清哪些关系不该用公有继承建模:
- has-a(有一个):午餐里有水果,但午餐不是水果。正确做法是把
Fruit对象作为Lunch的数据成员(包含/组合,见第 14 章)。 - is-like-a(像):律师像鲨鱼,但律师不是鲨鱼。继承只能给基类添加属性,不能删除属性,所以”像”不能用继承表达。
- is-implemented-as-a(按……实现):用数组实现栈,但栈不是数组。应把
Array对象作为Stack的私有成员隐藏实现。 - uses-a(使用):电脑用打印机,但互相不是对方的一种。可用友元函数处理两者通信。

▲ 图 13.3 is-a 用公有继承表达;has-a 用”包含一个成员对象”表达
13.5 多态公有继承:Brass 与 BrassPlus
派生类直接沿用基类方法是最简单的情况。更进阶的需求是:同一个方法名,对不同类型的对象表现出不同行为——这就是多态(polymorphic,“多种形态”)。实现多态公有继承有两个关键机制:在派生类中重新定义基类方法,以及使用虚方法。
银行例子:Brass 是普通支票账户,BrassPlus 是带透支保护的支票账户。BrassPlus 满足 is-a 测试吗?当然——Brass 有的它都有,还要额外存透支上限、贷款利率、欠款额;取款和显示两个操作的实现也不同。
// brass.h -- 银行账户类
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
// Brass 账户类
class Brass
{
private:
std::string fullName;
long acctNum;
double balance;
public:
Brass(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0);
void Deposit(double amt); // 存款:两个类行为相同
virtual void Withdraw(double amt); // 取款:虚方法
double Balance() const;
virtual void ViewAcct() const; // 显示:虚方法
virtual ~Brass() {} // 虚析构函数
};
// Brass Plus 账户类
class BrassPlus : public Brass
{
private:
double maxLoan; // 透支上限
double rate; // 贷款利率
double owesBank; // 当前欠银行的钱
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0, double ml = 500, double r = 0.11125);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct() const; // 重新定义
virtual void Withdraw(double amt); // 重新定义
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; }
void ResetOwes() { owesBank = 0; }
};
#endif三个要点:
ViewAcct()和Withdraw()在两个类里各有一份定义,限定名分别是Brass::ViewAcct()和BrassPlus::ViewAcct(),用对象类型决定用哪份。- 基类里这两个方法加了
virtual。虚方法决定:通过引用或指针调用时,按引用/指针所指向的对象类型选择方法版本,而不是按引用/指针自身的类型。不重写的Deposit()、Balance()只在基类声明一份。 - 基类声明了虚析构函数——即使它什么都不做(原因见 13.8 节)。
方法实现里的两个标准技巧。派生类构造函数用初始化列表把基类数据交给基类构造函数:
BrassPlus::BrassPlus(const string & s, long an, double bal,
double ml, double r) : Brass(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}普通派生类方法则用作用域解析运算符调用基类版本。BrassPlus::ViewAcct() 先让基类版本显示基类数据,再显示新增数据:
void BrassPlus::ViewAcct() const
{
// ……格式设置省略
Brass::ViewAcct(); // 显示基类部分
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout << "Loan Rate: " << 100 * rate << "%\n";
}常见坑:重定义方法里忘了作用域解析运算符
如果在
BrassPlus::ViewAcct()里直接写ViewAcct();,编译器会认为你在调用BrassPlus::ViewAcct()自己——一个没有出口的无限递归。重定义方法内调用基类版本时,必须写Brass::ViewAcct();。
BrassPlus::Withdraw() 演示了透支逻辑:余额够就走基类取款;不够但在透支额度内,就先 Deposit(advance) 垫资(把贷款存进去),再调 Brass::Withdraw(amt) 完成取款——绕开了基类取款方法里”余额不足报错”的检查:
void BrassPlus::Withdraw(double amt)
{
double bal = Balance();
if (amt <= bal)
Brass::Withdraw(amt);
else if (amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "Bank advance: $" << advance << endl;
cout << "Finance charge: $" << advance * rate << endl;
Deposit(advance);
Brass::Withdraw(amt);
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
}Balance() 不用加作用域解析,因为它没被派生类重新定义。
13.6 虚方法登场:一个数组管理多种账户
前一个测试程序都是用对象直接调用方法,还体现不出虚方法的威力。想用一个数组同时管理 Brass 和 BrassPlus 账户?数组元素必须同类型,但基类指针数组可以——每个 Brass * 既能指向 Brass 也能指向 BrassPlus:
// usebrass2.cpp -- 多态示例(节选核心)
const int CLIENTS = 4;
Brass * p_clients[CLIENTS];
// ……循环里按用户输入决定:
if (kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
// ……
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct(); // 按对象类型选择版本!
cout << endl;
}
for (int i = 0; i < CLIENTS; i++)
delete p_clients[i]; // 释放内存同一个循环语句 p_clients[i]->ViewAcct(),指向 Brass 时执行 Brass::ViewAcct(),指向 BrassPlus 时执行 BrassPlus::ViewAcct()。如果 ViewAcct() 不是虚方法,则所有情况都只会执行 Brass::ViewAcct()——因为程序按指针类型(Brass *)做选择。这就是多态的实质。
这里也体现了虚析构函数的必要性:delete p_clients[i] 时,若析构函数不是虚的,则只按指针类型调用 ~Brass();是虚的,才按对象类型先调 ~BrassPlus() 再自动调 ~Brass()。本例析构函数都为空看不出差别,但若派生类析构函数要释放 new 来的内存,漏掉 virtual 就是内存泄漏。
13.7 静态绑定与动态绑定
把源代码里的函数调用解释为执行某一段函数代码,叫绑定(binding)。编译阶段就完成的绑定叫静态绑定(或早期绑定);虚方法导致必须运行时才能决定的绑定叫动态绑定(或晚期绑定)。
为什么编译器决定不了?因为编译器不知道用户运行时会往基类指针里放哪种对象。
指针与引用的类型兼容:向上/向下转换
- 向上转换(upcasting):派生类指针/引用 → 基类指针/引用。公有继承下总是允许、无需显式转换,且可传递(
BrassPlusPlus也能转给Brass *)。这是 is-a 关系的直接体现。 - 向下转换(downcasting):基类指针/引用 → 派生类指针/引用。不允许隐式进行——派生类可能新增了成员,对基类对象调用派生类方法毫无意义。

▲ 图 13.4 向上转换总是安全;向下转换必须显式进行且可能不合理
按值传参会发生”截断”:把 BrassPlus 对象按值传给 void fv(Brass b),只有 Brass 部分被拷贝,多态失效。而按引用/指针传递时隐式向上转换发生,虚方法仍能正确选择版本——这就是”处理类对象应尽量按引用传递”的又一理由。
为什么不把动态绑定设为默认?
两个原因:效率与概念模型。动态绑定要求程序额外跟踪指针指向的对象类型(虚函数表等机制),有内存与速度开销。C++ 的设计原则是”不为用不到的特性付费”;同时,把方法声明为非虚也是对编译器和使用者的一种声明:这个方法不打算被重定义(如 Brass::Balance())。
什么时候用 virtual
基类方法若预期在派生类中被重新定义,就声明为 virtual;不希望被重定义的,就别加。
13.8 虚函数的工作原理与注意事项
实现机制:虚函数表
编译器给每个对象添加一个隐藏成员——指向**虚函数表(vtbl)**的指针。vtbl 是该类所有虚函数地址组成的数组:基类对象指向基类的表;派生类若重定义了某虚函数,自己的表里就存新地址;没重定义就存原地址。调用虚函数时,程序经对象的表指针查表、按位置取地址、跳转执行。

▲ 图 13.5 虚函数机制:对象 → vptr → vtbl → 函数地址
代价有三:每个对象多存一个地址;每个类多一张表;每次调用多一次查表。
几条必须记住的规则
- 构造函数不能是虚的:派生类不继承基类构造函数,创建对象时调用的是派生类自己的构造函数。
- 析构函数应当是虚的:只要类可能作基类,就给析构函数加
virtual——哪怕它什么都不做(virtual ~BaseClass() {})。这样delete基类指针时才能触发正确的析构序列。 - 友元不能是虚的:友元不是类成员,只有成员才能是虚函数;可让友元内部改调虚成员函数绕开。
- 重新定义将隐藏方法:派生类重定义基类方法时,隐藏的是基类所有同名方法,不管参数签名是否相同——重定义不是重载!
常见坑:重定义隐藏了基类的重载版本
基类有
showperks(int)、showperks(double)、showperks()三个重载,派生类只重定义了无参版本,那么另外两个对派生对象就不可用了。要么把基类所有版本都重新定义(不需要改动的版本可以只转调基类:void Hovel::showperks() const { Dwelling::showperks(); }),要么干脆别改参数签名。唯一例外是返回类型协变:基类返回Dwelling &,派生类可改为返回Hovel &,前提是Hovel是Dwelling的派生类。
13.9 访问控制:protected
protected 与 private 对外界一样不可访问;区别只在派生类内部:派生类成员可以直接访问基类的 protected 成员,却不能直接访问 private 成员。
如果把 Brass::balance 改为 protected,BrassPlus::Withdraw() 可以直接写 balance -= amt;,代码更简短。但这是有设计代价的——BrassPlus 可以写一个 Reset(double amt) { balance = amt; },绕开 Deposit()/Withdraw() 的全部安全检查,等于把 balance 变相公开了。
常见坑:滥用 protected 数据成员
Stroustrup 的建议是:数据成员宁可 private 也不要 protected;派生类要访问数据,请走基类方法。protected 更适合用在成员函数上——给派生类提供内部工具,又不暴露给公众(13.10 节的 AcctABC 就是范例)。
13.10 抽象基类(ABC)
有些时候 is-a 不好套:圆是椭圆的特例,但从 Ellipse 派生 Circle 很别扭——圆只需要半径,却被迫背上长轴、短轴、旋转角;Scale() 还可能把圆缩放成非圆。更好的思路是把两者的公共部分(圆心坐标、Move()、Area())抽出来放进抽象基类,让 Circle 和 Ellipse 都从它派生。
问题是:ABC 里没有数据,Area() 没法实现。C++ 的答案是纯虚函数——声明末尾加 = 0:
class BaseEllipse // 抽象基类
{
private:
double x; // 圆心 x 坐标
double y; // 圆心 y 坐标
public:
BaseEllipse(double x0 = 0, double y0 = 0) : x(x0), y(y0) {}
virtual ~BaseEllipse() {}
void Move(int nx, int ny) { x = nx; y = ny; }
virtual double Area() const = 0; // 纯虚函数
};类中只要有一个纯虚函数,它就是抽象类,不能创建对象——它存在的唯一意义是作基类。纯虚函数甚至可以提供定义(在实现文件里写 BaseEllipse::Move(...)),= 0 只是声明”本类是抽象的、函数可以不定义”。能创建对象的派生类(如 Ellipse、Circle)称为具体类。
银行例子重构:把 Brass 与 BrassPlus 的公共部分抽成 AcctABC。它提供 protected 方法 FullName()、AcctNum()、SetFormat()、Restore() 供派生类访问数据与格式化,Withdraw() 和 ViewAcct() 声明为纯虚函数:
// acctabc.h -- 银行账户类(节选)
class AcctABC
{
private:
std::string fullName;
long acctNum;
double balance;
protected:
struct Formatting { std::ios_base::fmtflags flag; std::streamsize pr; };
const std::string & FullName() const { return fullName; }
long AcctNum() const { return acctNum; }
Formatting SetFormat() const;
void Restore(Formatting & f) const;
public:
AcctABC(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt) = 0; // 纯虚函数
double Balance() const { return balance; }
virtual void ViewAcct() const = 0; // 纯虚函数
virtual ~AcctABC() {}
};
class Brass : public AcctABC
{
public:
Brass(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0) : AcctABC(s, an, bal) {}
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass() {}
};
class BrassPlus : public AcctABC { /* 新增 maxLoan/rate/owesBank 及重定义 */ };
#endif使用方式几乎不变:把管理数组的类型换成 AcctABC * p_clients[CLIENTS];,其余代码原样运行。ABC 的哲学是强制接口:纯虚函数逼迫每个具体派生类给出自己的实现,相当于组件设计中”所有派生类必须兑现的接口契约”。
通关标准:
能独立回答并演示:①派生对象的构造/析构顺序及初始化列表的写法;②基类指针/引用指向派生对象后,虚与非虚方法分别调用哪个版本;③为什么基类析构函数要 virtual;④纯虚函数与抽象类的关系;⑤重新定义方法为什么会隐藏基类重载版本。
13.11 继承与动态内存分配
基类用了 new(因而定义了析构函数、拷贝构造函数、赋值运算符)时,派生类怎么办?分两种情况。
情况一:派生类不用 new
派生类(如 lacksDMA,只有 char color[40] 数组成员)什么都不用做:默认析构函数执行完自己的代码后会自动调用基类析构函数;默认拷贝构造函数对新增成员逐成员拷贝,对继承的基类部分则调用基类的拷贝构造函数;默认赋值运算符同样自动调用基类赋值运算符。
情况二:派生类也用 new
派生类(如 hasDMA,有 char * style)必须显式定义三件套,且都要”照顾到基类部分”:
// dma.h -- 继承与动态内存分配(节选)
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);
};
class hasDMA : public baseDMA
{
private:
char * style;
public:
hasDMA(const char * s = "none", const char * l = "null", int r = 0);
hasDMA(const hasDMA & hs);
~hasDMA();
hasDMA & operator=(const hasDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const hasDMA & hs);
};析构函数:各管各的,基类部分自动处理——
baseDMA::~baseDMA() { delete [] label; } // 管基类资源
hasDMA::~hasDMA() { delete [] style; } // 只管派生类新增资源拷贝构造函数:派生类拷贝构造函数只能访问派生类数据,基类部分要靠初始化列表显式调用基类拷贝构造函数:
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) // 调用基类拷贝构造
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}baseDMA(hs) 能编译是因为基类引用可以引用派生对象——基类拷贝构造函数用 hs 的基类部分来构造新对象的基类部分。
赋值运算符:用作用域解析显式调用基类赋值运算符。注意必须用函数表示法而不能写 *this = hs;——后者会被解释为 hasDMA::operator=(),无限递归:
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator=(hs); // 拷贝基类部分(函数表示法)
delete [] style; // 释放旧内存
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}友元:派生类的友元 operator<<() 想输出基类部分,可以把派生引用强制转换为基类引用,让原型匹配选中基类的友元版本:
std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
os << (const baseDMA &) hs; // 转换以匹配基类友元版本
os << "Style: " << hs.style << std::endl;
return os;
}(更好的写法是第 15 章的 dynamic_cast<const baseDMA &>(hs)。)
13.12 类设计复习要点
- 编译器自动生成的特殊成员:默认构造函数(没定义任何构造函数时生成);拷贝构造函数(按值传参、按值返回、初始化同型对象时使用,逐成员初始化);赋值运算符(已有对象间赋值,逐成员赋值)。用了
new的类三者都可能要自己写。 - 构造函数不继承、析构函数不继承、赋值运算符不继承:基类构造器负责基类部分,派生类构造器通过初始化列表调用它;赋值运算符的参数类型随类而变,天然无法继承。
- 转换与 explicit:单参数构造函数会充当隐式转换函数,必要时用
explicit关闭隐式转换;反向转换靠转换函数(如operator double()),也要慎用、避免二义性。 - 按引用传参、按引用返回:省去拷贝构造与析构的开销,还保留多态。例外:函数内创建的临时对象不能返回引用(函数一结束对象就没了),必须按值返回,如
Vector operator+(const Vector & b) const { return Vector(x + b.x, y + b.y); }。 - const 的三处使用:保证不修改指向的数据(
const char * s)、保证不修改调用对象(void show() const)、保证返回的引用不被用于修改对象(const Stock & topval(...) const)。 - 按值传递派生对象会切片:形参
void inadequate(Brass ba)收到BrassPlus时只拷贝Brass部分,虚方法也失效——传引用才能保住多态。 - 派生→基类赋值:允许,只拷贝基类部分;基类→派生类赋值:默认不允许,除非派生类有转换构造函数
BrassPlus(const Brass &, ...)或专门定义了赋值运算符。
13.13 本章自测
自测:创建派生类对象时,构造函数和析构函数的调用顺序是什么?
构造:先基类构造函数,后派生类构造函数;派生类构造函数通过成员初始化列表指定调用哪个基类构造函数,不指定则用基类默认构造函数。析构:顺序相反,先派生类析构函数,再自动调用基类析构函数。
自测:派生类不继承基类的哪些成员?
构造函数、析构函数、赋值运算符、友元都不被继承。派生类继承基类的全部数据成员和一般方法(含 protected 成员的访问权,经由公有派生还有公有接口)。
自测:
Brass * pb = &brassPlusObj; pb->ViewAcct();在 ViewAcct 为虚/非虚时分别调用哪个版本?为什么?虚:按对象类型调用
BrassPlus::ViewAcct()(动态绑定,运行期查虚函数表);非虚:按指针类型调用Brass::ViewAcct()(静态绑定,编译期决定)。
自测:派生类重新定义
void show(int)而基类还有show()和show(double),派生对象还能调用后两者吗?不能。重新定义会隐藏基类所有同名方法(与参数无关)。要么把三个版本都在派生类里重新定义(无需改动的可以只转调基类版本),要么调整设计。
自测:派生类和基类都用了动态内存分配时,派生类的"三件套"各自如何调用基类对应部分?
析构函数:不用显式调用,派生类析构函数执行完自动调基类析构函数;拷贝构造函数:在成员初始化列表里写
baseDMA(hs);赋值运算符:函数体内用作用域解析显式调用baseDMA::operator=(hs);(必须用函数表示法防止递归)。