2017-06-13 22 views
36

我最近開始用C語言編程一段時間後再次用C語言編程,我對指針的理解有點生疏。引用超出範圍的char *

我想問一下爲什麼這個代碼不會導致任何錯誤:

char* a = NULL; 
{ 
    char* b = "stackoverflow"; 
    a = b; 
} 

puts(a); 

我認爲這是因爲b出去的範圍,a應該引用不存在的內存位置,因此它們會撥打printf時出現運行時錯誤。

我在MSVC中運行這個代碼約20次,沒有顯示錯誤。

+23

這不是未定義的。字符串文字是靜態分配的。這段代碼非常好。 –

+9

換句話說,'b'已經超出了範圍,但沒有指向它。 –

+2

OTOH,如果你改爲:'char'['s','t','a','c','k','o','v','e' ,'r','f','l','o','w','\ 0'}'。 –

回答

46

b被定義的範圍內,它被分配了一個字符串文字的地址。這些文字通常位於內存的只讀部分,而不是堆棧。

當你做你a=bb分配給a,即a現在包含一個字符串的地址。該地址在b超出範圍後仍然有效。

如果您採取了地址b,然後試圖取消引用該地址,那麼您將調用undefined behavior

所以,你的代碼是有效的,不調用不確定的行爲,但下列情況:

int *a = NULL; 
{ 
    int b = 6; 
    a = &b; 
} 

printf("b=%d\n", *a); 

另一個更微妙的例子:

char *a = NULL; 
{ 
    char b[] = "stackoverflow"; 
    a = b; 
} 

printf(a); 

這個例子和你之間的區別是b,這是一個數組,衰退當指向a時指向第一個元素。所以在這種情況下,a包含一個局部變量的地址,然後超出範圍。

編輯:

作爲一個側面說明,這是不好的做法,傳遞變量作爲printf第一個參數,因爲這可能導致format string vulnerability。最好使用字符串常量如下:

printf("%s", a); 

或者更簡單地說:

puts(a); 
+0

感謝您的解釋和例子;真的幫助:) – MattMatt2000

+2

@CometEngine很高興我能幫上忙。如果您覺得它有用,請隨時[接受此答案](https://stackoverflow.com/help/accepted-answer)。 – dbush

+0

當然:) – MattMatt2000

11

一行行,這是你的代碼做什麼:

char* a = NULL; 

a是一個指針不引用任何內容(設置爲NULL)。

{ 
    char* b = "stackoverflow"; 

b是一個指針引用靜態的,恆定字符串文字"stackoverflow"

a = b; 

a被設置爲也引用靜態的,恆定字符串文字"stackoverflow"

} 

b超出範圍。但由於a而不是引用b,那麼這並不重要(它只是引用相同的靜態,常量字符串文字作爲引用b)。

printf(a); 

打印靜態常量字符串字面"stackoverflow"通過a引用。

+3

空指針不「指向NULL」。它*是* NULL。 –

+0

@凱特湯普森你說得對。固定。 – SiggiSv

10

字符串文字是靜態分配的,所以指針無限期地有效。如果你說char b[] = "stackoverflow",那麼你會在堆棧中分配一個char數組,當範圍結束時將會變爲無效。這種差異也顯示爲修改字符串:char s[] = "foo"堆棧分配一個字符串,你可以修改,而char *s = "foo"只給你一個指向一個字符串的指針,可以放在只讀內存中,所以修改它是未定義的行爲。

9

其他人解釋說這段代碼是完全有效的。這個答案是關於你的期望,如果代碼已經無效,那麼調用printf時就會出現運行時錯誤。不一定如此。

讓我們來看看在你的代碼這一變化,這無效:

#include <stdio.h> 
int main(void) 
{ 
    int *a; 
    { 
     int b = 42; 
     a = &b; 
    } 
    printf("%d\n", *a); // undefined behavior 
    return 0; 
} 

這一方案不確定的行爲,但它發生是相當有可能會,事實上,印刷品42 ,由於幾個不同的原因 - 許多編譯器將爲main的整個主體分配b的堆棧槽,因爲沒有其他需要空間並且最小化堆棧調整的次數簡化了代碼生成;即使編譯器正式釋放堆棧槽,數字42可能仍然保留在內存中,直到其他內容覆蓋它爲止,並且在a = &b*a之間沒有任何內容可以這樣做;標準優化(「常量和副本傳播」)可以消除這兩個變量,並將*a的最後一個已知值直接寫入printf語句(就像您寫入printf("%d\n", 42)一樣)。

理解「未定義的行爲」並不意味着「該程序會崩潰可預測」是絕對重要的。這意味着「任何事情都可能發生」,並且任何包括看起來像編程人員可能打算工作(在這臺計算機,與這個今天編譯器)似乎工作。


最後一點,沒有一個積極的調試工具我有(Valgrind的,牙山,UBSan)跟蹤「自動」足夠詳細,以便捕獲該錯誤變量壽命方便,但海灣合作委員會6確實產生這種有趣的警告:

$ gcc -std=c11 -O2 -W -Wall -pedantic test.c 
test.c: In function ‘main’: 
test.c:9:5: warning: ‘b’ is used uninitialized in this function 
    printf("%d\n", *a); // undefined behavior 
    ^~~~~~~~~~~~~~~~~~ 

我相信這裏發生的事情是,它沒有我上述優化 - 複製的b最後已知值到*a再進printf - 但它的「最後已知值」爲b是一個「這個變量是未初始化的」哨兵而不是42.(然後生成代碼相當於printf("%d\n", 0)。)

2

字符串常量總是靜態分配和程序可以隨時存取,

char* a = NULL; 
 
{ 
 
    char* b = "stackoverflow"; 
 
    a = b; 
 
} 
 

 
printf(a);

這裏存儲到字符串字面量計算器是由編譯器與分配它將內存分配給int/char變量或指針

區別在於字符串文字是READONLY節/段中的位置。 變量b在堆棧中分配,但它保存只讀段/段的內存地址。

在代碼var'b'中有字符串文字的地址。即使當b失去其範圍爲字符串文字的記憶將永遠被分配

注:分配給字符串文字記憶是二進制組成部分,一旦程序被卸載

參考ELF二進制規範更多的瞭解都將被刪除詳情

3

的代碼,因爲你只是指定字符指針b另一個字符指針a不會產生任何錯誤,這是完全正常的。

在C中,您可以將指針引用分配給另一個指針。這裏實際上字符串「stackoverflow」被用作文字,並且該字符串的基地址位置將被分配給a變量。

雖然您超出了變量b的範圍,但仍然使用指針a完成了任務。所以它會打印結果沒有任何錯誤。

2

我認爲,作爲以前答案的證明,最好看看代碼中真正存在的內容。人們已經提到字符串文字位於.text部分。所以,他們(文字)總是在那裏。您可以輕鬆地找到的這個代碼

#include <string.h> 

int main() { 
    char* a = 0; 
    { 
    char* b = "stackoverflow"; 
    a = c; 
    } 
    printf("%s\n", a); 
} 

使用以下命令

> cc -S main.c 

內主。是你會發現,在最底層

... 
... 
... 
     .section  __TEXT,__cstring,cstring_literals 
L_.str:         ## @.str 
     .asciz "stackoverflow" 

L_.str.1:        ## @.str.1 
     .asciz "%s\n" 

你可以閱讀更多關於彙編部分(例如)在這裏:https://docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html

在這裏,你可以找到的Mach-O可執行文件的非常充分的準備報道: https://www.objc.io/issues/6-build-tools/mach-o-executables/

+0

謝謝:)我也看過msvc反彙編窗口;似乎是同樣的事情 – MattMatt2000