【C语言】深入理解指针

发布于:2024-08-02 ⋅ 阅读:(82) ⋅ 点赞:(0)

1. 字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;

⼀般使⽤:

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';

    return 0;
}

在这段代码中:

我们定义一个字符类型(char)的变量 ch,存放一个字符 'w'
如果想取出字符ch的地址,我们使用取地址操作符(&)取出字符变量ch的地址,
放到一个指针变量pc中,pc的类型是char*
未来想使用的话,对pc进行解引用操作,使用解引用操作符(*)通过pc中存放的地址,
找到指向的空间,*pc其实就是ch变量了。

这就是我们对指针基本的操作。取地址以及解引用操作。

还有⼀种使用方式如下:

int main()
{
    const char* p = "hello bit.";
    printf("%s\n", pstr);
    
    return 0;
}

同学不禁会有疑问:这里是把字符串 hello bit 放到字符指针 p ⾥吗?

我们仔细想想,很显然是放不下的

因为“hello bit.” 这个字符串一共有 11 个字符。分别是:h、e、l、l、o、空格 、b、i、t、. 还有一个字符串结束标志\0,总共占11个字节。而一个指针变量32位4个字节,64位8个字节。

很显然是放不下的。

 其实本质是把字符串 hello bit. 首字符的地址放到了p中

上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 p 中。

另外,同学们发现在代码中,指针p前面加上了const修饰,这里我们之前讲过

const放在*的左边,修饰的是指针指向的内容

其实“hello bit.” 这是一个常量字符串,我们其实也不需要修改该字符串, 所以加上const修饰,表示该指针指向的内容,也就是指向的这个常量字符串不应被修改。这是一个很好的编程习惯!

如果没有const ,并且后续代码中误写为*p = 'w'; 

试图修改字符串的第一个字符,这可能会导致未定义的行为甚至程序崩溃。

这里我们就知道了我们可以取一个字符的地址,以及取常量字符串的地址了!

下面是一道在《剑指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");
	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 是字符数组,它们在内存中分别有独立的存储空间来存储字符串的内容。当使用 == 比较时,比较的是数组的地址,而不是数组中的内容。由于它们是两个独立分配的数组,所以地址不同,会输出 str1 and str2 are not same 。 

str3 和 str4 是指向常量字符串的指针。对于字符串常量,在内存中相同的字符串常量通常只有一份存储。所以当它们指向相同的字符串常量时,指针的值是相同的,会输出 str3 and str4 are same 。 

 2. 数组指针变量

 2.1 数组指针变量是什么?

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

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

答案是:指针!

而我们也已经熟悉:

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

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

由此得出,   

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

那么,下⾯代码中,p1, p2 分别是什么?哪个又是数组指针变量呢?

int *p1[10];
int (*p2)[10];

分析:

在前面操作符,指针章节我们学习过,

[ ] 是数组下标访问操作符,* 可以表示指针,也可以表示解引用操作符,

这里[ ]的优先级是⾼于*号

数组指针是一个指针,p要想和*结合,必须加上()来保证p先和*结合。

这里我们发现,p2先和*结合,括号括起来,说明p2是⼀个指针变量,

然后指向的数组是10个元素,每个元素是整型,所以p2是一个数组指针

那p1是什么呢?我们先留个悬念。

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

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?

就是我们之前学习的 &数组名

int arr[10] = {0};

&arr;//得到的就是数组的地址

如果要存放个数组的地址,就得存放在数组指针变量中,如下:

int(*p)[10] = &arr;



 

我们调试看到 &arr 和 p 的类型是完全⼀致的。

数组指针类型可以拆开来理解:

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

3. ⼆维数组传参的本质

有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。

过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:

#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);
	return 0;
}

这⾥实参是⼆维数组数组名arr,形参也写成⼆维数组的形式arr[],那有没有其他的写法吗?

首先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,

也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

 所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表示的就是第一行的地址,是⼀维数组的地址。根据上⾯的例⼦,第一行的⼀维数组的类型就是 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");
	}
}

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

4. 函数指针变量

4.1 函数指针变量的创建

什么是函数指针变量呢?

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

整型指针变量应该是⽤来存放整型地址

数组指针变量应该是⽤来存放数组地址

我们不难得出结论:

函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。

那么函数真的有地址吗?

我们做个测试:

#include <stdio.h>

void test()
{
	printf("hehe\n");
}

int main()
{
	printf("test: %p\n", test);
	printf("&test: %p\n", &test);

	return 0;
}

输出结果如下: 

我们发现,确实打印出来了地址,所以函数是有地址的,另外,函数名也是函数的地址

当然也可以通过 &函数名 的⽅式获得函数的地址。

如果我们要将函数的地址存放起来,就得创建函数指针变量咯,

其实函数指针变量的写法其实和数组指针⾮常类似。如下:

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

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

函数指针类型可以拆开来理解:


 

4.2 函数指针变量的使⽤

我们可以通过函数指针调⽤指针指向的函数。
 

#include <stdio.h>

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

int main()
{
    int(*pf3)(int, int) = Add;
    printf("%d\n", (*pf3)(2, 3));
    printf("%d\n", pf3(3, 5));
    return 0;
}

 输出结果:

这里声明了一个函数指针 pf3 ,并将其指向了 Add 函数。然后通过两种方式来调用:

 
printf("%d\n", (*pf3)(2, 3));

一种先对函数指针进行解引用 (*pf3) ,然后传入参数 2 和 3 进行调用。

printf("%d\n", pf3(3, 5));

另一种方式直接使用函数指针 pf3 ,并传入参数 3 和 5 进行调用,

效果与前面解引用后调用是一样的。

其实,这个*号是个摆设。我们可写可不写。

接下来带领大家看看出自于《C陷阱和缺陷》这本书 两段有趣的代码,你能理解吗?

代码1
(*(void (*)())0)();

如何下手呢?

分析:

这里有很多括号,为了防止分析混乱,我们可以将括号一个个拆开:

我们可以从这个0下手,

0的前面有个括号,括号里放的是的void(*)() ,这种类型可以明白这是一个函数指针类型

指向的函数没有参数,返回类型是void,没有返回值。

第二步:(void(*)())0,这里是将0强制转换为函数指针类型,这样的话0就被当作一个地址。

第三步:(* ( void (*) () ) 0 ),对该函数指针进行解引用,即获取指针所指向的函数。

第四步:(*(void (*)())0)() 找到函数后,完成函数调用,不传参数。

 代码2
void (*signal(int , void(*)(int)))(int);

分析:

同样的思路,我们可以将括号一个个拆开:

于是我们会有疑问,signal他是一个函数,还是指针啊?

signal 左边有*,右边有(),先和谁结合呢?

这里函数调用 () 的优先级高于指针解引用 * 。

所以signal会先和函数调用()结合,说明signal是一个函数

那signal这个函数参数和返回类型又是什么呢?

我们发现signal函数的第一个参数的类型是int(整形),第二个参数的类型是函数指针类型,由此该函数指向的第一个参数是int,返回类型是void的函数;

而signal函数的返回类型也是一个函数指针,这个函数指针指向的是一个参数为int,返回类型是void的函数;

所以这是一个函数的声明。

所以,这个代码其实分析上还是有一定难度的

那有什么办法可以增加这个代码的可读性呢?

4.3.1 typedef关键字

typedef 是C语言的一个关键字,它是⽤来对类型重命名的,可以将复杂的类型简单化。

⽐如说,我们想定义一个无符号整型的变量,无符号整型叫做unsigned int

我们可以这样定义:

unsigned int num = 200;//定义了一个无符号整数类型的变量 num ,并将其初始化为 200 

这样写完全可以。

但是有的同学觉得每次写unsigned int,其实可能变得比较麻烦 

这时我们就可以使用typedef对这个无符号整型类型unsigned int重新起名叫uint,

这样用uint定义变量时,和用unsigned int定义变量是一样的

typedef unsigned int uint;
unsigned int num = 200;
uint num = 100;//用新创建的类型别名 uint 来定义并初始化变量 num ,赋值为100

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

typedef int* ptr_t;

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

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

typedef int(*parr_t)[5]; //新的类型名是必须写在*的右边

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

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

写成typedef void(*)(int) pfun_t 语法是不对的,但是我们可以这样思考。

他也是对类型重新起名的。

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

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

在这段代码中:

我们认为 void(*)(int) 这个函数指针类型比较复杂,因此对它重新取名为 pfun_t ,表示一个指向函数的指针类型,该函数接受一个 int 类型的参数且无返回值。

所以signal函数很清晰的就能看出,它接受两个参数,第一个是 int 类型,第二个是 pfun_t 类型(即前面定义的指向特定函数的指针类型),并且函数的返回值类型也是 pfun_t (即前面定义的指向特定函数的指针类型)。

5. 函数指针数组

数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,⽐如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,

那函数指针的数组如何定义呢?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

答案是:parr1

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

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

那函数指针数组有什么应用呢?

它可以实现转移表的效果,转移表是什么呢?下面我们来看一个实例

6. 转移表

函数指针数组的⽤途:转移表 。

在项目中函数指针数组像一个跳板指向各种函数,所以又被称为转移表。 

转移表最直观的应用在计算器的实现上

我们可以打印一个计算器简易的菜单:实现加减乘除四种运算 

void menu()
{
	printf("*************************\n");
    printf(" 1:add             2:sub \n");
    printf(" 3:mul             4:div \n");
    printf(" 0:exit                  \n");
    printf("*************************\n");
	printf("请选择你的操作:");
}

   接下来,实现基本计算功能的四种函数

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

6.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;
}
 
void menu()
{
	printf("*************************\n");
    printf(" 1:add             2:sub \n");
    printf(" 3:mul             4:div \n");
    printf(" 0:exit                  \n");
    printf("*************************\n");
	printf("请选择你的操作:");
}
int main()
{
	int input; //控制选择操作的参数
	do
	{
		int x = 0;
		int y = 0;
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", Add(x, y));
			break;
		case 2:
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", Sub(x, y));
			break;
		case 3:
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", Mul(x, y));
			break;
		case 4:
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", Div(x, y));
			break;
		case 5:
			printf("退出计算器\n");
			break;
		default:
			printf("输入错误请重新输入\n");
		}
	} while (input);
	return 0;
}

6.2 函数指针实现

    一般实现方法发现代码过于冗余,考虑用函数指针简化代码

//定义函数calc并定义函数指针变量ptr作为参数
void calc(int (*ptr)(int, int))
{
	int x = 0;
	int y = 0;
	int z = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	z = ptr(x, y);
	printf("%d\n", z);
}
int main()
{
	int input;
	do
	{
		menu();
		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");
		}
	} while (input);
	return 0;
}

6.3函数指针数组实现

int main()
{
	int input;
	int x = 0;
	int y = 0;
	int(*ptr[5])(int, int) = { 0,Add,Sub,Mul,Div };//转移表
                        //函数的调用应从下标为1的成员开始
	do
	{
		menu();
		scanf("%d", &input);
		if (input <= 4 && input >= 1)
		{
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = (*ptr[input])(x, y);//调用转移表
			printf("%d+ %d = %d\n", x, y, ret);
		}
		else if (input == 0)
		{
			printf("退出计算器");
		}
		else
		{
			printf("输入有误,重新输入");
		}
	} while (input);
	return 0;
}

本期博客内容就分享到这里,希望对你的学习有所帮助ღ( ´・ᴗ・` )

有疑问欢迎在评论区与我交流哦~