当前位置:   article > 正文

C语言指针进阶

C语言指针进阶

目录

前言

本期类容介绍:

一、字符指针

二、指针数组

 三、数组指针

3.1数组指针的引出和定义

3.2数组名和&数组名

3.3数组指针的使用

四、数组传参和指针传参

4.1一维数组传参

4.2二维数组传参

​编辑 4.3一级指针传参

总结:当一个函数的参数部分是一级指针时,函数可以接收什么类型的参数?

4.4二级指针传参

总结:当一个函数的参数部分是二级指针时,函数可以接收什么类型的参数?

五、函数指针

5.1函数指针的定义

5.2函数指针的使用

六、函数指针数组

函数指针的用途:转移表

七、指向函数指针数组的指针【了解】

八、回调函数

8.1什么是回调函数?

8.2回调函数的演示(用qsort):

什么是qsort?

8.3模拟实现qsort:


前言

前面一期我们已经介绍了指针基础,我们了解了什么是指针、指针和指针类型的区别、野指针、指针运算、指针和数组、二级指针以及指针数组,这些内容都是比较基础,这一期我们再来对指针的理解进行拔高一层。(本期干货满满,建议阅读时带杯水)

本期类容介绍:

1.字符指针

2.指针数组

3.数组指针

4.数组传参和指针传参

5.函数指针

6.函数指针数组

7.指向函数指针数组的指针

8.回调函数

一、字符指针

先来看一段代码:

  1. int main()
  2. {
  3. char* str = "abcdef123456";
  4. printf("%s", str);
  5. return 0;
  6. }

我们知道字符串一般存放在字符数组里面,但上面这段代码好像是把字符串放到了字符指针str里面。事实真的是这样吗?我们稍微分析一下就会发现:上面字符串超过了8个字节!而我们知道一个指针的大小是 4(x86)/ 8(x64)个字节,所以上面字符串不可能发到str里面。那里面放的是啥呢?其实str里面放的是字符串首元素的地址

画个图理解一下:(假设字符串的首元素的地址是0x11ff40)

 理解到这里,我们就可以做一下一道很经典的面试题了:

  1. int main()
  2. {
  3. char str1[] = "hello bit";
  4. char str2[] = "hello bit";
  5. const char* str3 = "hello bit";
  6. const char* str4 = "hello bit";
  7. if (str1 == str2)
  8. {
  9. printf("str1 and str2 are same\n");
  10. }
  11. else
  12. {
  13. printf("str1 and str2 are not same\n");
  14. }
  15. if (str3 == str4)
  16. {
  17. printf("str3 and str3 are same\n");
  18. }
  19. else
  20. {
  21. printf("str3 and str4 are not same\n");
  22. }
  23. return 0;
  24. }

思考一下,这段代码的结果是多少?

我们一起来看一下:

解释一下:

str1和str2是两个字符数组,里面存放的都是"hello bit" ,但他们(str1和str2)会在栈区上开辟不同的空间,也就是他们的地址肯定不一样,而我们知道数组名一般情况下代表首元素的地址(指针),所以str1不可能等于str2。而str3和str4存的都是字符常量的"hello bit"的首元素h的地址,字符串常量是放在只读数据区,只有一份。因为是常来量不可修改所以在*前面加上const(关于const修饰问题后面会介绍)。所以实际上两个指针都指向首元素的地址!所以,str3 == str4!而str3和str4一样还是局部变量也存放在栈区!

二、指针数组

前面一期我们已经介绍过指针数组了!指针数组本质是一个数组,里面的元素是指针类型的!

举几个例子:

  1. int main()
  2. {
  3. char* str[20];//一级字符指针数组
  4. int* arr[10];//一级整型指针数组
  5. int** arr1[5];//二级整型指针数组
  6. return 0;
  7. }

这就是几个指针数组的例子!上期我们还介绍了用指针数组来模拟二维数组的例子,本期我们再来介绍一遍:

  1. int main()
  2. {
  3. char str1[] = "abc";
  4. char str2[] = "def";
  5. char str3[] = "123";
  6. char* arr[3] = { str1,str2,str3 };
  7. for (int i = 0; i < 3; i++)
  8. {
  9. printf("%s\n", *(arr + i));
  10. }
  11. return 0;
  12. }

画个图理解一下:

 三、数组指针

3.1数组指针的引出和定义

数组指针是数组还是指针?和前面的指针数组的名字相似,前者我们介绍了,是数组,这个其实是指针!既然是指针就得有指向的对象!我们知道int类型的指针指向对象的类型是整型,char类型的指针指向对象的类型是字符型,那数组指针指向对象的类型应该是数组类型(数组类型就是去掉数组名剩下的就是数组类型)!

我们来看一个栗子:(下面哪个是数组指针哪个是指针数组)

  1. int mian()
  2. {
  3. int* p1[10];
  4. int(*p2)[10];
  5. return 0;
  6. }

我们知道 [ ]的优先级比*的高,所以p1会先与[]结合,所以p1是个数组,它的每个元素的类型是int *所以p1是指针数组

而第二个先把*与p2用括号括起来先让他们结合,*与p2先结合说明p2是指针,指向的对象是数组 [10],数组有10元素,每个元素的类型是int 类型!所以p2是数组指针

所以介绍到这里:我们就可以给出数组指针的定义:type (*指针名) [const Num];

3.2数组名和&数组名

我们前面几期对数组名和指针以及&数组名做了介绍,本期我们再来回顾一番:

我们知道数组名一般代表数组首元素的地址,有两个例外:

1. sizeof(数组名)---> 计算的是这个数组的大小!

2.&数组名,取出的是整个数组的大小!

举个栗子:

  1. int main()
  2. {
  3. int arr[10] = { 0 };
  4. printf("arr = %p\n", arr);
  5. printf("arr+1 = %p\n", arr + 1);
  6. printf("&arr = %p\n", &arr);
  7. printf("&arr+1 = %p\n", &arr + 1);
  8. return 0;
  9. }

看结果:

分析:这里第一个是arr + 1 == arr[1]也就是第二个元素,因为是整型所以差4,第二&arr + 1实际上是跳过了整个数组,一个数组10个元素每个元素4个字节,所以是差40!

这里不知道你有没有想过,arr是数组首元素的地址存放到int *的指针变量里面就可以,那&arr呢?该放到哪里?其实他应该放到数组指针的变量里面!因为arr指向数组首元素,也就是指向一个数字,而&arr指向的是整个数组,所以得用数组指针接收即int (*parr) [10] = &arr!

3.3数组指针的使用

数组指针已经学会了定义,那么他该如何使用呢?

既然是数组指针,指向的那即是数组;那数组指针就应该存放数组的地址!

看几个栗子:

  1. int main()
  2. {
  3. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  4. int(*parr)[10] = &arr;
  5. for (int i = 0; i < 10; i++)
  6. {
  7. printf("%d ", *((*parr) + i));
  8. }
  9. return 0;
  10. }

解释一下:

此时parr里面存的是数组arr的地址,也就是指向数组arr,先对其解引用指向数组首元素也就是指向arr,然后正再对他通过i进行偏移来访问数组的每个元素!

不知你有没有感觉这种方法很没必要!直接 *(arr+i)进行访问不就好了吗?其实数组指针不是这样用的!这仅仅说明数组指针可以这样用。其实数组指针是作为形参接二维数组的!

看例子:

  1. void print1(int arr[][5], int r, int c)
  2. {
  3. for (int i = 0; i < r; i++)
  4. {
  5. for (int j = 0; j < c; j++)
  6. {
  7. printf("%d ", arr[i][j]);
  8. }
  9. printf("\n");
  10. }
  11. }
  12. void print2(int(*pa)[5], int r, int c)
  13. {
  14. for (int i = 0; i < r; i++)
  15. {
  16. for (int j = 0; j < c; j++)
  17. {
  18. printf("%d ", *(*(pa + i) + j));
  19. }
  20. printf("\n");
  21. }
  22. }
  23. int main()
  24. {
  25. int arr[3][5] = { 1,2,3,4,5, 6,7,8,9,0, 6,6,6,6,6 };
  26. print1(arr, 3, 5);
  27. printf("-----------\n");
  28. print2(arr, 3, 5);
  29. return 0;
  30. }

这个栗子就是数组指针的作用---->作为形参接受二维数组!

print1就不用多说了,实参是一个二维数组,形参用一个二维数组接收一点问题都没有!

print2实参是一个二维数组的数组名,数组名代表数组首元素的地址,此时也就是&arr[0],我们前面数组那一期说过二维数组是一维数组的数组,可以把二维数组的每一行看做一个一维数组,所以此时实际上传上去的是一个一维数组的地址,接收数组的地址得用数组指针而每一行数组的元素个数是5,所以是int (*pa)[5];然后用它来访问数组元素:pa里面存的是二维数组的第一行的地址对他进行+i进行偏移然后解引用找到每一行的地址,然后加 j 进行偏移到每行每个元素的地址然后进行解引用就可以访问二维数组的每个元素了!

画个图理解一下:

OK,介绍到了这里,我们再来回顾着看看一下代码的意思:

  1. int main()
  2. {
  3. int arr[5];
  4. int* parr1[10];
  5. int(*parr2)[10];
  6. int(*parr3[10])[5];
  7. return 0;
  8. }

int arr[5] ----->arr个是数组5个元素,每个元素int类型

int* parr1[10] -----> parr1先与[]结合说明是一个数组,数组有10个元素,每个元素int *

int(*parr2)[10]-----> parr2先与*结合说是个指针,后面有个[10]说明指向的对象是个数组,有0个元素,每个元素int类型
int(*parr3[10])[5]----->parr3先与[10]结合说明是一个和数组,数组有10个元素每个元素

int(*)[5]类型

四、数组传参和指针传参

在写代码的时候很多时情下要把数组或者指针传给函数,这里就有问题:函数的参数应该怎样设计?下面我们一起来分析一下:

先来看看下面代码,并思考哪个对,哪个不对?

4.1一维数组传参

  1. void test(int arr[])
  2. {}
  3. void test(int arr[10])
  4. {}
  5. void test(int* arr)
  6. {}
  7. void test2(int* arr[20])
  8. {}
  9. void test2(int** arr)
  10. {}
  11. int main()
  12. {
  13. int arr[10] = { 0 };
  14. int* arr2[20] = { 0 };
  15. test(arr);
  16. test2(arr2);
  17. return 0;
  18. }

分析: 

4.2二维数组传参

  1. void test(int arr[3][5])
  2. {}
  3. void test(int arr[][])
  4. {}
  5. void test(int arr[][5])
  6. {}
  7. void test(int*arr)
  8. {}
  9. void test(int* arr[5])
  10. {}
  11. void test(int(*arr)[5])
  12. {}
  13. void test(int** arr)
  14. {}
  15. int main()
  16. {
  17. int arr[3][5] = { 0 };
  18. test(arr);
  19. return 0;
  20. }

分析:

 4.3一级指针传参

我们还是先来看一段代码:

  1. void print(int* p, int sz)
  2. {
  3. for (int i = 0; i < sz; i++)
  4. {
  5. printf("%d ", *(p + i));
  6. }
  7. }
  8. int main()
  9. {
  10. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  11. int* p = arr;
  12. int sz = sizeof(arr) / sizeof(arr[0]);
  13. print(p, sz);
  14. return 0;
  15. }

这是一个简单的打印函数,它的实参是一个指针和一个int类型的变量,他的形参也是一个同类型的指针接收,没问题!我们知道其实它还可以用数组接收!

总结:当一个函数的参数部分是一级指针时,函数可以接收什么类型的参数?

举个栗子:(其他类型的同理)

void test(char* p){}这个函数可以接收什么类型的参数?

1.一维字符数组名

2.一个字符变量的地址(&ch(ch是字符变量))

3.char*的指针变量

4.4二级指针传参

先来看一个二级指针传参数的栗子:
 

  1. void test(int** pstr)
  2. {
  3. printf("%d\n", **pstr);
  4. }
  5. int main()
  6. {
  7. int n = 5;
  8. int* p = &n;
  9. int** pp = &p;
  10. test(pp);
  11. test(&p);
  12. return 0;
  13. }

这也是一个二级指针传参,p是一级指针变量(存的是变量的地址),pp是二级指针变量(存的是一级指针变量的地址)test(pp)和test(&p)是一样的!二级指针传参其实还可以是指针数组!

总结:当一个函数的参数部分是二级指针时,函数可以接收什么类型的参数?

举个栗子(其他类型同理):

void test(char** p){}这个函数可以接收什么类型的参数?

1.取地址一级指针变量(&p (p是一级指针变量))

2.二级指针变量

3.数组指针

五、函数指针

5.1函数指针的定义

我们知道数组指针是指向数组的指针,那函数指针是不是指向函数的指针呢?答案是:是的!

数组指针里面存的是数组的地址,类比一下,函数指针里面存的是不是函数的地址呢?这里你肯定会好奇函数也有地址吗?其实是有的我们在前面有一期函数栈帧的创建与销毁中知道每一个函数都要开辟它自己的栈帧,都得向内存申请空间,那必然就得有地址!我们来验证一下:

  1. void test(int x)
  2. {
  3. x++;
  4. }
  5. int main()
  6. {
  7. int a = 5;
  8. test(a);
  9. printf("%p\n", &test);
  10. return 0;
  11. }

真的拿出了地址!我们再分析,我们知道数组名代表数组首元素的地址,那函数呢?我们来看一看:

果然一模一样!但我还想澄清的一点是其实根本没有函数名就是函数首元素的地址这么一说!他两本质上一个东西!即&函数名==函数名 ,也就是说函数指针可以存&函数名,也可以存函数名!

数组指针是:int (*parr)[10] = &arr; --->*parr先结合说明是指针,指向的是后面的数组有10个元素,每个元素int类型

那函数指针该如何做呢?看下面的一个例子:

  1. int Add(int x, int y)
  2. {
  3. return x + y;
  4. }
  5. int main()
  6. {
  7. int x = 3;
  8. int y = 5;
  9. int(*pf)(int, int) = &Add;
  10. printf("%d", (*pf)(x, y));
  11. return 0;
  12. }

这里的int (*pf)(int, int) = &Add;与上面的数组指针类似,其实它就是函数指针!*先与pf结合说明是指针,指向的对象是后面的函数,指向函数的有两个参数都是int类型,函数的返回值是int

OK,那函数指针该怎样定义呢?其实上面也使用了。我们来一起分析一下:我们知道,数组指针是type (*parr)[const_num] = &arr;同理我们可以类比得出函数指针的定义:type (*pf) (形参列表) =&函数名(函数名); 

5.2函数指针的使用

上面我们已经了解了函数指针,那函数指针该如何使用呢?我们一起来look一look:

  1. int test(int x, int y)
  2. {
  3. return (x > y) ? x : y;
  4. }
  5. int main()
  6. {
  7. int x = 3;
  8. int y = 5;
  9. int (*pf)(int,int) = &test;
  10. int ret = (*pf)(x, y);
  11. printf("%d ", ret);
  12. return 0;
  13. }

pf是函数指针指向函数,所以对其解引用找到函数,函数的两个参数都是int类型xy符合直接传过去,用ret接收返回值!

而我们上面说过函数名==函数的地址所以pf = test,即调用时(*pf) 的这颗星其实是没用的:

到这里函数指针 好像跟没用一样,我直test(x,y);不就好了吗?事实上,我们这里仅仅是举个栗子的,函数指针的真正用处是实现回调函数!不要急下面会详解!

我们介绍到这里就可一看看下面两段代码了!

1.   (* (void(*)() )0 )();

2.  void (* signal(int, void(*)(int)))(int);

这两段代码是什么意思呢?小编第一次看到也是一脸蒙x,哈哈!下面我来为大家介绍一下:

1.   (* (void(*)() )0 )();这段代码我们的入手点只能是0,这里的0可能是整数也有可能是一个编号(指针),经过观察好像是把0给强转了,一看强转的类型是:void (*)()这不就是一个函数指针类型吗!在往外面看,被强转的指针解引用作为参数被调用了!也就是说这段代码实际上是一次函数的调用(调用0地址处的函数)!这段代码其实是出现在《C陷阱与缺陷》:

2.  void (* signal( int, void(*)(int) ) )(int); 这段代码的入手点是signal,他没有被把他和*括起来,说明他是一个名字,往后看与()结合说明会是一个函数名,函数的而两个参数一个是int 类型,一个是函数指针类型,去掉函数值得名字和参数就是返回值类型,也就是说:

void (*)(int);  这其实是函数返回值类型,说到这里就应该很清楚了这是一次函数声明!

但这样声明会不会太过复杂,而且刻可读性太低!我们来改造一下:我们知道typedef能重命名!typedef  void(* ptf )(int) ;注意重命名的名字必须要与*在一块,上面代码转换为:

ptf signal(int, ptf);这样是不是简易很多,可读性也变高了!

六、函数指针数组

我们知道int* arr[10];这样的数组是指针数组,里面的每个元素的类型是int*的指针!函数指针也是指针,应该也能存放到数组中,其实这样的数组就叫函数指针数组!那他该如何定义呢?

  1. void test()
  2. {
  3. return;
  4. }
  5. int main()
  6. {
  7. int(*p)(int, int) =test;
  8. return 0;
  9. }

这是个函数指针的定义,我们在函数指针的定义上进行修改就可以得到函数指针数组!如下:

  1. int main()
  2. {
  3. int(*p[10])(int, int);
  4. return 0;
  5. }

这就是一个函数指针数组!因为[]的优先级比*的高,所以p先与[]结合变成数组,数组有10个元素,去掉数组就是元素类型-->int(*)(int,int);

函数指针的用途:转移表

栗子(计算器)版本1正常实现:

  1. int add(int x, int y)
  2. {
  3. return x + y;
  4. }
  5. int sub(int x, int y)
  6. {
  7. return x - y;
  8. }
  9. int mul(int x, int y)
  10. {
  11. return x * y;
  12. }
  13. int div(int x, int y)
  14. {
  15. assert(y != 0);
  16. return x / y;
  17. }
  18. void menu()
  19. {
  20. printf("****************************\n");
  21. printf("****** 1.add 2.sub *****\n");
  22. printf("****** 3.mul 4.div *****\n");
  23. printf("****** 0.exit **********\n");
  24. printf("****************************\n");
  25. }
  26. int main()
  27. {
  28. int x = 0;
  29. int y = 0;
  30. int input = 0;
  31. int ret = 0;
  32. do
  33. {
  34. menu();
  35. printf("请选择:> ");
  36. scanf("%d", &input);
  37. switch (input)
  38. {
  39. case 1:
  40. printf("请输入操作数x y: ");
  41. scanf("%d %d", &x ,& y);
  42. ret = add(x, y);
  43. printf("add = %d\n", ret);
  44. break;
  45. case 2:
  46. printf("请输入操作数x y: ");
  47. scanf("%d %d", &x, &y);
  48. ret = sub(x, y);
  49. printf("sub = %d\n", ret);
  50. break;
  51. case 3:
  52. printf("请输入操作数x y: ");
  53. scanf("%d %d", &x, &y);
  54. ret = mul(x, y);
  55. printf("mul = %d\n", ret);
  56. break;
  57. case 4:
  58. printf("请输入操作数x y: ");
  59. scanf("%d %d", &x, &y);
  60. ret = div(x, y);
  61. printf("div = %d\n", ret);
  62. break;
  63. case 0:
  64. printf("退出成功!\n");
  65. break;
  66. default :
  67. printf("选择数非法!重新选择!\n");
  68. }
  69. } while (input);
  70. return 0;
  71. }

这个应该是很多人首先想到的,但这不是最好的,这里很多冗余的代码!case 1--case4里面大多都是重复的代码,导致冗余!我们进行对这段代码优化:利用函数指针数组实现!如下:

版本2函数实现:

  1. int add(int x, int y)
  2. {
  3. return x + y;
  4. }
  5. int sub(int x, int y)
  6. {
  7. return x - y;
  8. }
  9. int mul(int x, int y)
  10. {
  11. return x * y;
  12. }
  13. int div(int x, int y)
  14. {
  15. assert(y != 0);
  16. return x / y;
  17. }
  18. void menu()
  19. {
  20. printf("****************************\n");
  21. printf("****** 1.add 2.sub *****\n");
  22. printf("****** 3.mul 3.div *****\n");
  23. printf("********** 0.exit **********\n");
  24. }
  25. int main()
  26. {
  27. int x = 0;
  28. int y = 0;
  29. int input = 0;
  30. int ret = 0;
  31. int(*parr[5])(int, int) = { NULL, add, sub, mul, div };
  32. do
  33. {
  34. menu();
  35. printf("请选择:> ");
  36. scanf("%d", &input);
  37. if (input >= 1 && input <= 4)
  38. {
  39. printf("请输入操作数x y:> ");
  40. scanf("%d %d", &x, &y);
  41. ret = (*parr[input])(x, y);
  42. printf("ret = %d\n", ret);
  43. }
  44. else if (input == 0)
  45. printf("退出成功!\n");
  46. else
  47. printf("操作数非法,重新输入!\n");
  48. } while (input);
  49. return 0;
  50. }

七、指向函数指针数组的指针【了解】

经过上面的介绍,我们应该很容易判断出指向函数指针数组的指针是一个指针,他指向的对象是一个数组,数组每个元素地类型是函数指针!

如何定义?我们下面一起来一步一步的看一看:

  1. void test(const char* str)
  2. {
  3. printf("%s\n", str);
  4. }
  5. int main()
  6. {
  7. //函数指针
  8. void (*pf)(const char*) = test;
  9. //函数指针数组
  10. void (*pfarr[10])(const char*) = { pf };
  11. //指向函数指针数组的指针
  12. void (*(*pf_p)[10])(const char*) = &pfarr;
  13. return 0;
  14. }

这个了解一下就好了,如果您有余力也可以在研究研究!

八、回调函数

8.1什么是回调函数?

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传给另一个函数,当这个指针被用来调用其所针指向的函数时,我们说着就叫回调函数!也就是说,回调函数不是由该函数的是实现方直接来调用的,而是在特定的事件或条件发生时由另一方来调用的,用于对改时间或条件的响应!

8.2回调函数的演示(用qsort):

什么是qsort?

我相信有一部分朋友特别是新手朋友对qsort是没有听过的!不过没关系,小编会带你了解,并且还会带您模拟实现一个!什么是qsort?他其实是快速排序函数,是个库函数,q是quick,sort时分类排序。

我们先来看一下qsort函数:

我们可以清楚地看到,这个函数有4个参数。我们先来看看这个的参数介绍:

第一个参数是void* base显然是一个指针,他是指向被排序数组的第一个对象的指针,为什么是void *的?首先void*是可以接收任意类型数据的指针,原因是函数调用者要传什么类型的指针实现者是无法知道的!因此直接设置成什么都能接收的类型,用的时候再强转!如果不强转不能用,无法解引用和指针+/-不知道跳过多少空间!

第二个参数是size_t num是被base指向数组的元素个数!size_t 是sizeof这个操作符的返回值,实际上是unsigned int 类型!

第三个是size_t szie是要排序的数组中每个元素的字节数

第四个是个函数指针,这个函数指针,指向的函数是来比较两个元素的!这个函数通常叫排序函数(需要自己实现)!我们来看看这个函数:

这个函数也有两个参数都是void*类型的,他这说明说:作为咱叔的 两个指针都得转换成为void*的类型,这个函数定义了返回的顺序!即两个指针强转成所需类型后作差比大小,大于返回大于0的数,小于返回 返回小于0的数,相等返回0!

下面我们就用三个栗子来说明一下:(分别是用qsort排序整型,用qsort排结构体名字和年龄)

  1. int cmp_int(const void* arr1, const void* arr2)
  2. {
  3. return (*(int*)arr1 - *(int*)arr2);
  4. }
  5. void print(int* arr, int sz)
  6. {
  7. for (int i = 0; i < sz; i++)
  8. {
  9. printf("%d ", arr[i]);
  10. }
  11. }
  12. int main()
  13. {
  14. int arr[10] = { 1,3,5,7,9,2,4,6,8,10 };
  15. int sz = sizeof(arr) / sizeof(arr[0]);
  16. qsort(arr, sz, sizeof(arr[0]), cmp_int);
  17. print(arr, sz);
  18. return 0;
  19. }

  1. struct Stu
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. int cmp_name(const void* str1, const void* str2)
  7. {
  8. return strcmp(((struct Stu*)str1)->name, ((struct Stu*)str2)->name);
  9. }
  10. void print(struct Stu* arr, int sz)
  11. {
  12. for (int i = 0; i < sz; i++)
  13. {
  14. printf("%s %d\n", (arr+i)->name, (arr+i)->age);
  15. }
  16. }
  17. int main()
  18. {
  19. struct Stu arr[] = { {"李华",32},{"小黑",8},{"花花",18} };
  20. int sz = sizeof(arr) / sizeof(arr[0]);
  21. qsort(arr, sz, sizeof(arr[0]), cmp_name);
  22. print(arr, sz);
  23. return 0;
  24. }

  1. struct Stu
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. int cmp_age(const void* arr1, const void* arr2)
  7. {
  8. return ((struct Stu*)arr1)->age - ((struct Stu*)arr2)->age;
  9. }
  10. void print(struct Stu* arr, int sz)
  11. {
  12. for (int i = 0; i < sz; i++)
  13. {
  14. printf("%s %d\n", (arr+i)->name, (arr+i)->age);
  15. }
  16. }
  17. int main()
  18. {
  19. struct Stu arr[] = { {"李华",32},{"小黑",8},{"花花",18} };
  20. int sz = sizeof(arr) / sizeof(arr[0]);
  21. qsort(arr, sz, sizeof(arr[0]), cmp_age);
  22. print(arr, sz);
  23. return 0;
  24. }

8.3模拟实现qsort:

我们学过冒泡排序,它的核心想就是交换。我们下面通过冒泡排序来模拟实现一个qsort函数:

在模拟实现这个函数之前,我们先来把问题分析一下,并找到解决的方案我们再来开始写,否则一顿狂敲之后又是一堆bug就不好了!

问题1:单纯的一个冒泡排序只能排序一个类型的数据,例如整型数组就只能排整型的;我们要想排其他的类型就不行了;我们想设计一个能排序所有类型的!

解决方案:我们把模拟实现的bubble_qsort的参数也模仿成和qsort差不多一样的:

bubble_qsort(void*base, int num, size_t size, 比较函数指针);

问题2:我们所熟知的冒泡的比较是前一个和后一个直接比较大小(例如arr[j] > arr[j+1])但如果我们要是比较两个字符串呢?好像就不行了!也就是说我们这个当前的还是局限于一种数据!

解决方案:我们把比较这里封装成一个函数,你要排序哪种数据就把相应的比较函数的指针(地址)给bubble_qsort的比较函数指针部分,然后他就会回调到那个比较函数进行比较!

问题3:如何判断交换?我们上面说了冒泡的核心是交换,我们应该在什么时候进行交换?

解决方案:我们可以和qsort的那个比较函数设计成一样样的,int cmp(const void*arr1,const void* arr2)如果它的第一个不第二个大返回大于0的数(我们可以返回1),第一个不第二个小返回小于0的数(-1),否则返回0!我们就可用比较函数的返回值来进行判断是否交换了!比如升序就得大于0才执行交换!但判断的时候把base转换为哪个类型的呢?我们通过考虑转成char*的最好,但char*的指针的的访问权限只有一个字节,如果过是int就不能正确的判断了,我们可以这样(char*)base + j *size;在与后一个比(char*)base + (j+1) *size;如果比较函数的返回值符合就交换,交换的的时候也是交换(char*)base + j *size和(char*)base + (j+1) *size;

问题4:如何实现交换?

我们可以一个一个字节的交换,我们接收到的是char*的指针,所以直接交换就OK,但交换几个字节?我们不知道,那我们就直接在交换函数的那里把交换的字节数给定,这样就没问题了!

下面看一下代码实现:
 

  1. #include<stdio.h>
  2. #include<string.h>
  3. struct Stu
  4. {
  5. char name[20];
  6. int age;
  7. };
  8. int cmp_age(const void* arr1, const void* arr2)
  9. {
  10. return ((struct Stu*)arr1)->age - ((struct Stu*)arr2)->age;
  11. }
  12. int cmp_name(const void* arr1, const void* arr2)
  13. {
  14. return strcmp(((struct Stu*)arr1)->name, ((struct Stu*)arr2)->name);
  15. }
  16. int cmp_int(const void* arr1, const void* arr2)
  17. {
  18. return *(int*)arr1 - *(int*)arr2;
  19. }
  20. void Swap(char* dest, char* src,int size)
  21. {
  22. char temp = 0;
  23. for (int i = 0; i < size; i++)
  24. {
  25. temp = *dest;
  26. *dest = *src;
  27. *src = temp;
  28. dest++;
  29. src++;
  30. }
  31. }
  32. void bubble_qsort(void* base, int num, size_t size, int (*cmp)(const void*, const void*))
  33. {
  34. int i = 0;
  35. for (i = 0; i < num - 1; i++)
  36. {
  37. int j = 0;
  38. for (j = 0; j < num - 1 - i; j++)
  39. {
  40. if (cmp_int((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
  41. {
  42. Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
  43. }
  44. }
  45. }
  46. }
  47. void print1(int* arr, int sz)
  48. {
  49. for (int i = 0; i < sz; i++)
  50. {
  51. printf("%d ", arr[i]);
  52. }
  53. }
  54. void print(struct Stu* arr, int sz)
  55. {
  56. for (int i = 0; i < sz; i++)
  57. {
  58. printf("%s %d\n",(arr+i)->name,(arr+i)->age);
  59. }
  60. }
  61. void test1()
  62. {
  63. int arr[] = { 1,9,8,2,7,3,6,5,4,10 };
  64. int sz = sizeof(arr) / sizeof(arr[0]);
  65. bubble_qsort(arr, sz, sizeof(arr[0]), cmp_int);
  66. print1(arr, sz);
  67. }
  68. void test2()
  69. {
  70. struct Stu arr[] = { {"李明",20},{"少青",3},{"翠花",18} };
  71. int sz = sizeof(arr) / sizeof(arr[0]);
  72. bubble_qsort(arr, sz, sizeof(arr[0]), cmp_name);
  73. print(arr, sz);
  74. }
  75. void test3()
  76. {
  77. struct Stu arr[] = { {"李明",20},{"少青",3},{"翠花",18} };
  78. int sz = sizeof(arr) / sizeof(arr[0]);
  79. bubble_qsort(arr, sz, sizeof(arr[0]), cmp_age);
  80. print(arr, sz);
  81. }
  82. int main()
  83. {
  84. test1();//整型
  85. //test2();//名字
  86. //test3();//年龄
  87. return 0;
  88. }

OK,本期分享到此结束!好兄弟我们下期再见! 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/650837
推荐阅读
相关标签
  

闽ICP备14008679号