字符指针变量
在指针的类型中我们知道有一种指针类型为字符指针
char*
;
一般使用:
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'b'; //修改指针指向的变量的值,屏幕打印出'b'
printf("%c\n", *pc); //屏幕打印出'b'
return 0 ;
}
还有一种使用方式如下:
#include<stdio.h>
int main()
{
const char* p = "abcdef"; //声明一个字符指针变量p指向字符串"abcdef"的首元素,是一个常量字符串,不能被修改
printf("%c\n", *p); //屏幕打印出'a'
printf("%s\n", p); //使用%s打印字符串的时候,只是提供首字符的地址就行,所以屏幕打印出'abcdef'
//char arr[] = "abcdef"; //进行初始化数组
//char* p = arr; //声明一个字符指针变量p指向数组的首元素
printf("%s\n", arr);
return 0 ;
}
画图理解:
代码 const char* p = "abcdef"; 特别容易让同学以为是把字符串
abcdef放到字符指针
p⾥了,但是本质是把字符串
abcdef⾸字符的地址放到了
p中。
《剑指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");//1
else
printf("str1 and str2 are not same\n");//2-打印
if (str3 == str4)
printf("str3 and str4 are same\n");//3-打印
else
printf("str3 and str4 are not same\n");//4
return 0;
}
输出结果:
画图理解:
这里str 3 和str 4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str 1 和str 2 不同,str 3 和str 4 相同。
数组指针变量
数组指针变量是什么?
指针数组是一种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量?还是数组?
答案是:指针变量
。
类比:
字符指针 -char* -指向字符的指针 - 字符指针变量中存放字符变量的地址。
char ch = 'w';
char* p = &ch;
整型指针 -int* -指向整型的指针 - 整型指针变量中存放整型变量的地址。
int a = 10;
int* p = &a;
数组指针 -char* -指向数组的指针 - 数组指针变量中存放
数组的地址`。
&数组名
// 包含标准输入输出库
#include<stdio.h>
// 主函数
int main()
{
// 定义并初始化大小为10的字符数组
char arr[10] = {1 , 2 , 3 , 4 , 5};
// 定义指向数组的指针p,p中存放的是数组arr的地址
char (*p)[10] = &arr; //p就是是数组指针,p中存放的是数组的地址
//int(*)[10] = int(*)[10]
//arr --int* arr+1跳过4个字节
//&arr[0] --int(*)[10] &arr[0]+1跳过4个字节
//&arr --int(*)[10] &arr+1跳过40个字节
// 返回0
return 0;
}
运行结果:
1 2 3 4 5
我们已经熟悉:
-
整形指针变量 :
int * pint
;存放整形变量的地址
,能够指向整形数据的指针
。 -
浮点型指针变量 :
float * pf
;存放浮点型变量的地址
,能够指向浮点型数据的指针
。 -
数组指针变量:
存放的应该是数组的地址,能够指向数组的指针变量
。
练习:
#include<stdio.h>
int main()
{
char* ch[5] = {0};
char* (*p)[5] = &ch;
return 0;
}
运行结果:
00000000
下面代码哪个是数组指针变量?
int *p1[10];
int (*p2)[10];
思考一下:p 1 ,p 2 分别是什么?
数组指针变量
int (*p)[10];
解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为 10 个整型的数组。所以p是一个指针,指向一个数组,叫 **数组指针**
。
注意:
[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
。
数组指针变量怎么初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的&数组名。
int arr[10] = { 0 };
&arr;//得到的就是数组的地址
如果要存放个数组的地址,就得存放在 数组指针变量 中,如下:
1 int(*p)[ 10 ] = &arr;
我们调试也能看到&arr和p的类型是完全一致的
。
数组指针类型解析:
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
二维数组传参的本质
有了数组指针的理解,我们就能够讲一下二维数组传参的本质了。
过去我们有一个二维数组的需要传参给一个函数的时候,我们是这样写的
:
// 定义一个名为Print的函数,接受一个二维整型数组a,以及两个整型参数r(行数)和c(列数)
void Pirnt(int a[ 3 ][ 5 ], int r, int c)
{
// 使用嵌套的for循环遍历二维数组a的每个元素
for(i= 0 ; i<r; i++) //i锁定行
{
for(j= 0 ; j<c; j++) //j锁定列
{
// 打印二维数组a中每个元素的值
printf("%d ", a[i][j]); //注意这里的i和j是循环变量,不是数组下标
}
// 打印换行符,换行显示二维数组
printf("\n");
}
}
// 主函数
int main()
{
// 声明并初始化一个3行5列的二维数组arr
int arr[ 3 ][ 5 ] = {{ 1 , 2 , 3 , 4 , 5 }, { 2 , 3 , 4 , 5 , 6 },{ 3 , 4 , 5 , 6 , 7 }};
// 调用Print函数,并传入数组arr以及其行列数作为参数
Pirnt(arr, 3 , 5 ); //将arr数组的内容打印出来
return 0 ;
}
运行结果:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
类比
:
一维数组传参
:
-
数组名是首元素地址
-
一维数组在传参的时候,传递的是数组的地址,也就是数组的首元素的地址。
-
函数的参数可以写成数组,也可以写成指针形式。
二维数组传参
:
- 数组名是首元素地址
-
二维数组可以理解为一维数组的数组,也就是每个元素是一个一维数组。
-
二维数组的每一行可以看做是一个一维数组,所以二维数组其实是一个一维数组,二维数组的首元素就是它的第一行。
-
所以二维数组的数组名表示的就是第一行的地址,是一维数组的地址。
如下图:
根据上面的例子,第一行的一维数组的类型就是int [5],所以第一行的地址的类型就是数组指针类型int(*)[5]。那就意味着
二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址 ,那么形参也是可以写成指针形式的
。如下:
void Print(int(*arr)[5], int r, int c)
{
// 使用嵌套的for循环遍历二维数组a的每个元素
for(i= 0 ; i<r; i++) //i锁定行
{
for(j= 0 ; j<c; j++) //j锁定列
{
// 打印二维数组a中每个元素的值
printf("%d ", *(*(arr+i)+j);//*(arr+i)获取第i行的首地址,然后再加上j,获取第i行第j个元素的地址,再用*解引用,获取该元素的值。
//*(arr+i) == arr[i]——>arr[i]是第i行的数组名,数组名又表示数组首元素的地址,arr[i]表示是&arr[i][0]
}
// 打印换行符,换行显示二维数组
printf("\n");
}
}
// 主函数
int main()
{
// 声明并初始化一个3行5列的二维数组arr
int arr[ 3 ][ 5 ] = {{ 1 , 2 , 3 , 4 , 5 }, { 2 , 3 , 4 , 5 , 6 },{ 3 , 4 , 5 , 6 , 7 }};
// 调用Print函数,并传入数组arr以及其行列数作为参数
Print(arr, 3 , 5 ); //将arr数组的内容打印出来
return 0 ;
}
运行结果
:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
总结: 二维数组传参,形参的部分可以写成数组,也可以写成指针形式
。
补充解释
:
第9行代码:*(*(arr+i)+j)
函数指针变量
函数指针变量的创建
什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们的类比关系,我们不难得出结论
:
函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。
那么函数是否有地址呢?
做个测试
:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//int a = 10;
//int* pa = &a;//整型指针变量
//int arr[5] = {0};
//int (*parr)[5] = &arr;//parr 是数组指针变量
//arr &arr
//&函数名和函数名都是函数的地址,没有区别,打印两个函数的地址是一样的。
//printf("%p\n", &Add);//打印函数的地址
//printf("%p\n", Add);//打印函数的地址
//int (*pf)(int,int) = &Add;//pf 函数指针变量
int (* pf)(int, int) = Add;//pf 函数指针变量
int ret2 = (*pf)(4, 5);//也可以写成 int ret2 = pf(4, 5);//调用函数指针变量
printf("%d\n", ret2);//ret2 = 9
int ret = Add(4, 5);//常规调用函数
printf("%d\n", ret);
//int (*pf)(int x, int y) = &Add;//pf 函数指针变量
//int (*)(int,int) 函数指针类型
return 0;
}
#include <stdio.h>
// 定义一个不返回数值的函数test
void test()
{
printf("hehe\n"); // 在控制台输出"hehe"
}
// 主函数
int main()
{
printf("test: %p\n", test); // 打印函数test的地址
printf("&test: %p\n", &test); // 打印函数test的地址
return 0; // 返回0
}
输出结果如下
:
test: 005913 CA
&test: 005913 CA
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方
式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针 非常类似
。如下:
// 定义一个无返回值、无参数的函数test
void test()
{
printf("hehe\n"); // 打印出"hehe"
}
// 声明一个指向test函数的指针pf1
void (*pf1)() = &test;
// 声明一个指向test函数的指针pf2,可以省略取地址运算符&
void (*pf2)()= test;
// 定义一个有两个参数、返回整型值的函数Add
int Add(int x, int y)
{
return x+y; // 返回x和y的和
}
// 声明一个指向Add函数的指针pf3
int (*pf3)(int, int) = Add;
// 声明一个指向Add函数的指针pf3,参数可以写上类型名或者省略
int (*pf3)(int x, int y) = &Add;
函数指针类型解析
:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
函数指针变量的使用
通过函数指针调用指针指向的函数。
#include <stdio.h>
// 定义一个函数,名称为Add,接受两个int类型的参数x和y,返回它们的和
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 声明一个指向函数的指针pf3,该函数接受两个int类型参数并返回int类型的结果
int (*pf3)(int, int) = Add;
// 通过指针pf3调用Add函数并输出结果
printf("%d\n", (*pf3)(2, 3));
// 通过指针pf3直接调用Add函数并输出结果
printf("%d\n", pf3(3, 5));
return 0;
}
输出结果
:
5
8
两段有趣的代码(了解一下
)
代码 1
(*(void (*)()) 0 )();
代码 2
void (*signal(int , void(*)(int)))(int);
两段代码均出自:《C陷阱和缺陷》这本书
typedef关键字(了解一下
)
typedef是用来类型重命名的,可以将复杂的类型,简单化。
比如,你觉得unsigned int
写起来不方便,如果能写成uint
就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将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
,就可以这样写:
1 typedef void(*pfun_t)(int);//新的类型名必须在*的右边
那么要简化代码 2 ,可以这样写:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
函数指针数组
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:
int * arr[ 10 ];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组 ,那函数指针的数组如何定义呢?
int (*parr1[ 3 ])();
int *parr2[ 3 ]();
int (*)() parr3[ 3 ];
答案是:parr 1
parr1
先和[]
结合,说明parr 1 是数组,数组的内容是什么呢?
是int (*)()
类型的函数指针。
举例
:
//指针数组
//加减乘除四个函数的地址存到数组中
//定义四个函数
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* arr1[6];//整型指针数组
//char* arr2[5];//字符指针数组
//arr3[5];//每个元素是函数的地址 - 函数指针数组
//int (*pf1)(int, int) = Add;
//int (*pf2)(int, int) = Sub;
//int (*pf3)(int, int) = Mul;
//int (*pf4)(int, int) = Div;
//函数指针数组,来存放这些函数的地址呢?
int (* pf[4])(int, int) = {Add, Sub, Mul, Div};
// 0 1 2 3
//通过数组下标来存放函数的地址
int i = 0;
for (i = 0; i < 4; i++)//遍历数组,调用函数
{
int ret = pf[i](6, 2);//通过数组下标调用函数
printf("%d\n", ret);
}
return 0;
}
运行结果
:
6
4
12
3
转移表
函数指针数组
的用途: 转移表
举例:计算器的一般实现
:
(1)
#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;
}
(2)
#include<stdio.h>
// 实现一个计算器
// 菜单函数
void menu()
{
// 打印菜单选项
printf("********************************\n");
printf("****** 1. add 2. sub *****\n");
printf("****** 3. mul 4. div *****\n");
printf("****** 0. exit *****\n");
printf("********************************\n");
}
// 加法函数
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 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;
}
运行结果
:
********************************
****** 1. add 2. sub *****
****** 3. mul 4. div *****
****** 0. exit *****
********************************
请选择:1
请输入两个操作数:10 20
30
请选择:2
请输入两个操作数:30 10
20
请选择:3
请输入两个操作数:2 3
6
请选择:4
请输入两个操作数:10 2
5
请选择:0
退出计算器
特殊情况
:
(1)(2)缺点
:
- 代码冗余:代码中有很多重复的函数定义,增加了代码的复杂度。
- 效率低:每一次用户输入都需要遍历数组,效率低。
(3)
在(2)的基础上,使用函数指针数组
来实现转移表。
#include<stdio.h>
// 菜单函数
void menu()
{
// 打印菜单选项
printf("********************************\n");
printf("****** 1. add 2. sub *****\n");
printf("****** 3. mul 4. div *****\n");
printf("****** 0. exit *****\n");
printf("********************************\n");
}
// 加法函数
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 input = 0;
int x = 0;
int y = 0;
int ret = 0;
// 函数指针数组 - 转移表
int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
// 0 1 2 3 4
do
{
menu(); // 调用菜单函数
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4) // 判断用户输入是否在合法范围内
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y); // 根据用户输入调用相应的函数,并计算结果
printf("%d\n", ret); // 输出计算结果
}
else if (input == 0)
{
printf("退出计算器\n"); // 用户选择退出
}
else
{
printf("选择错误,重新选择\n"); // 用户输入不合法
}
} while (input); // 循环直到用户选择退出
return 0;
}
运行结果
:
********************************
****** 1. add 2. sub *****
****** 3. mul 4. div *****
****** 0. exit *****
********************************
请选择:1
请输入两个操作数:10 20
30
请选择:2
请输入两个操作数:30 10
20
请选择:3
请输入两个操作数:2 3
6
请选择:4
请输入两个操作数:10 2
5
请选择:0
退出计算器
(4)
再次优化,Calc()
函数可以接收一个函数指针作为参数,并调用该函数。
#include<stdio.h>
// 输出菜单
void menu()
{
// 显示计算器菜单
printf("********************************\n");
printf("****** 1. add 2. sub *****\n");
printf("****** 3. mul 4. div *****\n");
printf("****** 0. exit *****\n");
printf("********************************\n");
}
// 加法函数
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 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;
}
运行结果
:
********************************
****** 1. add 2. sub *****
****** 3. mul 4. div *****
****** 0. exit *****
********************************
请选择:1
请输入两个操作数:10 20
30
请选择:2
请输入两个操作数:30 10
20
请选择:3
请输入两个操作数:2 3
6
请选择:4
请输入两个操作数:10 2
5
请选择:0
退出计算器
总结
- 指针变量:指向内存中其他变量的变量,可以用来存放地址、函数指针、数组指针等。
- 指针数组:存放指针变量的数组,可以用来存放不同类型的指针变量。
- 函数指针数组:存放函数指针的数组,可以用来实现转移表。
- 转移表:根据用户输入的选项,调用相应的函数。