== Part 2/4 ============================
=============================
■□ 第9节:自由记忆体管理
=============================
Q33:"delete p" 会删去 "p" 指标,还是它指到的资料,"*p" ?
该指标指到的资料。
"delete" 真正的意思是:「删去指标所指到的东西」(delete the thing pointed
to by)。同样的英文误用也发生在 C 语言的「『释放』指标所指向的记忆体」上
("free(p)" 真正的意思是:"free_the_stuff_pointed_to_by(p)" )。
========================================
Q34:我能 "free()" 掉由 "new" 配置到的、"delete" 掉由 "malloc()" 配置到的
记忆体吗?
不行。
在同一个程式里,使用 malloc/free 及 new/delete 是完全合法、合理、安全的;
但 free 掉由 new 配置到的,或 delete 掉由 malloc 配置到的指标则是不合法、
不合理、该被痛骂一顿的。
========================================
Q35:为什麽该用 "new" 而不是老字号的 malloc() ?
建构子/解构子、型别安全性、可被覆盖(overridability)。
建构子/解构子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 还会去呼叫
Fred 的建构子。同理,"delete p" 会去呼叫 "*p" 的解构子。
型别安全性:malloc() 会传回一个不具型别安全的 "void*",而 "new Fred()" 则
会传回正确型态的指标(一个 "Fred*")。
可被覆盖:"new" 是个可被物件类别覆盖的运算子,而 "malloc" 不是以「各个类别
」作为覆盖的基准。
========================================
Q36:为什麽 C++ 不替 "new" 及 "delete" 搭配个 "realloc()" ?
避免你产生意外。
当 realloc() 要拷贝配置区时,它做的是「逐位元 bitwise」的拷贝,这会弄坏大
部份的 C++ 物件。不过 C++ 的物件应该要能自我拷贝才对:用它们自己的拷贝建构
子或设定运算子。
========================================
Q37:我该怎样配置/释放阵列?
用 new[] 和 delete[] :
Fred* p = new Fred[100];
//...
delete [] p;
每当你在 "new" 运算式中用了 "[...]",你就必须在 "delete" 陈述中使用 "[]"。
^^^^
这语法是必要的,因为「指向单一元素的指标」与「指向一个阵列的指标」在语法上
并无法区分开来。
========================================
Q38:万一我忘了将 "[]" 用在 "delete" 由 "new Fred[n]" 配置到的阵列,会发生
什麽事?
灾难。
这是程式者的--而不是编译器的--责任,去确保 new[] 与 delete[] 的正确配
对。若你弄错了,编译器不会产生任何编译期或执行期的错误讯息。堆积(heap)被
破坏是最可能的结局,或是更糟的,你的程式会当掉。
========================================
Q39:成员函数做 "delete this" 的动作是合法的(并且是好的)吗?
只要你小心的话就没事。
我所谓的「小心」是:
1) 你得 100% 确定 "this" 是由 "new" 配置来的(而非 "new[]",亦非自订的
"new" 版本,一定要是最原始的 "new")。
2) 你得 100% 确定该成员函数是此物件最後一个会呼叫到的。
3) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 的物件了,包
括资料及运作行为在内。
4) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 指标了。
换句话说,你不能查看它、将它与其他指标或是 NULL 相比较、印出其值、
对它转型、对它做任何事情。
很自然的,这项警告也适用於:当 "this" 是个指向基底类别的指标,而解构子不是
virtual 的场合。
========================================
Q40:我该怎麽用 new 来配置多维阵列?
有很多方法,端视你对阵列大小的伸缩性之要求而定。极端一点的情形,如果你在编
译期就知道所有阵列的维度,你可以静态地配置(就像 C 一样):
class Fred { /*...*/ };
void manipulateArray()
{
Fred matrix[10][20];
//使用 matrix[i][j]...
//不须特地去释放该阵列
}
另一个极端情况,如果你希望该矩阵的每个小块都能不一样大,你可以在自由记忆体
里配置之:
void manipulateArray(unsigned nrows, unsigned ncols[])
//'nrows' 是该阵列之列数。
//所以合法的列数为 (0, nrows-1) 开区间。
//'ncols[r]' 则是 'r' 列的行数 ('r' 值域为 [0..nrows-1])。
{
Fred** matrix = new Fred*[nrows];
for (unsigned r = 0; r < nrows; ++r)
matrix[r] = new Fred[ ncols[r] ];
//使用 matrix[i][j]...
//释放就是配置的反动作:
for (r = nrows; r > 0; --r)
delete [] matrix[r-1];
delete [] matrix;
}
========================================
Q41:C++ 能不能做到在执行时期才指定阵列的长度?
可以。STL 有一个 vector template 提供这种行为。请参考“程式库”一节的 STL
项目。
不行。内建的阵列型态必须在编译期就指定它的长度了。
可以,内建的阵列可以在执行期才指定第一个索引的□围。譬如说,和上一则 FAQ
相较,如果你只需要第一个维度大小能够变动,你可以 new 一个阵列的阵列(而不
是阵列指标的阵列 "an array of pointers to arrays"):
const unsigned ncols = 100;
//'ncols' 不是执行期才决定的变数 (用来代表阵列的行数)
class Fred { ... };
void manipulateArray(unsigned nrows)
//'nrows' 是执行期才决定的变数 (用来代表阵列的列数)
{
Fred (*matrix)[ncols] = new Fred[nrows][ncols];
//用 matrix[i][j] 来处理
//deletion 是物件配置的逆运算:
delete [] matrix;
}
如果你不光是需要在执行期改变阵列的第一个维度的话,就不能这样做了。
========================================
Q42:怎样确保某类别的物件都是用 "new" 建立的,而非区域或整体/静态变数?
确定该类别的建构子都是 "private:" 的,并定义个 "friend" 或 "static" 函数,
来传回一个指向由 "new" 建造出来的物件(把建构子设成 "protected:",如果你想
要有衍生类别的话)。
class Fred { //只允许 Fred 动态地配置出来
public:
static Fred* create() { return new Fred(); }
static Fred* create(int i) { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
private:
Fred();
Fred(int i);
Fred(const Fred& fred);
virtual ~Fred();
};
main()
{
Fred* p = Fred::create(5);
...
delete p;
}
===============================
■□ 第10节:除错与错误处理
===============================
Q43:怎样处理建构子的错误?
丢出一个例外(throw an exception)。
建构子没有传回值,所以不可能采用它传回的错误码。因此,侦测建构子错误最好的
方法,就是丢出一个例外。
在 C++ 编译器尚未提供例外处理之前,我们可先把物件置於「半熟」的状态(譬如
:设个内部的状态位元),用个查询子("inspector")来检查该位元,就可让用户
查看该物件是否还活著。也可以用另一个成员函数来检查该位元,若该物件没存活
下来,就做个「没动作」(或是更狠的像是 "abort()" )的程式。但这实在很丑陋。
========================================
Q44:如果建构子会丢出例外的话,该怎麽处理它的资源?
物件里面的每个资料成员,都该自己收拾残局。
如果建构子丢出一个例外的话,该物件的解构子就“不会”执行。如果你的物件得回
复些曾做过的事情(像是配置记忆体、开启档案、锁定 semaphore),该物件内的资
料成员就“必须”记住这个「必须恢复的东西」。
举例来说:不要单单的把配置到的记忆体放入 "Fred*" 资料成员,而要放入一个「
聪明的指标」(smart pointer) 资料成员中;当该“聪明指标”死掉的话,它的解构
子就会删去 Fred 物件。
【译注】「聪明的指标」(smart pointer) 在 Q4 中有提到一点。
=============================
■□ 第11节:Const 正确性
=============================
Q45:什麽是 "const correctness"?
好问题。
「常数正确性」乃使用 "const" 关键字,以确保常数物件不会被更动到。譬如:若
"f()" 函数接收一个 "String",且 "f()" 想确保 "String" 不会被改变,你可以:
* 传值呼叫 (pass by value): void f( String s ) { /*...*/ }
* 透过常数参考 (reference): void f(const String& s ) { /*...*/ }
* 透过常数指标 (pointer) : void f(const String* sptr) { /*...*/ }
* 但不能用非常数参考 : void f( String& s ) { /*...*/ }
* 也不能用非常数指标 : void f( String* sptr) { /*...*/ }
在接收 "const String&" 参数的函数里面,想更动到 "s" 的话,会产生个编译期的
错误;没有牺牲任何执行期的空间及速度。
宣告 "const" 参数也是另一种型别安全方法,就像一个常数字串,它会“丧失”各
种可能会变更其内容的行为动作。如果你发现型别安全性质让你的系统正确地运作
(这是真的;特别是大型的系统),你会发现「常数正确性」亦如是。
========================================
Q46:我该早一点还是晚一点让东西有常数正确性?
越越越早越好。
延後补以常数正确性,会导致雪球效应:每次你在「这儿」用了 "const",你就得在
「那儿」加上四个以上的 "const"。
========================================
Q47:什麽是「const 成员函数」?
一个只检测(而不更动)其物件的成员函数。
class Fred {
public:
void f() const;
}; // ^^^^^--- 暗示说 "fred.f()" 不会改变到 "fred"
此乃意指:「抽象层次」的(用户可见的)物件状态不被改变(而不是许诺:该物件
的「每一个位元内容」都不会被动到)。C++ 编译器不会对你许诺「每一个位元」这
种事情,因为不是常数的别名(alias)就可能会修改物件的状态(把 "const" 指标
黏上某个物件,并不能担保该物件不被改变;它只能担保该物件不会「被该指标的动
作」所改变)。
【译注】请逐字细读上面这句话。
"const" 成员函数常被称作「查询子」(inspector),不是 "const" 的成员函数则
称为「更动子」(mutator)。
========================================
Q48:若我想在 "const" 成员函数内更新一个「看不见的」资料成员,该怎麽做?
使用 "mutable" 或是 "const_cast"。
【译注】这是很新的 ANSI C++ RTTI (RunTime Type Information) 规定,Borland
C++ 4.0 就率先提供了 const_cast 运算子。
少数的查询子需要对资料成员做些无害的改变(譬如:"Set" 物件可能想快取它上一
回所查到的东西,以加速下一次的查询)。此改变「无害」是指:此改变不会由物件
的外部介面察觉出来(否则,该运作行为就该叫做更动子,而非查询子了)。
这类情况下,会被更动的资料成员就该被标示成 "mutable"(把 "mutable" 关键字
放在该资料成员宣告处前面;也就是和你放 "const" 一样的地方),这会告诉编译
器:此资料成员允许 const 成员函数改变之。若你不能用 "mutable" 的话,可以用
"const_cast" 把 "this" 的「常数性」给转型掉。譬如,在 "Set::lookup() const"
里,你可以说:
Set* self = const_cast(this);
这行执行之後,"self" 的位元内容就和 "this" 一样(譬如:"self==this"),但
是 "self" 是一个 "Set*" 而非 "const Set*" 了,所以你就可以用 "self" 去修改
"this" 指标所指向的物件。
========================================
Q49:"const_cast" 会不会丧失最佳化的可能?
理论上,是;实际上,否。
就算编译器没真正做好 "const_cast",欲避免 "const" 成员函数被呼叫时,会造成
暂存器快取区被清空的唯一方法,乃确保没有任何「非常数」的指标指向该物件。这
种情况很难得会发生(当物件在 const 成员函数被启用的□围内被建立出来;当所
有非 const 的成员函数在物件建立间启用,和 const 成员函数的启用被静态系结住
;当所有的启用也都是 "inline";当建构子本身就是 "inline";和当建构子所呼叫
的任何成员函数都是 inline 时)。
【译注】这一段话很难翻得好(好啦好啦!我功力不足... :-< ),所以附上原文:
Even if a compiler outlawed "const_cast", the only way to avoid flushing
the register cache across a "const" member function call would be to
ensure that there are no non-const pointers that alias the object. This
can only happen in rare cases (when the object is constructed in the scope
of the const member fn invocation, and when all the non-const member
function invocations between the object's construction and the const
member fn invocation are statically bound, and when every one of these
invocations is also "inline"d, and when the constructor itself is "inline"d,
and when any member fns the constructor calls are inline).
=====================
■□ 第12节:继承
=====================
Q50:「继承」对 C++ 来说很重要吗?
是的。
「继承」是抽象化资料型态(abstract data type, ADT)与 OOP 的一大分野。
========================================
Q51:何时该用继承?
做为一个「特异化」(specialization) 的机制。
人类以两种角度来抽象化事物:「部份」(part-of) 和「种类」(kind-of)。福特汽
车“是一种”(is-a-kind-of-a) 车子,福特汽车“有”(has-a) 引擎、轮胎……等
等零件。「部份」的层次随著 ADT 的流行,已成为软体系统的一份子了;而「继承
」则添入了“另一个”重要的软体分解角度。
========================================
Q52:怎样在 C++ 中表现出继承?
用 ": public" 语法:
class Car : public Vehicle {
//^^^^^^^^---- ": public" 读作「是一种」("is-a-kind-of-a")
//...
};
我们以几种方式来描述上面的关系:
* Car 是「一种」("a kind of a") Vehicle
* Car 乃「衍生自」("derived from") Vehicle
* Car 是个「特异化的」("a specialized") Vehicle
* Car 是 Vehicle 的「子类别」("subclass")
* Vehicle 是 Car 的「基底类别」("base class")
* Vehicle 是 Car 的「父类别」("superclass") (这不是 C++ 界常用的说法)
【译注】"superclass" 是 Smalltalk 语言的关键字。
========================================
Q53:把衍生类别的指标转型成指向它的基底,可以吗?
可以。
衍生类别是该基底类别的特异化版本(衍生者「是一种」("a-kind-of") 基底)。这
种向上的转换是绝对安全的,而且常常会发生(如果我指向一个汽车 Car,实际上我
是指向一个车子 Vehicle):
void f(Vehicle* v);
void g(Car* c) { f(c); } //绝对很安全;不需要转型
注意:在这里我们假设的是 "public" 的继承;後面会再提到「另一种」"private/
protected" 的继承。
========================================
Q54:Derived* --> Base* 是正常的;那为什麽 Derived** --> Base** 则否?
C++ 让 Derived* 能转型到 Base*,是因为衍生的物件「是一种」基底的物件。然而
想由 Derived** 转型到 Base** 则是错误的!要是能够的话,Base** 就可能会被解
参用(产生一个 Base*),该 Base* 就可能指向另一个“不一样的”衍生类别,这
是不对的。
照此看来,衍生类别的阵列就「不是一种」基底类别的阵列。在 Paradigm Shift 公
司的 C++ 训练课程里,我们用底下的例子来比喻:
"一袋苹果「不是」一袋水果".
"A bag of apples is NOT a bag of fruit".
如果一袋苹果可以当成一袋水果来传递,别人就可能把香蕉放到苹果袋里头去!
========================================
Q55:衍生类别的阵列「不是」基底的阵列,是否表示阵列不好?
没错,「阵列很烂」(开玩笑的 :-) 。
C++ 内建的阵列有一个不易察觉的问题。想一想:
void f(Base* arrayOfBase)
{
arrayOfBase[3].memberfn();
}
main()
{
Derived arrayOfDerived[10];
f(arrayOfDerived);
}
编译器认为这完全是型别安全的,因为由 Derived* 转换到 Base* 是正常的。但事
实上这很差劲:因为 Derived 可能会比 Base 还要大,f() 里头的阵列索引不光是
没有型别安全,甚至还可能没指到真正的物件呢!通常它会指到某个倒楣的
Derived 物件的中间去。
根本的问题在於:C++ 不能分辨出「指向一个东西」和「指向一个阵列」。很自然的
,这是 C++“继承”自 C 语言的特徵。
注意:如果我们用的是一个像阵列的「类别」而非最原始的阵列(譬如:"Array"
而非 "T[]"),这问题就可以在编译期被挑出来,而非在执行的时候。
==========================
● 12A:继承--虚拟函数
==========================
Q56:什麽是「虚拟成员函数」?
虚拟函数可让衍生的类别「取代」原基底类别所提供的运作。只要某物件是衍生出来
的,就算我们是透过基底物件的指标,而不是以衍生物件的指标来存取该物件,编译
器仍会确保「取代後」的成员函数被呼叫。这可让基底类别的演算法被衍生者所替换
,即使我们不知道衍生类别长什麽样子。
注意:衍生的类别亦可“部份”取代(覆盖,override)掉基底的运作行为(如有必
要,衍生类别的运作行为亦可呼叫它的基底类别版本)。
========================================
Q57:C++ 怎样同时做到动态系结和静态型别?
底下的讨论中,"ptr" 指的是「指标」或「参考」。
一个 ptr 有两种型态:静态的 ptr 型态,与动态的「被指向的物件」的型态(该物
件可能实际上是个由其他类别衍生出来的类别的 ptr)。
「静态型别」("static typing") 是指:该呼叫的「合法性」,是以 ptr 的静态型
别为侦测之依据,如果 ptr 的型别能处理成员函数,则「指向的物件」自然也能。
「动态系结」("dynamic binding") 是指:「程式码」呼叫是以「被指向的物件」之
型态为依据。被称为「动态系结」,是因为真正会被呼叫的程式码是动态地(於执行
时期)决定的。
========================================
Q58:衍生类别能否将基底类别的非虚拟函数覆盖(override)过去?
可以,但不好。
C++ 的老手有时会重新定义非虚拟的函数,以提升效率(换一种可能会运用到衍生类
别才有的资源的作法),或是用以避开遮蔽效应(hiding rule,底下会提,或是看
看 ARM ["Annotated Reference Manual"] sect.13.1),但是用户的可见性效果必
须完全相同,因为非虚拟的函数是以指标/参考的静态型别为分派(dispatch)的依
据,而非以指到的/被参考到的物件之动态型别来决定。
========================================
Q59:"Warning: Derived::f(int) hides Base::f(float)" 是什麽意思?
这是指:你死不了的。
你出的问题是:如果 Derived 宣告了个叫做 "f" 的成员函数,Base 却早已宣告了
个不同型态签名型式(譬如:参数型态或是 const 不同)的 "f",这样子 Base "f"
就会被「遮蔽 hide」住,而不是被「多载 overload」或「覆盖 override」(即使
Base "f" 已经是虚拟的了)。
解决法:Derived 要替 Base 被遮蔽的成员函数重新定义(就算它不是虚拟的)。通
常重定义的函数,仅仅是去呼叫合适的 Base 成员函数,譬如:
class Base {
public:
void f(int);
};
class Derived : public Base {
public:
void f(double);
void f(int i) { Base::f(i); }
}; // ^^^^^^^^^^--- 重定义的函数只是去呼叫 Base::f(int)
========================
● 12B:继承--一致性
========================
Q60:我该遮蔽住由基底类别继承来的公共成员函数吗?
绝对绝对绝对绝对不要这样做!
想去遮蔽(删去、撤消)掉继承下来的公共成员函数,是个很常见的错误。这通常是
脑袋塞满了浆糊的人才会做的傻事。
========================================
Q61:圆形 "Circle" 是一种椭圆 "Ellipse" 吗?
若椭圆能够不对称地改变其两轴的大小,则答案就是否定的。
比方说,椭圆有个 "setSize(x,y)" 的运作行为,且它保证说「椭圆的 width() 为
x,height() 为 y」。这种情况之下,正圆形就不能算是一种椭圆。因为只要把某个
椭圆能做而正圆形不能的东西放进去,圆形就不再是个椭圆了。
这样一来,圆和椭圆之间可能有两种的(合法)关系:
* 将圆与椭圆完全分开来谈。
* 让圆及椭圆都同时自一个基底衍生出来,该基底为「不能做不对称的 setSize
运作的特殊椭圆形」。
以第一个方案而言,椭圆可继承自「非对称图形」(伴随著一个 setSize(x,y) ),
圆形则继承自「对称图形」,带有一个 setSize(size) 成员函数。
第二个方案中,可让卵形 "Oval" 类别有个 "setSize(size)":将 "width()" 和
"height()" 都设成 "size",然後让椭圆和圆形都自卵形中衍生出来。椭圆(而不是
正圆形)会加入一个 "setSize(x,y)" 运算(如果这个 "setSize()" 运作行为的名
称重复了,就得注意前面提过的「遮蔽效应」)。
========================================
Q62:对「圆形是/不是一种椭圆」这两难问题,有没有其他说法?
如果你说:椭圆都可以不对称地挤压,又说:圆形是一种椭圆,又说:圆形不能不对
称地挤压下去,那麽很明显的,你说过的某句话要做修正(老实说,该取消掉)。所
以你不是得去掉 "Ellipse::setSize(x,y)",去掉圆形和椭圆间的继承关系,就是得
承认你的「圆形」不一定是正圆。
这儿有两个 OO/C++ 新手最易落入的陷阱。他们想用程式小技巧来弥补差劲的事前设
计(他们重新定义 Circle::setSize(x,y),让它丢出一个例外,呼叫 "abort()" ,
或是选用两参数的平均数,或是不做任何事情),不幸的,这些技俩都会让使用者感
到吃惊:他们原本都预期 "width() == x" 和 "height() == y" 这两个事实会成立。
唯一合理的做法似乎是:降低椭圆形 "setSize(x,y)" 的保证事项(譬如,你可以改
成:「这运作行为“可能”会把 width() 设成 x、height() 设成 y,也可能“不做
任何事”」)。不幸的,这样会把界限冲淡,因为使用者没有任何有意义的物件行为
足以依靠,整个类别阶层也就无毫价值可言了(很难说服别人去用一个:问你说它是
做什麽的,你却只会耸耸肩膀说不知道的物件)。
==========================
● 12C:继承--存取规则
==========================
Q63:为什麽衍生的类别无法存取基底的 "private" 东西?
让你不被基底类别将来的改变所影响。
衍生类别不能存取到基底的私有(private)成员,它有效地把衍生类别「封住」,
基底类别内的私有成员如有改变,也不会影响到衍生的类别。
========================================
Q64:"public:"、"private:"、"protected:" 的差别是?
"Private:" 在前几节中讨论过了;"public:" 是指:「任何人都能存取之」;第三
个 "protected:" 是让某成员(资料成员或是成员函数)只能由衍生类别存取之。
【译注】"protected:" 是让「衍生类别」,而非让「衍生类别的物件案例」能存取
得到 protected 的部份。
========================================
Q65:当我改变了内部的东西,怎样避免子类别被破坏?
物件类别有两个不同的介面,提供给不同种类的用户:
* "public:" 介面用以服务不相关的类别。
* "protected:" 介面用以服务衍生的类别。
除非你预期所有的子类别都会由你们的工作小组建出来,否则你应该将基底类别的资
料位元内容放在 "private:" 处,用 "protected:" 行内存取函数来存取那些资料。
这样的话,即使基底类别的私有资料改变了,衍生类别的程式也不会报废,除非你改
变了基底类别的 protected 处的存取函数。
================================
● 12D:继承--建构子与解构子
================================
Q66:若基底类别的建构子呼叫一个虚拟函数,为什麽衍生类别覆盖掉的那个虚拟函
数却不会被呼叫到?
在基底类别 Base 的建构子执行过程中,该物件还不是属於衍生 Derived 的,所以
如果 "Base::Base()" 呼叫了虚拟函数 "virt()",则 "Base::virt()" 会被呼叫,
即使真的有 "Derived::virt()"。
类似的道理,当 Base 的解构子执行时,该物件不再是个 Derived 了,所以当
Base::~Base() 呼叫 "virt()",则 "Base::virt()" 会被执行,而非覆盖後的版本
"Derived::virt()"。
当你想像到:如果 "Derived::virt()" 碰得到 Derived 类别的物件成员,会造成什
麽样的灾难,你很快就会看出这规则的明智之处。
================================
Q67:衍生类别的解构子应该外显地呼叫基底的解构子吗?
不要,绝对不要外显地呼叫解构子(「绝对不要」指的是「几乎完全不要」)。
衍生类别的解构子(不管你是否明显定义过)会“自动”去呼叫成员物件的、以及基
底类别之子物件的解构子。成员物件会以它们在类别中出现的相反顺序解构,接下来
是基底类别的子物件,以它们出现在类别基底列表的相反顺序解构之。
只有在极为特殊的情况下,你才应外显地呼叫解构子,像是:解构一个由「新放入的
new 运算子」配置的物件。
===========================================
● 12E:继承--Private 与 protected 继承
===========================================
Q68:该怎麽表达出「私有继承」(private inheritance)?
用 ": private" 来代替 ": public." 譬如:
class Foo : private Bar {
//...
};
================================
Q69:「私有继承」和「成份」(composition) 有多类似?
私有继承是「成份」(has-a) 的一种语法变形。
譬如:「汽车有引擎」("car has-a engine") 关系可用成份来表达:
class Engine {
public:
Engine(int numCylinders);
void start(); //starts this Engine
};
class Car {
public:
Car() : e_(8) { } //initializes this Car with 8 cylinders
void start() { e_.start(); } //start this Car by starting its engine
private:
Engine e_;
};
同样的 "has-a" 关系也可用私有继承来表达:
class Car : private Engine {
public:
Car() : Engine(8) { } //initializes this Car with 8 cylinders
Engine::start; //start this Car by starting its engine
};
这两种型式的成份有几分相似性:
* 这两种情况之下,Car 只含有一个 Engine 成员物件。
* 两种情况都不能让(外界)使用者由 Car* 转换成 Engine* 。
也有几个不同点:
* 如果你想要让每个 Car 都含有数个 Engine 的话,就得用第一个型式。
* 第二个型式可能会导致不必要的多重继承(multiple inheritance)。
* 第二个型式允许 Car 的成员从 Car* 转换成 Engine* 。
* 第二个型式可存取到基底类别的 "protected" 成员。
* 第二个型式允许 Car 覆盖掉 Engine 的虚拟函数。
注意:私有继承通常是用来获得基底类别 "protected:" 成员的存取权力,但这通常
只是个短程的解决方案。
========================================
Q70:我比较该用哪一种:成份还是私有继承?
成份。
正常情形下,你不希望存取到太多其他类别的内部,但私有继承会给你这些额外的权
力(与责任)。不过私有继承不是洪水猛兽;它只是得多花心力去维护罢了,因为它
增加了别人动到你的东西、让你的程式出差错的机会。
合法而长程地使用私有继承的时机是:当你想新建一个 Fred 类别,它会用到 Wilma
类别的程式码,而且 Wilma 的程式码也会呼叫到你这个 Fred 类别里的运作行为时
。这种情形之下,Fred 呼叫了 Wilma 的非虚拟函数,Wilma 也呼叫了它自己的、会
被 Fred 所覆盖的虚拟函数(通常是纯虚拟函数)。要用成份来做的话,太难了。
class Wilma {
protected:
void fredCallsWilma()
{ cout << "Wilma::fredCallsWilma()\n"; wilmaCallsFred(); }
virtual void wilmaCallsFred() = 0;
};
class Fred : private Wilma {
public:
void barney()
{ cout << "Fred::barney()\n"; Wilma::fredCallsWilma(); }
protected:
virtual void wilmaCallsFred()
{ cout << "Fred::wilmaCallsFred()\n"; }
};
========================================
Q71:我应该用指标转型方法,把「私有」衍生类别转成它的基底吗?
当然不该。
以私有衍生类别的运作行为、夥伴来看,从它上溯到基底类别的关系为已知的,所以
从 PrivatelyDer* 往上转换成 Base*(或是从 PrivatelyDer& 到 Base&)是安全的
;强制转型是不需要也不鼓励的。
然而用 PrivateDer 的人应该避免这种不安全的转换,因为此乃立足於 PrivateDer
的 "private" 决定,这个决定很容易在日後不经察觉就改变了。
========================================
Q72:保护继承 (protected inheritance) 和私有继承有何关连?
相似处:两者都能覆盖掉私有/保护基底类别的虚拟函数,两者都不把衍生的类别视
为“一种”基底类别。
不相似处:保护继承可让衍生类别的衍生类别知道它的继承关系(把实行细节显现出
来)。它有好处(允许保护继承类别的子类别,藉这项关系来使用保护基底类别),
也有代价(保护继承的类别,无法既想改变这种关系,而又不破坏到进一步的衍生类
别)。
保护继承使用 ": protected" 这种语法:
class Car : protected Engine {
//...
};
========================================
Q73:"private" 和 "protected" 的存取规则是什麽?
拿底下这些类别当例子:
class B { /*...*/ };
class D_priv : private B { /*...*/ };
class D_prot : protected B { /*...*/ };
class D_publ : public B { /*...*/ };
class UserClass { B b; /*...*/ };
没有一个子类别能存取到 B 的 private 部份。
在 D_priv 内,B 的 public 和 protected 部份都变成 "private"。
在 D_prot 内,B 的 public 和 protected 部份都变成 "protected"。
在 D_publ 内,B 的 public 部份还是 public,protected 还是 protected
(D_publ is-a-kind-of-a B) 。
Class "UserClass" 只能存取 B 的 public 部份,也就是:把 UserClass 从 B 那
儿封起来了。
欲把 B 的 public 成员在 D_priv 或 D_prot 内也变成 public,只要在该成员的名
字前面加上 "B::"。譬如:想让 "B::f(int,float)" 成员在 D_prot 内也是 public
的话,照这样写:
class D_prot : protected B {
public:
B::f; //注意:不是写成 "B::f(int,float)"
};
======================================
■□ 第13节:抽象化(abstraction)
======================================
Q74:分离介面与实作是做什麽用的?
介面是企业体最有价值的资源。设计介面会比只把一堆独立的类别拼凑起来来得耗时
,尤其是:介面需要花费更高阶人力的时间。
既然介面是如此重要,它就应该保护起来,以避免被资料结构等等实作细节之变更所
影响。因此你应该将介面与实作分离开来。
========================================
Q75:在 C++ 里,我该怎样分离介面与实作(像 Modula-2 那样)?
用 ABC(见下一则 FAQ)。
========================================
Q76:ABC ("abstract base class") 是什麽?
在设计层面,ABC 对应到抽象的概念。如果你问机械师父说他修不修运输工具,他可
能会猜你心中想的到底是“哪一种”运输工具,他可能不会修理太空梭、轮船、脚踏
车、核子潜艇。问题在於:「运输工具」是个抽象的概念(譬如:你建不出一辆「运
输工具」,除非你知道要建的是“哪一种”)。在 C++,运输工具类别可当成是一个
ABC,而脚踏车、太空梭……等等都当做它的子类别(轮船“是一种”运输工具)。
在真实世界的 OOP 中,ABC 观念到处都是。
在程式语言层面,ABC 是有一个以上纯虚拟成员函数(pure virtual)的类别(详见
下一则 FAQ),你无法替一个 ABC 建造出物件(案例)来。
========================================
Q77:「纯虚拟」(pure virtual) 成员函数是什麽?
ABC 的某种成员函数,你只能在衍生的类别中实作它。
有些成员函数只存於观念中,没有任何实质的定义。譬如,假设我要你画个 Shape,
它位於 (x,y),大小为 7。你会问我「我该画哪一种 shape?」(圆、方、六边……
都有不同的画法。)在 C++ 里,我们可以先标出有一个叫做 "draw()" 这样的运作
行为,且规定它只能(逻辑上)在子类别中定义出来:
class Shape {
public:
virtual void draw() const = 0;
//... ^^^--- "= 0" 指:它是 "pure virtual"
};
此纯虚拟函数让 "Shape" 变成一个 ABC。若你愿意,你可以把 "= 0" 语法想成是:
该程式码是位於 NULL 指标处。因此,"Shape" 提供一个服务项目,但它现在尚无法
提供实质的程式码以实现之。这样会确保:任何由 Shape 衍生出的 [具体的] 类别
之物件,“将会”有那个我们事先规定的成员函数,即使基底类别尚无足够的资讯去
真正的“定义”它。
【译注】此处「定义」、「宣告」二词要分辨清楚!
========================================
Q78:怎样替整个类别阶层提供列印的功能?
提供一个 friend operator<< 去呼叫 protected 的虚拟函数:
class Base {
public:
friend ostream& operator<< (ostream& o, const Base& b)
{ b.print(o); return o; }
//...
protected:
virtual void print(ostream& o) const; //或 "=0;" 若 "Base" 是个 ABC
};
class Derived : public Base {
protected:
virtual void print(ostream& o) const;
};
这样子所有 Base 的子类别只须提供它们自己的 "print(ostream&) const" 成员函
数即可(它们都共用 "<<" operator)。这种技巧让夥伴像是有了动态系结的能力。
========================================
Q79:何时该把解构子弄成 virtual?
当你可能经由基底的指标去 "delete" 掉衍生的类别时。
虚拟函数把某物件所属之真正类别所附的程式码,而非该指标/参考本身之类别所附
的程式给系结上去。 当你说 "delete basePtr",且它的基底有虚拟解构子的话,则
真正会被呼叫到的解构子,就是 *basePtr 物件之型态所属的解构子,而不是该指标
本身之型态所附的解构子。一般说来这的确是一件好事。
让你方便起见,你唯一不必将某类别的解构子设为 virtual 的场合是:「该类别“
没有”任何虚拟函数」。因为加入第一个虚拟函数,就会替每个物件都添加额外的空
间负担(通常是一个机器 word 的大小),这正是编译器实作出动态系结的□密;它
通常会替每个物件加入额外的指标,称为「虚拟指标表格」(virtual table pointer)
,或是 "vptr" 。
========================================
Q80:虚拟建构子 (virtual constructor) 是什麽?
一种让你能做些 C++ 不直接支援的事情之惯用法。
欲做出虚拟建构子的效果,可用个虚拟的 "createCopy()" 成员函数(用来做为拷贝
建构子),或是虚拟的 "createSimilar()" 成员函数(用来做为预设建构子)。
class Shape {
public:
virtual ~Shape() { } //详见 "virtual destructors"
virtual void draw() = 0;
virtual void move() = 0;
//...
virtual Shape* createCopy() const = 0;
virtual Shape* createSimilar() const = 0;
};
class Circle : public Shape {
public:
Circle* createCopy() const { return new Circle(*this); }
Circle* createSimilar() const { return new Circle(); }
//...
};
执行了 "Circle(*this)" 也就是执行了拷贝建构的行为(在这些运作行为中,
"*this" 的型态为 "const Circle&")。"createSimilar()" 亦类似,但它乃建构出
一个“预设的”Circle。
这样用的话,就如同有了「虚拟建构子」(virtual constructors):
void userCode(Shape& s)
{
Shape* s2 = s.createCopy();
Shape* s3 = s.createSimilar();
//...
delete s2; // 该解构子必须是 virtual 才行!!
delete s3; // 如上.
}
不论该 Shape 是 Circle、Square,甚或其他还不存在的 Shape 种类,这函数都能
正确执行。
本文地址:http://com.8s8s.com/it/it29961.htm