深入理解指针
在初步了解了指针的用法之后,我们可以想一想,既然一个变量有地址,而且在上一篇文章中我们知道了一个数组也有地址,那么函数、字符串这些东西有没有地址呢?如果有,那这些地址有什么用?我们又要怎样来使用这些地址?看了这篇文章,相信你对指针会有更深入的理解!
目录
字符指针变量
数组指针变量
二维数组传参的本质
函数指针变量
typedef关键字
函数指针数组
转移表
字符指针变量
在对指针有了一个初步的了解之后,我们知道了存放一个char类型的数据的地址要用char*类型的指针变量,所以字符指针便是指向字符型数据的指针变量。
其实字符指针还有其他的用法,这里先补充一点字符串的知识:每一个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。因此,只要将字符串的首地址赋值给字符指针,即可让字符指针指向一个字符串。对于常量字符串而言,常量字符串本身代表的就是存放它的常量存储区的首地址,是一个地址常量。
来看下面这段代码
#include<stdio.h>
int main()
{
char* pc = "hello";
printf("%c\n", *pc);
printf("%s\n", pc);
return 0;
}
运行结果
像这段代码中的定义的字符串"hello"是常量字符串(也叫字符串字面量),而常量字符串的一个特性是不能被修改。在这段代码中,其实是将字符串"hello"的首地址(也是首字符'h'的地址)赋值给了字符指针pc。所以对pc变量解引用之后打印出来就是h,而在使用%s打印字符串的时候,我们只需要提供首字符的地址就可以了,在打印的时候通过字符串的首地址就可以找到该地址所指向的空间,然后像后打印,直到遇到'\0'为止。
注意:因为数组名是一个地址常量,其值是不能被修改的,所以这里不能使用++操作使其指向字符串中的某个字符。
那为什么常量字符串不能被修改呢?来看下面这段代码
#include<stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
char* str3 = "hello world";
char* str4 = "hello world";
printf("str1 = %p\n", str1);
printf("str2 = %p\n", str2);
printf("str3 = %p\n", str3);
printf("str4 = %p\n", str4);
return 0;
}
运行结果
在这段代码中,str1和str2都是字符数组的数组名,而数组名又是数组首元素的地址,str3和str4都是字符指针变量,它们都指向的是一个常量字符串。所以str1,str2,str3,str4这4个变量都是char*类型的指针变量,而这4个变量所指向的字符串的内容都是一样的,然而我们通过打印地址的方式来打印这4个变量时发现,str1和str2是不同的两个指针变量,说明str1和str2指向的是两块不同的空间,而str3和str4存储的地址是一样的,这意味着str3和str4指向的是同一块空间。
其实在c/c++中,会把常量字符串存储到单独的一块内存区域,这个区域叫只读数据区,而只读数据区中的数据是不能被修改的。这里str3和str4存储的地址之所以一样,是因为这两个常量字符串的内容是一样的,而且常量字符串中的内容是不会被修改的,所以内容相同的常量字符串,只需要保存一份就够了!
数组指针变量
前面我们说过的指针数组和现在要说的数组指针听起来很类似,但他们是完全不同的两个概念,下面做一个简单的辨析
指针数组:是数组,是存放指针变量的数组
数组指针:是指针,是存放数组地址的指针
下面举个例子
int main()
{
int arr[3] = { 1,2,3 };
int(*parr1)[3] = &arr;//parr1是数组指针变量
int arr1[3] = {1,2,3};
int arr2[3] = { 4,5,6 };
int arr3[3] = { 7,8,9 };
int* parr2[3] = { arr1,arr2,arr3 };//parr2是指针数组
return 0;
}
在这段代码中,parr1是数组指针,存放的是数组arr的地址,在int(*parr1)[3] = &arr;这句代码中,*先和parr1结合,表示parr1是指针变量,要注意的是,*parr1两边的圆括号()是必不可少的(圆括号的优先级最高),int表示parr1指向的数组元素类型,parr1是数组指针变量名,[10]中的10表示parr1指向的数组的元素个数,而parr1的类型是int(*)[3],即去掉变量名之后剩下的东西
parr2是指针数组,是存放int*类型元素的数组,在int* parr2[3] = { arr1,arr2,arr3 };这句代码中,*先和int结合,表示parr2存放的元素类型是int*,因为parr2中存放的是整型数组的数组名,而数组名表示的是数组首元素的地址,即地址的类型就是int*
二维数组传参的本质
说完了数组指针之后,我们可以会想数组指针有什么用呢?下面我们来看看数组指针在二维数组传参时的使用场景
在刚学完数组时,我们是用下标来访问数组元素的,例如
#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[i][j]来访问二维数组的第i行j列的元素的,运行结果:
接下来我们看看二维数组传参的时候用指针来接收,并用数组指针访问二维数组的场景
#include <stdio.h>
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;
}
这里首先要说明几点:
1:二维数组的数组名也是数组首元素的地址
2:二维数组可以理解为一维数组的数组,所以二维数组的首元素就是第一行(是一个一维数组)
3:二维数组的数组名就是二维数组第一行的地址
这里二维数组arr的每一行(一维数组)的类型是int [5],所以每一行的地址类型就是数组指针类型int(*)[5]
总结:二维数组传参的本质是传地址,传递的是二维数组第一行的一维数组的地址
所以在打印的时候, *(*(p + i) + j)这句代码相当于上面的代码中用数组下标a[i][j]来访问数组元素,*(p + i)相当于找到了二维数组的第i行,就相当于是二维数组第i行的数组名,因为数组名是数组首元素的地址,所以*(p + i)等价于a[i][0],*(*(p + i) + j)则相当于找到了二维数组第i行的第j个元素,等价于 *(*(p + i) + j)
运行结果:
函数指针变量
函数指针变量顾名思义,应该就是用来存放函数地址的变量。
通过前面的学习,我们了解到数组名就是数组首元素的地址,同理可知,一个函数名就是这个函数(的源代码)在内存中的起始地址,编译器会将不带()的函数名解释为该函数的入口地址。
要得到函数的地址,可以通过&函数名的方式,也可以直接用函数名,因为函数名就是该函数的地址。
说了这么多,那函数指针到底应该如何定义呢?来看下面这段代码
#include<stdio.h>
void test()
{
printf("hello\n");
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
//定义存放test函数地址的指针变量
void (*pf1)() = &test;
void (*pf2)() = test;
//定义存放Add函数地址的指针变量
int(*pf3)(int, int) = Add;
int(*pf4)(int, int) = &Add;
int(*pf5)(int x, int y) = Add;
int(*pf6)(int x, int y) = &Add;
printf("%p\n", pf1);
printf("%p\n", pf2);
printf("%p\n", pf3);
printf("%p\n", pf4);
printf("%p\n", pf5);
printf("%p\n", pf6);
return 0;
}
这段代码中,对函数指针变量的定义五花八门,但其实本质上都是一样的。这儿以int(*pf5)(int x, int y) = Add;这句代码为例,首先int表示变量pf5指向的函数的返回类型是int类型,pf5是函数指针的变量名,它旁边的*表示pf5是一个指针变量,其次(int x, int y)表示的是pf5指向函数的参数类型和个数,这里的x和y也可以不写,只要说明清楚这个函数的参数类型和个数就可以了,而在定义test函数的指针变量时,因为该函数没有参数,所以在参数部分就只有一个(),最后要说的一点就是函数名和&函数名没有区别,用哪个都可以
既然函数指针已经定义好了,那么函数指针的参数又是什么呢?还是以int(*pf5)(int x, int y) = Add;这句代码为例,去掉变量名之后,剩下的int(*)(int x, int y)就是它的类型。
运行结果:
可以看出,pf1、pf2是同一种函数指针类型,pf3、pf4、pf5、pf6是同一种函数指针类型。
函数指针变量定义好了之后,接下来就是如何使用了。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = Add;
printf("%d\n", (*pf)(2, 1));
printf("%d\n", pf(0, 5));
return 0;
}
在这段代码中,定义了一个加法函数Add,又定义了一个指向Add函数的函数指针pf,然后就是通过pf指针来调用Add函数了
在调用的过程中,我们发现不管是对pf变量解引用还是直接用pf指针来调用Add函数,都是可以的。实际上这里的*就相当于是一个摆设,甚至在调用的时候可以写成(****pf),只不过是加一颗*可以明显的看出pf是一个指针变量,而对函数的调用是通过对函数指针的解引用来实现的
运行结果:
typedef关键字
typedef关键字是用来对类型进行重命名的
例如,定义一个无符号的整型变量本来是
unsigned int n = 0;
但是每次定义都用unsigned int比较麻烦,就可以用typedef对unsigned int类型重命名一下,就比如叫uint
typedef unsigned int uint;
这样一命名之后,用uint定义一个无符号的整型变量和用unsigned int定义一个无符号的整型变量是一样的
unsigned int n1;
uint n2;
n1和n2都是无符号的整型变量
typedef关键字同样也可以对指针类型重命名,这样做除了是代码写起来更简单外,还可以将一些复杂的代码给简单化,就比如下面这句代码
void (*signal(int , void(*)(int)))(int);
我想,每一个看到这句代码的人,尤其是对于初学者来说都会忍不住头大,那这句代码到底是什么意思呢?
其实这句代码是一次函数调用,函数名就是signal,而这个函数有两个参数,第一个参数的类型是int,第二个参数的类型是一种函数指针void (*)(int),而void (*)(int)这个函数指针指向的函数的参数是int类型,返回类型是void,那signal这个函数的返回类型是什么呢?其实去掉函数名和函数参数之后,剩下的就是这个函数的返回类型,即void (*)(int)。要注意的是,这里的函数名和函数的参数必须要写在这个函数的返回类型void (*)(int)的*旁边的,而不是直接写在返回类型void (*)(int)的后面,这是语法规定的
那这句代码可不可以写的更简单一点呢?当然是可以的
这里我们可以用typedef关键字对函数指针类型void (*)(int)重命名,例如叫pfun_t。那上面那句看着很复杂的代码就可以简化成下面的样子
pfun_t signal(int, pfun_t);
相比于上面的代码而言,重命名之后写的代码明显要简单很多
函数指针数组
函数指针数组,顾名思义就是存放函数指针的数组。
那函数指针数组应该如何定义呢?来看下面的代码
int (*parr[2])(int, int);
在这句代码中,parr先和[ ]结合,说明parr是数组,而数组中存放的是int (*)(int, int)类型的函数指针,该函数的两个参数都是int类型,返回值也是int类型
转移表
说完了函数指针数组,接下来我们来看看这个东西到底要如何使用,即用函数指针数组来实现这里的转移表
首先我们来实现一个可以计算两个数的加、减、乘、除的计算器,代码如下
#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()
{
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;
}
这个代码,主要是由打印的菜单,先告诉使用者每个数字代表的功能,然后由使用者选择的不同数字,来调用不同的函数,以此来模拟实现这个简单的计算器,可以供道友们参考,具体的细节这里不再过多赘述
但是我们在这里也可以看到,在这段代码中,每次选择了一个数字之后,所执行的代码是类似的,所以这里有代码冗余的情况
下面我们通过函数指针数组来实现一下
#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()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
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 <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);
return 0;
}
这段代码中,先是定义好了计算两个数的加、减、乘、除的函数,由于这四个函数的地址的类型都是int (*)(int, int),所以可以用函数指针数组来保存这四个函数的地址,通过访问数组元素的方式来调用这四个函数,这样做的一个好处是避免了上面那个代码中由于所执行的操作类似而出现代码冗余的情况,代码相对更加灵活