C 語言中字元串常量的好處在哪裡?

標題中的問法可能不是很準確。我主要想問這樣一個問題。在C語言中如果我需要一個字元串的值可以這樣寫:

char* c = "Hello World";

如果我想要修改c中的某一個字元是不被允許的,只能通過字元數組來完成。因為"Hello World"是個字元串常量。在內存中唯一存在。

顯然在這樣的需求下,操作很繁瑣。所以我想要問的是這樣做的好處在哪裡?節省內存?還是其他什麼原因導致這樣的設計?

=======

我想我還是沒有表達清楚。在這樣的設計下直接修改字元串內容變得更複雜。那麼肯定有什麼原因導致這樣做會在其他方面變得更有利。否則就是沒事找事。我想知道這個原因是什麼?


先回答問題,再逐個澄清前面答案中不清楚的地方。

首先,問題問得很好,答案可能會比較無聊。C語言目前有兩個主要標準C89和C99。這兩個標準中對字元串常量的定義類似。要點:

  • 類型是char[] (原因不明,沒有看到過權威解釋,估計是歷史遺留。)
  • 連續存儲,末尾自動添加
  • 靜態存儲
  • 相同值常量的地址可以不同
  • 修改字元是未定義行為

來源:The C89 Draft和WG14/N1256 Septermber 7, 2007 ISO/IEC 9899:TC3。建議讀C99,更詳細清晰。

實際中,GCC會把字元串常量放到只讀的.rodata段中。舉最簡單的例子:

1 #include &

2 int main(void) {

3 char* s = "Hello world!";

4 s[0] = "L";

5 printf("%s
", s);

6 return 0;

7 }

運行

gcc -Wall -std=c99 test.c

objdump -s -j .rodata a.out

結果

Contents of section .rodata:

4005f8 01000200 48656c6c 6f20776f 726c6421 ....Hello world!

400608 00

代碼完全是合法的,編譯器不會抱錯,但程序運行會segmentation fault,因為.rodata和.text一樣是操作系統保護只讀的。這就是標準中所說的未定義行為。實際中大部分操作系統(沒有測試過)應該都會按照內存越權訪問處理。

這樣做的好處並不明顯。理論上fork的時候可以不拷貝.rodata,但主流的fork都實現了Copy-on-write,性能上應該差別不大了。其他的好處也不大。

簡單來說,這樣做既沒有什麼明顯的好處,也沒有太大的壞處,很可能只是Dennis Ritchie et al拍腦袋的一個決定,原因可能就是沒有原因。考慮到C並不是一個精心設計的語言,這種小問題並不少見。例如史上最昂貴的一個位元組的錯誤(The Most Expensive One-byte Mistake)。可惜DR老大已故去,恐怕無從考證了。

澄清一些前面答案中的問題。

@pansz

char* s = "Hello, world!"在C89和C99中都是完全合法的,GCC在-Wall下不會報錯,理由前面說了,字元串常量的類型在標準中就是char[]。為什麼不是const char[]?因為KR C里沒有const。這個寫法在C++03之前應該也是合法的(懶得去考證了),從C++03開始deprecated。除此之外,字元串常量的其他特性在C++03與C99中應該是一致的。

@陳良喬

根據標準(C或者C++)通過比較起始指針來判斷兩個字元串常量不相等是不靠譜的。這樣做倒是有一點實際意義,就是減少編譯器的全局分析負擔。每一個字元串常量都被默認在一個編譯單元內。判斷相等倒是沒問題。正確的做法還是strcmp或者指針賦值。

@Ivony

字元串常量所在的.rodata和.text(代碼)段一樣是由操作系統(loader)管理的,程序並不自行分配和銷毀,也無權修改。

另外C99中沒有完整的const correctness的定義,保護字元串常量不被修改也是操作系統的事。C++03做了一點改進。

@zhoutall

沒有什麼理由說棧上訪問字元串更快,如果考慮到初始化開銷(內存寫可以慢得要死)和緩存效應,棧訪問應該比常量慢,但實際中特別是優化後差別應該非常小。相比前兩個,malloc的分配開銷巨大(數十個ns,http://goog-perftools.sourceforge.net/doc/tcmalloc.html),肯定最慢。

@Tony

GCC放在.rodata段。理論上放在.text也可以,但是不清楚具體那個編譯器這麼干。


請習慣打開編譯警告,這可以解決很多問題。

char *s="hello";

這個寫法是語法錯誤的,編譯器會報告警告,正確的寫法是:

const char *s="hello";

按照正確的寫法,編譯正確的代碼可以運行正確。

(註:參見後文補充環節,這個寫法其實可以通過編譯,但既然我上面已經說錯話了,抹殺歷史是不好的,所以我還是保留了上面這段話。)

簡單的解釋,"hello" 是一個 const char * 類型的常量,他只能被賦值給 const char * 類型的變數,賦值給 char * 類型的變數從語法上來講是錯誤的。

那麼是否可以修改該變數的值呢?可以的。

const char *s="hello";
s = "world";

這個 s 是個變數,可以被賦值為不同的字元串常量。相同類型的數據之間可以賦值。s 指向的地址可以改變,但 s 指向地址中的內容不可通過 s 而改變。

要進一步說字元數組有什麼不同,我們可以看這個例子:

char a[] = "hello";
a = "world";

以上這樣的代碼會報錯,因為數組只能被初始化,不可以被賦值。可以把數組理解為下列形式:

char *const a = "hello"; 也就是說 a 指向的地址不能改變,但 a 的內容可以改變。

--

補充: 王寧的回答是正確的,原因在於我忽略了初始化變數與變數賦值是兩個不同的操作。

版本 A

char *s = "hello";

版本 B

char *s;
s = "hello"; /* 會有編譯器警告 */

雖然兩者看起來都是用字元串常量進行賦值,但是版本 A 的那個語句叫做初始化,版本 B 才是賦值。

使用字元串常量給非 const 類型賦值是不合適的(用最新的 C 標準來看);

用字元串常量給非 const 類型賦初值是可以的。

至於原因,王寧已經說了,因為最初的 C 標準裡面沒有 const 關鍵字,所以必須為這種用法提供一種可能性。允許用戶在沒有 const 的情況下正常操作字元串常量。

但我個人建議,在現在的編程中,不要將字元串常量用於非 const 的字元串。


「顯然在這樣的需求下,操作很繁瑣。」既然是需求,就不該對他進行繁瑣的操作。

設計的跟隨需求的。

-------------------------------------------------------

我理解你的問題,所以才會給你那樣的回復。「在這樣的設計下直接修改字元串內容變得更複雜」,所謂常量字元串就是常量,常量的設計就是保證內容只能訪問不能修改,而你要實現的是的要修改字元串內容?這和你的設計相悖啊?


首先要明確的一點是,我們需要的是一個字元串常量,所謂常量,你也知道,就是在運行期間不可修改的數據量。所以,你試圖通過c[0] = "a"去修改c所指向的一個常量字元串中的某個字元,這本身在邏輯上就是錯誤的,也就是說,你所謂的「顯然在這樣的需求下,操作很繁瑣」這個需求本身就是錯誤的,自然無法得到支持。如果你是在gcc下使用-Wall編譯選項,會得到如下的警告信息

警告: 不建議使用從字元串常量到『char*』的轉換 [-Wwrite-strings]

如果你希望可以在運行時刻修改某個字元串中的字元,你可以使用字元數組來保存字元串,而只有當你確認在整個運行時期都不會修改這個字元串中的字元時,你才應該選擇使用字元串常量。

這樣做的好處是什麼,你提到了,就是節省內存。多個相同的字元串常量只需要使用一個字元串就可以了。而之所以可以這樣做,是一這些字元串不會被修改,不可以被修改為前提的。


"Hello World"

這是一個表達式,其值是一個指針,這個指針指向了一個地址,這個地址可以按照字元串的方式來讀取。

在程序開始運行的時候,就會自動開闢一塊內存來儲存這幾個字元,並將這塊內存的地址,作為"Hello World"這個表達式的值。

很顯然,如果我們寫下printf( "Hello World" );輸出的卻是:"Hello Ivony",這會是非常令人匪夷所思而且無法理解的對吧?

但是如果"Hello World"這個指針所指向的內存區域,是可以修改的,那麼上面這種匪夷所思的情況就會出現。

把字元串常量的內存空間設置為只讀的是一種非常自然的設計,因為在C語言裡面,字元串表達式的值就是一個指針,如果這個指針指向的內存空間可以被修改,那就會出現當我們使用這個字元串的時候,它可能已經被別人改了?

而且事實上字元串本來就被設計為是個指針,那麼把它放在堆棧上,賦值的時候複製來複制去,以求能夠更改豈不是更不自然的設計?


沒有任何好處,而是需求。

這個問題就好像,吃飯有什麼好處一樣。答案是,不吃,你會餓。。。。

不管你怎麼寫,你都無法避免,你的代碼中一定會出現常量字元串!!!是的,你根本避不開這個問題。不過,只有這個字元串內容很敏感的時候,我們才會考慮在程序中隱藏他們,防止被搞反向工程的人將你輕鬆破解。

看了你的補充,你的意思是

char s[128];

char *p = s;

或者 char *p = (char*)malloc(128);

strcpy(p, "hello world");

這樣p不是可以修改了嗎,為什麼要搞出 char * p = "hello world" ,p[1] = "a" 不行呢。

答案是,前者你依然具有一個不可改寫的常量字元串,只不過你將它的內容複製到了一個可讀寫的緩衝區里去了。這樣你就有了一個可改寫的字元串。可是你的常量字元串依然存在。難道不是嘛!當你反編譯你的程序,是的,他們依然在只讀的segment 里。只要你在源代碼中眼睛看到的任何用雙引號括起來的字元串,你會發現編譯器都把他們彙集存儲在那了。

一種可能讓上面的那個hello word不會出現,

p[0]= "h";

p[1]="e";

..

我只是說有可能。假設編譯器忠實的翻譯上面的代碼,那麼hello world 的確可以不出現在只讀數據段。不排除編譯器有可能會它實現成和常量字元串一模一樣。


C語言很古老很低級的,其語法不像現在主流的動態語言那麼接近人類語言的表達習慣。所以不很好的掌握其語法,有時確實理解起來會很彆扭。

具體到這個問題,簡單說,當你在代碼中不管什麼地方用什麼樣的方式寫下"hello world"時,編譯器總會在生成的目標文件的數據段靜態的且不可修改的存一份拷貝。而在運行時,根據聲明字元串語法的不同,代碼的執行行為是有差異的。比如寫char *a = "...", 運行時(精確點,應是load的時候)只是簡單的把a的內容初始化成數據段的映射地址即可;寫成char a[]="...",運行時會先計算字元串長度,然後在棧上分配空間,再把數據段的內容拷貝過來。(大體原理如此,編譯器實現差異很大,比如有些實現,全局數組就不需要初始化過程)。

了解一下compile/link/load 的過程,非常有助於理解c語言。

忘了回答問題,那樣設計的目的,我覺得是為了靈活和高效。


很抱歉之前的回答有明顯錯誤,已修改。

E1:

char str[] = "hello, world";
str[1] = "a";

E2:

char *str = "hello, world";
str[1] = "a";

兩個程序都可以編譯,但第二個運行會出現段錯誤。兩個程序的區別在,第二個程序的 str 屬於已初始化變數,str 如果是局部變數則指向棧上的內存區域,如果是 static 或全局變數則指向進程的 data 段內存區域。data 段許可權是可讀可寫;第一個程序中 "hello, world" 是一個字元串面量,str 的確指向其地址,但該地址存在於在進程的 text 段,text 段除了保存常量還保存可執行代碼,因此是不允許可寫許可權的,而是只允許可讀可執行許可權。


啊哈,正好我最近研究過這些。

char p[] = "hello"; //1
char *p = "hello"; //2
char *p;
p = (char *)malloc(sizeof(char)*6);
strcpy(p, "hello"); //3

這三種情況下:

1中所有6個char字元都連續的存放在棧區。

2中的"Hello"存在程序內存的常量區中,是編譯時就固定下來的(不可更改),然後p是一個指向常量區"hello"的指針,p本身存在棧區。

3中malloc向堆申請了空間,p存放在棧區,指向malloc申請出來的地址,最後"hello"就被copy到了p所指向的地址。

從速度來看1中棧的數據都是直接讀的,另外兩種都需要通過指針間接讀取,所以1顯然是最快的。

我覺得首先如果字元串很小且確定,可以用1的寫法,在棧區速度快。

如果字元串很大或者不確定,要知道棧區大小是有限的,所以採用3的動態分配比較好。

如果字元串被大量復用,其實可以採用2中寫法,這樣只要引用了常量區的同一字元串,他們將會共用同一塊地址。(當然這種共用是合理的,因為那裡的字元串是不可修改的,且到程序結束才會被釋放)。


你應該問自己:

為什麼會有常量和變數?

既然定義為常量,你為什麼又要修改?

既然要修改,為什麼不定義變數?


推薦閱讀:

C 語言如何判斷等差數列?
在C語言中,math.h中定義的各種數學函數在電腦上具體是怎麼實現的?
數據結構中所講的動態分配的數組如何在 C 語言中實現?
c語言是否可以通過調用void函數來完成對數組的賦值?
C編譯器用什麼語言寫的?

TAG:編程 | C編程語言 | CC |