c++ primer plus 十三章 类继承

第 13 章 类继承

C++类提供了类库来提高重用性。类库由类声明和实现构成,以源代码的方式提供,所以要修改满足不同的需求,就需要修改代码。

C++提供类继承来扩展和修改类。类继承从已有的类派生出新的类,而派生类继承原有类(基类)的特征,包括方法。

继承可以完成的工作

  • 在已有类的基础上添加功能
  • 可给类添加数据
  • 可修改类方法的行为

13.1 一个简单的基类

从一个类派生出另一个类时,原始类称为基类继承类称为派生类

为了说明继承,首先需要一个基类。Webtwon俱乐部决定跟踪乒乓球会会员。作为俱乐部的首席程序员,需要设计一个简单的TableTennisPlayer类

实现的程序如下:

1、头文件

#ifndef TEST_TABTENN0_H
#define TEST_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 //TEST_TABTENN0_H

2、源文件

#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;
}

此处TableTennisPlayer构造函数使用了初始化列表的语法,相对于下面语句的初始化,省下调用string的默认构造函数的功夫

TableTennisPlayer::TableTennisPlayer(const string &fn, const string &ln, bool ht) {
		firstName = fn;
		lastName = ln;
		hasTable = ht;
}

3、执行文件

#include <iostream>
#include "tabtenn0.h"

int main(){
    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 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.

13.1.1 派生一个类

Webtown俱乐部的一些成员曾经参加过当地的乒乓球锦标赛,需要这样一个类,它能包括成员在比赛中的比分。与其从零开始,不如从TableTennisPlayer类派生出一个类。首先将RatedPlayer类声明从TableTennisPlayer类派生出来

继承的声明格式(公有继承):

//public表面声明是一个公有基类,公有派生类
class leaves : public root
{
    ......
}

// 书中例子
class RatePlayer : public TableTennisPlayer //基类(原始类):TableTennisPlayer,派生类(继承类):RatePlayer
{
    ......
}

派生类对象(上述code中的RatePlayer)的特征:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口

⚠️⚠️:基类的私有部分也将成为派生类的一部分,在一般情况下是访问不到的(通俗来说,就是被屏蔽了),但可以通过基类的公有和保护方法访问。

请添加图片描述

相应的,继承父类的数据成员和方法后的RatedPlayer还需要自己构建自己的成员函数,才能实现预期的功能:存储比分的时候还应实现检索比分重置比分的方法。

class RatedPlayer : public TableTennisPlayer {
private:
    unsigned int rating;    //add a data member
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;
    }
};

其实第一个构造函数和第二个构造函数所传递的数据是差不多的,因为第二个构造函数传进一个TableTennisPlayer类,这个类就包含了firstName,lastName和hasTable

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

⚠️注意:派生类不能直接访问基类的私有成员,而必须通过基类公有方法来进行访问私有的基类成员。具体来说,派生类构造函数必须使用基类构造函数。

派生类构造函数的要点

  • 创建派生类首先创建基类对象

  • ❗️❗️派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数

    • 如果省略成员初始化列表,情况会如何?答:它会自动地使用默认的基类构造函数

    • RatedPlayer(unsigned int r, const TableTennisPlayer &tp):TableTennisPlayer(tp){
      			rating = r;
      }
      

      这里TableTennisPlayer(tp)调用了编译器生成的拷贝构造函数(因为基类没有定义拷贝构造函数),但是在此时,浅拷贝已经足够了,因为它没有涉及动态内存分配,无需顾及深拷贝。

  • 派生类构造函数应初始化派生类新增的数据成员。

RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const 
                         string &ln,bool ht):TableTennisPlayer(fn,ln,ht) // :TableTennisPlayer(fn,ln,ht) 是成员初始化列表
{
   rating=r;
}

派生类对象的释放顺序与创建对象时的顺序相反(首先执行派生类的析构函数目,然后自动调用基类的析构函数)。

13.1.3 使用派生类

1、头文件

#ifndef TEST_TABTENN0_H
#define TEST_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;
    }
};

class RatedPlayer : public TableTennisPlayer {
private:
    unsigned int rating;    //add a data member
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;
    }
};

#endif //TEST_TABTENN0_H

2、源文件

#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;
}

//RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln, bool ht) : TableTennisPlayer(fn, ln, ht) {
    rating = r;
}

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer &tp) : TableTennisPlayer(tp) {
    rating = r;
}

3、执行文件

#include <iostream>
#include "tabtenn0.h"

int main(){
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Chuck","Blizzard", false);
    RatedPlayer rPlayer1(1140,"Mallory","Duck", true);
    
    rPlayer1.Name();
    if (rPlayer1.HasTable()){
        cout << ": has a table.\n";
    }else{
        cout << ": hasn't a table.\n";
    }

    player1.Name();
    if (player1.HasTable()){
        cout << ": has a table.\n";
    }else{
        cout << ": hasn't a table.\n";
    }

    cout << "Name:";
    rPlayer1.Name();
    cout << ";Rating: " << rPlayer1.Rating() << endl;
    //initialize RatedPlayer using TableTennisPlayer object
    RatedPlayer rPlayer2(1212,player1);
    cout << "Name:";
    rPlayer2.Name();
    cout << ";Rating: " << rPlayer2.Rating() << endl;

    return 0;
}

输出:
Duck Mallory: has a table.
Blizzard Chuck: hasn't a table.
Name:Duck Mallory;Rating: 1140
Name:Blizzard Chuck;Rating: 1212

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

  • 派生类对象可以使用基类的方法,条件是方法不是私有的:

    • RatedPlayer rPlayer1(1140,"Mallory","Duck", true);
      rPlayer1.name();		//derived object uses base method
      
  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象

    • RatedPlayer rPlayer1(1140,"Mallory","Duck", true);
      TableTennisPlayer *pt	= &rplayer1;
      pt->Name();				//invoke Name() with pointer
      
  • 基类引用可以在不进行显式类型转换的情况下引用派生类对象

    • RatedPlayer rPlayer1(1140,"Mallory","Duck", true);
      TableTennisPlayer &pt	= &rplayer1;
      pt.Name();				//invoke Name() with reference
      

基类指针引用只能用于调用基类方法。因此,不能使用 rt 或 pt来调用派生类的 ResetRanking 方法。

C++中要求引用和指针类型与赋给的类型匹配,但这规则对于继承来说是一种单向例外,不可以将基类对象和地址赋给派生类引用和指针。

如果基类引用和指针可以指向派生类对象,则如下情况均可正常使用:

void Show(const TableTennisPlayer &rt){
		using std::cout;
		cout << "Name:";
		rt.Name();
		cout << "\nTable:";
		if(rt.HasTable()){
				cout << "Yes\n";
		}else{
				cout << "No\n";
		}
}

void Wohs(const TableTennisPlayer *rt){
		using std::cout;
		cout << "Name:";
		(*rt).Name();
		cout << "\nTable:";
		if((*rt).HasTable()){
				cout << "Yes\n";
		}else{
				cout << "No\n";
		}
}
  • 基类引用定义的函数或指针参数可用于基类对象或派生类对象。

    • 形参为基类引用时,可指向基类对象或派生类对象。

      • TableTennisPlayer player1("Tara","Boomdea",false);
        RatedPlayer rplayer1(1140,"Mallory","Duck",true);
        Show(player1);		//works with TableTennisPlayer argument
        Show(rplayer1);		//works with RatedPlayer argument
        
    • 形参为指向基类的指针的函数,可使用基类对象的地址或派生类对象的地址作为实参。

      • TableTennisPlayer player1("Tara","Boomdea",false);
        RatedPlayer rplayer1(1140,"Mallory","Duck",true);
        Wohs(&player1);			//works with TableTennisPlayer* argument
        Wohs(&rplayer1);		//works with RatedPlayer* argument
        
  • 引用的兼容性问题允许将基类对象初始化为派生类对象。

  • 可将派生类对象赋给基类对象。

13.2 继承:is-a关系

C++中3种继承方式:

  • 公有继承
  • 保护继承
  • 私有继承

公有继承是最常用的方式,建立一种 is-a 关系。即派生类对象也是一个基类对象,可对基类对象执行的任何操作,也可对派生类对象执行。

13.3 多态公有继承

多态:同一个方法的行为随上下文而异。

实现多态公有继承的两种机制:

  • 在派生类中重新定义基类的方法
  • 使用虚方法

使用 virtual 方法,程序会根据引用或指针指向的对象的类型来选择方法,而不是根据引用或指针类型来选择方法。

如果要在派生类会重新定义基类的方法,通常将基类方法声明为虚的。即在类方法声明前加上 virtual 关键字。被virtual声明的类方法称为虚方法

class BrassPlus:public Brass
{
    private:
        ......
    public:
        ......
        virtual void ViewAcct() const; //虚方法
        virtual void Withdraw(double amt); // 虚方法
}

现在我们看一个例子:

你成了银行的首席程序员(假如真的是,请提携一下我)。银行要你搞两个类,一个类是用来表示银行账户的基本信息(Brass Account),另一个类是包含(Brass Account)所有信息和透支上限透支贷款利率当前的透支总额

13.3.1 开发Brass类和BrassPlus类

请添加图片描述

1、头文件

#ifndef TEST_BRASS_H
#define TEST_BRASS_H
#include <string>
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 Account Class
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 m1 = 500,double r = 0.11125);
    BrassPlus(const Brass &br,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 //TEST_BRASS_H

⚠️⚠️:Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,并且都使用了virtual关键字。并且这些方法对应的行为是不同的。就举ViewAcct()类细说一下,用了virtual关键字后,在哪个类里面,哪个类名就是它的限定名。

//behavior with virtual ViewAcct();
//method chosen according to reference type
Brass("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct();		//use Brass::ViewAcct()
b2_ref.ViewAcct();		//use BrassPlus::ViewAcct()

如果没有使用关键字virtual,程序将根据引用的类型或者指针的类型选择方法

//behavior with non-virtual ViewAcct();
//method chosen according to reference type
Brass("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct();		//use Brass::ViewAcct()
b2_ref.ViewAcct();		//use Brass::ViewAcct()

2、源文件

//
// Created by wzybb on 2022/9/22.
//

#include "brass.h"
#include <iostream>

using std::cout;
using std::endl;
using std::string;

//formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;

format setFormat();

void restore(format f, precis p);

//Brass methods
Brass::Brass(const std::string &s, long an, double bal) {
    fullName = s;
    acctNum = an;
    balance = bal;
}

void Brass::Deposit(double amt) {
    if (amt < 0) {
        cout << "Negative deposit not allowed;"
             << "deposit is cancelled.\n";
    } else {
        balance += amt;
    }
}

void Brass::Withdraw(double amt) {
    //set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    if (amt > balance) {
        cout << "Withdrawal amount of $" << amt
             << " exceeds your balance.\n"
             << "Withdrawal canceled.\n";
    } else if (amt < 0) {
        cout << "Withdrawal amount must be positive;"
             << "withdrawal canceled.\n";
    } else {
        balance -= amt;
    }
    restore(initialState, prec);
}

double Brass::Balance() const {
    return balance;
}

void Brass::ViewAcct() const {
    //set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    cout << "The information of Bank_Account:\n";
    cout << "Name: " << fullName << endl;
    cout << "account_number: " << acctNum << endl;
    cout << "balance: $" << balance << endl;

    restore(initialState, prec);
}


//BrassPlus Methods
BrassPlus::BrassPlus(const std::string &s, long an, double bal, double m1, double r) : Brass(s, an, bal) {
    maxLoan = m1;
    rate = r;
    owesBank = 0;
}

BrassPlus::BrassPlus(const Brass &br, double m1, double r) : Brass(br) {
    maxLoan = m1;
    rate = r;
    owesBank = 0;
}

void BrassPlus::ViewAcct() const {
    //set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    Brass::ViewAcct();  //display base portion
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to Bank: $" << owesBank << endl;
    cout.precision(3);      //###.### format
    cout << "Loan Rate:" << 100 * rate << "%\n";
    restore(initialState, prec);
}

//redefine Withdraw() works
void BrassPlus::Withdraw(double amt) {
    //set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    double bal = Balance();
    if (amt <= bal) {
        Brass::Withdraw(amt);
    } else if (amt <= bal + maxLoan - owesBank) {//假如实际存款不够提款的话,那么就还有款借
        double advance = amt - bal;//资金缺口
        owesBank += (1.0 + rate) * advance;
        cout << "Bank advance: $ " << advance << endl;//借款
        cout << "Finance charge: $" << advance * rate << endl;//借款手续费
        Deposit(advance);
        Brass::Withdraw(amt);
    } else {
        cout << "Credit limit exceeded.Transaction cancelled.\n";
    }
    restore(initialState, prec);
}

format setFormat() {
    return cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}

void restore(format f, precis p) {
    cout.setf(f, std::ios_base::floatfield);
    cout.precision(p);
}

程序设计的时候有一个妙处:

BrassPlus::Withdraw(double amt){
	 ....
	 if(){
	 		.....
	 		Deposit(advance);
	 		Brass::Withdraw(double amt);
	 }
}

使用Deposit()方法进行房贷,然后得到足够的结余后调用Brass::Withdraw,从而避免了错误消息

3、执行文件

#include <iostream>
#include "brass.h"

int main(){
    using std::cout;
    using std::endl;

    Brass piggy("Porcelot Pigg",381299,4000.00);
    BrassPlus Hoggy("Horatio Hogg",382288,3000.00);
    piggy.ViewAcct();
    cout << endl;
    Hoggy.ViewAcct();
    cout << endl;
    cout << "Despoiting $1000 into the Hogg Account:\n";
    Hoggy.Deposit(1000.00);
    cout << "New balance: $" << Hoggy.Balance() << endl;
    cout << "Withdrawing $4200 from the Hogg Account:\n";
    Hoggy.Withdraw(4200.00);
    Hoggy.ViewAcct();

    return 0;
}
输出:
The information of Bank_Account:
Name: Porcelot Pigg
account_number: 381299
balance: $4000.00

The information of Bank_Account:
Name: Horatio Hogg
account_number: 382288
balance: $3000.00
Maximum loan: $500.00
Owed to Bank: $0.00
Loan Rate:11.125%

Despoiting $1000 into the Hogg Account:
New balance: $4000
Withdrawing $4200 from the Hogg Account:
Bank advance: $ 200.00
Finance charge: $22.25
The information of Bank_Account:
Name: Horatio Hogg
account_number: 382288
balance: $0.00
Maximum loan: $500.00
Owed to Bank: $222.25
Loan Rate:11.125%

4、演示虚方法的行为:

由于上述程序,方法是通过对象调用的,现在我们通过指针调用,来展示虚函数特性

#include <iostream>
#include "brass.h"
#include <string>
const int CLIENTS = 4;

int main(){
    using std::cin;
    using std::cout;
    using std::endl;

    Brass *p_clients[CLIENTS];
    std::string temp;
    long tempnum;
    double tempbal;
    char kind;

    for (int i = 0; i < CLIENTS; ++i) {
        cout << "Enter client's name:";
        getline(cin,temp);
        cout << "Enter client's account number:";
        cin >> tempnum;
        cout << "Enter opening balance:$";
        cin >> tempbal;
        cout << "Enter 1 for Brass Account or "
            << "2 for BrassPlus Account: ";
        while (cin >> kind && (kind != '1' && kind != '2')){
            cout << "Enter either 1 or 2:";
        }
        if (kind == '1'){
            p_clients[i] = new Brass(temp,tempnum,tempbal);
        }else{
            double tmax,trate;
            cout << "Enter the overdraft limit: $";
            cin >> tmax;
            cout << "Enter the interest rate "
                << "as a decimal fraction: ";
            cin >> trate;
            p_clients[i] = new BrassPlus(temp,tempnum,tempbal,tmax,trate);
        }

        while (cin.get() != '\n'){
            continue;
        }
    }
    cout << endl;
    for (int i = 0; i < CLIENTS; ++i) {
        p_clients[i]->ViewAcct();
        cout << endl;
    }
    for (int i = 0; i < CLIENTS; ++i) {
        delete p_clients[i];      //free memory
    }
    cout << "Done.\n";
    return 0;
}

输出:
Enter client's name:Harry Fishsong
Enter client's account number:112233
Enter opening balance:$1500
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name:Dinah Otternoe
Enter client's account number:121213
Enter opening balance:$1800
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $350
Enter the interest rate as a decimal fraction: 0.12
Enter client's name:Brenda Birdherd
Enter client's account number:212118
Enter opening balance:$5200
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $800
Enter the interest rate as a decimal fraction: 0.10
Enter client's name:Tim Turtletop
Enter client's account number:233255
Enter opening balance:$688
Enter 1 for Brass Account or 2 for BrassPlus Account: 1

The information of Bank_Account:
Name: Harry Fishsong
account_number: 112233
balance: $1500.00

The information of Bank_Account:
Name: Dinah Otternoe
account_number: 121213
balance: $1800.00
Maximum loan: $350.00
Owed to Bank: $0.00
Loan Rate:12.000%

The information of Bank_Account:
Name: Brenda Birdherd
account_number: 212118
balance: $5200.00
Maximum loan: $800.00
Owed to Bank: $0.00
Loan Rate:10.000%

The information of Bank_Account:
Name: Tim Turtletop
account_number: 233255
balance: $688.00

Done.

多态性和虚函数特性由下面代码块提供:

for (int i = 0; i < CLIENTS; ++i) {
     p_clients[i]->ViewAcct();
     cout << endl;
}

❗️4、为何需要虚析构函数?

如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。举个例子:Brass的析构函数被调用,假如指针是指向BrassPlus对象,析构函数还是会指向Brass。

13.4. 静态联编和动态联编

函数名联编:将源代码中的函数调用解释为执行特定的函数代码块。

静态联编(早期联编):在编译过程中进行联编。

动态联编(晚期联编):编译器生成能够在程序运行时选择正确的虚方法的代码。

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

在C++中,不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型:

double x = 2.5;
int *pi = &x;			//invalid assignment,mismatched pointer types;
long &r1 = x;			//invalid assignment,mismatched reference type
  • 向上👆强制转换:将派生类引用或指针 ---------> 基类引用或指针。⚠️公有继承不需要进行显式类型转换。

    向上强制转换是可传递的。

  • 向下👇强制转换:将基类指针或引用 ---------> 派生类引用或指针。如果不使用显式类型转换,则向下强制转换不允许使用。

    • class Employee{
      private:
      		char name[40];
      		...
      public:
      		void show_name();
      		...
      };
      class Singer:public Employee{
        	...
      public:
        	void range();
        	....
      }
      ...
      Employee veep;
      Singer trals;
      ...
      Employee *pe = trala;
      Singer *ps = (Singer *) &veep;
      ...
      pe->show_name();
      //向下转换可能带来不安全的操作,因为Employer并不是Singer
      ps->range();
      
  • 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++虚成员函数来能满足这种需求。

13.4.2 虚成员函数和动态联编

编译器对非虚方法使用静态联编,而对于虚方法使用动态联编。

  • 有两种类型联编的原因?

    • 效率

      动态联编为使程序可在程序运行阶段进行决策,则必须采取一些方法来跟踪基类指针或引用指向的对象类型,但会增加额外的处理开销。此时静态联编效率更高。

      C++的指导原则之一:不要为不适用的特性付出代价。仅当程序设计确实需要虚函数时,才使用它们。

    • 概念模型

  • 虚函数的工作原理 C++规定了虚函数的行为,实现方法还是需要由编译器作者负责。

编译器处理虚函数的方法:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(Virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。

请添加图片描述

使用虚函数时,在内存和执行速度方面存在的成本:

  • 每个对象都将增大,增大量为存储地址的空间;
  • 对于每个类,编译器都创建一个虚函数地址表(数组);
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

13.4.3 虚函数的注意事项

  • 在基类方法的声明中使用关键字 virtual,可使该方法在基类以及所有派生类(包括派生类的方法)都是虚的。
  • 如果使用指向对象的引用或指针调用虚方法,程序将使用对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编晚期联编
  • 如果定义的类将被用作基类,则应将要在派生类中重新定义的类方法声明未为虚的。
13.4.3.1 构造函数

构造函数不能是虚函数。派生类不继承基类的构造函数,所以将构造函数声明为虚的无意义。

13.4.3.2 析构函数

析构函数应声明为虚函数,除非类不用做基类。

Employee *pe = new Singer;		//legal because Employee is base for Singer
....
delete pe;		//~Employee() or ~Singer()?

如果是默认的静态联编,delete语句将调用~Employee()析构函数。这会释放Singer中的Employee部分指向的内存,但不会释放新的类成员中指向的内存。但是如果析构函数是虚的,则上述是先调用Singer的析构函数来释放由Singer组合指向的内存,然后,调用~Employee()析构函数来释放由Employee组件指向的内存。

即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而 应提供虚构函数,即使它不执行任何操作

13.4.3.3 友元

友元不能是虚函数,因为设计的问题,友元不是类成员,而是通过让友元函数使用虚成员函数来解决。

13.4.3.4 没有重新定义

如果派生类没有重新定义函数,将使用函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本

13.4.3.5 重新定义将隐藏方法

重新定义不会生成函数的两个重载版本,而是隐藏了基类版本。

  • 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性称为 返回类型协变(Covariance of return type)

    • class Dwelling{
      public:
      		//a base method
      		virtual Dwelling &build(int n);
      };
      
      class Hoval:public Dwelling{
        	//a derived method with a covariant return type
        	virtual Hovel &build(int n);
        	...
      }
      

    允许返回类型随类型的变化而变化。⚠️注意:只适用于返回值,不适用于参数

  • 如果基类声明被重载,则应在派生类中重新定义所有的基类版本。

    • class Dwelling{
      public:
      		virtual void showperks(int a)const;
      		virtual void showperks(double x)const;
      		virtual void showperks()const;
      };
      
      class Hovel:public Dwelling{
      public:
        	virtual void showperks(int a)const;
        	virtual void showperks(double x)const;
        	virtual void showperks()const;
      }
      

13.5. 访问控制:protected

控制对类成员访问权限的三个关键字:private、public、protected

关键字 protectedprivate类似,在类外只能使用公有类成员来访问 protected 部分中的类成员

private和protected的区别:派生类成员直接访问基类的保护成员,但不能直接访问基类的私有成员。

对外部而言,保护成员的行为与私有成员类似,对派生类来说,保护成员的行为与公有成员类似。

// Brass类将balance成员声明为保护的
class Brass
{
    protected:
        double balance;
        ...
};

⚠️警告:最好对数据成员采用私有访问控制,不要使用保护访问控制,同时通过类方法使得派生类能够访问基类数据。

对成员函数来说,保护控制可以让派生类能够访问公众无法使用的内部数据。

13.6. 抽象基类

在类声明中包含纯虚函数时,则该类就是**抽象基类(Abstract Base Class,ABC)**。

C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0

class BaseEllipse //抽象基类
{
    private:
        ...
    public:
        ...
        virtual double Area() const = 0; //纯虚函数
};

如果在类声明中包含纯虚函数时,则不能创建该类的对象。

ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现接口。 [] ABC要求具体派生类覆盖其纯虚函数 -

13.6.1 应用ABC概念

下面程序,头文件声明了AcctABC类(ABC)、Brass类和BrassPlus类(两者都是具体类)。为帮助派生类访问基类数据,AcctABC提供了一些保护方法:派生类方法可以调用这些方法,但是它们并不是派生类的公有接口的组成部分。AcctABC还提供了两个纯虚函数,保证其事抽象类。

1、头文件

#ifndef TEST_ACCTABC_H
#define TEST_ACCTABC_H

#include <iostream>
#include <string>

class AcctABC {
private:
    std::string fullName;   //当前账号的主人
    long acctNum;           //银行账号
    double balance;         //当前的结余
protected:
    struct Formating {
        std::ios_base::fmtflags flag;
        std::streamsize pr;
    };

    const std::string &FullName() const { return fullName; }

    long AcctNum() const { return acctNum; }

    Formating SetFormat() const;

    void Restore(Formating &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;//PURE VIRTUAL FUNCTION
    double Balance() const { return balance; }

    virtual void ViewAcct() const = 0;

    virtual ~AcctABC() { }
};

//Brass Account Class
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(){}
};

//Brass Plus Account Class
class BrassPlus : public AcctABC{
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.10);
    BrassPlus(const Brass &ba,double ml = 500,double r = 0.1);
    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 //TEST_ACCTABC_H

2、源文件

#include <iostream>
#include "acctabc.h"

class Formating;

using std::cout;
using std::ios_base;
using std::endl;
using std::string;

//abc
AcctABC::AcctABC(const std::string &s, long an, double bal) {
    fullName = s;
    acctNum = an;
    balance = bal;
}

void AcctABC::Deposit(double amt) {
    if (amt < 0) {
        cout << "Negative deposit not allowed;"
             << "deposit is cancelled.\n";
    } else {
        balance += amt;
    }
}

void AcctABC::Withdraw(double amt) {
    balance -= amt;
}

//protected methods for formatting
AcctABC::Formating AcctABC::SetFormat() const {
    //set up ###.## format
    Formating f;
    f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
    f.pr = cout.precision(2);
    return f;
}

void AcctABC::Restore(Formating &f) const {
    cout.setf(f.flag, ios_base::floatfield);
    cout.precision(f.pr);
}

//Brass methods
void Brass::Withdraw(double amt) {
    if (amt < 0) {
        cout << "Withdrawal amount must be positive;"
             << "withdrawal canceled.\n";
    } else if (amt <= Balance()) {
        AcctABC::Withdraw(amt);
    } else {
        cout << "Withdrawal amount of $" << amt
             << " exceeds your balance.\n"
             << "Withdrawal canceled.\n";
    }
}

void Brass::ViewAcct() const {
    Formating f = SetFormat();

    cout << "The information of Bank_Account:\n";
    cout << "Name: " << FullName() << endl;
    cout << "account_number: " << AcctNum() << endl;
    cout << "balance: $" << Balance() << endl;
    Restore(f);
}

//BrassPlus Methods
BrassPlus::BrassPlus(const std::string &s, long an, double bal, double ml, double r) : AcctABC(s, an, bal) {
    maxloan = ml;
    owesBank = 0.0;
    rate = r;
}

BrassPlus::BrassPlus(const Brass &ba, double ml, double r) : AcctABC(ba) {
    maxloan = ml;
    owesBank = 0.0;
    rate = r;
}

void BrassPlus::ViewAcct() const {
    Formating f = SetFormat();

    cout << "BrassPLus Client:" << FullName() << endl;
    cout << "Account Number:" << AcctNum() << endl;
    cout << "Balance:$" << Balance() << endl;
    cout << "Maximum loan: $" << maxloan << endl;
    cout << "Owed to Bank: $" << owesBank << endl;
    cout.precision(3);      //###.### format
    cout << "Loan Rate:" << 100 * rate << "%\n";
    Restore(f);
}

void BrassPlus::Withdraw(double amt) {
    //set up ###.## format
    Formating f = SetFormat();

    double bal = Balance();
    if (amt <= bal) {
        AcctABC::Withdraw(amt);
    } else if (amt <= bal + maxloan - owesBank) {//假如实际存款不够提款的话,那么就还有款借
        double advance = amt - bal;//资金缺口
        owesBank += (1.0 + rate) * advance;
        cout << "Bank advance: $ " << advance << endl;//借款
        cout << "Finance charge: $" << advance * rate << endl;//借款手续费
        Deposit(advance);
        AcctABC::Withdraw(amt);
    } else {
        cout << "Credit limit exceeded.Transaction cancelled.\n";
    }
    Restore(f);
}

这个版本对输出格式和恢复格式进行了优化:定义了一个Formatting结构,用于存储两项格式设置;并使用该结构来设置和恢复格式。

3、执行文件

将上一个程序的执行文件中的

Brass *p_clients[CLIENTS];

改为:

AcctABC *p_clients[CLIENTS];

13.7. 继承和动态内存分配

如果派生类也使用动态内存分配(使用new和delete进行),有如下几个技巧:

13.7.1 情况1:派生类不使用new

假设基类使用动态内存分配:

// Base Class Using DMA(Dynamic Memory Allocation)
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); //赋值运算符重载
        ......
};

如果在基类中已经使用了动态内存分配(已定义显式析构函数、复制构造函数和赋值运算符),则在派生类中无需使用动态内存分配。

// derived class without DMA
class lacksDMA:public baseDMA
{
    private:
        char color[40];
    public:
        ......
};

此时的派生类lackDMA类不需要定义显式析构函数、复制构造函数和赋值运算符。

为什么不需要?

1、对于析构函数

实际上,派生类的默认构造函数总是要进行一些操作,执行自身代码后调用基类析构函数。因为我们假设lackDMA成员不需执行任何特殊操作,所以默认析构函数是合适的

2、对于拷贝构造函数

拷贝类成员或者继承的类组件时,则是使用该类的拷贝构造函数完成的。所以,lacksDMA类的默认拷贝构造函数使用显式baseDMA拷贝构造函数来复制lacksDMA对象的baseDMA部分

3、对于赋值运算符

如同2

派生类对象的这些属性也适用于本身是对象的类成员(如string,ps:string是标准类)。

13.7.2 情况2:派生类使用new

如果基类和派生类也使用了动态内存分配:

class hasDMA: public baseDMA
{
    private:
        char * style; // 构造函数中使用new
    public:
        ......
};

此时需要为派生类定义显式析构函数、复制构造函数和赋值运算符。对应的定义方法如下:

  • 析构函数

    派生类析构函数自动调用基类的析构函数,故其自身职责:对派生类构造函数执行工作的清理。

    baseDMA::~baseDMA()
    {
        delete [] label;
    }
    hasDMA::~hasDMA()
    {
        delete [] style;
    }
    
  • 复制构造函数

    复制构造函数只能访问自身的数据,不能访问基类的私有数据,所以必须调用基类的复制构造函数来进行数据共享。

    // 成员初始化列表将hasDMA引用传递给baseDMA构造函数。
    hasDMA::hasDMA(const hasDMA & hs):baseDMA(hs) //基类引用可以指向派生类型。
    {
        style = new char[std::strlen(hs.style) + 1];
        std::strcpy(style,hs.style);
    }
    

    所以,baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。

  • 赋值运算符

    派生类的显式赋值运算符必须负责将所有继承的baseDMA基类对象的赋值,可通过显式调用基类赋值运算符来进行该操作。

    hasDMA & hasDMA::operator=(const hasDMA & hs) //hasDMA使用DMA,所以需要一个显式复制运算符
    {
        if(this == &hs)
            return *this;
        baseDMA::operator=(hs); // 与 *this = hs 相同
        delete [] style;
        style = new char[std::strlen(hs.style) + 1];
        std::strcpy(style, hs.style);
        return *this;
    }
    

13.7.3 总结

当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的类方法来处理基类元素。三种方式来满足:

  • 析构函数:自动完成
  • 构造函数:通过在初始化成员列表中调用基类的复制构造函数来完成。否则会自动调用基类的默认构造函数。
  • 赋值运算符:通过 作用域解析运算符 来显式调用基类的赋值运算符来完成。

13.7.4 DMA和友元的继承

由于友元函数不能被继承,但是派生类应该如何使用基类的友元函数?

// 基类baseDMA的友元函数
friend std::ostream * operator<<(std::ostream & os, const baseDMA & rs)
// 派生类hasDMA的友元函数
friend std::ostream & operator<<(std::ostream &os,const hasDMA & rs);

hasBMA可访问style成员,但不能访问基类baseDMA友元中的label和rating,所以通过使用基类baseDMA类的友元函数 operator<< 来对其进行强制类型转换,便于原型匹配选择正确的函数。

std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
	os << (const baseDMA &) hs; //直接通过强制类型转换的方式来进行
	os << “Style: ” << hs.style << endl;
	return os;
}

关于继承和内存动态分配的代码例子

1、头文件

#ifndef TEST_DMA_H
#define TEST_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);
};

class lacksDMA: public baseDMA{
private:
    enum {COL_LEN = 40};
    char color[COL_LEN];
public:
    lacksDMA(const char*c = "black",const char *l = "null",int r = 0);
    lacksDMA(const char*c,const baseDMA &rs);
    friend std::ostream &operator<<(std::ostream &os,const lacksDMA &rs);
};

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 //TEST_DMA_H

2、源文件

//
// Created by wzybb on 2022/9/23.
//

#include "dma.h"
#include <cstring>

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(l) + 1];
    std::strcpy(style,s);
}

hasDMA::hasDMA(const hasDMA &hs):baseDMA(hs) {
    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;
    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;
}

作为hasDMA类的友元,该函数能够访问style成员。然而,还存在一个问题:该函数如不是baseDMA类的友元,那如何访问成员label和rating呢?答案是使用baseDMA类的友元函数operator<<().

下一个问题:因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决办法是使用强制转换类型转换,以便匹配原型时候能够选择正确的函数,因此代码将const hasDMA &转换为类型const baseDMA &参数,来访问hasDMA类中baseDMA部分。

3、执行文件

#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(map);//深拷贝,需要显式定义拷贝构造函数
    hasDMA map3;
    map3 = map;
    cout << "result of hasDMA copy:\n";
    cout << map << endl;
    cout << "result of hasDMA assignment:\n";
    cout << map2 << endl;
    return 0;
}

输出:
Displaying baseDMA object.
label:Portabelly
rating :8

Displaying lacksDMA object.
label:Blimpo
rating :4
color :red

Displaying hasDMA object.
label:Buffalo Keys
rating :5
Style : Mercator

result of lacksDMA copy:
label:Blimpo
rating :4
color :red

result of hasDMA assignment:
label:Buffalo Keys
rating :5
Style : Mercator

13.8. 类设计回顾

C++可用于解决各种类型的编程问题,但不能将类设计简化成带编号的例程。

13.8.1 编译器生成的函数

编译器会自动生成一些公有成员函数 -----> 特殊成员函数。

  • 默认构造函数

    • 默认构造函数要么没有参数,要么所有的参数都有默认值。

    • 如果没有定义任何构造函数,编译器将定义默认构造函数,便于创建对象。

    • 如果在派生类构造函数中成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。

      如果基类此时没有构造函数,将导致编译错误。

    • 构造函数的作用:确保对象总能被正确的初始化。

  • 复制构造函数

    复制构造函数接受其所属类的对象作为参数。使用复制构造函数的情况:

    • 将新对象初始化为一个同类对象。
    • 按值将对象传递给函数。
    • 函数将值返回对象。
    • 编译器生成临时对象。

    如果程序没有使用,则编译器提供原型,但不提供函数定义。

  • 赋值运算符

    • 默认的赋值运算符用于处理同类对象之间的赋值。

      初始化:语句创建新的对象;赋值:语句修改已有对象的值。

    • 编译器不会生成将一种类型赋给另一种类型的赋值运算符,所以要么显式定义,要么使用强制类型转换。前者容易导致编译器出现混乱,所以一般使用后者。

  • 默认析构函数

13.8.2 定义类(设计类)需要注意的问题

  • 构造函数

    用于创建新的对象,其他类方法只是被现有的对象调用,所以不能被继承。

  • 析构函数

    如果使用new进行动态内存分配,则一定要定义显式析构函数来完成类对象执行后的清理。基类应提供一个虚析构函数(即使不执行任何操作)。

  • 转换 C++11支持用explicit。使用explicit允许进行显式转换,禁止隐式转换。

  • 按值传递对象与传递引用

    3.编写使用对象作为参数的函数时,应按引用传递对象(不使用按值传递)。原因如下:

    • 提高效率
    • 在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。

    如果不修改引用,则将参数声明未const引用。

  • 返回对象和返回引用

    • 唯一区别:函数原型和函数头。
    • 应返回引用而不是返回对象的原因:返回引用可节省时间和内存。
    • ⚠️注意:函数不能返回临时对象的引用 -----> 函数结束,临时对象消失,所以引用不合法。
  • 使用const

    • 修饰参数 -----> 确保方法不修改参数。
    • 修饰方法 -----> 确保方法不修改调用它的对象。
    • 修饰返回引用的函数 -----> 确保引用或指针返回的值不能用于修改对象中的数据。

13.8.3 公有继承的考虑因素

  • is-a关系

    遵循 is-a 关系,如果派生类不是特殊的基类,则不要使用公有派生。

  • 什么不能被继承?

    • 构造函数
    • 析构函数
    • 赋值运算符
  • 私有成员与保护成员

    • 将基类成员设置私有可提高安全性,设置保护可简化代码编写,提高访问速度。
    • 但一般将基类的数据成员设为私有成员,而将方法设置为保护成员。
  • 虚方法

    在设计基类时,必须确定是否将类方法声明为虚的。

    • 如果派生类需要重新定义方法,则在基类中将方法设置为虚的。
    • 如果不需要,则不必将其声明为虚的。
  • 析构函数

    • 基类的析构函数应是虚的(virtual)。
  • 友友函数

    友元函数不是类成员,所以不能被继承。

    • 如果希望派生类的友元能使用基类的友元,可通过强制类型转换符。

      将派生类引用或指针转换为基类引用或指针,然后使用转换后的来调用基类的友元。

13.8.4 类函数的总结

请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值