5、幾點(diǎn)值得討論的地方:
(1)const究竟意味著什么?
說了這么多,你認(rèn)為const意味著什么?一種修飾符?接口抽象?一種新類型? 也許都是,在Stroustup最初引入這個(gè)關(guān)鍵字時(shí),只是為對(duì)象放入ROM做出了一種可能,對(duì)于const對(duì)象,C++既允許對(duì)其進(jìn)行靜態(tài)初始化,也允許對(duì)他進(jìn)行動(dòng)態(tài)初始化。理想的const對(duì)象應(yīng)該在其構(gòu)造函數(shù)完成之前都是可寫的,在析夠函數(shù)執(zhí)行開始后也都是可寫的,換句話說,const對(duì)象具有從構(gòu)造函數(shù)完成到析夠函數(shù)執(zhí)行之前的不變性,如果違反了這條規(guī)則,結(jié)果都是未定義的!雖然我們把const放入ROM中,但這并不能夠保證const的任何形式的墮落,我們后面會(huì)給出具體的辦法。無論const對(duì)象被放入ROM中,還是通過存儲(chǔ)保護(hù)機(jī)制加以保護(hù),都只能保證,對(duì)于用戶而言這個(gè)對(duì)象沒有改變。換句話說,廢料收集器(我們以后會(huì)詳細(xì)討論,這就一筆帶過)或數(shù)據(jù)庫系統(tǒng)對(duì)一個(gè)const的修改怎沒有任何問題。
(2)位元const V.S. 抽象const?
對(duì)于關(guān)鍵字const的解釋有好幾種方式,最常見的就是位元const 和 抽象const。下面我們看一個(gè)例子: class A { public: ...... A f(const A& a); ...... }; 如果采用抽象const進(jìn)行解釋,那就是f函數(shù)不會(huì)去改變所引用對(duì)象的抽象值,如果采用位元const進(jìn)行解釋,那就成了f函數(shù)不會(huì)去改變所引用對(duì)象的任何位元。 我們可以看到位元解釋正是c++對(duì)const問題的定義,const成員函數(shù)不被允許修改它所在對(duì)象的任何一個(gè)數(shù)據(jù)成員。 為什么這樣呢?因?yàn)槭褂梦辉猚onst有2個(gè)好處: 最大的好處是可以很容易地檢測到違反位元const規(guī)定的事件:編譯器只用去尋找有沒有對(duì)數(shù)據(jù)成員的賦值就可以了。另外,如果我們采用了位元const,那么,對(duì)于一些比較簡單的const對(duì)象,我們就可以把它安全的放入ROM中,對(duì)于一些程序而言,這無疑是一個(gè)很重要的優(yōu)化方式。(關(guān)于優(yōu)化處理,我們到時(shí)候?qū)iT進(jìn)行討論) 當(dāng)然,位元const也有缺點(diǎn),要不然,抽象const也就沒有產(chǎn)生的必要了。 首先,位元const的抽象性比抽象const的級(jí)別更低!實(shí)際上,大家都知道,一個(gè)庫接口的抽象性級(jí)別越低,使用這個(gè)庫就越困難。 其次,使用位元const的庫接口會(huì)暴露庫的一些實(shí)現(xiàn)細(xì)節(jié),而這往往會(huì)帶來一些負(fù)面效應(yīng)。所以,在庫接口和程序?qū)崿F(xiàn)細(xì)節(jié)上,我們都應(yīng)該采用抽象const。 有時(shí),我們可能希望對(duì)const做出一些其它的解釋,那么,就要注意了,目前,大多數(shù)對(duì)const的解釋都是類型不安全的,這里我們就不舉例子了,你可以自己考慮一下,總之,我們盡量避免對(duì)const的重新解釋。
(3)放在類內(nèi)部的常量有什么限制?
看看下面這個(gè)例子:
class A {
private:
const int c3 = 7; // ???
static int c4 = 7; // ???
static const float c5 = 7; // ??? ......
};
你認(rèn)為上面的3句對(duì)嗎?呵呵,都不對(duì)!使用這種類內(nèi)部的初始化語法的時(shí)候,常量必須是被一個(gè)常量表達(dá)式初始化的整型或枚舉類型,而且必須是static和const形式。這顯然是一個(gè)很嚴(yán)重的限制! 那么,我們的標(biāo)準(zhǔn)委員會(huì)為什么做這樣的規(guī)定呢?一般來說,類在一個(gè)頭文件中被聲明,而頭文件被包含到許多互相調(diào)用的單元去。但是,為了避免復(fù)雜的編譯器規(guī)則,C++要求每一個(gè)對(duì)象只有一個(gè)單獨(dú)的定義。如果C++允許在類內(nèi)部定義一個(gè)和對(duì)象一樣占據(jù)內(nèi)存的實(shí)體的話,這種規(guī)則就被破壞了。
(4)如何初始化類內(nèi)部的常量?
一種方法就是static 和 const 并用,在內(nèi)部初始化,如上面的例子; 另一個(gè)很常見的方法就是初始化列表:
class A {
public:
A(int i=0):test(i) {}
private:
const int i;
}; 還有一種方式就是在外部初始化,例如:
class A {
public:
A() {}
private:
static const int i;//注意必須是靜態(tài)的!
};
const int A::i=3;
(5)常量與數(shù)組的組合有什么特殊嗎? 我們給出下面的代碼:
const int size[3]={10,20,50};
int array[size[2]];
有什么問題嗎?對(duì)了,編譯通不過!為什么呢?
Const可以用于集合,但編譯器不能把一個(gè)集合存放在它的符號(hào)表里,所以必須分配內(nèi)存。在這種情況下,const意味著“不能改變的一塊存儲(chǔ)”。然而,其值在編譯時(shí)不能被使用,因?yàn)榫幾g器在編譯時(shí)不需要知道存儲(chǔ)的內(nèi)容。自然,作為數(shù)組的大小就不行了:) 你再看看下面的例子:
class A {
public:
A(int i=0):test[2]({1,2}) {}//你認(rèn)為行嗎?
private:
const int test[2];
};
vc6下編譯通不過,為什么呢? 關(guān)于這個(gè)問題,前些時(shí)間,njboy問我是怎么回事?我反問他:“你認(rèn)為呢?”他想了想,給出了一下解釋,大家可以看看:我們知道編譯器堆初始化列表的操作是在構(gòu)造函數(shù)之內(nèi),顯式調(diào)用可用代碼之前,初始化的次序依據(jù)數(shù)據(jù)聲明的次序。初始化時(shí)機(jī)應(yīng)該沒有什么問題,那么就只有是編譯器對(duì)數(shù)組做了什么手腳!其實(shí)做什么手腳,我也不知道,我只好對(duì)他進(jìn)行猜測:編譯器搜索到test發(fā)現(xiàn)是一個(gè)非靜態(tài)的數(shù)組,于是,為他分配內(nèi)存空間,這里需要注意了,它應(yīng)該是一下分配完,并非先分配test[0],然后利用初始化列表初始化,再分配test[1],這就導(dǎo)致數(shù)組的初始化實(shí)際上是賦值!然而,常量不允許賦值,所以無法通過。 呵呵,看了這一段冠冕堂皇的話,真讓我笑死了!njboy別怪我揭你短呀:)我對(duì)此的解釋是這樣的:C++標(biāo)準(zhǔn)有一個(gè)規(guī)定,不允許無序?qū)ο笤陬悆?nèi)部初始化,數(shù)組顯然是一個(gè)無序的,所以這樣的初始化是錯(cuò)誤的!對(duì)于他,只能在類的外部進(jìn)行初始化,如果想讓它通過,只需要聲明為靜態(tài)的,然后初始化。 這里我們看到,常量與數(shù)組的組合沒有什么特殊!一切都是數(shù)組惹的禍!
(6)this指針是不是const類型的?
this指針是一個(gè)很重要的概念,那該如何理解她呢?也許這個(gè)話題太大了,那我們縮小一些:this指針是個(gè)什么類型的?這要看具體情況:如果在非const成員函數(shù)中,this指針只是一個(gè)類類型的;如果在const成員函數(shù)中,this指針是一個(gè)const類類型的;如果在volatile成員函數(shù)中,this指針就是一個(gè)volatile類類型的。
(7)const到底是不是一個(gè)重載的參考對(duì)象?
先看一下下面的例子:
class A {
......
void f(int i) {......}//一個(gè)函數(shù)
void f(int i) const {......}//上一個(gè)函數(shù)的重載
......
}; 上面是重載是沒有問題的了,那么下面的呢?
class A {
......
void f(int i) {......}//一個(gè)函數(shù)
void f(const int i) {......}//?????
......
}; 這個(gè)是錯(cuò)誤的,編譯通不過。那么是不是說明內(nèi)部參數(shù)的const不予重載呢?再看下面的例子:
class A {
......
void f(int& ) {......}//一個(gè)函數(shù)
void f(const int& ) {......}//?????
......
}; 這個(gè)程序是正確的,看來上面的結(jié)論是錯(cuò)誤的。為什么會(huì)這樣呢?這要涉及到接口的透明度問題。按值傳遞時(shí),對(duì)用戶而言,這是透明的,用戶不知道函數(shù)對(duì)形參做了什么手腳,在這種情況下進(jìn)行重載是沒有意義的,所以規(guī)定不能重載!當(dāng)指針或引用被引入時(shí),用戶就會(huì)對(duì)函數(shù)的操作有了一定的了解,不再是透明的了,這時(shí)重載是有意義的,所以規(guī)定可以重載。
(8)什么情況下為const分配內(nèi)存?
以下是我想到的可能情況,當(dāng)然,有的編譯器進(jìn)行了優(yōu)化,可能不分配內(nèi)存。
A、作為非靜態(tài)的類成員時(shí);
B、用于集合時(shí);
C、被取地址時(shí);
D、在main函數(shù)體內(nèi)部通過函數(shù)來獲得值時(shí);
E、const的 class或struct有用戶定義的構(gòu)造函數(shù)、析構(gòu)函數(shù)或基類時(shí);。
F、當(dāng)const的長度比計(jì)算機(jī)字長還長時(shí);
G、參數(shù)中的const;
H、使用了extern時(shí)。 不知道還有沒有其他情況,歡迎高手指點(diǎn):)
(9)臨時(shí)變量到底是不是常量?
很多情況下,編譯器必須建立臨時(shí)對(duì)象。像其他任何對(duì)象一樣,它們需要存儲(chǔ)空間而且必須被構(gòu)造和刪除。區(qū)別是我們從來看不到編譯器負(fù)責(zé)決定它們的去留以及它們存在的細(xì)節(jié)。對(duì)于C++標(biāo)準(zhǔn)草案而言:臨時(shí)對(duì)象自動(dòng)地成為常量。因?yàn)槲覀兺ǔ=佑|不到臨時(shí)對(duì)象,不能使用與之相關(guān)的信息,所以告訴臨時(shí)對(duì)象做一些改變有可能會(huì)出錯(cuò)。當(dāng)然,這與編譯器有關(guān),例如:vc6、vc7都對(duì)此作了擴(kuò)展,所以,用臨時(shí)對(duì)象做左值,編譯器并沒有報(bào)錯(cuò)。
(10)與static搭配會(huì)不會(huì)有問題? 假設(shè)有一個(gè)類:
class A {
public:
......
static void f() const { ......}
......
}; 我們發(fā)現(xiàn)編譯器會(huì)報(bào)錯(cuò),因?yàn)樵谶@種情況下static不能夠與const共存! 為什么呢?因?yàn)閟tatic沒有this指針,但是const修飾this指針,所以...
(11)如何修改常量?
有時(shí)候我們卻不得不對(duì)類內(nèi)的數(shù)據(jù)進(jìn)行修改,但是我們的接口卻被聲明了const,那該怎么處理呢?我對(duì)這個(gè)問題的看法如下:
1)標(biāo)準(zhǔn)用法:
mutable class A {
public:
A(int i=0):test(i) { }
void SetValue(int i)const { test=i; }
private: mutable int test;//這里處理!
};
2)強(qiáng)制轉(zhuǎn)換:
const_cast class A {
public:
A(int i=0):test(i) { }
void SetValue(int i)const {
const_cast (test)=i;
}//這里處理!
private:
int test;
};
3)靈活的指針:
int* class A {
public:
A(int i=0):test(i) { }
void SetValue(int i)const { *test=i; }
private:
int* test; //這里處理!
};
4)未定義的處理
class A {
public:
A(int i=0):test(i) { }
void SetValue(int i)const {
int *p=(int*)&test; *p=i;
}//這里處理!
private:
int test;
}; 注意,這里雖然說可以這樣修改,但結(jié)果是未定義的,避免使用!
5)內(nèi)部處理:this指針
class A {
public:
A(int i=0):test(i) { }
void SetValue(int i)const {
((A*)this)->test=i;
}//這里處理!
private:
int test;
};
6)最另類的處理:空間布局
class A {
public:
A(int i=0):test(i),c('a') { }
private:
char c;
const int test;
};
int main()
{
A a(3);
A* pa=&a;
char* p=(char*)pa;
int* pi=(int*)(p+4);//利用邊緣調(diào)整
*pi=5;//此處改變了test的值!
return 0;
}
雖然我給出了6中方法,但是我只是想說明如何更改,但出了第一種用法之外,另外5種用法,我們并不提倡,不要因?yàn)槲疫@么寫了,你就這么用,否則,我真是要誤人子弟了:)
(12)最后我們來討論一下常量對(duì)象的動(dòng)態(tài)創(chuàng)建。 既然編譯器可以動(dòng)態(tài)初始化常量,就自然可以動(dòng)態(tài)創(chuàng)建,例如:
const int* pi=new const int(10); 這里要注意2點(diǎn):
1)const對(duì)象必須被初始化!所以(10)是不能夠少的。
2)new返回的指針必須是const類型的。 那么我們可不可以動(dòng)態(tài)創(chuàng)建一個(gè)數(shù)組呢? 答案是否定的,因?yàn)閚ew內(nèi)置類型的數(shù)組,不能被初始化。 這里我們忽視了數(shù)組是類類型的,同樣對(duì)于類內(nèi)部數(shù)組初始化我們也做出了這樣的忽視,因?yàn)檫@涉及到數(shù)組的問題,我們以后再討論。