指针进阶(二)

简介: 指针进阶(二)

1.函数指针数组


之前已经讲解过了整型指针数组,用于存放整型指针。那么这里的函数指针数组就是用于存放函数指针的。写法是怎样的呢?


void test()
{
  printf("hehe\n");
}
int main()
{
  void(*pf[10])() = { test };
  return 0;
}


代码的第7行,pf先与[]结合,说明pf是个数组名,将 pf[10]去掉,剩下的部分就是数组的元素类型,即void(*)();。所以pf是个函数指针数组。


1.1函数指针数组的应用实例


当我们想要实现一个对于整数的四则运算的计算器时,我们或许可以这样写:


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 x = 0;
  int y = 0;
  int input = 0;
  int ret = 0;
  do
  {
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf("*************************\n");
  printf("请输入你想做的运算:");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
    printf("请输入操作数:\n");
    scanf("%d %d", &x, &y);
      ret = Add(x, y);
    printf("%d\n", ret);
    break;
  case 2:
    printf("请输入操作数:\n");
    scanf("%d %d", &x, &y);
    ret = Sub(x, y);
    printf("%d\n", ret);
    break;
  case 3:
    printf("请输入操作数:\n");
    scanf("%d %d", &x, &y);
    ret = Mul(x, y);
    printf("%d\n", ret);
    break;
  case 4:
    printf("请输入操作数:\n");
    scanf("%d %d", &x, &y);
    ret = Div(x, y);
    printf("%d\n", ret);
    break;
  case 0:
    printf("退出程序!\n");
    break;
  default:
    printf("您的输入有误,请重新输入!\n");
  }
  } while (input);
  return 0;
}


但是这种写法会导致代码中重复的内容过多。那么能否改进改进呢?


我们观察这四个函数,可以发现他们的返回值和函数参数都是相同的。所以可以将这四个函数存放进一个函数指针数组中,这样在switch语句中调用对应的函数时,就可以使用下标进行快速访问。


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 x = 0;
  int y = 0;
  int input = 0;
  int ret = 0;
  int(*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };
  do
  {
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf("*************************\n");
  printf("请输入你想做的运算:");
  scanf("%d", &input);
  if (input >= 1 && input <= 4)
  {
    printf("请输入操作数:\n");
    scanf("%d %d", &x, &y);
    ret = pf[input](x, y);
    printf("运算结果为:%d\n", ret);
  }
  else if(input==0)
  {
    printf("退出程序!\n");
  }
  else
  {
    printf("你的输入有误,请重新输入!\n");
  }
  } while (input);
  return 0;
}

代码的第23行,这里使用0进行占位,将1,2,3,4这四个下标所对应的位置各自对应相应的函数。


1.2指向函数指针数组的指针


指向函数指针数组的指针是一个指针,其指向一个函数指针的数组。


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


这里注意:函数名代表的是函数的地址,&函数名代表的也是函数的地址。


2.回调函数


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


我们可以通过回调函数的机制,可以将上面的计算器的代码再进行改进。


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("请输入两个操作数:\n");
  scanf("%d %d", &x, &y);
  int ret = pf(x, y);
  printf("%d\n", ret);
}
int main()
{
  int x = 0;
  int y = 0;
  int input = 0;
  int ret = 0;
  do
  {
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf("*************************\n");
  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");
  }
  } while (input);
  return 0;
}


2.1 qsort函数


查看官方文档:


635c56b404d2d0305100e7ea56388b87_ecf64410c12a48a49093058ade9d8b83.png


qsort函数可以对任意数组进行排序,包括结构体数组也可以。qsort函数有四个参数,第一个参数是目标数组的起始位置。第二个参数是数组中元素的个数。第三个参数是数组中每个元素的大小。第四个参数是一个函数,这个函数能够用于比较数组中的两个元素。


注意:这里qsort函数中的最后一个参数是一个函数名,这个函数的两个类型必须是void*类型的。为什么必须是void*类型的呢?


这是因为void*类型指针变量可以接收任意类型的指针变量。因为对于这个函数的设计者而言,他也不知道用户要比较的什么类型的元素,所以就设计了void*这个可以接收任意类型指针的“万能指针变量”,但是void*类型的指针变量不能进行解引用操作,也不能进行加减操作。例如:


//这段代码不会有任何的警告和报错。
int main()
{
  int a = 0;
  char c = 0;
  float f= 1.0;
  void* pv;
  pv = &a;
  pv = &c;
  pv = &f;
  return 0;
}


qsort函数的底层是快速排序算法,假定我们使用qsort函数对整数组中的元素进行排序:(qsort函数默认是升序排列的)


int compare_int(void* e1, void* e2)
{
  return (*(int*)e1 - *(int*)e2);
}
void print(int* p,int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
  printf("%d ", p[i]);
  }
}
int main()
{
  int arr[10] = { 1,2,4,1,6,2,21,4745,64 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(arr, sz);
  printf("\n");
  qsort(arr,sz,sizeof(arr[0]),compare_int);
  print(arr, sz);
  return 0;
}

这里使用冒泡排序模拟实现qsort函数,对乱序的整型数组进行排序。


void swap(char* e1,char*e2,int sz)
{
  char tmp = 0;
  while (sz--)
  {
  tmp = *e1;
  *e1 = *e2;
  *e2 = tmp;
  e1++;
  e2++;
  }
}
int compare_int(const void* e1, const void* e2)
{
  return (*(int*)e1 - *(int*)e2);
}
void myBubble(void* base, int num, int sz, int (*pf)(void*, void*))
{
  int i = 0;
  for (i = 0; i < num - 1; i++)
  {
  int j = 0;
  for (j = 0; j < num - 1 - i; j++)
  {
    if (pf((char*)base +j*sz, (char*)base + sz*(j + 1)) > 0)
    {
    swap((char*)base + j * sz,(char*)base+(j+1)*sz,sz);
    }
  }
  }
}
void print(int* p, int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
  printf("%d ", p[i]);
  }
  printf("\n");
}
int main()
{
  int arr[10] = { 1,0,4,23,55,34,756,754,7,999 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(arr, sz);
  myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int);
  print(arr, sz);
  return 0;
}


这里bubble函数和库函数qsort的参数类型设计的是一样的,这里要特别注意,qsort函数的最后一个参数是一个函数名,这个函数的参数的是void*类型的,也就是说这个函数是用于接收要比较的两个元素的地址的,并不是接收两个函数本身,所以要如何找到这两个要比较的元素的地址呢?可以将base这个参数强制转换为char*类型的,这样子base每执行一次+1操作都会向后移动一个字节,要比较的元素的大小是已知的,为sz。假如要找到bubble函数中找到arr[4]这个元素的地址,就可(char*)base+4*sizeof(int);,base指针向后移动16个字节,就成功找到了arr[4]的地址。


在swap函数中,参数是两个元素的地址,第三个参数是每个元素的大小,要交换这两个元素,只需要交换这两个元素的每一个字节就可以了。


假如要比较结构体数组:(假设以姓名为标准进行比较:)


struct Student
{
  char name[20];
  int age;
};
void swap(char* e1, char* e2, int sz)
{
  char tmp = 0;
  while (sz--)
  {
  tmp = *e1;
  *e1 = *e2;
  *e2 = tmp;
  e1++;
  e2++;
  }
}
int compare_name(const void* e1, const void* e2)
{
  return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
}
void myBubble(void* base, int num, int sz, int (*pf)(const void*,const void*))
{
  int i = 0;
  for (i = 0; i < num - 1; i++)
  {
  int j = 0;
  for (j = 0; j < num - 1 - i; j++)
  {
    if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0)
    {
    swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
    }
  }
  }
}
int main()
{
  struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} };
  int sz = sizeof(arr) / sizeof(arr[0]);
  myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_name);
  return 0;
}


注意:这里字符串的比较是使用的strcmp函数进行比较的。不能直接进行字符串之间的加减操作。


假定以年龄为标准进行比较:


struct Student
{
  char name[20];
  int age;
};
void swap(char* e1, char* e2, int sz)
{
  char tmp = 0;
  while (sz--)
  {
  tmp = *e1;
  *e1 = *e2;
  *e2 = tmp;
  e1++;
  e2++;
  }
}
int compare_int(const void* e1, const void* e2)
{
  return ((struct Student*)e1)->age - ((struct Student*)e2)->age;
}
void myBubble(void* base, int num, int sz, int (*pf)(const void*,const void*))
{
  int i = 0;
  for (i = 0; i < num - 1; i++)
  {
  int j = 0;
  for (j = 0; j < num - 1 - i; j++)
  {
    if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0)
    {
    swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
    }
  }
  }
}
int main()
{
  struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} };
  int sz = sizeof(arr) / sizeof(arr[0]);
  myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int);
  return 0;
}

这里就可以直接对年龄进行相减的操作了。


3.指针和数组的习题组


请预测下面代码的运行结果:


前言:数组名只在两种情况下代表数组名整个数组:其他情况下,数组名代表的是数组首元素的地址。


  1. sizeof(数组名),当数组名单独放在sizeof内部,此时数组名代表的是整个数组。并且计算出的也是整个数组的大小。
  2. &数组名,当数组名被单独放在&符号之前时,此时的数组名代表的是整个数组的地址,数组名代表的是整个数组。


//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
//数组名被单独放在sizeof内部,代表的是整个数组,计算结果是整个数组的大小。
printf("%d\n",sizeof(a+0));//4/8
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,地址的大小为4/8个字节
printf("%d\n",sizeof(*a));//4
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,解引用操作,拿到了int类型的首元素,大小4字节。
printf("%d\n",sizeof(a+1));//4/8
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,+1是第二个元素的地址。为4/8个字节。
printf("%d\n",sizeof(a[1]));//4
//a[1]是数组的第二个元素,大小是4字节。
printf("%d\n",sizeof(&a));//4/8
//&a取出的是整个数组的地址,但也是地址,大小为4/8个字节
printf("%d\n",sizeof(*&a));//16
//对数组名进行&和*操作,得到的最终还是数组名,相当于是将数组名单独放在sizeof内部,计算的是整个数组的大小。
printf("%d\n",sizeof(&a+1));//4/8
//&a取出整个数组的地址,+1操作跳过整个数组,得到一个新地址,但只要是地址,就是4/8个字节。
printf("%d\n",sizeof(&a[0]));//4/8
//a[0]是数组首元素,&a[0]是数组首元素的地址,地址的大小是4/8个字节。
printf("%d\n",sizeof(&a[0]+1));//4/8
//&a[0]是数组首元素的地址,+1操作,得到了数组中第二个元素的地址,大小为4/8个字节。
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
//sizeof(数组名)计算的是整个数组的大小。数组中有6个元素,大小就是6.
printf("%d\n", sizeof(arr+0));//4/8
//数组名没有单独放在sizeof内部,所以代表的是首元素的地址,+0操作,还是首元素的地址,地址就是4/8字节
printf("%d\n", sizeof(*arr));//1
//*arr是数组首元素,数组中的元素都是char类型,所以计算结果为1字节
printf("%d\n", sizeof(arr[1]));//1
//arr[1]是数组中的第二个元素,大小是1个字节。
printf("%d\n", sizeof(&arr));//4/8
//&arr取出的是整个数组的地址,地址的大小是4/8.
printf("%d\n", sizeof(&arr+1));//4/8
//&arr代表的是整个数组的地址,+1操作跳过整个数组,得到一个新的地址,还是4/8个字节。
printf("%d\n", sizeof(&arr[0]+1));//4/8
//&arr[0]是数组首元素的地址,+1操作,得到数组第二个元素的地址,为4/8字节。
//要注意strlen函数的机制,strlen函数会计算从当前地址开始,一直到'\0'之前的所有元素个数
printf("%d\n", strlen(arr));//随机值
//arr是数组的起始地址,但是arr数组中并没有'\0',所以strlen会一直向后计算,知道遇到了'\0'为止。所以是个随机值。
printf("%d\n", strlen(arr+0));//随机值
//也是随机值,和上面的原因一样的
printf("%d\n", strlen(*arr));//报错
//这里将数组的第一个元素的ascll码,即97传过去了,意味着strlen函数要从97地址处开始向后计算,但是该内存空间是不允许被访问的,所以会报错。
printf("%d\n", strlen(arr[1]));//报错
//这里的原因与上一个类似的
printf("%d\n", strlen(&arr));//随机值
//&arr取出整个数组的地址,传递给strlen,由于不知道'\0'的位置,所以会计算出一个随机值。
printf("%d\n", strlen(&arr+1));//随机值
//&arr+1得到的是跳过一个数组之后的地址,由于不知道'\0'的位置,所以计算结果是随机值。
printf("%d\n", strlen(&arr[0]+1));//随机值
//同样的道理,也会计算出一个随机值
char arr[] = "abcdef";//要注意:字符串的末尾会自动添加'\0'。所以这个数组中含有7个元素
printf("%d\n", sizeof(arr));//7
//sizeof(数组名)计算的是整个数组的大小,要注意:字符串的末尾会自动添加'\0',会被sizeof加入计算结果的。
printf("%d\n", sizeof(arr+0));//4
//arr没有单独放在sizeof内部,+0仍然代表的是首元素的地址,大小4/8字节
printf("%d\n", sizeof(*arr));//1
//*arr是数组的首元素,数组是char类型数组,元素的大小都为1字节
printf("%d\n", sizeof(arr[1]));//1
//arr[1]是数组的第二个元素,数组是char类型的数组,大小都是1字节
printf("%d\n", sizeof(&arr));//4/8
//&arr取出的是整个数组的地址,只要是地址,大小就是4/8字节
printf("%d\n", sizeof(&arr+1));//4/8
//&arr取出整个数组的地址,+1操作跳过一个数组得到一个新的地址,大小仍然为4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8
//&arr[0]取出数组首元素的地址,+1操作得到数组第二个元素的地址,只要是地址,大小就是4/8字节
printf("%d\n", strlen(arr));//6
//数组中含有'\0',arr是数组首元素的地址,到'\0'这个字符之前共有6个元素
printf("%d\n", strlen(arr+0));//6
//相同的道理
printf("%d\n", strlen(*arr));//报错
//*arr是将a字符的ascll码值传递给了strlen函数,这块内存空间访问是非法的,报错。
printf("%d\n", strlen(arr[1]));//报错
//arr[1]是将b字符的ascll码值传入了,该空间访问也是非法的,报错
printf("%d\n", strlen(&arr));//6
//&arr是数组的首地址,传入strlen函数时,会被转化为char*类型。得出计算结果为6
printf("%d\n", strlen(&arr+1));//随机值
printf("%d\n", strlen(&arr[0]+1));//5
//&arr[0]+1是第二个元素的地址,向后计算时不会将第一个元素算入,结果为5
char *p = "abcdef";
printf("%d\n", sizeof(p));//4/8
//p是一个指针变量,指向字符串的第一个元素的地址,指针变量的大小是4/8字节
printf("%d\n", sizeof(p+1));//4/8
//p+1是第二个字符的地址,地址的大小是4/8个字节
printf("%d\n", sizeof(*p));//1
//*p是字符串的第一个元素,大小是1字节
printf("%d\n", sizeof(p[0]));//1
//p[0]是字符串的第一个元素,大小也是1字节
printf("%d\n", sizeof(&p));//4/8
//&p是指针变量的地址,&p是个二级指针,大小也是4/8字节
printf("%d\n", sizeof(&p+1));//4/8
//&p+1也是一个二级指针,大小是4/8字节
printf("%d\n", sizeof(&p[0]+1));//4/8
//&p[0]+1是数组第二个元素的地址,大小是4/8字节
printf("%d\n", strlen(p));//6
//p指向的是字符串的第一个元素的地址,strlen能计算出这个字符串中的元素个数,为6
printf("%d\n", strlen(p+1));//5
//p+1地址处开始算,元素个数就是5个
printf("%d\n", strlen(*p));//报错
//*p的值是97,该空间访问是非法的,会报错
printf("%d\n", strlen(p[0]));//报错
//p[0]的值是97,同理,该空间访问非法,会报错
printf("%d\n", strlen(&p));//随机值
//&p是p这个指针变量的地址,由于不知道'\0'的位置,所以计算出一个随机值
printf("%d\n", strlen(&p+1));//随机值
//&p是二级指针,+1操作之后,仍因为不知'\0'的位置,最终计算出一个随机值。
printf("%d\n", strlen(&p[0]+1));//5
//&p[0]+1是数组第二个元素的地址,第一个元素不会被算入,计算结果就是5了
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
//sizeof(数组名)计算的是整个数组的大小,为48字节
printf("%d\n",sizeof(a[0][0]));//4
//a[0][0]是数组中第一行第一列的元素0,大小是4字节
printf("%d\n",sizeof(a[0]));//16
//a[0]是二维数组中第一个一维数组的数组名,计算的是整个第一行的大小,为16个字节
printf("%d\n",sizeof(a[0]+1));//4/8
//a[0]并没有单独放在sizeof内部,所以代表的是二维数组第一个一维数组的第一个元素的地址,也就是int*类型的,再+1操作,代表的是二维数组的第一个一维数组的第二个元素地址,大小是4/8字节
printf("%d\n",sizeof(*(a[0]+1)));//4
//sizeof括号里的内容可以改写为:a[0][1],所以计算结果为4
printf("%d\n",sizeof(a+1));//4/8
//a这个数组名没有单独放在sizeof内部,所以a代表第一个一维数组的地址,+1代表第二个一维数组的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*(a+1)));//16
//sizeof括号里的可改写做:a[1],a[1]是数组第二行元素数组名,被单独放在sizeof内部,计算的是第二行元素的大小。16
printf("%d\n",sizeof(&a[0]+1));//4/8
//&a[0]代表的是第一个一维数组的地址,+1代表的第二个一维数组的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*(&a[0]+1)));//16
//sizeof括号里可以改写:a[1],是第二个一维数组的数组名,被单独放在sizeof的括号里,计算的是第二个一维数组的大小。16
printf("%d\n",sizeof(*a));//16
//*a与a[0]等价,被单独放在sizeof内部,计算的是第一行元素的大小。16
printf("%d\n",sizeof(a[3]));//16
虽然a[3]不存在,但是与sizeof的计算结果无关,sizeof只关心括号里的数据类型。最终将这个数据类型的大小算出来即可。
//a[3]也是一个一维数组的数组名,被单独放在sizeof内部,计算的是一行的元素的大小,为16.


结尾:指针进阶的内容先讲解到这里,后续会继续更新,如有不足,敬请指正!!


相关文章
|
18天前
|
存储
指针进阶详解(下)
指针进阶详解(下)
23 0
|
5天前
|
C语言
C语言进阶:进阶指针(下)
C语言进阶:进阶指针(下)
|
5天前
|
C语言
C语言进阶:指针的进阶(上)
C语言进阶:指针的进阶(上)
|
6天前
入门后指针进阶习题深度分析
入门后指针进阶习题深度分析
16 1
|
6天前
进阶指针笔试题
进阶指针笔试题
14 0
|
11天前
|
存储 C语言 C++
C语言进阶——指针
C语言进阶——指针
|
13天前
|
C语言
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror(下)
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror
6 0
|
13天前
|
安全 C语言
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror(中)
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror
22 0