<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 第?23?章?指針 **目錄** + [1\. 指針的基本概念](ch23s01.html) + [2\. 指針類型的參數和返回值](ch23s02.html) + [3\. 指針與數組](ch23s03.html) + [4\. 指針與`const`限定符](ch23s04.html) + [5\. 指針與結構體](ch23s05.html) + [6\. 指向指針的指針與指針數組](ch23s06.html) + [7\. 指向數組的指針與多維數組](ch23s07.html) + [8\. 函數類型和函數指針類型](ch23s08.html) + [9\. 不完全類型和復雜聲明](ch23s09.html) ## 1.?指針的基本概念 在[第?12?章 _棧與隊列_](ch12.html#stackqueue)講過,堆棧有棧頂指針,隊列有頭指針和尾指針,這些概念中的“指針”本質上是一個整數,是數組的索引,通過指針訪問數組中的某個元素。在[圖?20.3 “間接尋址”](ch20s04.html#link.indirect)我們又看到另外一種指針的概念,把一個變量所在的內存單元的地址保存在另外一個內存單元中,保存地址的這個內存單元稱為指針,通過指針和間接尋址訪問變量,這種指針在C語言中可以用一個指針類型的變量表示,例如某程序中定義了以下全局變量: ``` int i; int *pi = &i; char c; char *pc = &c; ``` 這幾個變量的內存布局如下圖所示,在初學階段經常要借助于這樣的圖來理解指針。 **圖?23.1.?指針的基本概念** ![指針的基本概念](https://box.kancloud.cn/2016-04-02_56ff80d58c4a7.png) 這里的`&`是取地址運算符(Address Operator),`&i`表示取變量`i`的地址,`int *pi = &i;`表示定義一個指向`int`型的指針變量`pi`,并用`i`的地址來初始化`pi`。我們講過全局變量只能用常量表達式初始化,如果定義`int p = i;`就錯了,因為`i`不是常量表達式,然而用`i`的地址來初始化一個指針卻沒有錯,因為`i`的地址是在編譯鏈接時能確定的,而不需要到運行時才知道,`&i`是常量表達式。后面兩行代碼定義了一個字符型變量`c`和一個指向`c`的字符型指針`pc`,注意`pi`和`pc`雖然是不同類型的指針變量,但它們的內存單元都占4個字節,因為要保存32位的虛擬地址,同理,在64位平臺上指針變量都占8個字節。 我們知道,在同一個語句中定義多個數組,每一個都要有`[]`號:`int a[5], b[5];`。同樣道理,在同一個語句中定義多個指針變量,每一個都要有`*`號,例如: ``` int *p, *q; ``` 如果寫成`int* p, q;`就錯了,這樣是定義了一個整型指針`p`和一個整型變量`q`,定義數組的`[]`號寫在變量后面,而定義指針的`*`號寫在變量前面,更容易看錯。定義指針的`*`號前后空格都可以省,寫成`int*p,*q;`也算對,但`*`號通常和類型`int`之間留空格而和變量名寫在一起,這樣看`int *p, q;`就很明顯是定義了一個指針和一個整型變量,就不容易看錯了。 如果要讓`pi`指向另一個整型變量`j`,可以重新對`pi`賦值: ``` pi = &j; ``` 如果要改變`pi`所指向的整型變量的值,比如把變量`j`的值增加10,可以寫: ``` *pi = *pi + 10; ``` 這里的`*`號是指針間接尋址運算符(Indirection Operator),`*pi`表示取指針`pi`所指向的變量的值,也稱為Dereference操作,指針有時稱為變量的引用(Reference),所以根據指針找到變量稱為Dereference。 `&`運算符的操作數必須是左值,因為只有左值才表示一個內存單元,才會有地址,運算結果是指針類型。`*`運算符的操作數必須是指針類型,運算結果可以做左值。所以,如果表達式`E`可以做左值,`*&E`和`E`等價,如果表達式`E`是指針類型,`&*E`和`E`等價。 指針之間可以相互賦值,也可以用一個指針初始化另一個指針,例如: ``` int *ptri = pi; ``` 或者: ``` int *ptri; ptri = pi; ``` 表示_`pi`指向哪就讓`ptri`也指向哪_,本質上就是把變量`pi`所保存的地址值賦給變量`ptri`。 用一個指針給另一個指針賦值時要注意,兩個指針必須是同一類型的。在我們的例子中,`pi`是`int *`型的,`pc`是`char *`型的,`pi = pc;`這樣賦值就是錯誤的。但是可以先強制類型轉換然后賦值: ``` pi = (int *)pc; ``` **圖?23.2.?把`char *`指針的值賦給`int *`指針** ![把char *指針的值賦給int *指針](https://box.kancloud.cn/2016-04-02_56ff80d5b32ee.png) 現在`pi`指向的地址和`pc`一樣,但是通過`*pc`只能訪問到一個字節,而通過`*pi`可以訪問到4個字節,后3個字節已經不屬于變量`c`了,除非你很確定變量`c`的一個字節和后面3個字節組合而成的`int`值是有意義的,否則就不應該給`pi`這么賦值。因此使用指針要特別小心,很容易將指針指向錯誤的地址,訪問這樣的地址可能導致段錯誤,可能讀到無意義的值,也可能意外改寫了某些數據,使得程序在隨后的運行中出錯。有一種情況需要特別注意,定義一個指針類型的局部變量而沒有初始化: ``` int main(void) { int *p; ... *p = 0; ... } ``` 我們知道,在堆棧上分配的變量初始值是不確定的,也就是說指針`p`所指向的內存地址是不確定的,后面用`*p`訪問不確定的地址就會導致不確定的后果,如果導致段錯誤還比較容易改正,如果意外改寫了數據而導致隨后的運行中出錯,就很難找到錯誤原因了。像這種指向不確定地址的指針稱為“野指針”(Unbound Pointer),為避免出現野指針,在定義指針變量時就應該給它明確的初值,或者把它初始化為`NULL`: ``` int main(void) { int *p = NULL; ... *p = 0; ... } ``` `NULL`在C標準庫的頭文件`stddef.h`中定義: ``` #define NULL ((void *)0) ``` 就是把地址0轉換成指針類型,稱為空指針,它的特殊之處在于,操作系統不會把任何數據保存在地址0及其附近,也不會把地址0~0xfff的頁面映射到物理內存,所以任何對地址0的訪問都會立刻導致段錯誤。`*p = 0;`會導致段錯誤,就像放在眼前的炸彈一樣很容易找到,相比之下,野指針的錯誤就像埋下地雷一樣,更難發現和排除,這次走過去沒事,下次走過去就有事。 講到這里就該講一下`void *`類型了。在編程時經常需要一種通用指針,可以轉換為任意其它類型的指針,任意其它類型的指針也可以轉換為通用指針,最初C語言沒有`void *`類型,就把`char *`當通用指針,需要轉換時就用類型轉換運算符`()`,ANSI在將C語言標準化時引入了`void *`類型,`void *`指針與其它類型的指針之間可以隱式轉換,而不必用類型轉換運算符。注意,只能定義`void *`指針,而不能定義`void`型的變量,因為`void *`指針和別的指針一樣都占4個字節,而如果定義`void`型變量(也就是類型暫時不確定的變量),編譯器不知道該分配幾個字節給變量。同樣道理,`void *`指針不能直接Dereference,而必須先轉換成別的類型的指針再做Dereference。`void *`指針常用于函數接口,比如: ``` void func(void *pv) { /* *pv = 'A' is illegal */ char *pchar = pv; *pchar = 'A'; } int main(void) { char c; func(&c); printf("%c\n", c); ... } ``` 下一章講函數接口時再詳細介紹`void *`指針的用處。 ## 2.?指針類型的參數和返回值 首先看以下程序: **例?23.1.?指針參數和返回值** ``` #include <stdio.h> int *swap(int *px, int *py) { int temp; temp = *px; *px = *py; *py = temp; return px; } int main(void) { int i = 10, j = 20; int *p = swap(&i, &j); printf("now i=%d j=%d *p=%d\n", i, j, *p); return 0; } ``` 我們知道,調用函數的傳參過程相當于用實參定義并初始化形參,`swap(&i, &j)`這個調用相當于: ``` int *px = &i; int *py = &j; ``` 所以`px`和`py`分別指向`main`函數的局部變量`i`和`j`,在`swap`函數中讀寫`*px`和`*py`其實是讀寫`main`函數的`i`和`j`。盡管在`swap`函數的作用域中訪問不到`i`和`j`這兩個變量名,卻可以通過地址訪問它們,最終`swap`函數將`i`和`j`的值做了交換。 上面的例子還演示了函數返回值是指針的情況,`return px;`語句相當于定義了一個臨時變量并用`px`初始化: ``` int *tmp = px; ``` 然后臨時變量`tmp`的值成為表達式`swap(&i, &j)`的值,然后在`main`函數中又把這個值賦給了p,相當于: ``` int *p = tmp; ``` 最后的結果是`swap`函數的`px`指向哪就讓`main`函數的`p`指向哪。我們知道`px`指向`i`,所以`p`也指向`i`。 ### 習題 1、對照本節的描述,像[圖?23.1 “指針的基本概念”](ch23s01.html#pointer.pointer0)那樣畫圖理解函數的調用和返回過程。在下一章我們會看到更復雜的參數和返回值形式,在初學階段對每個程序都要畫圖理解它的運行過程,只要基本概念清晰,無論多復雜的形式都應該能正確分析。 2、現在回頭看[第?3?節 “形參和實參”](ch03s03.html#func.paraarg)的習題1,那個程序應該怎么改? ## 3.?指針與數組 先看個例子,有如下語句: ``` int a[10]; int *pa = &a[0]; pa++; ``` 首先指針`pa`指向`a[0]`的地址,注意后綴運算符的優先級高于單目運算符,所以是取`a[0]`的地址,而不是取`a`的地址。然后`pa++`讓`pa`指向下一個元素(也就是`a[1]`),由于`pa`是`int *`指針,一個`int`型元素占4個字節,所以`pa++`使`pa`所指向的地址加4,注意不是加1。 下面畫圖理解。從前面的例子我們發現,地址的具體數值其實無關緊要,關鍵是要說明地址之間的關系(`a[1]`位于`a[0]`之后4個字節處)以及指針與變量之間的關系(指針保存的是變量的地址),現在我們換一種畫法,省略地址的具體數值,用方框表示存儲空間,用箭頭表示指針和變量之間的關系。 **圖?23.3.?指針與數組** ![指針與數組](https://box.kancloud.cn/2016-04-02_56ff80d5c37a5.png) 既然指針可以用`++`運算符,當然也可以用`+`、`-`運算符,`pa+2`這個表達式也是有意義的,如上圖所示,`pa`指向`a[1]`,那么`pa+2`指向a[3]。事實上,`E1[E2]`這種寫法和`(*((E1)+(E2)))`是等價的,`*(pa+2)`也可以寫成`pa[2]`,`pa`就像數組名一樣,其實數組名也沒有什么特殊的,`a[2]`之所以能取數組的第2個元素,是因為它等價于`*(a+2)`,在[第?1?節 “數組的基本概念”](ch08s01.html#array.intro)講過數組名做右值時自動轉換成指向首元素的指針,所以`a[2]`和`pa[2]`本質上是一樣的,都是通過指針間接尋址訪問元素。由于`(*((E1)+(E2)))`顯然可以寫成`(*((E2)+(E1)))`,所以`E1[E2]`也可以寫成`E2[E1]`,這意味著`2[a]`、`2[pa]`這種寫法也是對的,但一般不這么寫。另外,由于`a`做右值使用時和`&a[0]`是一個意思,所以`int *pa = &a[0];`通常不這么寫,而是寫成更簡潔的形式`int *pa = a;`。 在[第?1?節 “數組的基本概念”](ch08s01.html#array.intro)還講過C語言允許數組下標是負數,現在你該明白為什么這樣規定了。在上面的例子中,表達式`pa[-1]`是合法的,它和`a[0]`表示同一個元素。 現在猜一下,兩個指針變量做比較運算(`&gt;`、`&gt;=`、`&lt;`、`&lt;=`、`==`、`!=`)表示什么意義?兩個指針變量做減法運算又表示什么意義? 根據什么來猜?根據[第?3?節 “形參和實參”](ch03s03.html#func.paraarg)講過的Rule of Least Surprise原則。你理解了指針和常數加減的概念,再根據以往使用比較運算的經驗,就應該猜到`pa + 2 &gt; pa`,`pa - 1 == a`,所以指針之間的比較運算比的是地址,C語言正是這樣規定的,不過C語言的規定更為嚴謹,只有指向同一個數組中元素的指針之間相互比較才有意義,否則沒有意義。那么兩個指針相減表示什么?`pa - a`等于幾?因為`pa - 1 == a`,所以`pa - a`顯然應該等于1,指針相減表示兩個指針之間相差的元素個數,同樣只有指向同一個數組中元素的指針之間相減才有意義。兩個指針相加表示什么?想不出來它能有什么意義,因此C語言也規定兩個指針不能相加。假如C語言為指針相加也規定了一種意義,那就相當Surprise了,不符合一般的經驗。無論是設計編程語言還是設計函數接口或人機界面都是這個道理,應該盡可能讓用戶根據以往的經驗知識就能推斷出該系統的基本用法。 在取數組元素時用數組名和用指針的語法一樣,但如果把數組名做左值使用,和指針就有區別了。例如`pa++`是合法的,但`a++`就不合法,`pa = a + 1`是合法的,但`a = pa + 1`就不合法。數組名做右值時轉換成指向首元素的指針,但做左值仍然表示整個數組的存儲空間,而不是首元素的存儲空間,數組名做左值還有一點特殊之處,不支持`++`、賦值這些運算符,但支持取地址運算符`&`,所以`&a`是合法的,我們將在[第?7?節 “指向數組的指針與多維數組”](ch23s07.html#pointer.array3)介紹這種語法。 在函數原型中,如果參數是數組,則等價于參數是指針的形式,例如: ``` void func(int a[10]) { ... } ``` 等價于: ``` void func(int *a) { ... } ``` 第一種形式方括號中的數字可以不寫,仍然是等價的: ``` void func(int a[]) { ... } ``` 參數寫成指針形式還是數組形式對編譯器來說沒區別,都表示這個參數是指針,之所以規定兩種形式是為了給讀代碼的人提供有用的信息,如果這個參數指向一個元素,通常寫成指針的形式,如果這個參數指向一串元素中的首元素,則經常寫成數組的形式。 ## 4.?指針與`const`限定符 `const`限定符和指針結合起來常見的情況有以下幾種。 ``` const int *a; int const *a; ``` 這兩種寫法是一樣的,`a`是一個指向`const int`型的指針,`a`所指向的內存單元不可改寫,所以`(*a)++`是不允許的,但`a`可以改寫,所以`a++`是允許的。 ``` int * const a; ``` `a`是一個指向`int`型的`const`指針,`*a`是可以改寫的,但`a`不允許改寫。 ``` int const * const a; ``` `a`是一個指向`const int`型的`const`指針,因此`*a`和`a`都不允許改寫。 指向非`const`變量的指針或者非`const`變量的地址可以傳給指向`const`變量的指針,編譯器可以做隱式類型轉換,例如: ``` char c = 'a'; const char *pc = &c; ``` 但是,指向`const`變量的指針或者`const`變量的地址不可以傳給指向非`const`變量的指針,以免透過后者意外改寫了前者所指向的內存單元,例如對下面的代碼編譯器會報警告: ``` const char c = 'a'; char *pc = &c; ``` 即使不用`const`限定符也能寫出功能正確的程序,但良好的編程習慣應該盡可能多地使用`const`,因為: 1. `const`給讀代碼的人傳達非常有用的信息。比如一個函數的參數是`const char *`,你在調用這個函數時就可以放心地傳給它`char *`或`const char *`指針,而不必擔心指針所指的內存單元被改寫。 2. 盡可能多地使用`const`限定符,把不該變的都聲明成只讀,這樣可以依靠編譯器檢查程序中的Bug,防止意外改寫數據。 3. `const`對編譯器優化是一個有用的提示,編譯器也許會把`const`變量優化成常量。 在[第?3?節 “變量的存儲布局”](ch19s03.html#asmc.layout)我們看到,字符串字面值通常分配在`.rodata`段,而在[第?4?節 “字符串”](ch08s04.html#array.string)提到,字符串字面值類似于數組名,做右值使用時自動轉換成指向首元素的指針,這種指針應該是`const char *`型。我們知道`printf`函數原型的第一個參數是`const char *`型,可以把`char *`或`const char *`指針傳給它,所以下面這些調用都是合法的: ``` const char *p = "abcd"; const char str1[5] = "abcd"; char str2[5] = "abcd"; printf(p); printf(str1); printf(str2); printf("abcd"); ``` 注意上面第一行,如果要定義一個指針指向字符串字面值,這個指針應該是`const char *`型,如果寫成`char *p = "abcd";`就不好了,有隱患,例如: ``` int main(void) { char *p = "abcd"; ... *p = 'A'; ... } ``` `p`指向`.rodata`段,不允許改寫,但編譯器不會報錯,在運行時會出現段錯誤。 ## 5.?指針與結構體 首先定義一個結構體類型,然后定義這種類型的變量和指針: ``` struct unit { char c; int num; }; struct unit u; struct unit *p = &u; ``` 要通過指針`p`訪問結構體成員可以寫成`(*p).c`和`(*p).num`,為了書寫方便,C語言提供了`-&gt;`運算符,也可以寫成`p-&gt;c`和`p-&gt;num`。 ## 6.?指向指針的指針與指針數組 指針可以指向基本類型,也可以指向復合類型,因此也可以指向另外一個指針變量,稱為指向指針的指針。 ``` int i; int *pi = &i; int **ppi = &pi; ``` 這樣定義之后,表達式`*ppi`取`pi`的值,表達式`**ppi`取`i`的值。請讀者自己畫圖理解`i`、`pi`、`ppi`這三個變量之間的關系。 很自然地,也可以定義指向“指向指針的指針”的指針,但是很少用到: ``` int ***p; ``` 數組中的每個元素可以是基本類型,也可以復合類型,因此也可以是指針類型。例如定義一個數組`a`由10個元素組成,每個元素都是`int *`指針: ``` int *a[10]; ``` 這稱為指針數組。`int *a[10];`和`int **pa;`之間的關系類似于`int a[10];`和`int *pa;`之間的關系:`a`是由一種元素組成的數組,`pa`則是指向這種元素的指針。所以,如果`pa`指向`a`的首元素: ``` int *a[10]; int **pa = &a[0]; ``` 則`pa[0]`和`a[0]`取的是同一個元素,唯一比原來復雜的地方在于這個元素是一個`int *`指針,而不是基本類型。 我們知道main函數的標準原型應該是`int main(int argc, char *argv[]);`。`argc`是命令行參數的個數。而`argv`是一個指向指針的指針,為什么不是指針數組呢?因為前面講過,函數原型中的`[]`表示指針而不表示數組,等價于`char **argv`。那為什么要寫成`char *argv[]`而不寫成`char **argv`呢?這樣寫給讀代碼的人提供了有用信息,`argv`不是指向單個指針,而是指向一個指針數組的首元素。數組中每個元素都是`char *`指針,指向一個命令行參數字符串。 **例?23.2.?打印命令行參數** ``` #include <stdio.h> int main(int argc, char *argv[]) { int i; for(i = 0; i < argc; i++) printf("argv[%d]=%s\n", i, argv[i]); return 0; } ``` 編譯執行: ``` $ gcc main.c $ ./a.out a b c argv[0]=./a.out argv[1]=a argv[2]=b argv[3]=c $ ln -s a.out printargv $ ./printargv d e argv[0]=./printargv argv[1]=d argv[2]=e ``` 注意程序名也算一個命令行參數,所以執行`./a.out a b c`這個命令時,`argc`是4,`argv`如下圖所示: **圖?23.4.?`argv`指針數組** ![argv指針數組](https://box.kancloud.cn/2016-04-02_56ff80d5d2d9c.png) 由于`argv[4]`是`NULL`,我們也可以這樣循環遍歷`argv`: ``` for(i=0; argv[i] != NULL; i++) ``` `NULL`標識著`argv`的結尾,這個循環碰到`NULL`就結束,因而不會訪問越界,這種用法很形象地稱為Sentinel,`NULL`就像一個哨兵守衛著數組的邊界。 在這個例子中我們還看到,如果給程序建立符號鏈接,然后通過符號鏈接運行這個程序,就可以得到不同的`argv[0]`。通常,程序會根據不同的命令行參數做不同的事情,例如`ls -l`和`ls -R`打印不同的文件列表,而有些程序會根據不同的`argv[0]`做不同的事情,例如專門針對嵌入式系統的開源項目Busybox,將各種Linux命令裁剪后集于一身,編譯成一個可執行文件`busybox`,安裝時將`busybox`程序拷到嵌入式系統的`/bin`目錄下,同時在`/bin`、`/sbin`、`/usr/bin`、`/usr/sbin`等目錄下創建很多指向`/bin/busybox`的符號鏈接,命名為`cp`、`ls`、`mv`、`ifconfig`等等,不管執行哪個命令其實最終都是在執行`/bin/busybox`,它會根據`argv[0]`來區分不同的命令。 ### 習題 1、想想以下定義中的`const`分別起什么作用?編寫程序驗證你的猜測。 ``` const char **p; char *const *p; char **const p; ``` ## 7.?指向數組的指針與多維數組 指針可以指向復合類型,上一節講了指向指針的指針,這一節學習指向數組的指針。以下定義一個指向數組的指針,該數組有10個`int`元素: ``` int (*a)[10]; ``` 和上一節指針數組的定義`int *a[10];`相比,僅僅多了一個`()`括號。如何記住和區分這兩種定義呢?我們可以認為`[]`比`*`有更高的優先級,如果`a`先和`*`結合則表示`a`是一個指針,如果`a`先和`[]`結合則表示`a`是一個數組。`int *a[10];`這個定義可以拆成兩句: ``` typedef int *t; t a[10]; ``` `t`代表`int *`類型,`a`則是由這種類型的元素組成的數組。`int (*a)[10];`這個定義也可以拆成兩句: ``` typedef int t[10]; t *a; ``` `t`代表由10個`int`組成的數組類型,`a`則是指向這種類型的指針。 現在看指向數組的指針如何使用: ``` int a[10]; int (*pa)[10] = &a; ``` `a`是一個數組,在`&a`這個表達式中,數組名做左值,取整個數組的首地址賦給指針`pa`。注意,`&a[0]`表示數組`a`的首元素的首地址,而`&a`表示數組`a`的首地址,顯然這兩個地址的數值相同,但這兩個表達式的類型是兩種不同的指針類型,前者的類型是`int *`,而后者的類型是`int (*)[10]`。`*pa`就表示`pa`所指向的數組`a`,所以取數組的`a[0]`元素可以用表達式`(*pa)[0]`。注意到`*pa`可以寫成`pa[0]`,所以`(*pa)[0]`這個表達式也可以改寫成`pa[0][0]`,`pa`就像一個二維數組的名字,它表示什么含義呢?下面把`pa`和二維數組放在一起做個分析。 `int a[5][10];`和`int (*pa)[10];`之間的關系同樣類似于`int a[10];`和`int *pa;`之間的關系:`a`是由一種元素組成的數組,`pa`則是指向這種元素的指針。所以,如果`pa`指向`a`的首元素: ``` int a[5][10]; int (*pa)[10] = &a[0]; ``` 則`pa[0]`和`a[0]`取的是同一個元素,唯一比原來復雜的地方在于這個元素是由10個`int`組成的數組,而不是基本類型。這樣,我們可以把`pa`當成二維數組名來使用,`pa[1][2]`和`a[1][2]`取的也是同一個元素,而且`pa`比`a`用起來更靈活,數組名不支持賦值、自增等運算,而指針可以支持,`pa++`使`pa`跳過二維數組的一行(40個字節),指向`a[1]`的首地址。 ### 習題 1、定義以下變量: ``` char a[4][3][2] = {{{'a', 'b'}, {'c', 'd'}, {'e', 'f'}}, {{'g', 'h'}, {'i', 'j'}, {'k', 'l'}}, {{'m', 'n'}, {'o', 'p'}, {'q', 'r'}}, {{'s', 't'}, {'u', 'v'}, {'w', 'x'}}}; char (*pa)[2] = &a[1][0]; char (*ppa)[3][2] = &a[1]; ``` 要想通過`pa`或`ppa`訪問數組`a`中的`'r'`元素,分別應該怎么寫? ## 8.?函數類型和函數指針類型 在C語言中,函數也是一種類型,可以定義指向函數的指針。我們知道,指針變量的內存單元存放一個地址值,而函數指針存放的就是函數的入口地址(位于`.text`段)。下面看一個簡單的例子: **例?23.3.?函數指針** ``` #include <stdio.h> void say_hello(const char *str) { printf("Hello %s\n", str); } int main(void) { void (*f)(const char *) = say_hello; f("Guys"); return 0; } ``` 分析一下變量`f`的類型聲明`void (*f)(const char *)`,`f`首先跟`*`號結合在一起,因此是一個指針。`(*f)`外面是一個函數原型的格式,參數是`const char *`,返回值是`void`,所以`f`是指向這種函數的指針。而`say_hello`的參數是`const char *`,返回值是`void`,正好是這種函數,因此`f`可以指向`say_hello`。注意,`say_hello`是一種函數類型,而函數類型和數組類型類似,做右值使用時自動轉換成函數指針類型,所以可以直接賦給`f`,當然也可以寫成`void (*f)(const char *) = &say_hello;`,把函數`say_hello`先取地址再賦給`f`,就不需要自動類型轉換了。 可以直接通過函數指針調用函數,如上面的`f("Guys")`,也可以先用`*f`取出它所指的函數類型,再調用函數,即`(*f)("Guys")`。可以這么理解:函數調用運算符`()`要求操作數是函數指針,所以`f("Guys")`是最直接的寫法,而`say_hello("Guys")`或`(*f)("Guys")`則是把函數類型自動轉換成函數指針然后做函數調用。 下面再舉幾個例子區分函數類型和函數指針類型。首先定義函數類型F: ``` typedef int F(void); ``` 這種類型的函數不帶參數,返回值是`int`。那么可以這樣聲明`f`和`g`: ``` F f, g; ``` 相當于聲明: ``` int f(void); int g(void); ``` 下面這個函數聲明是錯誤的: ``` F h(void); ``` 因為函數可以返回`void`類型、標量類型、結構體、聯合體,但不能返回函數類型,也不能返回數組類型。而下面這個函數聲明是正確的: ``` F *e(void); ``` 函數`e`返回一個`F *`類型的函數指針。如果給`e`多套幾層括號仍然表示同樣的意思: ``` F *((e))(void); ``` 但如果把`*`號也套在括號里就不一樣了: ``` int (*fp)(void); ``` 這樣聲明了一個函數指針,而不是聲明一個函數。`fp`也可以這樣聲明: ``` F *fp; ``` 通過函數指針調用函數和直接調用函數相比有什么好處呢?我們研究一個例子。回顧[第?3?節 “數據類型標志”](ch07s03.html#struct.datatag)的習題1,由于結構體中多了一個類型字段,需要重新實現`real_part`、`img_part`、`magnitude`、`angle`這些函數,你當時是怎么實現的?大概是這樣吧: ``` double real_part(struct complex_struct z) { if (z.t == RECTANGULAR) return z.a; else return z.a * cos(z.b); } ``` 現在類型字段有兩種取值,`RECTANGULAR`和`POLAR`,每個函數都要`if ... else ...`,如果類型字段有三種取值呢?每個函數都要`if ... else if ... else`,或者`switch ... case ...`。這樣維護代碼是不夠理想的,現在我用函數指針給出一種實現: ``` double rect_real_part(struct complex_struct z) { return z.a; } double rect_img_part(struct complex_struct z) { return z.b; } double rect_magnitude(struct complex_struct z) { return sqrt(z.a * z.a + z.b * z.b); } double rect_angle(struct complex_struct z) { double PI = acos(-1.0); if (z.a > 0) return atan(z.b / z.a); else return atan(z.b / z.a) + PI; } double pol_real_part(struct complex_struct z) { return z.a * cos(z.b); } double pol_img_part(struct complex_struct z) { return z.a * sin(z.b); } double pol_magnitude(struct complex_struct z) { return z.a; } double pol_angle(struct complex_struct z) { return z.b; } double (*real_part_tbl[])(struct complex_struct) = { rect_real_part, pol_real_part }; double (*img_part_tbl[])(struct complex_struct) = { rect_img_part, pol_img_part }; double (*magnitude_tbl[])(struct complex_struct) = { rect_magnitude, pol_magnitude }; double (*angle_tbl[])(struct complex_struct) = { rect_angle, pol_angle }; #define real_part(z) real_part_tbl[z.t](z) #define img_part(z) img_part_tbl[z.t](z) #define magnitude(z) magnitude_tbl[z.t](z) #define angle(z) angle_tbl[z.t](z) ``` 當調用`real_part(z)`時,用類型字段`z.t`做索引,從指針數組`real_part_tbl`中取出相應的函數指針來調用,也可以達到`if ... else ...`的效果,但相比之下這種實現更好,每個函數都只做一件事情,而不必用`if ... else ...`兼顧好幾件事情,比如`rect_real_part`和`pol_real_part`各做各的,互相獨立,而不必把它們的代碼都耦合到一個函數中。“低耦合,高內聚”(Low Coupling, High Cohesion)是程序設計的一條基本原則,這樣可以更好地復用現有代碼,使代碼更容易維護。如果類型字段`z.t`又多了一種取值,只需要添加一組新的函數,修改函數指針數組,原有的函數仍然可以不加改動地復用。 ## 9.?不完全類型和復雜聲明 在[第?1?節 “復合類型與結構體”](ch07s01.html#struct.intro)講過算術類型、標量類型的概念,現在又學習了幾種類型,我們完整地總結一下C語言的類型。下圖出自[[Standard C]](bi01.html#bibli.standardc "Standard C: A Reference")。 **圖?23.5.?C語言類型總結** ![C語言類型總結](https://box.kancloud.cn/2016-04-02_56ff80d5e1387.gif) C語言的類型分為函數類型、對象類型和不完全類型三大類。對象類型又分為標量類型和非標量類型。指針類型屬于標量類型,因此也可以做邏輯與、或、非運算的操作數和`if`、`for`、`while`的控制表達式,`NULL`指針表示假,非`NULL`指針表示真。不完全類型是暫時沒有完全定義好的類型,編譯器不知道這種類型該占幾個字節的存儲空間,例如: ``` struct s; union u; char str[]; ``` 具有不完全類型的變量可以通過多次聲明組合成一個完全類型,比如數組`str`聲明兩次: ``` char str[]; char str[10]; ``` 當編譯器碰到第一個聲明時,認為`str`是一個不完全類型,碰到第二個聲明時`str`就組合成完全類型了,如果編譯器處理到程序文件的末尾仍然無法把`str`組合成一個完全類型,就會報錯。讀者可能會想,這個語法有什么用呢?為何不在第一次聲明時就把`str`聲明成完全類型?有些情況下這么做有一定的理由,比如第一個聲明是寫在頭文件里的,第二個聲明寫在`.c`文件里,這樣如果要改數組長度,只改`.c`文件就行了,頭文件可以不用改。 不完全的結構體類型有重要作用: ``` struct s { struct t *pt; }; struct t { struct s *ps; }; ``` `struct s`和`struct t`各有一個指針成員指向另一種類型。編譯器從前到后依次處理,當看到`struct s { struct t* pt; };`時,認為`struct t`是一個不完全類型,`pt`是一個指向不完全類型的指針,盡管如此,這個指針卻是完全類型,因為不管什么指針都占4個字節存儲空間,這一點很明確。然后編譯器又看到`struct t { struct s *ps; };`,這時`struct t`有了完整的定義,就組合成一個完全類型了,`pt`的類型就組合成一個指向完全類型的指針。由于`struct s`在前面有完整的定義,所以`struct s *ps;`也定義了一個指向完全類型的指針。 這樣的類型定義是錯誤的: ``` struct s { struct t ot; }; struct t { struct s os; }; ``` 編譯器看到`struct s { struct t ot; };`時,認為`struct t`是一個不完全類型,無法定義成員`ot`,因為不知道它該占幾個字節。所以結構體中可以遞歸地定義指針成員,但不能遞歸地定義變量成員,你可以設想一下,假如允許遞歸地定義變量成員,`struct s`中有一個`struct t`,`struct t`中又有一個`struct s`,`struct s`又中有一個`struct t`,這就成了一個無窮遞歸的定義。 以上是兩個結構體構成的遞歸定義,一個結構體也可以遞歸定義: ``` struct s { char data[6]; struct s* next; }; ``` 當編譯器處理到第一行`struct s {`時,認為`struct s`是一個不完全類型,當處理到第三行`struct s *next;`時,認為`next`是一個指向不完全類型的指針,當處理到第四行`};`時,`struct s`成了一個完全類型,`next`也成了一個指向完全類型的指針。類似這樣的結構體是很多種數據結構的基本組成單元,如鏈表、二叉樹等,我們將在后面詳細介紹。下圖示意了由幾個`struct s`結構體組成的鏈表,這些結構體稱為鏈表的節點(Node)。 **圖?23.6.?鏈表** ![鏈表](https://box.kancloud.cn/2016-04-02_56ff80d5f3b01.png) `head`指針是鏈表的頭指針,指向第一個節點,每個節點的`next`指針域指向下一個節點,最后一個節點的`next`指針域為`NULL`,在圖中用0表示。 可以想像得到,如果把指針和數組、函數、結構體層層組合起來可以構成非常復雜的類型,下面看幾個復雜的聲明。 ``` typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); ``` 這個聲明來自`signal(2)`。`sighandler_t`是一個函數指針,它所指向的函數帶一個參數,返回值為`void`,`signal`是一個函數,它帶兩個參數,一個`int`參數,一個`sighandler_t`參數,返回值也是`sighandler_t`參數。如果把這兩行合成一行寫,就是: ``` void (*signal(int signum, void (*handler)(int)))(int); ``` 在分析復雜聲明時,要借助`typedef`把復雜聲明分解成幾種基本形式: * `T *p;`,`p`是指向`T`類型的指針。 * `T a[];`,`a`是由`T`類型的元素組成的數組,但有一個例外,如果`a`是函數的形參,則相當于`T *a;` * `T1 f(T2, T3...);`,`f`是一個函數,參數類型是`T2`、`T3`等等,返回值類型是`T1`。 我們分解一下這個復雜聲明: ``` int (*(*fp)(void *))[10]; ``` 1、`fp`和`*`號括在一起,說明`fp`是一個指針,指向`T1`類型: ``` typedef int (*T1(void *))[10]; T1 *fp; ``` 2、`T1`應該是一個函數類型,參數是`void *`,返回值是`T2`類型: ``` typedef int (*T2)[10]; typedef T2 T1(void *); T1 *fp; ``` 3、`T2`和`*`號括在一起,應該也是個指針,指向`T3`類型: ``` typedef int T3[10]; typedef T3 *T2; typedef T2 T1(void *); T1 *fp; ``` 顯然,`T3`是一個`int`數組,由10個元素組成。分解完畢。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看