C语言指针的简单介绍与入门基础(3)

目录

1.字符指针变量

 2. 数组指针变量 

2.1 数组指针变量是什么?

2.2 数组指针变量与指针数组的辨析。

2.3 小结

 3. ⼆维数组传参的本质

 3.1二维数组的传参

3.2二维数组的一些有趣的形式

  4. 函数指针变量

4.1 函数指针变量的创建

 4.2 函数指针变量的使用

4.3 typedef关键字

5. 函数指针数组

6. 转移表 

6.1转移表的创建思路

 6.2 一般思路与运用转移表的计算器的

6.2.1  代码对比

6.2.2 源代码(有需要请自取)

6.2.2.1  运用转移码: 

6.2.2.2  一般思路:

 7. 最后总结


 

 

1.字符指针变量

1.1 什么是字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* ,我们常用它来定义字符串,但我们也可以用char类型的数组来存储数据。

例如:

4ca89483d93841ed8167192612552f7b.png

 上图中我们分别用char类型的数组arr1[ ]和char*类型的arr2定义一个字符串“Bite”,可以看出它们都能正确打印出字符串Bite。第一个是将Bite放进arr1中,那是不是代表第二个Bite也是整个放进arr2中呢?

我们进一步进行剖析。

026ef21876554571a8e6c733a0c01fae.png

 

我们发现,char* 本质是把字符串 Bite的⾸字符的地址放到了arr2中。

那就会有细心的同学发现,啊你这个char*不是与char类型的数组是一回事吗?不过是存储的地址不同罢了。诶,这不就是正是验证了我上一篇指针介绍里的数组名的本质了吗?字符串本质上是多个字符组成的字符数组。不熟悉的同学可以移步到我的上一篇文章——C语言指针的简单介绍与入门基础(2)。

1.2 char[ ] 和char*分别定义字符串的区别

但这两者就真的完完全全没有任何区别了吗?还真不是,我们再看一组代码:

336e90660b114f588f494b16674c5860.png

我们发现代码来到*(str2 + 1)这行时报错了。这是以因为

char*定义的字符串是常量字符串,是不能被更改的!!!

这是它跟用 char[ ] 定义的字符串的一个很大的区别,char类型数组定义的字符串的字符是一个个元素,是可更改的。但char*定义的字符串是常量字符串,是不可被更改的!!!这个区别在以后我们写代码时要用 char[ ] 还是 char* 来定义时非常重要,要细细考虑。

我们继续第二组代码 

int main()
{
	char arr1[] = "Bite";
	char arr2[] = "Bite";

	char* arr3 = "Bite";
	char* arr4 = "Bite";

	if (arr1 == arr2)
		printf("same\n");
	else
		printf("not same\n");

	if (arr3 == arr4)
		printf("same\n");
	else
		printf("not same\n");

	return 0;
}

让我们运行下,看看结果

aac1167430a64c84a746f72526758086.png

可能同学们看到这里云里来雾里去的看不懂,但是如果我们进入调试,一切就真相大白

6ae53c68c8924e61bc8a6fe9559bf17d.png

进入调试后我们发现,arr3和arr4是同一个地址,但arr1和arr2却是不同的

总结:

这里arr3和arr4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域, 当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以arr1和arr2不同,arr3和arr4相同。

简单来说就是,相同的常量字符串,没必要保存两份,因为常量字符串不能被修改,所以大家共同一份是能能满足的,这样也是节省空间。 

 

 2. 数组指针变量 

2.1 数组指针变量是什么?

之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。

那么数组指针变量是指针变量?还是数组?

答案是:指针变量。

我们已经熟悉:

• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。

• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。

那数组指针变量就应该是:存放的是数组的地址,能够指向数组的指针变量。  

有同学就好奇了,这数组指针变量长什么样呢?它跟之前我们学的指针数组有什么区别的?头都大了。让我为大家一一讲解,

976d88fe442041f89e9e7b641b71a079.png

上面这行代码就是数组指针变量的庐山真面目,

解释:p先和*结合,说明p是⼀个指针变量的变量,然后指着的是指向⼀个大小为4个整型的数组。所以 p是⼀个指针,指向⼀个数组,叫 数组指针。

这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

如果没有用()的话,那它就变成一个指针数组。

 

 

2.2 数组指针变量与指针数组的辨析。

b65940dfcec74bd9898d46551d6ffc58.png

  可能还是有同学看不懂,没事,我们一点一点来剖析:

int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 0 };
int arr3[] = { 0 };
int *p1[3] = { &arr1, &arr2, &arr3 };

我们先分别定义三个不同的数组,并将其数组名地址(即首元素地址)存入p1(指针数组)中,进入调试中我们看到,p1中分别存储着三个数组的首元素地址,而不是整个数组

172aa2319eb74d1f8403e407120cea92.png

接着我们看数组指针

729e97e0ff1d4f9eb8043e50d0ab4c62.png

 如果我们仿照p1中那样去存放*p2,系统则会报错并提醒我们p2的初值太多,这首先说明第一点,*p2一次只能存放一个数组地址。

这次我们只存放一个数组arr1,进入调试,我们清楚看到,arr1中的每个元素都存放在*p2中

6f6c475e3f85463aad871be17070a204.png

 这时候我们不免脑洞大开,那能不能像打印数组那样把*p2中存放的元素一一打印出来呢?我们试一试。

int(*p2)[5] = &arr1;
printf("%d\n", *(*p2 + 0));
printf("%d\n", *(*p2 + 1));
printf("%d\n", *(*p2 + 2));
printf("%d\n", *(*p2 + 3));
printf("%d\n", *(*p2 + 4));

 结果显而易见,还真的可以,

cbd28a03a30c46daaba324cc3e2c5839.png

 这时候就有同学又要说了,哎呀,你这搞来搞去不又是回到最开始的数组那边去了。其实还真是

96f157248ccd45c3b92db5d4d52c5a90.png

 上面&arr1和p2的类型是完全一样的。

别担心,区别还是有的。请看下列代码

int arr1[] = { 1,2,3,4,5 };
int(*p2)[5] = &arr1;
int(*p3)[3] = &arr1;
int(*p4)[8] = &arr1;

 前面的p2的元素个数是我们正正好卡着arr1的元素个数来设置的,但如果没有刚刚好呢?它会出错吗?让我们调试一下。

f72c1fbf73474bf3a4a1da02ad3d7252.png

 

2.3 小结

综上,我们做个小总结

如果我们把数组中的各个元素比作车厢,那么数组就是一整列火车,指针数组(int *p1[ ])就时专门只停放火车头的地方;而数组指针变量就是停放一整列火车的地方,并且地上有着指向各个车厢的标签,当标签比车厢少,即点到为止,如果标签比车厢多,则多余的标签会随意指向其他地方。

下面是数组指针类型的解析:

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

 

 3. ⼆维数组传参的本质

 3.1二维数组的传参

上面我们不惜花费了那么大的篇幅来介绍数组指针变量已经他与指针数组的区别,定是数组指针有它存在的重要性。有了对数组指针变量的初步理解,我们便紧接着来学习二维数组的本质了,

⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。(首元素就是的第一行,首元素的地址就是第一行的地址,第一行的地址就是一维数组的地址)

如果你还是觉得很拗口,那么我i再次用火车来举例。

aec7fe18b2ac4df0800970683c2e9f06.png

假如此时有一个3*5的二维数组,我们可以将它拆分成甲、乙、丙三列火车(三个元素),而每列火车上又各有五节车厢(5个元素)。此时,二维数组的首元素地址就是甲(第一行)的地址。

所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类 型就是数组指针类型 int(*)[5] 那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀ ⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。

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

971bfae601fe4b16901c5cac01d22790.png

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

 

 

3.2二维数组在内存中的存储方式

我们在最开始学习二维数组时知——定义二维数组时第一个[ ]可以置空,而第二个不可以。因为在内存中,二位数组是线性切连续存放的。我们告诉计算机第一行有几个元素(即有几列),计算机会自动算出一共要分成几行;反过来,你告诉计算机有几行,计算机并不能算出一共要分成几列。

b37ae6167eea4c0f986ccf50298556ad.png

 

3.3 二维数组的模拟 

 其实在C语言中,计算机系统在识别一维数组和二维数组时都是将其转换成指针的形式。一个例子,一个二维数组arr[ i ] [ j ],计算机会将其拆解成*( arr + i ),然后再识别成*(*(arr + i )+ j );

 

  4. 函数指针变量

4.1 函数指针变量的创建

什么是函数指针变量呢?

根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,

我们不难得出结论: 函数指针变量应该是⽤来存放函数地址的,

未来通过地址能够调用函数的。

这句话非常重要,接下来大有用处。

如果说函数指针变量存放的是函数的地址,那是不是就证明函数是有地址的?让我们实验出真知。

01f8282b963a4c509bd96cc19a1f7f95.png我们用上面二维数组传参用的test函数做实验,可以看出函数test也是有自己的地址的。函数名就是函数的地址,我i们可以通过&函数名的⽅式获得函数的地址。

如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针非常类似。

函数指针类型解析:

  1. int   (*pf3)   (int x, int y)
  2.   |        |          ------------
  3.   |        |               |
  4.   |        |             pf3指向函数的参数类型和个数的交代
  5.   |       函数指针变量名
  6.   pf3指向函数的返回类型
  7.  
  8. int  (*)  (int x, int y)   //pf3函数指针变量的类型

下面是一些函数指针变量的定义: 

void test()
{
	printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;


int Add1(int x, int y)
{
	return x + y;
}
int(*pf3)(int, int) = Add1;
int(*pf4)(int x, int y) = &Add1;//x和y写上或者省略都是可以的


int Add2(int a, int b)
{
	int c;
	c = a + b;
	printf("%d\n",c);
}
int(*pf5)(int, int) = Add2;
int(*pf6)(int x, int y) = &Add2;//x和y写上或者省略都是可以的

 

 4.2 函数指针变量的使用

通过函数指针调⽤指针指向的函数。我们用上面举例的三组代码来做演示。

c54eafafc58f44409a7264830b49e5f6.png 因为 int  (*)  (int x, int y)  是函数指针变量的类型 ,

又因为函数指针变量存储的是函数的地址,且 通过地址能够调用函数

所以我们在mian函数里面声明函数指针变量后便可直接调用函数。 

 

4.3 typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。

⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:

  1. typedef unsigned int uint;
  2. //将unsigned int 重命名为uint 

如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:

1 typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别:

⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

1 typedef int(*parr_t)[5];

//新的类型名必须在*的右边

函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:

2 typedef void(*pfun_t)(int);

//新的类型名必须在*的右边

那么要简化代码2,可以这样写:

  1. typedef void(*pfun_t)(int);

我们再利用上面的Add2函数来做个实战例子: 

6be819021e4d4cdc85628bfd2b161c5c.png

可以看出,用由typedef重命名的hs来定义的pf7与pf5和pf6是等效的。

typedef关键字在后面的学习中非常重要。

 

 

5. 函数指针数组

既然函数也有地址,那按常理函数的地址我们也可以放进数组里面,我们称之为函数指针数组

接下来就是告诉各位函数指针的数组该如何去定义。

int (*parr1[ 3 ])( )

 parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢?

是 int (*)() 类型的函数指针。

 

6. 转移表 

6.1转移表的创建思路

这一章学了这么多,咱们总要搞一点大的来玩玩把?

那今天学的函数指针数组能怎么用呢?

假如现在我们想要写一个计算器,大家会怎么写呢?是不是就先定义加减乘除四个函数(add、sub......),然后就用switch函数,1,2,3,4对应加减乘除,每个模块的语句都大同小异。这样会导致代码非常冗余,效率也不高.那有没有什么方法可以提高效率呢?当然有,就是运用我们今天学的知识,创建一个转移表。

  1. 先创建四个函数
  2. 分别将四个函数名的地址存入函数指针数组中
  3. 通过选中函数指针数组中对应的函数名,从而调动相对应的函数

45e823fe8dd745539fe18412a0443fa8.png

 

 6.2 一般思路与运用转移表的计算器的

6.2.1  代码对比

左边为一般思路,右边为运用了转移表。代码量谁多谁少一目了然。

50e328c8165a438cb50b986116755c19.png

6.2.2 源代码(有需要请自取)

6.2.2.1  运用转移码: 

//先分别定义加减乘除四个函数
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 (*parr1[5])(int x , int y) = {0, add, sub, mul, div };

int main()
{
	int input = 0 , ret = 0;
	int a =0, b = 0 ;
	do
	{
		printf("请选择\n");
		printf("1,加法       2,减法\n");
		printf("3,乘法       4,除法\n");
		printf("0,退出\n");
		scanf("%d", &input);

		if (input >= 1 && input <= 4)
		{
			printf("操作数\n");
			scanf("%d , %d", &a, &b);
			ret = (*parr1[input])(a, b);
			printf("%d",ret);

			printf("\n");
			printf("\n");
			printf("\n");
		}
		else if (input = 0)
		{
			printf("退出计算器");
		}
		else
			printf("输入有错误");

	} while (input);

	return 0;
}

6.2.2.2  一般思路:

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

 

 7. 最后总结

这一章的量会比较多一点,也会比较啰嗦一点,也确实是这一节的知识点还是比较多且重要的。特别是typedef关键字那块,看似与这节课无关,实则关系到后面数据结构等知识的理解和阅读。

希望这一章节对屏幕前的各位有所帮助,也特别感谢能坚持看到结尾的同学。如有差错与不足,还请多多包涵,并私信或评论区留言。由衷感谢。

 


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值