《C和指针》读书笔记-第六章指针
寫在前面,由于學過C語言,導致想要跳躍式地翻閱《C和指針》,后來發現這實為錯誤,對于這本經典著作,要做的是從頭到尾保持體系的完整性。
《C和指針》配套代碼請移步網站:Pointers on C
作者Kenneth A. Reek的個人網站
文章目錄
- 6.1 內存和地址
- 地址和內容
- 6.2 值和類型
- 6.3 指針變量的內容
- 6.4 間接訪問操作符
- 6.5 未初始化和非法的指針
- 6.6 NULL指針
- 6.7 指針、間接訪問和左值
- 6.8 指針、間接訪問和變量
- 6.9 指針常量
- 6.10指針的指針
- 6.11指針表達式
- 6.12 實例
- 6.13 指針運算
- 6.13.1 算術運算
- 6.13.2 關系運算
- 6.14 警告的總結
- 6.15 編程提示的總結
6.1 內存和地址
我們可以把計算機的內存看作是一條長街上的一排房屋。每座房子都可以容納數據,并通過一個房號來標識。
這個比喻頗為有用,但也存在局限性。計算機的內存由數以億萬計的位(bit)組成,每個位可以容納0或1.由于一個位所能表示的值的范圍太有限,所以單獨的位用處不大,通常許多位合成一組作為一個單位,這樣就可以存儲范圍較大的值。這里有一幅圖,展示了現實機器中的一些內存位置。
這些位置的每一個都被稱為字節(byte) ,每個字節都包含了存儲一個字符所需要的位數。在許多現代機器上,每個字節包含8個位,可以存儲無符號值0~255,或者有符號值-128 ~127.上面這張圖并沒有顯示這些位置的內容,但內存中的每個位置總是包含一些值。每個字節通過地址來標識,如上圖方框上面的數字所示。
為了存儲更大的值,我們把兩個或更多個字節合在一起作為一個更大的內存單位。例如,許多機器以字為單位存儲整數,每個字一般由2個或4個字節組成。下面這張圖所示的內存位置與上面這張圖相同,但這次它以4個字節的字表示。
由于它們包含了更多的位,每個字可以容納的無符號整數的范圍是從0至4294967295(232?12^{32}-1232?1),可以容納的有符號整數的范圍是從-2147483648(?231-2^{31}?231)至2147483647(231?12^{31}-1231?1).
注意,盡管一個字包含了4個字節,它仍然只有一個地址。至于它的地址是從它最左邊那個字節的位置還是最右邊那個字節的位置,不同的機器有不同的規定。另一個需要注意的硬件事項是邊界對齊(boundary alignment)。在要求邊界對齊的機器上,整型值存儲的起始位置只能是某些特定的字節,通常是2或4的倍數。但這些問題是硬件設計者的事情,它們很少影響C程序員。我們只對兩件事情感興趣:
地址和內容
這里有另外一個例子,這次它顯示了內存中5個字的內容。
這里顯示了5個整數,每個都位于自己的字中。如果你記住了一個值的存儲地址,以后可以根據這個地址取得這個值。
但是,要記住所有這些地址太笨拙了,所以高級語言所提供的特性之一就是通過名字而不是地址來訪問內存的位置。下面這張圖與上圖相同,但這次使用名字來代替地址。
當然,這些名字就是我們所稱的變量。有一點非常重要,你必須記住,名字與內存位置之間的關聯并不是硬件所提供的,它是由編譯器為我們實現的。所有這些變量給了我們一種更方便的方法記住地址-------硬件仍然通過地址訪問內存位置。
6.2 值和類型
現在讓我們來看一下存儲于這些位置的值。頭兩個位置所存儲的是整數。第三個位置所存儲的是一個非常大的整數,第4、5個位置所存儲的也是整數。下面是這些變量的聲明
int a=112,b=-1; float c=3.14; int *d=&a; float *e=&c;在這些聲明中,變量a和b確實用于存儲整型值。但是,它聲明的所存儲的是浮點值。可是,在上圖中c的值卻是一個整數。那么到底它是哪個呢? 整數還是浮點數?
答案是該變量包含了一序列內容為0或1的位。它們可以被解釋為整數,也可以被解釋為浮點數,這取決于它們被使用的方式。如果使用的是整型算術指令,這個值就被解釋為整型,如果使用的是浮點型指令,它就是個浮點型。
這個事實引出了一個重要結論:不能簡單地通過檢查一個值的位來判斷它的類型。 為了判斷值的類型(以及它的值),必須觀察程序中這個值的使用方式。考慮下面這個二進制形式表示的32位值:
01100111011011000110111101100010下面是這些位可能被解釋的許多結果中的幾種。這些值都是從一個基于Motorola 68000 的處理器上得到的。如果換個系統,使用不同的數據格式和指令,對這些位的解釋又將有所不同。
這里,一個單一的值可以被解釋為5個不同的類型。顯然,值的類型并非值本身所固有的一種特性,而是取決于它的使用方式。
6.3 指針變量的內容
讓我們把話題返回到指針,看看變量d和e 的聲明。它們都被聲明為指針,并用其他變量的地址予以初始化。指針的初始化是用&操作符完成的,它用于產生操作數的內存地址。
d和e的內容是地址而不是整型或浮點數數值。事實上,從圖中可以容易地看到,d 的內容與a 的存儲地址一致,而e 的內容和c的存儲地址一致,這也正是我們對這兩個指針進行初始化時所期望的結果。 區分變量d的地址(112)和它的內容(100)是非常重要的,同時也必須意識到100這個數值用于標識其他位置(是……的地址)。
在我們轉到下一步之前,先看一些涉及這些變量的表達式。
int a=112,b=-1; float c=3.14; int *d=&a; float *e=&c;下面這些表達式的值分別是什么呢?
a b c d e前三個非常容易,a=112,b=-1,c=3.14.指針變量其實也很容易,d的值是100,e的值是108.
6.4 間接訪問操作符
通過一個指針訪問它所指向的地址的過程稱為間接訪問(indirection) 或解引用指針(dereferencing the pointer) 。這個用于執行間接訪問的操作符是單目運算符*。
下面的聲明和前面相同
d的值是100.當我們對d使用間接訪問操作符時,它表示訪問內存位置100并察看那里的值。因此,*d的右值是112-----位置100的內容,它的左值是位置100本身。
注意上面列表中各個表達式的類型:d是一個指向整型的指針,對它進行解引用操作將產生一個整型值。類似地,對float* 進行間接訪問將產生一個float型值。
正常情況下,我們并不知道編譯器為每個變量所選擇的存儲位置,所以我們事先無法預測它們的地址。這樣,當我們繪制內存中的指針圖時,用實際數值表示地址是不方便的。所以,絕大部分書改用箭頭來代替,如下所示:
但是,這種記法可能會引起誤解,因為箭頭可能會使你誤以為執行了間接訪問操作,但事實上,它并不一定會執行這個操作。例如,根據上圖,你會推斷表達式d的值是什么?
如果你的答案是112,那么你就被這個箭頭誤導了。正確的答案是a 的地址,而不是它的內容。但是,這個箭頭似乎會把你的注意力吸引到a上。要使你的思維不受箭頭的影響是不容易的,這也是問題所在:除非存在間接訪問操作符,否則不要被箭頭所誤導。
下面這個修正后的箭頭記法試圖消除這個問題。
這種記法的意圖是既顯示指針的值,但又不給你強烈的視覺線索,以為這個箭頭是我們必須遵從的路徑。事實上,如果不對指針變量進行間接訪問操作,它的值只是簡單的一些位的集合。當執行間接訪問操作時,這種記法才使用實線箭頭表示實際發生的內存訪問。
注意箭頭起始位置在方框內部,因為它表示存儲于該變量的值。同樣,箭頭指向一個位置,而不是存儲于該位置的值。這種記法提示跟隨箭頭執行間接訪問操作的結果將是一個左值。
盡管這種箭頭記法很有用,但為了正確使用它,你必須記住指針變量的值就是一個數字。箭頭顯示了這個數字的值,但箭頭記法并未改變它本身就是個數字的事實。指針并不存在內建的間接訪問屬性,所以除非表達式中存在間接訪問操作符,否則你不能按箭頭所示實際訪問它所指向的位置。
6.5 未初始化和非法的指針
下面這個代碼段說明了一個極為常見的錯誤:
int *a; *a=12;這個聲明創建了一個名為a的指針變量,后面那條賦值語句把12存儲在a所指向的內存位置。
警告:
但是究竟a指向哪里呢?我們聲明了這個變量,但從未對它進行初始化,所以我們沒有辦法預測12這個值將存儲于什么地方。從這一點看,指針變量和其他變量并無區別。如果變量是靜態的,它會被初始化為0;但如果變量是自動的,它根本不會被初始化。無論是哪種情況,聲明一個指向整型的指針都不會“創建”用于存儲整形值的內存空間。
所以,如果程序執行這個賦值操作,會發生什么情況呢? 如果你運氣好,a的初始值會是個非法地址,這樣賦值語句將會出錯,從而終止程序。在UNIX系統上,這個錯誤被稱為“段違例(segmentation violation)”或“內存錯誤(memory fault)”。它提示程序試圖訪問一個并未分配給程序的內存地址。在一臺運行Windows的PC上,對未初始化或非法指針進行間接的訪問操作是一般保護性異常(General Protection Exception)的根源之一。
對于那些要求整數必須存儲于特定邊界的機器而言,如果這種類型的數據在內存中的存儲地址處于錯誤的邊界上,那么對這個地址進行訪問時將會產生一個錯誤。這種錯誤在UNIX系統中被稱為“總線錯誤(bus error)”。
一個更為嚴重的情況是:這個指針偶爾可能包含了一個合法的地址。接下來的事情很簡單:位于那個位置的值被修改,雖然你并無意去修改它。像這種類型的錯誤非常難以捕捉,因為引發錯誤的代碼可能與原先用于操作那個值的代碼完全不相干。所以,在你對指針進行間接訪問之前,必須非常小心,確保它們已被初始化!!!
6.6 NULL指針
標準定義了NULL指針,它作為一個特殊的指針變量,表示不指向任何東西。要使一個指針變量為NULL,你可以給它賦一個零值。為了測試一個指針變量是否為NULL,你可以將它與零值進行比較。之所以選擇零這個值是因為一種源代碼約定。就機器內部而言,NULL指針的實際值可能與此不同。在這種情況下,編譯器將負責零值和內部值之間的翻譯轉換。
NULL指針的概念是非常有用的,因為它給了你一種方法,表示某個特定的指針目前并未指向任何東西。例如,一個用于在某個數組中查找某個特定值的函數可能返回一個指向查找到的數組元素的指針。如果該數組不包含指定條件的值,函數就返回一個NULL指針。這個技巧允許返回值傳達兩個不同片段的信息。首先,有沒有找到元素?其次,如果找到,它是哪個元素?
提示:
盡管這個技巧在C程序中極為常用,但它違背了軟件工程的原則。用一個單一的值表示兩種不同的意思是件危險的事情,因為將來很容易無法弄清哪個才是它真正的用意。在大型的程序中,這個問題更為嚴重,因為你不可能在頭腦中對整個設計一覽無余。一種更為安全的策略是讓函數返回兩個獨立的值:首先是個狀態值,用于提示查找是否成功;其次是個指針,當狀態值提示查找成功時,它所指向的就是查找到的元素。
對指針進行解引用操作可以獲得它所指向的值。但從定義上來看,NULL指針并未指向任何東西。因此,對一個NULL指針進行解引用操作是非法的。在對指針進行解引用操作之前,你首先必須確保它并非NULL指針。
警告:
如果對一個NULL指針進行間接訪問操作會發生什么情況呢?\color{red}{如果對一個NULL指針進行間接訪問操作會發生什么情況呢?}如果對一個NULL指針進行間接訪問操作會發生什么情況呢?它的結果因編譯器而異。在有些機器上,它會訪問內存位置零。編譯器能偶確保內存位置零沒有存儲任何變量,但機器并未妨礙你訪問或修改這個位置。這種行為是非常不幸的,因為程序包含了一個錯誤,但機器卻隱藏了它的癥狀,這樣就使這個錯誤更加難以尋找。
在其他機器上,對NULL指針進行間接訪問將引發一個錯誤,并終止程序。宣布這個錯誤比隱藏這個錯誤要好得多,因為程序員能夠更容易修正它。
提示
如果所有的指針變量能夠被自動初始化為NULL,那實在是一件幸事,但事實并非如此。不論你的機器對解引用NULL指針這種行為作何反應,對所有的指針變量進行顯示的初始化是種好的做法。如果你已經知道指針將被初始化為什么地址,就把它初始化為該地址,否則就把它初始化為NULL。風格良好的程序會在指針解引用之前對它進行檢查,這種初始化策略可以節省大量的調試時間。
6.7 指針、間接訪問和左值
涉及指針的表達式能不能作為左值?如果能,又是哪些呢?對表5.1優先級表格進行快速查閱后可以發現,間接訪問操作符所需要的操作數是個右值,但這個操作符所產生的結果是個左值。
讓我們回到早些時候的例子。給定下面這些聲明
int a; int *d=&a;考慮下面的表達式:
指針變量可以作為左值,并不是因為它們是指針,而是因為它們是變量。對指針變量進行間接訪問表示我們應該訪問指針所指向的位置。 間接訪問指定了一個特定的內存位置,這樣我們可以把間接訪問表達式的結果作為左值使用。在下面這兩條語句中
*d=10-*d; //OK d=10-*d; //ERROR第一條語句包含了兩個間接訪問操作。右邊的間接訪問作為右值使用,所以它的值是d所指向的位置所存儲的值(a的值)。左邊的間接訪問作為左值使用,所以d所指向的位置(a)把賦值符右側的表達式的計算結果作為它的新值。
第二條語句是非法的,因為它表示把一個整型數量(10-*d)存儲于一個指針變量中。當我們實際使用的變量類型和應該使用的變量類型不一致時,編譯器會發出抱怨,幫組我們判斷這種情況。這些警告和錯誤信息是我們的朋友,編譯器通過產生這些信息向我們提供幫助。d=10-*d; 在devc++編譯器中返回錯誤
[Error] invalid conversion from 'int' to 'int*' [-fpermissive]可運行代碼如下:
#include<bits/stdc++.h> using namespace std;int main(){int a=12; //int 占用4字節 int *d =&a; //指針d指向變量a // *d 就是 a *d= 10 - *d;cout<<a<<endl; //&a 是a的地址,對地址間接訪問*&a 就是a *&a=25; // 即 a =25; cout<<a<<endl; }程序輸出結果:
-2 256.8 指針、間接訪問和變量
如果你自以為精通了指針,不妨看一下這個表達式,看看你是否明白它的意思。
*&a=25;如果你的答案是把25賦值給變量a,那么恭喜你,你答對了。讓我們來分析這個表達式,首先,&操作符產生a的地址,它是一個指針常量,接著,*操作符訪問其操作數所表示的地址。在這個表達式中,操作數是a的地址,所以值25就存儲于a中。
這條語句和簡單地使用a=25;有什么區別嗎?從功能上來說,它們是相同的。
6.9 指針常量
讓我們分析一個表達式。假定變量a存儲于位置100,下面這條語句的作用是什么?
*100=25;它看上去好像是把25賦值給a,因為a是位置100所存儲的變量。但是,這是錯的!這句語句實際上是非法的,因為字面值100的類型是整型,而間接訪問只能作用于指針類型表達式。如果想要把25存儲于位置100,必須使用強制類型轉換。
*(int * )100=25;強制類型轉換把值100從整型變成指向整型的指針,這樣對它進行間接訪問就是合法的。如果a存儲在位置100,那么這條語句的作用就是把值25存儲于a。但是,需要使用這種技巧的機會是絕無僅有的!為什么?因為通常無法預測編譯器會把某個特定的變量放在內存中的什么位置,所以無法預先知道它的地址。
這個技巧的唯一用處是偶爾需要通過地址訪問內存中某個特定的位置,它并不是用于訪問某個變量,而是訪問硬件本身。
6.10指針的指針
看下面的例子
int a=12; int *b=&a; int **c=&b;看一下內存分配
問題是,c是什么類型?顯然它是一個指針,但它指向的是什么?變量b是一個“指向整型的指針”,所以任何指向b的類型必須是指向“指向整型的指針”的指針,更通俗地說,是一個指針的指針。
它合法嗎?是的!指針變量和其他變量一樣,占據內存中某個特定的位置,所以用&操作符取得它的地址是合法的。
指針的指針如何聲明?
int ** c;表示表達式**c 的類型是int。
對表達式int **c=&b;進行分析:
int a=12; int *b=&a; int **c=&b;分析:*操作符具有從右向左的結合性,所以這個表達式相當于*(*c),必須從里向外逐層求值。*c訪問c所指向的位置,我們知道這是變量b。第二個間接訪問操作符訪問這個位置所指向的地址,也就是變量a。
上面的表達式的值各是多少呢?a的值是12,b的值是變量a的地址,c的值是變量b的地址。
*b的值是什么呢? *b作為右值,表示b所指向的地址里面的內容,也就是a,所以 *b=12;
*c的值是什么呢?*c作為右值,表示c所指向的地址里面的內容,也就是b,所以 *c=&a;
**c的值是什么呢?**c作為右值,表示(*c)所指向的地址里面的內容,即 **c= *&b,表示b這個地址里面的內容,也就是a,即 **c=12;
總結如下表
| a | 12 |
| b | &a |
| *b | a ,12 |
| c | &b |
| *c | b ,&a |
| **c | *b, a, 12 |
測試代碼
#include<iostream> using namespace std;int main(){int a=12;int *b=&a;int **c=&b;cout<<"a的值是: "<<a<<endl;cout<<"*b的值是:"<<*b<<endl;cout<<endl;cout<<"b的值是: "<<b<<endl;cout<<"*c的值是:"<<*c<<endl;cout<<endl;cout<<"a的地址是:"<<&a<<endl;cout<<"b的值是: "<<b<<endl;cout<<endl;cout<<"b的地址是:"<<&b<<endl;cout<<"c的值是: "<<c<<endl;cout<<endl;cout<<"*c的值=b的值:"<<*c<<endl;cout<<"**c的值=a的值:"<<**c<<endl; }6.11指針表達式
首先看一些聲明
char ch='a'; char *cp=&ch;現在我們有了兩個變量,它們初始化如下
圖中還顯示了ch后面的那個內存位置,因為我們所求值的有些表達式將訪問它(盡管在錯誤的情況下才會對它進行訪問)。由于我們不知道它的初始值,所以用一個問號來代替。
首先來個簡單的作為開始,如下面這個表達式
ch當它作為右值使用時,表達式的值為’a’,如下圖所示
這個粗橢圓提示變量ch的值就是表達式的值。但是,當這個表達式作為左值使用時,它是這個內存的地址而不是該地址所包含的值,所以它的圖示方式有所不同
此時該位置用粗方框標記,提示這個位置就是表達式的結果。另外,它的值并沒有顯示,因為它并不重要。事實上,這個值將被某個新值代替。接下來的表達式將以表格的形式出現。每個表的后面是表達式求值過程的描述。
作為右值,這個表達式的值是變量ch的地址。注意這個值同變量cp中所存儲的值一樣。但這個表達式并未提到cp,所以這個結果值并不是因為它而產生的。 第二個問題是,為什么這個表達式不是一個合法的左值? 優先級表格顯示&操作符的結果是個右值,它不能當作左值使用。但是為什么呢? 答案很簡單,當表達式&ch進行求值時,它的結果應該存儲于計算機的什么地方呢?它肯定會位于某個地方,但你無法知道它位于何處。這個表達式并未標識任何機器內存的特定位置,所以它不是一個合法的左值。
這個表達式前面見到過。它的右值就是cp的值。它的左值就是cp所處的內存位置。由于這個表達式并不進行間接訪問操作,所以不必依箭頭所示方向進行間接訪問。
這個例子與&ch類似,不過這次我們所取的是指針變量的地址。這個結果的類型是指向字符的指針的指針。同樣,這個值的存儲位置并未清晰定義,所以這個表達式不是一個合法的左值。
現在我們加入了間接訪問操作,所以它的結果應該不會令人驚奇。*cp作為右值表示 cp的內容,即‘a’;*cp作為左值表示cp的內容,也就是cp所存的地址。
需要記住的是
前提:cp是一個指針變量
cp作為左值,表示指針變量cp在內存中的位置
*cp作為左值,表示cp的內容,也就是存的地址
下面幾個表達式就比較有意思。
這個圖涉及的東西更多,所以讓我們一步一步研究它。這里有兩個操作符。*操作符的優先級高于+,所以首先執行的是間接訪問操作(如圖中cp到ch的實線箭頭所示),我們可以得到它的值(如虛線橢圓所示)。我們取得這個值的一份拷貝并把它與1相加,表達式的最終結果是字符’b’. 圖中虛線表示表達式求值時數據的移動過程。這個表達式的最終結果的存儲位置并未清晰定義,所以它不是一個合法的左值。優先級表格證實+的結果不能作為左值。
在這個例子中,我們在前面那個表達式中增加了一個括號。這個括號使得表達式先執行加法運算,就是把1和cp中所存儲的地址相加。此時的結果值是圖中虛線橢圓所示的指針。接下來的間接訪問操作隨著箭頭訪問緊隨ch之后的內存位置。這樣,這個表達式的右值就是這個位置的值,而它的左值就是這個位置本身。
在這里我們需要學習的很重要的一點。注意指針加法運算的結果是個右值,因為它的存儲位置并未清晰定義。如果沒有間接訪問操作,這個表達式將不是一個合法的左值。 然而,間接訪問跟隨指針訪問一個特定的位置。這樣*(cp+1)就可以作為左值使用,盡管cp+1本身并不是左值。間接訪問操作符是少數幾個其結果為左值的操作符之一。
但是,這個表達式所訪問的是ch后面的那個內存位置,我們如何知道原先存儲于那個地方的是什么東西?一般而言,我們無法得知,所以像這樣的表達式是非法的。
++和- -操作符在指針變量中使用的相當頻繁,所致在這總上下文環境中理解它們是非常重要的。在這個表達式中,我們增加了指針變量cp的值。(為了讓圖更清楚,我們省略了加法)。表達式的結果是增值后的指針的一份拷貝,因為前綴++先增加它的操作數的值再返回這個結果。這份拷貝的存儲位置并未清晰定義,所以它不是一個合法的左值。
后綴++操作符同樣增加cp的值,但它先返回cp值的一份拷貝然后再增加cp的值。這樣,這個表達式的值就是cp原來的值的一份拷貝。
前面兩個表達式的值都不是合法的左值。但如果我們在表達式中增加了間接訪問操作符,它們就可以成為合法的左值,如下圖的兩個表達式所示。
這里,間接訪問操作符作用域增值后的指針的拷貝上,所以的它的右值是ch后面那個內存地址的值,而它的左值就是那個位置本身。
下面這個例子很重要。
?*cp++
使用后綴++操作符所產生的結果不同: 它的右值和左值分別是ch的值和ch的內存位置,也就是cp原先所指\color{red}{它的右值和左值分別是ch的值和ch的內存位置,也就是cp原先所指}它的右值和左值分別是ch的值和ch的內存位置,也就是cp原先所指。同樣,后綴++操作符在周圍的表達式中使用其原先操作數的值。間接訪問操作符和后綴++操作符的組合常常令人誤解。 優先級表格顯示后綴++操作符的優先級高于*操作符,但表達式的結果看上去像是先執行間接訪問操作,實際上不是\color{red}{但表達式的結果看上去像是先執行間接訪問操作,實際上不是}但表達式的結果看上去像是先執行間接訪問操作,實際上不是。事實上,這里涉及三個步驟:
(1)++操作符產生cp的一份拷貝
(2)然后++操作符增加cp的值
(3)最后在cp的拷貝上執行間接訪問操作。
后綴表達式cp++:先返回cp的值的一份拷貝,然后再增加cp的值。這樣cp++的值就是cp原來的值的一份拷貝。
這個表達式常常在循環中出現,首先用一個數組的地址初始化指針,然后使用這種表達式就可以依次訪問該數組的內容。
在這個表達式中,由于這兩個操作符的結合性都是自右向左,所以首先執行的是間接訪問操作。然后,cp所指向的位置的值加1(由‘a’變成‘b’),表達式的結果是這個增值后的值的一份拷貝。
和前面一些表達式相比,最后3個表達式在實際應用中使用的較少。但是,對它們有一個透徹的理解有助于提高你的技能。
使用后綴++操作符,我們必須加上括號,使它首先執行間接訪問操作。這個表達式的執行結果和前一個表達式相似,但它的結果值是ch增值前的原先值。
這個表達式看上去相當詭異,但事實上并不復雜。這個表達式共有3個操作符,這些操作符的結合性都是從右向左的,所以首先執行的是++cp。cp下面的虛橢圓表示第一個中間結果。接著,我們對這個拷貝值進行間接訪問,它使我們訪問ch后面那個內存位置。第二個中間結果用虛線方框表示,因為下一個操作符把它當作一個左值使用。最后,我們在這個位置執行++操作,也就是增加它的值。我們之所以把結果值顯示為?+1是因為我們并不知道這個位置原先的值。
這個表達式和前一個表達式的區別在于這次第一個++操作符是后綴形式而不是前綴形式。由于它的優先級較高,所以先執行它。 間接訪問操作所訪問的是cp所指向的位置,而不是cp所指向那個位置后面那個位置。
解釋: 對于后面的*cp++,先執行++操作符產生cp的一份拷貝,然后++操作符增加cp的值(cp現在指向下一個位置),最后,在cp的拷貝上(原位置)執行間接訪問操作,所以 ,作為右值得到的是ch里面的值‘a’。然后’a’執行前綴++,得到的是‘b’。 只不過此時cp已經指向下一個位置。
6.12 實例
用法舉例:字符串長度函數strlen
#include<stdlib.h> #include<iostream> using namespace std;size_t strlen(char *string){int length=0;while(*string++ != '\0'){ //(此處作為右值)取值,并++//再復習一下*string++的執行過程// 1.++操作符生成string 的一個拷貝//2.++操作符增加string的值(新值)//3在string的拷貝(原先值)上執行間接訪問length+=1;}return length; } int main(){char a[10]={1,2,4};cout <<strlen(a)<<endl; }在指針到達字符串末尾的NUL字節之前,while語句中*string++表達式的值一直為真。它同時增加指針的值,用于下一次測試。這個表達式甚至可以正確地處理空字符串。
警告:
如果這個函數調用時傳遞給它的是一個NULL指針,那么while語句中的間接訪問將會失敗。函數是不是應該在解引用指針前檢查這個條件? 從絕對安全的角度來看,應該如此。但是,這個函數并不負責創建字符串。如果它發現參數為NULL,它肯定發現了一個出現在程序其他地方的錯誤。當指針創建時檢查它有效是符合邏輯的,移位這樣只需要檢查一次。這個函數采用的就是這種方法。如果函數失敗是因為粗心大意的調用者懶得檢查參數的有效性而引起的,那是他活該如此。
程序6.2和6.3增加了一層間接訪問。它們在一些字符串中搜索某個特定的字符值,但我們使用指針數組來表示這些字符串,如圖6.1所示。
函數的參數是strings和value,strings是一個指向指針數組的指針,value是我們所查找的字符值。注意指針數組以一個NULL指針結束。函數將檢查這個值來判斷循環何時結束。下面這行表達式
完成3項任務:
(1)它把strings當前所指向的指針復制到變量string中
(2)它增加strings的值,使它指向下一個值
(3)它測試string是否是NULL。當string指向當前字符串中作為終止標志的NUL字節時,內層的while循環就終止。
程序6.2:在一組字符串中查找:版本1
// 給定一個指向以NULL結尾的指針列表的指針,在列表中的字符串中查找一個特定的字符#include<stdio.h>#define TRUE 1 #define FALSE 0int find_char(char **strings , char value){char *string; //我們當前正在查找的字符串//對于列表中的每個字符串while( ( string = *strings++ ) != NULL){//觀察字符串中的每個字符,看看它是不是我們需要查找的那個while( *string != '\0'){if( *string ++ == value)return TRUE;}}return FALSE;}如果string尚未到達其結尾的NUL字節,就執行下面這條語句
if( *string ++ == value)它測試當前的字符是否與需要查找的字符匹配,然后增加指針的值,使它指向下一個字符。
程序6.3實現相同的功能,但它不需要對指向每個字符串的指針做一份拷貝。但是,由于存在副作用,這個程序將破壞這個指針數組。這個副作用使得該函數不如前面那個版本有用,因為它只適用于字符串只需要查找一次的情況。
程序6.3 在一組字符串中查找:版本2
#include<stdio.h> #include<assert.h>#define TRUE 1 #define FALSE 0int find_char (char ** strings ,int value){assert( strings!=NULL);//對列表中的每個字符串while( *strings !=NULL){while( ** strings !='\0'){if( *( *strings )++ ==value) return TRUE;}strings++;}return FALSE; }但是,在程序6.3中有兩個有趣的表達式。第一個是 **strings。第1個間接訪問操作訪問指針數組中的當前指針,第2個間接訪問操作隨該指針訪問字符串中的當前字符。內層的while語句測試這個字符的值并觀察是否到達了字符串的末尾。
第二個有趣的表達式是*(*strings)++.括號是需要的,這樣才能使表達式以正確的順序進行求值。 第一個間接訪問操作訪問列表中的當前指針。增值操作把該指針所指向的那個位置的值加1,但第二個間接訪問操作作用于原先那個值的拷貝上。這個表達式的直接作用是對當前字符串中的當前字符進行測試,看看是否到達了字符串的末尾。作為副作用,指向當前字符串字符的指針值將增加1.
6.13 指針運算
指針加上一個整數的結果是另一個指針。問題是,它指向哪里?如果你將一個字符指針+1,運算結果產生的指針指向內存中的下一個字符。float占據的內存空間不止1個字節,如果你將一個指向float的指針加1,將會發生什么呢? 它會不會指向該float值內部的某個字節呢?
幸運的是,答案是否定的。當一個指針和一個整數量執行算術運算時,整數在執行加法運算前始終會根據合適的大小進行調整。這個“合適的大小”就是指針所指向類型的大小,“調整”就是把整數值和“合適的大小”相乘。 為了更好地說明,試想在某臺機器上,float占據4個字節。在計算float型指針加3的表達式時,這個3根據float類型的大小(此例中為4)進行調整(相乘)。這樣,實際加到指針上的整型值為12.
把3與指針相加使指針的值增加3個float的大小,而不是3個字節。 這個行為較之獲得一個指向一個float值內部某個位置的指針更為合理。下圖中有一些加法運算的例子。調整的美感在于指針算法并不依賴于指針的類型。換句話說,如果p是一個指向char的指針,那么表達式p+1就指向下一個char。如果p是個指向float的指針,那么p+1就指向下一個float,其他類型也是如此。
6.13.1 算術運算
C的指針運算只限于兩種形式。
第一種形式是 指針 ± 整數
標準定義這種形式只能用于指向數組中某個元素的指針,如下圖所示。
并且這類表達式的結果類型也是指針。這種形式也適用于使用malloc函數動態分配獲得的內存。
對指針執行加法或減法運算之后如果結果指針所指的位置在數組的第1個元素的前面或在數組最后一個元素的后面,那么其效果就是未定義的。 讓指針指向數組最后一個元素后面的那個位置是合法的,但對這個指針執行間接訪問可能會失敗。
是該舉個例子的時候了。 這里有個循環,把數組中所有的元素都初始化為0.
#define N_VALUES 5 float values[N_VALUES]; float *vp;for(vp=&values[0];vp<&values[N_VALUES];)*vp++=0;for語句的初始部分把vp指向數組的第一個元素。
這個例子中的指針運算是用++操作符完成的。 增加值1與float長度相乘,其結果加到指針vp上。經過第1次循環之后,指針在內存中的位置如下:
經過5次循環之后,vp就指向數組最后一個元素后面的那個內存位置
此時循環終止。由于下標從零開始,所以具有5個元素的數組的最后一個元素的下標值為4.這樣,&values[N_VALUES] 表示數組最后一個元素后面那個內存位置的地址。當vp到達這個值時,我們就知道到達了數組的末尾,故循環終止。
第2種類型的指針運算具有如下形式:指針-指針
只有當兩個指針都指向同一個數組中的元素時,才允許從一個指針減去另一個指針,如下所示
兩個指針相減的結果類型是ptrdiff_t,它是一種有符號整數類型。減法運算的值是兩個指針在內存中的距離(以數組元素的長度為單位,而不是以字節為單位),因為減法運算的結果將除以數組元素類型的長度。 例如,如果p1指向array[i] 而 p2指向 array[j], 那么p2-p1的值就是j-i的值。
讓我們看一下它是如何作用于某個特定類型的。假定前圖中數據元素 的類型為float,每個元素占據4個字節的內存空間。如果數組的起始位置為1000,p1的值是1004,p2的值是1024,但表達式p2-p1的值將是5,因為兩個指針的差值(20)將除以每個元素的長度(4)。
同樣,這種對差值的調整使指針的運算結果與數據的類型無關。不論數組包含的元素類型如何,這個指針減法運算的值總是5.
那么,表達式p1-p2是否合法呢? 是的,如果兩個指針都指向同一個數組中的元素,這個表達式就是合法的。在前一個例子中,這個值將是-5.
如果兩個指針所指向的不是同一個數組中的元素,那么它們之間相減的結果是未定義的。
警告:
實際上,絕大多數編譯器都不會檢查指針表達式的結果是否位于合法的邊界之內。因此,程序員應該負起責任,確保這一點。越界指針和指向未知值的指針是兩個常見的錯誤根源。
6.13.2 關系運算
對指針執行關系運算也是有限制的。用下列關系操作符對兩個指針值進行比較是可能的:
< <= > >=不過前提是它們都是指向同一個數組中的元素。 根據你所使用的操作符,比較表達式將告訴你哪個指針指向數組中更前或更后的元素。 標準并未定義如果兩個任意的指針進行比較會產生什么結果。
然而,你可以在兩個任意的指針間執行相等或不相等的測試,因為這類比較的結果和編譯器閑則在何處存儲數據并無關系—指針要不指向同一個地址,要不指向不同的地址。
讓我們再觀察一個循環。它用于清楚一個數組中所有的元素。
#define N_VALUES 5 float values[N_VALUES]; float *vp;for(vp=&values[0];vp<&values[N_VALUES];)*vp++=0;for語句使用了一個關系測試來決定是否結束循環。這個測試是合法的,因為vp和指針變量都指向同意數組中的元素。
現在考慮下面這個循環
for( vp =&values[N_VALUES];vp>&values[0];)*--vp=0;它和前面那個循環所執行的任務相同,但數組元素將以相反的順序清除。我們讓vp指向數組元素最后那個元素后面的內存位置,但對它進行間接訪問之前先執行自減操作。當vp指向數組第一個元素時,循環便告結束,不過這發生在第一個元素被清除之后。
有些人可能反對像*–vp這樣的表達式,覺得它的可讀性差。但是,如果對其簡化,看看這個循環會發生什么:
for( vp = &values[N_VALUES-1];vp>= &values[0];vp--)*vp=0;現在vp指向數組最后一個元素,它的自減操作放在for語句的調整部分進行。這個循環存在一個問題,你能發現它嗎?
警告:
在數組第一個元素被清除之后,vp的值還將減去1,而接下去的一次比較運算是用于結束循環的。但這就是問題所在:比較表達式 vp >= &values[0] 的值是未定義的,因為vp移到了數組的邊界之外。標準允許指向數組元素的指針與指向數組最后一個元素后面的那個內存位置的指針進行比較,但不允許與指向數組第一個元素之前的那個內存位置的指針進行比較。
實際上,在絕大多數C編譯器中,這個循環將順利完成任務。然而,你還是應該避免使用它,因為標準并不保證它可行。你遲早可能遇到一臺這個循環將失敗的機器。對于負責可以指代碼的程序員而言,這類問題簡直是噩夢。
6.14 警告的總結
1 錯誤地對一個未初始化的指針變量進行解引用。
2 錯誤地對一個NULL指針進行解引用
3 向函數錯誤地傳遞NULL指針。
4 未檢測到指針表達式的錯誤,從而導致不可預料的錯誤
5 對一個指針進行加減運算,使它非法指向了數組第一個元素的前面的內存位置。
6.15 編程提示的總結
1 一個值應該只具有一個意思.
2 如果指針并不指向任何有意義的東西,就把它設置為NULL.
總結
以上是生活随笔為你收集整理的《C和指针》读书笔记-第六章指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《图解HTTP》读书笔记--第8章 确认
- 下一篇: 《大话数据结构》读书笔记-串