C語言指標常見問題

2023-01-05 21:03:33

  我們在學C語言時,指標是我們最頭疼的問題之一,針對C語言指標,博主根據自己的實際學到的知識以及開發經驗,總結了以下使用C語言指標時常見問題。

指標

指標做函數引數

學習函數的時候,講了函數的引數都是值拷貝,在函數裡面改變形參的值,實參並不會發生改變。

 

如果想要通過形參改變實參的值,就需要傳入指標了。

 

注意:雖然指標能在函數裡面改變實參的值,但是函數傳參還是值拷貝。不過指標雖然是值拷貝,但是卻指向的同一片記憶體空間。

 

指標做函數返回值

返回指標的函數,也叫作指標函數。

和普通函數一樣,只是返回值型別不同而已,先看一下下面這個函數,非常熟悉對不!

int fun(int x,int y);

接下來看另外一個函數宣告

int* fun(int x,int y);

這樣一對比,發現所謂的指標函數也沒什麼特別的。

注意:

  • 不要返回臨時變數的地址

  • 可以返回動態申請的空間的地址

  • 可以返回靜態變數和全域性變數的地址

函數指標

如果在程式中定義了一個函數,那麼在執行時系統就會為這個函數程式碼分配一段儲存空間,這段儲存空間的首地址稱為這個函數的地址。而且函數名錶示的就是這個地址。既然是地址我們就可以定義一個指標變數來存放,這個指標變數就叫作函數指標變數,簡稱函數指標。

函數指標定義

函數返回值型別 (* 指標變數名) (函數參數列);

  • 「函數返回值型別」表示該指標變數所指向函數的 返回值型別;

  • 「函數參數列」表示該指標變數所指向函數的參數列。

那麼怎麼判斷一個指標變數是指向變數的指標,還是指向函數的指標變數呢?

  • 看變數名的後面有沒有帶有形參型別的圓括號,如果有就是指向函數的指標變數,即函數指標,如果沒有就是指向變數的指標變數。

  • 函數指標沒有++和 --運算

函數指標使用

定義一個實現兩個數相加的函數。

int add(int a,int b)
{
    return a+b;
}
int main()
{
    int (*pfun)(int,int) = add;
    int res = pfun(5,3);
    printf("res:%d\n",res);
    
    return 0;
}

在給函數指標pfun賦值時,可以直接用add賦值,也可以用&add賦值,效果是一樣的。

在使用函數指標時,同樣也有兩種方式,1,pfun(5,3); 2,(*pfun)(5,3)

案例

計算器

用函數指標實現一個簡單的計算器,支援+、-、*、/、%

//plus sub multi divide mod     //加 減 乘 除 取餘

當功能太多時,switch語句太長,因此不是一種好的程式設計風格。好的設計理念應該是把具體的操作和和選擇操作的程式碼分開。

函數指標作為轉換表

轉換表就是一個函數指標陣列。

#include<stdio.h>
#include<math.h>
​
// 轉換表
// 轉換表 step1:
//(1.1)宣告 轉檯轉移函數
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
double hypotenuse(double, double);
//(1.2)宣告並初始化一個函數指標陣列    pfunc:陣列   陣列元素:函數指標  返回值:double型資料
double(*pfunc[])(double, double) = { add, sub, mul, div, hypotenuse };//5個轉移狀態
​
//狀態轉移函數的實現
double add(double a, double b){ return a + b;}
double sub(double a, double b){ return a - b; }
double mul(double a, double b){ return a * b; }
double div(double a, double b){ return a / b; }
double hypotenuse(double a, double b){ return sqrt(pow(a, 2) + pow(b, 2)); }
​
void test()
{
    //轉換表 step2:呼叫 函數指標陣列
    int n = sizeof(pfunc) / sizeof(pfunc[0]);//轉移表中 包含的元素個數(狀態轉移函數個數)
    for (int i = 0; i < n; ++i){
        printf("%.2lf\n",pfunc[i](3, 4));
    }
}
int main()
{
    test();
    return 0;
}

typedef

一,使用typedef為現有型別建立別名,給變數定義一個易於記憶且意義明確的新名字。

  • 型別過長,用typedef可以簡化一下

typedef unsigned int UInt32
  • 還可以定義陣列型別

typedef int IntArray[10];
IntArray arr;               //相當於int arr[10]

 

二、使用typedef簡化一些比較複雜的型別宣告。

例如:

typedef int (*CompareCallBack)(int,int);

上述宣告引入了PFUN型別作為函數指標的同義字,該函數有兩個型別分別為int、int、char引數,以及一個型別為int的返回值。通常,當某個函數的引數是一個回撥函數時,可能會用到typedef簡化宣告。 例如,承接上面的範例,我們再列舉下列範例:

int callBackTest(int a,int b,CompareCallBack cmp);

callBackTest函數的引數有一個CompareCallBack型別的回撥函數。在這個範例中,如果不用typedef,callBackTest函數宣告如下:

int callBackTest(int a,int b,int (*cmp)(int,int));

從上面兩條函數宣告可以看出,不使用typedef的情況下,callBackTest函數的宣告複雜得多,不利於程式碼的理解,並且增加的出錯風險。

所以,在某些複雜的型別宣告中,使用typedef進行宣告的簡化是很有必要的。

回撥函數

首先要明確的一點是,函數也可以作為函數的引數來傳遞。

當做函數引數傳入的函數,稱之為 回撥函數(至於為什麼要叫「回撥函數」,不能叫別的呢?其實這只是人為規定的一個名字。你也可以叫「maye專屬函數」,但是到時候你又會問為什麼要叫「maye專屬函數」,它特麼的總的有個名字吧!所以叫「回撥函數」就是王八的屁股:規定!)。

實現一個與型別無關的查詢函數

 

如何看懂複雜的指標

指標大家都學過了,簡單的指標相信大家都不放在眼裡,就不再贅述,但是複雜的你能理解嗎?能理解指標就學的差不多了,至於如何運用只要你看懂指標就知道應該給它賦什麼值,怎麼用。

  • 首先咱們一起來看看這個: int (*fun)(int *p)

    • 首先需要分析這個是不是一個指標,如果是,是什麼指標?如果不是,那是什麼?

      1. 根據(*fun)可知,fun是一個指標

      2. 然後看fun的後面是一個函數參數列,可以確定是一個指向函數的指標

      3. 指向的函數的返回值是什麼型別呢,再回頭看看最前面發現是一個int

      4. 最後我們可以根據這個函數指標寫出對應的函數

結果如下:

int foo(int *p)
{
    return 0;
}

 

右左法則

上面我們分析了一個函數指標,那結果是如何得出來的呢?全靠經驗嗎,NO,其實是有方法的。

這個方法叫做右左法則

  • 右左法則不是C標準裡面的內容,它是從C標準的宣告規定中歸納出來的方法。C標準的宣告規則,是用來解決如何建立宣告的,而右左法則是用來解決如何辯識一個宣告的。

  • 右左法則使用:

      1. 首先從最裡面的圓括號(應該是識別符號)看起,然後往右看,再往左看;

      2. 每當遇到圓括號時,就應該調轉閱讀方向;

      3. 一旦解析完圓括號裡面所有東西,就跳出圓括號;

      4. 重複這個過程知道整個宣告解析完畢。

 

案例走起

1.int (*p[5])(int*)

解析:

  1. 從識別符號p開始,p先與[]結合形成一個陣列,然後與*結合,表示是一個指標陣列;

  2. 然後跳出這個圓括號,往後看,發現了一個函數的參數列,說明陣列裡面裝的是函數指標;

  3. 在跳出圓括號,往前看返回型別,可以確定函數指標的型別。

2. int (*fun)(int *p,int (*pf)(int *))

解析:

  1. fun與*結合形成指標;

  2. 往後看是一個參數列,說明是一個函數指標,只不過引數裡面還有一個函數指標;

  3. 往前看可以確定函數指標的返回型別。

3. int (*(*fun)[5])(int *p)

解析:

  1. fun與*結合,形成指標;

  2. 往後看發現了一個[5]說明是一個指向陣列的指標;

  3. 再往前看,發現有一個*,說明陣列裡面存的是指標;

  4. 跳出圓括號往後看,發現了參數列,說明陣列裡面存的是函數指標;

  5. 再往前看可以確定函數指標的返回型別。

 

4. int (*(*fun)(int *p))[5]

解析:

  1. fun與*結合,形成指標;

  2. 往後看發現了參數列,說明fun是一個函數指標;

  3. 往前看遇到了*說明,函數指標的返回型別是一個指標,是什麼指標繼續往後解析;

  4. 往後看發現了[5] 說明是一個陣列指標,最前面一個int,說明fun這個函數指標的返回型別是一個陣列的指標

    型別為int (*)[5]

     

5. int(*(*fun())())()

解析:

  1. fun與()結合,說明fun是一個函數;

  2. 往前看發現了一個*,說明函數返回型別為指標,什麼指標呢?

  3. 往後看發現了參數列,fun函數返回的是一個函數指標,那這個函數指標的返回型別是什麼呢?

  4. 往前看又發現了一個*,說明函數指標返回型別也是一個指標,那這個指標是什麼指標呢?

  5. 往後看又發現了一個參數列,說明是個函數指標,往前看這個函數指標返回的是int型別

 

總結

實際當中,需要宣告一個複雜指標時,如果把整個宣告寫成上面所示的形式,對程式可讀性是一大損害。應該用typedef來對宣告逐層分解,增強可讀性

 

指標變數有兩種型別:指標變數的型別和指標所指向的物件的型別

指標變數的型別 只要把指標宣告語句裡的指標名字去掉,剩下的部分就是這個指標的型別。

  • int* ptr; //指標的型別是int

  • char* ptr; //指標的型別是char

  • int** ptr; //指標的型別是int**

  • int(*ptr)[3]; //指標的型別是int()[3]

  • int*(*ptr)[4]; //指標的型別是int*(*)[4]

指標變數指向的物件的型別

  • 你只須把指標宣告語句中的指標名字和名字左邊的指標宣告符*去掉,剩下的就是指標所指向的型別。

    • int*ptr; //指標所指向的型別是int

    • char*ptr; //指標所指向的的型別是char

    • int**ptr; //指標所指向的的型別是int*

    • int(*ptr)[3]; //指標所指向的的型別是int()[3]

    • int*(*ptr)[4]; //指標所指向的的型別是int*()[4]

 

注意事項:

  • 指標變數也是變數,也有儲存空間,存的是別的變數的地址。

    • 要注意指標的值,和指向的物件的值得區別

    • 普通變數中的記憶體空間存放的是,數值或字元等。 ----直接存取

    • 指標變數中的記憶體空間存放的是,另外一個普通變數的地址。----間接存取

       

  • 連續定義多個指標變數時,容易犯錯誤,比如:int *p,p1;只有p是指標變數,p1是整型變數

  • 避免使用為初始化的指標,很多執行錯誤都是由於這個原因導致的,而且這種錯誤又不能被編譯器檢查所以很難被發現,解決方法:初始化為NULL,報錯就能很快找到原因

  • 指標賦值時一定要保證型別匹配,由於指標型別確定指標所指向物件的型別,操作指標是才能知道按什麼型別去操作

  • 在用動態分配完記憶體之後一定要判斷是否分配成功,分配成功後才能使用。

  • 在使用完之後一定要釋放,釋放後必須把指標置為NULL

  •