C語言指針的陷阱
分類: C/Cpp |
轉自: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和一個空字符'/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或零指針常量。雖然對空指針解引用是非法的,但如果我們不小心對空指針進行了解引用,所出現的錯誤在調試時比解引用一個指向未知物的指針所引發的錯誤要方便得多,因為這個錯誤是可預料的。
第三種:返回了一個指向局部變量的指針。這種造成垂懸指針的原因和第二種相似,都是造成一個指向曾經存在的對象的指針,但該對象已經不再存在了。不同的是造成這個對象不復存在的原因。在第二種原因中造成這個對象不復存在的原因是內存被手動釋放掉了,而在第三種原因中是因為指針指向的是一個函數中的局部變量,在函數結束后,局部變量被自動釋放掉了(無需程序員去手動釋放)。如下面的程序:
#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返回的是一個無效的地址。