文章目錄
- 一、指針數組
- 指針數組模擬二維數組
- 二、數組指針
- 二維數組傳參的本質
- 三、字符指針變量
- 四、函數指針變量
- 4.1. 函數指針的應用
- 4.2 兩端有趣的代碼
- 4.3. typedef關鍵字
- 4.3.1 typedef 的使用
- 4.3.2. typedef與#define對比
- 五、函數指針數組
- 函數指針數組的應用
一、指針數組
指針數組到底是指針還是數組呢?
整型數組 int arr[] : 存放整型的數組;
字符數組 char arr[] : 存放字符的數組;
可以類比得到:
指針數組 int* arr[] : 存放指針的數組.
因此,指針數組本質上是數組,數組中的每個元素都存放的是地址(指針),每個元素的類型為指針類型.
示例:下方代碼中
parr先和 [ ] 結合,因此它是一個數組,這個數組有3個元素,每個元素的類型為int*
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 1,2,3,4,5 };
int arr3[5] = { 1,2,3,4,5 };
int* parr[3] = {arr1,arr2,arr3};
將數組名去掉就可以看出它的類型,即int* [3]
,也就是指針數組
指針數組模擬二維數組
int main()
{//創建三個整型數組int arr1[5] = { 1,2,3,4,5 };int arr2[5] = { 1,2,3,4,5 };int arr3[5] = { 1,2,3,4,5 };//由于數組名就是數組首元素地址,將每個數組名存放到指針數組parr中int* parr[3] = {arr1,arr2,arr3};int i = 0;for (i = 0;i < 3;i++)//遍歷三個數組名{int j = 0;for (j = 0;j < 5;j++)//遍歷每個數組{printf("%d ", parr[i][j]);}printf("\n");}return 0;
}
對 parr [ i ] [ j ] 進行解釋:
- parr [ i ] 訪問的是parr中的元素,找到的是第 i 個數組名,指向一維整形數組.
比如 parr [ 0 ]找到的是 arr1, parr [ 1 ]找到的是 arr2, parr [ 2 ]找到的是 arr3, 因此 parr [ 0 ] 就是arr1數組名,本質就是第一個數組首元素地址- parr [ i ] [ j ] 指向一維數組中的元素,找到的是第 i 個數組的第 j 個元素.
比如parr [ 0 ] [ 0 ] == arr1 [ 0 ] == 1- arr1, arr2, arr3本質上是指針,因此可以存到指針數組 parr 中.
- parr模擬的二維數組并非是真正的二維數組,因為二維數組在內存中是連續存放的.
二、數組指針
數組指針到底是數組還是指針呢?
整型指針
int* p = &a
: p 是存放整型變量地址、指向整型變量的指針;
字符指針char* pc = 'c'
: pc 是存放字符變量地址、指向字符變量的指針;
浮點型指針float* pf = &f
: pf 是存放浮點型變量地址、指向浮點型變量的指針;
數組指針:int (*p) [ 5 ] = &arr
: p 是指向整型數組的指針.
因此,數組指針本質上是指針,是指向數組的指針,存放的是數組的地址
示例:
int arr[] = { 1,2,3 };
int (*p)[3] = &arr;
- p先與*結合,說明p是一個指針變量,然后與 [ ] 結合,說明它指向的是一個數組,數組中有三個元素,每個元素的類型為
int
- 數組指針的初始化:數組指針存的是數組的地址,因此將整個數組取地址賦值給數組指針就可以
指針數組與數組指針的區分:
指針數組是存放指針的數組,“指針”是定語,修飾“數組”
數組指針是指向數組的指針,“數組”是定語,修飾“指針”
二維數組傳參的本質
int arr[3][5] = { { 1,2,3,4,5 }, {2,3,4,5,6},{3,4,5,6,7 } }
arr[3][5]可以看作是一個有三個元素的一維數組,每個元素是一個一維數組,第一個元素是{1,2,3,4,5},第二個元素是{2,3,4,5,6},第三個元素是{3,4,5,6,7},因此,遵循數組名是數組首元素地址的原則,那么認為二維數組的數組名其實就是第一行數組的地址,數組的地址類型為數組指針,那么二維數組傳參的本質傳遞的是第一行數組的地址,需要用數組指針類型來接收
用數組指針來打印二維數組
void print(int (*p)[5])
{int i = 0;for (i = 0;i < 3;i++){for (int j = 0;j < 5;j++){printf("%d ", *(*(p + i) + j));//也可以寫成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 } };print(arr);return 0;
}
p 是數組指針,
p + i
得到的是第 i 行元素的地址,*( p + i )
得到的是第 i 行元素的數組名,*( p + i ) + j
得到的是第 i 行第 j 個元素的地址,再進行解引用就得到該元素
有人會問,p + i
是地址,*( p + i )
得到的應該是數據,*( p + i ) + j
得到的還是數字,*(*( p + i ) + j)
就是對數字進行解引用,沒有意義了.
實際上 p + i 是二維數組中第 i 行數組的地址,那么 p + 0 == p == arr [ 0 ], 以第一行數組為例,第一行數組是一維數組,arr [ 0 ] [ j ] 得到的是第一行數組第 j 個元素,那么arr [ 0 ]本質上就是第一行數組的數組名,也就是第一行數組首元素 1 的地址. 因此
*(p + i)
實際得到的是第 i 行的數組名,本質還是地址,想得到 i 行 j 列 這個元素需要*(*(p + i) + j).
二維數組在想象中是以行和列形式存在的,但是在內存中的存儲是連續的, 因此只需要知道首元素地址就可以連續訪問
當然,二維數組傳參,形參部分也可以寫成二維數組(不能省略列),也可以寫成數組指針形式.
三、字符指針變量
char* a = "abcdef";
其中,a是一個字符指針,存放的是字符串第一個字符的地址,并不是整個字符串的地址,并且該字符串為常量字符串,不能被改變,因此嚴謹來說應該在前加上const
const char* a = "abcdef";
示例:
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");
- str3 和 str4 是兩個相同的常量字符串, 在內存中只會開辟一塊內存空間,不同的指針指向相同的常量字符串的時候,指向的是同一塊內存空間,因此 str3 和 str4 是相同的
- 用相同的常量字符串去初始化兩個字符數組,內存會為兩個數組單獨開辟兩個內存空間,因此str1和str2是不同的,他們分別指向兩個不同的地址
四、函數指針變量
函數指針:指向函數的指針,存放的是函數的地址.
首先了解什么是函數的地址:
void test()
{printf("666\n");
}
int main()
{test();printf("test = %p\n", test);printf("&test = %p\n", &test);return 0;
}
可以看出,函數名 test 和 &test 的地址是一樣的,因此函數名就表示函數的地址,想存儲函數的地址,就需要函數指針變量
函數指針的初始化:
int Add(int x, int y)
{return x + y;
}
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = &Add;
Add是一個函數,pf1 (pf2) 先與*結合,表明其是一個指針,然后與后面的( )結合,表明其指向的是一個函數,函數內部的參數有兩個,分別都是 int 類型,函數的返回類型是 int 型.
去掉變量名pf1,就可以看出他的類型是 int ( * ) ( int , int )
函數指針類型
4.1. 函數指針的應用
用函數指針實現兩個數相加:
可以看出,上述三種方法都可以實現Add函數的調用
- 直接用函數名調用函數.
- 利用函數指針 pf 取得Add函數的地址,然后對其解引用*pf得到函數名,再進行調用.
- 直接用函數指針pf進行調用.
總結:函數名Add實際就是函數的地址,函數指針pf也是函數的地址,函數名直接調用函數就相當于用地址調用函數,那么直接用pf調用,不解引用也是可以的
比如:
實際上可以看出:
005513CF為函數Add的地址
函數指針pf,*pf得到的都是005513CF,即函數Add的地址,因此利用函數指針調用函數的時候可以不用解引用,直接進行調用
4.2 兩端有趣的代碼
1.第一段代碼
(*(void (*)())0)();
從內部向外看
- 黃色部分
void ( * ) ( )
,表明類型為函數指針類型- 藍色括號 ( ) 0,表示將整型 0 強制類型轉換成函數指針類型,得到的是一個地址 00000000,意味著我們假設0地址處放著無參,返回類型為void的函數
- 綠色括號內部是對其進行解引用,得到的是該地址處的函數名
- 右側黑色括號表示調用這個函數
因此調用的是0地址處的函數,函數的返回類型為void,函數參數為無參
- 第二段代碼
void (*signal(int, void(*)(int)))(int);
signal,先與后面接的藍色括號結合,表明它指向一個函數,說明它是一個函數名,該函數內部有兩個參數,類型分別是 整型int 和 函數指針類型
void(*)(int)
,然后將函數名和函數參數去掉可以得到:
函數還缺一個返回類型,因此該函數的返回類型為函數指針類型
將這段代碼改寫以下就可以更清楚看到:
- signal 是函數名,指向的函數內部有兩個參數,參數類型分別是整型和函數指針類型,該函數的返回類型為函數指針類型
- 這種寫法只是便于分析,在編譯器中這種寫法是不允許的,只能寫成第一種形式
4.3. typedef關鍵字
4.3.1 typedef 的使用
如果類型寫起來太復雜,我們可以將其重新定義,進行簡化,比如
將unsigned int 重命名為uint
typedef unsigned int uint;
對于指針數組來說,正確的重命名為 parr_t 的方式是,parr_t 必須在 * 的右邊
typedef int(*parr_t)[5]
同樣對于函數指針,將其重命名為pfun_t,必須放在*的右邊
typedef void(*pfun_t)(int,void(*)())//新的類型名必須放在*的右邊
pfun_t signal(int,pfun_t);//對代碼進行簡化
4.3.2. typedef與#define對比
typedef int* ptr_t;
ptr_t p1,p2;
將
int*
類型重命名為ptr_t ,p1, p2
均為指針變量
#define PTR_t int*
PTR_t p1,p2;
將PTR_t 定義為int* 類型,那么p1為指針變量,p2為整型變量 ,因為替換之后為:
int* p1, p2
,是一個逗號表達式,*只會與p1結合
五、函數指針數組
函數指針數組:存放函數指針的數組,本質是一個數組,用來存放函數指針變量.
函數指針數組的定義:
int (* parr [ 3 ] ) ( );
parr先與[ ]結合,表明它是一個數組,該數組有3個元素,每個元素的類型為函數指針類型
int ( * ) ( )
函數指針數組的應用
轉移表的實現:
實現一個具有加減乘除的計算器:
void menu()
{printf("1.add 2. sub \n");printf("3.mul 4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;do {menu();printf("請選擇:");scanf_s("%d", &input);switch (input){case 1:printf("請輸入兩個參數:");scanf_s("%d %d", &a, &b);ret = Add(a, b);printf("%d \n", ret);break;case 2:printf("請輸入兩個參數:");scanf_s("%d %d", &a, &b);ret = Sub(a, b);printf("%d \n", ret);break;case 3:printf("請輸入兩個參數:");scanf_s("%d %d", &a, &b);ret = Mul(a, b);printf("%d \n", ret);case 4:printf("請輸入兩個參數:");scanf_s("%d %d", &a, &b);ret = Div(a, b);printf("%d \n", ret);break;case 0:printf("退出\n");break;default:printf("請重新輸入:");break;}} while (input);return 0;
}
通過選擇1、2、3、4實現計算器的運算,但是這種方法有一個缺點,每一個case下面幾乎都是相同的功能,唯獨不同的就是進行函數的調用,因此我們應該想辦法將這一重復的功能封裝成一個函數去實現
利用函數指針數組實現計算器:
void menu()
{printf("1.add 2. sub \n");printf("3.mul 4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;int (*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };do {menu();printf("請選擇:");scanf_s("%d", &input);if (input <= 4 && input >= 1){printf("請輸入操作數:");scanf_s("%d %d", &a, &b);ret = pf[input](a,b);printf("%d\n", ret);}else if(input==0){printf("退出\n");}else{printf("重新輸入");}} while (input);return 0;
}
利用函數指針實現轉移表:
將重復的功能封裝到calc函數中實現,通過函數指針調用加減乘除函數
main是主調函數,calc函數是回調函數
void menu()
{printf("1.add 2. sub \n");printf("3.mul 4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}
void calc(int (*pf)(int, int))
{int a, b;printf("請輸入兩個參數:");scanf_s("%d %d", &a, &b);int ret = pf(a, b);printf("%d \n", ret);
}
int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;do {menu();printf("請選擇:");scanf_s("%d", &input);switch (input){case 1:calc(Add);break;case 2:calc(Sub);break;case 3:calc(Mul);case 4:calc(Div);break;case 0:printf("退出\n");break;default:printf("請重新輸入:");break;}} while (input);return 0;
}