C語言指針基本知識

  • 2021 年 2 月 21 日
  • 筆記

對程式進行編譯的時候,系統會把變數分配在記憶體單位中,根據不同的變數類型,分配不同的位元組大小。比如int整型變數分配4個位元組,char字元型變數分配1個位元組等等。被分配在記憶體的變數,可以通過地址去找到,記憶體區每一個位元組都有一個編號,地址也可以形象的理解成我們生活中的住址,通過住址找到每一個人所在的地方。指針作為一個變數用來存放地址,可以通過指針來改動變數。

上圖就是一個簡單的定義一個一級指針變數和利用指針改變變數數值的過程。int*表示整型指針,*p表示解引用操作,就是利用指針找到a的地址然後再改變a的值。

地址用%p列印,用十六進位表示,在列印時候輸入指針變數p和取地址a得出的結果是相同的,證明了指針是用來存放地址的。

指針作為一個變數是有大小的,其大小在32位平台是4個位元組,64位平台上是8個位元組,大小與指針的類型無關。

上圖以32位平台舉例子,可以看到無論指針是整型、字元型、浮點型也無論一級指針還是二級指針,其在記憶體空間所佔的大小都是4個位元組。

指針有多種類別,按照級數來分便可以分為一級指針,二級指針,三級指針等等

一級指針是最基礎的指針,指向的是創建的變數的地址。就類似於上圖的前三個sizeof後面所寫的。前文講到指針也是一個變數,是用來存放地址的。既然是一個變數,就也要在記憶體開闢空間,開闢了空間就也會產生屬於指針變數自己的地址。二級指針便是用來存放一級指針地址的。以此類推多級指針也是如此。

指針也可以根據指針指向的變數的數據類型來進行分類,有整型指針,字元指針,數組指針,函數指針等等

整型指針和字元指針

這兩個是比較常見和容易理解的指針,依次用int*和char*表示,他們的區別在於指向變數類型不同,記憶體也不一樣,在進行解引用操作時訪問的位元組大小也因為變數類型的區別會有所差異。整型指針可以訪問4個位元組,而字元指針只能訪問1個位元組。也就是說對整型指針變數解引用,一次可以操作一個整型,而對字元變數解引用一次只能操作一個字元。

較為特殊的char*p=”hello”這並不是將整個字元串的地址傳個了p,而是傳了字元穿首元素『h’的地址,可以通過』h『的地址來找到整個字元串。此時出現char*p2=「hello」,p2和p代表的是同一處地址,因為hello是常量字元串,沒有必要開闢兩塊不同的空間的來存儲它。這是字元指針的一個特性。

void型指針

void型的指針可以接受任何類型的地址,但是不能對void型指針進行解引用操作。解引用操作要有特定的訪問位元組的數量,比如對整型指針解引用就是訪問4個位元組,字元型指針解引用就是訪問1個位元組,而void型指針無法確定訪問位元組個數,所以不能進行解引用操作。同時void*這種類型的指針也不能進行加減整數的操作,因為無法確定跳過的位元組個數。

此圖表示了void型指針可以接受任意類型的地址。

數組指針

這是一種指向數組的指針,例如int(*p)[10]這就是一個指向數組的指針,它指向的數組有10個元素,每個元素都是整型。給*p加上括弧是因為p和[10]優先結合,這樣的話就變成了一個數組而不是指針了。這個數組叫指針數組,int*p[10]這樣的寫法意思是一個有10個元素的數組,每一個元素都是整型指針,這和數組指針是兩個不同的東西。

指向數組的指針裡面存放的便是數組的地址,而非數組某個元素的地址,所以在定義數組指針時要用 &+數組名,而不是簡單使用 數組名。

 

上圖顯示出&arr和arr的不同,雖然起始地址相同,但arr+1隻讓指針向後移動了一個元素的空間,而&arr+1讓指針移動了一個數組的空間。

函數指針

函數指針顧名思義就是指向函數的指針,每個函數都有一個入口,這個入口的地址便是函數指針所指向的地址。函數地址的表示方法為 函數名或 &+函數名。例如一個函數叫Add,&Add和Add都是表示這個函數的地址沒有什麼差別。函數指針的寫法是  函數的返回類型(*)(函數的參數),例如函數Add,其函數指針的寫法就是int(*p)(int,int)=Add 。*p要加上括弧來保證*和p的優先結合來形成一個指針變數,如果不加括弧來優先結合,則會出現int* p(int,int)這樣的寫法,這就變成了函數的聲明,這個函數的返回類型是int*,函數的名字叫p,函數的參數是2個整型和原先的函數指針不是同一個意思。

用函數指針調用函數時可以不加*這個解引用符號,因為這個符號將不會在程式運行的時候起到作用。

 上圖顯示了*這個解引用符號在函數指針調用函數時候不起作用,以上的寫法都可以用。

根據函數指針的相關知識,可以來看這兩段程式碼。

程式碼1中間的 void(*)()是一個函數指針類型,將這個函數指針類型放在括弧中,是強制類型轉換的意思也就是把0強制轉換成一個函數指針,強制類型轉換這個部分簡單寫出來就是「(函數指針)0」是將0作為一個函數的地址,而最外層的括弧(*函數的地址)()這個是解引用操作,也就是通過0這個地址,找到了0地址處所在的函數,並且進行調用。

程式碼2   內部的(int,void(*)(int))這一段表示的函數的參數,第一個參數是一個整型,第二個參數是一個函數指針類型,這個函數指針指向的函數的返回類型是void,參數類型是int。而這個函數的名字就是signal。解決了這個部分的內容,剩下的就是void(*)(int),去除裡面的signal函數可以很明顯地看出來這是一個函數指針。一個函數由三部分組成,返回類型,函數名,函數的參數。也就是說參數和函數名去掉之後,函數聲明中就只剩下一個返回類型。此時,函數名和參數已經在前一步分析中得出,剩下的void(*)(int)便就是函數的返回類型,這個函數返回類型是也是一個函數指針。

這兩個程式碼來自於書本《C陷阱和缺陷》。

函數指針和數組的結合實例,簡易的計算器,這是函數指針數組的應用

數組傳參


數組在傳參的時候傳的是首元素的地址,數組名表示首元素的地址。函數的形參可以用數組形式表示也可以用指針形式表示。

一維數組的傳參比較簡單,例如int arr[3]這個數組,形參可以直接使用int arr[]或者int arr[3]用數組形式表示形參,形參處的元素個數可以寫也可以不寫,因為元素個數在這裡不起作用。或者用一級指針表示,int* arr這樣就反映了指針傳參傳的是首元素地址。

二維數組傳參相對比較複雜,由數組的知識可以知道,二維數組必須有規定的列數,所以要以數組形式傳參的時候列數不能省略。

以指針形式傳參,數組名仍然是首元素地址的意思,作為一個二維數組,首元素便是第一行的數組。比如int arr[3][5]這個二維數組的首元素是一個含有5個整型元素的數組,所以在傳參的時候傳的指針也應該是指向這個數組的指針。所以此時形參應該表示為int (*arr)[5],這表示一個數組指針,指向一個含有5個整型元素的數組,符合正確的傳參規則。

回調函數

回調函數是把函數指針作為參數傳給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由函數實現方直接調用,而是用另外一方或者特定條件下來調用。

比較常見的例子就是C語言裡面的庫函數快速排序,這裡需要自己實現的比較函數,就用到了回調函數,int_cmp作為函數的指針充當了qsort的參數。

 

模擬實現qsort快速排序函數,冒泡排序的推廣