读书笔记-Thinking in C++-第14章 继承和组合

 

 

14、继承和组合... 55

合成的语法... 55

继承的语法... 56

构造函数的初始化列表... 57

成员对象的初始化... 58

在初始化列表中对内建类型数据进行初始化... 58

将合成和继承组合起来... 58

自动调用析构函数... 59

构造和析构的顺序... 59

名字隐藏... 60

并不自动继承的函数... 61

继承和静态成员函数... 64

组合和继承如何选择?... 64

子类型... 65

私有继承... 67

Protected. 68

保护继承... 68

增量开发... 69

向上强制转换... 69

什么叫“upcasting”... 69

Upcasting和拷贝构造函数... 70

指针和引用的upcasting. 72

危机... 72

总结... 72

 

14、继承和组合

C++的一大特性就是代码重用,但绝不是象C一样简单的拷贝代码再修改。C++中所有的变革都是以类为基础的,即在现有类的基础上创建新的类。Compositon是一种,即新类是现有的类组合而成的。另外一种是继承,其为面向对象的编程技术的一座里程碑。

 

合成的语法

合成很简单,通常的用内建数据类型创建类本质上就是合成,只是此时基本单元是另外一个类而已。

//: 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 ///:~

 

成员变量i是私有的,因此可以在新类中嵌入X的对象作为新类的成员,并且其是安全的。

//: C14:Composition.cpp

// Reuse code with composition

#include "Useful.h"

class Y {

int i;

public:

X x; // Embedded object

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); // Access the embedded object

} ///:~

 

但是通常的做法是将内嵌对象作为私有成员,这样只有public函数对外提供接口,而你可以改变X的实现不会影响外部代码。新类中的permute直接调用X的服务即可,由新类对外提供统一接口。在新类内部访问X的成员变量或者函数都需要指定具体的对象。

//: C14:Composition2.cpp

// Private embedded objects

#include "Useful.h"

class Y {

int i;

X x; // Embedded object

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();

} ///:~

 

继承的语法

在新类的“}”之前,用表示继承,当有多个基类时用隔开。

//: C14:Inheritance.cpp

// 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);

} ///:~

 

 

Public表示基类X的成员中私有的仍为Y中私有的,public的仍为public的,但若没有publicX的一切在Y中都是私有的,这显然不是继承所要的。

 

名字一样,系统怎么区分的呢?

 

 

 

派生类可以直接访问基类的共有成员函数

 

 

当你不喜欢基类中的某个成员函数时,可以在新类中重新定义,将自动覆盖基类的成员函数。若想调用基类的对应函数,则必须用范围解析器::并带上基类名称。

 

基类的成员仍将占用内存,在内存问题上,不论是组合还是继承,都是将旧类作为一个内嵌对象处理的。

 

 

 

基类的公共成员函数可以自动被派生类调用。

 

新定义的函数隐藏了基类中的版本

 

 

构造函数的初始化列表

C++中,当创建新的对象时,编译器确保每一个子对象的构造函数会调用。前提是各个子对象有默认的构造函数,但是若子对象没有默认的构造函数或者你想改变某个默认参数呢?因为新类的构造函数无法访问子类对象的私有成员,因此解决的办法是在派生类的初始化列表中显式调用基类的构造函数,在派生类的参数列表之后“{”之前调用基类的构造函数,如:

MyType::MyType(int i) : Bar(i) { // ...

 

成员对象的初始化

对应组合方式,采用同样的技术来初始化成员对象,只不过是对象的名称而非基类类型名称,如:

MyType2::MyType2(int i) : Bar(i), m(i+1) { // .

Bar为基类,m为成员对象

 

在初始化列表中对内建类型数据进行初始化

构造函数的初始化列表允许显式调用成员对象的构造函数。基本的思想是在派生类构造函数的“{”之前,所有的成员对象必须已经初始化。

为了使语法一致,允许在初始化列表中对基本类型的成员变量进行初始化。在初始化列表中对成员变量进行初始化是一种良好的编程习惯。

//: C14:PseudoConstructor.cpp

class X {

int i;

float f;

char c;

char* s;

public:

X() : i(7), f(1.4), c('x'), s("howdy") {}

};

int main() {

X x;

int i(100);

// Applied to ordinary definition

int* ip = new int(47);

} ///:~

 

 

 

 

将合成和继承组合起来

可以在新类中同时采用基类和成员对象的方式。

//: C14:Combined.cpp

// 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的构造函数。并重新定义了f,主要只有在有继承的情况下才有所谓的重新定义,对于成员对象必须显式的指定对象名称并且只能操作成员对象的公共接口。若未定义C::f,则调用f()将调用B::f,而非a.f

 

自动调用析构函数

在初始化列表中需要显式调用构造函数,但无须显式调用析构函数,因为任意类只有一个析构函数,并且其不接收参数。编译器会确保每一个对象的析构函数被调用,并且是从最新派生的类开始的,一直到最开始的基类。

 

构造和析构的顺序

当一个对象拥有过多的子对象时,有必要搞明白构造和析构的顺序。

//: C14:Order.cpp

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

 

#ID是字符串连接符,即将其变成“ID”

 

ID非默认的构造函数,具备int参数但没有标识符,是强制在初始化列表中显式调用,没有标识符是为了防止编译报警。

 

基本原则是首先调用基类的构造函数,然后是成员对象的构造函数。析构的顺序正好相反,是为了防止可能的依赖关系。对于成员对象的构造函数的调用顺序,其不受初始化列表中的顺序影响,而只和成员对象在类中的定义顺序相关。否则可以有多种版本的构造,但析构只有一种。

 

名字隐藏

继承一个类,并为基类的某个成员函数提供一个新的定义,有两种可能。第一种是完全一样的参数和返回值,对于普通函数这叫重新定义,若基类中其为虚函数,则此为overriding

那该变参数列表或者返回值呢?

//: C14:NameHiding.cpp

// 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);

} ///:~

 

 

 

 

 

int f(string)int f(string)是重载关系

 

 

 

 

 

 

 

 

没有f的相关定义,两个重载版本在Derived1中都可以使用

重新定义了g(),尽管加了const,但没有任何作用,仍然认为是重新定义

 

 

完完全全的重新定义,此时int f(string)不可见

 

 

 

 

 

 

 

改变了返回值,覆盖了int f(string)int f(string)

 

 

改变了参数列表,覆盖了int f(string)int f(string)

 

 

 

 

 

 

 

 

 

 

 

 

任何时候重新定义基类中的某个重载函数,则在新类中基类中所有相关版本都会被隐藏覆盖。

 

 

当你改变基类函数的参数或者返回值时,你就改变了基类的接口,这通常不是继承所希望的。你不具备基类的基本特性,那你又继承什么呢?继承的终极目标是多态,若改变了基类的接口则相当于为新类定制某些功能,这就是合成compositon的范畴了。

 

并不自动继承的函数

构造函数和析构函数只能用于他们自己特定的对象,因此在继承的体系中,其下的各种构造析构都将调用。构造和析构函数不能继承,每个派生类都必须有自己的构造和析构函数。

另外操作符=不能继承,是因为其行为类似于构造函数。在继承中,若没有定义构造和析构及赋值操作符时,系统将自动生成无参构造、拷贝构造及。对于构造函数,为了使编译器自动生成无参构造和拷贝构造函数,你不能定义任何类型的构造函数。

 

自动生成的构造函数将按照成员顺序进行初始化。

//: C14:SynthesizedFunctions.cpp

// 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; // this为指向当前对象的指针,×this为当前对象

}

~GameBoard() { cout << "~GameBoard()/n"; }

};

 

//采用composiont时实现各种构造函数

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;

} ///:~

当自定义了某种构造函数时,编译器不会自动生成带参数的构造函数;

自动生成的不能用于不同类型的赋值。

Checkers中,当实现自己的拷贝构造函数和时,编译器不会自动调用基类对应的拷贝构造和,你必须显式的在初始化列表中调用基类的拷贝构造函数。

 

继承和静态成员函数

静态成员函数和非静态的特性基本相同:

自动引入派生类中;

新类中重新定义静态成员时,基类中所有重载的函数将被隐藏。

在新类中改变了基类函数的某个参数或返回值,所有具备那个函数名的基类函数将被隐藏。

但是静态成员函数不能是虚函数。

 

组合和继承如何选择?

组合和继承都将子对象作为新类的一员,占用内存,且都用初始化列表来构造这些子对象,那么区别在哪呢?

 

组合使用于在新类中你希望获得现有类的特性而非其接口。即你利用现有类来实现你新类的特性,用户见到的是新类的接口而非原有类的接口,即原有类对用户是隐藏的,这需要你将原有类作为私有的内嵌对象。

 

但有时候需要用户直接去访问新类中的合成对象。内嵌对象具备一定的访问控制机制。如:

//: C14:Car.cpp

// Public composition

class Engine {

public:

void start() const {}

void rev() const {}

void stop() const {}

};

class Wheel {

public:

void inflate(int psi) const {}

};

class Window {

public:

void rollup() const {}

void rolldown() const {}

};

class Door {

public:

Window window;

void open() const {}

void close() const {}

};

class Car {

public:

Engine engine;

Wheel wheel[4];

Door left, right; // 2-door

};

int main() {

Car car;

car.left.window.rollup();

car.wheel[0].inflate(72);

} ///:~

将各个合成的成员作为public的,便于用户访问合成对象,降低了新类的复杂度。但是不能用vehicle来合成car因为car不含有vehicle,而是vehicle。隐藏是一种的关系适用于继承,而含有适用于合成。

 

子类型

现在创建一个ifstream对象,其打开一个file同时记录file的名字。则可以用ifstreamstring来合成该类。

//: C14:FName1.cpp

// An fstream with a file name

#include "../require.h"

#include <iostream>

#include <fstream>

#include <string>

using namespace std;

class FName1 {

ifstream file;

string fileName;

bool named;

public:

FName1() : named(false) {}

 

FName1(const string& fname)

: fileName(fname), file(fname.c_str()) {

assure(file, fileName);

named = true;

}

string name() const { return fileName; }

void name(const string& newName) {

if(named) return; // Don't overwrite

fileName = newName;

named = true;

}

operator ifstream&() { return file; }

};

在任何使用ifstream的地方就可以使用FNAME1,因为有自动类型转换符operator ifstream&()FNAME1转换成ifstream。这只适用于函数调用传参数的时候。

int main() {

FName1 file("FName1.cpp");

cout << file.name() << endl;

// Error: close() not a member:

//!file.close();

} ///:~

 

file.close();

不能使用,是因为ifstream为私有成员,其接口在新类中对外是不可见的。

但可以利用ifstream的功能实现新的接口供用户使用。

void close() { file.close(); }

若只是需要ifstream的部分功能,那么这个方法可行,合成是很好的手段。但是当你需要ifstream的全部功能呢?这个时候再利用ifstream去实现所有新的接口就太麻烦了。这就是子类,用现有的类构建新类,新类具备所有旧类的接口并且可以添加新的成员函数。子类最适合于继承。

 

//: C14:FName2.cpp

// 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();

} ///:~

 

Getline需要ifstream参数,但仍然能接收file FName2,是因为FName2是一种ifstream,而非仅含有一个ifstream

 

私有继承

去掉public或者显式指定为private的都可以将基类作为私有的。此时新类具备所有基类的成员和功能,但其是隐藏的。新的对象不能作为基类的一个实例。

 

私有继承有什么用呢?采用合成技术将旧类对象作为私有对象似乎更适合于获得上述特性。通常为了避免混淆,避免用私有继承。但当你需要基类的相关接口同时又不想其类似一个基类对象时,可以用私有继承。

 

私有继承就象public合成一样,一般应避免,这些只有很少的场合需要。

 

将私有继承的成员public

私有继承会使得基类中的所有public成员变为private的,若希望部分可见,可在派生类的public域进行声明,无须任何参数或返回值。

//: C14:PrivateInheritance.cpp

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:

Pet::eat; // Name publicizes member

Pet::sleep; // Both overloaded members exposed

};

int main() {

Goldfish bob;

bob.eat();

bob.sleep();

bob.sleep(1);

//! bob.speak();// Error: private member function

} ///:~

因此当你想隐藏基类的部分功能时,私有继承就派上用场了。

 

在使用私有继承而非合成是需要慎重考虑。因为当和运行时的类型识别联系起来时,其将变得非常复杂。

 

Protected

Protected总是和继承相联系的,有时需要将基类中的部分特性对用户隐藏起来,同时又希望派生类能全部访问。Protected就可以解决这种问题,其意味着对于用户其是private的,而对于任何继承者是可见的。

 

数据成员总应是私有的,通过Protected机制可以对基类的成员函数的访问进行一定的控制。

//: C14:Protected.cpp

// 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);

} ///:~

 

保护继承

继承时,基类默认为private的。但通常都将基类对象申明为public 的,这样基类中的接口也是派生类的接口。在继承时也可以使用保护机制,其对于其它类来说implemented-in-terms-of而对于派生类来说是“isa”的关系,其很少使用。

 

增量开发

合成和继承的好处在于其支持增量开发,这样可以在新的代码中直接引入原有的代码而不会造成原有代码的bug通过组合或者继承原有的代码,再增添新的成员变量和函数,甚至重新定义新的基类中的函数,不会改动原有的代码,也不用调试原有代码,当出现bug时,这样可以确认在新代码中,便于定位分析问题。

 

程序开发是一个渐进的过程,你不必搞清原有代码的每一个细节,只要在原有的基础上一层层的建楼即可了,而不是每次从地基开始建整个高楼

 

总之,继承意味着这样一种关系:新类是旧类的一种。

 

向上强制转换

继承的最大特性不在于其为新类提供了基类的接口,而在于其称述了一种关系:新类是旧类的一种。这种特性不是一种简单的表述,其被编译器支持。例如,instrutmentwind类,我们可以说wind对象也是有一种instrutment,下面将显示编译器如何支持这种观念。

 

//: C14:Instrument.cpp

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

} ///:~

main中通过传递wind对象的引用来调用tuneC++对类型检查很严格,但实际上wind对象也是instrument对象,tune所调用的instrument的每一个成员在wind中都有。wind的引用或者指针自动转化为instrument的引用或者指针的行为就叫upcasting向上转换。

 

什么叫“upcasting”

从派生类到基类的转换在继承表中是往上走的,因此叫upcasting其总是安全的,因为其将一个特定的对象转换为了一个更通用的对象。唯一的变化是其可能失去某些成员,但它至少还是个instrument,因此编译器不用显式的强制转换。

 

Upcasting和拷贝构造函数

当编译器为派生类自动生成拷贝构造函数时,其将自动调用基类的拷贝构造函数,然后是各个成员对象的拷贝构造函数。

//: C14:CopyConstructor.cpp

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

} ///:~

 

return os << (Parent&)c << c.m

Parent(int ii)

Member(int ii)

Child(int ii)

calling copy-constructor:

Parent(const Parent&)

Member(const Member&)

values in c2:

Parent: 2

Member: 2

Child: 2

 

当自己实现拷贝构造函数时,忘记了调用基类的拷贝构造函数,这时候编译器会调用默认的构造函数,因为对于C++来说必须确保任何对象初始化过。

Child(const Child& c) : i(c.i), m(c.m) {}

Parent(int ii)

Member(int ii)

Child(int ii)

calling copy-constructor:

Parent() //默认的无参构造函数

Member(const Member&)

values in c2:

Parent: 0

Member: 2

Child: 2

 

因此当自己实现拷贝构造函数时必须确保调用了基类的拷贝构造函数。如下:

Child(const Child& c)

: Parent(c), i(c.i), m(c.m) {

cout << "Child(Child&)/n";

}

这又是一个upcasting的例子,将child对象作为基类的引用参数传递给基类,因为child的引用将自动转换为parent的引用。

 

指针和引用的upcasting

除了在函数调用时编译器可以实现自动转换外,在进行指针或者引用赋值时也可以自动转换。和函数调用一样,这两种情况都不需要显式的强制转换

Wind w;

Instrument* ip = &w; // Upcast

Instrument& ir = w; // Upcast

 

危机

upcast的时候会失去类型信息,如下编译器只能将ip作为Instrument的指针来调用

Wind w;

Instrument* ip = &w;

 

ip->play(middleC);

此时调用的是基类的Instrument::play( ) 而非本意Wind::play( ).,这种问题可以通过面向对象的编程技术的第三个里程碑多态polymorphism来解决,在C++中是通过虚函数来实现的

 

 

总结

当需要将现有类作为新类的部分功能实现时,可以用私有合成;而当你需要把新类转换为基类时可以采用继承,且通常是public继承。因为派生类具备基类的接口,因此其可以upcast为基类,这是多态的基础。

Bruce Eckel 《Thinking in Java》(Java编程思想)作者。Eckel有20年专业编程经验,并自1986年起教育人们如何撰写面向对象程序,足迹遍及全球,成为一位知名的 C++教师顾问,如今兼涉Java。他是C++标准委员会拥有表决权的成员之一,曾经写过另五本面向对象编程书籍,发表过150篇以上的文,是多本计算机杂志的专栏作家。Eckel开创Software Development Conference的C++、Java、Python等多项研讨活动。拥有应用物理学学士计算机工程学硕士学位。 目录 译者序 前言 第1 对象导言 第2 对象的创建与使用 第3 C++中的C 第4 数据抽象 第5 隐藏实现 第6 初始化与清除 第7 函数重载与默认参数 第8 常量 第9 内联函数 第10 名字控制 第11 引用拷贝构造函数 第12 运算符重载 第13 动态对象创建 第14 继承组合 第15 多态性虚函数 第16 模板介绍 附录A 编码风格 附录B 编程准则 附录C 推荐读物 索引 第2卷:实用编程技术 出版者的话 专家指导委员会 译者序 前言 第一部分 建立稳定的系统 第1 异常处理 第2 防御性编程 第二部分 标准C++库 第3 深入理解字符串 第4 输入输出流 第5 深入理解模板 第6 通用算法 第7 通用容器 第三部分 专题 第8 运行时类型识别 第9 多重继承 第10 设计模式 第11 并发 附录 附录A 推荐读物 附录B 其他 索引 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值