C++語言常見問題解:#33 ~ #53

這是我從臺灣的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/發現的《C++ Frequently Asked Questions》的繁體翻譯,作者是:葉秉哲,也是《C++ Programming Language》3/e繁體版的譯者,該文章是非常的好,出於學習用途而將它轉貼,本人未取得作者的授權,原文章的版權仍然歸屬原作者.

C++語言常見問題解
== Part 2/4 ============================

comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 2 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc

=============================
■□ 第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<Set*>(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" 的繼承。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章