文章目錄
- 1.字符指針變量
- 1.1 字符指針變量類型是什么
- 1.2字符指針變量的兩種使用方法:
- 1.3字符指針筆試題講解
- 1.3.1 代碼解剖
- 2.數組指針變量
- 2.1 什么是數組指針
- 2.2 數組指針變量是什么?
- 2.2.3 數組指針變量的舉例
- 2.3數組指針和指針數組的區別是什么?
- 2.3.1 數組指針和指針數組區別舉例
- 2.4 數組指針變量是怎么初始化
- 3.二維數組傳參的本質
- 4.函數指針變量
- 4.1 函數指針變量的創建
- 4.1.1什么是函數指針變量呢?
- 4.2 函數指針變量的使用
- 4.3 兩段有趣的代碼
- 4.4. typedef關鍵字
- 4.4.1 typedef關鍵字是什么?
- 4.4.2 typedef 關鍵字用法舉例
- 4.4.2.1 舉例1
- 4.4.2.1 舉例2
- 4.4.2.1 舉例3
- 4.4.2.1 舉例4
- 5.函數指針數組
- 5.1 什么是函數指針數組?
- 5.2 如何定義函數指針數組?
- 5.3 函數指針數組舉例
- 6.轉移表
- 6.1.1用函數指針數組作為轉移表實現計算器的代碼邏輯
- 6.1.2用函數指針作為轉移表實現計算器的代碼邏輯
在上一篇博客中:
C語言-指針講解(2)
我們更為深層地給大家介紹了指針的其他基礎用法
讓下面我們來回顧一下講了什么吧:
1.野指針指向的位置是不可知的。我們只有做到:
- 對指針初始化
- 小心數組越界
- 當指針變量不再使用時,應及時置NULL,指針使用之前檢查有效性。
才能在寫代碼避免出現野指針的情況。
2.assert.h頭文件定義了宏assert(),用于在運行中確保程序符合指定程序,如果不符合,就報錯運行運行。這個宏常常被稱為“斷言”。
3.學習指針的用處以及指針傳址調用的用法
4.除了sizeof(數組名)和&數組名,其他情況下數組名都是數組首元素的地址。
5.二級指針是存放一級指針的指針,二級指針是存放一級指針的地址。二級指針的相關運算。
6.存放指針的則稱作指針數組,指針數組每個元素都是用來存放地址的。
7.指針數組模擬二維數組的實例
那么這次博主會給大家介紹指針進階的地方,具體內容看下圖所示:
1.字符指針變量
1.1 字符指針變量類型是什么
我們知道,字符變量類型是char。
那同理,字符指針變量類型是char*。
1.2字符指針變量的兩種使用方法:
int main()
{char ch = 'w';char *pc = &ch;//用指針變量pc來接收ch變量的地址*pc = 'w';//通過對指針變量pc進行解引用操作訪問到ch變量的值return 0;
}
還有一種使用方法:
int main()
{const char* pstr = "hello bit.";//這里是把一個字符串放到pstr指針變量里了嗎?printf("%s\n", pstr);return 0;
}
這里需要注意的是:
這行代碼:const char* pstr = "hello bit."很容易讓同學以為是把字符串hello bit.放到字符指針pstr里了,但其實這是不對的。它本質上是把字符串hello bit.首字符的地址放到了pstr中。
如下圖所示:
通過此圖,我們更能直觀地看到上面代碼的意思實際上是把一個字符串的首字符h的地址存放到指針變量pstr中。
1.3字符指針筆試題講解
下面來自一道《劍指offer》關于字符串相關的筆試題,我們來學習一下。
#include <stdio.h>
int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char *str3 = "hello bit.";const char *str4 = "hello bit.";if(str1==str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if(str3==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}
大家不妨看看這個代碼運行結果是什么,輸出的是哪兩條語句?
我們不妨看一下vs的運行結果:
看到這里,也許有同學會有疑問為什么輸出的是那兩句。接下來我們給大家仔細分析一下。
1.3.1 代碼解剖
如下圖所示:
- 之前我們講過,除sizeof(數組名)和&數組名,其他情況下數組名都是數組首元素的地址。 因此這行代碼:if(str1==str2) 本質上就是兩個地址間進行比較。雖然這里的str1和str2數組存的內容是相同的,但本質上它們都是用相同的常量字符串去初始化不同的數組,這樣就會開辟出不同的內存塊。所以str1和str2不同。
- 而這里的str3和str4指向的是同一個常量字符串。通常C/C++會把常量字符串存儲到單獨的一個內存區域, 所以當幾個指針指向同一個字符串的時候,它們實際上會指向同一塊內存。所以str3和str4是相同的。
2.數組指針變量
2.1 什么是數組指針
數組指針,顧名思義它就是一種指針,里面存放的是數組的地址。
2.2 數組指針變量是什么?
根據前面的指針介紹,我們知道:
1.整形指針變量:int * ptr,存放的是整型變量的地址,能夠指向整形數據的指針。
2.浮點型指針變量:float * pf;存放浮點型變量的地址,能夠指向浮點型數據的指針。
那數組指針變量:就是存放的是數組的地址,能夠指向數組的指針變量。
2.2.3 數組指針變量的舉例
我們來看一下,下面哪個代碼是數組指針變量?
int *p1[10];
int (*p2)[10];
大家可以先思考一下:
這里的數組指針變量是這個:
int (*p)[10];
解釋如下:
p先和*結合,說明p是一個指針變量,然后它指向的是一個大小為10個整型的數組。所以p是一個指針,指向一個數組指針,叫數組指針。
可能有同學會有疑問,為什么p要和*先結合呢?
我們來看下圖所示:
從圖中:我們可以清晰看到,[]的優先級是比* 要高的。所以我們必須加上()來保證p先和*結合。
2.3數組指針和指針數組的區別是什么?
這兩個概念在詞語雖然很相近,但事實是兩個不同的事實。
2.3.1 數組指針和指針數組區別舉例
代碼如下:
int *a[5]和int (*a)[5];
分析:**我們從上面代碼,和剛剛給出操作符優先級的圖可以得知,它們之前相差一個括號,根據優先級的關系(()>[]>*),如果不加括號,a會先與[]結合,加了括號,a則會和 *結合,由此可見,加括號為的是改變運算的結合順序。
- int *a[5]沒有括號,a會先與[]結合,所以它是一個數組。
- int (*a)[5]加了括號,a會與 *結合,所以它是一個指針。
根據上面分析我們得出以下結論:
- int * [5]是指針數組,它本質上是一個數組,數組里面存放的是指針,指針類型是int*,指向的是一個整形數組。
int( * a)[5]是數組指針,它本質是一個指針,里面存放的是數組的地址,指向的是數組的類型是int[5]。
2.4 數組指針變量是怎么初始化
我們知道:數組指針變量是用來存放數組地址的,那如何獲取數組的地址呢,我們之前學習的&數組名,這個&數組名可以把整個數組的地址取出來。
1. int arr[10]={0};
2. &arr;//得到的是數組的地址
如果我們要存放個數組的地址,就得存放在數組指針變量中,
代碼如下:
int (*p)[10] =&arr
我們在調試中也能看到&arr和p的類型是完全一致的。
因此,那個數指針組類型解析如下:
3.二維數組傳參的本質
有了數組指針的理解,我們就能夠講一下二維數組傳參的本質了。
過去我們有一個二維數組傳參給一個函數的時候,我們是這么寫的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", a[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
這里實參是二維數組,形參也可以寫成二維數組的形式,那還有什么其他的寫法呢?
首先我們可以再次理解一下二維數組:二維數組起始可以看做每個元素是一維數組的數組,也就是二維數組的每個元素是一個一維數組。那么二維數組的首元素就是第一行,是個一維數組。
如下圖所示:
所以,我們根據數組名是數組首元素的地址這個規則,二維數組的數組名表示的就是第一行的地址,是一維數組的地址。根據上面的例子,第一行的一維數組的類型就是int[5],所以第一行的地址的類型就是數組指針類型int(*)[5]。那就意味著二維數組傳參本質也是傳遞了地址,傳遞的是第一行這個一維數組的地址,那么形參也是可以寫成指針形式的。
代碼如下:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{int i=0;int j=0;for(i=0;i<r;i++){for(j=0;j<c;j++){printf("%d ", *(*(p+i)+j));}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
可能有同學對上述代碼不理解什么意思?
printf("%d ", *(*(p+i)+j));
我們就來解釋一下吧~
- 我們知道,二維數組的數組名是第一行數組的地址。至于我們為什么要+j?
- 這是因為我們要拿到二維數組中每個元素的地址,而如果說不加j的話,我們總是拿到都是二維數組所在那行的第一個元素地址,這顯然是不合理的。
- 另外,我們看到外層還有一個*,因為是( * (p+i)+j)只是拿到的是每個元素的地址,但我們目的是要拿到它每個元素的值,因此我們還要再對它進行解引用操作。也就是在最外層的括號左邊,再加個 * 才能獲取二維數組每個元素的值。
總結:二維數組傳參,形參的部分可以寫成數組,也可以寫成指針形式。
4.函數指針變量
4.1 函數指針變量的創建
4.1.1什么是函數指針變量呢?
通過前面我們介紹整型指針,數組指針的時候,我們類比關系,不難的得出結論:
函數指針變量是用來存放函數地址,未來通過地址來調用函數的。
那么函數是否有地址呢?
我們不妨測試下面這幾行代碼:
#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;
}
輸出結果如下:
我們可以看到vs編譯器確實打印出來了地址。
因此我們可以得出以下結論:
- 函數是有地址的。
- 函數名就是函數的地址
- 可以通過&函數名的方式獲得函數的地址。
但如果我們要把函數的地址存放起來,那應該怎么做呢?
我們應該創建函數指針變量來存放函數的地址。
其實函數指針變量的寫法其實和數組指針非常類似。
代碼如下所示:
void test()
{printf("hehe\n");
}void (*pf1)() = &test;
void (*pf2)()= test;int Add(int x, int y)
{return x+y;
}int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y寫上或者省略都是可以的
函數指針類型解析:
4.2 函數指針變量的使用
這里我們來給大家演示一下函數指針指向的函數的例子。
include <stdio.h>int Add(int x, int y){return x+y;}int main(){int(*pf3)(int, int) = Add;printf("%d\n", (*pf3)(2, 3));printf("%d\n", pf3(3, 5));return 0;}
4.3 兩段有趣的代碼
接下來有兩段有趣的函數指針的代碼,給大家講一下代碼里面的邏輯~
代碼1:
(*(void (*)())0)();
這個代碼的含義可能很多人都無法理解,但沒事,接下來我將把代碼拆解來給大家講解一下。
我們先看下圖:
解剖代碼1
- 從上圖,我們知道:它其實把0這個整數強制轉換為(void (*)()這種函數指針類型,想讓0當做這種函數的地址。
當我們分析到這樣子,這個代碼的意思也就一目了然了。
它這個代碼1本質的意思就是:
調用0地址處的函數,函數的參數為無參,返回的類型為void。
代碼2:
void (*signal(int , void(*)(int)))(int);
我們繼續來分析一下這個代碼,看看它是什么意思。
同樣地,我們可以把這個代碼拆解成這樣,方便大家可以理解。
解剖代碼2:
從上面我們的代碼分析圖可以得知:
- 聲明的signal函數有2個參數,第一個參數是int類型的,第二個參數是函數指針類型的,該函數指向的是int類型,返回的類型是void。
- signal函數的返回類型也是一個函數指針,該函數指針向的函數,參數是int,返回的類型也是void。
另外,這兩段代碼均出自:《C陷阱和缺陷》這本書,大家有興趣的話可以找來看看~
4.4. typedef關鍵字
4.4.1 typedef關鍵字是什么?
typedef關鍵字,顧名思義,就是用來類型重名名的。可以將復雜的類型,簡單化。
4.4.2 typedef 關鍵字用法舉例
4.4.2.1 舉例1
1.比如,我們覺得數據類型 unsingned int寫起來不方便,如果寫成uint就方便多了,那我們可以怎么寫呢?
具體如下圖所示:
4.4.2.1 舉例2
如果是指針類型,能否重名呢?其實也是可以的,比如:將double*重命名為ptr,要怎么寫呢?
具體寫法如下所示:
4.4.2.1 舉例3
那我們能否對數組指針和函數指針進行重命名呢,這個也是可以的。但是這個跟之前的重名稍微有點區別:
如果我們數組類型是int(*)[10],要重命名為tmp,那我們可以這樣寫:
typedef int(*tmp)[10];//新的類型名必須在*右邊,這里就是將int(*)[10]類型重命名為tmp
int main() {int(*arr)[10];tmp v;return 0;
}
如下圖所示:
同時,我們通過vs調試,發現變量v也是數組指針int(*)[10]類型。
4.4.2.1 舉例4
同理:函數指針類型的重命名也是一樣的,比如,將void(*)(int)類型重命名為ptr_t,就可以這樣寫:
1 typedef void(*pfr_t)(int);//新的類型名必須在*的右邊
如果說,我們要簡化之前的這個代碼,我們該怎么寫呢?
void (*signal(int,void(*)(int)))(int);
如下圖所示:
從圖中,我們發現當我們把函數指針類型重命名為parr_t,vs編譯器是沒有發生任何報錯的。
5.函數指針數組
我們已經到知道:
數組是一個存放相同類型數據的存儲空間,并且我們之前也學習了指針數組。
比如:int *arr[10]; //數組的每個元素是int*
5.1 什么是函數指針數組?
如果我們把函數的地址存到一個數組中,那這個數組就叫函數指針數組。
5.2 如何定義函數指針數組?
我們來看下這行代碼:
int (*parr1[3])();
代碼分析:
由于我們前面介紹過,[]的優先級比*要高,因此,[]會先和parr1結合,說明parr1是數組。那數組的內容是什么呢?答案是:int(*)();
5.3 函數指針數組舉例
如下圖所示:
代碼分析:
從圖中我們發現,由于函數指針p1和p2都是相同的類型,且返回的數據類型為int。因此我們可以創建一個函數指針數組來把兩個函數指針的地址存進去。
6.轉移表
我們前面介紹了函數指針數組和函數指針的概念和基本用法。
那它們的用途是什么呢:轉移表
接下來我將介紹用函數指針或函數指針數組作為轉移表實現計算器的代碼邏輯:
一般來說,我們實現計算器的邏輯是這么寫的。
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}
int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("請選擇:");scanf("%d", &input);switch (input){case 1:printf("輸入操作數:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("輸入操作數:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("輸入操作數:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("輸入操作數:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("選擇錯誤\n");break;}} while (input);return 0;
}
雖然這樣這個計算器代碼這么寫也是可以的。
但是我認為這個代碼有以下不足的:
- switch語句中case子語句很多代碼都是相似的,這樣會顯得有點冗余。
- 如果這個計算器要增加更多的計算功能,比如:
a%b,a&b,a^b,a|b。那這樣的話就Switch的case子語句就會寫得更多,這樣整個代碼邏輯就會變得復雜了。
那如何優化這個計算器的代碼邏輯呢?
6.1.1用函數指針數組作為轉移表實現計算器的代碼邏輯
這個代碼我們可以改進成這樣:
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}void menu() {printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");
}int main()
{int input = 0;int(*ptr[5])(int x,int y) = {0,add,sub,mul,div};//轉移表//至于這里為什么函數指針數組要寫5個,主要是因為數組的下標是從0開始的,而這里存的add~div函數地址最好用下標1-4來表示,這樣可以方便后續計算。do{menu();printf("請選擇:");scanf("%d", &input);if (input >= 1 && input <= 4) {int x = 0;int y = 0;int ret = 0;printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = (*ptr[input])(x, y);printf("%d\n", ret);}else if (input == 0) {printf("退出計算器。\n");}else {printf("輸入有誤,請重新輸入:\n");}} while (input);return 0;
}
分析代碼
從上面這行代碼:
int(*ptr[5])(int x,int y) = {0,add,sub,mul,div};
我們用函數指針數組ptr[5]來把add,sub,mul,div這四個函數的地址分別存進函數指針數組里面去。這里就相當于一個跳板,也就是我們說的轉移表,后續根據用戶輸入input的值,通過函數數組指針ptr下標的值來調用所對應的函數。而那個變量x和y是調用函數時所傳的參數。
6.1.2用函數指針作為轉移表實現計算器的代碼邏輯
代碼如下:
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}void menu() {printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");
}void calc(int(*ptr)(int x, int y)) {int x = 0;int y = 0;int ret = 0;printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = (*ptr)(x, y);printf("%d\n", ret);
}int main()
{int input = 0;do{menu();printf("請選擇>:");scanf("%d", &input);switch (input) {case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出計算器。\n");break;default: {printf("輸入有誤,請重新輸入:\n");}}} while (input);return 0;
}
分析代碼:
- 從上述代碼中,我們是封裝了一個calc函數。根據用戶輸入input的值,進到switch所對應的case的子語句中,并把對應的函數地址傳給calc函數。然后在calc函數它的參數是函數指針類型,返回類型的是int。由于我們只用在calc函數內部進行打印,因此只用在calc函數中左邊寫上個void類型即可。
- 接著進入calc函數內部,根據用戶輸入變量x和y的值,將函數指針ptr的地址和參數x,y的值調用所對應的函數,并把最終計算的結果返回到ret,從而實現計算出兩個操作數的結果出來。
**好了,今天的指針講解3到這就結束了。 **
**如果覺得博主講得不錯的話,歡迎大家支持一下博主!!謝謝大家~ **