C和C++指針的最重要的區別在于:C++是一種類(lèi)型要求更強的語(yǔ)言。就void *而言,這一點(diǎn)表現得更加突出。C雖然不允許隨便地把一個(gè)類(lèi)型的指針指派給另一個(gè)類(lèi)型,但允許通過(guò)void *來(lái)實(shí)現。例如:
bird* b;rock* r;void* v;v = r;b = v;
C++不允許這樣做,其編譯器將會(huì )給出一個(gè)出錯信息。如果真的想這樣做,必須顯式地使用映射,通知編譯器和讀者。
參數傳遞準則
當給函數傳遞參數時(shí),人們習慣上應該是通過(guò)常量引用來(lái)傳遞,這種簡(jiǎn)單習慣可以大大提高效率:傳值方式需要調用構造函數和析構函數,然而如果不想改變參數,則可通過(guò)常量引用傳遞,它僅需要將地址壓棧。 事實(shí)上,只有一種情況不適合用傳遞地址方式,這就是當傳值是唯一安全的途徑,否則將會(huì )破壞對象(而不是修改外部對象,這不是調用者通常期望的)。
C++訪(fǎng)問(wèn)權限控制:public、private、protected
其中protected只有在繼承中才有不同含義,否則與private相同,也就是說(shuō)兩者只有一點(diǎn)不同:繼承的結構可以訪(fǎng)問(wèn)protected成員,但不能訪(fǎng)問(wèn)private成員。
前置聲明注意
struct X; // Declaration(incomplete type spec)struct Y{void f(X *memx);void g(X memx); // not allowed, the size of X is unknown.};
這里f(X*)引用了一個(gè)X對象的地址,這是沒(méi)有任何問(wèn)題的,但如果是void g(X memx);就不行了,編譯器會(huì )報錯。這一點(diǎn)很關(guān)鍵,因為編譯器知道如何傳遞一個(gè)地址,這一地址大小是一定的,而不用管被傳遞的對象類(lèi)型大小。如果試圖傳遞整個(gè)對象,編譯器就必須知道X的全部定義以確定它的大小以及如何傳遞它,這就使程序員無(wú)法聲明一個(gè)類(lèi)似于Y :: g(X) 的函數。
C++是純的嗎?
如果某個(gè)類(lèi)的一個(gè)函數被聲明為friend,就意味著(zhù)它不是這個(gè)類(lèi)的成員函數,但卻可以修改類(lèi)的私有成員, 而且它必須被列在類(lèi)的定義中,因此我們可以認為它是一個(gè)特權函數。這種類(lèi)的定義提供了有關(guān)權限的信息,我們可以知道哪些函數可以改變類(lèi)的私有部分。 因此,C++不是完全的面向對象語(yǔ)言,它只是一個(gè)混合產(chǎn)品。friend關(guān)鍵字就是用來(lái)解決部分的突發(fā)問(wèn)題。它也說(shuō)明了這種語(yǔ)言是不純的。畢竟C + +語(yǔ)言的設計是為了實(shí)用,而不是追求理想的抽象。
C++輸入輸出流的操縱算子(manipulator)有:endl、flush、ws、hex等。
cout<<flush; // 清空流cout << hex << "0x" << i; // 輸出16進(jìn)制cin>>ws; // 跳過(guò)空格
iostream.h還包括以下的操縱算子:
| 操縱算子 | 作用 |
|---|---|
| showbase/noshowbase | 在打印一整數值時(shí),標明數字基數(十進(jìn)制,八進(jìn)制和十六進(jìn)制);所用的格式能被C++編譯器讀出 |
| showpos/noshowpos | 顯示正值符號加(+) |
| uppercase/nouppercase | 顯示代表十六進(jìn)制值的大寫(xiě)字母A - F以及科學(xué)記數法中的E |
| showpoint/noshowpoint | 表明浮點(diǎn)數值的小數點(diǎn)和后面的零 |
| skipws/noskipws | 跳過(guò)輸入中的空白字符 |
| left | 左對齊,右填充 |
| right | 右對齊,左填充 |
| internal | 在引導符或基數指示符和值之間填充 |
| scientific | 使用科學(xué)記數法 |
| fixed | setprecision()或ios::precision()設置小數點(diǎn)后面的位數 |
如何建立我們自己的操縱算子?
我們可能想建立自己的操縱算子,這是相當簡(jiǎn)單的。一個(gè)像endl這樣的不帶參數的操縱算子只是一個(gè)函數,這個(gè)函數把一個(gè)ostream引用作為它的參數。對endl的聲明是:
ostream& endl(ostream&);
例子:產(chǎn)生一個(gè)換行而不刷新這個(gè)流。人們認為nl比使用endl要好,因為后者總是清空輸出流,這可能引起執行故障。
ostream& nl(ostream& os) {return os << "\n";}int main() {cout << "newlines" << nl << "between" << nl << "each" << nl << "word" << nl;return 0;}
C語(yǔ)言中const與C++中const的區別:
常量引進(jìn)是在早期的C++版本中,當時(shí)標準C規范正在制訂。那時(shí),常量被看作是一個(gè)好的思想而被包含在C中。但是,C中的const意思是“一個(gè)不能被改變的普通變量”,在C中,它總是占用存儲而且它的名字是全局符。C編譯器不能把const看成一個(gè)編譯期間的常量。在C中, 如果寫(xiě):
const bufsize=100;char buf[bufsize];
盡管看起來(lái)好像做了一件合理的事,但這將得到一個(gè)錯誤結果。因為bufsize占用存儲的某個(gè)地方,所以C編譯器不知道它在編譯時(shí)的值。在C語(yǔ)言中可以選擇這樣書(shū)寫(xiě):
const bufsize;
這樣寫(xiě)在C++中是不對的,而C編譯器則把它作為一個(gè)聲明,這個(gè)聲明指明在別的地方有存儲分配。因為C默認const是外部連接的,C++默認cosnt是內部連接的,這樣,如果在C++中想完成與C中同樣的事情,必須用extern把連接改成外部連接:
extern const bufsize;//declaration only
這種方法也可用在C語(yǔ)言中。
注意:在C語(yǔ)言中使用限定符const不是很有用,即使是在常數表達式里(必須在編譯期間被求出);想使用一個(gè)已命名的值,使用const也不是很有用的。C迫使程序員在預處理器里使用#define。
類(lèi)里的const和enum
下面的寫(xiě)法有什么問(wèn)題嗎?:
class bob {const size = 100; // illegalint array[size]; // illegal}
結果當然是編譯不通過(guò)。why?因為const在類(lèi)對象里進(jìn)行了存儲空間分配,編譯器不能知道const的內容是什么,所以不能把它用作編譯期間的常量。這意味著(zhù)對于類(lèi)里的常數表達式來(lái)說(shuō),const就像它在C中一樣沒(méi)有作用。
在類(lèi)里的const意思是“在這個(gè)特定對象的壽命期內,而不是對于整個(gè)類(lèi)來(lái)說(shuō),這個(gè)值是不變的”。那么怎樣建立一個(gè)可以用在常數表達式里的類(lèi)常量呢?
一個(gè)普通的辦法是使用一個(gè)不帶實(shí)例的無(wú)標記的enum。枚舉的所有值必須在編譯時(shí)建立,它對類(lèi)來(lái)說(shuō)是局部的,但常數表達式能得到它的值,這樣,我們一般會(huì )看到:
class bob {enum { size = 100 }; // legalint array[size]; // legal}
使用enum是不會(huì )占用對象中的存儲空間的,枚舉常量在編譯時(shí)被全部求值。我們也可以明確地建立枚舉常量的值:enum { one=1,two=2,three};
類(lèi)里面的const成員函數
class X {int i;public:int f() const;}
這里f()是const成員函數,表示只能const類(lèi)對象調用這個(gè)函數(const對象不能調用非const成員函數),如果我們改變對象中的任何一個(gè)成員或調用一個(gè)非const成員函數,編譯器將發(fā)出一個(gè)出錯信息。
關(guān)鍵字const必須用同樣的方式重復出現在定義里,否則編譯器把它看成一個(gè)不同的函數:
int X::f() const { return i;}
任何不修改成員數據的函數應該聲明為const函數,這樣它可以由const對象使用。
注意:構造函數和析構函數都不是const成員函數,因為它們在初始化和清理時(shí),總是對對象作些修改。
如果我們想要建立一個(gè)const成員函數,但仍然想在對象里改變某些數據,這時(shí)該怎么辦呢?這關(guān)系到按位const和按成員const的區別。按位const意思是對象中的每個(gè)位是固定的,所以對象的每個(gè)位映像從不改變。按成員const意思是,雖然整個(gè)對象從概念上講是不變的,但是某個(gè)成員可能有變化。當編譯器被告知一個(gè)對象是const對象時(shí),它將保護這個(gè)對象。
這里我們要介紹在const成員函數里改變數據成員的兩種方法。
第一種方法已成為過(guò)去,稱(chēng)為“強制轉換const”。它以相當奇怪的方式執行。取this(這個(gè)關(guān)鍵字產(chǎn)生當前對象的地址)并把它強制轉換成指向當前類(lèi)型對象的指針??磥?lái)this已經(jīng)是我們所需的指針,但它是一個(gè)const指針,所以,還應把它強制轉換成一個(gè)普通指針,這樣就可以在運算中去掉常量性。下面是一個(gè)例子:
class Y {int i, j;public:Y() { i = j = 0; }void f() const;};void Y::f() const {//! i++; // error((Y*)this)->j++; // ok , cast away const feature.}
這種方法可行,在過(guò)去的程序代碼里可以看到這種用法,但這不是首選的技術(shù)。問(wèn)題是:this沒(méi)有用const修飾,這在一個(gè)對象的成員函數里被隱藏,這樣,如果用戶(hù)不能見(jiàn)到源代碼(并找到用這種方法的地方),就不知道發(fā)生了什么。
mutable,以指定一個(gè)特定的數據成員可以在一個(gè)const對象里被改變。
class Y {int i;mutable int j;public:Y() { i = j = 0; }void f() const;};void Y::f() const {//! i++; // error((Y*)this)->j++; // ok , mutable.}
volatile關(guān)鍵字
volatile的語(yǔ)法與const是一樣的,但是volatile的意思是“在編譯器認識的范圍外,這個(gè)數據可以被改變”。不知何故,環(huán)境正在改變數據(可能通過(guò)多任務(wù)處理),所以,volatile告訴編譯器不要擅自做出有關(guān)數據的任何假定—在優(yōu)化期間這是特別重要的。如果編譯器說(shuō):“我已經(jīng)把數據讀進(jìn)寄存器,而且再沒(méi)有與寄存器接觸”。一般情況下,它不需要再讀這個(gè)數據。但是,如果數據是volatile修飾的,編譯器不能作出這樣的假定,因為可能被其他進(jìn)程改變了, 它必須重讀這個(gè)數據而不是優(yōu)化這個(gè)代碼。
注意:
- 就像建立const對象一樣,程序員也可以建立volatile對象,甚至還可以建立const volatile對 象,這個(gè)對象不能被程序員改變,但可通過(guò)外面的工具改變。
- 就像const一樣,我們可以對數據成員、成員函數和對象本身使用volatile,可以并且也只能為volatile對象調用volatile成員函數。
- volatile的語(yǔ)法與const是一樣的,所以經(jīng)常把它們倆放在一起討論。為表示可以選擇兩個(gè)中的任何一個(gè),它們倆通稱(chēng)為c-v限定詞。
宏的好處與壞處
宏的好處:#與##的使用
三個(gè)有用的特征:字符串定義、字符串串聯(lián)和標志粘貼。
字符串定義的完成是用#指示,它容許設一個(gè)標識符并把它轉化為字符串,然而字符串串聯(lián)發(fā)生在當兩個(gè)相鄰的字符串沒(méi)有分隔符時(shí),在這種情況下字符串組合在一起。在寫(xiě)調試代碼時(shí),這兩個(gè)特征是非常有效的。
#define DEBUG(X) cout<<#X " = " << X << endl
上面的這個(gè)定義可以打印任何變量的值。
我們也可以得到一個(gè)跟蹤信息,在此信息里打印出它們執行的語(yǔ)句。
#define TRACE(S) cout << #S << endl; S
#S定義了要輸出的語(yǔ)句。第2個(gè)S重申了語(yǔ)句,所以這個(gè)語(yǔ)句被執行。當然,這可能會(huì )產(chǎn)生問(wèn)題,尤其是在一行for循環(huán)中。
for (int i = 0 ; i < 100 ; i++ )TRACE(f(i)) ;
因為在TRACE( )宏里實(shí)際上有兩個(gè)語(yǔ)句,所以一行for循環(huán)只執行第一個(gè)。
for (int i = 0 ; i < 100 ; i++ )cout << "f(i)" << endl;f(i); // 第二條語(yǔ)句脫離了for循環(huán),因此執行不到
解決方法是在宏中用逗號代替分號。
標志粘貼在寫(xiě)代碼時(shí)是非常有用的,用##表示。它讓我們設兩個(gè)標識符并把它們粘貼在一起自動(dòng)產(chǎn)生一個(gè)新的標識符。例如:
#define FIELD(A) char *A##_string;int A##_size
此時(shí)下面的代碼:
class record{FIELD(one);FIELD(two);FIELD(three);//...};
就相當于下面的代碼:
class record{char *one_string,int one_size;char *two_string,int two_size;char *three_string,int three_size;//...};
宏的不好:容易出錯
下面舉個(gè)例子即可說(shuō)明:
#define band(x) (((x)>5 && (x)<10) ? (x) :0)int main() {for(int i = 4; i < 11; i++) {int a = i;cout << "a = " << a << "\t";cout << "band(++a)" << band(++a) << "\t";cout << "a = " << a << endl;}return 0;}
輸出:
a = 4 band(++a)0 a = 5
a = 5 band(++a)8 a = 8
a = 6 band(++a)9 a = 9
a = 7 band(++a)10 a = 10
a = 8 band(++a)0 a = 10
a = 9 band(++a)0 a = 11
a = 10 band(++a)0 a = 12
存儲類(lèi)型指定符
常用的有static和extern。
不常用的有兩個(gè):一是auto,人們幾乎不用它,因為它告訴編譯器這是一個(gè)局部變量,實(shí)際上編譯器總是可以從 變量定義時(shí)的上下文中判斷出這是一個(gè)局部變量。所以auto是多余的。還有一個(gè)是register,它也是局部變量,但它告訴編譯器這個(gè)特殊的變量要經(jīng)常用到,所以編譯器應該盡可能地讓它保存在寄存器中。它用于優(yōu)化代碼。各種編譯器對這種類(lèi)型的變量處理方式也不盡相同,它們有時(shí)會(huì )忽略這種存儲類(lèi)型的指定。一般,如果要用到這個(gè)變量的地址, register指定符通常都會(huì )被忽略。應該避免用register類(lèi)型,因為編譯器在優(yōu)化代碼方面通常比我們做得更好。
位拷貝(bitcopy)與值拷貝的區別(很重要)
由1個(gè)例子來(lái)說(shuō)明:一個(gè)類(lèi)在任何時(shí)候知道它存在多少個(gè)對象,可以通過(guò)包含一個(gè)static成員來(lái)做到,如下代碼所示:
#include <iostream>using namespace std;class test {static int object_count;public:test() {object_count++;print("test()");}static void print(const char *msg = 0) {if(msg) cout << msg << ": ";cout << "object_count = " << object_count << endl;}~test() {object_count--;print("~test()");}};int test::object_count = 0;// pass and return by value.test f(test x) {x.print("x argument inside f()");return x;}int main() {test h;test::print("after construction of h");test h2 = f(h);test::print("after call to f()");return 0;}
然而輸出并不是我們期望的那樣:
test(): object_count = 1
after construction of h: object_count = 1
x argument inside f(): object_count = 1
~test(): object_count = 0
after call to f(): object_count = 0
~test(): object_count = -1
~test(): object_count = -2
在h生成以后,對象數是1,這是對的。我們希望在f()調用后對象數是2,因為h2也在范圍內。然而,對象數是0,這意味著(zhù)發(fā)生了嚴重的錯誤。這從結尾兩個(gè)析構函數執行后使得對象數變?yōu)樨摂档氖聦?shí)得到確認,有些事根本就不應該發(fā)生。
讓我們來(lái)看一下函數f()通過(guò)傳值方式傳入參數那一處。原來(lái)的對象h存在于函數框架之外,同時(shí)在函數體內又增加了一個(gè)對象,這個(gè)對象是傳值方式傳入的對象的拷貝,這屬于位拷貝,調用的是默認拷貝構造函數,而不是調用構造函數。然而,參數的傳遞是使用C的原始的位拷貝的概念,但test類(lèi)需要真正的初始化來(lái)維護它的完整性。所以,缺省的位拷貝不能達到預期的效果。
當局部對象出了調用的函數f()范圍時(shí),析構函數就被調用,析構函數使object_count減小。 所以,在函數外面, object_count等于0。h2對象的創(chuàng )建也是用位拷貝產(chǎn)生的(也是調用默認拷貝構造函數),所以,構造函數在這里也沒(méi)有調用。當對象h和h2出了它們的作用范圍時(shí),它們的析構函數又使object_count值變?yōu)樨撝怠?/p>
總結:
- 位拷貝拷貝的是地址(也叫淺拷貝),而值拷貝則拷貝的是內容(深拷貝)。
- 深拷貝和淺拷貝可以簡(jiǎn)單理解為:如果一個(gè)類(lèi)擁有資源,當這個(gè)類(lèi)的對象發(fā)生復制過(guò)程的時(shí)候,資源重新分配,這個(gè)過(guò)程就是深拷貝,反之,沒(méi)有重新分配資源,就是淺拷貝。
默認的拷貝構造函數”和“缺省的賦值函數”均采用“位拷貝”而非“值拷貝”的方式來(lái)實(shí)現,倘若類(lèi)中含有指針變量,這兩個(gè)函數注定將出錯。
關(guān)于位拷貝和值拷貝的深入理解可以參考這篇文章:C++中的位拷貝與值拷貝淺談
test(const test& t) {object_count++;print("test(const test&)");}
這樣輸出才正確:
test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0
如果在main中加一句“f(h);”,即忽略返回值,那么返回的時(shí)候還會(huì )調用拷貝構造函數嗎?
答案是:會(huì )調用。這時(shí)候會(huì )產(chǎn)生一個(gè)臨時(shí)對象,由于該臨時(shí)對象沒(méi)有用處,因此會(huì )馬上調用析構函數銷(xiāo)毀掉。這時(shí)候輸出就會(huì )像下面這樣:
test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
test(const test&): object_count = 3
x argument inside f(): object_count = 3
test(const test&): object_count = 4
~test(): object_count = 3
~test(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0
如果一個(gè)類(lèi)由其它幾個(gè)類(lèi)的對象組合而成,如果此時(shí)該類(lèi)沒(méi)有自定義拷貝構造函數,那么編譯器遞歸地為所有的成員對象和基本類(lèi)調用拷貝構造函數。如果成員對象也含有別的對象,那么后者的拷貝構造函數也將被調用。
怎樣避免調用拷貝構造函數??jì)H當準備用傳值的方式傳遞類(lèi)對象時(shí),才需要拷貝構造函數。有兩種解決方法:
防止傳值方法傳遞
有一個(gè)簡(jiǎn)單的技術(shù)防止通過(guò)傳值方式傳遞:聲明一個(gè)私有private拷貝構造函數。我們甚至不必去定義它,除非我們的成員函數或友元函數需要執行傳值方式的傳遞。如果用戶(hù)試圖用傳值方式傳遞或返回對象,編譯器將會(huì )發(fā)出一個(gè)出錯信息。這是因為拷貝構造函數是私有的。因為我們已顯式地聲明我們接管了這項工作,所以編譯器不再創(chuàng )建缺省的拷貝構造函數。
class noCC {int i;noCC(const noCC&); // private and no definitionpublic:noCC(int I = 0) : i(I) {}};void f(noCC);main() {noCC n;//! f(n); // error: copy-constructor called//! noCC n2 = n; // error: c-c called//! noCC n3(n); // error: c-c called}
注意這里n2 = n也調用拷貝構造函數,注意這里要和賦值函數區分。
void get(const Slice&);非自動(dòng)繼承的函數
構造函數、析構函數和賦值函數(operator=)不能被繼承。
私有繼承的目的 private繼承的目的是什么,因為在類(lèi)中選擇創(chuàng )建一個(gè)private對象似乎更合適。將private繼承包含在該語(yǔ)言中只是為了語(yǔ)言的完整性。但是,如果沒(méi)有其他理由,則應當減少混淆,所以通常建議用private成員而不是private繼承。
然而,private繼承也不是一無(wú)用處。
這里可能偶然有這種情況,即可能想產(chǎn)生像基類(lèi)接口一樣的接口,而不允許處理該對象像處理基類(lèi)對象一樣。private繼承提供了這個(gè)功能。
能對私有繼承成員公有化嗎?
當私有繼承時(shí),基類(lèi)的所有public成員都變成了private。如果希望它們中的任何一個(gè)是可視的,可以辦到嗎?答案是可以的,只要用派生類(lèi)的public選項聲明它們的名字即可(新的標準中使用using關(guān)鍵字)。
#include <iostream>class base {public:char f() const { return 'a'; }int g() const { return 2; }float h() const { return 3.0; }};class derived : base {public:using base::f; // Name publicizes memberusing base::h;};int main() {derived d;d.f();d.h();// d.g(); // error -- private functionreturn 0;}
這樣,如果想要隱藏這個(gè)類(lèi)的基類(lèi)部分的功能,則private繼承是有用的。
多重繼承注意向上映射的二義性。比如base(有個(gè)f()方法)有兩個(gè)子對象d1和d2,且都重寫(xiě)了base的f()方法,此時(shí)子類(lèi)dd如果也有f()方法則不能同時(shí)繼承自d1和d2,因為f()方法存在二義性,不知道該繼承哪個(gè)f()方法。
解決方法是對dd類(lèi)中的f()方法重新定義以消除二義性,比如明確指定使用d1的f()方法。
當然也不能將dd類(lèi)向上映射為base類(lèi),這可以通過(guò)使用虛繼承解決,關(guān)鍵字virtual,base中的f()方法改成虛函數且d1和d2的繼承都改為虛繼承,當然dd繼承d1和d2用public繼承即可。
C語(yǔ)言中如何關(guān)閉assert斷言功能? 頭文件:<assert.h>或<cassert>
在開(kāi)發(fā)過(guò)程中,使用它們,完成后用#define NDEBUG使之失效,以便推出產(chǎn)品,注意必須在頭文件之前關(guān)閉才有效。
#define NDEBUG#include <cassert>
C++如何實(shí)現動(dòng)態(tài)捆綁?—即多態(tài)的實(shí)現(很重要)
C++中為了實(shí)現多態(tài),編譯器對每個(gè)包含虛函數的類(lèi)創(chuàng )建一個(gè)表(稱(chēng)為VTABLE,虛表)。在 VTABLE中,編譯器放置特定類(lèi)的虛函數地址。在每個(gè)帶有虛函數的類(lèi)中,編譯器秘密地置一指針,稱(chēng)為vpointer(縮寫(xiě)為VPTR),指向這個(gè)對象的VTABLE。通過(guò)基類(lèi)指針做虛函數調用時(shí)(也就是做多態(tài)調用時(shí)),編譯器靜態(tài)地插入取得這個(gè)VPTR,并在VTABLE表中查找函數地址的代碼,這樣就能調用正確的函數使晚捆綁發(fā)生。
為每個(gè)類(lèi)設置VTABLE、初始化VPTR、為虛函數調用插入代碼,所有這些都是自動(dòng)發(fā)生的,所以我們不必擔心這些。利用虛函數,這個(gè)對象的合適的函數就能被調用,哪怕在編譯器還不知道這個(gè)對象的特定類(lèi)型的情況下。
在vtable表中,編譯器放置了在這個(gè)類(lèi)中或在它的基類(lèi)中所有已聲明為virtual的函數的地址。如果在這個(gè)派生類(lèi)中沒(méi)有對在基類(lèi)中聲明為virtual的函數進(jìn)行重新定義,編譯器就使用基類(lèi)的這個(gè)虛函數地址。
下面舉個(gè)例子說(shuō)明:
#include <iostream>enum note { middleC, Csharp, Cflat };class instrument {public:virtual void play(note) const {cout << "instrument::play" << endl;}virtual char* what() const {return "instrument";}// assume this will modify the object:virtual void adjust(int) {}};class wind : public instrument {public:void play(note) const {cout << "wind::play" << endl;}char* what() const {return "wind";}void adjust(int) {}};class percussion : public instrument {public:void play(note) const {cout << "percussion::play" << endl;}char* what() const {return "percussion";}void adjust(int) {}};class string : public instrument {public:void play(note) const {cout << "string::play" << endl;}char* what() const {return "string";}void adjust(int) {}};class brass : public wind {public:void play(note) const {cout << "brass::play" << endl;}char* what() const {return "brass";}};class woodwind : public wind {public:void play(note) const {cout << "woodwind::play" << endl;}char* what() const {return "woodwind";}};instrument *A[] = {new wind,new percussion,new string,new brass};
下圖畫(huà)的是指針數組A[]。
下面看到的是通過(guò)instrument指針對于brass調用adjust()。instrument引用產(chǎn)生如下結果:
編譯器從這個(gè)instrument指針開(kāi)始,這個(gè)指針指向這個(gè)對象的起始地址。所有的instrument對象或由instrument派生的對象都有它們的VPTR,它在對象的相同的位置(常常在對象的開(kāi)頭),所以編譯器能夠取出這個(gè)對象的VPTR。VPTR指向VTABLE的開(kāi)始地址。所有的VTABLE有相同的順序,不管何種類(lèi)型的對象。 play()是第一個(gè),what()是第二個(gè),adjust()是第三個(gè)。所以編譯器知道adjust()函數必在VPTR + 2處。這樣,不是“以instrument :: adjust地址調用這個(gè)函數”(這是早捆綁,是錯誤活動(dòng)),而是產(chǎn)生代碼,“在VPTR + 2處調用這個(gè)函數”。因為VPTR的效果和實(shí)際函數地址的確定發(fā)生在運行時(shí),所以這樣就得到了所希望的晚捆綁。向這個(gè)對象發(fā)送消息,這個(gè)對象能斷定它應當做什么。
當多態(tài)地處理對象時(shí),傳地址與傳值有明顯的不同。所有在這里已經(jīng)看到的例子和將會(huì )看到的例子都是傳地址的,而不是傳值的。這是因為地址都有相同的長(cháng)度,傳派生類(lèi)型(它通常稍大一些)對象的地址和傳基類(lèi)(它通常小一點(diǎn))對象的地址是相同的。如前面解釋的,使用多態(tài)的目的是讓對基類(lèi)對象操作的代碼也能操作派生類(lèi)對象。
如果使用對象而不是使用地址或引用進(jìn)行向上映射,發(fā)生的事情會(huì )使我們吃驚:這個(gè)對象 被“切片”,直到所剩下來(lái)的是適合于目的的子對象。在下面例子中可以看到通過(guò)檢查這個(gè)對象的長(cháng)度切片剩下來(lái)的部分。
#include <iostream>using namespace std;class base {int i;public:base(int I = 0) : i(I) {}virtual int sum() const { return i; }};class derived : public base {int j;public:derived(int I = 0, int J = 0) : base(I), j(J) {}virtual int sum() const { return base::sum() + j; }};void call(base b) {cout << "sum = " << b.sum() << endl;}main() {base b(10);derived d(10, 47);call(b);call(d);}
函數call( )通過(guò)傳值傳遞一個(gè)類(lèi)型為base的對象。然后對于這base對象調用虛函數sum( )。 我們可能希望第一次調用產(chǎn)生10,第二次調用產(chǎn)生57。實(shí)際上,兩次都產(chǎn)生10。 在這個(gè)程序中,有兩件事情發(fā)生了。
對象切片實(shí)際上是去掉了對象的一部分,而不是象使用指針或引用那樣簡(jiǎn)單地改變地址的內容。因此,對象向上映射不常做,事實(shí)上,通常要提防或防止這種操作。我們可以通過(guò)在基 類(lèi)中放置純虛函數來(lái)防止對象切片。這時(shí)如果進(jìn)行對象切片就將引起編譯時(shí)的出錯信息。
最后注意:虛機制在構造函數中不工作。即在構造函數中調用虛函數沒(méi)有結果。
RTTI—運行時(shí)類(lèi)型識別(很重要)
概念
運行時(shí)類(lèi)型識別(Run-time type identification, RTTI)是在我們只有一個(gè)指向基類(lèi)的指針或引用時(shí)確定一個(gè)對象的準確類(lèi)型。
使用方法
一般情況下,我們并不需要知道一個(gè)類(lèi)的確切類(lèi)型,虛函數機制可以實(shí)現那種類(lèi)型的正確行為。但是有些時(shí)候,我們有指向某個(gè)對象的基類(lèi)指針,確定該對象的準確類(lèi)型是很有用的。
RTTI與異常一樣,依賴(lài)駐留在虛函數表中的類(lèi)型信息。如果試圖在一個(gè)沒(méi)有虛函數的類(lèi)上用RTTI,就得不到預期的結果。
RTTI的兩種使用方法
第一種使用typeid(),就像sizeof()一樣,看上都像一個(gè)函數。但實(shí)際上它是由編譯器實(shí)現的。typeid()帶有一個(gè)參數,它可以是一個(gè)對象引用或指針,返回全局typeinfo類(lèi)的常量對象的一個(gè)引用??梢杂眠\算符“==”和“!=”來(lái)互相比較這些對象。也可以用name()來(lái)獲得類(lèi)型的名稱(chēng)。注意,如果給typeid( )傳遞一個(gè)shape*型參數,它會(huì )認為類(lèi)型為shape*,所以如果想知道一個(gè)指針所指對象的精確類(lèi)型,我們必須逆向引用這個(gè)指針。比如,s是個(gè)shape*, 那么:
cout << typeid(*s).name()<<endl;
將顯示出s所指向的對象類(lèi)型。
也可以用before(typeinfo&)查詢(xún)一個(gè)typeinfo對象是否在另一個(gè)typeinfo對象的前面(以定義實(shí)現的排列順序),它將返回true或false。如果寫(xiě):
if(typeid(me).before(typeid(you))) //...
那么表示我們正在查詢(xún)me在排列順序中是否在you之前。
dynamic_cast<>模板。兩種方法的使用舉例如下:
#include <iostream>#include <typeinfo>using namespace std;class base {int i;public:base(int I = 0) : i(I) {}virtual int sum() const { return i; }};class derived : public base {int j;public:derived(int I = 0, int J = 0) : base(I), j(J) {}virtual int sum() const { return base::sum() + j; }};main() {base *b = new derived(10, 47);// rtti method1cout << typeid(b).name() << endl; // P4basecout << typeid(*b).name() << endl; // 7derivedif(typeid(b).before(typeid(*b)))cout << "b is before *b" << endl;elsecout << "*b is before b" << endl;// rtti method2derived *d = dynamic_cast<derived*>(d);if(d) cout << "cast successful" << endl;}
注意1:這里如果沒(méi)有多態(tài)機制,則RTTI可能運行的結果不是我們想要的,比如如果沒(méi)有虛函數,則這里兩個(gè)都顯示base,一般希望RTTI用于多態(tài)類(lèi)。
注意2:運行時(shí)類(lèi)型的識別對一個(gè)void型指針不起作用。void *確實(shí)意味著(zhù)“根本沒(méi)有類(lèi)型信息”。
void *v = new stimpy;stimpy* s = dynamic_cast<stimpy*>(v); // errorcout << typeid(*v).name() << endl; // error
RTTI的實(shí)現
**典型的RTTI是通過(guò)在VTABLE中放一個(gè)額外的指針來(lái)實(shí)現的。這個(gè)指針指向一個(gè)描述該特定類(lèi)型的typeinfo結構(每個(gè)新類(lèi)只產(chǎn)生一個(gè)typeinfo的實(shí)例),所以typeid( )表達式的作用實(shí)際上很簡(jiǎn)單。**VPTR用來(lái)取typeinfo的指針,然后產(chǎn)生一個(gè)結果typeinfo結構的一個(gè)引用—這是一個(gè)決定性的步驟—我們已經(jīng)知道它要花多少時(shí)間。
對于dynamic_cast<目標* > <源指針>,多數情況下是很容易的,先恢復源指針的RTTI信息再取出目標*的類(lèi)型RTTI信息,然后調用庫中的一個(gè)例程判斷源指針是否與目標*相同或者是目標*類(lèi)型的基類(lèi)。它可能對返回的指針做了一點(diǎn)小的改動(dòng),因為目的指針類(lèi)可能存在多重繼承的情況,而源指針類(lèi)型并不是派生類(lèi)的第一個(gè)基類(lèi)。在多重繼承時(shí)情況會(huì )變得復雜些,因為一個(gè)基類(lèi)在繼承層次中可能出現一次以上,并且可能有虛基類(lèi)。
用于動(dòng)態(tài)映射的庫例程必須檢查一串長(cháng)長(cháng)的基類(lèi)列表,所以動(dòng)態(tài)映射的開(kāi)銷(xiāo)比typeid()要大(當然我們得到的信息也不同,這對于我們的問(wèn)題來(lái)說(shuō)可能很關(guān)鍵),并且這是非確定性的,因為查找一個(gè)基類(lèi)要比查找一個(gè)派生類(lèi)花更多的時(shí)間。另外動(dòng)態(tài)映射允許我們比較任何類(lèi)型,不限于在同一個(gè)繼承層次中比較兩個(gè)類(lèi)。這使得動(dòng)態(tài)映射調用的庫例程開(kāi)銷(xiāo)更高了。
| 映射類(lèi)型 | 含義 |
|---|---|
| static_cast | 為了“行為良好”和“行為較好”而使用的映射,包括一些我們可能現在不用的映射(如向上映射和自動(dòng)類(lèi)型轉換) |
| const_cast | 用于映射常量和變量(const和volatile) |
| const_cast | 為了安全類(lèi)型的向下映射(本章前面已經(jīng)介紹) |
| reinterpret_cast | 為了映射到一個(gè)完全不同的意思。這個(gè)關(guān)鍵詞在我們需要把類(lèi)型映射回原有類(lèi)型時(shí)要用到它。我們映射到的類(lèi)型僅僅是為了故弄玄虛和其他目的。這是所有映射中最危險的 |
注意:如果想把一個(gè)const 轉換為非const,或把一個(gè)volatile轉換成一個(gè)非volatile(勿遺忘這種情況),就要用到const_cast。這是可以用const_cast的唯一轉換。如果還有其他的轉換牽涉進(jìn)來(lái),它必須分開(kāi)來(lái)指定,否則會(huì )有一個(gè)編譯錯誤。
聯(lián)系客服