標籤:自身 擴充 簡便 htm 包括 ima const 讀取字串 看到了
C語言指標的陷阱
轉自:http://blog.csdn.net/porscheyin/article/details/3461670
“C語言詭異離奇,陷阱重重,卻獲得了巨大成功!”——C語言之父Dennis M.Ritchie。Ritchie大師的這句話體現了C語言的靈活性以及廣泛的使用,但也揭示了C是一種在應用時要時刻注意自己行為的語言。C的設計哲學還是那句話:使用C的程式員應該知道自己在幹什麼。有時用C寫的程式會出一些莫名其妙的錯誤,看似根源難尋,但仔細探究會發現很多錯誤的原因是概念不清。在我們經常掉進去的這些“陷阱”中,圍繞著指標的數量為最。這一講將對使用指標時遇到的一些問題做出分析,以避免在日後落入此類“陷阱”之中。
1.指標與字串常量
在第二講指標的初始化中提到可以將一個字串常量賦給一個字元指標。但有沒有朋友想過為什麼能夠這樣進行初始化呢?回答這個問題之前,我們先來搞清楚什麼是字串常量。字串常量是位於一對雙引號內部的字元序列(可以為空白)。
當一個字串常量出現於運算式中,除以下三種情況外:
1. 作為 &操作符的運算元;
2. 作為sizeof操作符的運算元;
3. 作為字元數組的初始化值
字串常量都會被轉化為由一個指標所指向的字元數組。例如:char *cp = "abcdefg";不滿足上述3個條件,所以"abcdefg"會被轉換為一個沒有名字的字元數組,這個數組被abcdefg和一個Null 字元‘/0‘初始化,並且會得到一個指標常量,它的值為第一個字元的地址,不過這些都是由編譯器來完成的。現在可以解釋用一個字串常量初始化一個字元指標的原因了,一個字串常量的值就是一個指標常量。那麼對於下面的語句,朋友們也不該感到迷惑了:
printf("%c\n",*"abcdefg");
printf("%c\n", *("abcdefg"+ 1));
printf("%c\n","abcdefg"[5]);
*"abcdefg":字串常量的值是一個指標常量,指向的是字串的第一個字元,對它解引用即可得到a;
*("abcdefg"+ 1):對這個指標進行算術運算則其指向下一個字元,再對它解引用,得到b;
"abcdefg"[5]:既然"abcdefg"是一個指標,那麼"abcdefg"[5]就可以寫成*("abcdefg" + 5),所以得到f。
回憶一下大家所學的初始化數組的方法:char ca[ ] = {‘a‘,‘b‘, ‘c‘,‘d‘, ‘e‘,‘f‘, ‘g‘,‘/0‘};這種方法實在太笨拙了,所以標準提供了一種快速方法用於初始化字元數組:char ca[ ] = "abcdefg";這個字串常量滿足了上面的第3條:用來初始化字元數組,所以不會被轉換為由一個指標所指向的字元數組。它只是用單個字元來初始化字元數組的簡便寫法。再來對比以下兩個聲明:
char ca[ ] = "abcdefg";
char *cp = "abcdefg";
它們的含義並不相同,前者是初始化一個字元數組的元素,後者才是一個真正的字串常量,如所示:
char ca[ ] = "abcdefg";
圖1
char *cp ="abcdefg";
圖2
要注意的是:用來初始化字元數組的字串常量,編譯器會在棧中為字元數組分配空間,然後把字串中的所有字元複製到數組中;而用來初始化字元指標的字串常量會被編譯器安排到唯讀資料存放區區,但也是按字元數組的形式來儲存的,2。我們可以通過一個字元指標讀取字串常量但不能修改它,否則會發生執行階段錯誤。正如下面的例子:
1.charca[ ] = "abcdefg";
2.char*cp = "abcdefg";
3.ca[0]= ‘b‘;
4.printf("%s\n", ca );
5.cp[0]= ‘b‘;
6.printf("%s\n", cp );
此程式第3行修改的不是唯讀資料區中的字串常量,而是由字串常量複製而來的存在於棧中的字元數組ca的一個元素。但第5行卻修改了用於初始化字元指標的位於唯讀資料區的字串常量,所以會發生執行階段錯誤。大家不要認為所有的字串常量都儲存在不同的地址,標準C允許編譯器為兩個包含相同字元的字串常量使用相同的儲存地址,而且現實中大多數廠商的編譯器也都是這麼做的。來看下面的程式:
charstr1[] = "abc";
charstr2[] = "abc";
char*str3 = "abc";
char*str4 = "abc";
printf("%d\n", str1 == str2 );
printf("%d\n",str3 == str4 );
輸出的結果是:0 1
str1,str2是兩個不同的字元數組,分別被初始化為"abc",它們在棧中有各自的空間;而str3,str4是兩個字元指標分別被初始化為包含相同字元的字串常量,它們指向相同的地區。
2.strlen( )和sizeof
請看下面程式:
char a[1000];
printf("%d\n",sizeof(a));
printf("%d\n",strlen(a));
這段代碼的輸出可不一定是1000, 0。sizeof(a)的結果一定是1000,但strlen(a)的結果就不能確定了。根本原因在於:strlen( )是一個函數,而sizeof是一個操作符,這導致了它們的種種不同:
1.sizeof可以用類型(需要用括弧括起來)或變數做運算元,而strlen( )只接受char*型字元指標做參數,並且該指標所指向的字串必須是以‘/0‘結尾的;
2.sizeof是操作符,對數組名使用sizeof時得到的是整個數組所佔記憶體的大小,而把數組名作為參數傳遞給strlen( )後數組名會被轉換為指向數組第一個元素的指標;
3.sizeof的結果在編譯期就確定了,而strlen( )是在運行時被調用。
由於上例中的數組a[1000]沒有初始化,所以數組內的元素及元素個數都是不確定的,可能是隨機值,所以用strlen(a)會得到不同的值,這取決於產生的隨機數,但sizeof的結果一定是1000,因為sizeof是在編譯時間擷取char a[1000]中char和1000這兩個資訊來計算空間的。
3.const指標與指向const的指標
對於常量指標(const pointer)和指標常量大家應該可以分清楚了。常量指標:指標本身的值不可以改變,可以把const理解為唯讀,如:int *const c_p;指標常量:一個指標類型的常量,如:(int *)0x123456ff。現在引入一個新的概念:指向const的指標,即一個指標它所指向的是一個const對象,如:const int *p_to_const; 表明p_to_const是一個指向constint型變數的指標,p_to_const自身的值是可以改變的,但是不能通過對p_to_const解引用來改變所指的對象的值,看下面的例子會更加清晰:
int *p = NULL; //定義一個整型指標並初始化為NULL
int i = 0; //定義一個整型變數並初始化為0
const int ci = 0; //定義一個唯讀整型變數並初始化,程式中不能再對它賦值
const int *p_to_const = NULL; //定義一個指向唯讀整型變數的指標,初始化為NULL
p = &i; //ok,讓p指向整型變數i
p_to_const = &ci; //ok,讓p_to_const指向ci
*p = 5; //ok,通過指標p修改i的值
*p_to_const = 5; /*error,p_to_const所指向的是一個唯讀變數,不能通過p_to_const對
ci進行修改*/
p_to_const = &i; //ok,讓指向const對象的指標指向普通對象
p_to_const = p; //ok,將指向普通對象的指標賦給指向const對象的指標
p = (int *) ? //ok,強制轉化為(int *)型,賦值操作符兩側運算元類型相同
p = (int *) p_to_const; //ok,同上
p = ? // error,錯誤原因下述
p = p_to_const; //error,同上
對於最後兩行的賦值,需要說明一下。C語言中對於指標的賦值操作(包括實參與形參之間的傳遞)應該滿足:兩個運算元都是指向有限定符或都是指向無限定符的類型相相容的指標;或者左邊指標所指向的類型具有右邊指標所指向的類型的全部限定符。例如const int *表示“指向一個具有const限定符的int類型的指標”,即const所修飾的是指標所指向的類型,而非指標。因此,p = ? 中的&ic得到的是一個指向const int型變數的指標,類型和p_to_const一樣。p_to_const所指向的類型為const int,而p所指向的類型為int,p在賦值操作符左邊,p_to_const在賦值操作符右邊,左邊指標所指向的類型並不具有右邊指標所指向類型的全部限定符,所以會出錯。
小擴充:{讓我們再深入一些,如果現在有一個指標int **bp和一個指標const int **cbp那麼這樣的賦值也時錯誤的:cbp = bp;因為const int **表示“指向有const限定符的int類型的指標的指標”。int ** 和constint **都是沒有限定符的指標類型,它們所指向的類型是不一樣的(int **指向int *,而constint **指向const int *),所以它們是不相容的,根據指標賦值條件來判斷,這兩個指標之間不能相互賦值。
實際上和const int **相相容的類型是const int**const,所以下面代碼是合法的:
const int * *const const_p_to_const = &p_to_const;
/*定義一個指向有const限定符的int類型的指標的常指標,它必需在定義時初始化,程式中不能再對它賦值。由於既不能修改指標的值也不能通過指標改變所指對象的值,所以在實際中,這種指標的用途並不廣*/
const int **cpp;
cpp = const_p_to_const;
左運算元cpp所指向的類型是const int*,右運算元const_p_to_const指向類型也為const int*,滿足指標賦值條件:左邊指標所指向的類型具有右邊指標所指向類型的全部限定符,只不過const_p_to_const是一個const指標,不能被再賦值,所以反過來是不能進行賦值的。還要注意被const限定的對象只能並且必需在聲明時初始化。}
4.C語言中的值傳遞
在第3將中提到過C語言只提供函數參數的傳值調用機制,即函數調用時,拷貝出一個實參的副本並把這個副本賦值給形參,從此實參與形參是各不相干的,形參在函數中的改變不會影響實參。我在前面說過C語言中所有非數組形式的資料實參(包括指標)均以傳值形式調用,這並不與C語言只提供傳值調用機制矛盾,對於數組形參會被轉換為指向數組首元素的指標,當我們用數組名作為實參時,實際進行的也是值傳遞。請看程式:
#include
void pass_by_value(char parameter[])
{
printf("形參的值: %p\n",parameter);
printf("形參的地址:%p\n", ¶meter);
printf("%s\n",parameter);
}
int main( )
{
charargument[100] = "C語言只有傳值調用機制!";
printf("實參的值: %p\n",argument);
pass_by_value(argument);
return0;
}
在我機器上的輸出結果為:實參的值: 0022FF00
形參的值: 0022FF00
形參的地址:0022FED0
C語言只有傳值調用機制!
當執行pass_by_value(argument);時,實參數組名argument被轉換為指向數組第一個元素的指標,這個指標的值為(void *)0022FF00,然後把這個值拷貝一份賦給形式參數parameter,形參parameter雖然被聲明為字元數組,但是會被轉換為一個指標,它是建立在棧上的一個獨立對象(它有自己獨立的地址)並接收實參值的那份拷貝。從而我們看到了實參與形參具有相同的值,並且形參有一個獨立的地址。再來看一個簡單的例子:
#include
void pointer_plus(char *p)
{
p+= 3;
}
int main( )
{
char*a = "abcd";
pointer_plus(a);
printf("%c\n", *a);
return0;
}
如果哪位朋友認為輸出是d,那麼你還是沒有搞清楚值傳遞的概念,此程式中將a拷貝一份賦給p,從此a和p就沒有關係了,在函數pointer_plus中增加p的值實際上增加的是a的那份拷貝的值,根本不會影響到a,在主函數中a仍舊指向字串的第一個字元,因此輸出為a。如果想讓pointer_plus改變a所指向的對象,採用二級指標即可,程式如下:
#include
void pointer_plus(char**p)
{
*p += 3;
}
int main( )
{
char*a = "abcd";
pointer_plus(&a);
printf("%c\n", *a);
return0;
}
5.垂懸指標(Dangling pointer)
垂懸指標是我們在使用指標時經常出現的,所謂垂懸指標就是指向了不確定的記憶體地區的指標,通常對這種指標進行操作會使程式發生不可預知的錯誤,因此我們應該避免在程式中出現垂懸指標,一些好的編程習慣可以協助我們減少這類事件的發生。
造成垂懸指標的原因通常分為三種,對此我們一個一個地進行討論。
第一種:在聲明一個指標時沒有對其初始化。在C語言中不會對所聲明的自動變數進行初始化,所以這個指標的預設值將是隨機產生的,很可能指向受系統保護的記憶體,此時如果對指標進行解引用,會引發執行階段錯誤。解決方案是在聲明指標時將其初始化為NULL或零指標常量。大家應該養成習慣為每個新建立的對象進行初始化,此時所做的些許工作會為你減少很多煩惱。
第二種:指向動態分配的記憶體的指標在被free後,沒有進行重新賦值就再次使用。就像下面的代碼:
int *p =(int *)malloc(4);
*p = 10;
printf("%d\n", *p);
free(p);
……
……
printf("%d\n",*p);
這就可能會引發錯誤,首先我們聲明了一個p並指向動態分配的一塊記憶體空間,然後通過p對此空間賦值,再通過free()函數把p所指向的那段記憶體釋放掉。注意free函數的作用是通過指標p把p所指向的記憶體空間釋放掉,並沒有把p釋放掉,所謂釋放掉就是將這塊記憶體中的對象銷毀,並把這塊記憶體交還給系統留作他用。指標p中的值仍是那塊記憶體的首地址,倘若此時這塊記憶體又被指派用於儲存其他的值,那麼對p進行解引用就可以訪問這個當前值,但如果這塊記憶體的狀態是不確定的,也許是受保護的,也許不儲存任何對象,這時如果對p解引用則可能出現執行階段錯誤,並且這個錯誤偵測起來非常困難。所以為了安全起見,在free一個指標後,將這個指標設定為NULL或零指標常量。雖然對null 指標解引用是非法的,但如果我們不小心對null 指標進行瞭解引用,所出現的錯誤在調試時比解引用一個指向未知物的指標所引發的錯誤要方便得多,因為這個錯誤是可預料的。
第三種:返回了一個指向局部變數的指標。這種造成垂懸指標的原因和第二種相似,都是造成一個指向曾經存在的對象的指標,但該對象已經不再存在了。不同的是造成這個對象不複存在的原因。在第二種原因中造成這個對象不複存在的原因是記憶體被手動釋放掉了,而在第三種原因中是因為指標指向的是一個函數中的局部變數,在函數結束後,局部變數被自動釋放掉了(無需程式員去手動釋放)。如下面的程式:
#include
#include
int*return_pointer()
{
int i=3;
int *p =&i;
return p;
}
int main()
{
int *rp = return_pointer();
printf("%d\n", *rp);
return 0;
}
在return_pointer函數中建立了一個指標p指向了函數內的變數i (在函數內建立的變數叫做局部變數),並且將這個指標作為傳回值。在主函數中有一個指標接收return_pointer的傳回值,然後對其解引用並輸出。此時的輸出可能是3,也可能是0,也可能是其他值。本質原因就在於我們返回了一個指向局部變數的指標,這個局部變數在函數結束後會被編譯器銷毀,銷毀的時間由編譯器來決定,這樣的話p就有可能指向不儲存任何對象的記憶體,也可能這段記憶體中是一個隨機值,總之,這塊記憶體是不確定的,p返回的是一個無效的地址。
C語言指標的陷阱