C语言入门——指针初阶(2)

在上篇内容中,我们学习了关于指针变量、野指针和部分指针运算的知识,对于指针有了一个初步的了解。本篇文章中将延续上文展开剩余篇幅。

1.指针的关系运算

指针的关系运算实际上就是指针比较大小(地址比较大小)

当我们想要打印一个数组的内容的时候,通常会这么做

#include <stdio.h>

int main()
{
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

但是当我们学了指针的关系运算之后,我们就可以将指针的比较作为循环条件来打印数组

#include <stdio.h>

int main()
{
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int *p = &arr[0];
    while (p < arr + sz)
    {
        printf("%d ", *p);
        p++;
    }
    return 0;
}

2.const修饰指针

2.1 const修饰变量

我们知道,变量是可以被修改的。但是有时候在程序中我们并不希望创建的变量被修改,此时就可以用const修饰这个变量

上图中,变量x没有被const修饰,所以可以后续进行修改数值;变量y被const修饰后,再对它进行修改就会报错。

虽然我们无法修改被const修改的变量y,但是const仅仅是在语法上做了限制,y的本质还是变量,所以习惯上,我们称呼y为常变量。

但是尽管const修饰了变量y,我们还是可以通过指针使用y的地址去修改y的数值

2.2 const修饰指针变量

接上文,如果我们要想避免别人通过地址来修改被const修饰的变量的数值,就要用const将指针变量也修饰了

此时*p被const修饰后,再想通过地址修改变量y的值就会报错

另外的,const可以放在 * 的前面,也可以放在 * 的后面

1.const int *p

2.int const *p

3.int * const p

1和2中,const都在 * 的左边,此时修饰的是*p,也就是此时我们不能使用*p修改其指向空间的内容,但是我们仍然可以改变*p指向的空间

3中const在 * 的右边,此时修饰的是p,也就是此时我们不能改变*p指向的空间了,但是仍然可以使用*p修改其指向空间的内容

有点绕,但是不难理解。


3.assert断言

C语言中有宏assert(),被称为断言。我们可以在括号中输入一个表达式作为参数,若表达式的结果为真,程序继续执行,结果为假则报错并终止运行。

例如在上面const的讲解中,假设const只修饰了变量y,我们希望避免误操作导致使用了地址修改其数值,此时就可以使用assert()宏,使用之前记得包含<assert.h>头文件

我们将y初始化为0并且使用const修改后,不希望后续操作中修改其数值,就使用assert(y==0)来验证变量y的数值是否被修改,此时再使用地址来修改其数值就会报错

assert()的使用对程序员是十分友好的,它不仅能给出出错的行号,同时当我们已经确认程序无误后,不需要再做断言,我们就可以在头文件#include <assert.h>前面定义一个宏NDEBUG

#define NDEBUG
#include <assert.h>

4.传值调用和传址调用

先举个例子,我们写一个Add函数实现整型相加

#include <stdio.h>

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

int main()
{
	int a = 2;
	int b = 3;
	int ret = Add(a, b);
	return 0;
}

此处,我们将a和b的值传到了Add函数中,此时为传值调用,Add的形参和实参占用的是不一样的空间

在一些情况中,我们使用传值调用就可以解决问题,但是当我们在解决一些交换变量内容之类的问题的时候,传值调用就没有办法得到我们想要的效果,例如:

这是因为,传值调用中,实参传递给形参的时候,形参会创建一块临时空间来存放实参的值,所以此时形参和实参并不位于同一块空间,对形参的修改也影响不到实参

要解决这个问题,我们只需要把地址作为参数传入函数中

就顺利实现了我们想要的效果,这种方式就叫传址调用。

如果我们只需要使用变量值来进行计算,可以使用传值调用;但是当我们要在函数中修改实参的内容时,就要使用传址调用了


5.数组名和指针访问数组

5.1 数组名的理解

实际上,数组名就是数组首元素的地址。验证如下:

#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);
    return 0;
}

我们创建一个数组,然后分别取arr和&arr[0]并打印地址,接着我们会发现二者完全一样

但是例外的,当我们运行这段代码时

#include <stdio.h>

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

输出的结果是40,也就是整个数组的大小,但是arr不是首元素地址吗?输出的应该是4或8才对

事实上数组名的理解有两个例外:

  • sizeof内单独存放数组名时,此时的数组名表示整个数组,而不是首元素的地址,所以计算的是整个数组的大小
  • 当我们&数组名的时候(&arr),此时取出的不是首元素地址,而是整个数组的地址,+1的话也是跳过整个数组而不是跳过一个元素

除此之外,其他任何地方在使用数组名的时候都表示首元素地址

5.2 指针访问数组

在学习指针之前,我们访问数组的内容通常是这样的

#include <stdio.h>

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

学习指针之后,我们就可以用这种方式

#include <stdio.h>

int main()
{
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int *p = arr;
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}

我们会发现,现在arr和p中存放的都是首元素的地址,所以arr和p是等价的,那么我们是否可以把arr[i]换成p[i]呢

完全没问题。而且我们也可以将*(p+i)替换成*(arr+i)

 也就是说,arr[i]和*(arr+i)其实是等价的,按照交换律

* ( arr + i ) == * ( i + arr )

∴ arr [ i ] == i [ arr ]

 

这种方法虽然可以,但是并不推荐这么做。平时我们还是最好按照习惯去写


6.一维数组作为函数参数

我们知道,数组也可以作为参数传递给函数。但是实际上传参的时候并不是传递一整个数组,而是传递数组的首元素地址。

例如我们分别在函数外和函数内分别求一个数组的元素个数

因为只是把数组的首元素地址传入了函数中,所以无法得到正确的数组元素个数。另外,函数的形参既可以写成上图中数组的形式,也可以写成指针的形式


7.二级指针

我们知道,指针变量内部存放了变量的地址,但是指针变量本身也是个变量,也有自己的地址,那么:指针变量的地址该存到哪里呢?

这里就引入二级指针的概念了

#include <stdio.h>

int main()
{
    int a = 10;
    int *pa = &a;
    int **ppa = &pa;
    return 0;
}

二级指针的一个显著标志就是有两颗 *,后一个 * 说明ppa是指针变量,前一个 * 和 int 组合说明ppa指向的类型是 int * 类型的变量。

 (地址随便编的)

对二级指针解引用就像剥洋葱一样,一层解完解下一层

*ppa = pa

*pa = a


8.指针数组和数组指针

8.1 指针数组

学习指针数组的时候很多人都容易混淆:指针数组是数组还是指针?

实际上很容易区分,就像整型数组是存放整型的数组、字符数组是存放字符的数组一样:指针数组就是存放指针的数组

指针数组的每个元素都是一个指针,也就是一个地址,分别指向不同的区域。

这么说来,前面提到数组名就是数组首元素地址,那么我们是否能使用指针数组模拟二维数组呢?

8.2 指针数组模拟二维数组

#include <stdio.h>

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

上面这段代码中,arr是一个指针数组,操作符[ ]的优先级比 * 高,所以arr先和[ ]结合,代表arr是一个数组,arr前面的int *说明了数组内的元素类型

虽然我们可以使用指针数组模拟二维数组,但是二维数组中的元素都是连续的,而指针数组中的元素并不连续。

8.3 数组指针变量

同上,数组指针变量就是指向数组的指针变量,存放了数组的地址,实质上是一个指针变量

那么问题来了,你能认得出哪个是指针数组哪个是数组指针吗

int  *p [10]

int (*p)[10]

上面刚提到,操作符[ ] 的优先级比 * 高,要创建一个数组指针变量,就要用括号将*和指针变量名括起来,变量名先和*结合说明是一个指针变量,否则就会先和[ ]结合变成一个数组,所以第一个是指针数组,第二个是数组指针变量

现在我们知道了数组指针变量是用来存放数组的地址的,现在试试使用&操作符将数组的地址存到数组指针中吧

数组指针的类型很好区分,我们去掉指针变量名,剩下的就是数组指针的类型:

type (*p ) [ ]

<type>是数组指针指向的数组的元素类型,方括号中的数字就是数组指针指向的数组的元素个数


9.二维数组传参的本质

前面我们已经使用指针数组模拟过二维数组了,实际上,二维数组传参的本质和其十分相似。

二维数组,可以看作每个元素都是一个一维数组的数组,前面我们在学习中也知道了数组名就是数组的首元素地址。所以二维数组传参的时候实际上就是传递了首元素的地址——也就是第一个一维数组的地址。

那么我们用二维数组名加减整数,就可以访问其内部的一维数组;再对其内部一维数组的地址加减整数,就可以访问一维数组中的每一个元素。

通过这两层关系,我们就可以实现访问二维数组的每一个元素。


10.字符指针变量

我们知道指针变量的类型有int *,char *等等,而类型为char *的字符指针变量也有一些知识需要我们了解

平时我们可能很少使用到char *类型的指针变量,顶多可能偶尔会写到这样的代码

#include <stdio.h>

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

很简单,很表层,但是字符指针变量并不仅限于这点功能

还有一种使用方法如下:

#include <stdio.h>

int main()
{
    char *pc = "hello";
    printf("%s\n", pc);
    return 0;
}

可能有些同学会以为“hello”这一整个字符串都被放在字符指针中了,实际上,当我们想将一个字符串存到字符指针中时,其实只是把字符串首字符的地址放在了这个字符指针中

也就是说,pc此时指向的是h的位置

《剑指offer》中有一道和字符指针、字符串相关的笔试题,通过这道题我们再对字符指针有一个深入的了解

#include <stdio.h>
int main()
{
 char str1[] = "hello bit.";
 char str2[] = "hello bit.";
 const char *str3 = "hello bit.";
 const char *str4 = "hello bit.";
 if(str1 ==str2)
 printf("str1 and str2 are same\n");
 else
 printf("str1 and str2 are not same\n");
 
 if(str3 ==str4)
 printf("str3 and str4 are same\n");
 else
 printf("str3 and str4 are not same\n");
 
 return 0;
}

 运行这段代码,输出如下

当我们使用相同的字符串去初始化两个数组的时候,这两个数组会开辟出不同的空间;而当几个不同的指针指向同一个字符串的时候,实际上这些指针指向了同一块内存。


11.函数指针变量

11.1 函数指针变量的理解

根据之前学习不同指针变量的经验,我们不难理解:函数指针变量就是存放函数的地址的指针变量

难道函数也有地址吗?我们可以测试一下

#include <stdio.h>

void test()
{
    printf("hello");
}

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

随便创建一个函数,我们尝试打印它的地址,输出结果如下

确实打印出来了地址,而且我们发现直接用函数名和&函数名都可以获得函数的地址

说明:函数名就是函数的地址

知道了怎么取出函数的地址,我们就要知道怎么存放函数地址。函数指针变量的指针类型长得其实和数组指针非常相似:

type (*p ) (  )

其中,<type>是函数指针指向的函数的返回类型,*后面跟着函数指针变量名,第二个括号内填入函数指针指向的函数的不同参数类型

例如:

#include <stdio.h>

void test()
{
    printf("hello");
}

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

int main()
{
    void (*p1)() = test;
    int (*p2)(int, int) = Add; //第二个括号中可以只写类型不写参数名
    return 0;
}

11.2 函数指针变量的使用

我们知道,平时调用函数的时候一般只会写函数名,而函数名也是函数的地址,我们将函数地址存到指针变量中后,指针变量名是否就和函数名等价了呢?

#include <stdio.h>

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

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

事实上,不管是否对函数指针解引用,我们都可以调用函数指针指向的函数

输出结果:


12.typedef关键字

typedef 可以帮助我们将一些复杂的类型重命名

例如我觉得 unsigned int 这个类型太长了,写代码不方便,能不能改简单一点呢?

我们只需要:

typedef unsigned int uint;

上面,我们就使用了 typedef 将 unsigned int 重命名为了 uint ,在后续的代码中我们只需要使用 uint 就可以实现 unsigned int 的功能了

当然,指针变量也能修改

typedef int* pty_t;

但是对于数组指针和函数指针,修改的方式就有所不同了,我们要在第一个括号内部修改新类型名

typedef int(*parr_t)[10];
typedef void(*pfun_t)(int,int);

13.函数指针数组和转移表

13.1 函数指针数组

存放函数的地址的数组就是函数指针数组,我们已经学习了指针数组和函数指针,那么你知道函数指针数组该怎么定义吗?

type (*parr [ ]) ( )

 其中,parr先和 [ ] 结合,说明是一个数组,其内部元素类型是 type(* )( ) 

13.2 转移表

学习函数指针数组后,我们就可以用它制作转移表

例如我们写一个计算器,没学函数指针数组之前会这样写
 

#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;
    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);
        switch (input)
        {
        case 1:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = sub(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = div(x, y);
            printf("ret = %d\n", ret);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);
    return 0;
}

 会发现,其中有很多段代码是重复的,太长了且太冗余

当我们使用函数指针数组后,就可以省去switch中的大段重复代码

#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;
}

上面这段代码中的函数指针数组就是转移表。转移表是一种数据结构,主要用于存储预先计算的结果,以便在需要时进行快速查找。


14.回调函数

回调函数就是通过函数指针调用的函数

例如上面的实现计算机功能中有这么一段代码

switch (input)
        {
        case 1:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = sub(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = div(x, y);
            printf("ret = %d\n", ret);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }

我们可以写一个函数来避免重复的代码,将上面的add、sub、mul、div函数作为参数传入

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);
}

此时pf被用来调用其指向的函数,被调用的函数就是回调函数

优化后的代码:

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;
}

明显更加简洁,避免了代码的重复

完.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值