🤡博客主頁:醉竺
🥰本文專欄:《C語言深度解剖》《精通C指針》
😻歡迎關注:感謝大家的點贊評論+關注,祝您學有所成!
??💜💛想要學習更多C語言深度解剖點擊專欄鏈接查看💛💜???
目錄
1.函數指針
2.函數指針數組
3.指向函數指針數組的指針?
4.回調函數?
4.1 void* 的使用
4.2?使用回調函數,模擬實現qsort
4.3 使用qsort排序結構體?
5.指針和數組筆試題解析?
1.一維數組
2.字符數組
3. 字符串數組
4.字符串指針?
5.二維數組?
6.指針筆試題(難點)
7.對數組和指針的一些思考
1.函數指針
首先看一段代碼:?

輸出的是兩個地址,這兩個地址是 test 函數的地址。

對于函數加不加取地址符號” & “效果都一樣,調用函數加不加解引用符號” * “也都一樣。
那我們的函數的地址要想保存起來,怎么保存?(即函數指針的形式是怎樣的?)
下面我們看代碼:?

首先,能給存儲地址,就要求pfun1或者pfun2是指針,那哪個是指針?
答案是:?
pfun1可以存放。pfun1先和*結合,說明pfun1是指針,指針指向的是一個函數,指向的函數無參數,返回值類型為void。
閱讀兩段有趣的代碼:

分析上述代碼1和代碼2分別代表什么含義?
代碼1解釋:

代碼1是 一次函數調用
調用0地址處的一個函數
首先代碼中將0強制類型轉換成類型為?void (*) ( ) 的函數指針
然后去調用0地址處的函數
第一個” * “號,可無可有,上面已講過。?
代碼2解釋:

代碼2是 一次函數的聲明?
聲明的函數名字為 signal
signal函數的參數有2個,第一個參數是int型,第二個參數類型為函數指針型void (*) (int),該函數指針指向的類型是 返回值為void,其中一個參數是int型的函數
?signal函數的返回類型是一個函數指針,該函數指針指向的類型也是?返回值為void,其中一個參數是int型的函數
如果代碼按照下面這樣子寫,大家可能就更容易理解了,不過這種語法是錯誤的(其實很多復雜指針之所以難學,跟C語言語法風格的設計有很大關系,設計的不夠直觀):

代碼2太復雜,如何簡化:?
typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t);

2.函數指針數組
數組是一個存放相同類型數據的存儲空間,那我們已經學習了指針數組,比如:
int* arr[10];
//數組的每個元素是int*
如果一個數組中存放的都是函數的地址,那這個數組就叫函數指針數組,那函數指針數組如何定義呢??


例子:(計算器)
1.打印菜單及實現函數功能
void menu()
{printf("*********1.Add 2.Sub*********\n");printf("*********3.Mul 2.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;
}
方法1:分支循環實現計算器
int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戲!\n");break;case 1:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Add(x, y);printf("運算結果為:%d\n", ret);break;case 2:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Sub(x, y);printf("運算結果為:%d\n", ret);break;case 3:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Mul(x, y);printf("運算結果為:%d\n", ret);break;case 4:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Div(x, y);printf("運算結果為:%d\n", ret);break;default:printf("輸入不符合條件請重新輸入!\n");break;}} while (input);return 0;
}
方法2:函數指針數組的應用->轉移表,改善方式一的冗余?
int main()
{int ret = 0;int input = 0;int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; // 轉移表do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);if (input >= 1 && input <= 4){printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = pfArr[input](x, y);printf("運算結果為:%d\n", ret);}else if (input == 0){printf("退出游戲!\n");break;}else{printf("輸入不符合條件請重新輸入!\n");}} while (input);return 0;
}
方式3:回調函數->實現計算器(第4節會學習回調函數)
void Cal(int (*pfun)(int, int))
{int x = 0, y = 0;printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);int ret = pfun(x, y);printf("運算結果為:%d\n", ret);
}int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戲!\n");break;case 1:Cal(Add);break;case 2:Cal(Sub);break;case 3:Cal(Mul);break;case 4:Cal(Div);break;default:printf("輸入不符合條件請重新輸入!\n");break;}} while (input);return 0;
}
3.指向函數指針數組的指針?
指向函數指針數組的指針:首先是一個指針,該指針指向一個 數組,數組的元素都是 函數指針 ; 如何定義??
void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函數指針pfunvoid (*pfun)(const char*) = test;//函數指針的數組pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函數指針數組pfunArr的指針ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}
4.回調函數?
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進 行響應。?
首先演示一下qsort函數的使用:

4.1 void* 的使用
在C語言中,void*?是一種特殊的指針類型,它表示一個指向未知類型的指針。這種指針可以指向任何類型的數據,但它不知道所指向數據的具體類型。void*?主要用于以下幾種情況:?
-
函數指針參數或返回值:當函數需要處理多種類型的數據,但具體類型不確定時,可以使用?
void*?作為參數類型或返回類型。例如,標準庫函數?memcpy?就使用了?void*?作為參數。 -
動態內存分配:
malloc?函數返回?void*?類型的指針,因為它可以分配任何類型的內存。在使用時,通常需要將?void*?強制轉換為實際需要的類型。 -
類型無關的代碼:有時,我們希望編寫可以處理任何類型的代碼,例如通用數據結構或容器,這時可以使用?
void*?來實現類型無關性。
?示例1:作為函數參數
#include <stdio.h>void print_value(void* ptr) {// 強制類型轉換int value = *(int*)ptr;printf("%d\n", value);
}int main() {int x = 10;print_value((void*)&x);return 0;
}
在這個例子中,print_value?函數接受一個?void*?類型的參數,并在函數內部將其強制轉換為?int*?類型,然后打印出該指針指向的整數值。?
示例2:動態內存分配
#include <stdio.h>
#include <stdlib.h>int main() {// 動態分配一個整型大小的內存void* ptr = malloc(sizeof(int));// 強制類型轉換后使用*(int*)ptr = 20;printf("%d\n", *(int*)ptr);// 釋放內存free(ptr);return 0;
}
在這個例子中,我們使用?malloc?分配內存,它返回一個?void*?類型的指針。我們將其強制轉換為?int*?類型,以便能夠存儲一個整數值。
注意事項:
- 使用?
void*?時,必須確保類型轉換是正確的,否則可能會導致未定義行為。 void*?指針不能直接進行算術操作,例如自增或自減,因為編譯器不知道指針指向的數據類型大小。- 在使用?
void*?指針前,應確保它確實指向了正確的數據類型,否則在解除引用時可能會出現問題。?
4.2?使用回調函數,模擬實現qsort
(這里內部結構采用冒泡的方式)?
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{int i = 0;for (i = 0; i < size; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; j++){if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0){_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}printf("\n");return 0;
}
4.3 使用qsort排序結構體?

5.指針和數組筆試題解析?
注意一下代碼測試環境為visual stdio 2022 x86環境下(地址/指針 大小為4字節)?
1.一維數組


2.字符數組

下面一些代碼存在一些問題:

-
strlen(*arr):這里?*arr?是數組的第一個元素,是一個字符,不是字符串的地址,所以這是錯誤的。 -
strlen(arr[1]):這里?arr[1]?是數組的第二個元素,是一個字符,不是字符串的地址,所以這是錯誤的。 -
strlen(&arr):這里?&arr?是整個數組的地址,但是?strlen?需要一個以?\0?結尾的字符串的地址,所以這是錯誤的。 -
strlen(&arr + 1):這里?&arr + 1?是數組后面的內存地址,這并不是一個有效的字符串地址,所以這是錯誤的。 -
strlen(&arr[0] + 1):這里?&arr[0] + 1?是數組的第二個元素的地址,但是數組沒有以?\0?結尾,所以這也是錯誤的。
- 使用?
strlen?函數時,需要確保傳遞的參數是一個以?\0?結尾的字符串的地址。 strlen(*arr)、strlen(arr[1])、strlen(&arr)、strlen(&arr + 1)?和?strlen(&arr[0] + 1)?都是錯誤的,因為它們傳遞的參數不是字符串的地址。- 由于數組?
arr?沒有以?\0?結尾,所以?strlen(arr)?和?strlen(arr + 0)?也可能導致未定義行為。
為了避免這些問題,確保在使用?strlen?時傳遞的參數是一個以?\0?結尾的字符串的地址,并且在使用?sizeof?時理解你正在計算的是數組的大小還是指針的大小。
3. 字符串數組

下面一些代碼存在一些問題:?

-
strlen(*arr):這是錯誤的,*arr?是數組第一個元素,即字符?'a',而不是一個字符串的地址。因此,strlen(*arr)?會導致未定義行為,因為?strlen?預期的是一個字符串的地址。 -
strlen(arr[1]):這也是錯誤的,arr[1]?是數組第二個元素,即字符?'b',同樣不是一個字符串的地址。因此,strlen(arr[1])?也會導致未定義行為。 -
strlen(&arr):這是錯誤的,&arr?是整個數組的地址,strlen?會嘗試計算從該地址開始直到遇到 null 字符的字符數。但是,因為?&arr?指向的是整個數組,而不是字符串的起始位置,所以這可能會導致計算出一個錯誤的結果,或者在某些情況下導致未定義行為。 -
strlen(&arr + 1):這是錯誤的,&arr + 1?是數組后面的內存地址,它不指向任何有效的字符串。因此,strlen(&arr + 1)?會導致未定義行為。
4.字符串指針?

下面一些代碼存在一些問題:

-
strlen(*p):這是錯誤的,*p?是字符串的第一個字符,不是一個字符串的地址,所以這是未定義行為。 -
strlen(p[0]):這也是錯誤的,p[0]?是字符串的第一個字符,不是一個字符串的地址,所以這是未定義行為。 -
strlen(&p):這是錯誤的,&p?是指針?p?的地址,不是一個字符串的地址,所以這是未定義行為。 -
strlen(&p + 1):這也是錯誤的,&p + 1?是指針?p?后面的
5.二維數組?


sizeof(a[3]):這是錯誤的,因為?a?只有 3 行,a[3]?超出了數組的范圍,這將導致未定義行為。正確的做法是確保索引在數組的范圍內。
但是在Visua Stdio運行中,并沒有報錯,結果反而是16為什么呢?
- 在 C 語言中,
sizeof?運算符返回的是操作數的大小,以字節為單位。當你嘗試計算?sizeof(a[3])?時,實際上你在嘗試獲取數組的第四行(記住數組索引是從 0 開始的)的大小。然而,由于你的數組?a?只有 3 行,a[3]?實際上是一個越界的訪問。- 在 Visual Studio 中,當你嘗試訪問越界的數組行時,你可能會得到一個看似合理的值(比如 16 字節),這是因為?
sizeof?運算符不會實際訪問內存,它只是返回類型的大小。在這種情況下,a[3]?被當作一個指向?int[4](即一個有 4 個整數的數組)的指針,因此?sizeof(a[3])?返回的是?int[4]?的大小,即 4 個整數乘以每個整數的大小(通常在 32 位系統中是 4 字節,在 64 位系統中是 8 字節,取決于?int?的大小)。- 這就是為什么你得到了 16 字節的結果,因為它相當于?
4 * sizeof(int)。然而,這并不意味著?a[3]?是一個有效的數組行,它只是?sizeof?運算符根據?a[3]?的類型推斷出的結果。實際上,訪問?a[3]?是未定義行為,可能會導致程序崩潰或其他意外結果。
下面對二維數組的進行一些拓展:



總結:?

6.指針筆試題(難點)
筆試題1:

筆試題2:

?筆試題3

筆試題4:?

筆試題5 :

筆試題6:



筆試題7:


筆試題8:

7.對數組和指針的一些思考



?想繼續深入學習指針,可以訂閱下方”精通C指針“專欄?哦~
《精通C指針》
http://t.csdnimg.cn/gbpQp