指针复习 ( 下 )

这是指针复习的第二篇,主要介绍指针与数组、函数的应用。

一、指针与数组

1.1 数组名的理解

在说明指针与数组的关系之前,我们必须要先了解「数组名」的意义

我们在指针复习 ( 下 ) 中,指针±整数的部份有写过下面这段代码

 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];

这里我们是使用 &arr[0] 来取得数组首元素的地址,但事实上,数组名本就代表了首元素的地址

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	printf("%p\n", &arr[0]);
	printf("%p\n", arr);
	return 0;
}

在这里插入图片描述

从输出结果可以看到,直接使用数祖名打印地址的结果是和取首元素地址打印的结果相同。这也就印证了我们上面所述,数组名代表了首元素的地址

那如果使用 sizeof(数组名) 结果会是多少呢 ?

如果依照我们在指针复习 ( 下 ) 所说,地址的大小不是 4 就是 8。 那这里的结果是 4 或 8 吗 ?

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	printf("%zd\n", sizeof(arr));
	return 0;
}

在这里插入图片描述

输出结果发现,sizeof(arr) 的结果是整个数组的大小而不是地址大小。因此我们就需要提到两个数组名的例外

  1. sizeof(数组名) : 当 sizeof 中单独放数组名,这里的数组名不是代表首元素的地址,而是整个数组,因此计算的是整个数组的大小,单位为字节。
  2. &数组名 : 这里的数组名也是代表整个数组,取出的是整个数组的地址

除了这两个例外,其他地方如果看到数组名原则上都是代表数组首元素地址

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

在这里插入图片描述

我们前面说, &arr 取出的是整个数组的地址,但是代码中打印地址的结果却又是相同的,那 arr 和 &arr 到底有什么区别 ?

这和我们在指针复习 ( 一 ) 所提到,取出的地址是取出最小字节的地址。但如果我们去对地址进行± 就会体现这两者的不同

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

在这里插入图片描述

也就说明了 &arr 是取出整个数组地址而不是首元素地址,当然往后偏移多少字节是取决于数据类型

1.2 使用指针访问数组

如果我们访问数组内容可以使用**数组名[下标]来进行访问,那可以使用指针变量[下标]** 的方式来访问吗 ?

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    //输⼊
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    //输⼊
    int* p = arr;
    for(i=0; i<sz; i++)
    {
        scanf("%d", p+i);
        //scanf("%d", arr+i);//也可以这样写
    }
    //输出
    for(i=0; i<sz; i++)
    {
        printf("%d ", p[i]); // 也可以写成 printf("%d ",*(p+i));
        // 同理,事实上我们如果打印 arr[i] 相当于 *(arr+i); 
        // 数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的
    }
    return 0;
}

在这里插入图片描述

1.3 一维数组传参的本质

我们知道函数中是可以传入数组的,但是传入的真的是整个数组吗 ?

#include <stdio.h>
void test(int arr[])
{
 int sz2 = sizeof(arr)/sizeof(arr[0]);
 printf("sz2 = %d\n", sz2);
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int sz1 = sizeof(arr)/sizeof(arr[0]);
 printf("sz1 = %d\n", sz1);
 test(arr);
 return 0;
}

输出结果

sz1 = 10
sz2 = 1

我们发现函数内部并没有正确获得数组的元素个数,但我们不是将数组传入了吗 ? 这时就必须要说明一维数组传参的本质了

在本质上,数组传参传递的是首元素的地址,既然传入的是地址,那函数形参理论上也就应该使用指针变量来接收首元素的地址。

因此如果我们 test 函数中测试 sizeof(arr) 得到的会是 4 or 8。而不是数组的大小。

这也就说明了,当我们使用一维数组当做函数的形参时,这个一维数组会退化为指针

以 test 函数为例 形参是 int arr[] 事实上会退化成 int* arr

1.4 指针数组

指针数组是指针还是数组呢 ?

/*
我们称 int a[5] = {0}; 做整型数组 -- 用来存储整型的数组
我们称 char a[] = "abc"; 做字符数组 -- 用来存储字符的数组
因此如果我们写了这样的一行代码
int* a[5] = {...}; 就叫做指针数组 -- 也就是用来存储 「指针」 的数组

*/

在这里插入图片描述

1.5 指针数组模拟二维数组

我们可以把**二维数组看作一维数组的数组,**如此一来,二维数组中的每个一维数组也有首元素地址。

#include <stdio.h>
int main()
{
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};
    //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
    int* parr[3] = {arr1, arr2, arr3};
    int i = 0;
    int j = 0;
    for(i=0; i<3; i++)
    {
        for(j=0; j<5; j++)
        {
            printf("%d ", parr[i][j]);
            // 相当于 printf("%d ", *(*(parr+i)+j));
        }
        printf("\n");
    }
    return 0;
}

它是如何体现的呢 ?

在这里插入图片描述

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素

上述的代码模拟出⼆维数组的效果,实际上并非完全是⼆维数组,因为每⼀⾏并非是连续的

后面会更正式的探讨二维数组传参的本质

1.6 字符指针变量

指针类型中有一个 char*

一般使用

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}

另一种使用方式

int main()
{
 const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 printf("%s\n", pstr);
 return 0;
}

实际上, const char* pstr = "hello bit."; 意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。而不是把整个字符串存放到 pstr 中

1.7 数组指针变量

我们前面有提到,指针数组是用来存放指针的数组。那数组指针呢 ? 数组指针就是用来存放数组的指针 !

// p1 p2 个别是什么 ?
int *p1[10];  // p1先和[10]结合,因此 p1是指针数组
int (*p2)[10]; // p2先和*结合,因此 p2是数组指针
// 这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前说的 &数组名

如果要存放数组的地址

#include <stdio.h>

int main(){
    int arr[10] = {0};
    int (*p)[10] = &arr;
    
    return 0;
}

在这里插入图片描述

我们可以透过调试看到, &arrp 的类型是一样的 !

数组指针的定义

int (*p) [10] = &arr;
 |   |    |
 |   |    |
 |   |    p指向数组的元素个数
 |   p是数组指针变量名
 p指向的数组的元素类型

有了数组指针的概念,我们就可以进一步的探讨二维数组传参的本质了

1.8 二维数组传参的本质

一般情况下,我们要传递二维数组到函数中,是这样写的

#include <stdio.h>
void test(int a[3][5], int r, int c)
{
    int i = 0;
    int j = 0;
    for(i=0; i<r; i++)
    {
        for(j=0; j<c; 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}};
    test(arr, 3, 5);
    return 0;
}

那可以使用其他方式写吗 ?

我们先再次理解一下二维数组

⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组
在这里插入图片描述
根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址。因此形参也可以写成指针的形式

#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
    int i = 0;
    int j = 0;
    for(i=0; i<r; i++)
    {
        for(j=0; j<c; 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}};
    test(arr, 3, 5);
    return 0;
}

二、指针与函数

2.1 函数指针变量

什么是函数指针变量呢 ? 根据前面的说明了这么多指针变量应该不难理解,函数指针变量就是用来存放函数地址的指针

当然,函数名就是函数的地址。因此 &函数名 和 直接写函数名都是可以的两者等价

不过要定义一个函数指针变量就不是像前面一样这么简单了

// 函数指针变量的定义
int      (*pf3)      (int x, int y) // 形参名称可以不写,但是类型和个数都必须和指向的函数相同
 |          |         ------------
 |          |              |
 |          |        pf3指向函数的参数类型和个数
 |      函数指针变量名
 pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
void test()
{
 printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
 return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;

2.2 函数指针变量的使用

#include <stdio.h>
int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int(*pf3)(int, int) = Add;

 printf("%d\n", (*pf3)(2, 3));
 printf("%d\n", pf3(3, 5));
 return 0;
}

输出结果

5
8

使用函数指针变量的作用就是可以透过函数指针去调用目标函数,而不是直接透过函数名调用。

2.3 函数指针数组

把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。要注意函数的类型要是一样的!

函数指针数组该如何定义呢 ?

int (*parr1[3])(); // parr1先和[]结合 说明 parr1是数组 数组的类型就是除掉parr1和[] 也就是 int(*)()
// 因此parr1 是数组,类型是函数指针

2.4 函数指针数组的使用

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a*b;
}
int div(int a, int b)
{
    return a / b;
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
    do
    {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf( "请选择:" );
        scanf("%d", &input);
        if ((input <= 4 && input >= 1))
        {
            printf( "输⼊操作数:" );
            scanf( "%d %d", &x, &y);
            ret = (*p[input])(x, y); // 我们可以直接透过解引用数组中所存储的函数地址,来调用目标函数!
            printf( "ret = %d\n", ret);
        }
        else if(input == 0)
        {
            printf("退出计算器\n");
        }
        else
        {
            printf( "输⼊有误\n" );
        }
    }while (input);
    return 0;
}

2.5 回调函数

回调函数的概念
什么是回调函数呢 ? 回调函数就是通过函数指针调用的函数。
如果我们将某一个函数的指针 ( 地址 ) 作为参数传递给另外一个函数,这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。
回调函数不是由函数实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,对于该事件或条件的响应。
简单说,将A函数地址传递给B,在B函数内通过指针调用A函数实现操作,那么A函数就称为回调函数,也就是不直接调用A,而是通过B调用A。

我们以上面的简易计算机代码为例。除了使用函数指针数组的方式之外,还能不能再简化一些 ?

//使⽤回到函数改造后
#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
void calc(int(*pf)(int, int))
{
    int ret = 0;
    int x, y;
    printf("输⼊操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("ret = %d\n", ret);
}
int main()
{
    int input = 1;
    do
    {
        printf("*************************\n");
        printf("   1:add      2:sub      \n");
        printf("   3:mul      4:div      \n");
        printf("*************************\n");
        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 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);
    return 0;
}
// 这段代码中的 add、sub、mul、div 都可以称作回调函数

三、二级指针

我们都说 指针变量 指针变量。既然它是变量,它就应该要有地址,有地址我就应该可以使用指针来存储。

那存储普通变量 我们使用指针变量。要存储指针变量是不是就应该使用二级指针指针变量 ?

/* 
其实就是这样的概念
存储 int变量 -> 用int*
存储 char变量 -> 用char*

要存储 int*变量 -> 用 int** -- 二级指针

*/

3.1 二级指针视图

在这里插入图片描述

如果对 ppa 解引用则得到的会是pa的地址 ( *ppa)

如果再对 *ppa 解引用 得到的就是 a 的内容 ( **ppa )

我们在指针复习 ( 一 ) 中也有说过,如果希望在函数内部改变函数外部的变量内容,就使用传址传递。

那如果我们希望在函数内部改变一级指针的指向内容呢 ? 也就是把一级指针的地址传到函数当中,此时,形参就要使用二级指针 !

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值