C++語言常見問題解:#105 ~ #120

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

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") 所參數化。

發佈了26 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章