[C语言]----深入理解指针(3)

哈喽哈喽,同志们,咱们的深入理解指针已经来到了(3),其实指针这一块学起来还是要废些功夫的。但是,希望我们都能坚持下去,不管是指针,还是C语言,还是计算机这条路,只要坚持下去,相信我们都势如破竹!!!
在这里插入图片描述

1.字符指针变量

  • 一般情况下,我们要创建一个字符指针会这样使用:

#include<stdio.h>

int main()
{
	char ch = 'b';
	char* pc = &ch;
	printf("%c\n", *pc);
	*pc = 'u';//改变ch的值
	printf("%c\n", ch);
	return 0;
}

代码输出的结果:
在这里插入图片描述
我们还有一种使用方式(拿一个字符指针指向字符串):

int main()
{
	char* pc = "hello world";
	return 0;
}
  • 那么请问,这个是表示把hello world放到p里面去了吗?
  • 不是的哈,其实在这里hello world这个字符串就相当于是一个字符数组,我们是把这个字符数组的首元素的地址放进了p里。
    //char arr[] = “hello world”;
    //char* p = arr;
    和这两行代码是非常非常之相似的。
  • 我们接下来可以来验证一下:
int main()
{
	char* pc = "hello world";//这个是表示把hello world放到p里面去了吗?
	printf("%c\n", *pc);
	return 0;
}

代码运行结果:
在这里插入图片描述
这个运行结果表示,果然和我们所说的一样!

  • 我们可以将字符串想象成这样一个字符数组来理解,但是其实它们是有差异的。
  • 我们知道字符串是可以改变的,但是字符串是一个常量字符串,而常量字符串是不能修改的.
    在这里插入图片描述
  • 大家可以看到,当我们要对字符串的第一个字符进行修改的时候,我们调试一下就发现编译器报错了(写入访问权限冲突).
  • 那么既然这个字符指针变量是不能被修改的,我们何不把它用const修饰一下,对其进行限制呢?
int main()
{
const char* pc = "hello world";//这个> > 是表示把hello world放到p里面去了吗?
printf("%c\n", *pc);
*pc = 'b';
return 0;
}
  • 大家还记得我们在深入理解指针(1)里讲的const修饰指针变量吧!我们来回忆一下,这里我们将const放在( * )前修饰的是 ( *pc ) , 所以当我们想通过p来改变其所指向的内容就不能实现了。
  • 这样加了const修饰了之后,我们一旦对其修改,编译器就会报错。
  • 我们如果想要使用指针打印这个字符串有两种方式:
#include<stdio.h>
#include<string.h>

int main()
{
	const char* pc = "hello world";
	//第一种
	printf("%s\n", pc);
	
	//第二种(使用循环遍历整个字符串)
	int n = strlen(pc);
	int i = 0;
	for (i = 0; i < n; i++)
	{
		printf("%c" , * (pc + i));
	}
	return 0;
}
  • 接下来哈,我们来看一道题,这道题是来自《剑指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 和 str2 ,这是我们的两个在数组中的存储:
    在这里插入图片描述

  • 不管这两个数组的内容是不是完全相同,只要我们定义了,那么内存就会开辟一个空间用来存放这个数组。
  • 我们看到第一个 if 语句中的判断条件是( str1 == str2 ),我们又知道 str1 和 str2 都是数组名,而数组名就是数组首元素的地址,既然两个数组都在内存中开辟了空间,那么它们的地址当让在内存中是不一样的。
  • 第二个 if 语句的判断条件是( str3== str4 ),我们可以看到 str3 和 str4 是指针,指向的是常量字符串,在前面我们有讲到,常量字符串是不能修改的,那么,内容相同的常量字符串有必要在内存中保存两份吗?
  • 答案是不会,内存中只会保存一份。所以它们指向的内存中的同一块空间,所以它们两个是相等的。
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/235142171c4f45fb91bc4275bae908d3.png

2.数组指针变量

2.1数组指针变量是什么

  • 还记得我们前面讲到过指针数组,通过讲解,我们了解到了,指针数组其实就是存放指针的数组。那么,数组指针是数组呢???还是指针呢???
  • 接下来我们还是通过类比来看一下,它到底是数组还是指针?
    字符指针----char* p----指向字符的指针,存放的是字符的地址
    整型指针----int* p-----指向整型的指针,存放的是整型的地址
    数组指针----------------指向数组的指针,存放的是数组的地址???//yes yes yes
  • 数组指针其实就是一种指针变量,是存放数组地址的指针变量.

2.2数组指针变量怎么初始化

  • 不知道大家还记不记得我们当时讲对数组名的理解:

    //arr 是整个数组首元素的地址
    //&arr[0] 也是数组首元素的地址
    //&arr  取出的是数组的地址
    
  • 那我们该怎样去定义一个数组指针呢?

int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;
	/*
	这样定义我们该怎么去理解呢?
	1.(*p)代表的是它是一个指针变量
	2. [10]代表的是它指向的是一个数组,并且这个数组有10个元素
	3. int 代表它指向的数组是int类型的
	*/
	return 0;

}
  • 我们想一下这个指针变量的类型是什么呢?
  • 整型指针变量的类型是( int* )
    字符指针变量的类型是( char* )
    那么我们上面定义的数组指针变量的类型就是( int * [10] )
  • 还记得我们前面有讲到一个+1运算吗?

arr
arr+1
二者相差4个字节

&arr[0]
&arr[0]+1
二者相差4个字节

&arr
&arr+1
二者相差40个字节

  • 今天我们学了数组指针,就可以通用数组指针类型来解释它为甚是40.
  • 原因就是类型决定的,它是int(4个字节)类型的,有10个元素,所以是40个字节.

3.二维数组传参的本质

  • 通常情况下,数组指针是在二维数组中使用的.
  • 如果我们要打印出一个二维数组,要如何实现呢?
//通常的写法
void print(int arr[3][5], int r, int l)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < l; 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;
}
  • 我们已经学过,一维数组名是数组首元素的地址,那么二维数组名是谁的地址呢?
  • 我们之前学二维数组的时候,我们说二维数组是一维数组的数组,我们可以把二维数组看成是一个一维数组,而二维数组的每一行元素看成是一个元素,也就是说,我们把一维数组作为数组的元素,这个数组就是二维数组.
    在这里插入图片描述
  • 那么,我们是不是可以说,二维数组的数组名就是二维数组第一行的地址.
  • 那接下来我们就可以利用数组指针来实现二维数组的输出.
//数组指针的写法
void print(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} };
	print(arr, 3, 5);//打印二维数组
	return 0;
}

4.函数指针变量

4.1 函数指针变量的创建

  • 要搞明白函数指针变量是什么之前,我们首先要了解什么是函数指针,我们依然采用类比的方式来理解:

1.字符指针 ——指向字符——存放字符的地址
2.整型指针——指向整型——存放整型的地址
3.数组指针——指向数组——存放数组的地址
4.函数指针——指向函数——存放函数的地址???//yes yes yes

  • 很好,肯定又有人疑惑了函数这玩意儿的地址是什么,不会是( &函数名 )吧???那么离谱???
  • 是的是的,yes yes就是它。
  • 来吧,写一个小代码来感受一下吧!
//计算和
int Add(int y, int x)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	Add(a, b);
	printf("%p\n",&Add);//打印一下,看一下是不是
	return 0;
}
  • 请看打印结果:
    在这里插入图片描述
  • 那是不是和数组名一样,函数名是不是也是函数首元素的地址呢?
//计算和
int Add(int y, int x)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	Add(a, b);
	printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

看一下结果:

在这里插入图片描述

  • 呦呵!!!果真是哈!!!两个地址一模一样诶!!!你是不是在想,我果真是拿捏住了 ! ! ! 哈哈哈哈,其实,这是给你挖的坑。
    在这里插入图片描述
  • 数组中数组名是数组首元素的地址,但是函数中( 函数名 )和( &函数名 )都是函数的地址,在函数中不存在什么函数首元素的哈!
  • 那我们该怎样去定义一个函数指针变量呢?
	int (*pf)(int, int) = Add;
	// pf  就是一个函数指针变量。

其实,我们大家仔细观察可以发现,函数指针变量的书写和数组指针变量的书写是十分相似的。

int arr[5] = { 0 };
int(*pa)[8] = &arr;
int (*pf)(int, int) = &Add;
  • ( *pf )表示 pf 这个变量是指针变量
  • (int, int)中的圆括号表示pf指向的是一个函数,圆括号里面的int,int表示的函数的两个参数类型。
  • int 表示这个函数的返回值是 int 类型的。

4.2 函数指针变量的使用

  • 我们将函数的地址存起来是为了通过这个地址找到它,然后来调用这个函数。接下来我们就举个简单的例子来使用一下。
int Add(int y, int x)
{
	return x + y;
}

int main()
{
	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = &Add;
	
	int r1 = (*pf1)(3, 5);
	int r2 = (*pf2)(3, 5);
	
	int r3 = pf1(3, 5);
	int r4 = pf2(3, 5);
	
	printf("%d\n", r1);
	printf("%d\n", r2);
	printf("%d\n", r3);
	printf("%d\n", r4);
	return 0;
}
  • 代码运行结果如下:
    在这里插入图片描述
  • 大家会发现在使用函数指针来调用函数的时候,( *pf )中的星号有一种脱裤子放屁的感觉,在这里不论你加不加星号都是函数的地址,所以这里 的星号是可以省略的。
  • 如果大家有兴趣的话就可以试一下加多个星号,最后你会发现,结果都是一样的,这里的星号就像是摆设一样的。

4.3 两段有趣的代码

这两段代码都是出自《C陷阱和缺陷》这本书。

  • 代码1:
(*(void (*)())0)();//这是一次函数调用
  1. 先看 void (*)() 是什么???
    //这是一个函数指针类型
  2. 那么再来看 ( void (*)() )0 这个是什么???
    //这是一个强制类型转换,我们写成这样( 类型 )0,就比较好看一些。 这个的意思就是,0是一个整型常量,将 0 强制类型转换成一个地址( 函数指针 )
  3. ( * (void (*)())0)() 这个前面的星号就代表我们将0强制类型转换成一个地址之后,我们再将其解引用,后面的原括号就代表,我们解引用完了之后,我们再调用这个函数。
  • 代码2:
void (*signal(int , void(*)(int)) )(int);
  • 这是一个函数声明,声明的函数的名字叫:signal.signal函数有2个参数,第一个参数的类型是int,第二个参数的类型是 void()(int) 的函数指针类型,该指针可以指向一个函数,指向的参数类型是int,返回值是void。signal 函数的返回类型是void()(int) 的函数指针,该指针可以指向一个函数,指向的参数是int,返回值是void。

4.3.1 typedef 关键字

  • 有些时候,我们在创建一个变量时,这个变量的类型可能会比较复杂,这时候我们可以通过使用 typedef关键字来对其简单化。

typedef unsigned int uint;//使用typedf将( unsigned int )简化成了uint
int main()
{
	unsigned int  num1;//定义一个无符号整型的变量
	uint num2;
	//此时定义的变量num1和num2的类型是一样的
	return 0;
}
  • 那么,typedf能不能对指针类型重命名呢?
typedef int* pint;//简化整型指针变量

typedef int(*parr_t)[5];//简化数组指针变量

int main()
{
	int* p1 = NULL;
	pint p2 = NULL;
	
	int arr[5] = {0};
	parr_t p2 = &arr;
    return 0;
}
  • 重命名函数指针变量
void text(char* s)
{

}
typedef  void (*pf_t)(char*)//对函数指针类型重命名产生新的类型
int main()
{
    void (*pf1)(char*) = text;
	pf_t pf2 = text;
	return 0;
}
  • 我们学到这里,就可以使用typedef关键字对void (signal(int , void()(int)) )(int);简化一下。
typedef void(*pf_t)(int);

int main()
{
	void (*signal(int, void(*)(int)))(int);
	pf_t signal2(int, pf_t);
	//此时上面两行代码的意思是完全一样的 
	return 0;
}
  • 我们在前面也学过#define
typedef int* ptr_t;
#define PTR_T int*

int main()
{
	ptr_t p1;//p1是整型指针
	PTR_T p2;//p2是整型指针
	//此时二者是没什么差异的

	ptr_t p3, p4;//p3 p4是整型指针
	PTR_T p5, p6;//p5是整型指针,p6是整型
	return 0;
}
  • 我们来调试观察一下:
    在这里插入图片描述
  • typedef对类型的重命名会更加彻底一些,如果我们要对一个类型进行重命名时,我们尽量使用typedef.

5.函数指针数组

  • 接下来,我们还是通过类比来看看什么是函数指针数组。
  • 整型数组: 存放整型的数组
  • 字符数组: 存放字符的数组
  • 指针数组: 存放指针的数组
  • 字符指针数组: 存放字符指针的数组
  • 函数指针数组: 存放函数指针的数组
  • 我们先来实现一个” + - * / “的函数
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 (*pf1)(int, int) = add;
	int (*pf2)(int, int) = sub;
	int (*pf3)(int, int) = mul;
	int (*pf4)(int, int) = div;
  • 那我们大家可以看到这四个函数指针变量的类型都是int ( * )( int, int ),那么,如果要把多个相同类型的函数指针存放在一个数组中,这个数组就是函数指针数组。
int (*pfarr[4])(int, int) = { add, sub, mul, div };
//pfarr就是一个函数指针数组
  • 我们来画图理解一下:
    在这里插入图片描述
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 (*pf1)(int, int) = add;
	int (*pf2)(int, int) = sub;
	int (*pf3)(int, int) = mul;
	int (*pf4)(int, int) = div;

	int (*pfarr[4])(int, int) = { add, sub, mul, div };

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", pfarr[i](8, 4));
	}
	return 0;
}
  • 代码运行结果如下:

在这里插入图片描述

  • 看完之后大家是不是又觉得这个操作就是又脱裤子放屁了,直接调用不就行了吗?搞那么复杂,让我以为那么高级。
  • 别急别急,假设我们想要用代码实现一个计算器,我们来看一下这个代码该怎么实现呢?
#include<stio.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;
}

void menu()
{
	printf("**********************************\n");
	printf("*******    1.add    2.sub   ******\n");
	printf("*******    3. mul   4.div   ******\n");
	printf("*******    0.exit           ******\n");
	printf("**********************************\n");

}
int main()
{
	int input = 0;
	int x = 0;
	int y = 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");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}

	} while (input);
	return 0;
}

6.转移表

  • 虽然在上面我们已经完成了简易计算机的实现,但是我们发现这个代码里有两个主要的问题:

1.代码大量重复,显得不那么干脆利落。
2.如果我们想要实现更多的运算( >> << & | ^ && || 等 ),代码也会随之大量加长。

  • 接下来,我们就对其进行一个优化:
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 menu()
{
	printf("**********************************\n");
	printf("*******    1.add    2.sub   ******\n");
	printf("*******    3. mul   4.div   ******\n");
	printf("*******    0.exit           ******\n");
	printf("**********************************\n");

}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;//计算出的结果

	//创建一个函数指针数组
	//我们之所以让第一个元素为NULL,是为了确保后面的函数与其下标相对应。
	int (*pfarr[5])(int, int) = {NULL, add, sub, mul, div};

	do
	{
		menu();
		printf("请选择:》");
		scanf("%d", &input);
		if (input >=1 && input <= 4)
		{
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			ret = pfarr[input](x, y);
			printf("%d\n", ret);
		}
		else if(input==0)
		{
			printf("退出计算器!\n");
			break;
		}
		else
		{
			printf("选择错误,请重新输入!\n");
		}
	} while (input);
	return 0;
}
  • 其实这里的函数指针数组就是转移表,这个函数指针就像一个跳板一样,我们只要简单地对其进行使用,就可以跳转到我们想要的函数里,完成代码的运算。
  • 我们还有另一种优化方式(比较推荐这一种):
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 menu()
{
	printf("**********************************\n");
	printf("*******    1.add    2.sub   ******\n");
	printf("*******    3. mul   4.div   ******\n");
	printf("*******    0.exit           ******\n");
	printf("**********************************\n");

}

void calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;//计算出的结果
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}
int main()
{
	int input = 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");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}

	} while (input);
	return 0;
}

在这里插入图片描述

在这里插入图片描述


哈哈哈哈哈哈好开心啊,深入理解指针(3)终于结束了,家人们,期待深入理解指针(4)叭!!!

  • 16
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

论迹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值