天天看點

C語言指針與函數

作者:C語言基礎

C語言指針函數

C語言指針函數就是函數中用到了指針的函數,主要是有以下兩種方式

  • 以指針為參數的函數
  • 以指針為傳回值的函數

指針做函數參數

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

C語言指針與函數

每個函數都有一個獨立的棧區,在函數傳參的過程中,是把實參的值拷貝給形參,修改形參的值并不能作用到實參。如果想要通過形參改變實參的值,就需要傳入實參的位址,可以通過尋址方式作用到實參上,如下圖:

C語言指針與函數

想要修改實參的值,需要傳入實參的位址,故想要修改該指針變量的指向需要傳入指針變量的位址,也就是二級指針。多級指針中也是依次類推,資料結構中常有二級指針傳參。

示例程式| 傳參的方式動态申請一維數組

傳參的方式修改一級指針的值,需要傳入二級指針,通過尋址的方式修改一級指針,如下測試代碼:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
void createArray(int** parray, int arrayNum) 
{
  *parray = (int*)calloc(arrayNum,sizeof(int));
  assert(parray);
}

int main()
{
  int* p = NULL;
  createArray(&p, 3);
  for (int i = 0; i < 3; i++) 
  {
    printf("%d\t", p[i]);
  }
  printf("\n");
  return 0;
}           

運作結果如下:

C語言指針與函數

示例程式| 封裝函數操作數組

通常在封裝函數操作數字類(int ,float,double,…)數組一定要傳入數組長度,操作字元串類通常不需要,因為字元串存在字元串結束标記。例如封裝周遊數組函數和字元串比較函數,代碼如下:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
//等效void printArray(int array[], int arrayNum) 
void printArray(int* array, int arrayNum) 
{
  for (int i = 0; i < arrayNum; i++) 
  {
    printf("%d\t", array[i]);
  }
  printf("\n");
}
int myStrcmp(const char* str1, const char* str2) 
{
  int i = 0;
  int j = 0;
  //字元串比較從左往右比,找到不同的字元即可得到比較結果
  while (str1[i] == str2[j]&&str1[i] != '\0')
  {
    i++;
    j++;
  }
  return str1[i] - str2[j];
}
int main()
{
  int array[5] = { 1,2,3,4,5 };
  printArray(array, 5);
  printf("%d\n", myStrcmp("string1", "string")>0);
  printf("%d\n", myStrcmp("string", "string")==0);
  printf("%d\n", myStrcmp("string", "string1")<0);
  return 0;
}           

運作結果如下:

C語言指針與函數

當然比較函數你也可以傳回0,-1,1,隻需要在字元串比較函數中分類讨論下即可。

指針做函數傳回值

指針當做函數傳回值和普通函數一樣,隻是傳回值類型不同而已,既然傳回是一個指針,*指針等效變量,故*函數調用也可以等效變量。把指針當做函數傳回值注意項:

  • 不要傳回臨時變量的位址
  • 可以傳回動态申請的空間的位址
  • 可以傳回靜态變量和全局變量的位址

當函數傳回臨時變量的位址時,位址中存儲的資料随着函數調用完會被回收掉,導緻擷取垃圾值。如下測試代碼:

#include <stdio.h>
int* testFunc() 
{
  int number = 1314;
  return &number;
}
int main()
{
  int* result=testFunc();
  //第一次資料做了保留
  printf("%d\n", *result);
  //後續資料被回收了,垃圾值
  printf("%d\n", *result);
  printf("%d\n", *result);
  return 0;
}           

運作結果如下:

C語言指針與函數

在vs開發工具中會友善給予提醒,希望看到這類提醒當做錯誤處理,及時改善,友善提醒如下:

C語言指針與函數

示例程式| 傳回值的方式動态申請一維數組

可以傳回動态申請的空間的位址,堆區記憶體需要調用free函數手動釋放,如下測試代碼:

#include <stdio.h>
#include <stdlib.h>
int* createArray(int arrayNum) 
{
  int* p = (int *)calloc(arrayNum, sizeof(int));
  return p;
}
int main()
{
  int* p = NULL;
  p = createArray(3);
  for (int i = 0; i < 3; i++) 
  {
    printf("%d\t", p[i]);
  }
  free(p);
  p = NULL;
  return 0;
}           

運作結果如下:

C語言指針與函數

示例程式| 用字元串初始化堆區記憶體并傳回首位址

其實和數字類的操作沒什麼太大差別,唯一要注意的是字元串申請統計長度用strlen,申請是可見長度加1,拷貝指派用strcpy完成,如下測試代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
char* createArray(const char* str) 
{
  //申請長度是可見度長度+1
  unsigned int length = strlen(str)+1;
  char* p = (char *)calloc(length, sizeof(int));
  assert(p);
  //不能直接 p=str,文法沒問題但是意義不同
  strcpy(p, str);
  return p;
}
int main()
{
  char* pstr = NULL;
  pstr = createArray("coolmoying");
  puts(pstr);
  free(pstr);
  pstr = NULL;
  return 0;
}           

運作結果如下:

C語言指針與函數

C語言函數指針

什麼是函數指針

如果在程式中定義了一個函數,那麼在運作時系統就會為這個函數代碼配置設定一段存儲空間,這段存儲空間的首位址稱為這個函數的位址。擷取函數位址有以下兩種方式:

  • 函數名
  • &函數名

既然是位址我們就可以定義一個指針變量來存放,這個指針變量就叫作函數指針變量,簡稱函數指針。函數指針的唯一作用就是調用函數,函數指針沒有++和 –運算

如何建立函數指針

函數傳回值類型 (*指針變量名) (函數參數清單);

簡單來說一句話,用(*變量名) 替換函數名,剩下的照抄即可,形參名可寫可不寫就是函數指針變量。如下函數的函數指針建立:

C語言指針與函數

如何通過函數指針調用函數

函數指針可以通過不同的初始化方式,調用除了函數名不同,其他類型相同的所有函數。調用方式有以下兩種:

  • 直接函數指針名替換函數名去調用函數
  • (*函數指針)替換函數名的方式去調用函數

推薦使用第一種方式,代碼看起來比較簡單。如下測試代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
void test() 
{
  printf("Test\n");
}
void test2() 
{
  printf("Test2\n");
}
int Max(int a, int b) 
{
  return a > b ? a : b;
}
void printArray(int(*p)[3], int row, int cols) 
{
  for (int i = 0; i < row; i++) 
  {
    for (int j = 0; j < cols; j++) 
    {
      printf("%d ", p[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  //建立函數指針變量
  void (*pTest)() = NULL;
  int(*pMax)(int a, int b) = NULL;
  //參數名可省略
  void (*pprint)(int(*)[3], int, int) = NULL;
  //函數指針指派
  //兩種方式即可
  pTest = test;
  pTest = &test;
  pMax = Max;
  pprint = printArray;
  //函數指針變量調用函數
  //兩種方式即可
  pTest();
  (*pTest)();
  printf("%d\n",pMax(1, 2));
  int array[2][3] = { 1,2,3,4,5,6 };
  pprint(array, 2, 3);
  //調用除了函數名不同,其他類型相同的所有函數
  pTest = &test2;
  pTest();
  return 0;
}           

運作結果如下:

C語言指針與函數

回調函數

回調函數就是以函數指針作為某個函數的參數,函數指針比較重要的應用就是回調函數,在Windows SDK,多線程,事件進行中大量用到回調函數。函數指針變量可以作為某個函數的參數來使用的,回調函數就是一個通過函數指針調用的函數。簡單講:回調函數是由别人的函數執行時調用你實作的函數。通俗的講:你到一個商店買東西,沒有貨,留給店員電話,有貨了,打電話給你,然後你去取貨。在這個例子裡,你的電話号碼就叫回調函數,你把電話留給店員就叫登記回調函數,店裡後來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店裡去取貨叫做響應回調事件。如下測試代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
void get() 
{
  printf("取貨成功!!!\n");
}
void wait() 
{
  printf("等待售貨員電話!...\n");
}
void salesperson(bool flag, void(*Doing)()) 
{
  if (flag == true)  //有貨 
  {
    printf("通知取貨\n");
    Doing();
  }
  else         //無貨
  {
    printf("無貨\n");
    Doing();
  }
}
int main()
{
  //通常回調函數有關聯的事件
  //這裡簡單用有無貨物來做
  salesperson(false, wait);
  salesperson(true, get);
  return 0;
}           

通常salesperson是第三方封裝好的,我們隻需要實作salesperson函數指針,通過salesperson去調用自己的函數,通常别人設計的回調函數都會綁定事件,目前初步接觸了解下。運作結果如下:

C語言指針與函數

C語言萬能指針充當函數指針

萬能指針充當函數指針使用前必須要強制類型轉換,函數指針的類型就是去掉變量名即可 ,如下測試代碼:

#include <stdio.h>
#include <stdlib.h>
void test() 
{
  printf("調用成功!!!\n");
}
int main()
{
  void* p = test;
  //正常指針調用:p();
  //test類型: void(*)()
  //強轉文法:  (類型)(表達式)
  ((void(*)())p)();
  return 0;
}           

運作結果如下:

C語言指針與函數

複雜函數指針解析

右左法則

首先找到辨別符,然後往右看,再往左看,每當遇到圓括号時,就應該調轉閱讀方向,一旦解析完圓括号裡面的所有東西,就跳出圓括号,重複這個過程直到整個聲明解析完畢。

示例1| int (*func)(int *p)

首先找到那個辨別符,就是func,它的外面有一對圓括号,而且左邊是一個*号,這說明func是一個指針,然後跳出這個圓括号,先看右邊,也是一個圓括号,這說明(*func)是一個函數,而func是一個指向這類函數的指針,就是一個函數指針,這類函數具有int*類型的形參,傳回值類型是 int。

示例2| int (*func)(int *p, int (*f)(int*))

func被一對括号包含,且左邊有一個*号,說明func是一個指針,跳出括号,右邊也有個括号,那麼func是一個指向函數的指針,這類函數具有int *和int (*)(int*)這樣的形參,傳回值為int類型。再來看一看func的形參int (*f)(int*),類似前面的解釋,f也是一個函數指針,指向的函數具有int*類型的形參,傳回值為int。

示例3| int (*func[5])(int *p)

func右邊是一個[]運算符,說明func是一個具有5個元素的數組,func的左邊有一個*,說明func的元素是指針,要注意這裡的*不是修飾 func的,而是修飾func[5]的,原因是[]運算符優先級比*高,func先跟[]結合,是以*修飾的是func[5]。跳出這個括号,看右邊,也是一對圓括号,說明func數組的元素是函數類型的指針,它所指向的函數具有int*類型的形參,傳回值類型為int。

示例4| int (*(*func)[5])(int *p)

func被一個圓括号包含,左邊又有一個*,那麼func是一個指針,跳出括号,右邊是一個[]運算符号,說明func是一個指向數組的指針,現在往左看,左邊有一個*号,說明這個數組的元素是指針,再跳出括号,右邊又有一個括号,說明這個數組的元素是指向函數的指針。總結一下就是:func是一個指向數組的指針,這個數組的元素是函數指針,這些指針指向具有int*形參,傳回值為int類型的函數。

示例5| int (*(*func)(int *p))[5]

func是一個函數指針,這類函數具有int*類型的形參,傳回值是指向數組的指針,所指向的數組的元素是具有5個int元素的數組。

示例6| int (*(*(*func)(int *))[5])(int *)

func是一個函數指針,這類函數的傳回值是一個指向數組的指針,所指向數組的元素也是函數指針,指向的函數具有int*形參,傳回值為int。

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

客觀請留步

如果閣下正好在學習C/C++,看文章比較無聊,不妨關注下關注下小編的視訊教程,通俗易懂,深入淺出,一個視訊隻講一個知識點。視訊不深奧,不需要鑽研,在公交、在地鐵、在廁所都可以觀看,随時随地漲姿勢。

繼續閱讀