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;
}
本期博客内容就分享到这里,希望对你的学习有所帮助ღ( ´・ᴗ・` )
有疑问欢迎在评论区与我交流哦~
完