繼承是面向對象程序設計的基石
14.1 組合語法
下面一段代碼把類型X的一個對象作爲公共對象嵌入到一個新類內部,實現了組合的語法。
//: C14: useful.h
//A class to reuse
#ifndef USEFUL_H
#define USEFUL_H
class X{
int i;
public:
X() { i = 0; }
void set(int ii) { i = ii; }
int read() const { return i; }
int permute() { return i = i * 47; }
};
#endif //USEFUL_H
//:C14: Composition.cpp
//reuse code with composition
#include "useful.h"
class Y{
int i;
public:
X x;
Y() { i = 0; }
void f(int ii ) { i = ii; }
int g() const { return i; }
};
int main()
{
Y y;
y.f(47);
y.x.set(37);
return 0;
}
將x放到公有成員的一個弊端是把它暴露給了類的使用者,更好的做法是將其作爲私有成員,然後向外部提供訪問的接口。如下:
//:C14: Composition2.cpp
//private embedded object
#include "useful.h"
class Y{
int i;
X x;
public:
Y() { i = 0; }
void f(int ii ) { i = ii; x.set(ii); }
int g() const { return i * x.read(); }
void permute() { x.permute(); }
};
int main()
{
Y y;
y.f(47);
y.permute();
return 0;
}
對於類的使用者來說,他只需要關心Y類能做什麼,而不關心裏面如何實現,因此這種表達是更清晰的。y.permute();比y.x.set(47);更直觀,更符合封裝特性。
14.2 繼承語法
定義一派生類時,在類名後加一個冒號,後面接基類的名字。新類將會自動獲得基類中的所有數據成員和成員函數
//: C14:Inheritance.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Simple inheritance
#include "Useful.h"
#include <iostream>
using namespace std;
class Y : public X {
int i; // Different from X's i
public:
Y() { i = 0; }
int change() {
i = permute(); // Different name call
return i;
}
void set(int ii) {
i = ii;
X::set(ii); // Same-name function call
}
};
int main() {
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = "
<< sizeof(Y) << endl;
Y D;
D.change();
// X function interface comes through:
D.read();
D.permute();
// Redefined functions hide base versions:
D.set(12);
} ///:~
Y對X進行了公有繼承。Y將包含X中所有的內容,並且仍然遵守訪問保護機制,X中的私有成員仍然佔有存儲空間,Y類也不能通過.或->運算直接訪問。公有繼承不改變基類的訪問權限,即派生類可以直接訪問基類中的公有數據成員和成員函數。如果派生類中重定義了基類中的某個函數,則一個Y類對象調用該函數時會使用派生類的版本,而不是基類的版本;如果想使用基類的版本,應該加上作用域運算符::,如下:
Y y(1);
X x(2);
y.set(5);//這裏調用的是Y類的版本,它將同時修改y中的數據成員i和x中的成員i
y.X::set(6);//這裏使用了明確的作用域運算符,將只修改x中的
14.3 構造函數的初始化表達式
我們知道在初始化一個派生類前要先初始化基類,然後是派生類的數據成員。c++如何來保證這個順序不會出錯呢?並且,新類的構造函數沒有辦法直接調用基類的構造函數,因此不能在派生類的構造函數裏調用基類的構造函數。
c++提供的專門的語法:構造函數的初始化表達式
模仿繼承的語法,在派生類的構造函數的參數列表後之後加上冒號,後面可以包括基類的初始化,派生類的數據成員初始化
並且,處於語法的一致性,對內建類型的成員,可以選擇與類的初始化相同的方式進行初始化。如下:
#include <iostream>
#include <string>
using namespace std;
class Base{
int i;
public:
Base(int ii) {
i = ii;
cout << "Base::i = " << i << endl;
}
~Base();
};
class Derived: public Base{
int x;
string s;
public:
Derived(int xx, string ss): Base(xx), x(xx), s(ss){
xx = x;
cout << "Derived::x = " << x << endl;
}
~Derived()
};
int main()
{
Derived a(4, "hello");
}
這樣,所有的成員對象在構造函數的左括號之前就被初始化了,這種方法對程序設計很有幫助。
14.4 組合和繼承的聯合
同時使用組合和繼承
//: C14:Combined.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Inheritance & composition
class A {
int i;
public:
A(int ii) : i(ii) {}
~A() {}
void f() const {}
};
class B {
int i;
public:
B(int ii) : i(ii) {}
~B() {}
void f() const {}
};
class C : public B {
A a;
public:
C(int ii) : B(ii), a(ii) {}
~C() {} // Calls ~A() and ~B()
void f() const { // Redefinition
a.f();
B::f();
}
};
int main() {
C c(47);
} ///:~
類C繼承了B同時有一個A類的成員對象,注意在C的構造函數中調用了基類的構造函數和成員對象構造函數,不論你寫代碼時的順序如何,編譯器會保證先調用基類的構造函數,而不是成員對象的構造函數。
14.4.1 構造函數和析構函數調用的順序
//: C14:Order.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Constructor/destructor order
#include <fstream>
using namespace std;
ofstream out("order.out");
#define CLASS(ID) class ID { \
public: \
ID(int) { out << #ID " constructor\n"; } \
~ID() { out << #ID " destructor\n"; } \
};
CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);
class Derived1 : public Base1 {
Member1 m1;
Member2 m2;
public:
Derived1(int) : m2(1), m1(2), Base1(3) {
out << "Derived1 constructor\n";
}
~Derived1() {
out << "Derived1 destructor\n";
}
};
class Derived2 : public Derived1 {
Member3 m3;
Member4 m4;
public:
Derived2() : m3(1), Derived1(2), m4(3) {
out << "Derived2 constructor\n";
}
~Derived2() {
out << "Derived2 destructor\n";
}
};
int main() {
Derived2 d2;
} ///:~
以上代碼的打印:
Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
Member2 destructor
Member1 destructor
Base1 destructor
可以看出,構造是從類層次最根處開始,而在每一層,首先會調用基類構造函數,然後調用成員對象構造函數。析構函數的調用順序則嚴格地完全相反;對於成員對象,構造函數調用的次序完全不受構造函數的初始化表達式中的次序影響,而是由成員對象在類的聲明的次序決定的。
14.5 名字隱藏
如何繼承一個類並且對他的成員函數重新進行定義,可能會出現兩種情況。
基類中
- 的普通函數在派生類中明確地定義操作和返回類型,這叫做重定義(redefining)
- 如果該基類成員函數時虛函數,這叫做重寫(overloading)
但是如果在派生類中改變了成員函數的參數列表和返回類型,會發生什麼情況呢?
//: C14:NameHiding.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Hiding overloaded names during inheritance
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//! d2.f(s); // string version hidden
Derived3 d3;
//! x = d3.f(); // return int version hidden
Derived4 d4;
//! x = d4.f(); // f() version hidden
x = d4.f(1);
} ///:~
從main()函數中被註釋掉的3行可以推導出一下結論:
- 任何時候重載了基類中的函數(包括修改了參數列表和返回類型),在新類中將無法使用基類中該函數的其他重載版本
- 基類中的函數有多個的重載版本,只重定義其中一個版本(參數列表和返回值都不變),將不能使用其他版本
14.6 非自動繼承的函數
有3中函數不能被繼承,如果想要實現相關功能,必須在新類中分別創建。在繼承過程中,如果不親自創建這些函數,編譯器就會生成他們(編譯器會生成默認構造函數和複製構造函數):
- 構造函數
- 析構函數
- operator=賦值運算符
構造和析構函數用來處理對象的創建和清楚操作,它們只知道它們特定層次上的對象做些什麼。而operator=類似於構造函數,不能保證在繼承後還能有相同的作用。
//: C14:SynthesizedFunctions.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Functions that are synthesized by the compiler
#include <iostream>
using namespace std;
class GameBoard {
public:
GameBoard() { cout << "GameBoard()\n"; }
GameBoard(const GameBoard&) {
cout << "GameBoard(const GameBoard&)\n";
}
GameBoard& operator=(const GameBoard&) {
cout << "GameBoard::operator=()\n";
return *this;
}
~GameBoard() { cout << "~GameBoard()\n"; }
};
class Game {
GameBoard gb; // Composition
public:
// Default GameBoard constructor called:
Game() { cout << "Game()\n"; }
// You must explicitly call the GameBoard
// copy-constructor or the default constructor
// is automatically called instead:
Game(const Game& g) : gb(g.gb) {
cout << "Game(const Game&)\n";
}
Game(int) { cout << "Game(int)\n"; }
Game& operator=(const Game& g) {
// You must explicitly call the GameBoard
// assignment operator or no assignment at
// all happens for gb!
gb = g.gb;
cout << "Game::operator=()\n";
return *this;
}
class Other {}; // Nested class
// Automatic type conversion:
operator Other() const {
cout << "Game::operator Other()\n";
return Other();
}
~Game() { cout << "~Game()\n"; }
};
class Chess : public Game {};
void f(Game::Other) {}
class Checkers : public Game {
public:
// Default base-class constructor called:
Checkers() { cout << "Checkers()\n"; }
// You must explicitly call the base-class
// copy constructor or the default constructor
// will be automatically called instead:
Checkers(const Checkers& c) : Game(c) {
cout << "Checkers(const Checkers& c)\n";
}
Checkers& operator=(const Checkers& c) {
// You must explicitly call the base-class
// version of operator=() or no base-class
// assignment will happen:
Game::operator=(c);
cout << "Checkers::operator=()\n";
return *this;
}
};
int main() {
Chess d1; // Default constructor
Chess d2(d1); // Copy-constructor
//! Chess d3(1); // Error: no int constructor
d1 = d2; // Operator= synthesized
f(d1); // Type-conversion IS inherited
Game::Other go;
//! d1 = go; // Operator= not synthesized
// for differing types
Checkers c1, c2(c1);
c1 = c2;
} ///:~
Chess類繼承了Game類,但沒有顯式地創建構造函數和析構函數,也沒有重載operator=運算符。但是main()函數中,chess d1; chess d2(d1); d1 = d2; 這3條語句卻都編譯通過可以運行。這說明:編譯器確實幫我們創建了默認構造函數、複製構造函數,析構函數、operator運算符。再次聲明:這不是繼承來的,而是編譯器爲我們創建的。而Chess d3(1); 編譯不能通過,是因爲沒有接收一個整型參數的構造函數,很好理解,因爲編譯器不知道你可能需要什麼樣的參數來構造對象,因此指望它爲你帶參數的構造函數是不可能的。
注意54行的函數f(),它本來是接收一個Game::Other類型的參數,但調用的時候傳了一個Chess類的對象。這個叫做“自動類型轉換”,也叫“隱式類型轉換”,我的知乎問答有相關的解釋
d1 = go; 語句也編譯不通過,因爲operator=只能作用於同種對象。如果想把一種類型轉換爲另一種類型,則這個operator=必須自己寫出。
關於Chess類,沒有明確地寫出構造函數,但是編譯器爲我們生成的構造函數卻自動地調用了基類的默認構造函數或複製構造函數。但是觀察Chechers類,它明確地寫出了構造函數,複製構造函數,和賦值運算符。並且顯式地在初始化列表裏調用了基類的構造函數。
這一點很重要,一旦決定自己寫複製構造函數和賦值運算符,編譯器就不會幫我們自動地調用基類的構造函數,而必須由我們手動完成。
14.6.1 繼承和靜態成員函數
靜態成員函數與非靜態成員函數的共同點:
- 他們均可被繼承到派生類中
- 如果重新定義了一個靜態成員,所有在基類中的其他重載函數會被隱藏
- 如果改變了基類中一個函數的特徵,所有使用該函數名字的基類版本都將會被隱藏
14.7 組合與繼承的選擇
最精煉的說法:
- 使用繼承:兩個類的關係能用is-a表達,如貓是一種動物
- 使用組合:兩個類的關係能用has-a表達,如汽車有四個輪子
14.7.1 子類型設置
當你要寫一個類的時候,需要另一個類的所有東西都包含進來,這被稱作子類型化(subtyping)。創建一個新類,並且希望這個新類與存在的類有着嚴格相同的接口,所以能在已經用過這個已存在的類的任何地方使用這個新類,這就是必須要使用繼承的地方。
//: C14:FName2.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Subtyping solves the problem
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName2 : public ifstream {
string fileName;
bool named;
public:
FName2() : named(false) {}
FName2(const string& fname)
: ifstream(fname.c_str()), fileName(fname) {
assure(*this, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
};
int main() {
FName2 file("FName2.cpp");
assure(file, "FName2.cpp");
cout << "name: " << file.name() << endl;
string s;
getline(file, s); // These work too!
file.seekg(-200, ios::end);
file.close();
} ///:~
FName2類創建的對象可以替換以前你使用的任意一個ifstream對象,因爲前者由後者繼承而來,他們有着嚴格相同的接口,你以前所使用的ifstream對象的所有操作,FName2類中都包括,所以完全不影響以前的代碼。同時前者的對象還具有自己的新的功能。
14.7.2 私有繼承
大部分情況不會這麼用,它的目的是爲了語言的完整性。去掉public或者顯式地聲明爲private,可以私有繼承。創建的新類具有基類的數據和功能,但這些功能是隱藏的。它只是部分的內部實現,該類的用戶不能訪問這些內部功能,最重要的是:派生類不能看成這個基類的實例(回憶我們之前說派生類可以看成是一個基類,但如果用了private就不再成立)。但是,如果是爲了使用一個類的某些功能,並不會私有地繼承,而是使用組合創建一個私有的對象。
14.7.2.1 對私有繼承成員公開化
當私有繼承時,基類的所有public成員都變成了private。如果希望他們中任何一個是可視的,只要用派生類的public部分聲明他們的名字即可:
//: C14:PrivateInheritance.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
class Pet {
public:
char eat() const { return 'a'; }
int speak() const { return 2; }
float sleep() const { return 3.0; }
float sleep(int) const { return 4.0; }
};
class Goldfish : Pet { // Private inheritance
public:
using Pet::eat; // Name publicizes member
using Pet::sleep; // Both members exposed
};
int main() {
Goldfish bob;
bob.eat();
bob.sleep();
bob.sleep(1);
//! bob.speak();// Error: private member function
} ///:~
使用using
關鍵字將基類中的公有成員函數聲明爲在派生類中可見,protected成員也可以,但基類中的private成員仍然無法訪問。如果給出一個重載函數的名字將使基類中所有它的重載版本公有化。
14.8 protected
基類的private成員繼承後不能訪問,但在實際項目中,有時希望某些東西是隱藏起來的,但仍允許其派生類生成的成員訪問,這時protected就派上用場了。protected:就這個類的用戶而言,它是private的,但它可以被從這個類繼承來的任何類使用
//: C14:Protected.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// The protected keyword
#include <fstream>
using namespace std;
class Base {
int i;
protected:
int read() const { return i; }
void set(int ii) { i = ii; }
public:
Base(int ii = 0) : i(ii) {}
int value(int m) const { return m*i; }
};
class Derived : public Base {
int j;
public:
Derived(int jj = 0) : j(jj) {}
void change(int x) { set(x); }
};
int main() {
Derived d;
d.change(10);
} ///:~
這裏不管你是創建一個Base類對象還是Derived對象,都無法直接訪問Base類中的read和set函數。但是Derived類內部可以訪問他們。與private繼承一樣,protected繼承也不常用,它的存在只是爲了語言的完備性。
14.9 運算符的重載與繼承
除了運算符以外,其餘的運算符可以自動地繼承到派生類中。
14.10 多重繼承
Bruce Eckel大師說:“不管我們如何認爲我們必須使用多重繼承,我們總是能通過單繼承完成“,這裏的內容放到第二捲去講了。
14.11 漸增式開發
繼承和組合的優點之一是它支持漸增式開發(incremental development),它允許在已存在的代碼中引進新代碼,而不會給原來的代碼帶來錯誤,即使產生了錯誤,這個錯誤也只與新代碼有關。
14.12 向上類型轉換
繼承的最重要的方面不是它爲新類提供了成員函數,而是它是基類與新類之間的關係,這種關係可被描述爲:新類屬於原有類。這種描述是直接被編譯器支持的。
//: C14:Instrument.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Inheritance & upcasting
enum note { middleC, Csharp, Cflat }; // Etc.
class Instrument {
public:
void play(note) const {}
};
// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {};
void tune(Instrument& i) {
// ...
i.play(middleC);
}
int main() {
Wind flute;
tune(flute); // Upcasting
} ///:~
tune()函數接受一個Instrument類型的引用,然而在調用時,卻傳遞了一個Wind類的對象給它;這是可以的,最終編譯器調用了Instrument版本的play(),這就是一種向上類型轉換。之所以說是向上類型轉換,是因爲在畫UML圖時,Wind類是繼承於Instrument類的,箭頭從下而上指向Instrument。
(抱歉,畫不了表達繼承關係的圖,只能用這個sequence序列圖代替一下了)
14.12.1
向上類型轉換總是安全的,因爲是從更專門的類型到更一般的類型——對於這個類接口可能出現的唯一事情是它失去成員函數。
14.12.2
如果允許編譯器爲派生類生成拷貝構造函數,它將首先自動地調用基類的拷貝構造函數,然後是個成員對象的拷貝構造函數(內建類型上執行位拷貝)。
//: C14:CopyConstructor.cpp
// From Thinking in C++, 2nd Edition
// Available at http://www.BruceEckel.com
// (c) Bruce Eckel 2000
// Copyright notice in Copyright.txt
// Correctly creating the copy-constructor
#include <iostream>
using namespace std;
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent&)\n";
}
Parent() : i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(const Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
friend ostream&
operator<<(ostream& os, const Child& c){
return os << (Parent&)c << c.m
<< "Child: " << c.i << endl;
}
};
int main() {
Child c(2);
cout << "calling copy-constructor: " << endl;
Child c2 = c; // Calls copy-constructor
cout << "values in c2:\n" << c2;
} ///:~
Child中的operator<<很有意思,它通過將Child對象類型轉換爲Parent&,這是一處向上類型轉化,因此編譯器調用了基類的版本。另外Child沒有顯式地定義複製構造函數。編譯器將通過調用Parent和Member的複製構造函數來生成它的複製構造函數。但如果要自己寫的話,一定要記得正確地調用基類的複製構造函數。這乍一看有點奇怪,但它是向上類型轉換的一種。
Child(const Child& c): Parent(c), i(c,i), m(c.m)
{
cout << "Child(const Child&)\n";
}
14.12.3 再論組合和繼承
確定應當使用組合還是繼承,最清楚的方法之一是詢問是否需要從新類向上類型轉換