【C语言详解】一步一步带你手撕指针 ~ You got it?(下)

简介: 【C语言详解】一步一步带你手撕指针 ~ You got it?(下)

🌅 一级指针传参


#include <stdio.h>
void print(int *p, int sz) //一级指针传参,一级指针接收
{
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0; }


一级指针传参,形参可以用一级指针接收,也可以用数组接收(但不推荐)


思考:


当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

比如:


void test1(int* p)//test1函数能接收什么参数?
{
  //...
}
int main()
{
  int a = 10;
  int* p = &a;
  int arr[10];
  test1(arr);
  test1(&a);
  test1(p);
  return 0;
}


分析如下:


0a2653c851af460fa595bd959398a8f1.png


形参为一级指针,实参可以是数组,也可以是一级指针(地址)


🌅 二级指针传参


void test(int** ptr) //二级指针接收
{
  printf("num = %d\n", **ptr);
}
int main()
{
  int n = 10;
  int* p = &n;
  int** pp = &p;//ppa是一个二级指针
  test(pp);//二级指针
  test(&p);//取地址一级指针,类型为二级指针
  return 0;
}


二级指针传参时,形参最好用二级指针来接收(指针数组不推荐)


思考:

当函数的参数为二级指针的时候,可以接收什么参数?

比如:


void test(char** p) {
}
int main()
{
  char c = 'b';
  char* pc = &c;
  char** ppc = &pc;
  char* arr[10];
  test(&pc);//取地址一级指针,类型是二级指针
  test(ppc);//传的是二级指针
  test(arr);//Ok?    ok  因为arr数组名是首元素,char*的地址——类型为char**
  return 0;
}


提问:如果arr2是个二维数组,可以吗?


不可以,要写成char(*p)[5]才可以。



形参为二级指针时,传参可以是二级指针,也可以是数组指针首元素的地址


🐾函数指针


🌅 函数指针的定义


数组指针:是指向数组的指针。

函数指针:类比可知是指向函数的指针,存放函数地址的指针。


int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int arr[10];
  int(*p)[10] = &arr;//p是一个数组指针变量
  printf("%p\n", &Add);
  printf("%p\n", Add);
}


我们可以得知:函数名 == &函数名(完全等价)

都可以用来表示函数地址


数组名 != &数组名(完全不同)

函数名 == &函数名(完全等价)


🌅 函数指针的类型


了解了函数指针的定义,那么函数指针类型该怎么样写呢?


int (*pf)(int x, int y) = &Add;//函数指针变量


分析:

接下来,我们试一下这个:int test(char* str)的函数指针咋写?


int test(char* str)
{}
int main()
{
  int (*pf)(char*) = &test;
}


那我们的函数的地址要想保存起来,怎么保存?

下面我们看代码:


void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();


首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是:


pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。


阅读两段有趣的代码:


//代码1 
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);


我们慢慢把代码逐层剖析:


0a2653c851af460fa595bd959398a8f1.png


代码1: ①:首先是把0强制类型转换成一个函数指针类型,这就意味着0地址处放着一个返回类型是void,无参的一个函数

②:调用0地址处的这个函数


2d65d23f6d4748949b924e4057485923.png


上面的代码是不是太废眼睛了呢?我们接下来用重定义typedef简化一下吧:


typedef void(*pf_t)(int);
  //给函数指针类型void(*)(int)重新起名叫:pf_t
  pf_t signal(int, pf_t);
  //替换后等效于void (*signal(int, void(*)(int)))(int)


注 :推荐《C陷阱和缺陷》,这本书中提及这两个代码。


🐾函数指针数组


🌅 函数指针数组的定义


数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,

比如:


int *arr[10];
//数组的每个元素是int*


那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?


int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];


答案是:parr1

parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢?

是 int (*)( ) 类型的函数指针。


函数指针数组的用途:转移表


🌅 函数指针数组的实现


例子:(计算器)


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;
    do{
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        printf("请输入2个操作数:> ");
        scanf("%d %d", &x, &y);
        switch (input)
        {
        case 1:
            ret = Add(x, y);
            break;
        case 2:
            ret = Div(x, y);
            break;
        case 3:
            ret = Mul(x, y);
            break;
        case 4:
            ret = Div(x, y);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("重新选择\n");
            break;
        }
        printf("ret = %d\n", ret);
    } while (input);
    return 0;
}


我在测试中发现了,有bug!

0a2653c851af460fa595bd959398a8f1.png


当input等于0和5的时候,发现程序出错了


接下来我们对此进行修改:

把以下代码加入到switch语句的case中:


printf("请输入2个操作数:> ");
 scanf("%d %d", &x, &y);


void menu()
{
  printf("**************************\n");
  printf("****  1.add   2.sub   ****\n");
  printf("****  3.mul   4.div   ****\n");
  printf("****  0.exit          ****\n");
  printf("**************************\n");
}
int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  do
  {
  menu();
  printf("请选择:>");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
    printf("请输入2个操作数:>");
    scanf("%d%d", &x, &y);
    ret = Add(x, y);
    printf("ret = %d\n", ret);
    break;
  case 2:
    printf("请输入2个操作数:>");
    scanf("%d%d", &x, &y);
    ret = Sub(x, y);
    printf("ret = %d\n", ret);
    break;
  case 3:
    printf("请输入2个操作数:>");
    scanf("%d%d", &x, &y);
    ret = Mul(x, y);
    printf("ret = %d\n", ret);
    break;
  case 4:
    printf("请输入2个操作数:>");
    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;
}


但是我觉得还不够好:


代码冗余,出现了多次的printf 、scanf(每个case中都有)

可读性低

对此我们利用函数数组指针来优化,通过函数数组的下标来进行跳转到目标函数:


void menu()
{
  printf("**************************\n");
  printf("****  1.add   2.sub   ****\n");
  printf("****  3.mul   4.div   ****\n");
  printf("****      0.exit      ****\n");
  printf("**************************\n");
}
int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  //转移表
  int (*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};
    //pfArr就是函数数组指针
  do
  {
  menu();
  printf("请选择:>");
  scanf("%d", &input);
  if (input == 0)
  {
    printf("退出计算器\n");
  }
  else if(input >= 1 && input<=4)
  {
    printf("请输入2个操作数:>");
    scanf("%d%d", &x, &y);  
    ret = pfArr[input](x, y);
    printf("ret = %d\n", ret);
  }
  else
  {
    printf("选择错误\n");
  }
  } while (input);
  return 0;
}

0a2653c851af460fa595bd959398a8f1.png

上面应用了函数指针数组,通过函数数组的下标来进行跳转到对应的目标函数

我们把这种函数指针称为转移表


🐾指向函数指针数组的指针


指向函数指针数组的指针是一个 指针

指针指向一个 数组 ,数组的元素都是 函数指针 ;

如何定义?


void test(const char* str) {
  printf("%s\n", str);
}
int main()
{
  //函数指针pfun
  void (*pfun)(const char*) = test;
  //函数指针的数组pfunArr
  void (*pfunArr[5])(const char* str);
  pfunArr[0] = test;
  //指向函数指针数组pfunArr的指针ppfunArr
  void (*(*ppfunArr)[5])(const char*) = &pfunArr;
  return 0;
}


🐾回调函数


🌅回调函数的定义


回调函数就是一个通过函数指针调用的函数


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


举个简单的例子:


void test()
{
  printf("hehe\n");
}
void print_hehe(void (*p)())//函数指针接收,该形参与test函数类型相同
{
  if (1)
  p();
}
int main()
{
  print_hehe(test);
  return 0;
}


图例分析如下:


0a2653c851af460fa595bd959398a8f1.png


把test的地址传给了print_hehe函数,通过print_hehe函数来调用test函数,所以我们称print_hehe函数为回调函数


🌅回调函数的应用

我们拿switch版本的计算机来实践:


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;
}
void Calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    printf("请输入2个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", pf(x, y));
}
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;
}


把冗余的代码封装成一个Calc函数。

把输入的函数地址传给Calc,Calc通过传入的地址,从而找到了目标函数


🐾sqort函数


🌅sqort函数的定义


sqort是一个包含在<stdlib.h> 头文件下的库函数,主要根据一定的比较条件进行快速排序。

也可以对所有类型的数据进行排序,一个函数解决所有类型的排序问题,不需要根据不同的类型些不同的函数,提高效率!🔥


接下来我们来回顾一下冒泡排序:


void bubble_sort(int arr[], int sz)
{
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] = { 9,8,7,6,5,4,3,2,1,0 };//  排序为升序
    int sz = sizeof(arr) / sizeof(arr[0]);
    print_arr(arr, sz);
    bubble_sort(arr, sz);
    print_arr(arr, sz);
    return 0;
}


我们发现:冒泡排序只能对整数进行排序,而不能对其他其他类型的数据进行排序。


对此我们来看看qsort这个函数:



void qsort(void *base,
           size_t num,
           size_t width,
           int (*cmp)(const void *, const void*))


(1):base 👉 待排序数组首地址(可直接输入待排序数组名,或是指向数组的指针)

.


(2):num👉数组中待排序元素数量(可以用sizeof()来求)

.

(3):width👉 各元素的占用空间大小(可以用sizeof(arr[0])来求)

.

(3):cmp 👉 用来比较待排序数据种两个元素的函数。如果,返回的是大于0的数字表示第一个元素大于第二个元素、等于0的话就是表示第一个元素等于第二个元素、小于0的话就是第一个元素小于第二个元素。


ps:程序员们一定要学会看英文资料,对此我们可以下载一个MSDN(可以私聊我拿安装包哦),里面会有各种函数的解析(英文),以后的工作也会接触到英文资料等,加油吧!


那么为什么qsort函数可以排序多种数据类型呢❓


首先:因为void* base指针和num(待排序数组的元素个数)、size(待排序数组的元素大小)可以描述出任意类型。


眼尖的同学会发现👉参数base的类型是viod*


因为void*“海纳百川”,无具体类型,它可以接收任意类型的指针。


接下来我们来看*cmp函数:


这个比较函数指定元素的比较方式,要求使用者自行定义函数。


elem1小于elem2,返回值小于0

elem1大于elem2,返回值大于0

elem1等于elem2,返回值为0


🌅sqort函数的使用


题目:将arr数组当中元素进行排序,用qsort函数实现!


int int_cmp(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");
}
int main()
{
  int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  qsort(arr, sz, sizeof(arr[0]), int_cmp);
  print_arr(arr, sz);
  return 0;
}


结果:


0a2653c851af460fa595bd959398a8f1.png


排序的整型数据:用> <

排序的结构体数据:需要使用者提供一个函数,实现两个数据的比较

那如何用qsort排序结构体呢?


#include<stdlib.h>
#include<string.h>
#include<stdio.h>
struct Stu
{
  char name[20];
  int age;
};
int sort_by_age(const void* e1, const void* e2)//年龄
{
  return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;//升序
}
int sort_by_name(const void* e1, const void* e2)//名字
{
  return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) -> name);//升序
}
int main()
{
  struct Stu stu[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20} };
  //按年龄来
  qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_age);
  //按名字来
  qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_name);
  return 0;
}


e1 - e2为升序;e2 - e1为降序


我们摸透了sqort接下来模拟实现一下吧


🌅sqort函数的模拟实现


(采用冒泡的方式)


void swap(char* buf1, char* buf2, int width)
{
    for (int i = 0; i < width; i++)
    {
        char t = *buf1;
        *buf1 = *buf2;
        *buf2 = t;
        buf1++;
        buf2++;
    }
}
int cmp_int(const void* e1, const void* e2)
{
    return (*(int*)e1 - *(int*)e2);//void强制类型转换为元素的地址的类型
}
void bubble_sort(void* base, int num, int width,int(*cmp)(const void* e1, const void* e2))
{
    for (int i = 0; i < num - 1; i++)
    {
        for (int j = 0; j < num - 1 - i; j++)
        {
            //if(arr[j]>arr[j+1])比较            
            if (cmp((char*)base + (j)*width, (char*)base + (j + 1) * width) > 0)
            {
                //交换
                // int t=arr[j];
                // arr[j]=arr[j+1];
                // arr[j+1]=t;
                swap((char*)base + (j)*width, (char*)base + (j + 1) * width,width);
            }
        }
    }
}
void print_arr(int arr[], int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main()
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//0 1 2 3 4 5 6 7 8 9
    print_arr(arr, sz);
}


因为base是void*类型 ,并且语法不支持(指针±整数),而char类型是所以类型中的最小的。所以(char)base + j * size可以确定元素地址。


📢写在最后


能看到这里的都是棒棒哒🙌!

想必指针也算是C语言中最难🔥的部分了,如果认真看完以上部分,肯定有所收获。

指针内容比较难懂,我们一定多敲代码,都说了2w行代码才算入门了C语言!你写了多少行呢?

接下来我还会继续写关于📚《指针练习题目》等…

💯如有错误可以尽管指出💯

🥇想学吗?我教你啊🥇

🎉🎉觉得博主写的还不错的可以一键三连撒🎉🎉


相关文章
|
16天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
41 0
|
15天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
11 2
|
16天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
16天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
22天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
22天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
22天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
22天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
22天前
|
C语言
C语言指针(2)
C语言指针(2)
12 1
|
15天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
13 0