文章目錄
- 一、回調函數:通過函數指針實現靈活調用
- 1.1 什么是回調函數?
- 1.2 回調函數的實際應用:簡化計算器代碼
- 二、qsort函數
- 2.1 qsort函數的參數說明
- 2.2 使用qsort排序整型數據
- 2.3 使用qsort排序結構體數據
- 示例:學生信息排序
- 2.4 qsort函數的模擬實現:用冒泡排序理解通用排序
- 2.4.1 模擬實現思路
- 1. `main()`主程序:
- 2. `bubble_sort()`
- 3.`swap():`
- 2.4.2 模擬實現代碼
- 三、`sizeof`與`strlen`
- 3.1 sizeof:計算內存大小
- 3.2 strlen:計算字符串長度
- 3.3 核心區別對比
- 四、數組與指針筆試題解析
- 4.1 一維數組:數組名
- 4.2 字符數組:`\0`
- 代碼1:無`\0`的字符數組(`sizeof`解析)
- 代碼2:無`\0`的字符數組(`strlen`解析)
- 代碼3:含`\0`的字符數組(字符串)
- 代碼4:字符指針指向常量字符串
- 4.3 二維數組:行地址與元素地址的嵌套
- 4.4 指針運算
- 題目1:數組地址的跨越
- 題目2:結構體指針的算術運算
- 題目3:逗號表達式與數組初始化
- 題目4:二維數組與指針偏移差異
- 題目5:數組地址與元素地址的轉換
- 題目6:指針數組的偏移
- 題目7:三級指針的嵌套訪問
一、回調函數:通過函數指針實現靈活調用
1.1 什么是回調函數?
回調函數是一種特殊的函數調用方式:當一個函數的指針(地址)作為參數傳遞給另一個函數,并且這個指針在被調用時指向的函數被執行,那么這個被執行的函數就稱為回調函數。
簡單來說,回調函數不是由函數本身直接調用,而是在特定條件下由其他函數通過指針觸發,用于響應特定事件或完成特定邏輯。
1.2 回調函數的實際應用:簡化計算器代碼
以計算器程序為例,傳統實現中,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;do{printf("******************");printf(" ******0:退出 *****");printf(" ******1:add *****");printf(" ******2:sub *****");printf(" ******3:mul *****");printf(" ******4:div *****");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;}} while (input != 0);return 0;
}
我們發現每個case
下都有大量的重復語句,可以把這些語句封裝成函數calc
,提高代碼的可利用性。但是每個case
下調用的函數不一樣,不能直接用一個函數籠統表示。這就需要利用函數指針,把需要調用的函數作為參數傳給calc
:
改造后(回調函數版):
// 定義通用計算函數,接收函數指針作為參數
void calc(int(*pf)(int, int)) {int x, y, ret;printf("輸入操作數:");scanf("%d %d", &x, &y);ret = pf(x, y); // 通過函數指針調用回調函數printf("ret = %d\n", ret);
}// 主函數中直接通過函數名調用
case 1: calc(add); break;
case 2: calc(sub); break;
case 2: calc(mul); break;
case 2: calc(div); break;
優勢:將重復的輸入輸出邏輯封裝到calc
函數中,通過傳遞不同的計算函數指針(add
/sub
等)實現靈活調用,減少代碼冗余,提高可維護性。
二、qsort函數
qsort
是C語言標準庫中的快速排序函數,支持對任意類型的數據進行排序(整型、結構體等),其核心是通過回調函數定義排序規則。
2.1 qsort函數的參數說明
void qsort(void* base, // 待排序數組的起始地址size_t nmemb, // 數組元素個數size_t size, // 每個元素的大小(單位:字節)int (*compar)(const void*, const void*) // 比較函數(回調函數)
);
- 比較函數:需用戶自定義,返回值規則:
- 若
p1 > p2
,返回正數; - 若
p1 == p2
,返回0; - 若
p1 < p2
,返回負數。
- 若
注意:
- 使用
qsort()
要先包含頭文件#include<stdlib.h>
void*
是無具體類型的指針,不可以直接解引用,也不可以進行±整數的運算,必須先強制類型轉換。
2.2 使用qsort排序整型數據
#include <stdio.h>
#include <stdlib.h> // qsort所在頭文件int int_cmp(const void* p1, const void* p2) {// 將void*轉換為int*,再解引用獲取值return *(int*)p1 - *(int*)p2;
}int main() {int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};int n = sizeof(arr) / sizeof(arr[0]);qsort(arr, n, sizeof(int), int_cmp);for (int i = 0; i < n; i++) {printf("%d ", arr[i]); // 輸出:0 1 2 3 4 5 6 7 8 9}return 0;
}
2.3 使用qsort排序結構體數據
結構體包含多種類型成員(如姓名、年齡),可通過不同的比較函數實現按不同字段排序。
示例:學生信息排序
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strcmp所在頭文件struct stu
{char name[20];int age;
};int cmp_stu_by_name(const void* p1, const void* p2)
{return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
}int cmp_stu_by_age(const void* p1, const void* p2)
{return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}int main()
{struct stu arr[3] = { {"rare",20},{"daisy",18}, {"sivan",22} };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);return 0;
}
注意:
- 比較字符串大小可以用
strcmp()
函數,其所在的頭文件是string.h
strcmp()
是按照對應字符的ASCII碼值比較的,不是比較長度。例如:"abq">"abcd"
- 結構體中指針的訪問要用
->
2.4 qsort函數的模擬實現:用冒泡排序理解通用排序
qsort
的核心是通用性(支持任意類型),其底層可基于冒泡、快速排序等算法實現。以下用冒泡排序模擬qsort
的邏輯。
2.4.1 模擬實現思路
1. main()
主程序:
- 創建一個無序數組
- 計算數組元素個數
sz
- 調用
bubble_sort()
函數
2. bubble_sort()
qsort()
四個參數:- 首個地址
void* base
- 元素個數
size_t sz
- 每個元素大小
size_t width
- 自定義比較函數
int(*cmp)(const void* p1 , const void* p2)
- 首個地址
- 調用
cmp
比較大小:- 將首個元素地址轉為
char*
類型,因為char
大小為1字節,方便不同類型傳入,進行比較。 - 這里相當于“指針 ± 整數”,指針類型是
char*
,所以 ± 整數都是跳過整數個字節,j*width
就是跳過一個元素的長度。因此這里傳入的是第j
和j+1
個元素進行比較。
- 將首個元素地址轉為
- 調用
swap
: 升序排序,cmp()>0
則交換。這里原理同上,傳入第j
和j+1
個元素的地址。
3.swap():
- 逐字節進行交換,循環次數就是
width
,每個元素的大小。
2.4.2 模擬實現代碼
#include <stdio.h>
int cmp_int(const void* p1, const void* p2)
{return *((int*)p1) - *((int*)p2);
}void swap(char* buf1, char* buf2, size_t width)
{for (int i = 0; i < width; i++){char temp = *buf1;*buf1 = *buf2;*buf2 = temp;buf1++;buf2++;//逐字節交換}
}void bubble_sort(void* base,size_t sz,size_t width,int(*cmp)(const void* p1 ,const void* p2))
{for (int i = 0; i < sz - 1; i++){for (int j = 0;j < sz - 1 - i;j++){if ( cmp( (char*)base + j * width,(char*)base + ( j + 1 )*width )> 0){swap((char*)base + j * width, (char*)base + (j + 1) * width,width);}}}
}int main()
{int arr[10] = { 1,2,7,6,9,8,10,6,4,9 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);for (size_t i = 0; i < 10; i++){printf("%d ", arr[i]);}return 0;
}
三、sizeof
與strlen
3.1 sizeof:計算內存大小
sizeof
是C語言的操作符,其核心功能是計算變量或類型所占用的內存空間大小(單位:字節)。它的特點是:
- 只關注內存占用,不關心內存中存儲的數據;
- 操作數可以是變量,也可以是類型(如
sizeof(int)
); - 對于數組名,
sizeof(數組名)
計算的是整個數組的大小(其他情況數組名通常表示首元素地址)。
示例代碼:
#include <stdio.h>
int main() {int a = 10;printf("%zd\n", sizeof(a)); // 結果:4(int類型占4字節)printf("%zd\n", sizeof a); // 結果:4(變量名可省略括號)printf("%zd\n", sizeof(int)); // 結果:4(int類型的大小)return 0;
}
3.2 strlen:計算字符串長度
strlen
是C語言標準庫函數(聲明于<string.h>
),功能是計算字符串的長度,其核心邏輯是:
- 從傳入的地址開始,逐個字符計數,直到遇到
\0
停止(\0
不計入長度); - 若字符串中沒有
\0
,會越界查找,導致結果未定義(UB)。
示例代碼:
#include <stdio.h>
#include <string.h>
int main() {char arr1[3] = {'a', 'b', 'c'}; // 無\0char arr2[] = "abc"; // 隱含\0printf("%d\n", strlen(arr1)); // 結果:隨機值(越界查找)printf("%d\n", strlen(arr2)); // 結果:3(遇到\0停止)return 0;
}
3.3 核心區別對比
特性 | sizeof | strlen |
---|---|---|
本質 | 操作符(編譯期計算) | 庫函數(運行期計算) |
功能 | 計算內存空間大小(字節) | 計算字符串長度(\0 前字符數) |
關注內容 | 僅關心內存占用,與數據無關 | 依賴\0 結束符,無\0 則越界 |
參數類型 | 變量、類型、表達式等 | 僅接受char* (字符串首地址) |
返回值 | size_t (無符號整數) | size_t (無符號整數) |
sizeof()
不會計算括號內的表達式,而strlen()
會計算。
int main()
{int a = 0;short b = 4;printf("%d\n",sizeof(b = b + a));//輸出 2 ,取決于 b的類型printf("%d\n",b);//輸出4return 0;
}
四、數組與指針筆試題解析
4.1 一維數組:數組名
代碼示例:
int a[] = {1,2,3,4};
printf("%d\n", sizeof(a)); // 16
printf("%d\n", sizeof(a+0)); // 4/8(首元素地址,指針大小)
printf("%d\n", sizeof(*a)); // 4(首元素是int,占4字節)
printf("%d\n", sizeof(a+1)); // 4/8(第二個元素地址,指針大小)
printf("%d\n", sizeof(a[1])); // 4
printf("%d\n", sizeof(&a)); // 4/8(數組地址,指針大小)
printf("%d\n", sizeof(*&a)); // 16(*&a等價于a,整個數組大小)
printf("%d\n", sizeof(&a+1)); // 4/8(跳過整個數組的地址,指針大小)
printf("%d\n", sizeof(&a[0])); // 4/8(首元素地址,指針大小)
printf("%d\n", sizeof(&a[0]+1)); // 4/8(第二個元素地址,指針大小)
關鍵結論:
- 數組名
a
在sizeof(a)
和&a
中表示整個數組,其他情況均表示首元素地址。 - 指針運算(如
a+1
)的結果仍是指針,大小為4/8字節。
4.2 字符數組:\0
字符數組的核心陷阱在于是否包含\0
,這直接影響strlen
的結果。
代碼1:無\0
的字符數組(sizeof
解析)
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr)); // 6(6個char,每個1字節)
printf("%d\n", sizeof(arr+0)); // 4/8
printf("%d\n", sizeof(*arr)); // 1(首元素是char)
printf("%d\n", sizeof(&arr)); // 4/8
printf("%d\n", sizeof(&arr+1)); // 4/8
代碼2:無\0
的字符數組(strlen
解析)
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr)); // 結果:隨機值(無\0,越界查找)
printf("%d\n", strlen(arr+0)); // 結果:隨機值(同arr,首元素地址)
printf("%d\n", strlen(*arr));
// 錯誤(*arr是'a',ASCII值97,相當于把 97作為地址傳給strlen,視為地址越界)
代碼3:含\0
的字符數組(字符串)
char arr[] = "abcdef"; // 隱含'\0',共7個元素
printf("%d\n", sizeof(arr)); // 結果:7(包含'\0')
printf("%d\n", strlen(arr)); // 結果:6('\0'前共6個字符)
代碼4:字符指針指向常量字符串
char *p = "abcdef"; // p存儲字符串首地址
printf("%d\n", sizeof(p)); // 結果:4/8(指針大小)
printf("%d\n", strlen(p)); // 結果:6(字符串長度)
printf("%d\n", strlen(p+1)); // 結果:5(從'b'開始計數)
4.3 二維數組:行地址與元素地址的嵌套
代碼示例:
int a[3][4] = {0}; // 3行4列的二維數組
printf("%d\n", sizeof(a)); // 結果:48(3×4×4字節,整個數組大小)
printf("%d\n", sizeof(a[0])); // 結果:16(第0行數組大小:4×4字節)
printf("%d\n", sizeof(a[0]+1)); // 結果:4/8(第0行第1列元素地址)
printf("%d\n", sizeof(a+1)); // 結果:4/8(第1行的地址,行指針)
printf("%d\n", sizeof(*(a+1))); // 結果:16(第1行數組大小)
printf("%d\n", sizeof(*a)); // 結果:16(第0行數組大小,*a等價于a[0])
核心邏輯:
- 二維數組
a
可視為“數組的數組”,a[i]
是第i
行的一維數組名。 a
表示首行地址(行指針,類型為int(*)[4]
),a+1
指向第1行。
4.4 指針運算
題目1:數組地址的跨越
int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a + 1); // &a是數組地址,+1跳過整個數組
printf("%d,%d", *(a+1), *(ptr-1)); // 結果:2,5
a+1
指向第1個元素(值2),ptr-1
指向數組最后一個元素(值5)。
題目2:結構體指針的算術運算
假設結構體大小為20字節
struct Test {int Num; char *pcName; short sDate;char cha[2]; short sBa[4];
};
struct Test *p = (struct Test*)0x100000; printf("%p\n", p + 0x1); // 結果:0x100014(+20字節,指針按結構體大小偏移)
printf("%p\n", (unsigned long)p + 0x1); // 結果:0x100001(整數+1)
printf("%p\n", (unsigned int*)p + 0x1); // 結果:0x100004(+4字節,按int*偏移)
題目3:逗號表達式與數組初始化
int a[3][2] = { (0,1), (2,3), (4,5) }; // 逗號表達式取右值,等價于{1,3,5}
int *p = a[0]; // p指向第0行第0列
printf("%d", p[0]); // 結果:1
注意:
- 二維數組初始化要用
{{},{},{}}
!!! - 第一行相當于三個逗號表達式(依次執行每個表達式,結果為最后一個表達式的值),所以這里只初始化了三個元素,分別是1,3,5.
1 | 3 |
---|---|
5 | 0 |
0 | 0 |
題目4:二維數組與指針偏移差異
x86環境下:
int a[5][5];
int(*p)[4] ;
p = a; //類型差異,會發出警告,不影響運行
printf("%p %d", &p[4][2] - &a[4][2] , &p[4][2] - &a[4][2]);
// 結果:fffffffc -4
- 二維數組雖然以多行多列的表格形式理解起來更直觀,但其實在內存中也是連續存儲的,相當于一行多列的表格。
p[4][2]==*(*(p+4)+2)
p指向元素個數為4的數組,p+整數 跳過一個數組的大小,也就是4個整型,16字節。- 根據圖示找到
p[4][2]
的位置,與a[4][2]
做差,得到其中元素個數,%d
用于打印有符號整數,結果為-4
. - 內存中以補碼的形式存儲(x86):
-4 原碼:1000 0000 0000 0000 0000 0000 0000 0100
-4 補碼:1111 1111 1111 1111 1111 1111 1111 1100
十六進制:f f f f f f f c
- 因此%p打印結果:
fffffffc
- p只計算了指針偏移量,沒有訪問內容,所以不會造成野指針問題
題目5:數組地址與元素地址的轉換
int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};
int *ptr1 = (int*)(&aa + 1); // 跳過整個數組,指向數組后地址
int *ptr2 = (int*)(*(aa + 1));
//aa是首元素地址,也就是第0行地址, +1跳過1行,等價于aa[1],指向第1行首元素
printf("%d,%d", *(ptr1-1), *(ptr2-1)); // 結果:10,5
題目6:指針數組的偏移
char *a[] = {"work","at","alibaba"};
//a 是指針數組,每個元素是char*類型 ,每個元素指向一個字符串
char **pa = a; // pa指向指針數組首元素
pa++; // 指向第二個元素("at")
printf("%s\n", *pa); // 結果:at
題目7:三級指針的嵌套訪問
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c+3,c+2,c+1,c};
char ***cpp = cp;
printf("%s\n", **++cpp); // 結果:POINT
printf("%s\n", *--*++cpp+3); // 結果:ER
printf("%s\n", *cpp[-2]+3); // 結果:ST
printf("%s\n", cpp[-1][-1]+1); // 結果:EW
圖解:
printf("%s\n", **++cpp);
- 跳過一個
char**
,解引用兩次得到是P的地址,%s
打印完整字符串,輸出POINT
printf("%s\n", *--*++cpp + 3);
- 第一次打印的
++cpp
會改變cpp
指向的位置 ++cpp
再跳過一個char**
,解引用得到c+1
,再自減,向前跳一個char*
,再解引用得到E的地址- 再
+3
,得到E
的地址,輸出結果為ER
printf("%s\n", *cpp[-2]+3);
- 第二次打印的
++cpp
會改變cpp
指向的位置 cpp[-2] ==*(cpp-2)
cpp
向前跳過2個char**
,再解引用得到c+3
,再解引用得到F
的地址- 再
+3
得到S
的地址,輸出結果為ST
printf("%s\n", cpp[-1][-1]+1);
- 第三次打印的
cpp[-2]
不會改變cpp
的值,此時cpp
依舊指向c+1
cpp[-1]
向前跳過1個char**
,再解引用得到c+2
,cpp[-1][-1]
再向前跳過1個char*
,再解引用得到N
的地址- 再
+1
得到E
的地址,輸出結果為EW