在上一篇博客中:
C語言-指針講解(3)
我們給大家介紹了指針進(jìn)階的用法
讓下面我們來回顧一下講了什么吧:
1.字符指針變量類型以及用法
2.數(shù)組指針本質(zhì)上是一個指針,里面存放數(shù)組的地址。而指針數(shù)組本質(zhì)上是個數(shù)組,里面存放的是指針,指向的是整型數(shù)組。
以及數(shù)組指針變量的用法。
3.二維數(shù)組傳參的本質(zhì):
- 二維數(shù)組起始可以看做每個元素是一維數(shù)組的數(shù)組,也就是二維數(shù)組的每個元素是一個一維數(shù)組。
- 二位數(shù)組的數(shù)組名表示的就是第一行的地址,是一維數(shù)組的地址。
- 二維數(shù)組傳參傳參本質(zhì)也是傳遞了地址,傳遞的是第一行這個一維數(shù)組的地址。
4.函數(shù)指針以及函數(shù)指針變量的用法和舉例
5.函數(shù)指針數(shù)組的概念以及函數(shù)指針數(shù)組的用法。
6.用函數(shù)指針數(shù)組作為轉(zhuǎn)移表來實現(xiàn)整個計算器的代碼邏輯。
那么這次博主將更為深層的介紹指針的高級用法,具體內(nèi)容如下:
1.回調(diào)函數(shù)
1.1 回調(diào)函數(shù)是什么?
回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。
如果我們把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當(dāng)這個指針被用來調(diào)用所指向的函數(shù)時,被調(diào)用的函數(shù)就是回調(diào)函數(shù)?;卣{(diào)函數(shù)不是由該函數(shù)的實現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外一方調(diào)用的,用于對該事件或條件進(jìn)行相應(yīng)。
1.2 回調(diào)函數(shù)舉例
如下所示:
1.2.1回調(diào)函數(shù)應(yīng)用和代碼分析:
我們在上一篇博客:C語言-指針講解(3)
中。
已經(jīng)講過紅色框中的代碼是重復(fù)出現(xiàn)的,雖然里面執(zhí)行計算的邏輯是區(qū)別的,但是輸入輸出操作時冗余的,有沒有辦法可以簡化一些呢?
答案是有的。
代碼改進(jìn):
我們發(fā)現(xiàn)紅色框中的代碼,只有調(diào)用函數(shù)的邏輯是有差異的,我們可以把調(diào)用的函數(shù)的地址以參數(shù)的形式傳遞過去,然后使用函數(shù)指針接收,函數(shù)指針指向的函數(shù)就調(diào)用什么函數(shù),這里其實使用的就是回調(diào)函數(shù)的功能。
2.qsort使用舉例
在介紹qsort函數(shù)之前,我們首先分享一個查閱C/C++庫函數(shù)的網(wǎng)站給大家:
C/C++庫函數(shù)官網(wǎng)查詢
2.1 qsort函數(shù)的參數(shù)詳解:
如下圖所示:
通過上圖的分析,我們發(fā)現(xiàn)qsort中的第二、三個參數(shù)都是比較常見的。那我們就重點介紹第一個參數(shù)和第四個參數(shù)吧。
2.1.1 qsort函數(shù)中的第一個參數(shù)重點介紹
根據(jù)上圖的介紹,我們不難看出qsort第一個參數(shù)的意思是base指向待排序的第一個元素。但是為什么它前面的類型是void*呢?很多同學(xué)可能對此不理解。
下面我來重點介紹一下為什么是前面的類型是void *?
如下圖所示:
- 比方說:我這里要定義了一個a的變量,而如果說我用char* 的指針變量來接收a的地址,顯然是不合理的,因為vs彈出警告說:int * 到 char * 的類型不兼容。
- 反倒void* 是一個通用的指針類型。它可以接受任意數(shù)據(jù)類型的地址。因此前面的指針類型我們是可以寫成void*的。
- 需要注意的是:雖然說這里void * 的指針可以接受任意數(shù)據(jù)類型的地址,但是呢它還是具有一定的局限性,比方說:我們從上圖發(fā)現(xiàn)雖然void * 的指針拿到任意數(shù)據(jù)類型的地址,但是它始終是個無類型的指針,因此我們?nèi)绻麑oid*的變量1或-1要訪問多少個字節(jié)我們是不知道的,同樣的道理;我們對它進(jìn)行解引用操作訪問多少個字節(jié),這個也是不知道的。因此像這種寫法在vs編譯器上是會報錯的,所以對于void * 的指針,它只是用來存放地址的,我們可以把任意類型的地址放到void * ,但是void * 的變量不能進(jìn)行+1,-1,以及解引用的操作。
那這時,可能有同學(xué)會想為什么qsort這個函數(shù)別人要設(shè)計成void*的指針呢?
這是因為,我們知道,qsort這個函數(shù)是庫函數(shù)實現(xiàn)的。
- 我們也可以做個比喻:實現(xiàn)那個代碼的人叫程序員A,比方說他設(shè)計好這個函數(shù),讓我們這些同學(xué)以及開發(fā)者和全世界的人去使用,但是程序員A,但是他不能預(yù)料到別人要用qsort這個函數(shù)要排什么類型的數(shù)據(jù)。
- 比如張三的要用qsort排整型的數(shù)據(jù),李四要用qsort來排浮點型的數(shù)據(jù),王五要用qsort來排字符串型的數(shù)據(jù),甚至有人用qsort來排結(jié)構(gòu)體類型的數(shù)據(jù),而結(jié)構(gòu)體類型的數(shù)據(jù)又被稱作為自定義類型的數(shù)據(jù)。就是我們排什么類型的元素都行,只需把排序的第一個元素的地址傳給qsort就行。因為qsort它不知道排什么類型,但是我們把它的地址放到void是不是就行,因為void的指針是可以接受任意類型的地址。
2.1.2 qsort函數(shù)中的第四個參數(shù)重點介紹
如下圖所示:
我們之前也對qsort這個函數(shù)的第4個參數(shù)有所了解,本質(zhì)就是指向的函數(shù)能夠比較兩個元素。
當(dāng)然呢,如果我們對第四個參數(shù)有更深的理解,我們會發(fā)現(xiàn)通過qsort內(nèi)部去調(diào)用指針?biāo)赶蚰莻€的函數(shù)從而比較這兩個元素的,并且我們發(fā)現(xiàn)這個函數(shù)指針返回的值是int型的,因此它們是通過比較兩個元素的大小從而確定返回的值是大于0,是等于0,還是小于0?
舉個例子:
- 如果p1指向元素的值要比p2指向元素的指向的值要大,返回的是1個大于0的數(shù)。
- 如果p1指向元素的值要跟p2所指向的元素的值相等,返回的是0。
- 如果p1所指向的元素要比p2所指向的元素要相小,則返回的是小于0的數(shù)。
2.2 qsort 函數(shù)用法舉例
2.2.1qsort 函數(shù)用法舉例1:
這里我們演示一下用qsort函數(shù)對數(shù)組以升序的方式進(jìn)行排序。
//這個函數(shù)能夠比較e1和e2指向的兩個元素,并且給出返回值
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//test1測試qsort排序整型數(shù)組
void test1()
{
int arr[] = { 3,1,5,7,2,4,8,6,0,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
int main() {
test1();
return 0;
}
這里可能有同學(xué)對于下面這行代碼不太理解,博主這里就解釋一下吧:
return *(int*)e1 -*(int*)e2;
分析:
- 我們知道cmp_int是個函數(shù)指針類型,然后它這個cmp_int 函數(shù)指針參數(shù)部分e1和e2是個void *的指針,首先我們假設(shè)e1存放的是3的地址,而e2存放的是1的地址,那如何比較比較它們的大小呢?其實很簡單,我們只需在e1前面加上個 * ,e2前面加上個 * 。
也是就:
*e1, *e2這樣子。- 但是那個e1和e2都是void * 的指針,這樣是不能直接比較它們的大小的,我們要通過對這e1和e2強轉(zhuǎn)為int * 的指針,然后再對它進(jìn)行解引用操作,這樣比較兩個元素的大小。這是什么原理呢?這是因為qsort函數(shù)的使用者博主,現(xiàn)在我確實知道當(dāng)我們排序拿出這組數(shù)據(jù)的時候,如果cmp_int來比較這兩個整型的話。e1指向的是整型,e2指向的是整型,那我們要拿出整型的值,將e1和e2轉(zhuǎn)為整型指針后再解引用,是不是就拿到呢。
- 另外,當(dāng)我們拿到這兩個元素的數(shù)據(jù),如果要比較這兩個數(shù)的大小,那我們只需返回e1-e2的值即可。
這里需要注意的是:
- qsort函數(shù)中的第四個參數(shù)不一定只是調(diào)用了一次cmp_int函數(shù),有可能是調(diào)用了多次cmp_int函數(shù)。 另外,qsort里面有復(fù)雜的代碼,它目前對于大家來講相當(dāng)于一個黑箱子,大家都不知道里面是怎么實現(xiàn)的,但博主接下來會給大家演示如何模擬實現(xiàn)一個qsort函數(shù)。
- 另外,qsort函數(shù)第排序的過程中是需要比較大小的,這個可能很多人會有點懵,因為我們排出它的順序是需要比較大小的,如果我們不知道這些元素的大小是不能對它進(jìn)行排序的。
我們不妨用vs來運行一下此程序,看看能否把數(shù)組中的數(shù)字以升序的形式排列出來~
如下圖所示:
當(dāng)然,如果有同學(xué)比較好奇,想用qsort函數(shù)實現(xiàn)降序的效果,那我們就把cmp_int函數(shù)返回值由e1-e2改成e2-e1即可。
也就是這樣子,如下所示:
2.2.1qsort 函數(shù)用法舉例2:
當(dāng)然,我們也可以用qsort函數(shù)來排序結(jié)構(gòu)體的數(shù)據(jù)。
如下所示:
//test2測試qsort函數(shù)排序結(jié)構(gòu)體數(shù)據(jù)
struct Stu //學(xué)生
{
char name[20];//名字
int age;//年齡
};
//假設(shè)按照年齡來比較
int cmp_stu_by_age(const void* e1, const void* e2) {
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//strcmp - 是庫函數(shù),是專門用來比較兩個字符串的大小的
//假設(shè)按照名字來比較
//int cmp_stu_by_name(const void* e1, const void* e2)
//{
// return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
//}
void test2()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
//qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
test2();
return 0;
}
這里可能有同學(xué)對qsort排序結(jié)構(gòu)體的數(shù)據(jù)不理解,沒有關(guān)系,博主后期的博客會繼續(xù)深入介紹C語言結(jié)構(gòu)體方面的知識,大家盡情關(guān)注一下哈,目前我們只是看一下qsort排序結(jié)構(gòu)體數(shù)據(jù)后的結(jié)果哦~
2.2.1通過vs調(diào)試窗口觀看qsort排序前后的結(jié)果:
這里我們通過放兩個動圖:一個是按照結(jié)構(gòu)體年齡的數(shù)據(jù)來排的動圖,一個是按照結(jié)構(gòu)體名字的數(shù)據(jù)來排的動圖。
動圖一:
這是我們根據(jù)結(jié)構(gòu)體年齡升序排列的動圖效果。動圖2:
這是我們根據(jù)結(jié)構(gòu)體名字的順序來升序排列的動圖效果。
這里可能有些同學(xué)對這個名字排序有點疑惑,為什么lisi的名字是最小的,而zhangsan這個名字是最大的?
- 這是因為這里的名字進(jìn)行比較本質(zhì)上就是每個名字中的第一個字符進(jìn)行比較,如果說這三個名字中的第一個字符都相等,它就會進(jìn)行這三個名字中的第二個字符進(jìn)行比較。直到這三個名字中的第n個字符各不相同,那么這時就能分出哪個字符大,哪個字符小。 就無需對這三個名字的第n往后的字符進(jìn)行比較。
![]()
- 從上圖的ASCLL碼表我們可以得知:顯然這三個名字中的首字符z最大,l最小。那么就無需對這三個名字中的第二個字符進(jìn)行比較。 也就是說我們可以得出以下結(jié)論:
lisi<wangwu<zhangsan。
另外,我們講一下,這里講的qsort函數(shù)其實用的是快速排序的思想,大家如果感興趣的可以學(xué)一下。
3. qsort函數(shù)的模擬實現(xiàn)
這里呢,博主給大家演示用qsort函數(shù)來模擬實現(xiàn)一個冒泡排序。
首先呢,我們先給大家看一下普通冒泡排序的代碼:
#include <stdio.h>
void Bubblesort(int arr[], int sz) {
//趟數(shù)
int i = 0;
for (i = 0; i < sz - 1; i++) {
int j = 0;
for (j = 0; j < sz - 1 - i; j++) {
if (arr[j + 1] < arr[j]) {
int tmp = arr[j+1];
arr[j + 1] = arr[j];
arr[j] = tmp;
}
}
}
}
void print_arr(int arr[], int sz) {
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
//
int main() {
int arr[] = { 3,1,5,6,9,0 ,15};
int sz = sizeof(arr) / sizeof(arr[0]);
Bubblesort(arr, sz);
print_arr(arr, sz);
return 0;
}
代碼分析及其優(yōu)化:
但是我們發(fā)現(xiàn)這個代碼局限性比較小,只能對整型進(jìn)行排序,而如果說我們要對浮點型,字符型,結(jié)構(gòu)體類型進(jìn)行排序,顯然是不可行的?
那我們要怎么優(yōu)化這個程序呢?
如下圖所示:
如上圖所示,我們發(fā)現(xiàn)如果要對這個整型冒泡排序算法改成能夠排序任意類型元素的算法。就要做出這三種改變
- 改造參數(shù) - 讓這個函數(shù)能夠接受任意類型的數(shù)據(jù)
- 改造比較方法 - 讓函數(shù)能夠在排序時,比較不同類型的數(shù)據(jù)
- 兩個數(shù)交換部分要改變
3.1參數(shù)部分改變
首先,如果把參數(shù)部分改變,就要改成跟qsort參數(shù)類型是一樣的。
如下代碼所示:void Bubble_sort(void* base, size_t sz, size_t width, int (*Cmp_int)(const void* e1, const void* e2))
3.2改造比較方法
- 首先,我們要調(diào)用它的Bubble_sort函數(shù)中的第四個參數(shù):int (* Cmp_int)(const void* e1, const void* e2)來作為判斷條件來比較,因為這個函數(shù)指針Cmp_int本質(zhì)上就是通過指向比較兩個元素的函數(shù)的指針。重復(fù)調(diào)用此函數(shù)來比較兩個元素。而這里,我們假設(shè)判斷它是否為升序數(shù)組:我們就要判斷e1-e2是否>0,如果是返回的大于0的數(shù),則交換;反之,則不交換。根據(jù)上面的介紹,那個if語句我們可以寫成這樣:
if(Cmp_int()>0);
那Cmp_int函數(shù)的參數(shù)類型我們可以寫成什么呢?
- 我們發(fā)現(xiàn),這里的參數(shù)部分可以寫成兩個比較元素的地址,因為只要把元素的地址傳給Cmp_int函數(shù),然后通過調(diào)用這個函數(shù),返回的值如果是e1>e2,就要對這兩個元素的值進(jìn)行交換。
那我們應(yīng)該怎么改呢?
- 我們可以把代碼這樣,如下所示:
if (Cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
有同學(xué)可能不知道為什么要這么改?我們來解釋一下把:
如下圖所示:
從上圖,我們發(fā)現(xiàn),如果我們要找到數(shù)組中5的地址,就要跳過2 * width字節(jié),就是跳過2個這么寬的元素;
而如果說要找到數(shù)組中的2的地址,就要跳過3 * width個字節(jié)。就相當(dāng)于跳過3個這么寬的元素,這是因為base指針指向的是數(shù)組首元素的地址,而width表示的是一個元素類型所占的大小,只有知道一個元素類型的大小,才能訪問到它后面元素的地址。 那么照這個規(guī)律,我們推而廣之,就能得出下面這個式子:(char*)base + j * width;
既然跳過兩個整型元素,就是2 * width,那跳過j個整型元素,就是j * width;而跳過(j+1)整型元素就是(j+1) x width。那最終我們條件判斷就可以寫成這樣:
if (Cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
需要注意的是,這里的base由void *強轉(zhuǎn)為char * 。是因為char * 是只占一個字節(jié),只有char * 類型才能保證一個一個字節(jié)一個字節(jié)走的,只有這樣才能j * width具體能控制哪個元素。
3.3改造兩個數(shù)交換部分
當(dāng)我們改造好比較方法后,接下來就要對兩個數(shù)交換部分進(jìn)行比較,這個也是比較簡單,我們可以封裝一個Swap函數(shù),把那兩個比較元素的地址以及寬度width作為參數(shù)部分傳過去。
代碼如下:Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
這里會有同學(xué)會有疑問,為什么傳的是要傳width這個參數(shù)給Cmp_int函數(shù)呢
- 因為如果是單單傳這兩個元素參數(shù)過去的話,我們是不知道這兩個元素有多大的,因為我們在排序過程中,這個冒泡排序程序壓根不知道排的是什么類型的數(shù)據(jù),它只知道交換的是兩個元素的起始地址,因此這里要把它們交換幾個字節(jié)搞上去。
- 另外當(dāng)我們進(jìn)入Swap函數(shù)內(nèi)部,我們形參還是用char *指針的方式來接收,方便之后的交換操作。
根據(jù)上圖,我們發(fā)現(xiàn)當(dāng)我們要對5和2這兩個數(shù)字進(jìn)行交換的話,就要對它們每組的字節(jié)數(shù)進(jìn)行交換,并且我們發(fā)現(xiàn)這些數(shù)字在vs內(nèi)存中是以十六進(jìn)制,并且倒著放來進(jìn)行存儲的
如下圖所示:
那如果我們要對這兩個數(shù)進(jìn)行交換的話,我們是一對字節(jié)一對字節(jié)地交換,因為它是個char * 的指針。交換完之后buf和buf2進(jìn)行++的操作,找到下一個字節(jié)進(jìn)行交換,它這里width總共是占4個字節(jié),因此要交換4次就能完成交換的操作。Swap函數(shù)內(nèi)部代碼如下:
void Swap(char* buf1, char* buf2, size_t width) { int i = 0; for (i = 0; i < width; i++) { char tmp = *buf1; *buf1 = *buf2; *buf2 = tmp; buf1++; buf2++; } }
如上代碼: 我們不難看出,當(dāng)for循環(huán)結(jié)束的時候,2和5這兩個數(shù)字的一組中的字節(jié)數(shù)全都進(jìn)行了交換。
綜上所述:我們發(fā)現(xiàn)以一對字節(jié)一對字節(jié)地交換非常適用在這種場景下的,反之,不能直接在Swap函數(shù)內(nèi)部創(chuàng)建臨時變量來交換數(shù)據(jù),比方說:整型占4個字節(jié),浮點型也占4個字節(jié)。這種情況下,即使Swap這個函數(shù)根據(jù)該數(shù)據(jù)類型的字節(jié)數(shù)還是不能準(zhǔn)確知道它傳的數(shù)據(jù)類型是什么,而用char *的方式創(chuàng)建臨時變量,它每次只會交換兩個十六進(jìn)制數(shù),也就是1個字節(jié)。這樣我們根據(jù)width參數(shù)所占的字節(jié)數(shù),一對字節(jié)一對字節(jié)地交換能夠很好地控制所要交換的次數(shù)。
4.模擬實現(xiàn)qsort函數(shù)源代碼展示:
下面是博主模擬實現(xiàn)qsort源代碼:
//程序員A,設(shè)計一個qsort函數(shù)功能,能夠排序任意類型的數(shù)據(jù)
int Cmp_int(const void* e1, const void* e2) {
return (*(int*)e1 - *(int*)e2);
}
void Swap(char* buf1, char* buf2, size_t width) {
int i = 0;
for (i = 0; i < width; i++) {
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void Bubble_sort(void* base, size_t sz, size_t width, int (*Cmp_int)(const void* e1, const void* e2)) {
int i = 0;
for (i = 0; i < sz - 1; i++) {
int j = 0;
for (j = 0; j < sz - 1 - i; j++) {
if (Cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
//為什么要強轉(zhuǎn)為char*,因為char*是占一個字節(jié),只有char才能保證一個字節(jié)一個字節(jié)走的,只有這樣,才能控制j*witdh具體能控制哪個元素
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
}
}
}
}
如果大家有需要的話實現(xiàn)對任意數(shù)據(jù)類型進(jìn)行排序的話,可以借鑒一下博主模擬實現(xiàn)qsort函數(shù)的代碼哦!
5.用模擬實現(xiàn)的qsort函數(shù)對數(shù)據(jù)進(jìn)行排序
下面博主以動圖的方式給大家演示用模擬實現(xiàn)的qsort函數(shù)對數(shù)據(jù)進(jìn)行排序。
5.1 用模擬實現(xiàn)的qsort函數(shù)對數(shù)組進(jìn)行升序或降序的操作動圖演示:
1.數(shù)組升序:
2.數(shù)組降序:
5.2 用模擬實現(xiàn)的qsort函數(shù)對結(jié)構(gòu)體類型的數(shù)字排序動圖演示:
按結(jié)構(gòu)體的名字排序:
按結(jié)構(gòu)體的年齡排序:文章來源:http://www.zghlxwxcb.cn/news/detail-766406.html
** 好了,今天的分享到這就結(jié)束了,如果覺得博主講得有些知識點不太清楚的話,可以在評論區(qū)指出**
** 如果覺得博主講得還不錯的話,希望大家一鍵三連支持一下!謝謝大家?。?!**文章來源地址http://www.zghlxwxcb.cn/news/detail-766406.html
到了這里,關(guān)于C語言-指針講解(4)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!