C语言进阶之路:指针进阶

66 阅读11分钟

C语言进阶之路:指针进阶

1.前言

我们在以前学过了指针的一些基础知识,例如:指针是一个地址,指针变量是用来存放指针的变量,对于指针变量指向的对象可以通过指针直接改变,就是指针的类型:

char*,int*,short*,long*,......

我们还能了解到指针的大小在相同的系统下的大小是相同的(32位平台是4byte,64位平台是8byte),而前面的数据类型是编译器看这个变量指向的对象的方式,像char就是一个一个字节的看,int就是四个四个字节的看

当然今天讲的当然不是这些东西,而是更加深入的东西,那接下来就跟着我的眼睛来学一学吧!

2.指针数组

指针数组顾名思义就是用来存放指针的数组,主语是数组,其结构大概就是

数据类型*+数组名+[最大容纳量]

前面的数据类型是指的数组里的指针的类型,假如数据类型是int就是在指数组里存放的是指针,这些指针指向的对象的类型都是int类型的

例如:

int main()
{
    int a=3;
    int b=2;
    int c=8;
    int* pa=&a;
    int* pb=&b;
    int* pc=&c;
    int* arr[3]={pa,pb,pc};
    //我们就可以借这个数组将这些指针保存在一起
    //也就可以通过数组来改变指针指向的对象
    *arr[0]=0;
    printf("a=%d\n",a);
    return 0;
}

屏幕截图 2024-10-31 092629.png

就像上面那样,我们就可以通过以数组的方式来改变指针指向的对象,这样做的好处是:当代码中的指针的量较多时,可以通过将这些指针放到一个数组中,这样便于修改

3.数组指针

刚刚我们了解到了指针数组,那么接下来我要开始为大家讲解一下数组指针了,数组指针也很好理解,也就是存放数组的指针,主语是指针,指向的是数组的一整个整体

其基本结构是:

数据类型+(*指针变量名)+[指向的数组的最大容量]

这里相较于指针数组就是多了个**( )**,其作用很明显就是提高优先级,防止变量名和后面的[ ]先结合,然后再作为一个整体和 * 再结合,当然正因为有了这个( ),导致了数组指针和指针数组有着本质区别

例如:

int main()
{
    int arr[6]={1,2,3,4,5,6};
    int (*parr)[6]=&arr;//数组指针
    //指针类型是int (*)[6]
    //打印数组   
    for(int i=0;i<6;i++)
    {
        printf("%d ",*(*parr+i));
    }
    printf("\n");
    return 0;
}

屏幕截图 2024-10-31 164815.png

在下面对这串代码进行解释一下:

首先

我们得知道指针的类型就是除了指针变量名之外的部分就是指针的名字,那么在这里,我们就能很好地看出来指针类型是int (*)[6]

然后

我们在之前就已经知道了数组名通常是指数组首元素的地址,而*parr是指的是这个数组的整体,但由于知道了数组的首元素地址就可以知道整个数组,所以我们可以知道*parr这里就是arr,而arr是一个地址,所以还需要再进行一次解引用(*)才是该数组的地址对应的元素,所以这里是*(*parr+i)

但这时候或许有人就有疑问了,那么这样的意义是什么呢?

因为对于这个一维数组,通过正常的用下标也能打印出这个数组的元素,例如:

int main()
{
    int arr[6]={1,2,3,4,5,6};
    for(int i=0;i<6;i++)
    {
        printf("%d ",arr[i]);
    }
    printf("\n");
    return 0;
}

很明显,正常来说,我们打印一个一维数组的时候大多都是用这种方法,因为这种方法更加直接,简便

但事实上,这个数组指针通常是用在二维数组及二维以上数组

void print(int(*parr)[4], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", *(*(parr+i)+j);
        }
        printf("\n");
    }
}

int main()
{
    int arr[3][4]={1,2,3,4,2,3,4,5,3,4,5,6};
    int row = sizeof(arr) / sizeof(arr[0]);
    int col = sizeof(arr[0]) / sizeof(arr[0][0]);
    print(arr)
    return 0;
}

屏幕截图 2024-10-31 202425.png

那我现在我先来给大家解释一下:

首先

为什么接受数组的类型还是int (*)[4]?

我们现在设这个二维数组为arr[3][4],我们知道arr通常表示为首元素地址,在二维数组中,我们可以将二维数组看作几个一维数组的总和,而前面的arr[ ],就是这个一维数组的数组名,所以,我们就能够知道二维数组的首元素其实就是该二维数组的第一行,所以这里的传参的arr就可以认作一个一维数组,在上面我们就说过一维数组是数据类型,所以我们就可以知道这里的形参的数据类型是int (*)[4]

然后

为什么这里的打印的时候用的是***(*([arr+i)+j)**?

我们知道arr表示的是首元素的地址,所以这里的arr表示的就是该数组的第一行,当我们对其进行+1的操作的时候,其就会跳过一行(本质上是因为二维数组是排在一起的,所以就能够计算一行的字节大小,然后在进行跳过),而对arr+i进行解引用的操作,就可以得到一行的首元素的地址,因此,在其后面进行**+j**的操作,我们就可以得到相应的位置,最后在进行一次解引用,就可以得到这些位置上的元素

最后

我们知道,我们正常使用二维数组的时候其实也可以进行上面的操作,例如:

int main()
{
    int arr[3][4]={1,2,3,4,2,3,4,5,3,4,5,6};
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",*(*(arr+i)+j));
        }
    }
    return 0;
}

屏幕截图 2024-10-31 202707.png

但我们平常都是直接用arr[i][j],但是得到的结果和这一样,所以我们就可以知道***(*(arr+i)+j)arr[i][j]**是等价的,所以,我们上面的打印的方法也可以改成我们常见的形式:

printf("%d ",parr[i][j]);

当然除此之外,根据这个原理还可以将其改成这种:

printf("%d ",*(parr[i]+j));

4.一维数组和二维数组的指针传参

当我们看别人代码对数组的传参的时候,我们可以看到各式各样的传参形式

1.一维数组

先看一维数组,例如:

//对于一维数组,我们可以这样
void test1(int arr[10])
{};//对于这种形式毋庸置疑,跟原来的定义中一样,所以显然是有的
void test2(int arr[])
{};//跟上面同理
void test3(int* arr)
{};//对于这种形式,我们也好理解,因为我们传参传的是arr
//arr是通常是首元素的地址,是个指针,所以这里的数据类型是int* 也是可以的
void test4()
{};

int main()
{
    int arr[]={0};
    test1(arr);
    test2(arr);
    return 0;
}

2.指针数组

接着就是指针数组,例如:

void test1(int* arr[])
{};//这里的类型和原本定义的一样,所以在这里就不再解释
void test2(int** arr)
{};//因为arr指的是首元素地址,*arr得到的也是一个指针,所以还要再解引用一次
//所以是int** arr

int main()
{
    int* arr[10]={0};
    test1(arr);
    test2(arr);
    return 0;
}

3.二维数组

最后讲的就是二维数组了,例如:

void test1(int arr[3][4])
{};//这里不多赘述
void test2(int arr[][4])
{};//我们知道二维数组的行可以不定义,但列一定要定义
void test3(int (*arr)[4])
{};//我们通过上面的学习,也了解了数组指针的存在,
//由于传参传的是数组首元素地址,所以这里用数组指针是没问题的

int main()
{
    int arr[3][4]={0};
    test1(arr);
    return 0;
}

5.函数指针

那么接下来,我将给大家讲解在我们日常中较为常用的函数指针

首先

我们要知道函数内存中其实有自己的地址,我就在此演示一下:

int Add(int x,int y)
{
    return x+y;
}

int main()
{
    printf("%p\n",&Add);
    return 0;
}

屏幕截图 2024-11-01 084849.png

这里我们应该能够初步了解函数在内存是有自己的空间的,当然还有就是**&函数名函数名**取的地址都是一样的

屏幕截图 2024-11-01 085311.png

并且不存在像数组那样的数组名表示首元素地址的说法(函数的地址在只读代码区)

然后

毕竟函数有自己的地址,那么其也顺理成章的有相应的指针,其指针的基本结构:

函数最后的返回类型 (*指针变量名)(传参的数据类型(可以有多个))

例如:

int Add(int x,int y)
{
    return x+y;
}

int main()
{
    int (*ad)(int,int)=Add;//&Add也行
    //这里就已经创建好了Add函数相应的指针
    //使用方法:
    printf("%d\n",(*ad)(1,2));
    return 0;
}

屏幕截图 2024-11-01 091726.png

这样我们就成功地通过函数指针来使用函数了,但是我们会想,我们知道了函数名就是一个地址,那函数使用时,都可以直接通过函数名即地址来使用函数,那么这里的ad已经是保存了函数的地址,那能否直接使用指针变量名来使用这个函数吗?

int Add(int x,int y)
{
    return x+y;
}

int main()
{
    int (*ad)(int,int)=Add;
    printf("%d\n",ad(1,2));
    return 0;
}

屏幕截图 2024-11-01 091943.png

经过测试,结果也是显而易见的,所以我们知道了解引用(*)在这里可以当作摆设,所以我们想放多少个解引用(*)就可以放多少

int Add(int x,int y)
{
    return x+y;
}

int main()
{
    int (*ad)(int,int)=Add;
    printf("%d\n",(********ad)(1,2));
    return 0;
}

屏幕截图 2024-11-01 091946.png

这里就很明显就能看到解引用(*)在这里只是摆设,注意:但是假如要使用解引用的话一定要带( )

有人这时候可能就会感觉这样的用法有点多此一举了,明明正常调用函数不就可以了吗?

这样的话,我就要举一个例子了:

创建一个基础的计算器,有加减乘除的功能

//对于这个问题,我们可以使用正常的方法将其写出来

//菜单
void menu()
{
	printf("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n");
	printf("XXXXXXX  1.ADD      2.SUB       XXXXXXX\n");
	printf("XXXXXXX  3.MUL      4.DIV       XXXXXXX\n");
	printf("XXXXXXX  0.EIXT                 XXXXXXX\n");
}
  
//计算机的计算基础
double Add(double x,double y)
{
    return x+y;
}

double Sub(double x,double y)
{
    return x-y;
}

double Mul(double x,double y)
{
    return x*y;
}

double Div(double x,double y)
{
    return x/y;
}

int main()
{
    menu();
    printf("请做出选择:>");
    int choice=0;
    do
    {
        scanf("%d",&choice);
        switch(choice)
        {
            case 1:
                {
                    double x=0,y=0;
                    printf("请输入两位操作数:>");
                    scanf("%lf %lf",&x,&y);
                	printf("%lf\n",Add(x,y));
                	break;
                }
            case 2:
                {
                    double x=0,y=0;
               	 	printf("请输入两位操作数:>");
                	scanf("%lf %lf",&x,&y);
                	printf("%lf\n",Sub(x,y));
                	break;
                }
            case 3:
                {
                    double x=0,y=0;
                	printf("请输入两位操作数:>");
                	scanf("%lf %lf",&x,&y);
                	printf("%lf\n",Mul(x,y));
                	break;
                }
            case 4:
                {
                    double x=0,y=0;
                	printf("请输入两位操作数:>");
                	scanf("%lf %lf",&x,&y);
                	printf("%lf\n",Div(x,y));
                	break;
                }
            case 0:
                {
                    printf("退出\n");
                    break;
                }
            default:
                {
                    printf("输入错误,请重新输入\n");
                    break;
                }
        }
    }while(choice);
    return 0;
}

但我们观察这串代码总会感觉这串代码有点冗余了,所以这时候,我们或许就会想有什么方法可以使这些相同部分放到一个函数中吗?

所以这时候就可以用到我们的函数指针了:

//菜单
void menu()
{
	printf("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n");
	printf("XXXXXXX  1.ADD      2.SUB       XXXXXXX\n");
	printf("XXXXXXX  3.MUL      4.DIV       XXXXXXX\n");
	printf("XXXXXXX  0.EIXT                 XXXXXXX\n");
}

//计算的基础函数
double Add(double x,double y)
{
    return x+y;
}

double Sub(double x,double y)
{
    return x-y;
}

double Mul(double x,double y)
{
    return x*y;
}

double Div(double x,double y)
{
    return x/y;
}

//功能总和
void calc(double (*cal)(double,double))
{
    double x=0,y=0;
    printf("输入两位操作数:>");
    scanf("%lf %lf",&x,&y);
    printf("%lf\n",cal(x,y));
}

int main()
{
    menu();
    printf("请做出选择:>");
    int choice=0;
    double (*parr[4])(double,double)={Add,Sub,Mul,Div};
    do
    {
        scanf("%d",&choice);
        switch(choice)
        {
            case 1:
            case 2:
            case 3:
            case 4:
                {
                    //将这些功能都放到这个函数中
                    //可以大幅度减少冗余问题
                    calc(parr[choice-1]);
                }
            case 0:
                {
                    printf("退出\n");
                    break;
                }
            default:
                {
                    printf("输入错误,请重新输入\n");
                    break;
                }
        }
    }while(choice);
    return 0;
}

当然还有一种更好的方法减少冗余:

//菜单
void menu()
{
	printf("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n");
	printf("XXXXXXX  1.ADD      2.SUB       XXXXXXX\n");
	printf("XXXXXXX  3.MUL      4.DIV       XXXXXXX\n");
	printf("XXXXXXX  0.EIXT                 XXXXXXX\n");
}

//计算的函数
double Add(double x,double y)
{
    return x+y;
}

double Sub(double x,double y)
{
    return x-y;
}

double Mul(double x,double y)
{
    return x*y;
}

double Div(double x,double y)
{
    return x/y;
}

int main()
{
    menu();
    printf("请做出选择:>");
    int choice=0;
    double (*parr[5])(double,double)={0,Add,Sub,Mul,Div};
    do
    {
        scanf("%d",&choice);
        if(choice==0)
        {
            printf("退出\n");
        }
        else if(choice>0&&choice<5)
        {
            double x=0,y=0;
            printf("请输入两个操作数:>");
            scanf("%lf %lf",&x,&y);
         	printf("%lf\n",parr[choice](x,y));   
        }
        else
        {
            printf("输入错误\n");
        }
    }
    while(choice);
    return 0;
}

当我们以后需要将这个进行拓展功能也简单,只需要稍微修改几个字符就解决了

6.总结

对于这些指针,我们在编程中以后也是可以使用起来,尽量使我们的代码更简便,有更高的拓展性,特别是函数指针,其可以大幅度减少冗余的现象