继承是面向对象程序设计的基石
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 再论组合和继承
确定应当使用组合还是继承,最清楚的方法之一是询问是否需要从新类向上类型转换