C语言指针详解-包过系列(三)目录版

1、字符指针变量

我们知道若指针所指向的内容是字符,那么指针的类型就是字符指针类型 char* ,这个指针变量就是字符指针变量。

1.1、字符指针变量的一般应用

int main()
{
    char ch = 'a';
    char* pc = &ch;
    *pc = 'w';
    printf("%c", ch);
    return 0;
}

在这里插入图片描述

1.2、常量字符串

#include<stdio.h>
int main()
{
    const char* pt = "hello world.";//请思考这里是把一个字符串放到了 pt 指针当中了吗?
    printf("%s\n", pt);
    return 0;
}

如上面代码所示的 char* name = “字符串” 就是常量字符串的表达方式。那么我们思考一下注释中的问题:这里是把一个字符串放到了 pt 指针当中了吗?
答案是:否。毕竟前面是一个指针变量,其存储的应当为地址,故放入的是首字符的地址。因为字符是连续存放的,输出时系统会自动向后遍历,直至遇到 “/0”。
运行效果图:
在这里插入图片描述

1.3、常量字符串与普通字符串的区别

1.3.1 常量字符串的不可修改性

1.1 的代码和运行图中我们可见一般情况下的字符(字符串)是可以通过指针解引用来进行修改的。而常量字符串则不可修改,如下面代码示例演示

int main()
{
    char* pt = "hello world.";
    printf("%s\n", pt);
    *pt = "Hello World";
    return 0;
}

在这里插入图片描述
原因请见下文 1.3.2

1.3.2 常量字符串的存储

我们知道在存储位置中有栈区,堆区,静态区。其实在此之外还有一部分称为代码段。一般字符(字符串)存储在栈区中,而常量字符串则存储在代码段中。所以即使通过指针解引用也不能更改字符串内容。正因如此,当多个指针变量指向的常量字符串内容一致时,系统不会存储多份相同的常量字符串,而是会只存储一份,各个变量共用这一份常量字符串(即字符串地址相同)。为使各位对此更加清晰,我们通过以下代码和图解来进行进一步解析。

int main()
{
    char str1[] = "You are handsome !";
    char str2[] = "YOu are handsome !";
    char* str3 = "You are so beautiful";
    char* str4 = "You are so beautiful";

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

在这里插入图片描述
在这里插入图片描述

2、数组指针变量

2.1、数组指针变量定义

数组指针,顾名思义它是一种指针。我们已经熟知:
整型指针变量:int * p;存放的是整型变量的地址,指向整型数据的指针。
浮点型指针变量:float * p;存放的是浮点型变量的地址,指向浮点型数据的指针。
由上推知:数组指针变量应当存放的是数组的地址,指向数组的指针。
数组指针变量表达式:int (*p) [20];
在这个表达式中,p会先和*结合,表明 p 是一个指针变量,然后指针指向的是一个大小为20个整型变量的数组。
注意:因为 [ ] 的优先级要高于 * 号,所以必须加上 ()来保证 p 先和 * 结合

2.2、数组指针变量的初始化

既然数组指针变量存放的是数组的地址,那么我们就要将数组的地址给指针变量。数组地址:&数组名

int arr[10] = {0};//定义数组并初始化
int (*p) [10] = &arr;//将数组的地址赋值给指针变量 

在这里插入图片描述

3、二维数组传参本质

在数组章节中曾提及二维数组传参的如下写法:

#include <stdio.h>

void print_arr(int arr[4][5], int a, int b)
{
    int i = 0;
    for (i = 0; i < a; i++)
    {
        int j = 0;
        for (j = 0; j < b; j++)
        {
            printf("%d", arr[i][j]);
        }
        printf("\n");
    }
}

int main()
{
    int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8}};
    print_arr(arr, 4, 5);
    return 0;
}

在上述代码中,实参为二维数组,形参也为二维数组。那么下面来介绍另一种传参方式,使用数组指针进行传参。
二维数组在之前数组章节曾讲过,其可以看成每个元素是一维数组的数组,即二维数组的每个元素是一个一维数组,二维数组首元素就是第一行,是一个一维数组。
在这里插入图片描述
二维数组的数组名就是第一行的地址,是一个一维数组的地址,第一行一维数组类型为int [5],故其地址类型为int (*) [5];。如此也就证明二维数组传参实际上是传递了第一行一维数组的地址。由此可知,函数形参部分也可以写成指针形式。如下所示:

#include <stdio.h>

void print_arr(int (*p) [5], int a, int b)//数组形参设置为数组指针
{
    int i = 0;
    for (i = 0; i < a; i++)
    {
        int j = 0;
        for (j = 0; j < b; j++)
        {
            printf("%d", *( * ( p + i ) + j ) );//深入理解系统对数组arr[ i ]的编译。
            // arr[ i ] = *(arr + i)
        }
        printf("\n");
    }
}

int main()
{
    int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8} };
    print_arr(arr, 4, 5);
    return 0;
}

4、函数指针变量

4.1、函数指针变量的创建

根据上文内容,我们可以推断出函数指针变量是存储函数地址的变量,同时我们可以通过函数的地址来调用函数。

4.1.1、验证函数地址的存在

下面我们来验证函数地址的存在:

#include<stdio.h>

void test()
{
    printf("hehe\n");
}

int main()
{
    printf("test = %p\n", test);//函数名
    printf("&test = %p\n", &test);//取函数地址
    return 0;
}

在这里插入图片描述
由此说明函数存在地址,函数名与 &函数名 都可以得到函数的地址。

4.1.2、函数指针变量表达式

表达式如下:

int*pf)(int x , int y);
//或 int (*pf)(int , int)

在这里插入图片描述
去掉变量名即为其变量类型:

int*)(int x , int

4.2、函数指针变量的使用

在使用函数指针变量调用函数时,(*pf)()与 pf()两种方法都是可行的,因为二者表示的意思都一致,都是对应函数的地址。
代码演示:

#include<stdio.h>

int div(int a , int b)
{
    return a / b;
}

int main()
{
    int (*pf) (int, int) = div;
    printf("%d\n", (*pf)(4 , 2) );//调用方式一
    printf("%d\n", pf(4, 2));//调用方式二
    return 0;
}

在这里插入图片描述

4.3、深入理解函数指针

4.3.1、例题一
(*( void (*)() ) 0)();//请思考此行代码的意义

请思考上面代码的意义,解析见 4.3.3

4.3.2、例题二
void (*signal(int, void(*)(int)))(int);//请思考此行代码的意义

请思考上面代码得意义,解析见 4.3.4

4.3.3、例题一解析

在这里插入图片描述
入手关键点在 0 处,其前方应当为0的类型,( void (*) () ) 0 典型的强制类型转换,将0从整型 int 强制转换为函数指针类型 void ( * ) () ,从而 0 就成为一个函数指针变量,整体就是通过函数指针调用函数。

4.3.4、例题二解析

在这里插入图片描述
入手关键点在中间的函数名及其参数部分 signal(int, void(*)(int)) 我们可见在这一小部分中已经包含了signal函数的函数名和该函数内的两个参数的类型,但无参数名,根据C语言标准可知这应当是一个函数声明。故剩余部分为函数的返回类型,为 void (*) (int)函数指针类型。因为解引用操作符 * 后要跟名称,所以signal(int, void(*)(int))放在了 * 之后。综上,整体为一个函数声明

4.4、关键字 typedef 介绍
4.4.1、typedef 表达式与实例演示

typedef是用来类型的重命名的,可以简化复杂的类型,如 4.3.4 例题二中的函数指针类型。其重定义表达式如下:

typedef name1 name2;

name1 表示需要重定义类型名
name2 表示 name1 重定义后的类型名

具体演示如下:

#include<stdio.h>

int div(int a , int b)
{
    return a / b;
}

int main()
{
    typedef int(*son)(int, int);
    // * 后要跟名称,所以新的类型名要放在 * 后
    son pf = div;
        printf("%d\n", (*pf)(18 , 2) );
        printf("%d\n", pf(10, 2));
    return 0;
}

在这里插入图片描述

4.4.2、define 与 typedef 的区别

1、 本质区别:
#define 是预处理指令,用于文本替换。在编译之前,预处理器会直接将 #define 定义的宏替换成指定的文本。
typedef 是类型定义命令,用于为已存在的数据类型创建一个新的名字。
2.、作用范围:
#define 定义的宏没有作用域限制,一旦定义,就会一直有效,除非被 #undef 取消定义。
typedef 定义的类型有作用域限制,它遵循C语言的变量作用域规则。
3、 类型检查:
#define 不进行类型检查。它仅仅是在预处理阶段进行文本替换,所以不会检查替换后的类型是否正确。
typedef 会进行类型检查。当你使用 typedef 定义的新类型时,编译器会检查类型是否匹配。
4、 使用方式:
#define 可以用于定义常量、宏函数等,不仅仅限于类型。
typedef 仅用于定义类型的别名。
5、 内存分配:
#define 不会分配内存,因为它只是在预处理阶段进行文本替换。
typedef 本身也不分配内存,但它定义的类型在创建变量时会分配内存。

5、函数指针数组

我们知道数组是一个存储相同类型数据的存储空间。故函数指针数组就是一块连续的存储函数指针的空间。
表达式如下:

type (* name[number]();

name 会先和 [ ] 结合,表明 name 是数组,数组的内容是 type (*) () 类型的函数指针。(因为 * 后面要跟名称,所以 * 后面跟上数组名)

6、转移表

转移表是函数指针数组实例化的体现。
下面以简易计算器的改造为例:

//改造前的计算器
#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;
}

上述代码的条件分支中我们可以看到有许多重复的部分
在这里插入图片描述
这样就出现两个问题:

1、将这样重复的代码实现成函数。 2、这个函数又能完成不同的任务

如此我们有两种思路:

1、设计回调函数

2、引入函数指针数组来设计转移表。

在此我们以思路二进行改造(思路一请见《C语言指针详解(四)》)。首先我们要思考,设计转移表的话函数指针数组中的元素应当如何设置。我们一共自定义了 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 (*parr[5])(int , int) = { 0 , add , sub , mul , div };//转移表
    //之所以首元素为 0  是为了便于选择,使1-4都对应具体函数
    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 = (parr[input])(x, y);
            printf("ret = %d\n", ret);
        }
        else if (input == 0)
        {
            printf("退出计算器\n");
        }
        else
        {
            printf("输入错误");
        }
    } while (input);
    return 0;
}

全文至此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢()(),这将是对我最大的肯定与支持!!!谢谢!!!()()

  • 60
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值