目錄
一、字符指針變量
二、數組指針變量
三、二維數組傳參的本質
四、函數指針變量
4.1 函數指針變量
4.2 函數指針變量的使用
4.3 函數指針變量的拓展
五、函數指針數組
六、轉移表的應用
通過深入理解指針(1)和深入理解指針(2),我們對指針有了一個初步的了解,學會了一級指針、二級指針、指針數組……而深入理解指針(3),主要是為了學習不同數據類型的指針變量。
一、字符指針變量
? ? ?字符串指針變量的指針類型為char*,下面我們通過這段代碼來解析字符指針變量。
int main()
{printf("指針接收字符\n");char ch = 'w';char* pc = &ch;printf("\t*pc=%c\n", *pc);printf("----------------\n");printf("指針接收字符串\n");const char* pstr = "abcdef";//const 加了一層保護,使其變成常量字符串,被修改編譯器會報錯printf("\t*pstr=%c\n", *pstr);//其實是把字符串的首字符地址放到pstr,字符串出現在表達式中時,他的值就是第一個字符的地址printf("\tpstr=%s\n", pstr); //%s占位符的特點就是只要告訴他字符串的首地址, 就可以讀取整個字符串printf("\tpstr[3]=%c\n", pstr[3]);//[]是特殊的解引用操作符,等價于*(pstr+3),相當于得到第1個元素偏移3得到第四個元素printf("\tabcdef[3] = % c\n", "abcdef"[3]);//可以把字符串想象成一個字符數組,可以通過下標去訪問他return 0;
}
指針接收字符
? ? ? ? *pc=w
----------------
指針接收字符串
? ? ? ? *pstr=a
? ? ? ? pstr=abcdef
? ? ? ? pstr[3]=d
? ? ? ? abcdef[3] = d?
? ? ? ?字符指針變量,顧名思義就是指向字符的指針變量,所以利用指針接收字符的地址(第31行代碼),最后解引用該指針變量得到的是對應的字符,非常容易理解。 但字符指針變量還有一種方式,就是接收字符串的地址。
? ? ? ?通過第35行代碼,我們用字符指針變量pstr接收了字符串“abcdef”,那這是把整個字符串放到pstr指針變量里面了嗎?
? ? ? 其實并不是的,我們通過第36行代碼的運行結果,發現將指針變量pstr解引用后得到的是‘a’,這說明字符指針變量pstr接收字符串的本質是將字符串的首字符地址存放到pstr中,所以如果字符串出現在表達式中,他的值就是第一個字符的地址。
? ? ? 既然pstr存放的是字符串首字符的地址,那么我們打印出來的是一個地址,但我們在看向第37行代碼,當我們用%s的占位符時,卻可以直接將整個字符串打印出來,這說明了%s占位符的特點就是只要告訴他字符串首字符的地址,他就可以直接讀取整個字符串。
? ? ?那為什么,我們知道了字符串的首元素地址,就可以通過%s打印出字符串全體呢?這是因為其實我們可以把字符串理解成一個字符數組,他具有數組的特點,可以通過首元素地址找到后面的全部元素,并且也可以像數組一樣通過下標去訪問每個元素,比如我們想訪問字符串下標為3的元素(d),那么通過第39行代碼我們可以發現“abcdef”[3]是可行的,
? ? ? 既然可以通過下標去訪問字符串,那么既然pstr是接收字符串的指針變量,那么我們同樣可以通過首元素地址的指針偏移來找到下標為3的元素,第38行代碼中的pstr[3](等價“*(pstr+3)”)也是可行的。
下面是一道和字符串相關的面試題。
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;
}
str1 and str2 are not same
str3 and str4 are same
為什么str1和str2的地址不同,而str3和str4的地址相同呢??
? ? ? ?將常量字符串賦值給數組(str1和str2),本質上是將這個常量字符串復制一份到數組中,這兩個數組其實并不在一個空間,所以str1=str2,并且復制出來的常量字符串是可以修改的。
? ? ? ?而如果通過字符指針變量指向常量字符串(str3和str4),對于常量字符串來說,是只能讀不能改的,從內存利用率來說,內容相同的字符串只會保存一份,所以str3=str4.
二、數組指針變量
我們學過指針數組,它是一個存放指針的數組。
那什么是數組指針變量呢?我們通過已經學過的指針變量來類比一下。
所以數組指針變量是一個存放的是數組的地址,并且能夠指向數組的指針變量。?
int* p1[10];
int(*p2)[10];
以上哪個是數組指針變量呢?
? ? ?對于int*p1[10]來說,首先p1會先和[ ]結合,然后int和*結合,所以p1有10個元素,并且每個元素是int*類型,所以p1是一個存放指針的數組,p1是指針數組。([ ]的優先級高于*)
? ? ?對于int(*p2)[10]來說,p2先和*結合了,所以*p2是一個指針,int和[10]代表p2指向的是一個數組,并且有10個元素,并且每個元素的類型是int,所以p2是數組指針。(因為[ ]的優先級高于*,所以必須加上( )來保證p和*先結合)
? ? ?那數組指針如何初始化呢?既然指針變量是用來存放數組地址的,而&arr是取整個數組的地址,所以寫法就是int(*p2)[10]=&arr。
三、二維數組傳參的本質
數組指針有什么用呢?其實數組指針有自己的應用場景,在此之前要先了解二維數組傳參的本質
以往我們對有一個二維數組需要傳遞給函數時,我們是這樣寫的
void test(int a[][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;
}
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7?
形參和實參都是二維數組的形式,但其實還有其他寫法。
? ? ? 對于二維數組來說,可以看做是每個元素是一維數組的數組,也就是二維數組的每個元素是一個一維數組。那么二維數組的首元素就是第一行,是個一維數組。
? ? ?根據一維數組的數組名名就是首元素地址、一維數組傳參本質是傳遞首元素地址這個規則,我們可以推出二維數組的數組名就是就是第一行(一維數組)的地址,二維數組傳參本質是傳遞第一行這個一維數組的地址。
? ? ?根據上面的代碼,我們知道該二維數組第一行的一維數組的數據類型是int[5],所以第一行的地址類型就是數組指針類型int(*)[5],所以我們可以將形參類型寫成指針形式。 ??
? ? 接下來對上面的代碼進行改寫,將形參寫成數組指針類型。
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;
}
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7?
怎么去理解*(*(p+i)+j))呢?我們要進行拆解!(假設訪問二維數組中的一個元素)
首先是p+i,二維數組的首元素地址是第一行的一維數組,所以p存放的是第一行的地址,所以+i會跳過i行,i=0時,此時跳過0行,拿到的是第一行的地址,i=1時,跳過1行,拿到的是第二行的地址,i=2時,跳過2行,拿到的是第三行的地址。
然后是*(p+i),假設i已經確定,此時就是通過解引用拿到了一行的數據。
然后是*(p+i)+j,此時*(p+i)已經拿到一行的數據了,通過j來訪問這一行的其他元素地址,當j=0時,就是首元素地址,j=1時,就跳過一個元素,拿到第二個元素的地址,以此類推,找到了該行所有元素的地址。
然后是*(*(p+i)+j)),假設j已經確定,此時*(p+i)+j就是一個元素的地址,再對他進行解引用,找到該元素。
底層邏輯還是通過指針的偏移量去訪問每個元素。所以p[i][j]的寫法也是可行的。
所以根據二維數組傳參的本質-----傳遞首行這個一維數組的地址,我們找到了數組指針變量的應用場景。
四、函數指針變量
4.1 函數指針變量
通過類比,函數指針就是指向函數的指針,那么函數指針變量就是用來存放函數的地址。
對應數組arr來說,arr是數組首元素地址,而&arr代表是整個數組的地址,而對于函數來說,函數名是函數的地址,&函數名也是函數的地址。
既然函數指針變量是用來存放函數的地址的,所以未來也可以通過函數的地址去調用函數。
函數指針怎么創建?
int(*p)(int, int) = Add;
int(*p)(int x, int y) = &Add;
( )將*和p結合起來,說明這是一個指針,(int,int)說明這個指針指向一個函數,并且形參類型是int和int,開頭的int說明該函數的返回類型是int。
add和&add是一樣的,因為對于函數來說,函數名是地址,&函數名也是地址
同理*p和p也是一樣的,函數指針變量是可以不需要解引用。
形參的形參名可寫可不寫
int (*pf3) (int x, int y)| | ————————————————| | || | pf3指向函數的參數類型和個數交代| 函數指針變量名
pf3指向函數的返回類型int (*)(int x, int y)//pf3函數指針變量的類型
4.2 函數指針變量的使用
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;
}
5
8
注意:因為Add和&Add都是函數的地址,所以對于pf3來說,即使不解引用也是可以調用函數的,但如果解引用了,一定要記得用括號( )將*和pf3放在一起!!
4.3 函數指針變量的拓展
fun1(char* p, int (*)(char*));(*(void (*)())0)();void (*signal(int, void(*)(int)))(int);
分析這3個代碼
1.fun1的的第1個形參的類型是字符指針,第2個形參int(*)(char*),(*)代表這個形參是個指針,int和(char*)表名這是一個函數指針,形參類型為字符指針,返回值為整型。函數指針作為其他函數的形參時,其自身的函數名和形參名可以省略,僅保留數據類型即可。
2.多個括號要逐步拆解,void(*)( )說明這是一個void類型的函數指針,沒有形參,類型放在(),就是強制類型轉換,所以(void(*)( )0)的意思時將0這個整數值強制轉換成一個void(*)( )類型的函數指針,再進行解引用,得到的是函數指針的地址,結尾的( )就是調用0地址處的函數。所以上述代碼實際上是一個函數調用,將0轉化成一個void(*)( )類型的函數地址,再去調用0地址處的函數。
3.首先,*沒有和signal在一起,signal(int,void(*)(int))說明signal是一個函數名,該函數的形參有兩個類型,一個是int,一個是void(*)(int)類型的函數指針,剩下的部分就是該函數的返回類型,所以signal的返回類型是void(*)(int)類型的函數指針。上述代碼其實是一個函數聲明。
通過上述的擴展,我們復習到了
1.認識函數指針類型
2.強制類型轉換
3.通過函數指針調用函數的方式
4.函數的定義、聲明、調用
4.4 typedef關鍵字
typedef是用來類型重命名的,可以將復雜的類型簡單化
typedef unsigned int uint;
//將unsigned int 重命名為uinttypedef int* ptr_t;//整形指針
//int*重命名為ptr_ttypedef int(*parr_t)[5];//數組指針
//int(*5)重命名為parr_ttypedef void(*pfun_t)(int);//函數指針
//void(*)(int)重名名為pfun_tvoid (*signal(int, void(*)(int)))(int);//進行改寫
pfun_t signal(int, pfun_t);
關于typedef,常規寫法是 ?typedef 類型 重命名 ?,但是對于數組指針類型和函數指針類型稍有區別,重命名部分要寫在*的后面。
五、函數指針數組
? ? ? 數組是一個存放相同類型數據的存儲空間,所以函數指針數組存放的是具有相同返回類型和形參的函數指針。
? ? ?函數指針數組怎么創建呢?
int (*parr1[3])();
int* parr2[3]();
?如上圖代碼,其實是parr1,首先parr1先和[ ]結合,說明parr1是個數組,且有3個元素,存放的是int(*)()類型的函數指針。
? ?函數指針數組的應用場景,我們可以通過轉移表來理解。
六、轉移表的應用
函數指針數組,用數組取每個元素的方式去調用函數,就叫轉移表。
當我們想要對兩個數進行加減乘除運算操作時,以下是計算機的一般實現。
#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語句的相關代碼會變得非常冗長,且重復性很高,所以此時用函數指針數組,可以很好地解決這個問題。下面我們通過函數指針數組來實現。
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;int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //轉移表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);if ((input <= 4 && input >= 1)){printf("輸入操作數:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);printf("ret = %d\n", ret);}else if (input == 0)printf("退出計算器\n");elseprintf("輸入有誤\n");} while (input);return 0;
}
我們發現原本通過switch語句的選擇代碼,直接變成了函數指針數組的下標訪問,代碼簡潔清晰。
? ? ? 為什么可以使用函數指針數組?因為add、sub、mul、div這四個函數的形參以及返回類型是意義的,所以他們的函數指針類型也是一致的,根據數組只能存放相同數據類型的特點,所以這幾個函數可以被放在一個函數指針數組里,當放進函數指針數組時,我們就可以通過下標去訪問并調用對應的函數!