C++語言常見問題解
== Part 4/4 ============================
comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 4 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc
=======================================
■□ 第17節:和 C 連結/和 C 的關係
=======================================
Q105:怎樣從 C++ 中呼叫 C 的函數 "f(int,char,float)"?
告訴 C++ 編譯器說:它是個 C 的函數:
extern "C" void f(int,char,float);
確定你有 include 進來完整的函數原型 (function prototype)。一堆 C 的函數可
以用大括號框起來,如下:
extern "C" {
void* malloc(size_t);
char* strcpy(char* dest, const char* src);
int printf(const char* fmt, ...);
}
========================================
Q106:怎樣才能建一個 C++ 函數 "f(int,char,float)",又能被 C 呼叫?
想讓 C++ 編譯器知道 "f(int,char,float)" 會被 C 編譯器用到的話,就要用到前
一則 FAQ 已詳述的 "extern C" 語法。接着在 C++ 模塊內定義該函數:
void f(int x, char y, float z)
{
//...
}
"extern C" 一行會告訴編譯器:送到 linker 的外部信息要採用 C 的呼叫慣例及籤
名編碼法(譬如,前置一個底線)。既然 C 沒有多載名稱的能力,你就不能讓 C 程
式能同時呼叫得到多載的函數羣。
警告以及實作相關事項:
* 你的 "main()" 應該用 C++ 編譯之(爲了靜態對象的初始化)。
* 你的 C++ 編譯器應該能設定連結的程序(爲某些特殊的鏈接庫)。
* 你的 C 和 C++ 編譯器可能要是同一個牌子的,而且是兼容的版本(亦即:有相
同的呼叫慣例等等)。
========================================
Q107:爲什麼 linker 有這種錯誤訊息:C/C++ 函數被 C/C++ 函數呼叫到?
看前兩則 FAQs 關於 extern "C" 的使用。
========================================
Q108:該怎麼把 C++ 類別的對象傳給/傳自 C 的函數?
例子:
/****** C/C++ header file: Fred.h ******/
#ifdef __cplusplus /*"__cplusplus" is #defined if/only-if
compiler is C++*/
extern "C" {
#endif
#ifdef __STDC__
extern void c_fn(struct Fred*); /* ANSI-C prototypes */
extern struct Fred* cplusplus_callback_fn(struct Fred*);
#else
extern void c_fn(); /* K&R style */
extern struct Fred* cplusplus_callback_fn();
#endif
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
class Fred {
public:
Fred();
void wilma(int);
private:
int a_;
};
#endif
"Fred.C" 是個 C++ 模塊:
#include "Fred.h"
Fred::Fred() : a_(0) { }
void Fred::wilma(int a) : a_(a) { }
Fred* cplusplus_callback_fn(Fred* fred)
{
fred->wilma(123);
return fred;
}
"main.C" 是個 C++ 模塊:
#include "Fred.h"
int main()
{
Fred fred;
c_fn(&fred);
return 0;
}
"c-fn.c" 是個 C 模塊:
#include "Fred.h"
void c_fn(struct Fred* fred)
{
cplusplus_callback_fn(fred);
}
把指向 C++ 對象的指針傳到/傳自 C 的函數,如果傳出與收回的指針不是“完全相
同”的話,就會失敗。譬如,不要傳出一個基底類別的指針卻收回一個衍生類別的指
標,因爲 C 編譯器不懂該怎麼對多重及虛擬繼承的指針做轉型。
========================================
Q109:C 的函數能不能存取 C++ 類別的對象資料?
有時可以。
(請先讀一讀前一則關於和 C 函數間傳遞 C++ 對象的 FAQ。)
你可以安全地從 C 函數中存取 C++ 對象的資料,只要 C++ 的對象類別:
* 沒有虛擬函數(包含繼承下來的虛擬函數).
* 所有資料都在同一個存取等級中 (private/protected/public).
* 完全被包含的子對象中也都沒有虛擬函數.
如果 C++ 類別有任何基底類別(或是任何被完全包含的子對象中有基底類別)的話
,技術上來說,存取該資料沒有可移植性的,因爲語言沒規定在繼承之下的類別配置是
什麼樣子。不過經驗上,所有 C++ 編譯器的做法都一樣:基底類別對象先出現(在
多重繼承之下,則由左到右排列之),子對象次之。
還有,如果類別(或是任何基底類別)含有任何虛擬函數,你時常可以(但不是一直
都可以)假設有一個 "void*" 出現在對象第一個虛擬函數之所在,或是在該對象的
第一個 word 那裏。同樣的,語言對它也沒規定到,但這似乎是「大家」都採取的做
法。
如果該類別有任何虛擬基底類別,情況會更復雜而且更沒有可移植性。常見的做法是:
讓對象最後才包含基底類別之對象 (V)(不管 "V" 在繼承階層中在哪兒出現),物
件的其它部份則以正常的次序出現。每個有 V 這個虛擬基底類別的衍生類別,實際
上都有個“指針”指向最後一個對象的 V 的部份。
========================================
Q110:爲什麼我總覺得 C++ 讓我「離機器更遠了」,不像 C 那樣?
因爲事實上正是如此。
做爲一個 OOPL,C++ 讓你以該問題的領域來思考,讓你以問題領域的語言來設計程
式,而非以解題的領域來着手。
一個 C 最強的地方是:它沒有「隱藏的機制」:你看到的就是你得到的,你可以一
邊閱讀 C 的程序,一邊「看到」每個系統時脈。C++ 則不然; C 的老手(像從前的
我們)對這種特性常會有矛盾的心理(或是說「敵視」),但是很快的他們會發現:
C++ 提供了抽象化的層次及經濟的表現能力,大大降低維護成本,又不會損及執行效
率。
很自然的,用任何語言都會寫出壞程序;C++ 並不會確保任何高品質﹑可重用性﹑抽
象化,或是任何「正字標記」的品質因子。C++ 不會讓差勁的程序者寫不出差勁的程
式;她只是協助明智的發展者做出高人一等的軟件。
===================================
■□ 第18節:指向成員函數的指針
===================================
Q111:「指向成員函數的指針」和「指到函數的指針」的型態有差別嗎?
是的。
考慮底下的函數:
int f(char a, float b);
如果它是普通的函數,它的型態是: int (*) (char,float);
如果它是 Fred 類別的運作行爲,它的型態是: int (Fred::*)(char,float);
========================================
Q112:怎樣把指向成員函數的指針傳給 signal handler﹑X event callback 等等?
【譯註】這是和 UNIX﹑X Window System 相關的問題,但其它系統亦可推而廣之。
不要這樣做。
因爲若無對象去激活它,成員函數是無意義的,你不能直接使用它(如果 X 窗口系
統是用 C++ 寫的話,或許就可以直接傳對象的參考值了,而不光是傳個指向函數的
指針;自然地,對象會包含所有要用到的函數,甚至更多)。
若想修改現有的軟件,可拿最頂層的(非成員的)函數當作一層包裝 (wrapper),透
過其它技巧(或許是放在全域變量中),把該對象包起來。這個最頂層的函數,會透
過適當的成員函數去使用該全域變量。
譬如,你想在中斷處理中呼叫 Fred::memfn() 的話:
class Fred {
public:
void memfn();
static void staticmemfn(); // 用個 static 成員函數就行了
//...
};
//wrapper 函數會記得哪個對象該去激活全域對象的成員函數:
Fred* object_which_will_handle_signal;
void Fred_memfn_wrapper() { object_which_will_handle_signal->memfn(); }
main()
{
/* signal(SIGINT, Fred::memfn); */ //不能這樣做
signal(SIGINT, Fred_memfn_wrapper); //Ok
signal(SIGINT, Fred::staticmemfn); //Also Ok
}
注意:靜態成員函數不需要真正的對象才能激活,所以指向靜態成員函數的指針,和
普通的指向函數的指針,具有兼容的型態(詳見 ARM ["Annotated Reference
Manual"] p.25, 158)。
========================================
Q113:當我想以成員函數做爲中斷服務例程 (ISR) 時,爲什麼編譯器產生(型態不
符)的錯誤?
這是前兩個問題的特例,所以請先看看前兩則解答。
非靜態的成員函數,都有一個隱藏的參數,對應到 'this' 指針,該 'this' 指針會
指向該對象的案例資料 (instance data),可是系統中斷的硬件/韌體並未提供這個
'this' 參數。你得用「正常的」函數(不是類別的成員)或是靜態成員函數來做爲
中斷服務例程才行。
一個可行的解法是:用一個靜態成員做爲中斷服務例程,讓它能自己到某處去找案例
/成員的配對,以供中斷呼叫之用。這麼一來,當中斷產生時,正常的 method 就會
被激活,不過以技術觀點來看,你得先呼叫一箇中介函數。
========================================
Q114:爲什麼我取不出 C++ 函數的地址?
這可由前一則 FAQ 推論過來。
詳細的解答:在 C++ 裏,成員函數有一個隱含的參數,指向該對象本身(成員函數
內的 "this" 指針)。正常的 C 函數與成員函數的呼叫慣例可視爲不同,所以它們
指針的型態(指向成員函數 vs 指向函數)既不同也不兼容。C++ 引進一個新的指針
型態:指向成員的指針,要提供一個對象才能激活之(見 ARM ["Annotated
Reference Manual"] 5.5)。
注意:不要去把指向成員函數的指針強制轉型成指向函數的指針;這樣做的結果是未
定義的,且下場可能會很慘。譬如,指向成員函數的指針,“不必然”會包含某正常
函數的機器地址(看 ARM, 8.1.2c, p.158)。如前例所提,如果你有個指向正常 C
函數的指針的話,請用上層的(非成員的)函數,或是用 "static" 成員函數(類別
成員函數)。
========================================
Q115:怎樣宣告指向成員函數的指針數組?
用 "typedef" 好讓你的腦筋保持清醒。
class Fred {
public:
int f(char x, float y);
int g(char x, float y);
int h(char x, float y);
int i(char x, float y);
//...
};
typedef int (Fred::*FredPtr)(char x, float y);
這是指向成員函數的指針數組:Here's the array of pointers to member functions:
FredPtr a[4] = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };
呼叫對象 "fred" 的某一個成員函數:
void userCode(Fred& fred, int methodNum, char x, float y)
{
//假設 "methodNum" 在 [0,3] 區間內
(fred.*a[methodNum])(x, y);
}
你可以用 #define 讓這個呼叫清楚些:
#define callMethod(object,ptrToMethod) ((object).*(ptrToMethod))
callMethod(fred, a[methodNum]) (x, y);
====================================
■□ 第19節:容器類別與 template
====================================
Q116:怎樣自一個連結串行/雜湊表等等裏面,插入/存取/改變元素?
我將以最簡單的「插入連結串行」爲例。想把元素插入串行的頭尾很容易,但只限
於這些功能的話,會使鏈接庫過於低能(太低能的鏈接庫比沒有更糟)。
完整的解答會讓 C++ 新手消化不良,所以我只提幾個項目。第一個是最簡單的,第
二和第三是比較好的。
[1] 替 "List" 加入一個「現在位置」的性質,加入像是 advance()﹑backup()﹑
atEnd()﹑atBegin()﹑getCurrElem()﹑setCurrElem(Elem)﹑insertElem(Elem)
﹑removeElem() 等等的運作行爲。
即使在這個小例子裏已經夠用了,但「只有一個」現在位置的記號的話,想存取
串行中兩個以上位置的元素就不太容易(譬如:「對所有 x,y 序對,做底下的
事情……」)。
[2] 把上述的 List 運作行爲拿掉,移到獨立的類別 "ListPosition" 中。
ListPosition 的作用是:代表 List 裏「現在的位置」,這樣就允許許多位置
並存於同一個串行中。ListPosition 是 List 的夥伴,所以 List 的內部可對
外界隱藏起來(否則 List 的內部就會被它的公共運作行爲所公開)。注意:
ListPosition 可以把運算子多載起來,像是 advance()、backup(),因爲運算
子多載只是正常運作行爲的語法糖衣而已。
[3] 把整個位置處理(iteration)當成是一個基元事件(atomic event),建一個
class template 去涵蓋該事件。
它不會在內部循環中使用公共存取運作行爲(它有可能是虛擬函數),所以效率
能增進。不幸的,你的應用軟件會多出些額外的二元碼,因爲 template 是以空
間換取時間的。欲知詳情,請見 [Koenig, "Templates as interfaces,"
JOOP, 4, 5 (Sept 91)], 以及 [Stroustrup, "The C++ Programming Language
Second Edition," under "Comparator"].
========================================
Q117:「樣版」(template)的用意是什麼?
Template 本意是個壓餅模子,它把餅乾都壓成差不多一個樣子(雖然餅乾的原料不
盡相同,但它們都有相同的基本外形)。同理,class template 是個樣版模子,用
來描述如何將一系列的對象類別弄成同一個基本型式;而 function template 則是
用以描述一系列看起來差不多的函數。
Class template 常用於製造型別安全的容器(即使這僅止於「如何使用它」而已)。
========================================
Q118:"function template" 的語法/語意是什麼?
考慮底下這個交換兩個整數自變量的函數:
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
假如我們想交換 float、long、String、Set 和 FileSystems,我們還得寫那些
大致看起來都一樣、只有型態不同的程序代碼,有夠煩人。這種不花腦筋的重複性工作
,正是計算機的專長,於是我們想出了 function template:
template<class T>
void swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
每次我們以一組型別來使用 "swap()",編譯器會找到上面這定義,並造出另一個
"template function" ,來當作它的「案例」(instantiation)。譬如:
main()
{
int i,j; /*...*/ swap(i,j); // 案例化 "int" 的 swap
float a,b; /*...*/ swap(a,b); // 案例化 "float" 的 swap
char c,d; /*...*/ swap(c,d); // 案例化 "char" 的 swap
String s,t; /*...*/ swap(s,t); // 案例化 "String" 的 swap
}
(注意:"template function" 是 "function template" 實體化之後的案例。)
========================================
Q119:"class template" 的語法/語意是什麼?
考慮像是個整數數組的容器類別:
// 這會放在像是 "Array.h" 的標頭檔中
class Array {
public:
Array(int len=10) : len_(len), data_(new int[len]){}
~Array() { delete [] data_; }
int len() const { return len_; }
const int& operator[](int i) const { data_[check(i)]; }
int& operator[](int i) { data_[check(i)]; }
Array(const Array&);
Array& operator= (const Array&);
private:
int len_;
int* data_;
int check(int i) const
{ if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
return i; }
};
如同前述的 "swap()" ,一再爲 float、char、String、Array-of-String 等等來重
復設計 Array 類別,是很煩人的。
// 這會放在像是 "Array.h" 的標頭檔中
template<class T>
class Array {
public:
Array(int len=10) : len_(len), data_(new T[len]) { }
~Array() { delete [] data_; }
int len() const { return len_; }
const T& operator[](int i) const { data_[check(i)]; }
T& operator[](int i) { data_[check(i)]; }
Array(const Array<T>&);
Array& operator= (const Array<T>&);
private:
int len_;
T* data_;
int check(int i) const
{ if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
return i; }
};
不像 template function 那樣,template classes(案例化的 class template)必
須將那些用來案例化的參數型態明示出來:
main()
{
Array<int> ai;
Array<float> af;
Array<char*> ac;
Array<String> as;
Array< Array<int> > aai;
} // ^^^-- 注意這空格;不要用 "Array<Array<int>>"
// (編譯器會把 ">>" 看成單一的元素)
========================================
Q120:什麼是「參數化型別」(parameterized type)?
另一種 "class template" 的說法。
「參數化型別」是一種型別,它被另一個型別或數值所參數化(parameterized)了。
像 List<int> 是一個型別 ("List") ,它被另一個型別 ("int") 所參數化。