C语言高级指针(数组指针,函数指针)

C语言中指针的运用最为重要,在熟练使用一级指针之后,高级指针便接踵而来,今天就来介绍高级指针。


二级指针 (int ** p)
对于一级指针 int * q 来说,q是一个int * 类型的变量,指向一个 int 类型的变量。对于二级指针 int ** p 来说,p是一个 int ** 类型的变量,指向一个 int * 类型的变量,即 p 指向一个一级指针变量。

#include <stdio.h>

int main()
{
    int **p = NULL;
    int *q = NULL;
    int a = 3;
    q = &a; //q指向a
    p = &q; //p指向q, *p的值为q(即*p=q), 所以*p指向a
    printf("&a=%p, q=%p\n", &a, q); //q指向a
    printf("&q=%p, p=%p\n", &q, p); //p指向q
    printf("a=%d, *q=%d, **p=%d\n", a, *q, **p);
    //*q=a, **p=a 

    return 0;
} 

运行结果如下:
在这里插入图片描述


指针数组 (int * a[5])
指针数组,即指针的数组,是由指针构成的数组,数组中每个元素都是指针变量。

#include <stdio.h>

int main()
{
    int *a[5];//指针数组,每个元素都指向int类型的变量
    int b[5]={1, 2, 3, 4, 5}; 
    for(int i=0; i<5; i++)
    {//把int类型的变量b[i]的地址赋值给a[i]
        a[i] = &b[i];
    }
    for(int i=0; i<5; i++)
    {//通过指针变量a[i]修改b[i]
        *a[i] = 5-i;
    }
    for(int i=0; i<5; i++)
    {//打印数组b中的内容
        printf("%d ", b[i]);
    }   
    printf("\n");
    for(int i=0; i<5; i++)
    {//通过指针变量a[i]打印数组b中的内容
        printf("%d ", *a[i]); 
    }   
    printf("\n");

    return 0;
}

运行结果:
在这里插入图片描述


数组指针 (int ( * p)[5])
数组指针,就是指数组的指针,它是一个指针变量,但是指向的不是一个 int 类型的变量,而是一个数组。
这里,为了理解数组指针,我们先来看一看数组。对于一个一维数组 int a[5], 数组名是a,数组长度是 5,我们知道,a 既是数组名也是数组首元素的地址,记住是首元素,即 a = &a[0],也就是说 a 是指向 a[0] 的。当你定义 int a[5] 时,会在内存中分配一块连续的内存空间,只需要知道第一个变量的地址(a),再加上偏移量(数组下标)就可以访问数组中的任意一个元素。好了,既然 a 是数组首元素的地址,那么什么是数组的地址呢?
对于一个变量,我们直接使用"&“取地址, 对于数组也是如此,直接对数组名使用”&",取的就是数组的地址。

#include <stdio.h>

int main()
{
    int a[5];
    printf("%p %p\n", a, &a);

    return 0;
}

在这里插入图片描述
这里发现,数组的地址和数组的首元素地址是相同的,那么,a 和 &a 是一样的吗?显然不是,a 指向a[0],是int * 类型的;&a 指向一个长度为5的数组,是 int (*)[5] 类型的,它是一个数组指针。那么,指向不同类型数据的指针变量有什么不同呢?
我们通过指针的加减法来理解不同类型的指针变量的不同。先区别一下以下两个概念:
偏移量
指针变量在进行加减法时加上或减去的值就是偏移量,如 (p-i 或 p+i) 表示取指针 p 向左或者向右偏移 i 个偏移单位的地址。
偏移单位
指针变量在进行偏移时,每偏移一个偏移量所偏移的字节个数。

#include <stdio.h>

int main()
{
    int a=3;
    int *p=&a;
    //这里不关心p指向的值,只单纯的对它做加法,看看地址的变化
    printf("int*: %u %u %u\n", p, p+1, p+2);
    //这里观察偏移量为1时、为2时的地址分别是多少
    char b='c';
    char *q=&b;
    printf("char*: %u %u %u\n", q, q+1, q+2);

    return 0;
}

在这里插入图片描述
这里很好的发现,p 是 int * 类型,q 是 char * 类型,它们的偏移量相同,但是偏移单位不同,分别是4个字节和1个字节,所以,指针的类型不同,指针的偏移单位也不同。那么之前的问题也随之而解了。

#include <stdio.h>

int main()
{
    int a[5]={};
    int (*p)[5] = &a; //对p初始化,让其指向数组a
    printf("int a[5]:  %u %u\n", a, a+1);
    printf("int (*p)[5]: %u %u\n", p, p+1);
    return 0;
}

在这里插入图片描述
可以知道,由于 a 指向的是 int 类型的数据,所以偏移单位是4个字节;p 指向的是长度为5的存储 int 类型数据的数组,所以它的偏移单位是5 * 4 = 20个字节。
那么,如何通过数组指针访问数组元素呢?

#include <stdio.h>

int main()
{
    int a[5]={1, 2, 3, 4, 5}; 
    int (*p)[5] = &a; //对数组指针初始化,p和&a的类型是相同的,所以*p可以起到a的作用
    printf("a[3]: %d %d\n", a[3], (*p)[3])
    //p和&a的类型是相同的,所以*p可以起到a的作用
    printf("a[2]: %d %d\n", *(a+2), *(*p+2));
    //a也是指针,a[2]本质上是通过指针访问元素,即 a[2]=*(a+2),同理,(*p)[2]=*(*p+2)
    return 0;
}

在这里插入图片描述
接下来,谈一谈一维数组和二维数组的相同点与不同点。
在定义一个一维数组时(int a[5]) ,会分配一块地址连续的内存块(5 * 4=20字节)。在定义一个二维数组时 (int mat[3][5]),同样分配一个地址连续的内存块(3 * 5 * 4=60字节)。所以,二维数组和一位数组具有同样的物理结构,都可以通过对首元素地址进行偏移来访问元素。

#include <stdio.h>
int main()
{
    int mat[3][5]={};
    for(int i=0; i<3*5; i++)
    {//给数组赋值
        mat[i/5][i%5]=i+1;
    }
    //二维数组也是连续的内存块,取它的首元素地址
    int *m = &mat[0][0];
    for(int i=0; i<3*5; i++)
    {//通过首元素地址访问
        printf("%d\t", m[i]);
        i%5==4 && printf("\n");
    }   

    return 0;
}

在这里插入图片描述

但是它们也有不同点。对于一维数组 int a[5], a[i] = * (a+i),即一维数组名是一维数组首元素地址;对于二维数组 int mat[3][5],mat[i][j] = * ( * (mat+i)+j),这里发现,它和之前的数组指针的写法相同,* ( * p+i)= * (* (p+0)+i)。所以,对于一个二维数组数 int mat[3][5],它的数组名 mat 是 int ( * )[5] 类型的。
二维数组是包含若干个一维数组的一维数组,在内存中是连续的。它的数组名是数组指针类型,对数组名加1(mat+1),则地址会偏移一个一维数组,此时 mat+1 指向行下标为1的一维数组,* (mat+1) 指向行下标为1的一维数组的首元素,(* (mat+1)+1)指向这个一维数组中下标为1的元素,* (* (mat+1)+1)为这个元素的值。

#include <stdio.h>

int main()
{
    int mat[3][5]={};
    int (*p)[5]=mat;
    //p指向长度为5的一维数组,p的偏移单位是一维数组的内存长度
    for(int i=0; i<3*5; i++)
    {//给数组赋值
        mat[i/5][i%5]=i+1;
    }
    for(int i=0; i<3; i++)
    {//通过p访问元素
        for(int j=0; j<5; j++)
        {
            printf("%d\t", *(*(p+i)+j));
            //也可以写作p[i][j]
        }
        printf("\n");
    }
    return 0;
}

看到这里,发现对于任何指针来说,我们必须明确它的偏移单位,这样才能在使用时不出错。

#include <stdio.h>

int main()
{
    int a=0;

    int *p1 = &a; 
    printf("%u %u\n", p1, p1+1);
    //指针p1的偏移单位是1个int数据的长度,是4个字节

    int **ps;
    printf("%u %u\n", ps, ps+1);
    //指针ps的偏移单位是1个int*数据的长度,是4或8个字节

    int (*p2)[1];
    printf("%u %u\n", p2, p2+1);
    //指针p2的偏移单位是1个int数据的长度,是4个字节

    int (*p3)[0];
    printf("%u %u\n", p3, p3+1);
    //指针p3的偏移单位是0个int数据的长度,无论偏移量多少,都不偏移

    return 0;
}

在这里插入图片描述


指针函数
指针函数是一个函数,这个函数的返回值是一个指针。例如:char * strcpy(char * dest, const char * src); 这是C语言的字符串拷贝函数,它在拷贝结束后将拷贝完成的字符串的地址return返回。指针函数并不难理解,就不介绍了。


函数指针
函数指针是一个指针,这个指针指向一个函数。定义一个函数指针变量时,需要明确指针指向函数的类型(函数返回值类型,函数形参类型)。当让函数指针指向一个函数后,就可以通过函数指针调用函数了。
这里要知道,定义一个函数后(是定义不是声明),函数名就是函数的地址,在调用函数时,需要通过函数地址进入函数。

#include <stdio.h>

void func(void)
{
    printf("function!\n");
}
int bar(void)
{
    printf("bar!\n");
}
void goo(int a)
{
    printf("goo!\n");
}

int main()
{
	//打印func()函数的地址
	printf("%p\n", func);
	//定义函数指针变量pf,指向的函数返回值为void,形参列表为void
    void (*pf)(void) = func;//让pf指向相同类型的
    //定义函数指针pb,指向的函数返回值为int类型,形参列表为void
    int (*pb)(void) = bar;
    //定义函数指针pg,指向的函数返回值为void,形参列表只有一个形参,为int类型
    void (*pg)(int) = goo;
	//调用函数
    pf();
    pb();
    pg(1);

    return 0;
}

在这里插入图片描述
我们还可以声明函数指针数组,即一个数组中的元素是一类相同的函数指针。

#include <stdio.h>

void f1(void)
{
    printf("f1\n");
}
void f2(void)
{
    printf("f2\n");
}
void f3(void)
{
    printf("f3\n");
}
void f4(void)
{
    printf("f4\n");
}
void f5(void)
{
    printf("f5\n");
}

int main(){
    //给函数指针取别名PF
    typedef void (*PF)(void);
    PF arr[5]={f1,f2,f3,f4,f5};
    //void (*parr[5])(void) = {f1,f2,f3,f4,f5}也是一种声明的方法
    for(int i=0; i<5; i++)
    {//调用函数
        arr[i]();
    }
    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值