深入理解指针(3)

这篇文章主要以各种各样的指针类型以及指针和数组的结合类型进行展开叙述,如(字符指针,数组指针,函数指针,函数指针数组),并且还简单的写了一个了转移表。内容有点长,但是我相信如果能看完的话,能对你有所帮助。

目录

1.字符指针变量

1.1.字符指针的申明

1.2.常量字符串

1.3.打印字符串

 1.4.字符串相关题目

2.数组指针变量

2.1.数组指针的创建

2.2.数组指针的运用

3.二维数组传参的本质

3.1.二维数组名的理解

3.2.二维数组传参的遍历

4.函数指针变量

4.1.函数的地址

4.2.函数指针的创建

4.3.函数指针的使用

4.4.函数指针的拓展思考

4.4.1.有趣的题目

4.4.2.typedef关键字

4.4.2.1.typedef的用法

4.4.2.2.typedef 与 define 的区别

4.5.函数指针的作用

5.函数指针数组

5.1.函数指针数组的创建

 5.2.函数指针数组的使用

6.转移表

6.1.普通代码

6.2.采用函数指针优化代码

6.3.采用函数指针数组(最优)


1.字符指针变量

1.1.字符指针的申明

#include <stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;	// pc 是指针变量

	char arr[10] = "abcdef";
	char* p1 = arr;

	char* p2 = "abcdef";
    
    return 0;
}

如上代码就是字符指针的申明以及初始化方式。

1.2.常量字符串

大家可能对上述第三个申明方式有所疑惑,下面进行解析:

"abcdef"这个是一个常量字符串,在C语言中常量字符串本身就是一个地址,这里我们可以和数组名所对应在一起,数组名就是数组首元素的地址。

但是指针p 指向字符数组时,数组内容是可变的。

而指针p 直接指向一个常量字符串时,常量字符串的内容不能修改。

下面结合代码进行解释:

#include <stdio.h>
int main()
{
	char arr[10] = "abcdef";
	char* p1 = arr;
	*p1 = 'w';    // 将字符数组首元素的地址赋值为'w'

	char* p2 = "abcdef";
	*p2 = 'w';

	return 0;
}

通过调试 ,我们发现在对*p2 的值进行修改时,程序发出一个异常警告:写入访问权限冲突。

所以这里有个比较良好的代码习惯,就是如何指针直接指向常量字符串的时候,直接将指针解引用锁定,即const char* p2; 这样避免后面我们直接对常量字符串进行操作并且编译器又没有直接得提示。

1.3.打印字符串

在C语言中我们有一个比较好用的系统占位符%s,这个可以直接打印出字符串。

但是当用%s 打印字符串的时候,需要提供起始地址,直到 ' \0 ' 结束

#include <stdio.h>
int main()
{
	char arr[10] = "abcdef";
	char* p1 = arr;

	char* p2 = "abcdef";

	printf("%s\n", arr);
	printf("%s\n", p1);
	printf("%s\n", p2);
	return 0;
}

执行结果为

abcdef
abcdef
abcdef

 1.4.字符串相关题目

这里我们引入《剑指offer》中和字符串相关的一道笔试题。

题目如下:

#include <stdio.h>
int main()
{
	char str1[] = "hello world.";
	char str2[] = "hello world.";
	const char* str3 = "hello world.";
	const char* str4 = "hello world.";
	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 and str2 are not same
str3 and str4 are same

题目解析:

  • 题目要求判断str1 == str2,意思就是判读数组str1首元素的地址和数组str2首元素的地址是否相等。str1 与 str2 是一个字符数组,他们在内存空间中分别在某一处地址上创建数组,故两者不相等。
  • 题目要求判断str3 == str4,意思就是判断指针变量str3和指针变量str4是否相等。 str3 与 str4 是指向同样数据的常量字符串,在C语言中常量只进行一次存储,而指针变量指向常量字符串是指向常量字符串首字符的地址,所以两者相等。

其次我们通过调试也可以直观的看出两个值是否相等。如下图调试监视窗口。

2.数组指针变量

数组指针是指向数组的指针,本质上是一个指针。我们可以结合整型指针是指向整型变量,字符型指针是指向字符型变量来进行辨析。数组是修饰指针的,所以数组指针代表指向数组的指针。 

而指针数组是存放指针的数组,这个在上一章也有明确的解释,还不明白的可以去翻看上一篇文章。

数组指针的辨析与运用

2.1.数组指针的创建

这么我们围绕着数组指针和指针数组的区别来展开讲。

  • 数组指针引用为 int (* p)[10]        [10] 表示指向数组的元素个数,不可省略。
  • 指针数组引用为 int* p[3]             [3]   表示数组中存储指针的个数,当明确个数时,可省略。

因为[ ] 的优先级比 * 高,所以如果没有加括号时,p会优先与 [ ] 结合形成数组,再明确数组中存储指针类型。所以得对 *p 加上括号,使得强制先与 * 形成指针,再明确指针指向的int [10]类型,也就是数组。

如下代码可以直观的感受两者之间的不同。

#include <stdio.h>
int main()
{
	int arr1[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int arr2[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int arr3[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* pa[3] = { arr1,arr2,arr3 };	// 指针数组
	int(*parr)[10] = &arr1;				// 数组指针

	return 0;
}

下面就有一个问题为什么数组指针赋值&arr 而不是 arr 或者 &arr[0] 呢:

因为数组指针,指针指向的是整个数组,所以要传入整个数组的地址而

  • &arr[0] 代表数组首元素的地址
  •  arr      代表数组首元素的地址
  • &arr     代表整个数组的地址

如果还不清楚的话可以去看一下之前的文章

数组名的理解

2.2.数组指针的运用

数组指针一般用在函数传参的时候,这点下面我们会详细的叙述。还可以用来遍历数组,但是不常用,并且也容易出错且麻烦。

下面是两种采用指针遍历数组的方式:

#include <stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };

    // 第一种
	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);	// p[i] = *(p + i)
	}
	printf("\n");

    // 第二种
	int(*parr)[10] = &arr;	// 数组指针
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *((*parr) + i));	// *((*parr) + i) = (*parr)[i]
	}
	return 0;
}

(*parr)[i] 其实就是 arr 因为 *&arr = arr,所以访问各个元素是arr[i] 换做数组指针就是(*parr)[i]。

由此可以明显地看出用数组指针遍历数组有点多此一举的味道,所以我们一般不用数组指针来遍历数组,我们一般采用数组指针来进行函数传参。

3.二维数组传参的本质

二维数组虽然形式上来看是多行多列的,实际上再内存中存储的形式是连续存储的。所以我们可以采用数组指针来模拟二维数组。

3.1.二维数组名的理解

由上一章我们知道数组名代表首元素的地址,那么二维数组名同理也是首元素的地址,但这个首元素指的是首个一维数组还是首个一维数组的首个元素,下面我们进行深入探究。

因为不管是首个一维数组的地址还是首个一维数组首个元素的地址两者值看似都是相等的,所以我们通过地址 + 1的操作来直观的观察。

有上图不难看出二维数组名arr + 1 操作后地址跨越了20个字节(即一个一维数组),所以arr数组名代表的是首个一维数组的地址,而不是一维数组的首个元素的地址

3.2.二维数组传参的遍历

过去我们有一个二维数组传参给函数的时候,我们如下写法

#include <stdio.h>

void print(int arr[3][5], int row, int col)
{
	for (int 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},{2,3,4,5,6},{3,4,5,6,7} };	// 二维数组

	print(arr, 3, 5);
	return 0;
}

这里是实参是二维数组,形参也设置成二维数组接受。

但是我们刚刚学习了数组指针,并且数组名arr代表首元素的地址,我们很自然的猜想能否用数组指针来作为形参来接受二维数组的传参呢?

下面我们就用代码来证实一下:

#include <stdio.h>

void print(int(*arr)[5], int row, int col)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);	// arr[i][j] = *(*(arr + 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} };	// 二维数组

	print(arr, 3, 5);	// 数组名arr表示的是首元素的地址,即首个一维数组的地址。
	return 0;
}

执行结果为:

1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

估可以采用数组指针来完成二维数组的遍历,因为本质上二维数组名arr 是一个一维数组的地址,故可以用一个可以用来存储一维数组地址的指针来存储。

总结:二维数组传参,形参部分可以写成二维数组形式,也可以写成数组指针形式。

4.函数指针变量

4.1.函数的地址

我们上面说了数组名是数组首元素的地址,&arr是数组的地址,那么函数名和&函数和函数的地址是否有关系呢?

下面我们就直接输出函数名和&函数名观察两者之间的区别。

我们发现 函数名test 与 &test 值相同,由于函数地址就是一个地址,所以函数名与取地址函数时一样的,这里和数组有点区别。

4.2.函数指针的创建

指针变量申明分两步:

  1. 明确是一个指针 * + 指针名
  2. 明确指针的类型

所以函数指针的申明: (*p) 指针 * 指针名 + 函数类型 int  (int, int) = 一个函数的地址

如下面代码

#include <stdio.h>
int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int (*p)(int, int) = Add;
	return 0;
}

但是如果我们写成int *p(int, int) 就成了一个名为p返回类型为int的函数。因为 ( ) 的优先级比 * 高,所以 p 会优先与 ( ) 结合成一个函数,再确定返回类型 int * 型。

4.3.函数指针的使用

通过函数指针调用指针指向的函数。

#include <stdio.h>
int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int (*p)(int, int) = Add;

	int c = Add(2, 3);		// 函数名调用
	printf("%d\n", c);

	int d = (*p)(3, 4);		// 函数指针调用
	printf("%d\n", d);

	int e = p(4, 5);		// 函数指针调用
	printf("%d\n", e);	
	return 0;
}

由于上面讲到的 test = &test 所以 p = *p,所以再用函数指针调用指针指向的函数的时候 * 可以省略,也可以加很多个。

4.4.函数指针的拓展思考

4.4.1.有趣的题目

在《C陷阱与缺陷》这本书中有两段比较新奇的代码

代码1
 (*(void (*)())0)();
  • void (*)() 表示函数指针类型(去掉变量名就是对应的类型)
  • ( void (*)() ) 表示强制类型转换( "( )" 中间写类型,表示强制类型转换 ) 
  • ( void (*)() ) 0 表示将0强制类型转换成 void(*)() 的函数指针类型
  • 意味着我们假设 0 地址处放着无参,返回类型是void的函数
  • ( *( void (*)() ) 0 )() 表示调用0地址处的函数 ( (*p)() 表示调用指针p指向的函数)

但是这段是运行在微处理器的,是模拟开机时候的过程,正常我们写代码时是运行不了的并且也无意义。

代码2
void (*signal(int , void(*)(int)))(int);
  • void(*)(int) 表示函数指针类型,且有一个参数为int
  • signal 表示函数名 (因为signal先与后面的 "( )" 结合成函数,"( )"中的的是其两个参数)
  • 除去signal函数整体还剩下void(*)(int) 这表示函数指针类型
  • void (*signal(int , void(*)(int)))(int); 代表一个名为signal的函数,有整型和函数指针类型,并且返回一个函数指针类型的变量。
4.4.2.typedef关键字
4.4.2.1.typedef的用法

typedef可以用来重新定义类型,目的是为了将复杂的类型简单化。

C语言中已给int ,float,double,char 等类型,但是如果我们要自定义其他类型的话就要使用到typedef的功能。

typedef 重命名其实很简单,只需要将申明变量时,将变量名更替为新的类型名就行了。

比如如下代码所示就是一个简单的类型重命名

#include <stdio.h>
typedef unsigned int u_int;	// 重命名unsigned int 为 u_int
typedef int* ptr_t;		    // 重命名int*类型为ptr_t
int main()
{
	u_int a = 10;	// 申明 u_int 类型的变量即无符号整型变量
	ptr_t p = NULL;	// 申明 ptr_t 类型的变量即整型指针变量
	return 0;
}

这样我们在申明和使用这个类型的时候就比较轻松和简单了。

那么我们尝试一下将函数指针类型的重命名

#include <stdio.h>
typedef int(*pf_t)(int, int);	// 将 int(*)(int, int) 重命名为 pf_t
int main()
{
	pf_t pf = NULL;	// 申明一个 pf_t 类型的函数指针,即 int(*)(int, int) 类型的函数指针
	return 0;
}

我们上面知道将变量名去掉就是类型,所以 int(*)(int, int)就是一个函数指针类型,pf_t 顶替了原本申明变量时变量名的位子,所以 pf_t 就是这个类型的新的类型名,我们以后想用的时候,都可以直接用这个新的名字,极大简化代码,更好理解。

下面我就简单地展示 typedef 重命名变量的代码

#include <stdio.h>
typedef unsigned int u_int;
typedef int* ptr_t;		// 整型指针
typedef int** pptr_t;	// 二级指针
typedef int* arr_pt[5];	// 指针数组
typedef int(*parr_t)[5];	// 数组指针
typedef int(*pf_t)(int, int);	// 将 int(*)(int, int) 重命名为 pf_t
typedef int Elemtype;	// 自定义类型,方便灵活更改,可以是整型,字符型之类的也可以是一个结构体类型。
typedef struct SListNode
{
	Elemtype a;
	struct SListNode* next;	// 由于程序是向下执行的所以到这里还没重命名类型,不能直接用 SList*
}SList;
int main()
{
	struct SListNode s1 = { NULL, NULL };
	SList s2 = { NULL, NULL };	// 大大简化代码
	struct SListNode* L1 = NULL;
	SList* L2 = NULL;			// 大大简化代码
	return 0;
}
4.4.2.2.typedef 与 define 的区别

我们用一段代码来简单说明两者间的区别。

#include <stdio.h>
#define PTR_T int*
typedef int* ptr;
int main()
{
	int* pt1 = NULL, pt2 = NULL;
	PTR_T pt3 = NULL, pt4 = NULL;
	ptr pt5 = NULL, pt6 = NULL;
	return 0;
}

下面是调试的监视窗口,我们可以看到变量的类型。 

注意pt2是 int 变量, 因为define宏定义其实只是简单的字符替换,而typedef重命名是将其作为一个新的变量类型来运用。

4.5.函数指针的作用

函数指针一般在回调函数中发挥作用,这个我们在深入理解指针(4)会深入去讲。

5.函数指针数组

5.1.函数指针数组的创建

函数指针数组是一个数组,而这个数组里存储的是函数指针(函数的地址)。

申明变量分两步

  1. 明确要申明一个什么。   数组parr[ ]
  2. 明确申明的类型。          函数指针类型 int(*)(int, int)

所以函数指针数组申明为 int(* (parr[]) )(int, int),由于 [ ] 的优先级比 * 高,所以可以简化成 int(*parr[])(int, int)

由于数组的性质,所以函数指针类型必须要一样。 

如下代码就是简单的函数指针数组的创建

#include <stdio.h>
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(*)(int, int)

int main()
{
	int(*pf1)(int, int) = Add;	// pf1是函数指针变量
	int(*(pfarr[4]))(int, int) = { Add, Sub, Mul, Div };	// pfarr是函数指针数组
}

 5.2.函数指针数组的使用

如图就是函数指针数组的简单使用,省略了解引用(函数指针可以省略解引用访问,因为都是函数的地址)。 

6.转移表

函数指针数组的用途:转移表

举例:计算机的一般实现:

6.1.普通代码

#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 x = 0, y = 0;
	int input = 0;
	int ret = 0;

	do {
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
		default:
			break;
		}

	} while (input);

	return 0;
}

6.2.采用函数指针优化代码

但是我们会发现上面的代码中像如下代码一直进行重复,内容太过冗杂。

因此我们得用一个函数封装一下如图所示功能,但是由于Add Sub 等操作的不同,我们得采用一个函数指针变量来接受这些不同的函数地址,这样就可以在同个函数内完成不同的运算函数。 这就是回调函数的机制

下面是优化后的代码

#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 = 0, y = 0, ret = 0;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}
int main()
{
	int x = 0, y = 0;
	int input = 0;
	int ret = 0;

	do {
		menu();
		printf("请选择:");
		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;
		case 0:
			printf("退出程序\n");
		default:
			break;
		}

	} while (input);

	return 0;
}

6.3.采用函数指针数组(最优)

那如果我们采用函数指针数组来进行编写的话,代码会更加简约,并且后续添加功能更加方便。

代码如下:

#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 x = 0, y = 0;
	int input = 0;
	int ret = 0;
	int (*p[5])(int, int) = { 0, Add, Sub, Mul, Div };	// 函数指针数组变量
	do {
		menu();
		printf("请选择:");
		scanf("%d", &input);
		if (input > 0 && input < 5)
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = p[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("输入有误\n");
		}
	} while (input);

	return 0;
}

如果这篇文章对你有所帮助的话,不妨点个免费的赞和关注,感谢支持!

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值