类继承
本章内容:
is-a 关系的继承;
如何以公有方式从一个类派生出另一个类;
保护访问;
构造函数成员初始化列表;
向上和向下强制转换;
虚成员函数;
早期(静态)联编与晚期(动态)联编;
抽象基类;
纯虚函数;
何时及如何使用公有继承
面向对象编程的主要目的之一是提供可重用的代码;
C++类提供了更高层次的重用性,类库由类声明和实现构成,因为类组合了数据表示和类方法;
C++提供了比修改代码更好的方法来扩展和修改类——类继承;
13.1 一个简单的基类
从一个类派生出另一个类,原始类成为基类,继承类称为派生类;
程序清单13.1 tabtenn0.h
1 #ifndef TABTENN0_H_ 2 #define TABTENN0_H_ 3 4 class TableTennisPlayer 5 { 6 private: 7 enum {LIM = 20}; 8 char firstname[LIM]; 9 char lastname[LIM]; 10 bool hasTable; 11 public: 12 TableTennisPlayer(const char * fn = "none", const char * ln = "none", bool ht = false); 13 void Name() const; 14 bool HasTable() const { return hasTable; }; 15 void ResetTable(bool v) { hasTable = v; }; 16 }; 17 18 #endif
程序清单 13.2 tabtenn0.cpp
#pragma warning(disable:4996) #include<iostream> #include"tabtenn0.h" #include<cstring> TableTennisPlayer::TableTennisPlayer(const char* fn, const char * ln, bool ht) { std::strncpy(firstname, fn, LIM - 1); firstname[LIM - 1] = '\0'; std::strncpy(lastname, ln, LIM - 1); lastname[LIM - 1] = '\0'; hasTable = ht; } void TableTennisPlayer::Name() const { std::cout << lastname << ", " << firstname; }
程序清单 13.3 usett0.cpp
1 #include<iostream> 2 #include"tabtenn0.h" 3 4 int main() 5 { 6 using std::cout; 7 TableTennisPlayer player1("Chuck", "Blizzard", true); 8 TableTennisPlayer player2("Tara", "Boomdea", false); 9 player1.Name(); 10 if (player1.HasTable()) 11 cout << ": has a table.\n"; 12 else 13 cout << ": hasn`t a table.\n"; 14 15 player2.Name(); 16 if (player2.HasTable()) 17 cout << ": has a table.\n"; 18 else 19 cout << ": hasn`t a table.\n"; 20 21 return 0; 22 }
13.1.1 派生一个类
将RatedPlayer 类声明为从TabletennisClass 类派生出来而来:
class RatedPlayer: public TableTennisPlayer
{
. . .
}
冒号指出 RatedPlayer 类的基类是 TableTennisplayer 类;
声明头表明 TableTennisPlayer 是一个公有基类,这被称为共有派生;
Ratedpalyer 对象具有特征:
派生类对象存储了基类的数据成员(派生类继承了基类的实现);
派生类对象可以使用基类的方法(派生类继承了基类的接口)
继承特性中添加:
派生类需要自己的构造函数;
派生类可以根据需要添加额外的数据成员和成员函数
RatedPalyer类声明:
class Ratedpalyer: public TableTennisPlayer
{
private:
usigned int rating; // add a data member
public:
RatedPlayer ( unsigned int r = 0 ,const char * fn = "none",const char * ln = "none",bool ht = false );
RatedPlayer ( usigned int r,const TableTennisPlayer & tp );
usigned int Rating () { return rating;}; // add a method
void ResetRating ( usigned int r ) { rating = r;}; // add a method
}
构造函数必须给新成员和继承的成员提供数据
13.1.2 构造函数: 访问权限
派生类不能直接访问基类的私有成员,必须通过基类方法进行访问;
派生类构造函数必须使用基类构造函数;
创建派生类对象时,程序首先创建基类对象,基类对象应当在程序进入派生类构造函数之前被创建:
RatedPlayer::RatedPalyer ( usigned int r, const char * fn, const char * ln, bool ht ): TableTennisPlayer (fn, ln,ht)
{
rating = r;
}
首先参数传递给 TableTennisPlayer 构造函数,创建一个嵌套 TabletennisPalyer 对象,并将数据存到该对象中;
然后,程序进入 RealPlayer 构造函数,完成 RealPlayer 对象的创建;
如果省略成员初始化列表,程序将使用默认的基类基类构造函数;
除非使用默认构造函数,否则应显示调用正确的基类构造函数;
派生类构造函数的要点:
1. 基类对象首先被创建;
2. 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
3. 派生类构造函数应初始化派生类新增的数据成员
记住: 基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员;
派生类的构造函数总是调用一个基类构造函数; 使用初始化列表句法指明要使用的基类构造函数;
派生类对象过期时,程序首先调用派生类析构函数,再调用基类析构函数
13.1.3 使用派生类
程序清单13.4 tabletenn1.h
1 #ifndef TABTENN0_H_ 2 #define TABTENN0_H_ 3 4 class TableTennisPlayer 5 { 6 private: 7 enum { LIM = 20 }; 8 char firstname[LIM]; 9 char lastname[LIM]; 10 bool hasTable; 11 public: 12 TableTennisPlayer(const char * fn = "none", const char * ln = "none", bool ht = false); 13 void Name() const; 14 bool HasTable() const { return hasTable; }; 15 void ResetTable(bool v) { hasTable = v; }; 16 }; 17 18 // simple derived class 19 class RatedPlayer : public TableTennisPlayer 20 { 21 private: 22 signed int rating; 23 public: 24 RatedPlayer (unsigned int r = 0, const char * fn = "none", const char * ln = "none", bool ht = false); 25 RatedPlayer(unsigned int r, const TableTennisPlayer & tp); 26 unsigned int Rating() { return rating; } 27 void ResetRating(unsigned int r) { rating = r; } 28 }; 29 30 #endif
程序清单13.5 tabtenn1.cpp
1 #pragma warning(disable:4996) 2 #include<iostream> 3 #include"tabtenn1.h" 4 #include<cstring> 5 6 TableTennisPlayer::TableTennisPlayer(const char* fn, const char * ln, bool ht) 7 { 8 std::strncpy(firstname, fn, LIM - 1); 9 firstname[LIM - 1] = '\0'; 10 std::strncpy(lastname, ln, LIM - 1); 11 lastname[LIM - 1] = '\0'; 12 hasTable = ht; 13 } 14 15 void TableTennisPlayer::Name() const 16 { 17 std::cout << lastname << ", " << firstname; 18 } 19 20 // RatedPlayer methods 21 RatedPlayer::RatedPlayer(unsigned int r, const char * fn, const char * ln, bool ht) : TableTennisPlayer(fn, ln, ht) 22 { 23 rating = r; 24 } 25 26 RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp), rating(r) 27 {}
程序清单13.6 usett1.cpp
1 #include<iostream> 2 #include"tabtenn1.h" 3 4 int main() 5 { 6 using std::cout; 7 using std::endl; 8 TableTennisPlayer player1("Tara", "boomdea", false); 9 RatedPlayer rplayer1(1140, "Mallory", "Duck", true); 10 rplayer1.Name(); 11 if (rplayer1.HasTable()) 12 cout << ": has a table.\n"; 13 else 14 cout << ": hasn`t a table.\n"; 15 player1.Name(); 16 if (player1.HasTable()) 17 cout << ": has a table.\n"; 18 else 19 cout << ": hasn`t a table.\n"; 20 21 cout << "Name: "; 22 rplayer1.Name(); 23 cout << "; Rating: " << rplayer1.Rating() << endl; 24 25 RatedPlayer rplayer2(1212, player1); 26 cout << "Name: "; 27 rplayer2.Name(); 28 cout << "; Rating: " << rplayer2.Rating() << endl; 29 30 return 0; 31 }
13.2 派生类和基类之间的特殊联系
派生类与基类之间有一些特殊关系:
派生类对象可以使用基类方法,条件是方法不是私有的;
基类指针可以在不进行显示类型转换的情况下指向派生类对象;
基类引用可以在不进行显示类型转换的情况下引用派生类对象
RatedPalyer rpalyer ( 1140, "Mallory", "Duck", true ); // 派生类
TableTennisPlayer & rt = rpalyer; // 基类引用引用派生类
TableTennisPlayer * pt = &rplayer; // 基类指针指向派生类
rt.Name();
pt->Name();
基类指针和引用只能用于调用基类方法,不能使用 rt 或 pt 调用派生类的 ResRanking 方法
不能将基类对象和地址赋给派生引用和指针;
基类引用定义的函数或指针参数可用于基类对象或派生类对象;
形参为指向基类的指针的函数,可以使用基类对象的地址或派生类对象的地址作为实参;
引用兼容性可以将基类对象初始化派生类对象,也可以将派生类对象赋给基类对象;
13.3 继承—— is-a 关系
派生类和基类之间的特殊关系是基于 C++ 继承的底层模型的;
C++ 有3种继承方式: 共有继承、保护继承和私有继承;
共有继承是最常用的方式,它建立一种 is-a 的关系,即派生类对象也是一个基类对象:
可以对基类对象执行的任何操作,也可以对派生类对象执行
13.4 多态共有继承
方法的行为取决于调用该方法的对象——具有多种形态(多态);
两种重要的机制可用于实现多态公有继 承:
1. 在派生类中重新定义基类的方法;
2. 使用虚方法;
程序清单 13.7 brass.h
1 #pragma once 2 #ifndef BRASS_H_ 3 #define BRASS_H_ 4 5 // Brass Account Class 6 class Brass 7 { 8 private: 9 enum {MAX = 35}; 10 char fullName[MAX]; 11 long acctNum; 12 double balance; 13 public: 14 Brass(const char * s = "Nullbody", long an = -1, double bal = 0.0); 15 void Deposit(double amt); 16 virtual void Withdraw(double amt); 17 double Balance() const; 18 virtual void ViewAcct() const; 19 virtual ~Brass() {} 20 }; 21 22 // Brass Plus Account Class 23 class BrassPlus : public Brass 24 { 25 private: 26 double maxloan; 27 double rate; 28 double owesBank; 29 public: 30 BrassPlus(const char * s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.10); 31 BrassPlus(const Brass & ba, double ml = 500, double r = 0.1); 32 virtual void ViewAcct() const; 33 virtual void Withdraw(double amt); 34 void ResetMax(double m) { maxloan = m; } 35 void ResetRate(double r) { rate = r; } 36 void ResetOwes() { owesBank = 0; } 37 }; 38 39 #endif
程序清单 13.8 brass.cpp
1 #pragma warning(disable:4996) 2 #include<iostream> 3 #include<cstring> 4 #include"brass.h" 5 6 using std::cout; 7 using std::ios_base; 8 using std::endl; 9 10 // Brass methods 11 Brass::Brass(const char * s, long an, double bal) 12 { 13 std::strncpy(fullName, s, MAX - 1); 14 fullName[MAX - 1] = '\0'; 15 acctNum = an; 16 balance = bal; 17 } 18 19 void Brass::Deposit(double amt) 20 { 21 if (amt < 0) 22 cout << "Negative deposit not allowed; " << "deposit is cancelled.\n"; 23 else 24 balance += amt; 25 } 26 27 void Brass::Withdraw(double amt) 28 { 29 if (amt < 0) 30 cout << "Negative withdraw not allowed; " << "Withdraw is cancelled.\n"; 31 else if (amt <= balance) 32 balance -= amt; 33 else 34 cout << "Withdrawal amount of $" << amt << " exceseeds your balance.\n" << "Withdraw is canced.\n"; 35 } 36 37 double Brass::Balance() const 38 { 39 return balance; 40 } 41 void Brass::ViewAcct() const 42 { 43 ios_base::fmtflags initialState = cout.setf(ios_base::fixed, ios_base::floatfield); 44 cout.setf(ios_base::showpoint); 45 cout.precision(2); 46 cout << "Client: " << fullName << endl; 47 cout << "Account Number: " << acctNum << endl; 48 cout << "Balance: $" << balance << endl; 49 cout.setf(initialState); 50 } 51 52 // BrassPlus Methods 53 BrassPlus::BrassPlus(const char * s, long an, double bal, double ml, double r) : Brass(s, an, bal) 54 { 55 maxloan = ml; 56 owesBank = 0.0; 57 rate = r; 58 } 59 60 BrassPlus::BrassPlus(const Brass & ba, double ml, double r) : Brass(ba) 61 { 62 maxloan = ml; 63 owesBank = 0.0; 64 rate = r; 65 } 66 67 // redefine how ViewAcct() works 68 void BrassPlus::ViewAcct() const 69 { 70 ios_base::fmtflags initialState = cout.setf(ios_base::fixed, ios_base::floatfield); 71 cout.setf(ios_base::showpoint); 72 cout.precision(2); 73 74 Brass::ViewAcct(); // dispaly base portion 75 cout << "Maxinum loan: $" << maxloan << endl; 76 cout << "Owed to ban: $" << owesBank << endl; 77 cout << "Loan Rate: $" << 100 * rate <<"%"<< endl; 78 cout.setf(initialState); 79 } 80 81 // redefine how Withdraw() works 82 void BrassPlus::Withdraw(double amt) 83 { 84 ios_base::fmtflags initialState = cout.setf(ios_base::fixed, ios_base::floatfield); 85 cout.setf(ios_base::showpoint); 86 cout.precision(2); 87 88 double bal = Balance(); 89 if (amt <= bal) 90 Brass::Withdraw(amt); 91 else if (amt <= bal + maxloan - owesBank) 92 { 93 double advance = amt - bal; 94 owesBank += advance * (1.0 + rate); 95 cout << "Bank advance: $" << advance << endl; 96 cout << "Finance charge: $" << advance * rate << endl; 97 Deposit(advance); 98 Brass::Withdraw(amt); 99 } 100 else 101 cout << "Credit limit exceeded. Transaction cancelled.\n"; 102 cout.setf(initialState); 103 }
程序清单 13.9 usebrass1.cpp
1 #include<iostream> 2 #include"brass.h" 3 4 int main() 5 { 6 using std::cout; 7 using std::endl; 8 Brass Piggy("Porcelot Pigg", 381299, 4000.00); 9 BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00); 10 Piggy.ViewAcct(); 11 cout << endl; 12 Hoggy.ViewAcct(); 13 cout << endl; 14 15 cout << "Depositing $1000 into the Hogg Account: \n"; 16 Hoggy.Deposit(1000.00); 17 cout << "New balance: $" << Hoggy.Balance() << endl; 18 cout << "Withdrawing $4200 from Pigg Account: \n"; 19 Piggy.Withdraw(4200.00); 20 cout << "Pigg account balance: $" << Piggy.Balance() << endl; 21 cout << "Withdrawing $4200 from Hoggy Account: \n"; 22 Hoggy.Withdraw(4200.00); 23 Hoggy.ViewAcct(); 24 25 return 0; 26 }
派生类不能直接访问基类的私有数据,必须使用基类的公有方法才能访问:
1. 构造函数在初始化基类私有数据时,采用成员初始化列表句法:
将基类信息传递给基类构造函数,然后使用构造函数体初始化 BrassPlus 类新增的数据项
2.非构造函数不能使用成员初始化列表句法,但派生类方法可以调用公有的基类方法;
在派生类方法中使用作用域解析操作符来调用基类方法
如果要在派生类中重新定义基类方法,通常应将基类方法声明为虚拟的
虚方法的使用:
1 #include<iostream> 2 #include"brass.h" 3 4 const int CLIENTS = 4; 5 const int LEN = 40; 6 7 int main() 8 { 9 using std::cin; 10 using std::cout; 11 using std::endl; 12 Brass * p_clients[CLIENTS]; // 定义指向基类Brass的指针数组(基类指针可以指向派生类BrassPlus对象) 13 14 int i; 15 for (i = 0; i < CLIENTS; i++) 16 { 17 char temp[LEN]; 18 long tempnum; 19 double tempbal; 20 char kind; 21 cout << "Enter client`s name: "; 22 cin.getline(temp, LEN); 23 cout << "Enter client`s account number: "; 24 cin >> tempnum; 25 cout << "Enter opening balance: $"; 26 cin >> tempbal; 27 28 cout << "Enter 1 for Brass Account or 2 for BrassPlus Account: "; 29 while (cin >> kind && (kind != '1'&& kind != '2')) 30 cout << "Enter either 1 or 2: "; 31 if (kind == '1') 32 p_clients[i] = new Brass(temp, tempnum, tempbal); 33 else 34 { 35 double tmax, trate; 36 cout << "Enter the overdraft limit: $"; 37 cin >> tmax; 38 39 cout << "Enter the interest rate as a decimal fraction: "; 40 cin >> trate; 41 p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate); 42 } 43 while (cin.get() != '\n') 44 continue; 45 } 46 cout << endl; 47 for (i = 0; i < CLIENTS; i++) 48 { 49 p_clients[i]->ViewAcct(); // 多态 50 cout << endl; 51 } 52 for (i = 0; i < CLIENTS; i++) 53 { 54 delete p_clients[i]; 55 } 56 cout << "Done.\n"; 57 58 return 0; 59 }
程序创建指向 Brass 的指针数组,使用的是公有继承模型,Brass指针既可以指向Brass对象也可以指向BrassPlus对象
多态特性:
for ( i = 0;i < CLIENTS;i++ )
{
p_clients[i] -> ViewAcct();
cout << endl;
}
如果数组成员指向 Brass 对象,则调用 Brass::ViewAcct();
如果数组成员指向 BrassPlus 对象,则调用 BrassPlus::ViewAcct()
为何需要使用虚拟析构函数:
如果析构函数不是虚拟的,则将只调用对应于指针类型的析构函数;
使用虚拟构造函数可以保证正确的析构函数被调用
13.4.2 静态联编和动态联编
将源代码中的函数调用解释为执行特定函数代码块被称为函数名联编;
编译器在编译过程中进行的联编称为静态联编;
编译器在程序运行时选择正确的虚方法代码,称为动态联编
13.4.3 指针和引用类型的兼容性
将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显示类型转换;
基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,C++使用虚成员函数来满足这种要求
13.4.4 虚拟成员函数和动态联编
编译器对非虚方法使用静态联编;
编译器对虚方法使用动态联编:
在基类中将 ViewAcct() 声明为虚拟的;
编译器生成的代码在程序执行时,根据对象类型将 ViewAcct() 关联到 Brass::ViewAcct() 或 BrassPlus::ViewAcct()
静态联编效率更高,因此被 C++ 设置为默认的选择;
提示: 如果要在派生类中定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法
虚函数工作原理:
13.4.5 有关虚函数注意事项
1. 在基类方法的声明中使用关键字 virtual 可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚拟的;
2. 使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法;
3. 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚拟的;
构造函数不能是虚函数,派生类不继承基类的构造函数;
析构函数应当是虚函数,除非类不用做基类;
提示: 通常应给基类提供一个虚拟析构函数,即使它并不需要析构函数
友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数;
如果派生类没有重新定义函数,将使用该函数的基类版本;
如果重新定义继承的方法,应确保与原来的原型完全相同,如果不相同,会导致隐藏同名基类方法;
如果基类声明被重载,则应该在派生类中重新定义所有的基类版本;
13.5 访问控制: protected
关键字 protected 与 private 相似,在类外只能用公有类成员来访问 protected 部分的类成员;
派生类可以直接访问基类保护成员,但不能直接访问基类的私有成员;
警告: 最好对数据成员采用私有云访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够基类数据
13.6 抽象基类
Circle (圆) 类与 Ellipse (椭圆)类有很多共同点:
从 Circle 类与 Ellipse 类中抽象出它们的共性,将这些特性放在一个ABC中;
从 ABC 中派生出 Circle 和 Ellipse 类,这样便可以使用基类指针数组同时管理 Circle 和 Ellipse 对象;
C++ 通过使用纯虚数提供未实现的函数,纯虚函数声明的结尾处为 = 0:
当类声明中包含纯虚函数时,则不能创建该类的对象;
包含纯虚函数的类只能用作基类;
在原型中使用 = 0 指出类是一个抽象基类,在类中可以不定义该函数;
ABC 描述的是至少使用一个纯虚函数的接口,从 ABC 派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口;
13.6.1 应用 ABC 概念
程序清单13.11 acctabc.h
1 #ifndef ACCTABC_H_ 2 #define ACCTABC_H_ 3 4 // Abstract Base Class 5 class AcctABC 6 { 7 private: 8 enum { MAX = 35 }; 9 char fullName[MAX]; 10 long acctNum; 11 double balance; 12 protected: 13 const char * FullName() const { return fullName; } 14 long AcctNum() const { return acctNum; } 15 std::ios_base::fmtflags SetFormat() const; 16 public: 17 AcctABC(const char * s = "Nullbody", long an = -1, double bal = 0.0); 18 void Deposit(double amt); 19 virtual void Withdraw(double amt) = 0; // pure virtual function 20 double Balance() const { return balance; } 21 virtual void ViewAcct() const = 0; // pure virtual function 22 virtual ~AcctABC() {} 23 }; 24 25 // Brass Account Class 26 class Brass: public AcctABC 27 { 28 public: 29 Brass(const char * s = "Nullbody", long an = -1, double bal = 0.0) : AcctABC(s, an, bal) 30 {} 31 virtual void Withdraw(double amt); 32 virtual void ViewAcct() const; 33 virtual ~Brass() {} 34 }; 35 36 // BrassPlus Account Class 37 class BrassPlus: public AcctABC 38 { 39 private: 40 double maxloan; 41 double rate; 42 double owesBank; 43 public: 44 BrassPlus(const char * s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.10); 45 BrassPlus(const Brass & ba, double ml = 500, double r = 0.1); 46 virtual void ViewAcct() const; 47 virtual void Withdraw(double amt); 48 void ResetMax(double m) { maxloan = m; } 49 void ResetRate(double r) { rate = r; } 50 void ResetOwes() { owesBank = 0; } 51 }; 52 53 #endif
程序清单13.12 acctABC.cpp
1 #pragma warning(disable:4996) 2 #include<iostream> 3 #include<cstring> 4 using std::cout; 5 using std::ios_base; 6 using std::endl; 7 8 #include"acctabc.h" 9 10 // Abstract Base Class 11 AcctABC::AcctABC(const char * s, long an, double bal) 12 { 13 std::strncpy(fullName, s, MAX - 1); 14 fullName[MAX - 1] = '\0'; 15 acctNum = an; 16 balance = bal; 17 } 18 19 void AcctABC::Deposit(double amt) 20 { 21 if (amt < 0) 22 cout << "Negative deposit not allowed; deposit is cancelled.\n"; 23 else 24 balance += amt; 25 } 26 27 void AcctABC::Withdraw(double amt) 28 { 29 balance -= amt; 30 } 31 32 // protected method 33 ios_base::fmtflags AcctABC::SetFormat() const 34 { 35 ios_base::fmtflags initialState = cout.setf(ios_base::fixed, ios_base::floatfield); 36 cout.setf(ios_base::showpoint); 37 cout.precision(2); 38 return initialState; 39 } 40 41 // Brass methods 42 void Brass::Withdraw(double amt) 43 { 44 if (amt < 0) 45 cout << "Withdrawal amount must be positive; withdrawal canceled.\n"; 46 else if (amt <= Balance()) 47 AcctABC::Withdraw(amt); 48 else 49 cout << "Withdrawal amount of $" << amt << " exceeds your balance.\n Withdrawal canceled.\n"; 50 } 51 52 void Brass::ViewAcct() const 53 { 54 ios_base::fmtflags initialState = SetFormat(); 55 cout << "Brass Client: " << FullName() << endl; 56 cout << "Account Number: " << AcctNum() << endl; 57 cout << "Balance: $" << Balance() << endl; 58 cout.setf(initialState); 59 } 60 61 // BrassPlus Methods 62 BrassPlus::BrassPlus(const char * s, long an, double bal, double ml, double r) :AcctABC(s, an, bal) 63 { 64 maxloan = ml; 65 owesBank = 0.0; 66 rate = r; 67 } 68 69 BrassPlus::BrassPlus(const Brass & ba, double ml, double r) : AcctABC(ba) 70 { 71 maxloan = ml; 72 owesBank = 0.0; 73 rate = r; 74 } 75 76 void BrassPlus::ViewAcct() const 77 { 78 ios_base::fmtflags initialState = SetFormat(); 79 cout << "Brass Client: " << FullName() << endl; 80 cout << "Account Number: " << AcctNum() << endl; 81 cout << "Balance: $" << Balance() << endl; 82 cout << "Maxinum loan: $" << maxloan << endl; 83 cout << "Owed to bank: $" << owesBank << endl; 84 cout << "Loan Rate: " << 100 * rate << "%\n"; 85 cout.setf(initialState); 86 } 87 88 void BrassPlus::Withdraw(double amt) 89 { 90 ios_base::fmtflags initialState = SetFormat(); 91 double bal = Balance(); 92 if (amt < bal) 93 AcctABC::Withdraw(amt); 94 else if (amt <= bal + maxloan - owesBank) 95 { 96 double advance = amt - bal; 97 owesBank += advance*(1.0 + rate); 98 cout << "Bank advance: $" << advance* rate << endl; 99 Deposit(advance); 100 AcctABC::Withdraw(amt); 101 } 102 else cout << "Credit limit exceeded. Transaction cancelled.\n"; 103 cout.setf(initialState); 104 }
程序清单13.13 usebrass3.cpp
1 #include<iostream> 2 #include"acctabc.h" 3 4 const int CLIENTS = 4; 5 const int LEN = 40; 6 7 int main(int argc, char ** argv) 8 { 9 using std::cin; 10 using std::cout; 11 using std::endl; 12 AcctABC * p_clients[CLIENTS]; // 定义指向基类Brass的指针数组(基类指针可以指向派生类BrassPlus对象) 13 14 int i; 15 for (i = 0; i < CLIENTS; i++) 16 { 17 char temp[LEN]; 18 long tempnum; 19 double tempbal; 20 char kind; 21 cout << "Enter client`s name: "; 22 cin.getline(temp, LEN); 23 cout << "Enter client`s account number: "; 24 cin >> tempnum; 25 cout << "Enter opening balance: $"; 26 cin >> tempbal; 27 28 cout << "Enter 1 for Brass Account or 2 for BrassPlus Account: "; 29 while (cin >> kind && (kind != '1'&& kind != '2')) 30 cout << "Enter either 1 or 2: "; 31 if (kind == '1') 32 p_clients[i] = new Brass(temp, tempnum, tempbal); 33 else 34 { 35 double tmax, trate; 36 cout << "Enter the overdraft limit: $"; 37 cin >> tmax; 38 39 cout << "Enter the interest rate as a decimal fraction: "; 40 cin >> trate; 41 p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate); 42 } 43 while (cin.get() != '\n') 44 continue; 45 } 46 cout << endl; 47 for (i = 0; i < CLIENTS; i++) 48 { 49 p_clients[i]->ViewAcct(); // 多态 50 cout << endl; 51 } 52 for (i = 0; i < CLIENTS; i++) 53 { 54 delete p_clients[i]; 55 } 56 cout << "Done.\n"; 57 58 return 0; 59 }
16.3.2 ABC理念
设计 ABC 之前,首先应开发一个模型——指出编程问题所需的类以及它们之间的相互关系;
如果要设计类的继承层次,则只能将那些不会被用作基类的类设计为具体的类;
使用 ABC 实施接口规则:
ABC 要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 所设置的接口规则;
使用ABC使得组件设计人员能够制定 “接口约定”,确保了从ABC派生出来的所有组件都至少支持ABC指定功能
13.7 继承和动态内存分配
13.8 类设计回顾
13.8.1 编译器生成的成员函数
1. 默认构造函数
默认构造函数要么没有参数,要么所有参数都有默认值;
如果没有定义任何构造函数,编译器将定义默认构造函数:
自动生成的默认构造函数能用来创建对象;
调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数
如果派生类构造函数的成员初始化列表中没有显示的调用基类构造函数,编译器将使用基类的默认构造函数来构造派生类对象的基类部分;
如果定义了某种构造函数,编译器将不会定义默认构造函数,如果需要默认构造函数,则必须自己提供;
如果类包含指针成员,则必须初始化这些成员,最好提供一个显示默认构造函数,将所有的类数据成员都初始化合理的值
2. 复制构造函数
复制构造函数接收其所属类的对象作为参数:
Star ( const Star & );
使用复制构造函数的情况:
将新的对象初始化为一个同类的对象;
按值将对象传递给函数;
函数返回值返回对象;
编译器生成临时文件
需要定义自己的复制构造函数:
使用 new 初始化的成员指针通常要求执行深复制;
类包含需要修改的静态变量
3. 赋值操作符
默认的赋值操作符用于处理同类对象之间的赋值:
如果语句创建新的对象,则使用初始化; Star alpha = sirius
如果语句修改已有的对象的值,则是赋值; Star dogstar; dogstar = sirius;
如果需要显示定义复制构造函数,则基于相同的原因,也需要显示定义赋值操作符:
Star & Star::operator = ( const Star & );
返回一个Star对象的引用
编译器不会生成将一种类型赋给另一种类型的赋值操作符,如果希望字符串赋给Star对象:
1. 显示定义定义赋值运算符
Star & Star :: operator = ( const char * ) { . . . } // 效率更高
2. 使用转换函数
13.8.2 其他的类方法
1. 构造函数
构造函数不同于其他类方法,因为它创建新的对象,而其他类方法被现有的对象调用:
构造函数不被继承的原因;
继承意味着派生类对象可以使用基类方法,然而,构造函数在完成工作之前,对象并不存在
2. 析构函数
一定要定义显示析构函数来释放类构造函数使用 new 分配的所有内存,并完成类对象所需的任何特殊清理工作;
对于基类,即使不需要构造函数,也应提供一个虚拟析构函数
3. 转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换:
Star ( const char * ); // converts char * to Star
Star ( const Spectral &, int members = 1 ); // converts Spectral to Star
将可转换的类型传递给以类为参数的函数时,将调用转换构造函数:
Star north;
north = "polaris";
第二条语句调用 Star::operator = ( const Star * ) 函数,使用 Star::star ( const char * )生成一个 Star 对象;
在带一个参数的构造函数原型中使用 explicit 将禁止进行隐式转换,但允许显式转换:
class Star
{
. . .
public:
explicit Star( const char * );
. . .
};
Star north;
north = "polaris"; // not allowed cause explicit
north= Star( "polaris" ); // allowed
要将类对象转换为其他类型,应定义转换函数:
转换函数可以是没有参数的类成员函数;
转换函数可以是返回类型被声明为目标类型的类成员函数;
4. 按值传递对象与传递引用
通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象;
按值传递对象涉及调用复制构造函数,然后调用析构函数;
如果函数不修改对象,应将参数声明为 const 引用;
按引用传递对象的另一个原因,在继承使用虚函数时,被定义为接收基类引用参数的函数可以接受派生类
5. 返回对象和返回引用
函数不能返回函数中创建的临时对象的引用,函数结束时,临时对象取消,这种情况应返回对象;
如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象
6. 使用 const
使用 const 可以确保方法不修改参数:
Star :: Star ( const char & s ) { . . . };
使用 const 可以确保方法不修改调用它的对象:
void Star :: show() const { . . . };
这里 const 表示 const Star * this,而 this 指向调用的对象
通常,可以将返回引用的函数放在赋值语句的左侧,意味着可以将值赋给引用的对象;
可以使用 const 来确保引用或指针返回的值不能用于修改对象中的数据:
const Stock & Stock :: topval ( const STock & s ) const
{
. . .
}
注意: 如果函数将参数声明为指向 const 的引用或指针,则不能将该参数传递给另一个函数,除非后者确保了参数不会被修改
13.8.3 公有继承的考虑因素
1. is-a 关系
在某些情况下,最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其它类;
表示 is-a 关系的方式之一是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用就可以引用派生类;
2. 什么不被继承
构造函数不能继承:
创建派生类时,必须调用派生类的构造函数;
派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,已创建派生类对象的基类部分
析构函数不被继承:
释放对象时,程序首先调用派生类的析构函数,然后调用基类的析构函数;
对于基类,虚构函数应设置为虚的
赋值运算符不能继承:
派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异
3. 赋值运算符
如果编译器将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符:
默认版本或隐式版本采用成员赋值;
如果对象属于派生类,编译器将使用基类赋值运算符来处理派生对象中基类部分的赋值;
如果显式地为基类提供了赋值运算符,将使用该运算符;
如果成员是另一个类的对象,则对于该成员,将使用其所属类的赋值运算符
如果派生类使用了 new,则必须提供显式赋值运算符,必须给类的每个成员提供赋值运算符:
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;
}
将派生类对象赋给基类对象,将调用 Brass :: operator = (const Brass &) 方法
要将基类对象赋给派生类对象:
1. 定义转换函数:
BrassPlus (const Brass &, double ml = 500, double r = 0.1);
2. 定义一个用于将基类赋给派生类的赋值运算符:
BrassPlus & BrassPlus :: operator = ( const Brass & ) { . . . }
4. 私有成员和保护成员
对于派生类而言,保护成员类似于公有成员;
对于外部而言,保护成员类似于私有成员;
派生类可以直接访问基类的保护成员,但只能通过基类成员访问私有成员
5. 虚方法
设计基类时,必须确定是否将类方法声明为虚的;
如果希望派生类重新定义方法,则应在基类中将方法定义为虚的,这样可以启用动态联编;
如果不声明为虚的:您不希望它被重新定义;
6. 析构函数
基类的析构函数应当是虚的,这样通过指向对象的基类指针或引用来删除派生对象时:
程序将首先调用派生类的析构函数;
然后调用基类的析构函数
7. 友元函数
友元函数并非成员函数,不能继承;
可能希望派生类的友元函数能使用基类的友元函数:
可以通过强制类型转换将派生类引用或指针转换为基类引用或指针;
再使用转换后的指针或引用来调用基类的友元函数
也可以使用15章运算符 dynamic_cast<>来进行强制类型转换
8. 有关使用基类方法的说明
以公有方式派生的类的对象可以通过多种方式来使用基类的方法:
派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法;
派生类的构造函数自动调用基类的构造函数;
派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数;
派生类的构造函数显式地调用成员初始化列表中指定的基类构造函数;
派生类方法可以使用作用域解析运算符来调用公有和保护的基类方法;
派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数
13.8.4 类函数小结
C++ 类函数有很多不同的变体,其中有些是可以继承,有些不可以;
有些运算符函数既可以是成员函数,也可以是友元函数,而有些运算符函数只能是成员函数;
继承通过已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码;
公有继承建立 is-a 关系,这意味着派生类对象也应该是某种基类对象;
作为 is-a 模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承构造函数、析构函数和赋值运算符;
派生类可以直接访问基类的公有成员和保护成员,并能通过基类的公有方法和保护方法访问基类的私有成员;
每个派生类都必须有自己的构造函数;
如果希望派生类可以重新定义基类的方法,则可以使用关键字 virtual 将它们声明为虚的,采用动态联编;
基类的析构函数应该为虚的;
可以考虑定义一个ABC: 只定义接口,而不涉及实现:
ABC必须至少包含一个纯虚方法,可以在声明中的分号前面加上 = 0 来声明纯虚方法
virtual double area() const = 0;
不一定非得定义纯虚方法;
对于包含纯虚成员的类,不能使用它来创建对象;
纯虚方法用于定义派生类的通用接口
来源:https://www.cnblogs.com/kidycharon/p/9824033.html