零基础入门C语言之深入了解指针3

阅读本篇文章前,建议阅读专栏内前面的文章。


目录

前言

一、字符指针变量

二、数组指针变量

三、函数指针

总结


前言

本文接着前面两篇文章继续为读者介绍关于指针的知识。


一、字符指针变量

我们知道在指针的类型中有一种叫做字符指针,我们一般这么使用它:

cpp 复制代码
#include <stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

我们键入如下的代码:

cpp 复制代码
#include <stdio.h>
int main()
{
	char arr[10] = "abcdef";
	char* pstr = arr;
	printf("%s\n", pstr);
	*pstr = 'w';
	printf("%s\n", pstr);
}

其运行结果如下:

我们这个时候换一种写法,变成下面的代码可以吗:

cpp 复制代码
#include <stdio.h>
int main()
{
	char* p = "abcdef";
	printf("%s\n", p);
	*p = 'w';
	printf("%s\n", p);
	return 0;
}

我们发下出现了如下的报错信息:

此时我们已经打印出了abcdef,但是却没有按照我们的想法将p中内容进行修改。这是因为我们上面这种形式是将p变成了一个常量字符串,它其中的内容是无法修改的。我们最好在它的前面加上const修饰一下。

我们再键入如下代码,并且思考一下我提出的问题:

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

我们定义pstr的这行代码很容易让人以为是把字符串放到了字符指针之中,实际上它本质上和数组一样,就是把首元素的地址放到了指针之中。如下图所示:

我们上面代码的意思就是把一个常量字符串的首字符h的地址存放在指针变量pstr之中。我们下面看一道与之相关的笔试题:

cpp 复制代码
#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;
}

你可以先来思考一下会输出什么呢?其运行结果如下:

那产生这个结果的原因是什么呢?首先str1和str2是分别创建了两个数组,所以两者的首元素地址一定不一样,这就会有相应的输出。而str3和str4都指向的是同一个常量字符串,在C/C++中,通常会把常量字符串存储到单独的一个内存区中,当几个指针指向同一个字符串的时候,它们实际上指向的是同一块内存。但是用相同的常量字符串去初始化不同的数组时就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

二、数组指针变量

我们之前学习过了指针数组,那是一种在数组中存放指针的结构,本质来说是一个数组。而现在我们介绍的这个叫做数组指针,那么这个东西本质上是数组还是指针呢,相信你现在应该能做出正确的判断了。没错,这个东西就是一个指针。我们熟悉的指针类型比如说整型指针变量存放的是整型变量的地址,能够指向整型数据;而浮点型指针变量存放的就是浮点型指针变量的地址,能够指向浮点型数据。以此类推,我们现在介绍的这个数组指针变量就应该是存放数组类型的地址,能够指向数组数据的指针变量。那么在这里,我们之前强调数组类型具体是什么的意义就很明显了。我们看看下面两种类型哪个是数组指针呢?

cpp 复制代码
int *p1[10];
int (*p2)[10];

很明显,第一种是我们在之前文章中所写的指针数组,而第二种才是我们的数组指针。对于数组指针,我们可以这么理解,因为这个数组的类型是int[10],所以我们把它看为一个整体,然后再加上一个*,这就是指向我们这个数组的指针,变量名字为p2。那么这个数组指针的类型就应该是int(*)[10]。我们要注意,[]的优先级是高于*的,所以我们必须加上()才能保证p先和*来结合。

既然数组指针变量可以用来存放数组的地址,那我们该如何对这种变量进行初始化呢?具体操作如下:

cpp 复制代码
int arr[10] = {0};
&arr;//得到的就是数组的地址
int(*p)[10] = &arr;

我们进入调试窗口,可以看到如下界面:

我们发现二者类型是相同的。我们具体解析一下数组指针类型:

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

那么这个数组指针到底有什么作用呢?我们可以借助它来理解清楚二维数组传参的本质是什么。我们之前在进行二维数组的传参的时候,是这种写法:

cpp 复制代码
#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]。这就意味着二维数组传参本质上也是传递了地址,只不过传递的是第一行这个一维数组的地址,那么我们也可以把形参写成指针的形式,如下:

cpp 复制代码
#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;
}

其结果如下:

三、函数指针

根据我们上面对于数组指针的介绍,我们自然而然会认为函数指针是一个指向函数的指针,其中存放着函数的地址,但是事实是否果真如此呢?我们需要来探究一下。

首先我们需要弄明白一个重要的问题:函数是否具有地址呢?我们键入如下代码进行测试:

cpp 复制代码
#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("test: %p\n", test);
 printf("&test: %p\n", &test);
 return 0;
}

其运行结果如下:

这说明无论是直接使用函数名,还是&函数名,其表示的都是函数的地址。那么我们如果想要把函数的地址存放到变量之中该如何去定义呢?其实,函数指针的写法和数组指针的写法是十分相似的。如下:

cpp 复制代码
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;//x和y写上或者省略都是可以的

那么我们来解析一下这种写法:

cpp 复制代码
int (*pf3) (int x, int y)
 |    |     ------------ 
 |    |           |
 |    |           pf3指向函数的参数类型和个数的交代
 |    函数指针变量名
 pf3指向函数的返回类型

int (*) (int x, int y) //pf3函数指针变量的类型

那我们学习这个函数指针变量有什么用呢?我们通过它可以直接调用指针指向的函数:

cpp 复制代码
#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;
}

其运行结果如下:

在掌握上面的相关知识之后,我们来看两段十分有趣的代码,并且尝试理解一下它们的含义,首先先来看看第一段:

cpp 复制代码
(*(void (*)())0)();

我们逐层拆解一下这段代码,首先是void(*)()这个东西,很明显这个就是我们在上面介绍的函数指针,只不过这个函数他的返回值类型是空,同时也不需要传入任何形式的参数,然后我们在它的外面加上一个括号,之后再加一个0,这是什么意思????如果这个你不理解的话,我写成这种形式,你来看看:(int*)0。是不是就有些理解了,这本质上是一个强制类型转换,我们把0强制类型转换为了那个函数指针类型。然后我们再解引用地址,就说明我们在调用这个函数,因为它没有参数,所以又有了一个单独的括号。所以总的来说,这个代码是在表示我们假设0这个地址放着一个无参,返回类型为void的函数,然后我们去进行调用的过程。

怎么样,是不是很有意思?我们再看一看下面这个代码,并思考一下其含义:

cpp 复制代码
void (*signal(int , void(*)(int)))(int);

我们再来逐层拆解一下这行代码,首先是void(*)()这个东西,很明显这个就是我们在上面介绍的函数指针,只不过这个函数他的返回值类型是空,同时也不需要传入任何形式的参数,我们看到它和int整型类型一起在signal后的括号内,那这种形式,不就是一个函数的形式吗。也就是说signal是一个函数名,他的第一个参数类型是一个整型,第二个参数类型则是一个函数指针类型。我们再把刚刚分析完毕的东西去掉,剩余的东西我们发现它还是一个函数指针,这就说明我们的signal函数最后会返回一个函数指针类型,我们可以写成这样方便理解(但是实际上是错误的写法):

cpp 复制代码
void (*)(int) signal(int , void(*)(int));

这样就看得相当明确了,所以这段代码的含义就是我们声明了一个名叫signal的函数,他需要两个参数,类型分别为整型和函数指针类型,这个函数最终会返回一个函数指针类型的值。

分析完上面两段代码之后,我们发现有的时候类型会写的很麻烦,那么我们想要简化这种代码的时候该如何去做呢?这时候我们就需要使用typedef这个关键字了。这个关键字就是用来将类型重命名的,可以将复杂的类型简单化。比如如下:

cpp 复制代码
typedef unsigned int uint;//将unsigned int 重命名为uint
typedef int* ptr_t;

我们这就是分别重命名了无符号整型和整型指针两种类型,但是如果说我们要去操作数组指针和函数指针的时候,就稍微有些不同了。他们应该如下操作:

cpp 复制代码
typedef int(*)[5] parr_t //这种形式是错误的
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
typedef void(*)(int) pfun_t; //这种形式是错误的
typedef void(*pfun_t)(int);//新的类型名必须在*的右边

那么我们就可以简化上面的第二段代码了:

cpp 复制代码
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

这时候可能有些读者会有疑问,如果说我们用typedef可以更改名称的话,我们可不可以用define来完成这种操作呢?操作如下:

cpp 复制代码
typedef int* ptr_t;

#define PTR_T int*

这个可以说是可以的,也可以说是不可以的。要看具体情况,如果说遇到下面的情况,就会出现问题。

cpp 复制代码
ptr_t p1, p2;
PTR_T p3, p4;

上面这段代码,**p1和p2都是整型指针变量,而p3是整型指针变量,p4是整型变量。**为什么会出现这种问题呢?这是因为define就相当于是一种替换的感觉,他会把代码变成下面这样:

cpp 复制代码
int *p3, p4;

这个*就会直接与p3结合,而p4就只有一个int。

在掌握完上面的知识后,我们来思考一个问题,我们已经学习过指针数组了,只不过当时我们存放的指针都是整型指针等基本类型的指针。那么我们能否把函数指针野村放到一个数组之中呢?答案显然是可以的。我们如果把一系列函数的地址都存放到一个数组之中,那这个数组就叫做函数指针数组。我们该如何对这个数组中进行定义呢?其定义方式如下所示:

cpp 复制代码
int (*parr1[3])(int, int);

我们先让parr1和[]结合,说明parr1是数组,其内容是int(*)(int, int)类型的函数指针。

那这个函数指针数组究竟有什么用处呢?我们通过下面这个计算器的代码来进行探究:

cpp 复制代码
#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;
}

可以看到上面是我们对于计算器的一般实现,我们如果用函数指针数组实现的话,是什么样子的呢?

cpp 复制代码
#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;
}

其运行结果如下:

可以看见,如果说我们之后要添加更多的运算,比如逻辑运算和位运算,我们如果使用switch-case语句,就要持续添加一系列的选择,这让代码变得十分冗长复杂,而我们这种函数指针数组的方法就极大地简化了代码。那么这就形成了转移表。转移表是一种数据结构,用于根据输入值确定需要执行的函数或操作。它通常是一个包含指针的数组,数组中每个元素包含指向对应函数或操作的指针。通过查找对应的输入值进行索引,程序可以调用对应位置的函数或操作,从而实现通过输入值动态调度程序执行不同的功能。当然我们也可以采取下面这种回调函数的方式来实现:

关于回调函数我们会在之后的文章中具体进行解读。


总结

本文深入讲解了指针的高级应用,包括字符指针、数组指针和函数指针。首先介绍了字符指针的特点,指出常量字符串无法修改的特性,并分析了指针与数组的区别。其次详细解析了数组指针的定义和使用,特别是其在二维数组传参中的作用。最后探讨了函数指针的概念,通过实例演示如何存储和调用函数地址,并介绍了函数指针数组在简化代码结构中的应用。文章还解析了两段复杂指针代码的含义,并讲解了typedef在简化指针类型声明中的用法。这些内容为理解指针的高级特性和实际应用场景提供了重要参考。

相关推荐
陌路203 小时前
C17值类别概念
开发语言·c++
liu****3 小时前
笔试强训(十三)
开发语言·c++·算法·1024程序员节
侯小啾3 小时前
【09】C语言中的格式输入函数scanf()详解
c语言·开发语言
初学小白...4 小时前
实现Runnable接口
java·开发语言
Bruce-li__4 小时前
CI/CD流水线全解析:从概念到实践,结合Python项目实战
开发语言·python·ci/cd
JustNow_Man4 小时前
【Cline】插件中clinerules的实现逻辑分析
开发语言·前端·javascript
hope_wisdom5 小时前
C/C++数据结构之用链表实现栈
c语言·数据结构·c++·链表·
ceclar1235 小时前
C++容器forward_list
开发语言·c++·list
夏玉林的学习之路5 小时前
Anaconda的常用指令
开发语言·windows·python