@TOC


前言

本小節,我們將繼續學習C語言轉移表,什麼是回調函數,回調函數又是什麼?qsort函數怎麼使用,怎麼理解處理,要注意的細節,當然qsort使用舉例,最後我們進行qsort函數的模擬實現!文章乾貨滿滿,走起!


一、轉移表

C語言轉移表是指根據一定條件,實現程序執行流程的跳轉或轉移的機制。

具體來説,C語言中實現轉移表的主要方式有:

  1. goto語句: goto語句可以實現無條件跳轉,直接跳轉到指定標籤所在的代碼塊
goto 標籤名;

例如:

goto label;
  1. switch語句: switch語句根據表達式的值,選擇性地執行一個代碼塊。它實現了有條件跳轉。
switch(表達式)
{
  case 常數表達式1:
    //語句
    break;

  case 常數表達式2:  
    //語句
    break;

  //其他case
  
  default:
   //語句
}
  1. continue語句: continue 用於跳過循環體剩餘部分,直接跳轉到循環條件判斷語句。

例如:

for(i=0;i<10;i++)
{
  if(i==5)
    continue;
  printf("%d",i);
}
  1. break語句: break 用於跳出整個循環或switch語句。

例如:

for(i=0;i<10;i++)
{
  if(i==5)
    break;
  printf("%d",i);
}
  1. return語句: return 用於從函數中返回。

例如:

int func()
{
  return 0;
}
  1. 拓展:longjmp()/setjmp():

setjmp()longjmp()是C語言中的兩個非常重要的函數,它們可以實現非局部跳轉的功能。

  • setjmp()函數聲明如下:
int setjmp(jmp_buf env);
  • jmp_buf是可以保存環境信息的結構體。
  • setjmp()會將當前函數的執行環境信息保存到env中,並返回0。
  • 然後程序可以正常執行。
  • 當需要跳轉時,調用longjmp(env, val);
    longjmp()函數聲明如下:
void longjmp(jmp_buf env, int val);
  • longjmp()第一個參數就是setjmp()保存的env
  • 它會將程序跳轉回setjmp()後面要執行的代碼。
  • 但此時setjmp()會返回longjmp()第二個參數val,而不是0。 jmp_buf env是setjmp和longjmp函數用來保存環境信息的結構體變量。

jmp_buf是一個預定義的數據類型,它用來描述一個環境的狀態。

  • env是一個jmp_buf類型的變量。
  • 當調用setjmp(env)時,setjmp函數會將當前函數調用棧(包括函數參數、局部變量等環境信息)保存到env這個結構體變量中。
  • 之後程序可以正常執行。
  • 當需要非局部跳轉時,調用longjmp(env, val)longjmp函數第一個參數就是這個env
  • longjmp通過env這個結構體,可以恢復到setjmp函數保存環境時的狀態。實現一個“跳回”的效果。

小總結:

  • jmp_buf是一個結構體類型,它可以保存一個函數環境的狀態信息。
  • env是一個此類型的變量,用於在setjmplongjmp之間傳遞環境信息。
  • setjmp函數把當前環境信息保存到env中。
  • longjmp函數通過env這個結構體,實現恢復到setjmp時的環境狀態,從而實現非局部跳轉。


哎!當然你可以把env可以看作是一個“傳送令牌”,只要通過longjmp把令牌改了,他就重新傳送到setjmp,然後繼續執行,它連接setjmp和longjmp,使得longjmp能找到正確的環境信息進行跳轉。

所以通過setjmp()/longjmp()就實現了一個非局部跳轉:程序似乎"跳回"到setjmp()後面要執行的代碼,但實際上環境已經發生了變化。這在異常處理等場景中很有用。 工作原理是:

  1. setjmp()函數會保存當前函數調用棧(包括函數參數和局部變量等信息)的環境,並返回0。
  2. 之後程序可以正常執行。
  3. 當需要非局部跳轉時,調用longjmp(),並將在setjmp()保存的環境作為參數傳入。
  4. 這個時候程序就會跳轉回setjmp()保存的環境,彷彿從setjmp()後面繼續執行。但此時setjmp()會返回非0值。

舉個例子

# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <setjmp.h>

jmp_buf env; //jmp_buf是一個預定義的數據類型,它用來描述一個環境的狀態。
			//env是一個jmp_buf類型的變量。

void func()
{
	//設置跳轉點
	int ret = setjmp(env);

	if (0 == ret)
	{
		//正常流程
		printf("In func()\n");

		//觸發跳轉
		longjmp(env, 1);
	}
	else
	{
		//跳轉後流程
		printf("Jumped back to func()\n");
	}
}

int main()
{
	func();
	return 0;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句

程序執行流程:

  1. 主函數調用func()函數。
  2. func()內首先調用setjmp()設置跳轉點env。由於setjmp()第一次調用會返回0,所以進入if塊。
  3. 打印"In func()"信息。
  4. 調用longjmp(),觸發非局部跳轉。
  5. 程序跳轉回setjmp()設置的環境env,此時setjmp()返回1
  6. 執行else塊內的代碼,打印"Jumped back to func()"。
  7. func()返回,主函數結束。

通過在函數內使用setjmp()/longjmp(),實現了從函數內非局部跳回函數外的功能。這與goto不同,可以實現跨函數的非順序跳轉。它常用於異常和錯誤處理等場景。

  1. C語言函數指針數組可以用來實現轉移表。

具體來説:

  • 定義一個函數指針數組,元素類型為函數指針。
  • 每個數組元素都指向一個具體的函數。
  • 根據條件調用數組對應元素所指向的函數。

這與傳統的switch語句實現轉移的效果是一致的。

一個簡單的示例:

// 函數定義
void func1() 
{
  printf("func1\n");
}

void func2() 
{
  printf("func2\n"); 
}

// 主函數
int main() 
{

  // 函數指針數組
  void (*funcs[])(void) = 
  {
    func1, 
    func2
  };

  int id = 1; // 條件值

  // 根據條件調用數組元素函數
  funcs[id]();
  
  return 0;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_回調函數_02

這樣就實現了根據條件值動態調用不同函數的功能,相當於一個簡單的轉移表。

函數指針數組用於轉移表的優點是:

  • 更靈活,可以在運行時動態添加/刪除函數
  • 擴展性好,支持條件複雜情況下的多路徑轉移
  • 與傳統switch語句相比代碼更簡潔清晰

所以總的來説,函數指針數組正是C語言實現轉移表的一個很好的選擇。它可以很好地替代switch語句實現更復雜的多路轉移邏輯。 通過這個你可能不太能看出哪裏能很好的替代switch語句,讓我們來看一個典型的例子,實現一個計算器!!!

switch實現計算器:


主要實現計算器程序思路:

  1. 定義了四個運算函數add、sub、mul、div實現四則運算。
  2. main函數中:
  • 使用do while循環控制程序循環執行。
  • 打印菜單讓用户選擇運算類型。
  • 根據用户選擇用switch case調用對應的運算函數。
  • 每次運算前輸入兩個操作數,運算後打印結果。
  1. 選擇0退出循環,退出程序。
#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;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_回調函數_03

實現是實現了,但是case裏面的每個代碼塊除了ret = (?)(x,y);有點不同,其他都很相似,這麼多代碼重複寫,會造成代碼的冗餘,如果我們又繼續給用户增加功能,比如&,^,>>等等,然後一個功能我們就加一個case,case多了,代碼重複的也多咋改變呢?

不着急,我們不是學習了函數指針數組嗎?

我們可以把函數的地址存儲在數組裏面,然後通過指針訪問數組下標(0,1,2,3,4,5...),然後解引用找到我們要找到我們要實現函數的地址然後給他傳參,再接收他的計算的返回值不就搞定了。 哈哈哈哈!!掌聲應該送給自己,説做就做!讓我們繼續往下走。

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句_04

函數指針數組實現計算器:


思路:

  1. 定義了4個函數Add、Sub、Mul、Div,用於四則運算。
  2. menu()函數打印菜單界面。
  3. 定義了一個函數指針數組pfArr,元素類型為int (*)(int, int),可以存儲這4個二元運算函數的地址。
  4. 在主函數中使用do-while循環不斷運行:
  • 調用menu()打印菜單
  • scanf輸入選擇
  • 根據選擇從pfArr數組中獲取對應函數的地址
  • 調用該函數進行運算
  • 打印結果
void menu()//封裝菜單
{
	printf("******************************\n");
	printf("****  1. add     2. sub   ****\n");
	printf("****  3. mul     4. div   ****\n");
	printf("****  0. exit             ****\n");
	printf("******************************\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;
}


int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		//函數指針數組的方式解決一下
		//這裏的函數指針數組,我們稱為轉移表
		// 
										為什麼這裏要加NULL,直接{add,sub,mul,div}不行嗎?
						     如果真是可以的話,那我們觀察他的下標  0   1   2   3
							  此時此刻,如果我們選擇1.add,那麼(*p[1])取出的地址是sub,這不對呀,
							  如果我們在前面加一個NULL,{ NULL,add,sub,mul,div }
							                      下標為  0    1   2   3   4
							  地址:*p[ 0 ] == 0,     *p[ 1 ] ==add
		int (*pfArr[])(int, int) = { NULL, Add, Sub, Mul, Div };
		//                           0     1    2    3    4
		printf("請選擇:");
		scanf("%d", &input);

		if (input == 0)
		{
			printf("退出計算器\n");
		}
		else if (input >= 1 && input <= 4)
		{										
			printf("請輸入兩個操作數:");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);           //(*p[input])==add/sub/mul/div函數名,也就是函數的地址
				                                //(p[input])也可以,*號有無,都相當於函數名,也是函數地址
				                                // 也就是ret=(p[input])(x,y);
			printf("%d\n", ret);
		}
		else
		{
			printf("選擇錯誤,重新選擇\n");
		}
	} while (input);
	return 0;
}

解釋: 當input輸入1, pfArr[1]取得Add的地址,然後通過Add函數的地址,執行指令。(當然同理input輸入2,3,4也是同樣的步驟)。 如果要增加功能,那麼可以int (*pfArr[])(int, int) = { NULL, Add, Sub, Mul, Div };增加相應的功能,然後增加相應功能的代碼塊! 比如,你想要增加位運算(&, |, ^)的功能:

  1. 增加位運算函數:
int And(int x, int y) {
  return x & y;
}

int Or(int x, int y) {
  return x | y;  
}

int Xor(int x, int y) {
  return x ^ y;
}
  1. 修改菜單顯示:
void menu() {

  printf("******************************\n");
  printf("****  1. add     2. sub   ****\n"); 
  printf("****  3. mul     4. div   ****\n");
  printf("****  5. &       6. |     ****\n");
  printf("****  7. ^              ****\n");  
  printf("****  0. exit             ****\n");
  printf("******************************\n");

}
  1. 增加函數指針:
int (*pfArr[])(int, int) = {NULL, Add, Sub, Mul, Div, And, Or, Xor};
  1. 判斷函數選擇範圍:
if(input >= 1 && input <= 7) {

  ret = pfArr[input](x, y);

}

這樣就增加了位運算的功能選擇了。

如果還需要其他運算,可以繼續增加對應的函數和菜單顯示即可。

但是,思考———> int (*p[5])(int x, int y) = { NULL,add,sub,mul,div };那函數的add,sub,mul,div這些地址是不是也和一維數組一樣,存儲在函數指針數組裏面,他們的地址是否是連續的呢? 解釋:

函數地址在函數指針數組中的存儲方式與一維數組類似,但有一點不同:

  • 函數指針數組pfArr中,add、sub等函數地址的存儲是連續的,就像一維數組元素一樣,如下標0,1,2,3,4這樣連續存儲後就可以訪問了。
  • 但是,函數本身的代碼可能不一定存儲在連續內存地址中

更準確地説:

  • 在函數指針數組pfArr中,add、sub等函數地址是以連續方式存儲的。
  • 而函數本身的代碼可能分散在不同代碼段(code section)中,地址不一定連續。

舉個例子:

假設add函數代碼在地址0x00001000,sub函數代碼在0x00002000,mul在0x00003000

那麼在函數指針數組pfArr中:

pfArr[1] 指向 add函數地址 0x00001000  
pfArr[2] 指向 sub函數地址 0x00002000
pfArr[3] 指向 mul函數地址 0x00003000

我們可以看到,pfArr[1]、pfArr[2]、pfArr[3]中的函數地址是以連續的方式存儲在數組中的。

但是函數本身的代碼地址0x00001000、0x00002000、0x00003000並不連續。

所以總結來説:

  • 函數指針數組pfArr中函數地址是連續存儲的
  • 但函數代碼本身不一定連續存儲在內存中

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_函數指針數組_05

二、回調函數是什麼?

C語言中的回調函數是指在函數調用的過程中,被另外一個函數作為參數傳遞並調用的函數。

回調函數的主要特徵如下:

  1. 回調函數必須事先定義。
  2. 回調函數的地址作為參數傳遞給另一個函數,這個函數稱為主函數。
  3. 主函數在適當的時候,通過調用回調函數的地址來調用回調函數。

一個典型的回調函數使用場景例子:

// 回調函數定義
void callback_func(int value) 
{
  printf("value is %d\n", value);
}

// 主函數定義
void main_func(void (*callback)(int)) 
{
  int num = 10;
  
  // 調用回調函數
  callback(num); 
}

int main() 
{
  // 註冊回調函數
  main_func(callback_func);
  
  return 0;
}

注:回調函數的特點是函數的調用關係由使用者在運行時決定,而不是在編譯時就確定,這提供了更大的靈活性。

那可不可以使用回調函數實現計算器呢?

  1. 定義一個通用的計算函數calc,它接收一個函數指針作為參數。
  2. main函數中,根據用户選擇直接調用calc函數,並傳入相應的運算函數。
  3. 回調函數是Add、Sub、Mul、Div

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_函數指針數組_06

void menu()
{
	printf("******************************\n");
	printf("****  1. add     2. sub   ****\n");
	printf("****  3. mul     4. div   ****\n");
	printf("****  0. exit             ****\n");
	printf("******************************\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;
}


//calc功能強大了

void calc(int (*pf)(int,int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("請輸入兩個操作數:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}

int main()
{
	int input = 0;
	
	do
	{
		menu();
		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;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_回調函數_07

三、qsort函數細解

3.1 類比冒泡排序?

通過前面我們學過冒泡排序,qsort函數的排序讓我們類比一下:

void bubble_sort(int arr[], int sz)
{
	//趟數
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的過程
		//兩兩相鄰元素的比較
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	//將一組整數排序為升序
	int arr[10] = { 3,1,5,2,4,6,8,7,0,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	bubble_sort(arr, sz);
	print_arr(arr, sz);
	return 0;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句_08

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句_09

3.2 qosrt函數超詳解

庫函數的學習和查看⼯具很多,⽐如: C/C++官⽅的鏈接:https://zh.cppreference.com/w/c/header cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/ qsort函數是C標準庫中用於對數組進行快速排序的函數。(注:qsort函數底層使用的排序算法就是快速排序。)

  • 函數原型:
void qsort(
    void* base,//base 指向了要排序的數組的第一個元素
    size_t num, //base指向的數組中的元素個數(待排序的數組的元素的個數)
    size_t size,//base指向的數組中元素的大小(單位是字節)
    int (*compar)(const void*p1, const void*p2)//函數指針 - 指針指向的函數是用來比較數組中的2個元素的
    );

.- [ ] 分析定義:

  • base指向要排序的數組首地址。
  • num表示數組元素個數。
  • size表示每個元素的大小,以字節為單位。
  • compar是比較函數,它接收兩個void指針,返回值小於0表示第一個參數小於第二個參數,等於0表示相等,大於0表示第一個參數大於第二個參數。

.- [ ] 特點:

  • qsort使用快速排序算法,時間複雜度為O(nlogn)
  • 調用qsort時,需要提供一個比較函數compar來判斷兩個元素的大小關係。
  • 比較函數通過void指針間接訪問元素,避免與數據類型綁定,實現了最大程度的通用性。
  • qsort會在內部調用比較函數多次對數組進行排序,這就是回調機制的實現。
  • qsortinplace排序,不需要額外的空間。

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句_10

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_函數指針數組_11

3.2.1qsort函數排序整型數據

#include <stdio.h>
#include <stdlib.h>

void print_arr(int arr[], int sz)
{//打印函數
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int cmp_int(const void* p1, const void* p2)
{//好比冒泡排序
	return *(int*)p1 - *(int*)p2;
}

//測試qsort排序整型數據的
int main()
{
	int arr[10] = { 4,2,5,3,1,6,7,8,0,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);//打印前
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	//arr->數組首元素地址
	//sz->數組元素個數////也可以這樣寫sz==sizeof(arr)/sizeof(arr[0])
	//sizeof(arr[0])->數組元素大小,這裏字節大小為4
	//cmp_int比較函數
	print_arr(arr, sz);//打印後
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_回調函數_12

3.2.2 使⽤qsort排序結構數據

  1. 定義結構體類型
struct Stu
{
	char name[20];//名字
	int age;
};
  1. 定義比較函數
  • 怎麼比較2個結構體數據? - 不能直接使用 > < ==比較
  • 可以按照名字比較
  • 可以按照年齡比較
//按照年齡比較
int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p2)->age - ((struct Stu*)p1)->age;
}
  1. 聲明結構體數組和元素個數
void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 38}, {"wangwu", 18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}

代碼實現:

# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

////測試qsort函數排序結構體數據
struct Stu
{
	char name[20];//名字
	int age;
};

int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p2)->age - ((struct Stu*)p1)->age;
}
void print(struct Stu arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%s %d\n", arr[i].name, arr[i].age);
	}
}

void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 38}, {"wangwu", 18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	print(arr, sz);
}

//	//兩個字符串不能使用> < ==
//	//而是使用庫函數strcmp - string compare
	int cmp_stu_by_name(const void* p1, const void* p2)
	{
		return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
	}
	
	void test3()
	{
		struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 38}, {"wangwu", 18} };
		int sz = sizeof(arr) / sizeof(arr[0]);
		qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
		print(arr, sz);
	}


int main()
{
	test3();//姓名排序
	//test2();//年齡排序
	

	return 0;
}
運行test2()//年齡排序

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_回調函數_13

四、 qsort函數的模擬實現

4.1 模擬qsort整形數據

  1. 主函數:
  • 定義int測試數據
  • 調用bubble排序
  • 打印結果驗證
#include <stdio.h>
int main()
{
	int arr[] = { 3, 1, 5, 8, 4, 2, 9, 6, 7, 0 };
	int i = 0;  //元素個數                   //元素大小
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
	//數組首元素地址arr                                    //比較函數
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
  1. bubble函數:
  • 接收基地址、元素個數、元素大小、比較函數作為參數
  • 實現冒泡排序核心算法
  • 通過傳遞的比較函數int_cmp來決定排序順序
  • 使用_swap函數交換元素
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);
			}
		}
	}
}
  1. int_cmp函數:
  • 定義一個int類型的數據比較函數
  • 通過減法實現兩個int*指針所指向的數據比較
  • 返回值大於0表示p1大於p2(升序),小於0表示p1小於p2(降序)
  • 實現了冒泡排序中的比較規則
int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
  1. _swap函數:
  • 定義泛型數據交換函數
  • 通過循環交換每個字節實現數據交換
  • 使用char*來交換,實現數據類型無關

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;
	}
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_函數指針數組_14

總結: 每個代碼塊實現的功能:

  • 主函數: 測試驅動開發
  • bubble: 實現冒泡排序算法
  • int_cmp: 提供比較規則
  • _swap: 實現泛型數據交換

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_switch語句_15

4.2 模擬qsort排序結構數據

各個代碼塊分析如下:

  1. struct Stu定義結構體類型,包含姓名和年齡字段。
# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

struct Stu
{
	char name[20];
	int age;
};
  1. Swap函數實現泛型數據交換,通過循環交換每個字節實現數據交換。
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++;
	}
}
  1. bubble_sort2函數實現冒泡排序算法,和普通冒泡排序區別在於使用void*作為參數,通過width實現對結構體進行排序。
void bubble_sort2(void* base, int sz, int width, int (*cmp)(const void* p1, const void* p2))
{
	int i = 0;
	//趟
	for (i = 0; i < sz - 1; i++)
	{
		//每一趟冒泡排序的過程
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//if (arr[j] > arr[j + 1])
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				/*int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;*/
				//交換
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
  1. cmp_stu_by_age函數實現結構體比較規則,根據年齡字段進行比較。
int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
  1. test4函數定義測試數據,調用bubble_sort2進行排序,打印結果驗證。
void test4()
{
	struct Stu arr[] = { {"zhangsan", 18},{"lisi", 35},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort2(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	//打印arr數組的內容
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", arr[i].name, arr[i].age);
	}
}
  1. main函數調用test4函數進行測試。
int main()
{
	//模擬結構體按年齡排序
	test3();
	return 0;
}

6種轉移表實現整合longjmp()/setjmp()函數和qsort函數詳解_函數指針數組_16

小總結

  • struct Stu定義了需要排序的數據類型
  • Swap函數實現數據交換
  • bubble_sort2實現泛型冒泡排序
  • cmp_stu_by_age定義了結構體比較規則
  • test4函數測試驅動開發

總結

一、轉移表 利用函數指針數組實現轉移表是動態規劃算法解決最優子結構問題時使用的一種技術。它記錄了子問題的解,避免重複計算。

二、回調函數是什麼? 回調函數是指在函數調用後,被當作參數傳遞給另一個函數的函數。調用方在需要時,會調用被調用方內部的這個函數。

三、qsort函數細解

3.1 類比冒泡排序? qsort函數實現的也是冒泡排序算法。不同之處在於:

  • qsort是通用排序函數,可以對任意數據類型進行排序,而冒泡排序只能對數組進行排序;
  • qsort通過回調函數來指定元素的比較方式,而冒泡排序直接比較元素值;
  • qsort內部實現採用快速排序思想,而不是純粹的冒泡排序。

3.2 qsort函數超詳解

qsort函數原型:

void qsort(void *base, size_t num, size_t size, int (*compar)(const void*, const void*));
  • base:待排序數組首地址
  • num:數組元素個數
  • size:每個元素大小
  • compar:比較函數回調,返回值小於0時交換元素

3.2.1 qsort排序整型數據

直接傳入整型比較函數如int cmp(const void*, const void*)

3.2.2 使用qsort排序結構數據

定義結構體比較函數,通過強制類型轉換比較結構體字段

四、qsort函數的模擬實現

4.1 模擬qsort整形排序 實現了一個簡單快速排序函數,可以對整型數組排序。

4.2 模擬qsort結構體排序 同樣實現快速排序,但使用結構體比較函數作為回調。

感謝你的收看,如果文章有錯誤,可以指出,我不勝感激,讓我們一起學習交流,如果文章可以給你一個幫助,可以給博主點一個小小的贊😘