国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

C語言指針導(dǎo)學(xué)(6)——使用指針時的“陷阱”

2019-11-06 06:03:40
字體:
供稿:網(wǎng)友
 

C語言指針導(dǎo)學(xué)(6)——使用指針時的“陷阱”

     六.使用指針時的“陷阱”

“C語言詭異離奇,陷阱重重,卻獲得了巨大成功!”——C語言之父Dennis M.Ritchie。Ritchie大師的這句話體現(xiàn)了C語言的靈活性以及廣泛的使用,但也揭示了C是一種在應(yīng)用時要時刻注意自己行為的語言。C的設(shè)計哲學(xué)還是那句話:使用C的程序員應(yīng)該知道自己在干什么。有時用C寫的程序會出一些莫名其妙的錯誤,看似根源難尋,但仔細(xì)探究會發(fā)現(xiàn)很多錯誤的原因是概念不清。在我們經(jīng)常掉進(jìn)去的這些“陷阱”中,圍繞著指針的數(shù)量為最。這一講將對使用指針時遇到的一些問題做出分析,以避免在日后落入此類“陷阱”之中。

1.指針與字符串常量

在第二講指針的初始化中提到可以將一個字符串常量賦給一個字符指針。但有沒有朋友想過為什么能夠這樣進(jìn)行初始化呢?回答這個問題之前,我們先來搞清楚什么是字符串常量。字符串常量是位于一對雙引號內(nèi)部的字符序列(可以為空)。

當(dāng)一個字符串常量出現(xiàn)于表達(dá)式中,除以下三種情況外:

1.作為 &操作符的操作數(shù);

2.作為sizeof操作符的操作數(shù);

3.作為字符數(shù)組的初始化值

字符串常量都會被轉(zhuǎn)化為由一個指針?biāo)赶虻淖址麛?shù)組。例如:char *cp = "abcdefg";不滿足上述3個條件,所以"abcdefg"會被轉(zhuǎn)換為一個沒有名字的字符數(shù)組,這個數(shù)組被abcdefg和一個空字符'/0'初始化,并且會得到一個指針常量,它的值為第一個字符的地址,不過這些都是由編譯器來完成的?,F(xiàn)在可以解釋用一個字符串常量初始化一個字符指針的原因了,一個字符串常量的值就是一個指針常量。那么對于下面的語句,朋友們也不該感到迷惑了:

PRintf("%c/n",*"abcdefg");

        printf("%c/n", *("abcdefg"+ 1));

        printf("%c/n","abcdefg"[5]);

        *"abcdefg":字符串常量的值是一個指針常量,指向的是字符串的第一個字符,對它解引用即可得到a;

    *("abcdefg"+ 1):對這個指針進(jìn)行算術(shù)運算則其指向下一個字符,再對它解引用,得到b;    "abcdefg"[5]:既然"abcdefg"是一個指針,那么"abcdefg"[5]就可以寫成*("abcdefg" + 5),所以得到f。

回憶一下大家所學(xué)的初始化數(shù)組的方法:char ca[ ] = {'a','b','c','d','e','f','g','/0'};這種方法實在太笨拙了,所以標(biāo)準(zhǔn)提供了一種快速方法用于初始化字符數(shù)組:char ca[ ] = "abcdefg";這個字符串常量滿足了上面的第3條:用來初始化字符數(shù)組,所以不會被轉(zhuǎn)換為由一個指針?biāo)赶虻淖址麛?shù)組。它只是用單個字符來初始化字符數(shù)組的簡便寫法。再來對比以下兩個聲明:

char ca[ ] = "abcdefg";

char *cp = "abcdefg";

它們的含義并不相同,前者是初始化一個字符數(shù)組的元素,后者才是一個真正的字符串常量,如下圖所示:

                 char ca[ ] = "abcdefg";

                                     

                                                                         圖1

 

                char *cp ="abcdefg";

              

                                                         圖2

要注意的是:用來初始化字符數(shù)組的字符串常量,編譯器會在棧中為字符數(shù)組分配空間,然后把字符串中的所有字符復(fù)制到數(shù)組中;而用來初始化字符指針的字符串常量會被編譯器安排到只讀數(shù)據(jù)存儲區(qū),但也是按字符數(shù)組的形式來存儲的,如圖2。我們可以通過一個字符指針讀取字符串常量但不能修改它,否則會發(fā)生運行時錯誤。正如下面的例子:

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行修改的不是只讀數(shù)據(jù)區(qū)中的字符串常量,而是由字符串常量復(fù)制而來的存在于棧中的字符數(shù)組ca的一個元素。但第5行卻修改了用于初始化字符指針的位于只讀數(shù)據(jù)區(qū)的字符串常量,所以會發(fā)生運行時錯誤。大家不要認(rèn)為所有的字符串常量都存儲在不同的地址,標(biāo)準(zhǔn)C允許編譯器為兩個包含相同字符的字符串常量使用相同的存儲地址,而且現(xiàn)實中大多數(shù)廠商的編譯器也都是這么做的。來看下面的程序:

        charstr1[] = "abc";

        charstr2[] = "abc";

        char*str3 = "abc";

        char*str4 = "abc";

        printf("%d/n", str1 == str2 );

        printf("%d/n",str3 == str4 );

    輸出的結(jié)果是:0 1

str1,str2是兩個不同的字符數(shù)組,分別被初始化為"abc",它們在棧中有各自的空間;而str3,str4是兩個字符指針分別被初始化為包含相同字符的字符串常量,它們指向相同的區(qū)域。

2.strlen( )和sizeof

請看下面程序:

char a[1000];

printf("%d/n",sizeof(a));

printf("%d/n",strlen(a));

這段代碼的輸出可不一定是1000, 0。sizeof(a)的結(jié)果一定是1000,但strlen(a)的結(jié)果就不能確定了。根本原因在于:strlen( )是一個函數(shù),而sizeof是一個操作符,這導(dǎo)致了它們的種種不同:

1.sizeof可以用類型(需要用括號括起來)或變量做操作數(shù),而strlen( )只接受char*型字符指針做參數(shù),并且該指針?biāo)赶虻淖址仨毷且?/0'結(jié)尾的;

2.sizeof是操作符,對數(shù)組名使用sizeof時得到的是整個數(shù)組所占內(nèi)存的大小,而把數(shù)組名作為參數(shù)傳遞給strlen( )后數(shù)組名會被轉(zhuǎn)換為指向數(shù)組第一個元素的指針;

3.sizeof的結(jié)果在編譯期就確定了,而strlen( )是在運行時被調(diào)用。

由于上例中的數(shù)組a[1000]沒有初始化,所以數(shù)組內(nèi)的元素及元素個數(shù)都是不確定的,可能是隨機值,所以用strlen(a)會得到不同的值,這取決于產(chǎn)生的隨機數(shù),但sizeof的結(jié)果一定是1000,因為sizeof是在編譯時獲取char a[1000]中char和1000這兩個信息來計算空間的。

3.const指針與指向const的指針

對于常量指針(const pointer)和指針常量大家應(yīng)該可以分清楚了。常量指針:指針本身的值不可以改變,可以把const理解為只讀的,如:int *const c_p;指針常量:一個指針類型的常量,如:(int *)0x123456ff?,F(xiàn)在引入一個新的概念:指向const的指針,即一個指針?biāo)赶虻氖且粋€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進(jìn)行修改

        p_to_const = &i;                                  //ok,讓指向const對象的指針指向普通對象

        p_to_const = p;                                   //ok,將指向普通對象的指針賦給指向const對象的指針

        p = (int *) ⁣                                      //ok,強制轉(zhuǎn)化為(int *)型,賦值操作符兩側(cè)操作數(shù)類型相同

p = (int *) p_to_const;                         //ok,同上

p = ⁣                                              //error,錯誤原因下述

p = p_to_const;                                  //error,同上

對于最后兩行的賦值,需要說明一下。C語言中對于指針的賦值操作(包括實參與形參之間的傳遞)應(yīng)該滿足:兩個操作數(shù)都是指向有限定符或都是指向無限定符的類型相兼容的指針;或者左邊指針?biāo)赶虻念愋途哂杏疫呏羔標(biāo)赶虻念愋偷娜肯薅ǚ@鏲onst int *表示“指向一個具有const限定符的int類型的指針”,即const所修飾的是指針?biāo)赶虻念愋?,而非指針。因此,p = ⁣ 中的&ic得到的是一個指向const int型變量的指針,類型和p_to_const一樣。p_to_const所指向的類型為const int,而p所指向的類型為int,p在賦值操作符左邊,p_to_const在賦值操作符右邊,左邊指針?biāo)赶虻念愋筒⒉痪哂杏疫呏羔標(biāo)赶蝾愋偷娜肯薅ǚ?,所以會出錯。

    小擴(kuò)展:{讓我們再深入一些,如果現(xiàn)在有一個指針int **bp和一個指針const int **cbp那么這樣的賦值也是錯誤的:cbp = bp;因為const int **表示“指向有const限定符的int類型的指針的指針”。int ** 和const int **都是沒有限定符的指針類型,它們所指向的類型是不一樣的(int **指向int *,而const int **指向const int *),所以它們是不兼容的,根據(jù)指針賦值條件來判斷,這兩個指針之間不能相互賦值。

實際上和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;

左操作數(shù)cpp所指向的類型是const int*,右操作數(shù)const_p_to_const指向類型也為const int*,滿足指針賦值條件:左邊指針?biāo)赶虻念愋途哂杏疫呏羔標(biāo)赶蝾愋偷娜肯薅ǚ?,只不過const_p_to_const是一個const指針,不能被再賦值,所以反過來是不能進(jìn)行賦值的。還要注意被const限定的對象只能并且必需在聲明時初始化。}

4.C語言中的值傳遞

在第3將中提到過C語言只提供函數(shù)參數(shù)的傳值調(diào)用機制,即函數(shù)調(diào)用時,拷貝出一個實參的副本并把這個副本賦值給形參,從此實參與形參是各不相干的,形參在函數(shù)中的改變不會影響實參。我在前面說過C語言中所有非數(shù)組形式的數(shù)據(jù)實參(包括指針)均以傳值形式調(diào)用,這并不與C語言只提供傳值調(diào)用機制矛盾,對于數(shù)組形參會被轉(zhuǎn)換為指向數(shù)組首元素的指針,當(dāng)我們用數(shù)組名作為實參時,實際進(jìn)行的也是值傳遞。請看程序:

#include <stdio.h>

 

void pass_by_value(char parameter[])

{

printf("形參的值:  %p/n",parameter);

printf("形參的地址:%p/n", &parameter);

printf("%s/n",parameter);

}

 

int main( )

{

          charargument[100] = "C語言只有傳值調(diào)用機制!";

   printf("實參的值:  %p/n",argument);

   pass_by_value(argument);

          return0;

}

在我機器上的輸出結(jié)果為:實參的值:  0022FF00

形參的值:  0022FF00

形參的地址:0022FED0

C語言只有傳值調(diào)用機制!

當(dāng)執(zhí)行pass_by_value(argument);時,實參數(shù)組名argument被轉(zhuǎn)換為指向數(shù)組第一個元素的指針,這個指針的值為(void *)0022FF00,然后把這個值拷貝一份賦給形式參數(shù)parameter,形參parameter雖然被聲明為字符數(shù)組,但是會被轉(zhuǎn)換為一個指針,它是創(chuàng)建在棧上的一個獨立對象(它有自己獨立的地址)并接收實參值的那份拷貝。從而我們看到了實參與形參具有相同的值,并且形參有一個獨立的地址。再來看一個簡單的例子:

#include <stdio.h>

 

void pointer_plus(char *p)

{

           p+= 3;

}

 

int main( )

{

          char*a = "abcd";

     pointer_plus(a);

          printf("%c/n", *a);

          return0;

}

如果哪位朋友認(rèn)為輸出是d,那么你還是沒有搞清楚值傳遞的概念,此程序中將a拷貝一份賦給p,從此a和p就沒有關(guān)系了,在函數(shù)pointer_plus中增加p的值實際上增加的是a的那份拷貝的值,根本不會影響到a,在主函數(shù)中a仍舊指向字符串的第一個字符,因此輸出為a。如果想讓pointer_plus改變a所指向的對象,采用二級指針即可,程序如下:

#include <stdio.h>

 

void pointer_plus(char**p)

{

           *p += 3;

}

 

int main( )

{

          char*a = "abcd";

     pointer_plus(&a);

          printf("%c/n", *a);

          return0;

}

5.垂懸指針(Dangling pointer)

垂懸指針是我們在使用指針時經(jīng)常出現(xiàn)的,所謂垂懸指針就是指向了不確定的內(nèi)存區(qū)域的指針,通常對這種指針進(jìn)行操作會使程序發(fā)生不可預(yù)知的錯誤,因此我們應(yīng)該避免在程序中出現(xiàn)垂懸指針,一些好的編程習(xí)慣可以幫助我們減少這類事件的發(fā)生。

造成垂懸指針的原因通常分為三種,對此我們一個一個地進(jìn)行討論。

第一種:在聲明一個指針時沒有對其初始化。在C語言中不會對所聲明的自動變量進(jìn)行初始化,所以這個指針的默認(rèn)值將是隨機產(chǎn)生的,很可能指向受系統(tǒng)保護(hù)的內(nèi)存,此時如果對指針進(jìn)行解引用,會引發(fā)運行時錯誤。解決方法是在聲明指針時將其初始化為NULL或零指針常量。大家應(yīng)該養(yǎng)成習(xí)慣為每個新創(chuàng)建的對象進(jìn)行初始化,此時所做的些許工作會為你減少很多煩惱。

第二種:指向動態(tài)分配的內(nèi)存的指針在被free后,沒有進(jìn)行重新賦值就再次使用。就像下面的代碼:

          int *p =(int *)malloc(4);

         *p = 10;

printf("%d/n", *p);

         free(p);

         ……

         ……

printf("%d/n",*p);

這就可能會引發(fā)錯誤,首先我們聲明了一個p并指向動態(tài)分配的一塊內(nèi)存空間,然后通過p對此空間賦值,再通過free()函數(shù)把p所指向的那段內(nèi)存釋放掉。注意free函數(shù)的作用是通過指針p把p所指向的內(nèi)存空間釋放掉,并沒有把p釋放掉,所謂釋放掉就是將這塊內(nèi)存中的對象銷毀,并把這塊內(nèi)存交還給系統(tǒng)留作他用。指針p中的值仍是那塊內(nèi)存的首地址,倘若此時這塊內(nèi)存又被指派用于存儲其他的值,那么對p進(jìn)行解引用就可以訪問這個當(dāng)前值,但如果這塊內(nèi)存的狀態(tài)是不確定的,也許是受保護(hù)的,也許不保存任何對象,這時如果對p解引用則可能出現(xiàn)運行時錯誤,并且這個錯誤檢測起來非常困難。所以為了安全起見,在free一個指針后,將這個指針設(shè)置為NULL或零指針常量。雖然對空指針解引用是非法的,但如果我們不小心對空指針進(jìn)行了解引用,所出現(xiàn)的錯誤在調(diào)試時比解引用一個指向未知物的指針?biāo)l(fā)的錯誤要方便得多,因為這個錯誤是可預(yù)料的。

第三種:返回了一個指向局部變量的指針。這種造成垂懸指針的原因和第二種相似,都是造成一個指向曾經(jīng)存在的對象的指針,但該對象已經(jīng)不再存在了。不同的是造成這個對象不復(fù)存在的原因。在第二種原因中造成這個對象不復(fù)存在的原因是內(nèi)存被手動釋放掉了,而在第三種原因中是因為指針指向的是一個函數(shù)中的局部變量,在函數(shù)結(jié)束后,局部變量被自動釋放掉了(無需程序員去手動釋放)。如下面的程序:

#include<stdio.h>

#include<stdlib.h>

 

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函數(shù)中創(chuàng)建了一個指針p指向了函數(shù)內(nèi)的變量i (在函數(shù)內(nèi)創(chuàng)建的變量叫做局部變量),并且將這個指針作為返回值。在主函數(shù)中有一個指針接收return_pointer的返回值,然后對其解引用并輸出。此時的輸出可能是3,也可能是0,也可能是其他值。本質(zhì)原因就在于我們返回了一個指向局部變量的指針,這個局部變量在函數(shù)結(jié)束后會被編譯器銷毀,銷毀的時間由編譯器來決定,這樣的話p就有可能指向不保存任何對象的內(nèi)存,也可能這段內(nèi)存中是一個隨機值,總之,這塊內(nèi)存是不確定的,p返回的是一個無效的地址。


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 仪征市| 鄂托克旗| 肇庆市| 贞丰县| 江川县| 赤城县| 会同县| 蒲江县| 宝山区| 南川市| 武陟县| 曲靖市| 昌都县| 镇远县| 鹿邑县| 长垣县| 郑州市| 霍邱县| 卢龙县| 武穴市| 宣威市| 勃利县| 温宿县| 钟祥市| 泗水县| 建宁县| 黄山市| 陕西省| 江达县| 响水县| 遂昌县| 呼图壁县| 普兰店市| 铜山县| 青海省| 武冈市| 玛多县| 梁河县| 阿勒泰市| 江陵县| 大埔县|