🌈write in front :
🔍個人主頁 : @啊森要自信的主頁
??真正相信奇跡的家伙,本身和奇跡一樣了不起啊!
歡迎大家關注🔍點贊👍收藏??留言📝>希望看完我的文章對你有小小的幫助,如有錯誤,可以指出,讓我們一起探討學習交流,一起加油鴨。
文章目錄
- 前言
- 一、🚣 字符指針變量
- 二、?? 數組指針變量
- 2.1 數組指針變量是什么?
- 2.2 數組指針變量怎么初始化
- 三、???維數組傳參的本質
- 四、🚤函數指針變量
- 4.1 函數指針變量的創建
- 4.2 函數指針變量的使?
- 4.3 兩段有趣的代碼
- 4.3.1 typedef關鍵字
- 五、🚢函數指針數組
- ??總結
前言
通過對前面指針的學習,你可能對指針有了一些理解,比如,數字名的理解,然后怎么使用指針來訪問數組,二級指針,指針數組 …
有了這些的理解,本小節我們繼續深入理解指針,啊森將會帶你理解字符指針變量,數組指針變量,二維數組傳參的本質,函數指針變量和函數指針數組,通過這些學習,我們最后來通過這些知識來實現轉移表,話不多說,讓我們啟航!
一、🚣 字符指針變量
在C語言中,字符指針變量是一種指向字符型數據的指針變量。它可以用來指向一個字符數組的首地址,也可以用來指向一個字符型變量的地址。
指針類型為字符指針 char*
字符指針變量的聲明和初始化如下所示:
char str[] = "Hello"; // 聲明一個字符數組
char *ptr; // 聲明一個字符指針變量
ptr = str; // 將字符數組的首地址賦給字符指針變量
通過字符指針變量可以訪問和操作字符數組中的元素,也可以通過指針運算來訪問字符串中的字符。例如:
int main()
{char str[] = "Hello";char* ptr;ptr = str;printf("%c\n", *ptr); // 輸出字符數組的第一個字符printf("%c\n", *(ptr + 1)); // 輸出字符數組的第二個字符return 0;
}
當然,以上是把字符串放在字符數組里面,不過我們可不可以把字符串直接放在指針里面呢?當然可以!
int main()
{const char* pa = "hello,world";//這?是把?個字符串放到pa指針變量?了嗎?printf("%s\n", pa);return 0;
}
思考:這?是把?個字符串放到pa指針變量?了嗎?
當然不是!
–>代碼 const char* pa = "hello,world"
; 特別容易讓同學以為是把字符串 hello,world
放
到字符指針 pa
?了,但是本質是把字符串 hello,world
. ?字符的地址放到了pa
中。
這個是內存布局,"hello,world"是一個字符串常量,它的值存儲在內存中,而pa是一個指向這個字符串常量的指針,它的值是字符串常量的地址。所以pa存儲的值是104,也就是"hello,world"的第一個字符的ASCII碼值,以此我們就可以通過第一個字符串常量的地址遍歷后面的字符,順藤摸瓜的找到字符"\0"
才停止。
總結:代碼的意思是把?個常量字符串的?字符 h 的地址存放到指針變量 pa 中。
《劍指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;
}
這個代碼打印結果不一樣的原因是:
-
str1
和str2
是字符數組,它們分別用"hello bit."
初始化了兩個不同的數組。雖然內容相同,但它們占用的內存地址是不同的,所以str1 == str2 判斷為不相等,打印"str1 and str2 are not same"。 -
str3
和str4
是字符指針,它們都指向同一個字符串常量"hello bit."
。字符串常量存儲在只讀內存區域,無論如何定義,它們的地址是相同的。所以str3 == str4 判斷為相等,打印"str3 and str4 are same"。
換句話說:
- 字符數組比較地址,地址不同就不相等。
- 字符串常量比較內容,內容相同就相等。
所以結果不同的原因是str1、str2是數組,str3、str4是指針,它們的比較規則不同。
如果將str3和str4也定義為字符數組,則它們的比較結果也會是不相等,打印"str3 and str4 are not same"。
二、?? 數組指針變量
2.1 數組指針變量是什么?
之前我們學習了指針數組,指針數組是?種數組,數組中存放的是地址(指針)。
數組指針變量是指針變量?還是數組?
答案是:指針變量。
我們已經熟悉:
? 整形指針變量: int * pa;
存放的是整形變量的地址,能夠指向整形數據的指針。
? 浮點型指針變量: float * pf;
存放浮點型變量的地址,能夠指向浮點型數據的指針。
那數組指針變量應該是:存放的應該是數組的地址,能夠指向數組的指針變量。
下?代碼哪個是數組指針變量?
那數組指針變量應該是:存放的應該是數組的地址,能夠指向數組的指針變量。
下?代碼哪個是數組指針變量?
1. int *p1[10];2. int (*p2)[10];
思考?下:p1, p2分別是什么?
數組指針變量
數組指針變量
1 int (*p)[10];
解釋:p先和*結合,說明p是?個指針變量變量,然后指著指向的是?個??為10個整型的數組。所以
p是?個指針,指向?個數組,叫 數組指針。
這?要注意:[]的優先級要?于 * 號的,所以必須加上 () 來保證p先和 * 結合。
2.2 數組指針變量怎么初始化
數組指針變量是?來存放數組地址的,那怎么獲得數組的地址呢?就是我們之前學習的 &數組名
int arr[10] = {0};
&arr;//得到的就是數組的地址
如果要存放個數組的地址,就得存放在數組指針變量中,如下:
1 int(*p)[10] = &arr;
我們調試也能看到 &arr
和 p
的類型是完全?致的。
數組指針類型解析:
1. int (*p) [10] = &arr;2. | | |3. | | |4. | | p指向數組的元素個數5. | p是數組指針變量名6. p指向的數組的元素類型
三、???維數組傳參的本質
有了數組指針的理解,我們就能夠講?下?維數組傳參的本質了。
過去我們有?個?維數組的需要傳參給?個函數的時候,我們是這樣寫的:
#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;
}
C語言二維數組傳參的本質是:
二維數組在傳參時會自動退化為一維數組指針。
具體來說:
-
二維數組名代表整個二維數組,它其實就是一維數組指針,指向該數組的首行地址。
-
當二維數組作為參數傳遞給函數時,它會自動退化為一維數組指針。
-
函數內部無法得知傳入的參數原本是二維數組,它只能看到一個一維數組指針。
舉個例子:
void func(int arr[][3])
{// arr在這里實際類型是int (*arr)[3]
}int main()
{int a[4][3] = {0};func(a); // a傳參時自動退化為一維數組指針return 0;
}
這里a是二維數組,但傳給func函數時,func內部的arr參數實際上是一個指向int[3]類型一維數組的指針。
所以二維數組傳參的本質,就是自動退化為一維數組指針。
而數組指針變量,它也可以指向二維數組首行地址,從而實現對二維數組的操作。
例如:
int (*ptr)[3] = a; // ptr指向a二維數組的首行
所以二維數組傳參本質是退化為一維數組指針,而數組指針變量也可以指向二維數組,兩者聯系在一起,都可以看作是一維數組指針來操作二維數組。
那我們就來試試數組指針變量來遍歷數組吧---------->
?先我們再次理解?下?維數組,?維數組起始可以看做是每個元素是?維數組的數組,也就是?維數組的每個元素是?個?維數組。那么?維數組的?元素就是第??,是個?維數組。
如下圖:
根據數組名是數組首元素的地址這個規則,二維數組的數組名表示的就是第一行的地址,是一維數組的地址。
根據上面的例子,第一行的一維數組的類型就是 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;
}
總結:?維數組傳參,形參的部分可以寫成數組,也可以寫成指針形式。
四、🚤函數指針變量
4.1 函數指針變量的創建
類比一下:
數組指針變量應該是:存放的應該是數組的地址,能夠指向數組的指針變量。
C語言中的函數指針變量是指向函數的指針變量。
函數指針變量的定義格式是:
返回類型 (*變量名)(參數類型列表);
例如:
int (*ptr)(int, char); // ptr是一個指向返回類型為int,參數為int和char的函數的指針
函數指針變量和普通指針變量一樣,它也需要指向具體的函數地址才能調用該函數。
例如:
int func(int a, char b)
{return a + b;
}int main()
{int (*ptr)(int, char); // 函數指針變量聲明ptr = func; // 指向func函數ptr(10, 'a'); // 通過函數指針調用func函數return 0;
}
函數指針變量的主要特點:
- 可以指向不同類型的函數
- 通過它可以調用被指向的函數
- 可以作為函數參數或返回值進行傳遞
- 常用在回調函數機制中
函數指針變量是用來存放函數地址的,通過這個地址可以調用函數。函數確實有地址!
#include <stdio.h>void print()
{printf("lalala\n");
}
int main()
{printf("test: %p\n", print);printf("&test: %p\n", &print);return 0;
}
輸出:
確實打印出來了地址,所以函數是有地址的,函數名就是函數的地址,當然也可以通過 &函數名
的?式獲得函數的地址。
如果我們要將函數的地址存放起來,就得創建函數指針變量咯,函數指針變量的寫法其實和數組指針?常類似。如下:
void print()
{printf("lalala\n");
}
void (*pf1)() = &print;
void (*pf2)() = print;int Add(int x, int y)
{return x + y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y寫上或者省略都是可以的
1. int (*pf3) (int x, int y)2. | | ------------3. | | |4. | | pf3指向函數的參數類型和個數的交代5. | 函數指針變量名6. pf3指向函數的返回類型7. 8. int (*) (int x, int y) //pf3函數指針變量的類型
看到這里,你可能會發現數組指針變量和函數指針變量其實好像也是差別不大呀!
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)(1, 2));printf("%d\n", pf3(3, 4));return 0;
}
代碼輸出的結果是一樣的,為什么這兩種方式都可以呢?
解釋:
首先, pf3
是一個函數指針變量,它指向Add函數。
在C語言中,函數指針與一般指針運算方式是一致的。
也就是說,對函數指針進行解引用(*pf3)
后的結果,就是被指向的函數本身。
所以:
- (*pf3)(1, 2)
- 等價于 解引用pf3,得到Add函數,然后調用Add(1, 2)
- pf3(3, 4)
- 等價于 調用函數指針pf3,它指向的函數Add(3, 4)
兩者調用的函數都是Add,參數也一致,所以結果是相同的。
函數指針與一般指針在語法上表現形式不同,但本質上都是指向函數地址的指針。所以對函數指針進行解引用或直接調用效果是一致的。
因此上述代碼兩種打印方式結果相同,原因就是函數指針與普通指針在語法和語義上是一致的。
4.3 兩段有趣的代碼
注:兩段代碼均出?:《C陷阱和缺陷》這本書
代碼1
1 (*(void (*)())0)();
讓我們分析一下:
-
(void (*)())0
意思是把0這個整數值,強制類型轉換成一個地址,這個函數沒有參數,返回類型是void。 -
*操作符對它進行解引用,得到
void ()
類型的匿名函數。 -
對這個匿名函數進行調用
()
,也就是調用0地址處的地址。
所以整個表達式:
(void (*)())0
- 獲取函數指針,指向0地址
-
- 解引用函數指針,得到匿名函數
() - 調用匿名函數
- 解引用函數指針,得到匿名函數
換句話說,這個代碼是:
獲取一個指向0地址的函數指針,然后解引用它得到一個匿名函數,并對這個匿名函數進行調用。
由于指針指向0地址,實際調用的是內核NULL地址下的代碼。這通常會觸發異常或者崩潰。
所以這個代碼展示了一個通過函數指針調用匿名函數的語法,它實際上是在嘗試訪問空指針下的代碼從而觸發錯誤。
代碼2
1 void (*signal(int , void(*)(int)))(int);
精簡理解:
- 首先上述代碼是函數聲明
- signal是一個函數
- signal函數的參數有2個,第一個是int類型
- 第二個是函數指針類型,該指針指向的函數參數是int,返回類型是void
- signal函數的返回類型是這種類型的void(*)(int)函數指針
- 該指針指向的函數參數是int,返回類型是void
細節分析拓展如下:
此時注意signal并沒有與前面的*用括號結合 這個代碼定義了一個signal函數,它的功能是設置信號處理函數。
-
void(*)(int) 定義了一個函數指針,該函數指針指向一個返回void,接受一個int參數的函數。
-
signal是函數名,它有兩個參數:
-
int: 表示信號編號
-
void(*)(int): 函數指針,表示要設置的信號處理函數
-
-
signal函數的返回值是一個函數指針:void (*)(int)
-
這個返回值函數指針也指向一個返回void,接受一個int參數的函數。
所以整個函數聲明可以解釋為:
signal函數用于設置信號處理函數。它接收兩個參數:
- 信號編號
- 要設置的信號處理函數
signal函數返回原來的信號處理函數。
所以這個函數聲明定義了一個典型的設置信號處理函數的接口 - signal(),它可以用來設置和獲取信號的處理回調函數。
**總結來說:**這個函數聲明使用了嵌套的函數指針定義了signal函數的接口格式,目的是為了設置和獲取信號處理回調函數。
4.3.1 typedef關鍵字
當你看到了這里,你可能在想,這么長void (*signal(int , void(*)(int)))(int);
的代碼,寫出來真麻煩,有沒有辦法可以簡化他的長度呢,看起來可觀,容易理解呢?可不能自己寫著寫著把自己轉忽悠了哈哈哈。
當然!
typedef 是?來類型重命名的,可以將復雜的類型,簡單化。
C語言中的typedef主要用于定義類型別名。
typedef語法:
typedef 舊類型名 新類型名;
例如:
typedef int Int;
這行代碼定義Int
為int
類型的別名。
typedef的主要用途:
- 為復雜類型定義簡短的名稱
比如定義指針、函數指針等:
typedef int (*FuncPtr)(int);
- 隱藏實現細節
比如用結構體指針替換結構體:
typedef struct {int x;int y;
} Point;typedef Point* PointPtr;
- 向下兼容
如果需要修改類型定義,可以使用typedef
避免修改大量代碼。
- 提高代碼可讀性
給類型起個有意義的名稱,比如用Person替換struct Person。
- 標準庫也廣泛使用
typedef
如size_t
、ptrdiff_t
等標準類型都是通過typedef
定義的。
所以總體來說,typedef主要用于為類型起別名,簡化和隱藏類型,提高代碼可讀性和兼容性。它廣泛應用于C標準庫和程序開發中。
本小節由于篇幅有限,我們先講第一點:
?如我們有數組指針類型 int(*)[5]
,需要重命名為 parr_t
,那可以這樣寫:
1 typedef int(*parr_t)[5]; //新的類型名必須在*的右邊
函數指針類型的重命名也是?樣的,?如,將 void(*)(int)
類型重命名為 pf_t
,就可以這樣寫:
1 typedef void(*pfun_t)(int);//新的類型名必須在*的右邊
那么要簡化代碼2,可以這樣寫:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
五、🚢函數指針數組
數組是?個存放相同類型數據的存儲空間,我們已經學習了指針數組,
?如:
int *arr[10];
//數組的每個元素是int*
那要把函數的地址存到?個數組中,那這個數組就叫函數指針數組,那函數指針的數組如何定義呢?
這三個捏個才是正確的呢?int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
parr1 先和 [] 結合,說明 parr1是數組,數組的內容是什么呢?
是 int (*)() 類型的函數指針。
這個定義相當于:
- 定義一個函數指針數組parr1
- 數組長度為3
- 每個元素都是一個函數指針
- 指向一個返回int,無參數的函數
這里給出一個C語言函數指針數組的簡單實現示例:
// 定義函數原型
int func1(void);
int func2(void);int func1(void)
{printf("func1 called\n");return 0;
}int func2(void)
{printf("func2 called\n");return 0;
}int main()
{// 定義函數指針數組,可以存儲2個函數指針int (*funcPtrArr[2])(void);// 初始化函數指針數組元素funcPtrArr[0] = func1;funcPtrArr[1] = func2;// 通過索引調用函數指針數組元素指向的函數funcPtrArr[0]();funcPtrArr[1]();return 0;
}
輸出結果為:
主要實現步驟:
- 定義函數原型
- 定義函數指針數組
- 初始化數組元素,使每個元素指向對應的函數
- 通過數組索引,調用函數指針指向的函數
這個示例演示了如何定義和使用函數指針數組來管理和調用多個函數。
實際應用中,可以通過函數指針數組實現回調函數、插件等機制。函數也可以作為參數傳遞給其他函數。
總之,函數指針數組提供了一種靈活高效的方式來管理和調用多個函數在C語言中。怎么高效?下一屆我們做一個計算器,轉移表就可以清楚理解他的巧妙之處!
??總結
一、字符指針變量
字符指針變量用來存儲字符串,可以通過字符指針訪問字符串中的每個字符。
二、數組指針變量
2.1 數組指針變量實際指向數組第一個元素的地址。
2.2 可以通過數組名直接初始化數組指針,也可以通過地址運算符&初始化。
三、二維數組傳參的本質
二維數組傳參實際上是傳一級指針,等同于傳數組指針。
四、函數指針變量
4.1 通過函數原型聲明函數指針變量類型,并使用地址運算符&初始化。
4.2 通過函數指針調用函數,等同于使用普通函數名調用。
4.3 typedef可以簡化函數指針變量類型定義。
五、函數指針數組
函數指針數組可以存儲和管理多個函數指針,通過數組索引調用不同函數。
總之,C語言指針變量提供了一種靈活的方式來操作和管理數據,如字符串、數組、函數等。指針變量的概念和使用需要熟練掌握,它是C語言的重要知識點。感謝你的收看,如果文章有錯誤,可以指出,我不勝感激,讓我們一起學習交流,如果文章可以給你一個幫助,可以給博主點一個小小的贊😘