[指针]从浅到深理解指针-第三课

一、字符指针变量

  在指针的类型中我们知道有一种指针类型为字符指针char * ;一般我们是如下使用的:

int main()
{
char c = 'w';
char* pc = &c;
printf("%c",*pc);//结果为w
return 0;
}

  但是还有一种用法:

//代码一
int main()
{
 const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
          //并不是。因为"hello bit."是一个常量字符串,不可以更改,这里的赋值是将字符串中的首字符的地址赋值给pstr
 *(pstr+2) = 'L';
 
 printf("%s\n", pstr);
 printf("%c",*pstr);//结果为h,说明了字符串只把首字符'h'的地址赋值给了pstr
 return 0;
}

//代码二
int main()
{
  char arr[] = "hello bit.";//这⾥是把⼀个字符串放到字符数组里
  //这里的字符串是可以修改的
  arr[0]='H';//此时下标为0的元素会被修改为H
  
  printf("%s",arr);//“printf”: 格式字符串“ %s ”需要类型“ char * ”的参数
  
 return 0;
}

  让我们调试一下:

在这里插入图片描述
  对于代码一而言,我们发现,我们无法通过指针pstr去修改常量字符串的内容;但是对于代码二来说,"hello bit."不是常量字符串,而是将字符串存放在字符数组里,所以其值可以被修改。
在这里插入图片描述

总结小知识:
1.使用%s打印字符串的时候,只需要提供首字符的地址就行
2.常量字符串的内容是不可以被修改的

  《剑指offer》中收录了⼀一道和字符串相关的笔试题,通过上面的知识,我们一起来学习一下:

#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");//代号1
 else
 printf("str1 and str2 are not same\n");//代号2
 
 if(str3 ==str4)
 printf("str3 and str4 are same\n");//代号3
 else
 printf("str3 and str4 are not same\n");//代号4
 
 return 0;
}

  这里的答案我就直接给出来了是代号2代号3处的代码被打印出来了,那么为什么是一个相同,另一个不相同呢?

在这里插入图片描述

  代号2的原因是:str1[]和str2[]是不同的数组,会开辟不同的空间去存放字符串"hello bit."
  代号3的原因是:因为"hello bit."是常量字符串,不可以被修改,那么内存中只需要保留这一份就可以了,指针str3和str4指向同一个地方。(内容相同的常量字符串只需要保存一份就够了!)

在这里插入图片描述

注意:C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。


二、数组指针变量

  通过类比,我们可以知道:

//字符指针  --- char * ---指向字符的指针 --- 字符指针变量中存放字符变量的地址
char ch = 'w';
char* pc = &ch;

//整型指针  --- int * ---指向整型的指针 --- 整型指针变量中存放整型变量的地址
int a = 10;
int* p = &a;

  那么什么是数组指针?答:数组指针 - 指向数组的指针 - 数组指针变量中存放数组的地址。

int main()
{
int arr[10] = {}1,2,3,4};//数组的类型是int [10],元素类型是int
int (*p)[10] = &arr;//此时p就是数组指针,p中存放的是数组的地址;指针p的类型是int (*)[10]
//但不是int* p[10]数组指针的意思,而是指针数组的意思,注意区分
return 0;
}

  区分是指针数组还是数组指针的关键在于:p与谁结合
  1.如果和解引用操作符(*)结合,则是数组指针。理解:首先(*p)表明了p是指针,然后将(*p)去掉,剩下 int [10],表明指针p指向的是数组(数组的元素类型是int ,元素个数是10个)。
  2.如果和[ ]结合,则是指针数组

  举个列子:

int* p1[10];//指针数组
int (*p2)[10];//数组指针

  这时我们应该已经知道了多种的数据类型,那么我们就可以对指针数据类型的意义数组名的理解的内容做补充。

arr --- int*               arr+1      跳过4个字节  --- arr+1*sizeof(int)
&arr[0] --- int*          &arr+1      跳过4个字节  --- arr+1*sizeof(int)
&arr --- int(*) [10]      &arr+1      跳过40个字节  --- &arr+1*sizeof(int [10]) --- &arr+1*40 ---即偏移40个字节
//指针类型决定了指针+-整数的偏移量为多少

  来个练习:

char * ch[5];
pc   = &ch;//此时pc的类型应该怎么写?

  答案:

char* ch[5];
char* (*p) [5] = &ch;//那么怎么更具体的理解数组指针?
//(*p)中*用来说明p是一个指针变量,是用来存放地址的,而&ch是取出了数组的地址放在了p里面,
//[5]代表着指针变量p指向的数组的元素个数,char*代表着指针变量p指向的数组的元素类型

总结

int (*p) [10] = &arr;
  1. int ----------p指向的数组的元素类型
  2. *p ----------p是数组指针变量名
  3. [10] ----------p指向数组的元素个数

三、二维数组传参的本质

  为了引出二维数组的本质,先让我们对数组指针进行一些代码测试:

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr;
int i = 0;
for(i = 0;i < 10;i++)
{
printf("%d ",(*p)[i]);//结果为 1 2 3 4 5 6 7 8 9 10 打印出数组的元素
}
(*p)[i] = arr[i];//其实*p 就是 arr,p指针变量是存放着数组的地址的,那么*p 就找到数组arr了,所以*p 就是 arr

//类比一下,更好理解

int a = 10;
int* p = &a;//p存放的是a整型变量的地址
*p;//解引用操作*p就是a,那么对*p进行操作,也就是对a进行操作
*p = 0;//等同于a = 0 

  通过代码可以看到,我们可以使用数组指针来访问数组但是这样使用不是更麻烦吗?就不可以使用int * p = arr;来的更好吗?确实,数组指针一般不是这么用,这就需要讲到二维数组传参的本质了。

  下面来进行二维数组传参的代码:

#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);//二维数组数组名进行传参,将arr数组的内容打印出来
 return 0;
 }
那么二维数组的数组名是如何理解的呢?我们进行类比看看。

  一维数组传参:数组名是首元素的地址,一维数组在传参的时候,其实传递的是首元素的地址。(函数的参数可以写成数组,也可以写成指针)

那么二维数组呢?

  二维数组传参:其实二维数组的数组名也是数组首元素的地址,但是二维数组传参的时候,其实传递的是第一行的地址

那么什么是第一行呢?下面看图片理解

在这里插入图片描述

  此时我们在对二维数组传参的代码中形参的部分进行修改:

#include <stdio.h>

 void test(int (*arr)[5], int r, int c)//既然传递的是第一行(一维数组)的地址,则接收是用数组指针接收它
 //第一行中数组元素个数为5个,元素类型为int ,则数组指针写为int (*arr)[5]
 {
 	int i = 0;
 	int j = 0;
 	for(i=0; i<r; i++)
	{
 		for(j=0; j<c; j++)
 		{
 		printf("%d ", *(*(arr+i)+j));//*(*(arr+i)+j)  等价于 *(arr+i)[j]
 		//*(arr+i)代表着第i行的数组名,[j]代表访问数组的下标为j的元素
 		//arr[i]是第i行的数组名,数组名又代表数组首元素的地址,arr[i]表示是&arr[i][0]
 		}
 		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);//二维数组数组名进行传参,将arr数组的内容打印出来
 return 0;
 }

  在代码中,将int arr[3][5]改为了int (*arr)[5],这就是二维数组传参的本质就是如此。
在这里插入图片描述


四、函数指针变量

  变量有地址,数组有地址,函数是否也有地址呢?其实也是有的。

int Add(int x;int y)
{
return x+y;
}
int main()
{
printf("%p",&Add);
printf("%p",Add);//打印的两个地址是一样的
return 0;
}

  这时可能有同学会想,数组arr中&arr和arr是有区别的,那么函数Add和&Add也是有区别的。告诉你们,其实&函数名和函数名都是函数的地址,没有区别

4.1函数指针变量的创建

  既然我们知道了函数也是有地址的,那就得用指针来存放函数的地址,那数组指针来类比一下就简单了,形式如下:

int arr[10];
int (*parr)[10] = &arr;//数组指针的创建

int Add(int x , int y);
int (*pf)(int , int) = &Add;//函数指针的创建
int Add(int x;int y)
{
return x+y;
}
int main()
{
int (*pf)(int ,int) = &Add;//pf  --- 函数指针变量,其形参部分的x,y可以加也可以不加
}

  去掉函数指针变量的名字就是函数指针变量的类型了:int (*)(int , int)

4.2函数指针变量的使用

  了解函数指针变量后,我们就可以去使用函数指针变量了

int Add(int x, int y)
{
	return x+y;
}

int main()
{
	int (*pf)(int, int) = &Add;
	int ret = (*pf)(4, 5);//(*pf)(4,5) 等价于 Add(4,5)
	printf("%d ", ret);//结果为9
	return 0;
}

  因为pf是函数指针,存放了函数Add的地址,那么通过解引用操作(*pf)找到的就是Add。但是再看下面的一串代码,可以从中发现什么问题呢?

int Add(int x, int y)
{
	return x+y;
}

int main()
{
	int (*pf)(int, int) = Add;
	int ret = pf(4, 5);//修改的位置
	printf("%d ", ret);//结果为9
	return 0;
}

  当我们把pf前面的解引用操作符给去掉后再执行代码,发现代码依旧运行,而且结果后修改之前一样。那么我们就得到一个结论:函数指针变量pf前有没有解引用操作符*都是一样的(也不管解引用操作符*有几个)

  可以这样来理解这个结论:因为&AddAdd是一个意思,代表着函数的地址,那么当代码为int (*pf)(int, int) = Add;这样的时候,我们将其转化为int (*pf)(int, int) = &Add;,转化后知道pf存放的就是函数Add的地址,即pf代表着函数Add的地址,与Add也代表着函数Add的地址一样,得出pf = Add所以可以Add(4,5)来调用函数,那么就可以pf(4,5)来调用函数

4.3两段有趣的代码

//代码一
int main()
{
   (  *(void (*)()   )  0)  ();//(void (*)()  0 )意思是将0地址,强制类型转化为void (*)(),函数指针
}

  代码一代表的意思是:一次函数调用,调用0地址处放的那个函数(函数地址的值为0,所以地址0(地址被当函数指针好理解)指向那个函数)。0地址处放的这个函数是没有参数,返回类型是void,最左边的*可以去掉,也可以不去掉

//代码二
int main()
{
void (*signal(  int , void(*)(int)  )  )  (int);
}

  代码二代表的意思是:一个函数的声明。其中函数名为signal;形参部分为( int , void(*)(int) ),形参中一个类型为int整型,另一个类型void(*)(int)函数指针类型;返回类型为void(*)(int)函数指针类型。那么不应该是写成这样吗?void (*) (int) signal( int , void(*)(int) ) ;但是语法不通过。

4.3.1 typedef关键字

  typedef关键字的作用:重新定义名字

typedef  unsigned int uint;

int main()
{
	unsigned int a;
	uint         b;

	return 0;
}

  上面我们将unsigned int简化为了uint

//1
typedef int* pint_t;//对整型指针类型重命名

//2
typedef int(*parr_t)[6] ;//把int(*)[6]重命名为了parr_t
//与一般的定义不完全一样,注意,比较特殊
int arr[6] = {0};
int(*p)[6] = &arr;//p是数组指针变量
parr_t p2  = &arr;//p2是数组指针变量,

//3
typedef int(*pf_t)(int ,int);//把int(*)(int,int)重命名为了pf_t,对函数指针类型重命名
int Add(int x, int y)
{
return x+y;
}
int main()
{
int (*pf1)(int ,int) = Add;
pf_t pf2 = Add;
return 0;
}

  通过学习了typedef关键字,我们就可以对那两段有趣的代码中的代码二进行改变。

typedef void(*pf_t)(int);

int main()
{
//void (*signal(  int , void(*)(int)  )  )  (int);
pf_t signal(int ,pf_t);
}

  此时就可以把void (*signal( int , void(*)(int) ) ) (int);修改为pf_t signal(int ,pf_t),这样就更好理解了。


五、函数指针数组

  函数指针数组该如何理解并写出形式呢?

int* arr[6];//整型指针数组
char* arr[5];//字符指针数组
//那么函数指针数组:一个一维数组,每一个元素时函数指针,该指针指向函数

  在理解了函数指针数组的意思上,我们在通过代码学习如何创建函数指针数组和使用函数指针数组。

#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()
{
//用4个函数指针分别指向4个函数
int (*pf1)(int,int) = &add;
int (*pf2)(int,int) = &sub;
int (*pf3)(int,int) = &mul;
int (*pf4)(int,int) = &div;
//此时我们可以看出这4个函数指针的类型都为int(*)(int ,int )
//那么我们可以把他们都放在同一个数组中
//需要注意的是:想把数据放入到同一个数组中,其数据类型必须一致相同

int(*pf[4])(int ,int) = {pf1, pf2, pf3, pf4};//函数指针数组的创建

int i = 0;
for(i = 0;i < 4;i++)
{
int ret = pf[i](6,2);
printf("%d\n",ret);
}
return 0;
}

  让我们解读int(*pf[4])(int ,int)。函数指针数组其实很好理解,我们在函数指针的基础上int(*pf)(int ,int),然后再加上数组[6],就变成int(*pf[4])(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 main()
{
	int x, y =0;
	int ret = 0;
	int input = 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("结果是:%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;
		default:
			break;
		}
		if (input < 0 || input>4)
		{
			printf("输入错误!请重新输入\n");
		}
	
	} while (input);
	printf("已成功退出。\n");
	return 0;
}

  上面我们实现了一个可以进行加减乘除功能的模拟计算器,让我们想一想,如果计算器需要更新添加新的功能,如&|^等操作,代码中的分支语句switch中我们要不断地加代码,会使代码量很多,这种的效果不是很好。那么有什么办法可以对其进行处理简化了?这就可以使用我们上面学习的函数指针数组

#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 main()
{
	int x, y = 0;
	int ret = 0;
	int input = 0;
	int (*pf[5])(int, int) = { 0,add,sub,mul,div };//小技巧:我们在数组下标0出,用0数据代替,使得add的下标为1,
	//以此类推下去,div的下标为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 >= 1 && input <= 4)//注意:不能写成 1<= input <= 4
		{
			printf("请输入二个操作数:");
			scanf("%d %d", &x, &y);

			ret = pf[input](x, y); // pf[input](x,y) 也可以写成(*pf[input])(x,y),和函数指针的用法一样
			printf("结果为:%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出成功!\n");
		}
		else
			printf("请重新输入!\n");
		
	} while (input);

	
	return 0;
}

  通过使用函数指针数组,我们可以把代码简化成很简单的形式。

  注意:在if条件语句中的input判断的地方,我们不能写成if(1<= input <=4),这样是错误写法
  那是为什么呢?因为在if(1<= input <=4)中,我们前面对input进行赋值时赋值了大于4的数字,他也会判断该条件为真(非0的数),其中input会先和1比较大小,如果成立,则1<=input为真(常用数字1表示真),那么1肯定小于等于4,该条件肯定成立。即二个小条件成立,整个1<= input <=4条件成立,所以即便input赋的值大于4,还是会执行该条件成立里面的代码,这与我们的想法不符合。


谢谢观看!希望以上内容帮助到了你,对你起到作用的话,可以一键三连加关注!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!

  • 50
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值