畫外音:今天是個大晴天,溫暖的陽光透過窗子照進了這間寬敞的辦公室,辦公室里三三兩兩的人們正在各自的計算機前努力工作,一切都顯得那么的安靜、祥和、有條不紊
……
“啊~!救命啊!Solmyr
你又用文件夾砸我!”
“愚蠢者是應該受到懲罰的。”
畫外音:
…… 呃,好吧,我得承認有點小小的例外。這里是一家軟件公司,發出慘叫的這位是
zero
,新進的大學生;這邊一臉優雅,看上去很有修養一點也不象剛剛砸過人的這位,是
Solmyr ,資深程序員,負責
zero 這一批新人的培訓。啊,故事開始了
……
“我干了什么啦?”zero
揉著鼻子問道,“這次你拿來砸我的文件夾又大了一號!”
“你過來自己看看你犯下的錯誤。”Solmyr
翻出了 zero
剛剛交上來的一段代碼:
……
char* msg = “Connectting ... Please wait“
……
if(
Status == S_CONNECTED )
strcpy(msg, “Connectted“);
……
“我犯了什么錯誤啦?這是一個很平凡的字符串聲明而已”,zero
不滿的說到。
“你看不出來嗎?connect
這個單詞的進行時和過去時你都拼錯了,多打了一個
t”,Solmyr
不緊不慢地回答。
“就為了這個你又用文件夾砸我
…… 啊!這次又是光盤盒!”
“這是商用軟件,你以為是在
QQ 上和
PPMM
聊天,有錯別字不要緊啊?更糟糕的是,我故意留了這么長的時間給你,到現在你還沒發現你真正的錯誤在什么地方。你可真不是一般的菜啊~”,Solmyr
故意拖了個長音,滿意的看到
zero
處于爆發的邊緣,“好吧,讓我們從基礎開始,C
語言中是怎樣處理字符串的?”
“這個我知道”,zero
顯得很有自信,“C/C++
語言中,字符串是一段連續的字符型內存單元,每個單元存放一個字符,并用\0
作為結尾的標記。”
“那么使用指針之前,我們應當
……”
“我們應當保證這個指針指向合法的內存,要么指向一塊已經存在的內存,要么為它動態分配一塊。”,zero
開始露出得意的笑容 ——
這種程度的問題,哈!
“好!那么你的代碼中
msg 這個指針指向哪里?”
笑容凝固了。
“這個
…… 呃 …… 我想 …… 它應該指向一塊合法內存,因為以前我這么做的時候,它能工作
……”,zero 期期艾艾的說。
“合法內存?這塊內存是誰分配的?它有多大?生存周期多長?有哪些特殊的性質?”
“……”
“唉!”,Solmyr
重重的嘆了口氣,“我就知道會這樣。好吧,讓我們先從簡單的開始。”。Solmyr
飛快的鍵入了如下代碼:
char
msg[] = “Hello“;
char* pmsg = (char*)malloc(
sizeof(“Hello“) );
strcpy(pmsg, “Hello“);
“上面這些代碼你應該都很清楚了:msg
是一個字符數組,C
語言保證會為它分配一段連續的內存,并將其初始化為
“Hello“ 。pmsg
是一個字符指針,我們調用了
malloc 函數為它動態分配了一塊內存,并用
strcpy 函數填充其值為
“Hello“
。這兩種做法的共通點是:首先用正常手段獲得一段內存,然后填充值。接著再來看這個:”
char* msg =
“Hello“;
“這一句代表什么意思?首先
msg 是個指針,C/C++
語言不負責為它分配一塊內存;其次我們也沒有顯式的為它分配一塊內存。它指向哪里?指向
“Hello“
,就是你直接寫在代碼里的那一個。”
“什么叫做‘直接寫在代碼里的那一個’?”,zero
露出了困惑的表情
“舉個例子你就明白了:”,Solmyr
再鍵入:
double
db = 1.5;
“這 一行里面,1.5
是個什么東西?它是一個 double
類型常量,C/C++
語言要處理它們,也要分配內存來存放這些東西。同理,當你在代碼里寫了
“Hello“ ,實際上
C/C++
語言就分配了一塊內存存放這個字符串,當你寫
char* msg = “Hello“
的時候,你就是把這樣一塊內存的地址賦給了指針
msg 。所以
msg
確實指向一塊合法內存,這是有時候這段代碼能夠工作的原因。但是這樣做,其中蘊涵了許多問題,我來問你,指向這塊內存的指針應該是什么類型?”
“當然是
char*”,zero
不加思索的回答。
“錯!應該是
const char*
。想當然耳,寫在程序中的字符串你不希望它發生變化,所以很明顯的,這塊內存應該被解釋為常量。但是你在聲明
msg 的時候做了什么?”
“呃
…… 我用了一個非常量的指針去指向了一個常量字符串。”,這一次,zero
明顯的審慎多了。
“正確。看你原來的代碼,你不僅用一個非常量指針指向它,而且還對這個指針執行了
strcpy
,往里寫了內容。在我們的編譯器上,這么做會引發什么后果?”
“呃
…… 引發一個運行時錯誤?”
“部分正確。準確的講,只有在工程編譯選項為調試版本的時候,如果工程編譯選項為發布版本,一切都很正常
—— 奇怪嗎?并不,記住這一點:C/C++
允許你打破任何保護。所以如果這兩行代碼在調試的時候沒有被發現而溜進了發布版本里”,說到這,Solmyr
狠狠的瞪了 zero
一眼,“將會是很難發現的。”
“可是說來說去這么做還是沒有什么危害不是嗎?msg
指向一塊合法內存,內容正確,而且也并不是真的不能寫入,有什么好擔心的呢?”,zero
抱怨道。
Solmyr
順手抓起杯子,zero
反射性的立刻縮頭護臉。“別擔心,我只是喝水而已。”,Solmyr
面無表情 —— 如果忽略他嘴角那一絲壞笑的話
—— 的說到,“沒有危害是嗎?看看下面的代碼:”
char* str1 =
“Hello“;
char* str2 = “Hello“;
*str1 = ‘P‘;
cout
<< str2 << endl;
“猜猜運行結果是什么?”,Solmyr
一邊調整工程設置,一邊問道。
“這還用問嗎?當然是輸出
Hello 了。”
“回答錯誤,正確答案是
……”,Solmyr
按下了運行按鈕,屏幕顯示的居然是
Pello !。
zero
大為詫異,撓著頭試圖找出其中的邏輯,突然間靈光一閃:“我明白了!str1
和 str2
實際指向同一段內存!因為 C/C++
語言在處理 Hello
字符串的時候把它當作常量,所以就做了優化,只保存了一份
Hello !是不是這樣!”zero
興奮的轉向 Solmyr。
“嗯,
看起來有時候你也不是那么菜么”,Solmyr
贊許的點頭,“不過你還是說錯了一點:這個不是
C/C++
語言的做法,是這個編譯器的做法。簡單的說,你如果要對這種字符串寫的話,其結果如何,是沒有定義的。所謂沒有定義,就是
C/C++
語言不保證會得到怎樣的結果,可能這樣也可能樣,完全決定于你的編譯器作者怎么想。想想看吧,哪天你的程序出現了古怪的問題
—— 比如顯示信息出現了混亂 ——
起因卻是你在無關的地方寫了一個字符串,會怎樣?這是維護時最大的惡夢之一。現在你明白危害在哪里了?”
zero
有如大夢初醒一般忙不迭地點頭:“我知道了,我知道了。”
“知道了還不快去改!”
……
zero
跑回坐位修改他的程序去了,辦公室里再度恢復了寧靜,所有的人都埋頭于他們的工作之中。只有
Solmyr
一邊喝著咖啡一邊揉著太陽穴,喃喃地吐出不祥的詞句:“這樣的日子才剛剛開始啊
……”