C语言的灵魂-->指针的犄角旮旯

 

目录

一.字符指针

二.指针数组

 三.数组指针

四.数组参数

1.一维数组传参

2.二维数组传参

五.函数指针

①.函数指针的用途

②.函数指针数组

③回调函数


一.字符指针

我们都知道字符指针是存放一个字符的地址的,但是请诸位看以下的代码:

这里是把一个字符串放到pstr指针变量里了吗?通过打印出来的结果,再结合打印%s遇到'\0'就会停止,所以我们猜测pstr存储的应该是字符串首字符的地址,然后向后打印,遇到字符串的\0终止。事实其实的确如此,就是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

我们再来看一下的代码:

 这串代码的输出结果是什么呢?在笔者看来结果应该是 str1!=str2并且str3!=str4

结果却是str1!=str2,str3=str4,那么可以推测str1和str2的地址不相同,而str3和str4的地址相同,那么为什么呢?

这里还是要归结到计算机的存储,在计算机看来,str1和str2是开辟的两个数组,是可修改的,所以地址不可以相同,反过来想,如果地址相同,那么我修改str1的内容,str2的内容也会被修改,这显然是不合适的。那str3和str4又如何解释呢?首先str3和str4存放的是常量字符串首字符h的地址,对于计算机来说,常量字符串一般是不做修改的,所以如果就把相同的常量字符串存到同一块空间,即指针指向同一个地址,这样节省了空间,这也是相当合理的。

二.指针数组

指针数组,顾名思义,就是存放指针的数组

例如常见的指针数组有:

 常见的应用:

 我们很轻易就可以利用它实现二维数组的功能。

它也可以:

 看到这里,可能有的朋友就要问了,这和二维数组不一模一样了吗?不,它们的区别在于,指针数组所存的三个数组的地址是不连续的,而在二维数组是连续存放的。通过调试可以发现:

 三.数组指针

显而易见,数组指针就是指向数组的指针

 我们都知道&数组名和数组名指向的都是数组首元素的地址,但是它们的类型和表达的含义是不同的,&数组名是取出整个数组的地址,应该用一个数组指针指向,而数组名仅用一个相同类型的指针指向即可

 我们发现arr和&arr的地址符合我们所说的,但是arr+1和&arr+1所展现的地址就不相同了,而且我们很惊奇的发现&arr+1和&arr相差恰好12个字节,这是巧合吗?

这里就不得不谈到地址+1 的问题,我们知道地址+1跳过的是这个地址所属类型的大小所以我们分析一下,就可以发现,arr是数组首元素的地址是int*类型,所以加1跳过4/8(取决于编译环境)个字节,而&arr取出整个数组的地址,它的类型是int*[3],所以+1要跳过整个数组大小。

数组指针的应用:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
    }
}
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

我们发现,学习了数组指针,我们在面对二维数组的时候有了两种传参方式,一种是int arr[3][5],即以数组的形式,另一种是int(*arr)[5],以数组指针的形式传参,也就是我们在有些地方看到的行指针。

四.数组参数

1.一维数组传参

 上面几种传参方式都是可以的,以此我们发现怎么设计传参,本质上是要看传过来的是什么类型

2.二维数组传参

 对于二维数组,我们要始终谨记,它的数组名表示第一行元素的地址,传参时函数形参的设计只能省略第一个[]的数字,对于一个二维数组,可以不知道它有多少行,但是必须知道它每一行有多少元素,对于更高维的数组也是如此,第一个[]的数字传参时可以省略,其余不能省略

五.函数指针

我们知道,指针是指向地址的,函数指针,难道函数还有指针?

显然,函数是由地址的。我们怎么理解呢?

其实,函数指针和数组指针类比是比较相似的。

如果说指向数组的指针是数组指针,那么指向函数的指针就是函数指针.

那么,怎么存储函数的地址呢?表现形式是什么呢

 比如上述的Add函数,怎么存储这个地址呢?

我们是这样表示的:int (*pf)(int,int)=&Add;这里的第一个int是这个函数的返回类型,(*pf)说明它是个指针,括号(int,int)是函数两个参数的类型。

常见用法:

下面来看两端有趣的代码:

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

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

这两串代码均出自于《C陷阱与缺陷》这本书,感兴趣的朋友可以去看看。

那么我们先来看一下第一个代码,这段代码我们可以这样解读:

 

经过层层剖析,我们可以解读这样看起来比较麻烦的代码。再来看第二个代码

 这样解析可能比较麻烦,那么有没有一种方法可以简化一下呢?我们可以借助typedef重定义来简化。

 

 笔者写了这些,可能对于大家来说还是决定函数指针比较鸡肋,接下来笔者要说一下函数指针的用途。

①.函数指针的用途

我们可以简易写一个计算器实现加减乘除的功能。

#include<stdio.h>
void menu()
{
	printf("****************************\n");
	printf("***** 1.add   2.sub ********\n");
	printf("***** 3.mul   4.div ********\n");
	printf("***** 0.exit********\n");
	printf("****************************\n");
}
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;
}
//回调函数,通过函数回头调用它的函数
void calc(int(*pf)(int, int))
{
	int x, y, ret;
	printf("请输入两个操作数>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}
	int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do 
	{
		menu();
		printf("请选择\n");
		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;
		default:
			printf("输入错误,请重新输入\n");
			break;
		
		}
	} while (input);
	return 0;
}

我们通过回调函数来调用函数的地址,这样可以实现一个简易的计算器,但是当代码量大大增加,需要很多的函数的时候,这时用switch语句会显得比较冗余,那么我们可以借助函数指针数组来更简易实现这个功能。

②.函数指针数组

在之前曾描述过指针数组存放指针,那么显而易见,函数指针数组存放的是函数指针。那么,我们可以借助这个知识,重新写一下这个代码。

#include<stdio.h>
void menu()
{
	printf("****************************\n");
	printf("***** 1.add   2.sub ********\n");
	printf("***** 3.mul   4.div ********\n");
	printf("***** 0.exit********\n");
	printf("****************************\n");
}
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 input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
	do 
	{
		menu();
		printf("请选择\n");
		scanf("%d", &input);
		if (input == 0)
			printf("退出计算器\n");
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		
	} while (input);//代码大大简化
	return 0;
}

这样代码可读性和去冗余都做得很好。

③回调函数

回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这时回调函数。

之前我们在写一个计算器的时候曾经用过回调函数,这里就不多赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值