C语言学习(12)-- 进阶指针

本文详细解析了字符指针、数组与指针的区别,介绍了指针数组、数组指针的用法,讨论了数组传参、指针传参的不同方式,并探讨了函数指针、函数指针数组以及回调函数在编程中的应用,以实际例子展示了如何优化代码和减少冗余。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 字符指针

一般来说,字符指针使用char*来表示。在学习的过程中,如果所传的参数是数组,那么指向的其实是数组首元素的地址。在以下例子中,理论上p里面应该存的是4个字符的地址大小,但字符串大小是7个字节。其中不是把字符串赋给p,而是把首字符地址赋给p。

#include<stdio.h>
int main()
{
    char* p = "abcdef";     //不是把字符串赋给p,而是首字符地址赋给p
    printf("%c\n",*p);  //a
    printf("%s\n",p);   //abcdef
    return 0;
}

理论上来讲,那既然是字符串的话,是不是也可以像往常一样,对其中的内容进行更改呢?我们做出了以下尝试。可以发现,我们更改p指向的首字符地址中内容时,报错了。主要是因为字符串“abcdef”是一个常量字符串,不可以进行更改。所以正常来说,使用const char* p = “abcdef”是最合理的。

练习:分析下列程序

char arr1[] = "abcdef";

    char arr2[] = "abcdef";

    char* p1 = "abcdef"; 

    char* p2 = "abcdef";

    if (arr1 == arr2)

    {

        printf("hehe\n");

    }

    else{

        printf("haha\n");

    }

if (p1 == p2)

    {

        printf("hehe\n");

    }

    else{

        printf("haha\n");

    }

答案:

haha

hehe

分析: 首先新建两个数组,两个数组开辟两块不同的空间,因此两个空间地址必然不同。而p1和p2都是常量字符串,不能被更改,因此打印hehe

2. 指针数组

指针数组的本质就是数组,可以通俗理解为存放指针元素的数组。常用场景:

    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};

    int* parr[] = {arr1,arr2,arr3};  //数组名表示的是首元素地址。
    int i = 0;
    for ( i = 0; i < 3; i++)
    {
        int j = 0;
        for ( j = 0; j < 5; j++)
        {
            printf("%d ",*(parr[i] + j));
        }
        printf("\n");        
    }

3. 数组指针

数组指针本质上是指针,是可以指向数组的指针。再复习一下先前学习过的:

arr - 首元素地址

arr[0] - 首元素地址

&arr - 数组地址

那什么是数组指针呢?如果想要存放数组的地址又需要如何表示呢? 

//要把数组的地址存起来 
int arr[10]  = {1,2,3,4,5,6,7,8,9,10};
 int(*p)[10] = &arr;     //数组指针 - p相当于是数组指针的名字,不是*p

    //需要打印出arr中的每个元素
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    for ( i = 0; i < sz; i++)
    {
        printf("%d ",(*p)[i]);
// printf("%d ",*(*p + i)); //p = &arr - *p == arr
    }

分析:

&arr - 数组的地址
int* p = &arr - p是个指向整型的指针,但是&arr是数组,所以不可以用p来存放
int* p[10] = &arr - 这是个指针数组 - 因为[ ]的结合优先性高于*

从程序的优化性来说,数组指针更适合用于二维数组

往常如果我们想要打印出二维数组,最常使用的就是参数为数组的形式。那如果把参数换成指针的形式,应该如何操作呢?

分析:

  • 二维数组本质上可以看作是一维数组。数组名字表示为首元素的地址,于是在二维数组中,数组名字表示第一行元素的地址,而第一行元素中又由5个元素组成。因此创建数组指针p作为是第一行数组指针——int (*p)[5]

在print2()中,使用到了两种打印方式,分别是

  • 方式1:printf("%d ",*(*(p + i) + j));——p+i找到每行元素地址,解引用找到每行元素,加j表示找到每列元素地址,解引用找到元素。
  • 方式2:printf("%d ",(*(p + i))[j]);
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int* p = arr;
    for ( i = 0; i < 10; i++)
    {
        printf("%d ",p[i]);
        printf("%d ",*(p + i));   
        printf("%d ",*(arr + i));
        printf("%d ",arr[i]);
    }

其中p是指向arr的指针,表示数组首元素的地址。arr本身就表示数组首元素地址。于是可以得出 p == arr,第二三句打印指令的结果是一致的。

其次,arr[i] == *(arr + i) == *(p + i) == p[i]。于是方式2的打印语句可以再次调整简化为

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

#include<stdio.h>
//参数是数组的形式
void print1(int arr[3][5], int row, int col)
{
    int i = 0;
    int j = 0;
    for ( i = 0; i < row; i++)
    {
        for ( j = 0; j < col; j++)
        {
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
    
}
//参数是指针的形式
void print2(int (*p)[5], int row, int col)  //相当于是个一维数组
{
    int i = 0;
    for ( i = 0; i < row; i++)
    {
        int j = 0;
        for ( j = 0; j < col; j++)
        {
            printf("%d ",p[i][j]);
            printf("%d ",*(*(p + i) + 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}};
    //打印二维数组
    print1(arr,3,5);
    print2(arr,3,5); 

    return 0;
}

练习:分析以下语句的含义

  •     int arr[5];  —— arr是一个数组,该数组有5个元素,每个元素的类型是int
  •     int *parr1[10]; —— parr1是一个数组,该数组有10个元素,每个元素的类型是int*,因此 parr1是一个指针数组
  •     int (*parr2)[10]; —— parr2是一个指针,该指针指向一个数组,该数组有10个元素,每个元素的类型是int,因此parr2是个数组指针。
  •     int (*parr3[10])[5]; —— parr3 是个数组,该数组有10个元素,每个元素都是一个数组指针,该数组指针有5个元素,每个元素都是int类型。具体图示分析如下。

4. 数组传参、指针传参

在写代码的过程中,难免要把【数组】或者【指针】传给函数,那函数的参数要如何设计呢?

一维数组传参

在移位数组的传参过程中,以下方法都可以使用。可以直接用数组传参,也可以把数组名看作是首元素地址进行指针传参。

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

二维数组传参 

使用数组传参需要注意: 二维数组只能省略行,坚决不能省略列。

使用地址传参的思想,参考(3. 数组指针)中的内容。

void test(int (*arr)[5]){}
void test(int arr[3][5])
int main()
{
    int arr[3][5] = {0};
    test(arr);

return 0;
}

一级指针传参

在一级指针传参中,① 能直接传入变量地址 ② 能传入存放地址的变量。

#include<stdio.h>

void test(int *p){}

int main()
{
    int a = 10;
    int* p = &a;
    test(&a);
    test(p);
    
    return 0;
}

二级指针传参 

在二级指针传参中,和一级指针有相似之处。① 能直接传入变量地址 ② 能传入存放地址的变量。此外,③ 能传入指针数组

#include<stdio.h>
void test(int **p){}
int main()
{

    int* arr [10];
    test(arr);

return 0
}

5. 函数指针

我们知道数组指针是指向数组的指针,那函数指针就是指向函数的指针。在数组指针中&arr ==数组地址 arr == 首元素地址,而在函数指针中&函数名 == 函数名 == 函数地址

定义函数指针和定义数组指针的规律也相似。

  • 返回值类型 (*指针名称)(参数1类型,参数2类型...)= 函数名称
  • 例如:int(*p)(int,int) = Add;

例如:

#include<stdio.h>

void Print(char* str)
{
    printf("%s\n",str);
}

int main()
{
    void (*p)(char*) = Print;  //函数指针
    (*p)("hello,world!");

    return 0;
}

练习:分析以下代码

  • (*(void (*)())0)();    —— 把0强制转换成void (*)( )函数指针类型 - 0就是一个函数的地址,然后调用0地址处的该函数。实际上就是一次函数调用。

  • void (*signal(int, void (*)(int)))(int); —— signal是一个函数声明,有两个参数,一个是int类型,一个是函数指针类型。该函数指针指向的函数参数是int,返回类型是void。signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

 如果按照理解来说,原来的语句应该写成这样的形式void(*)(int)  signal(int, void (*)(int));但实际上来说是不对的。那为了更容易能正确书写,还可以有以下编写方式。把void(*)(int)替换成pfun_t

需要注意的是书写规范。

typedef void(* pfun_t)(int)
pfun_t signal(int,pfun_t) (int)

最后,实际上在函数指针的调用中,无论是否有解引用符号,都是可以的。这里的解引用符号实际上没有作用。 

    int (*pa)(int,int) = Add;
    printf("%d\n",(pa)(2,3));
    printf("%d\n",(*pa)(2,3));

6. 函数指针数组

要把函数的地址存到一个数组中,则这个数组就叫做函数指针数组。函数指针数组本质是数组,元素类型是函数指针。在以下的例子中,四个函数的参数类型相同,返回类型相同,所以都可以用同样的函数指针*pa进行表示。此时就需要一个数组来存放这些函数地址,即函数指针的数组。int (*parr[4])(int,int) = {Add,Sub,Mul,Div}

#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];
    //需要一个数组,这个数组可以存放4个函数的地址 - 函数指针的数组
    int (*pa)(int,int) = Add; // Sub/Mul/Div
    int (*parr[4])(int,int) = {Add,Sub,Mul,Div}; //函数指针数组
    int i = 0;
    for ( i = 0; i < 4; i++)
    {
        printf("%d\n",parr[i](2,3));
    }
    
    return 0;
}

练习:

char* my_strcpy(char* dest, const char* src);

要求:

1. 写一个函数指针pf,能够指向my_strcpy

char* (*pf)(char*,const char*) ;

2. 写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址。

 char*(*pfArr[4])(char*,const char*) ;

实际来说,函数指针数组会常用做转移表。实例:计算器。

#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 XOR(int x,int y)
{
    return x ^ y;
}

void menu()
{
    printf("*****************************\n");
    printf("*****1. add******2. sub******\n");
    printf("*****3. mul******4. div******\n");
    printf("*****5. xor******0. exit******\n");
    printf("*****************************\n");
}


int main()
{
   //实现计算器
    int input = 0;
    int x = 0;
    int y = 0;
    //pfArr 是一个函数指针数组 - 转移表
    int (*pfArr[6])(int,int) = {0,Add,Sub,Mul,Div,XOR};
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d",&input);
        if (input >=1 && input <= 5)
        {
            printf("请输入两个操作数:>");
            scanf("%d%d",&x,&y);
            int ret = pfArr[input](x,y);
            printf("%d\n",ret);
        }
        else if(input == 0){
            printf("退出\n");
        }else
        {
            printf("选择错误\n");
        }

    }while(input);
    
    return 0;
}

7. 指向函数指针数组的指针 

指向函数指针数组的指针本质是一个指针,指向的是一个数组,数组的元素类型都是函数指针。其中ppfArr就是指向函数指针数组的指针。

8. 回调函数

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

对于6中的实例计算器来说的话,能想到的最简单的代码就是用switch...case语句,但是在实现的过程中会有冗余的情况发生。或许想说那不然就把case事件执行的前两句语句分装成函数,但其实还是需要多次重复调用,本质上是没有什么区别的。那到底要怎样解决这样的冗余问题呢?使用回调函数

switch (input)
        {
        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;
        case 5:
            printf("请输入两个操作数:>");
            scanf("%d%d",&x,&y);
            printf("%d\n",XOR(x,y));
            break;
        case 0:
            printf("退出\n");
            break;
        default:
            break;
        }

 其中我们在函数Calc中,把函数指针作为参数传入。很好的解决了代码冗余的问题。

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

    }while(input);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贪睡脑子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值