目錄
?編輯
一、回調函數
二、qsort使用舉例
2.1 使用qsort排序整型數據
2.2 使用qsort排序結構體數據
三、qsort的模擬實現
四、NULL、\0、0、'0'、null、NUL的區別
五、C99中的變長數組
一、回調函數
? ? ? 函數指針是將函數的地址取出來,再通過函數地址去調用,那為什么不直接用函數名調用呢??原因是因為函數指針可以用來實現回調函數,而回調函數有自己的應用場景。
? ? ? 回調函數就是?個通過函數指針調?的函數。 如果你把函數的指針(地址)作為參數傳遞給另?個函數,當這個指針被?來調?其所指向的函數 時,被調?的函數就是回調函數。
#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;
}
以上這段代碼中,我們發現case部分的代碼總是重復出現,這段代碼只有調用函數的邏輯有差異(但是函數的返回類型和形參是一樣的),其他輸入輸出操作都是冗余的,那么這個時候我們可以把調用的函數地址以參數的形式傳去,用函數指針接收,函數指針指向什么函數就調用什么函數,這里其實就是使用的回調函數功能。
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;
}
void calc(int(*pf)(int, int))
{int ret = 0;int x, y;printf("輸入操作數:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}
int main()
{int input = 1;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:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("選擇錯誤\n");break;}} while (input);return 0;
}
? ?回調函數不是由該函數的實現方直接調?,?是在特定的事件或條 件發?時由另外的??調?的,?于對該事件或條件進?響應。
? ? ? 怎么理解上面這段話呢?我們可以發現回調函數并非直接調用的,而是當需要進行某種運算時(特定需求的發生),根據需求將函數地址傳給pf,然后在calc(另外一方)函數中通過pf(間接調用)來調用這個函數。
二、qsort使用舉例
前面學習的冒泡排序,只能排序整形數據,那我們如何完成其他數據的排序呢?就得用到qsort
qsort是一個庫函數,可以完成任意數據的排序,我們首先通過cplusplus的網站來了解qsort,qsort的頭文件是stdlib.h,下面我們能來分析他的形參類型。
1.第一個形參void*base是一個void*類型的指針(因為該數組可能是任意類型,所以只有void*才可以接收任意類型的數據的地址),base指向要排序的數組的第一個元素位置。
2.第二個形參size_t num是一個無符號整型,num指向的是待排序數組中的元素個數。(只要知道元素的個數才能確定比較的次數。)
3.第三個形參size_t size是一個無符號整型,size指向數組中元素的大小(單位是字節,因為qsort完成任何類型的排列,所以對象可能是結構體也可能是整型,需要具體傳入去 運算)。
4.第四個形參int (*compar)(const void*,const void*));,compar是一個函數指針,返回類型是int類型,兩個形參的類型是void*類型。該函數指針指向的函數是用來比較數組中兩個元素的方法。這個方法是根據我們的需求(比較整型或者比較結構體數據),去構造一個函數用來比較,構造的函數返回類型和形參類型必須一致。
qsort通過返回值來判斷p1和p2的大小,當返回值>0,說明p1大于p2,返回值=0,說明p1=p2,返回值<0,說明p1<p2。
了解了qsort,下面利用qsort來實現排序。
2.1 使用qsort排序整型數據
int int_cmp(const void* p1, const void* p2)//整型的比較方法
{return(*(int*)p1 - *(int*)p2);//void*類型的指針必須強轉后才可以進行運算。
}
int main()
{int arr[] = { 9,8,7,6,5,4,0,2,1,3 };int num = sizeof(arr) / sizeof(arr[0]);//確定數組的個數int size = sizeof(int);//確定數組的每個元素占用字節大小qsort(arr, num, size, int_cmp);for (int i = 0; i < num; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}
運行結果:0 1 2 3 4 5 6 7 8 9 ?
注意事項:
1.qsort的使用必須包含頭文件stdlib.h
2創建比較方法int_cmp函數時要注意該函數返回的結果必須是>0,=0,<0;
3.int_cmp傳入的是void*類型的指針,必須強轉成int*類型再解引用才可以進行運算。
4.如果想要完成逆序,將int_cmp的代碼return(*(int*)p1 - *(int*)p2)中的p1和p2交換即可。
2.2 使用qsort排序結構體數據
struct Stu//學生
{char name[20];//名字int age;//年齡
};
//創建用年齡比較的方法
int cmp_stu_by_age(const void* p1, const void* p2)
{return((struct Stu*)p1)->age - ((struct Stu*)p2)->age;//也可以寫成(*(struct stu*)p1).age-(*(struct stu*)p2).age //結構體變量.成員名 或者 結構體指針->成員名
}
//創建用名字比較的方法
int cmp_stu_by_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);//strcmp函數是專門用來比較字符串大小的//字符串的比較方法:從左到右的順序逐個比較兩個字符串的字符,直到遇到第一個不同的字符,然乎根據字符的ascii值來確定兩個字符串的大小關系。
}
//創建一個打印數組函數
void prinf(struct Stu s[], int num)
{for (int i = 0; i < num; i++){printf("第%d個同學的名字是%s,年齡是%d\n", i + 1, s[i].name, s[i].age);}
}
int main()
{struct Stu s[] = { {"zhangsan",20},{"lisi",30},{"wangwu",15} };int num = sizeof(s) / sizeof(s[0]);//元素個數int size = sizeof(struct Stu);//學生類型的大小printf("比較前\n");prinf(s, num);printf("通過年齡比較后\n");qsort(s, num, size, cmp_stu_by_age);prinf(s, num);printf("通過名字比較后\n");qsort(s, num, size, cmp_stu_by_name);prinf(s, num);
}
運行結果:
比較前
第1個同學的名字是zhangsan,年齡是20
第2個同學的名字是lisi,年齡是30
第3個同學的名字是wangwu,年齡是15
通過年齡比較后
第1個同學的名字是wangwu,年齡是15
第2個同學的名字是zhangsan,年齡是20
第3個同學的名字是lisi,年齡是30
通過名字比較后
第1個同學的名字是lisi,年齡是30
第2個同學的名字是wangwu,年齡是15
第3個同學的名字是zhangsan,年齡是20?
注意事項:
1.要訪問結構體成員的兩個方法:結構體變量.成員名 ? ?結構體指針->成員名
2.strcmp是專門用來比較字符串的大小的,并且它的返回值也恰好和qsort一樣,所以可以直接去調用。字符串的比較方法:從左到右的順序逐個比較兩個字符串的字符,直到遇到第一個不同的字符,然乎根據字符的ascii值來確定兩個字符串的大小關系。
3.結構體類型相較于整型類型,不能直接用+-<>等運算符,因為結構體中的成員屬性可能有多個,直接比較編譯器無法判斷根據哪一個成員屬性來比較。
?
三、qsort的模擬實現
? ? ? ?qsort展現的是不同數據類型的快速排序,在學習qsort之前,我只知道冒泡排序,而冒泡排序只能排序整型類型,那么我們可以通過會回調函數的方法,來改造冒泡排序,使其成為可以排序任意數據類型的排序方法。
? ? ?在模擬實現前,我們要比較qsort和冒泡排序,兩者的數據類型不一樣,所以我們對他的改造需要體現在兩個方面。
1.由于數據類型不同,所以比較的方法必須改造。
2.由于不同數據類型占用字節大小不同,在利用指針偏移量操作的時候會有差異,所以交換的方法也必須改造。
3.由于數據類型不同,創建比較方法和交換方法時傳入的兩個參數必須是void*類型
4.模擬實現qsort,就要保證改造的排序函數bubble的返回類型和形參都要保持一致
int int_cmp(const void* p1, const void* p2)//比較方法
{return(*(int*)p1 - *(int*)p2);
}
void swap(void* p1, void* p2, int size)//交換方法,這里要引入size,讓swap函數知道交換的數據是什么類型
{for (int i = 0; i < size; i++){char temp = *((char*)p1+ i);//void*類型必須要先強制轉化成char*類型*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = temp;//為什么這里要使用字符類型?,因為我們并不知道傳入的是什么數據類型,所以用char*(1個字節)來作為單位元,每次交換一個字節,交換次數恰好和size相同}
}
void bubble(void* base, int num, int size, int (*cmp)(const void* p1, const void* p2))
{for (int i = 0; i < num - 1;i++){for (int j = 0; j < num - i - 1; j++){if (int_cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0)//不知道是什么數據類型,所以用char*比較好操作,一次只操作一個字節。{swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}//使用前必須強轉成char*類型}}
}
int main()
{int arr[] = { 9,8,7,6,5,4,0,2,1,3 };int num = sizeof(arr) / sizeof(arr[0]);//確定數組的個數int size = sizeof(int);//確定數組的每個元素占用字節大小bubble(arr, num, size, int_cmp);for (int i = 0; i < num; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}
運行結果:0 1 2 3 4 5 6 7 8 9?
? ? ? ?要注意的是,由于交換方法和比較方法的改造,由于不知道比較的是什么數據類型,所以都強轉成char*類型進行操作,因為char*類型操作一次是一個字節,方便計算。這樣恰好就是一次交換一個字節,執行size次后就完成整個元素的交換。所以必須傳入size。
?
四、NULL、\0、0、'0'、null、NUL的區別
NULL:本質是0,一般用于指針的初始化
\0:\ddd形式的轉移字符,本質也是0,在字符串中作為結束標志,ASCII碼值為0
0:數字0
'0':字符0,ASCII碼值為48
null/NUL:本質就是\0,作為字符串結束標志
五、C99中的變長數組
? ? ? ? 在C99標準之前,C語?在創建數組的時候,數組大小的指定只能使?常量、常量表達式,或者如果我們初始化數據的話,可以省略數組??。
int arr1[10];
int arr2[3+5];
int arr3[] = {1,2,3};
?這樣的語法限制,讓我們創建數組就不夠靈活,有時候數組?了浪費空間,有時候數組??了不夠?的。?
? ? ?C99中給?個變?數組(variable-length array,簡稱 VLA)的新特性,允許我們可以使?變量指定數組大小。
int n = a+b;
int arr[n];
上??例中,數組 arr 就是變?數組,因為它的?度取決于變量 n 的值,編譯器沒法事先確定,只有運?時才能知道 n 是多少。
? ?變?數組的根本特征,就是數組?度只有運?時才能確定,所以變?數組不能初始化。它的好處是程序員不必在開發時,隨意為數組指定?個估計的?度,程序可以在運?時為數組分配精確的?度。有 ?個?較迷惑的點,變?數組的意思是數組的??是可以使?變量來指定的,在程序運?的時候,根據變量的??來指定數組的元素個數,?不是說數組的??是可變的。數組的大小?旦確定就不能再變化了。