读书笔记-Thinking in C++-第14章 继承和组合
来源:互联网 发布:什么手游不卖数据 编辑:程序博客网 时间:2024/05/17 17:40
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的,但若没有public则X的一切在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 {
Member
Member
public:
Derived1(int) : m2(1), m1(2), Base1(3) {
out << "Derived1 constructor/n";
}
~Derived1() {
out << "Derived1 destructor/n";
}
};
class Derived2 : public Derived1 {
Member
Member
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的名字。则可以用ifstream和string来合成该类。
//: 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,而对于派生类来说是“is-a”的关系,其很少使用。
增量开发
合成和继承的好处在于其支持增量开发,这样可以在新的代码中直接引入原有的代码而不会造成原有代码的bug。通过组合或者继承原有的代码,再增添新的成员变量和函数,甚至重新定义新的基类中的函数,不会改动原有的代码,也不用调试原有代码,当出现bug时,这样可以确认在新代码中,便于定位分析问题。
程序开发是一个渐进的过程,你不必搞清原有代码的每一个细节,只要在原有的基础上一层层的建楼即可了,而不是每次从地基开始建整个高楼。
总之,继承意味着这样一种关系:新类是旧类的一种。
向上强制转换
继承的最大特性不在于其为新类提供了基类的接口,而在于其称述了一种关系:新类是旧类的一种。这种特性不是一种简单的表述,其被编译器支持。例如,instrutment和wind类,我们可以说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对象的引用来调用tune。C++对类型检查很严格,但实际上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为基类,这是多态的基础。
- 读书笔记-Thinking in C++-第14章 继承和组合
- 读书笔记-Thinking in C++-第8章 const
- 读书笔记-Thinking in C++-第6章 初始化和清除Initialization& Cleanup
- 读书笔记-Thinking in C++-第11章 引用和拷贝构造函数
- 读书笔记-Thinking in C++-第15章 多态和虚函数
- 读书笔记-Thinking in C++-第11章 引用和拷贝构造函数
- Thinking in Java 读书笔记 第七章 复用类(继承、组合)
- thinking in c++读书笔记---------继承与组合
- 读书笔记-Thinking in C++-第7章 函数重载和默认参数Function Overloading &Default Arguments
- 读书笔记-Thinking in C++-第9章 内联inline函数
- 读书笔记-Thinking in C++-第10章 名字空间namespace
- 读书笔记-Thinking in C++-第13章 动态对象的创建
- Thinking in C++读书笔记--13.4继承与组合
- Thinking in C++: 第1章 继承:类型关系
- Thinking in Java读书笔记(第七章) :重载,继承,重写和多态的区别
- Thinking in C++: 第1章 多态性
- Thinking in Java第三版读书笔记-第六章:复用类
- Thinking in Java第三版读书笔记-第七章:多态性
- 数据汇总加排序
- 圣诞节前发布的开源项目小结-欢迎补充
- 新VB9出来了
- 自动循环播放的MP3播放器(续)
- Apache Geronimo 1.0 正式发布 - 明天来详细分析其组件
- 读书笔记-Thinking in C++-第14章 继承和组合
- bld.inf、.mmp和.pkg文件浅析
- Running linux(5th)读书笔记1
- 邮件发送
- 超级战国风云打法
- Carbide.C++ IDE简介
- 密码口令设置的技巧
- 古代美女帅哥集中营
- CppUnit与单元测试