• c指针进阶


    目录

    char* 指针

    指针数组

     数组指针

    应用

    接收一维数组(不常用)

    接收二维数组

     存放数组指针的数组

     一维数组传参

    二维数组传参

    函数指针

    两段有趣的代码

    函数指针数组

     应用——计算器

    指向函数指针数组的指针 

    回调函数

    qsort

    修改冒泡排序


    char* 指针

    一个char*类型的指针可以接收下面类型的地址

    1. //接收字符
    2. char ch = 'w';
    3. char* pc = &ch;
    4. //接收常量字符串
    5. const char* pa = "abcdef";
    6. //接收数组首地址
    7. char arr[] = "abcdef";
    8. char* p = arr;

    需要注意字符串常量不可改变,所以要用const修饰,正因如此,在遇到一组相同的字符串常量时,编译器会给他们分配相同的地址空间(存放在代码段)

    上面比较的是二者地址大小,可以用strcmp比较字符串大小。

    指针数组

    1. int* arr1[ ];//整形指针数组
    2. char* arr2[ ];//一级字符指针数组
    3. char** arr3[ ];//二级字符指针数组

    结合优先级为数组名和[]结合,然后*与类型结合表示数组里存放指针类型。 

    前面我们用指针数组模拟了一个二维数组,具体做法就是将多个元素相同的数组首元素地址依次存放到指针数组里,再逐个打印。我们也可以用它来输出多个字符串

    1. const char* arr[5] = {"abcdef", "zhangsan", "hehe", "wangcai", "ruhua"};
    2. int i = 0;
    3. for (i = 0; i < 5; i++)
    4. {
    5. printf("%s\n", arr[i]);
    6. }

     数组指针

    数组指针是指针,所以我们要用()将*与变量名结合使之成为一个指针,后面跟[ ]内表示指向对象的下标大小(不可省略

    1. int arr[10] = {1,2,3,4,5};
    2. int (* pa)[10] = &arr;//取出的是数组的地址存放到pa中,pa是数组指针变量
    3. //int(*)[10] -> 数组指针类型
    4. int a = 0;
    5. int* p = &a;
    6. //int* 指针类型

    那数组指针与其他指针有什么区别码?答案当然是有的,我们可以对比一下它们的地址: 

     

    可以发现并没有什么不同,但又是合理的,数组指针和整形指针指向起始位置才能保证访问数据的完整性。但是,起点相同,并不代表走的路相同。

    前二者+1跳过一个整形(4个字节),而&arr直接跳过了40个字节,其实很好理解,因为它的类型是int (*) [10]类型,我们可以把*理解为乘号,他访问字节的能力是 4*10 = 40个字节。 

     arr作为首元素地址有两个例外:

    >>>sizeof(arr) 注意arr传参后为指针类型(4/8字节)

    >>>&arr

    应用

    接收一维数组(不常用)

    1. void print1(int *arr, int sz)//一维数组(指针)传参
    2. {
    3. int i = 0;
    4. for (i = 0; i < sz; i++)
    5. {
    6. printf("%d ", *(arr+i));
    7. }
    8. printf("\n");
    9. }
    1. void print2(int(*p)[10], int sz)//数组指针
    2. {
    3. int i = 0;
    4. for (i = 0; i < sz; i++)
    5. {
    6. printf("%d ", (*p)[i]);//arr;
    7. }
    8. printf("\n");
    9. }

    区别在于是否接收&,对&arr解引用就可以得到数组首地址啦。

    接收二维数组

    1. void print3(int arr[3][5], int r, int c)//二维数组传参
    2. {
    3. int i = 0;
    4. for (i = 0; i < r; i++)
    5. {
    6. int j = 0;
    7. for (j = 0; j < c; j++)
    8. {
    9. printf("%d ", arr[i][j]);
    10. }
    11. printf("\n");
    12. }
    13. }
    1. void print(int (*p)[5],int l,int r)//数组指针
    2. {
    3. for (int i = 0; i < l; i++)
    4. {
    5. for (int j = 0; j < r; j++)
    6. {
    7. printf("%d ", *(*(p + i) + j));
    8. }
    9. printf("\n");
    10. }
    11. }
    12. int main(){
    13. int arr[3][5] = { {1,2,3,4,5},{2},{2,3} };
    14. print(arr,3,5);
    15. return 0;
    16. }

    和一维数组不同,二维数组的数组名表示第一行的地址,也就是说二维数组的首元素就是第一行地址。

     存放数组指针的数组

    int (*parr3[10])[5];

    我们先来拆分一下:去掉数组名,我们可以看到,int (*)[5]是一个数组指针,而parr3是一个包含10个元素的数组

     

     一维数组传参

    我们来分析下面的传参是否合法:

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

    可能最后一个不好理解,因为指针数组的每个元素都是int*类型,而接收一级指针的地址(首元素地址)需要用二级指针。

    二维数组传参

    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)//错误,接收的是arr(整形地址),而非&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);

    函数指针

    顾名思义,函数指针可以通过接收函数名或函数地址将函数的地址用指针保存起来。

    1. int Add(int x, int y)
    2. {
    3. return x + y;
    4. }
    5. int main()
    6. {
    7. int (*pf) (int x,int y) = Add;//可省略参数名,*必须与变量名结合
    8. return 0;
    9. }

    声明类型:int (*) (int,int) 

     对于一个函数,不存在首元素地址这样的概念,所以取地址与否都将直接拿到函数的地址。

    也就是说,我们通过函数指针访问函数的时候可以选择性地使用*操作符。 

    两段有趣的代码

    (*(void (*)())0)();//*表示解引用可以省略

    对于void(*) ( )表示一个函数指针(返回类型为空,参数为空),也就是说,这段代码先将0转换成函数指针类型,再执行(*0)()*必须与0结合)意为调用0地址处的函数

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

     根据分号我们可以该代码是一个函数声明。声明对象是signal,我们将它分成两部分——

    1. signal(int,void (*)(int))//内部
    2. void (*)(int);//外部

    内部:signal函数第一个参数为int,第二个参数为返回类型为void,参数为int函数指针

    外部:signal函数的返回类型也是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void

    我们可以对这段代码稍作修改,增强其可读性:

    1. typedef void (*pf_t)(int);//规定:pf_t只能放在括号里
    2. pf_t(signal(int,pf_t);

    函数指针数组

    作用:可以存放多个返回类型和参数相同的函数指针类型。

     应用——计算器

    加减乘除函数和菜单函数(冗余):

    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. return x / y;
    16. }
    17. void menu()
    18. {
    19. printf("***************************\n");
    20. printf("***** 1.add 2. sub ****\n");
    21. printf("***** 3.mul 4. div ****\n");
    22. printf("***** 0.exit ****\n");
    23. printf("***************************\n");
    24. }

    常规做法:

    1. int main()
    2. {
    3. int input = 0;
    4. int x = 0;
    5. int y = 0;
    6. int ret = 0;
    7. do
    8. {
    9. menu();
    10. printf("请选择:>");
    11. scanf("%d", &input);
    12. switch (input)
    13. {
    14. case 1:
    15. printf("请输入2个操作数:>");
    16. scanf("%d %d", &x, &y);
    17. ret = Add(x, y);
    18. printf("%d\n", ret);
    19. break;
    20. case 2:
    21. printf("请输入2个操作数:>");
    22. scanf("%d %d", &x, &y);
    23. ret = Sub(x, y);
    24. printf("%d\n", ret);
    25. break;
    26. case 3:
    27. printf("请输入2个操作数:>");
    28. scanf("%d %d", &x, &y);
    29. ret = Mul(x, y);
    30. printf("%d\n", ret);
    31. break;
    32. case 4:
    33. printf("请输入2个操作数:>");
    34. scanf("%d %d", &x, &y);
    35. ret = Div(x, y);
    36. printf("%d\n", ret);
    37. break;
    38. case 0:
    39. printf("退出计算器\n");
    40. break;
    41. default:
    42. printf("选择错误\n");
    43. break;
    44. }
    45. } while (input);
    46. return 0;
    47. }

    函数指针数组:

    利用它们函数返回类型和参数一样的性质,我们可以创建一个有函数指针数组来存放这些函数。

    1. int main()
    2. {
    3. int input = 0;
    4. int x = 0;
    5. int y = 0;
    6. int ret = 0;
    7. int (*pfarr[])(int, int) = {0, Add,Sub,Mul,Div };//指针数组,0表示0地址
    8. do {
    9. menu();
    10. printf("请选择:>");
    11. scanf("%d", &input);
    12. if (input == 0)
    13. {
    14. printf("退出计算器\n");
    15. break;
    16. }
    17. if (input >= 1 && input < 5)
    18. {
    19. scanf("%d %d", &x, &y);
    20. int ret = pfarr[input](x, y);
    21. printf("%d\n", ret);
    22. }
    23. else
    24. {
    25. printf("输入错误\n");
    26. }
    27. } while (input);

    运行结果: 

    指向函数指针数组的指针 

    1. int (*parr)(int,int);//函数指针
    2. int (*pfarr[4])(int,int);//函数指针数组
    3. int (*(*ptr)[4])(int,int);//指向函数指针数组的指针

    简单了解一下这个指针:*ptr为指针,它指向外面的数组[4],每个元素都是函数指针类型,像这样的指针被称为指向函数指针数组的指针

    回调函数

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

    对于刚才实现加法器的第一种方法存在重复代码和类型相同的函数,我们可以实现一个回调函数简化代码:

    1. void cal(int(*pf)(int, int))
    2. { int x = 0;
    3. int y = 0;
    4. int ret = 0;
    5. printf("请输入2个操作数:>");
    6. scanf("%d %d", &x, &y);
    7. ret = pf(x, y);
    8. printf("%d\n", ret);
    9. }

    我们用一个函数指针作为参数去接收函数的地址,再通过这个指针在函数体内部进行函数调用这个过程就叫做回调函数。(当调用Add时,Add函数就被称为回调函数)

    1. //简化后的switch case:
    2. switch (input)
    3. {
    4. case 1:
    5. cal(Add);
    6. /*printf("请输入2个操作数:>");
    7. scanf("%d %d", &x, &y);*/
    8. /*ret = Add(x, y);*/
    9. /*printf("%d\n", ret);*/
    10. break;
    11. case 2:
    12. /*printf("请输入2个操作数:>");
    13. scanf("%d %d", &x, &y);
    14. ret = Sub(x, y);
    15. printf("%d\n", ret);*/
    16. cal(Sub);
    17. break;
    18. case 3:
    19. /*printf("请输入2个操作数:>");
    20. scanf("%d %d", &x, &y);
    21. ret = Mul(x, y);
    22. printf("%d\n", ret);*/
    23. cal(Mul);
    24. break;
    25. case 4:
    26. /*printf("请输入2个操作数:>");
    27. scanf("%d %d", &x, &y);
    28. ret = Div(x, y);
    29. printf("%d\n", ret);*/
    30. cal(Div);
    31. break;
    32. case 0:
    33. printf("退出计算器\n");
    34. break;
    35. default:
    36. printf("选择错误\n");
    37. break;

    区别cal(ADD(2,3))和cal(ADD),前者传递的是函数返回值,后者传递的是函数地址。 

    qsort

    qsort函数是c语言内置的排序函数。它的作用是对任意类型数据进行快速排序。它的函数声明如下:

    void* base 

    指向第一个元素的地址,将其转换成void*类型(可以接收任意类型的指针)。在具体使用void*类型的指针时,需要注意:

    不要通过解引用访问指针或者对指针进行加减操作,因为不知道访问多少字节合适。

    1. int a = 0;
    2. void* p = &a;
    3. *p = 10;//err
    4. p++;//err

    如需访问,需先强制转换成具体类型。

    size_t num

    数组元素个数

    size_t size

    获取单个元素所占字节大小。void*类型指针不知道向后偏移多少字节去遍历数组,通过该参数可对不同类型的不同偏移量的数据进行访问。

    int (*compar)(const void*,const void*))

    比较两个元素大小的函数指针。通过自己实现两个数的比较规则来对不同数据类型(字符串,结构体等)体等)进行排序,以达到对多种数据排序的功能。

    当函数返回值>0时,表示p1大于p2,当函数返回值<0时,表示p1小于p2,当函数返回值为0时,表示p1等于p2。此时的排序顺序默认为升序。

    对整形排序:

    1. #include //qsort所属库
    2. int cmp_int(void const* p1, void const* p2)//比较整形
    3. {
    4. return *(int*)p1 - *(int*)p2;
    5. }
    6. void print(int arr[], int sz)
    7. {
    8. int i = 0;
    9. for (i = 0; i < sz; i++)
    10. {
    11. printf("%d ", arr[i]);
    12. }
    13. printf("\n");
    14. }
    15. void test1()
    16. {
    17. int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
    18. int sz = sizeof(arr) / sizeof(arr[0]);
    19. qsort(arr, sz, sizeof(arr[0]), cmp_int);
    20. print(arr, sz);
    21. }

    对结构体排序:

    1. struct student
    2. {
    3. char name[20];
    4. int age;
    5. };
    6. ?*int cmp_s_name( const void* p1, const void* p2)//结构体名字
    7. {
    8. return strcmp(((struct student*)p1)->name, ((struct student*)p2)->name);
    9. }*/
    10. int cmp_s_age(const void* e1, const void* e2)//结构体整形
    11. {
    12. return ((struct student*)e1)->age - ((struct student*)e2)->age;
    13. }
    14. void test2()
    15. {
    16. struct student s[] = { {"zhangsan", 20}, {"lisi", 55}, {"wangwu", 40} };
    17. //按照名字比较
    18. int sz = sizeof(s) / sizeof(s[0]);
    19. }

    注意:(强制类型转换) 优先级低于->操作符。

    qsort(s, sz, sizeof(s[0]), cmp_s_name);//name
    

    qsort(s, sz, sizeof(s[0]), cmp_s_age);//年龄

    修改冒泡排序

    我们知道冒泡排序只能对整形进行排序,通过qsort函数函数传参我们能否得到一点启发,将冒泡排序修改成可以对任意类型进行排序呢?答案是可以的。 

    1. void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
    2. {
    3. int i = 0;
    4. //趟数
    5. for (i = 0; i < sz - 1; i++)
    6. {
    7. //一趟冒泡排序的过程
    8. int j = 0;
    9. for (j = 0; j < sz - 1 - i; j++)
    10. {
    11. //排序逻辑
    12. }
    13. }
    14. }

    对于int*类型的数据,我们可以通过它的偏移量遍数据,而void*类型的数据无法获取具体偏移量,我们又想到,数据在内存中存放的单位是字节,我们可以通过数据所占字节数获取到每个元素。然后对满足要求的数据进行交换我们可以一个字节一个字节交换保证数据的完整性。

    1. void Swap( char* e1, char* e2,int width)
    2. {
    3. int i = 0;
    4. for ( i = 0; i < width; i++)
    5. {
    6. char tmp = *e2;
    7. *e2 = *e1;
    8. *e1 = tmp;
    9. e1++;
    10. e2++;
    11. //如果e1为void*类型,则需先创建新的指针接收强转后的e1再进行++操作
    12. }
    13. }
    14. //内部逻辑
    15. if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
    16. {
    17. //交换
    18. Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
    19. }

    cmp函数指针通过接收不同类型的数据调用相应的回调函数对数据进行比较判断。

  • 相关阅读:
    最短路径:Dijkstra算法及Floyd算法 ← PPT
    【Linux网络(一)初识计算机网络】
    Makefile 学习二:命令和变量
    2022年全球市场电动采光天窗总体规模、主要生产商、主要地区、产品和应用细分研究报告
    手动调用绘图事件
    视图解析器常见功能、处理静态资源、类型转换器
    使用 Python 函数callable和isinstance的意义
    设计模式-day01
    工业交换机常见的故障有哪些?
    VoLTE基础自学系列 | eSRVCC稳态呼叫切换流程
  • 原文地址:https://blog.csdn.net/dwededewde/article/details/132914472