初识C语言·指针(3)

目录

1字符指针变量

2 数组指针变量

3 二维数组传参的本质

4 函数指针变量

5 函数指针变量的使用

6 关键字typedef

7 函数指针数组


1字符指针变量

指针变量有很多种,今天我们选取部分进行专门的介绍,首先登场的的是字符指针变量。

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

这是一般的使用方法,但是如果我们给字符指针变量一个常量字符串呢?

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

打印出来的结果是没有异议的,只是,指针变量只能存一个元素的地址,那pc存的是谁的地址呢?

这里当然不是把整个字符串都放在了pc里面,不难猜测pc里面存的是该常量字符串的首元素地址。

所以打印出来的是整个字符串。

现在我们来看一道题。

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

提问这里最后打印的结果是什么?

这道题的关注点不是在const ,这里的str3和str4指向的都是常量字符串,只是为了更加严谨,我们加上了const而已,那么我们的关注点就应该是常量字符串了。

题目要求比较的是地址,其实就是问我们这几个字符串所在空间是一样的吗?

我们先看str1 str2,这两个是字符数组,都在栈区开辟空间,会给内存申请不同的两块空间,所以它们所在的空间是不一样的,自然地址就是不一样的,所以第一个的比较结果是not same。

在看str3 str4,前面提到,它们指向的是常量字符串,常量字符串是在静态区存储的,但是这两个字符串都一样,系统就认为常量字符串反正不能被改,那就不给多个空间了,毕竟是一样的,所以str3 str4指向的地址都是一样的,是常量字符串的首元素地址,所以第二个的结果是 same。


2 数组指针变量

上一篇文章讲到过,指针数组是数组,存放的是指针变量,那么现在引入一个新概念,数组指针,那么同理,数组指针,字符指针,整型指针等等,都是指针,那么现在介绍这种较为特殊的指针。

int main()
{
	int* pa[10];
	int(*pb)[10];
	return 0;
}

先来区别哪个是数组指针哪个是指针数组。

显然,第一个是指针数组,*先和int结合。第二个因为有了圆括号,*就先和pb结合,就先是指针,指向的是一个包含了10个元素的整型数组。所以区分数组指针还是指针数组的重要的点就是看有没有圆括号,其实这涉及的是运算符的优先级,圆括号的优先级比大部分的运算符的优先级都要高。

前面提起的野指针的概念,一个指针如果不初始化就会变成野指针,那么数组指针应该怎么初始化呢?其实只要给它一个数组名就行了,毕竟这个不属于两种特殊情况。

经过内存窗口也确实证明了这两个的地址是一样的。


3 二维数组传参的本质

在有了数组指针的理解之后,我们来了解一下二维数组传参的本质。

先看代码。

void test(int arr[3][5])
{
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
	test(arr);
	return 0;
}

创建好二维数组后,我们传参,形参是二维数组,实参也是二维数组,那么我们可不可以用刚学到的数组指针呢?

当然是可以的,我们知道,二维数组是连续一维数组的集合,一维数组传参的时候传的是首元素的地址,那么我们就可以类比一下就是,二维数组传参,传的是首元素的地址,但是这个首元素是第一个一维数组,传参传的就是整个一维数组的地址。

既然是整个数组的地址,那么我用数组指针来接收岂不美哉?试试?

void test(int (*pa)[5])
{
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", *(*(pa + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
	test(arr);
	return 0;
}

自然是避免不了两次解引用操作的,第一次解引用是确定行,第二次解引用是确定列,然后就是打印了。


4 函数指针变量

数组指针变量特殊吧?现在介绍更特殊的,函数指针变量,顾名思义,函数指针变量指向的是函数。

既然是指针,那我们同样可以看到函数指针变量的地址

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

地址看到了吧?结果是一样的吧?那么你也猜到我要说什么了,在上一篇提及到,函数名和数组名一样特殊,数组名是首元素地址,函数名是函数的地址,所以打印的时候我有没有&符号都是一样的。

函数名我们有了一定的理解,现在我们来讨论一下函数指针变量如何创建。

void(*p1)() = test;

这,就是刚才写的函数的函数指针变量,我们从左到右依次解说。
void是因为,test函数的返回类型是void类型,所以写void,(*p1)其实和数组指针的写法很像的,用圆括号括起来使它先是一个指针,最后的()可能你没反应过来,那就再来一个。

int Add(int x, int y)
{
	return x + y;
}
int (*p)(int, int) = Add;
int main()
{
	int a = 4, b = 8;
	int c = Add(a, b);
	return 0;
}

看吧,函数Add的参数是两个int类型,所以那个括号括的就是函数参数的类型,有一个值得注意的点就是函数指针的参数可以只写类型,不写具体的变量。但是定义函数的时候是不能这样操作的,系统会报错。函数的参数有多少,不管是什么类型,创建函数指针变量的时候都要一起写进去,即便是结构体类型也要写,虽然很少见。

最后就是介绍一下函数指针变量的类型,数组int arr[10],int  [ 10 ]就是这个数组的类型,函数指针变量的类型如法炮制,int (*p)(int , int ),把变量名去掉就是函数指针变量的类型,int (*) (int,int)就是该函数指针变量的类型。


5 函数指针变量的使用

函数指针变量的定义,创建我们都学会了,现在就是如何使用的问题。

怎么调用函数?传参嘛。用什么调用,用指针调用呗。调用数组我们写的是数组名,调用函数我们写的就是函数指针变量名咯。

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*p)(int, int) = Add;
	int c =(*p)(2,5);
	printf("%d ", c);
	return 0;
}

像这样,可能看起来甚至没有直接写Add来的快?不急,后面大有用处的。

先看这两段有趣的代码。(均出自于C陷阱和缺陷)

 (*(void (*)())0)();

这里突破点是void (*)(),这是函数指针,然后0的前面有括号,括号?不就是强制类型转化嘛,所以把0强制转化为函数指针了,然后调用这个函数指针,调用的时候是不用写函数的返回类型的。因为函数的参数是空,所以调用的使用参数也是空的。

void (*signal(int , void(*)(int)))(int);

再来看这段代码,看这种理论性很强的代码,无非就是找准突破点,慢慢分析就行了。

比如这个的突破点一看就是*signal(int,void(*)(int)),这,代表的是signal函数指针指向的函数的参数的是一个整型和一个函数指针,然后调用这个函数。

函数的要素 参数,返回类型,我们现在知道这个函数的参数的整型和一个函数指针,那么剩下的是什么呢?没错,剩下的就是函数的返回类型了,你看整体,一看就是函数指针嘛,所以这段代码的意思是调用一个函数,函数的返回类型是函数指针,参数是整型和函数指针。

文字终究不能言传身教,所以下来一定要尝试自己阅读这段代码。

什么?还不会?那就来看看这个关键字吧,typedef。


6 关键字typedef

它的作用就是重命名,比如你觉得unsigned int太难写了,你直接重命名为u是可以的,像这样。

int main()
{
	typedef unsigned int u;
	u a = 199;
	printf("%u ", a);
	return 0;
}

当然,,实际上这样的使用很少,只是举个例子,应用的场景呢,比较常见的就是一个函数返回类型是函数指针,参数也是函数指针这样的,看起来很绕我们才使用重命名,对于一般的类型,什么指针类型,整型这些就像刚才那样重命名就行,函数指针类型和数组指针类型有点区别,具体操作如下。

前面提及,数组指针的类型是int(*) [10],那么重命名的时候写法是这样的,

typedef int (*haha)[10];

没错,里面的haha就是重命名之后的结果,重命名之后的结果要写到(*)的里面,同理,函数指针的重命名也是一样的,客官下来自行尝试一下。

那么,现在就可对刚才的代码2进行非常6的简化了。

void (*signal(int , void(*)(int)))(int);

这个的返回类型是void (*)(int),参数是整型和void (*)(int),两个重复了,而且挺长,我们就重命名这个函数指针为ptr,整体结果就变成了这样。

ptr signal(int, ptr)

看吧,是不是一下就简洁了起来。


7 函数指针数组

数组指针学了,函数指针学了,那么再来一个函数指针数组阁下又应该如何应对呢?

首先,函数指针数组,不是函数,不是指针,更不是函数指针,这点相信可以看出来,所以它是个数组,存放的是函数指针。

嚯,这下这个就厉害了,前面直接使用函数指针的使用我们可能觉得看起来不如函数直接来的快。现在有了函数指针数组,也就是代表,我们把函数指针存放在进去,调用的时候直接使用该数组就行了,比如我们要实现一个会加减乘除的计算器(其实要是想,把按位运算加进去也是可以的),我们就写出四个函数,写出对应的函数指针,存放到数组里面去,调用的时候我选择性的调用就可以了,具体操作如下。

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	int (*p1)(int, int) = Add;
	int (*p2)(int, int) = Sub;
	int (*p3)(int, int) = Mul;
	int (*p4)(int, int) = Div;
	int (*p[4])(int, int) = { Add,Sub,Mul,Div };
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", (*p[i])(a, b));
	}
	return 0;
}

函数指针数组的是 int (*p[])(int,int),解释一下它从左到右,分别是函数的返回类型,(*p[]),p是数组名,[]的优先级比*高,所以就先和[]结合,让它先是一个数组,再和*结合,再然后就是(int,int)函数的参数。

当然,这里实现的是两个整数的加减乘除,除法有点瑕疵,但是问题不大,主要是看一下函数指针数组的实现,也被称为转移表

可以理解为转移表就是存放了多个函数指针的数组,通过下标访问可以实现函数的调用。


感谢阅读!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值