C语言的灵魂-->指针的犄角旮旯

简介: C语言的灵魂-->指针的犄角旮旯

一.字符指针


我们都知道字符指针是存放一个字符的地址的,但是请诸位看以下的代码:


1669213949112.jpg


这里是把一个字符串放到pstr指针变量里了吗?通过打印出来的结果,再结合打印%s遇到'\0'就会停止,所以我们猜测pstr存储的应该是字符串首字符的地址,然后向后打印,遇到字符串的\0终止。事实其实的确如此,就是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。


我们再来看一下的代码:


1669213957225.jpg


这串代码的输出结果是什么呢?在笔者看来结果应该是 str1!=str2并且str3!=str4,


1669213965443.jpg


结果却是str1!=str2,str3=str4,那么可以推测str1和str2的地址不相同,而str3和str4的地址相同,那么为什么呢?


这里还是要归结到计算机的存储,在计算机看来,str1和str2是开辟的两个数组,是可修改的,所以地址不可以相同,反过来想,如果地址相同,那么我修改str1的内容,str2的内容也会被修改,这显然是不合适的。那str3和str4又如何解释呢?首先str3和str4存放的是常量字符串首字符h的地址,对于计算机来说,常量字符串一般是不做修改的,所以如果就把相同的常量字符串存到同一块空间,即指针指向同一个地址,这样节省了空间,这也是相当合理的。


二.指针数组


指针数组,顾名思义,就是存放指针的数组。


例如常见的指针数组有:


1669213981068.jpg


常见的应用:


1669213990114.jpg


我们很轻易就可以利用它实现二维数组的功能。


它也可以:


1669213998205.jpg


看到这里,可能有的朋友就要问了,这和二维数组不一模一样了吗?不,它们的区别在于,指针数组所存的三个数组的地址是不连续的,而在二维数组是连续存放的。通过调试可以发现:


1669214006153.jpg


三.数组指针


显而易见,数组指针就是指向数组的指针。


1669214018391.jpg


我们都知道&数组名和数组名指向的都是数组首元素的地址,但是它们的类型和表达的含义是不同的,&数组名是取出整个数组的地址,应该用一个数组指针指向,而数组名仅用一个相同类型的指针指向即可。


1669214025387.jpg


我们发现arr和&arr的地址符合我们所说的,但是arr+1和&arr+1所展现的地址就不相同了,而且我们很惊奇的发现&arr+1和&arr相差恰好12个字节,这是巧合吗?


这里就不得不谈到地址+1 的问题,我们知道地址+1跳过的是这个地址所属类型的大小。所以我们分析一下,就可以发现,arr是数组首元素的地址是int*类型,所以加1跳过4/8(取决于编译环境)个字节,而&arr取出整个数组的地址,它的类型是int*[3],所以+1要跳过整个数组大小。


数组指针的应用:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
    }
}
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

我们发现,学习了数组指针,我们在面对二维数组的时候有了两种传参方式,一种是int arr[3][5],即以数组的形式,另一种是int(*arr)[5],以数组指针的形式传参,也就是我们在有些地方看到的行指针。


四.数组参数


1.一维数组传参


1669214052393.jpg


上面几种传参方式都是可以的,以此我们发现怎么设计传参,本质上是要看传过来的是什么类型。


2.二维数组传参


1669214068992.jpg


对于二维数组,我们要始终谨记,它的数组名表示第一行元素的地址,传参时函数形参的设计只能省略第一个[]的数字,对于一个二维数组,可以不知道它有多少行,但是必须知道它每一行有多少元素,对于更高维的数组也是如此,第一个[]的数字传参时可以省略,其余不能省略。


五.函数指针


我们知道,指针是指向地址的,函数指针,难道函数还有指针?


1669214081053.jpg


显然,函数是由地址的。我们怎么理解呢?


其实,函数指针和数组指针类比是比较相似的。


如果说指向数组的指针是数组指针,那么指向函数的指针就是函数指针.


那么,怎么存储函数的地址呢?表现形式是什么呢?


比如上述的Add函数,怎么存储这个地址呢?


我们是这样表示的:int (*pf)(int,int)=&Add;这里的第一个int是这个函数的返回类型,(*pf)说明它是个指针,括号(int,int)是函数两个参数的类型。


常见用法:


1669214089032.jpg


下面来看两端有趣的代码:


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


这两串代码均出自于《C陷阱与缺陷》这本书,感兴趣的朋友可以去看看。


那么我们先来看一下第一个代码,这段代码我们可以这样解读:


1669214103314.jpg


经过层层剖析,我们可以解读这样看起来比较麻烦的代码。再来看第二个代码


1669214113019.jpg


这样解析可能比较麻烦,那么有没有一种方法可以简化一下呢?我们可以借助typedef重定义来简化。


1669214123362.jpg


笔者写了这些,可能对于大家来说还是决定函数指针比较鸡肋,接下来笔者要说一下函数指针的用途。


①.函数指针的用途


我们可以简易写一个计算器实现加减乘除的功能。


#include<stdio.h>
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, y, ret;
  printf("请输入两个操作数>");
  scanf("%d %d", &x, &y);
  ret = pf(x, y);
  printf("%d\n", ret);
}
  int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  do 
  {
  menu();
  printf("请选择\n");
  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;
  default:
    printf("输入错误,请重新输入\n");
    break;
  }
  } while (input);
  return 0;
}


我们通过回调函数来调用函数的地址,这样可以实现一个简易的计算器,但是当代码量大大增加,需要很多的函数的时候,这时用switch语句会显得比较冗余,那么我们可以借助函数指针数组来更简易实现这个功能。


②.函数指针数组


在之前曾描述过指针数组存放指针,那么显而易见,函数指针数组存放的是函数指针。那么,我们可以借助这个知识,重新写一下这个代码。

#include<stdio.h>
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;
  int x = 0;
  int y = 0;
  int ret = 0;
  int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
  do 
  {
  menu();
  printf("请选择\n");
  scanf("%d", &input);
  if (input == 0)
    printf("退出计算器\n");
  else if (input >= 1 && input <= 4)
  {
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    ret = pfArr[input](x, y);
    printf("%d\n", ret);
  }
  } while (input);//代码大大简化
  return 0;
}


这样代码可读性和去冗余都做得很好。


③回调函数


回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这时回调函数。


之前我们在写一个计算器的时候曾经用过回调函数,这里就不多赘述了。

相关文章
|
19天前
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
30 1
|
19天前
|
存储 程序员 编译器
【C语言】指针篇-简单快速了解指针-必读指南(1/5)
【C语言】指针篇-简单快速了解指针-必读指南(1/5)
|
5天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
4天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
5天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
5天前
|
C语言
C语言指针(3)
C语言指针(3)
9 1
|
5天前
|
C语言
C语言指针(2)
C语言指针(2)
9 1
|
11天前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
11天前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
11天前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)