C语言中多种指针相关类型详解

目录

指针数组

数组指针

arr与&arr的区别

数组指针

1.数组指针的创建格式:

2.数组指针的用处

数组参数,指针参数

一维数组传参

二维数组传参(注意事项)

一级指针传参

二级指针传参

函数指针

函数指针数组

函数指针真正用处用法


指针数组

指针数组是指在一组数中能够存放多个相同指针类型的一组数

指针数组的书写格式:

  • int* * *arr[5];——存放int *类型的数组
  • char* ch[10];——存放char *类型的数组
  • int** pp[5];——存放int** 二级指针类型的数组

当然还可以类比出三级指针,四级指针等等等等....

指针的初始化:

如int* arr[5]={0};

在这里我们将数组arr里的每一个元素赋了0,也就相当于将指针数组里面的所有元素都变为了空指针,这是因为在编译器中,NULL相当于0被强制类型转换为了(void*)类型,所以NULL本质上就是0。

数组指针

数组指针是一种指针类型,是一个指向数组的指针。

数组指针的书写格式:

#include <stdio.h>
int main()
{
    int arr[5]={0};
    char s[5]={0};
    int (*p)[5]=&arr;//这个就是数组指针的格式,
                     //也可以用其他类型如char等
    char (*b)[5]=&s;
    return 0;
}

arr与&arr的区别

创造一个int型数组int arr[5]={0}; 我们都知道arr是数组名,也就是首元素的地址,但&arr表示的是什么?

在编译器中输入以下代码:

#include <stdio.h>
int main()
{
  int arr[5] = { 0 };
  printf("%p\n", arr);
  printf("%p\n",arr + 1);
  printf("%p\n", &arr);
  printf("%p\n", &arr + 1);
  return 0;
}

最终输出结果如下:

观察上面的输出后的图,我们可以看到arr与&arr的地址是相同的,但arr与&arr其实是两个完全不同的概念。

我们可以看到,arr+1后的地址与arr的地址相差了4,这是因为arr是数组名,相当于存放了数组首元素的地址,也就是一个int*类型的指针,所以每加一后都会直接跳过四个字节,读取到数组中下一个元素。而我们再看向&arr与&arr+1的地址,经过计算后得到两地址相差的数值大小为20,我们会发现,这刚好是我们创建数组的所占的总共空间大小(20个字节)。

也就是说,&arr其实取到的并不是数组首元素地址,类型并不为int*指针,而取到的是整个数组的地址,类型为数组指针,所以当我们加一的时候,由于是数组指针加一,所以会跳过20个字节。那么数组指针具体又是什么呢?

数组指针

就像int型会有int指针,char类型有char指针,数组也会有对应的数组指针,例如int型数组,数组指针的书写格式就为int(*)[],具体用法如下:

数组指针的应用

1.数组指针的创建格式:

2.数组指针的用处

Ⅰ:访问一维数组

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

这里虽然访问一维数组用这种方法繁冗且不建议使用,但还是做下解释。

Ⅱ:访问二维数组

如果要写一个函数,将遍历二维数组将每一个元素打印出来,我们会怎么写呢?

在以前会使用下面的方法:

#include <stdio.h>
void print(int a[3][5], int x, int y)
{
  int i = 0;
  int j = 0;
  for (i = 0; i < x; i++)
  {
    for (j = 0; j < y; j++)
    {
      printf("%d ", a[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
  print(arr, 3, 5);
  return 0;
}

但现在,我们可以用数组指针的方式来实现:

#include <stdio.h>
void print(int(*p)[5], int x, int y)
{
  int i = 0;
  int j = 0;
  for (i = 0; i < x; i++)
  {
    for (j = 0; j < y; j++)
    {
      printf("%d ", *(*(p + i) + j));
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
  print(arr, 3, 5);
  return 0;
}

在二维数组中例如arr[3][5],其中的“[3]”所代表的是有3行元素,“[5]”代表的是每行元素有5列。而arr[5]则代表一行的地址

 所以当我们在传参时,只需要将形参定为int型的指针数组,先将每一行的地址传入,就可以实现遍历数组的作用。

再次理解:

#include <stdio.h>
int main()
{
  int a[10] = { 0 };
  int* p = a;
  //*(a+i)==*(p+i)==a[i]==p[i]
}

数组参数,指针参数

一维数组传参

看下面代码,判断传参是否正确:

#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main()
{
    int arr[10]={0};
    int *arr2[20]={0};
    test(arr);
    test2(arr2);
    return 0;
}

一维数组传参经验总结:

  • 在数组传参时可以使用数组形式,而且数组后可以写大小也可不写,因为数组在传参过程中不会创建数组,所以数组大小填错或不填都可以
  • 数组传参传入的数组名是首元素地址,可以用指针来接收

所以上述代码中的传参都是正确的。

二维数组传参(注意事项)

看下面代码,判断传参是否正确:

#include <stdio.h>
void test(int arr[3][5])//ok?  ----1
{}
void test(int arr[][])//ok?    ----2
{}
void test(int arr[][5])//ok?   ----3
{}
void test(int *arr)//ok?       ----4
{}
void test(int (*arr)[5])//ok?  ----5
{}
void test(int **arr)//ok?      ----6
{}
int main()
{
    int arr[3][5]={0};
    test(arr);
}

二维数组传参经验总结:

  • 二维数组传参时可以写成形参二维数组的形式,行可以不写大小,但是列一定要写大小。
  • 在传参时传入的是数组指针类型,所以不能用一级指针接收,也不能用指针数组、二级指针来接收,这样是类型上就不对的了

综上,2,4,6是错误的传参形式。

一级指针传参

Q:当函数的参数为一级指针时,函数能接收什么参数?

A:可以传入一个地址,或数组名,或直接传入一个一级指针。

二级指针传参

Q:当函数的参数为二级指针时,函数能接收什么参数?

A:可以传入一级指针的地址、二级指针、指针数组的数组名。

函数指针

函数不同于数组,函数名与在函数名前加上&本质上是没有区别的,下面是函数指针的一些使用:

#include <stdio.h>
int Add(int x, int y)
{
  return x + y;
}
void Fun(char x)
{
  ;
}
int main()
{
  //Add与&Add本质上没有区别,都表示为同一个地址
  printf("%p\n", Add);
  printf("%p\n", &Add);
  //函数指针的创建举例
  int (*p)(int, int) = Add;
  int (*pa)(int, int) = &Add;
  void (*ppa)(char) = Fun;
  //函数指针的调用也同函数名一样,可以直接用名或者在前面加上解引用符
  printf("%p\n", p);
  printf("%p\n", *p);
  //函数的调用
  printf("%d\n", Add(1, 2));
  printf("%d\n", (&Add)(1, 2));//这里特别注意,要将&Add用小括号括起来,否则Add就会先与后面的(1,2)结合,从而无法调用函数。
  printf("%d\n", p(1, 2));
  printf("%d\n", (*p)(1, 2));//*p也是先要用小括号括起来,否则会与后面的(1,2)结合,从而无法调用函数得到想得到的结果。
  return 0;
}

特别注意:以上面代码为例,若要以&Add或者*p这种形式来使用,要先用小括号括起来,否则Add/p就会先与后面的(1,2)结合,从而无法调用函数。

在学习函数指针时,又偶然发现了一道很经典,非常,特别,诡异的题目:

//请分析下面这段代码:
(*(void(*)())0)();

??????????????????????????????????????????????????????????????????????????????????????

在刚看到这段代码时我的心情如上👆。心想:“魔法,这一定是魔法”。但其实冷静思考过后,最终还是理解了这一段离谱且诡异的代码:

首先从最里开始,void(*)  ()这样单独拆开后我们不难发现,这是一个函数指针类型,只不过是一个void型的函数且不用传参,在这整体上再套多一个小括号并在后面加上数字0,变为(void(*) () )0的样子,这里最外一层的小括号其实是强制类型转换符,将0这个地址上放入一个函数指针(也就是放了一个函数)。而最终变为(*(void(*)())0)(),其实是调用了0地址上的这个参数为无参,返回类型是void的函数。所以总结来说,整个代码是在调用0地址处的函数

但是!!!!

当我们将上面代码在编译器中正式运行时是不可能的,会出现运行中止或报错或根本不运行。这是因为地址0是我们普通用户禁止被使用的地址,我们是无法使用的,因为0地址是开机启动时才访问的地址。

再来看一条简单一点的代码(可能)

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

上面代码要这么看:void(* signal(int,void(*)(int)) )(int);

这样子就不难看出(应该不难)signal(int,void(*)(int))是一个函数的声明,因为函数的参数位置如果是调用的话,则不应该为类型,所以这个是函数的声明,这是一个函数名为signal,参数为int型和函数指针型的函数,函数名有了,参数也有了,现在缺少的就是返回类型,这个返回的类型便是剩下的→void ( *)(int)

Tip:在这里要特别注意,*总是要和名字在一起,不可以写为void ( *)(int) signal(int,void(*)(int))的形式

看吧,不难吧(后仰摊手)

什么??还是难???那好吧,下面我们尝试着简化一下这样的代码(其实我也觉得难)

将void(*)(int)这样类型的函数指针类型重命名为pfun_t,这里要用到typedef但注意!!不是写为这样:typedef void(* )(int) pfun_t。而是要写为:typedef void(* pfun_t)(int) 这里这么写是因为*总要与名字在一起(上面tip里有说过)。

经过函数指针的重命名后,上面的代码可以最终简化为:

pfun_t signal(int,pfun_t);


函数指针数组

按照字面意思,我们可以知道,这是一组存放函数指针类型元素的数组。

函数指针数组的使用:

#include <stdio.h>
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 (*arr[5])(int, int) = { Add,Sub,Mul,Div };
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    printf("%d\n",arr[i](8, 4));
  }
  return 0;
}

函数指针真正用处用法

函数指针可以利用写一个函数,使其形参的类型为函数指针类型,从而达到一个函数可以调用多个不同函数的一个函数。

我们先写下下面这样的一段代码:

#include <stdio.h>
void Menu()
{
  printf("******************************\n");
  printf("****  1.Add      2.Sub    ****\n");
  printf("****  3.Mul 4.Div 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
  {
    int x = 0;
    int y = 0;
    Menu();
    printf("请输入你要的操作");
    scanf("%d", &input);
    switch (input)
    {
      case 0:
        printf("结束操作\n");
        break;
      case 1:
        printf("请输入你想要操作的两个数:");
        scanf("%d %d", &x, &y);
        printf("%d\n", Add(x, y));
        break;
      case 2:
        printf("请输入你想要操作的两个数:");
        scanf("%d %d", &x, &y);
        printf("%d\n", Sub(x, y));
        break;
      case 3:
        printf("请输入你想要操作的两个数:");
        scanf("%d %d", &x, &y);
        printf("%d\n", Mul(x, y));
        break;
      case 4:
        printf("请输入你想要操作的两个数:");
        scanf("%d %d", &x, &y);
        printf("%d\n", Div(x, y));
        break;
      default:
        printf("输入错误,请重新输入");
        break;
    }
  } while (input);
}

在上面我们会发现case1,2,3,4大量代码是重复冗余的,且Add,Sub,Mul,Div函数都是返回的int型,且都有两个int型的参数,对于这种情况,我们可以利用写一个函数,来调用这些形式上差不多相同的函数,能够做到缩减代码量的目的,而调用这些函数以形参形式传过去,就需要用到函数指针类型。

在这里我们创建一个Cal函数,其参数形式为函数指针型,若以上面函数来举例,则写出如下代码:

void Cal(int (*fun)(int,int))
{}

然后再将重复的代码放入Cal函数内之后删去,便可以达到减少重复代码的目的了。下面是完整的更改后的代码:

#include <stdio.h>
void Menu()
{
  printf("******************************\n");
  printf("****  1.Add      2.Sub    ****\n");
  printf("****  3.Mul 4.Div 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 Cal(int (*fun)(int, int))
{
  int x = 0;
  int y = 0;
  printf("请输入你想要操作的两个数:");
  scanf("%d %d", &x, &y);
  printf("%d\n", fun(x, y));
}

int main()
{
  int input = 0;
  do
  {
    Menu();
    printf("请输入你要的操作");
    scanf("%d", &input);
    switch (input)
    {
    case 0:
      printf("结束操作\n");
      break;
    case 1:
      Cal(Add);
      break;
    case 2:
      Cal(Sub);
      break;
    case 3:
      Cal(Mul);
      break;
    case 4:
      Cal(Div);
      break;
    default:
      printf("输入错误,请重新输入");
      break;
    }
  } while (input);
}

看起来比上一种写法干净整洁了许多。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值