假設b是乙個const string物件:
classstring
;const
string b("
hello world
"); //
b是乙個const物件
看看下面的情形:
char *str = b; // 呼叫b.operator char*()
strcpy(str, "hi mom"); // 修改str指向的值
b的值現在還是"hello world"嗎?或者,它是否已經變成了對母親的問候語?答案完全取決於string::operator char*的實現。
下面是乙個有欠考慮的實現,它導致了錯誤的結果。但是,它工作起來確實很高效,所以很多程式設計師才掉進它的錯誤陷阱之中:
//乙個執行很快但不正確的實現
inline string::operator
char*() const
這個函式的缺陷在於它返回了乙個"控制代碼"(在本例中,是個指標),而這個控制代碼所指向的資訊本來是應該隱藏在被呼叫函式所在的string物件的內部。這樣,這個控制代碼就給了呼叫者自由訪問data所指的私有資料的機會。換句話說,有了下面的語句:
char *str = b;
情況就會變成這樣:
str------------------------->"hello world\0"
顯然,任何對str所指向的記憶體的修改都使得b的有效值發生變化。所以,即使b宣告為const,而且即使只是呼叫了b的某個const成員函式,b也會在程式執行過程中得到不同的值。特別是,如果str修改了它所指的值,b也會改變。
string::operator char*本身寫的沒有一點錯,麻煩的是它可以用於const物件。如果這個函式不宣告為const,就不會有問題,因為這樣它就不能用於象b這樣的const物件了。(const物件不能呼叫非const成員函式)
但是,將乙個string物件轉換成它相應的char*形式是很合理的一件事,無論這個物件是否為const。所以,還是應該使函式保持為const。這樣的話,就得重寫這個函式,使得它不返回指向物件內部資料的控制代碼:
//乙個執行慢但很安全的實現
inline string::operator
char*() const
這個實現很安全,因為它返回的指標所指向的資料只是string物件所指向資料的拷貝;通過函式返回的指標無法修改string物件的值。當然,安全是要有代價的:這個版本的string::operator char* 執行起來比前面那個簡單版本要慢;此外,函式的呼叫者還要記得delete掉返回的指標。
如果不能忍受這個版本的速度,或者擔心記憶體洩露,可以來一點小小的改動:使函式返回乙個指向const char的指標:
classstring
;inline
string::operator
const
char*() const
指標並不是返回內部資料控制代碼的唯一途徑。引用也很容易被濫用。下面是一種常見的用法,還是拿string類做例子:
classstring
private:
char *data;
};string s = "
i'm not constant";
s[0] = '
x'; //
正確, s不是const
const
string cs = "
i'm constant";
cs[0] = '
x'; //
修改了const string,
//但編譯器不會通知
注意string::operator是通過引用返回結果的。這意味著函式的呼叫者得到的是內部資料data[index]的另乙個名字,而這個名字可以用來修改const物件的內部資料。這個問題和前面看到的相同,只不過這次的罪魁禍首是引用,而不是指標。
這類問題的通用解決方案和前面關於指標的討論一樣:或者使函式為非const(const物件就不能呼叫該函式了),或者重寫函式,使之不返回控制代碼(返回const引用即可)。如果想讓string::operator既適用於const物件又適用於非const 物件,可以參見條款21。
並不是只有const成員函式需要擔心返回控制代碼的問題,即使是非const成員函式也得承認:控制代碼的合法性失效的時間和它所對應的物件是完全相同的。這個時間可能比使用者期望的要早很多,特別是當涉及的物件是由編譯器產生的臨時物件時。
例如,看看這個函式,它返回了乙個string物件:
string somefamousauthor() //隨機選擇乙個作家名
return
""; //
程式不會執行到這兒,
//但對於乙個有返回值的函式來說,
//任何執行途徑上都要有返回值
}
somefamousauthor的返回值是乙個string物件,乙個臨時string物件(參見條款m19)。這樣的物件是暫時性的,它們的生命週期通常在函式呼叫表示式結束時終止。例如上面的情況中,包含somefamousauthor函式呼叫的表示式結束時,返回值物件的生命週期也就隨之結束。
具體看看下面這個使用somefamousauthor的例子,假設string宣告了乙個上面的operator const char*成員函式:
const char *pc = somefamousauthor();
cout << pc;
不論你是否相信,誰也不能**這段**將會做些什麼,至少不能確定它會做些什麼。因為當你想列印pc所指的字串時,字串的值是不確定的。造成這一結果的原因在於pc初始化時發生了下面這些事件:
1. 產生乙個臨時string物件用以儲存somefamousauthor的返回值(預設拷貝建構函式,淺拷貝指標)。
2. 通過string的operator const
char*成員函式將臨時string物件轉換為const char*指標,並用這個指標初始化pc。
3. 臨時string物件被銷毀,其析構函式被呼叫(我感覺臨時物件沒有被銷毀,但是函式內部string物件被銷毀,而臨時物件淺拷貝指標,所以臨時物件的data指標指向無效的記憶體空間)。析構函式中,data指標被刪除(**詳見條款11)。然而,data和pc所指的是同一塊記憶體,所以現在pc指向的是被刪除的記憶體--------其內容是不可確定的。
避免返回內部資料的控制代碼
假設b是乙個const string物件 class string const string b hello world b是乙個const物件 既然b為const,最好的情況當然就是無論現在還是以後,b的值總是 hello world 這就寄希望於別的程式設計師能以合理的方式使用b了。特別是,千萬...
學習 避免返回內部資料的控制代碼
請看物件導向世界裡發生的一幕 物件a 親愛的,永遠別變心!物件b 別擔心,親愛的,我是const。然而,和現實生活中一樣,a會懷疑,能相信b嗎?同樣地,和現實生活中一樣,答案取決於b的本性 其成員函式的組成結構。假設b是乙個const string物件 class string const stri...
c 避免返回內部資料的控制代碼
c primer中說了,在乙個物件呼叫其成員函式時,它隱含的乙個形參this指標。例如,我們定義了乙個函式ctest ttt 實際上在編譯器中該函式的定義就是ctest ttt ctest const this 該this指標所指向的內容可以改變,但是該this指標不可以被改變。當我們用ctest的...