深入了解指针(3)
1.字符指针变量
我们先来看一种之前我们用的方式去使用字符指针变量
char str= 'a';
char* p = &str;
*p = 's';
现在我们再来学习一个新的使用方法
int main()
{
const char* p = "hello world"; // 我们把这个字符串理解成一个字符数组 也就是把首元素地址传给了p指针
// 这个字符串叫做常量字符串 是不能被修改的 即使不加这个const 也是不能修改的
//*p = 'a'; // error
// 我们无法通过解引用操作去修改常量字符串
// 我们来验证一下
printf("%c\n", *p);
// 输出 三种方法的结果是一样的
printf("%s\n", p); // 打印的时候只需要一个起始地址 后面的内容只要是连续的地址 就可以一次性打印出来
printf("%s\n", "hello world");
int len = strlen(p);
for (int i = 0; i < len; i++)
{
printf("%c", *(p + i));
}
return 0;
}
再来看一道题
// 我们再来看一种情况
#include <stdio.h>
int main9()
{
char strl[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (strl == str2)
printf("strl and str2 are same \n"); // 1
else
printf("strl 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
// 最终结果是2和3
// 因为str1 和 str2 是两个字符数组 在内存中保存了两份 他们两个根本就没有关联
// 但是 str3 和 str4 指向的都是一个常量字符串 常量字符串 在内存中不会创建两个内存空间去储存
// 因此实际上str3 和 str4 这两个指针指向的都是都一个地址
return 0;
}
2.数组指针变量
2.1数组指针变量是什么
数组指针式一种指针变量,是存放数组地址的指针变量
存放的是这种地址 &arr 代表整个数组的地址
我们来分析一下:
int arr[10] = {0};
int (*p)[10] = &arr; // 我们需要一个*号去和p结合让他变成指针变量
// 这个的意思就是有个指针变量是p指向了一个10个元素的数组的首元素地址 这个数组每个元素都是int形
// 这个p现在就是数组指针
// 如果没有这个括号 int* p[10] 这个时候p先和[]结合 就是指针数组 放着指针的数组
再来一个例子:
char* arr[5];
char* (*p)[5] = &arr;
总结 (重要!!!!!!!)
- 数组指针变量的类型 是根据指向的数组的类型相关的
- 数组指针类型是【 类型 ( * )[数组元素数量] = 整个数组地址】(比如 (char* ( * )[5])就是一个数组指针类型)
- 需要一个括号来使*和p结合 这样才是指针变量
这里我们可以联系一下之前的 &arr + 1 为什么是跳过一个数组的地址大小呢 也就是+40
因为数组指针类型是 是int ( * )[10] 也就是10个int类型元素的数组 所以+ 1就是跳过一个数组 也就是40个字节
2.2数组指针变量怎么初始化
数组指针变量是用来存放数组地址的 那么怎么初始化呢
就是将数组的地址传给数组指针变量
我们之前学过& 通过这个就可以获得数组的地址
int (*p)[10] = &arr; // 这个就是初始化
3.二维数组传参的本质
前面我们学习了数组指针变量
我们先来看一下下面这个例子:
// 数组指针变量的运用
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int sz = sizeof(arr) / sizeof(&arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", (*p)[i]);// 这个写法并不好
// 这里这个(*p)[i] 可以这样理解(*p)== (*&arr)
// (*&arr) == arr 所以这里其实就是arr[i]
// 因为p里面放的是整个数组的地址
}
return 0;
}
我们发现上面是在一维数组上运用的数组指针变量 非常的别扭 ,并不是一种好的写法
实际上,数组指针变量 会用在二维数组
在看实例之前我们先要知道一个知识 那就是二维数组传参的实质是什么
首先 我们知道数组名 是数组的首元素地址
那么二维数组的数字名字自然也不例外 也是二维数组的首元素地址
那么二维数组的首元素是什么呢
实际上二维数组的首元素 是一维数组 以一维数组为元素的数组是二维数组
以二维数组为元素的是三维数组 以此类推
因此 二维数组的首元素 实际上就是 第一个一维数组的地址 是一个数组指针
我们来看一个实例
// 数组指针变量, 会在二维数组使用
// 我们知道二维数组传过来的是一个一维数组的数组地址
// 我们就要用 数组指针变量来存放这个一维数组的地址
void test(int (*p)[5], int x, int y)
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ",(*(p + i))[j]);// *(*(p + i) + j) +j 表示跳过j个整形元素
// *(p + i) 代表解引用出一维数组的首元素地址 +i 可以跳过i个数组
}
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;
}
4.函数指针变量
其实和数组指针变量类似
数组指针 指向数组–存放数组的地址
函数指针-指向函数-存放函数的地址
所以函数指针变量 就是存放函数地址的变量
来看一个实例
// 函数指针变量
//
// 函数指针变量的写法和数组指针变量的写法很类似
int Add(int x,int y)
{
return x + y;
}
int main2()
{
int arr[8] = { 0 };
int(*pa)[8] = &arr; // 这个是数组指针变量
int (*pf)(int, int) = &Add; //这个是函数指针变量
// (*pf)代表这是个指针变量 (int, int)代表指向的函数接受两个int 型的参数 int代表指向的函数返回int类型的数据
int a = 10;
int b = 20;
int z = Add(a, b);
printf("%p\n", &Add);
printf("%p\n", Add);
// 函数名和&函数名都代表的函数的地址
}
函数指针变量 提供了一个可行的道路去操作 调用函数
int main()
{
int (*pa)(int, int) = Add;
int (*pb)(int, int) = &Add;
int r1 = (*pa)(3, 7);
int r2 = pb(3, 7); // 这个*号是可以不写的 因为里面存档的是一个函数地址,不管解引用还是不解引用都是函数地址
int r3 = Add(3, 7);
printf("%d\n", r1);
printf("%d\n", r2);
printf("%d\n", r3);
return 0;
}
*函数指针变量的格式: 函数返回的类型 (指针变量名字)(函数形参) = 函数名/&函数名
我们来看两段有趣的代码
int main()
{
(* (void (*)() ) 0 )(); // 这其实是一次函数调用
// 首先void (*) () 是一个函数指针类型 一个不返回任何类型 也不接受参数的函数
// 用括号括起来(void (*) ()) 表示强制转换 (void (*) ())0 的意思就是将0这个int类型 强制转换成 void (*) ()类型
// 而0就是这个函数指针变量的名字 相当于int (*pb)(int, int)中的pb
//(*(void (*) ())0)) 就相当于解引用 相当于这个(*pb) 实际上这个(*)是可以省略的
// 因此(* (void (*)() ) 0 )();其实就是一次函数调用
return 0;
}
// 函数声明
// 声明的函数叫 signal
// signal有两个参数 第一个参数类型是int
// 第二个参数类型是 void(*)(int) 这是一个函数指针类型 该指针可以指向一个函数,这个函数的参数是int 返回类型是void
// signal函数的返回值是void(*)(int) 的函数指针
// 也就是void (*signal(int, void(*)(int)))(int) 去掉signal(int, void(*)(int)) 剩下的就是它的返回类型
// 按照我们之前的习惯我们可能会这样写
// void(*)(int) signal(int,void(*)(int)); // 实际上这个代码的意思就是函数声明 但是编译器不支持
// 实际上这个会报错 编译器不支持这个写法
void (*signal(int, void(*)(int)))(int);
int main()
{
return 0;
}
我们发现这种void (*signal(int, void( * )(int)))(int); 函数声明的方式实在是有点难理解了
那有没有更好理解的想法呢 那我们就要学习typedef关键字了
4.1typedef关键字
typedef是用来 类型重命名的 可以将复杂的类型,简单化
比如 有些时候我们觉得unsigned int 太麻烦了 我们就可以用typedef重命名类型名字
typedef unsigned int unit;
int main()
{
unsigned int um1;
unit um2;
return 0;
}
那指针是否可以进行同样操作呢,是可以的
typedef int* pint;
int main()
{
int* p1 = NULL;
pint p2 = NULL;
return 0;
}
我们来看数组指针类型
//typedef int(*)[5] parr_t; // 这种写法是错误的
typedef int(*parr_t)[5];
// 这个时候parr_t 等价于 int(*)[5] 是一个数组指针类型
int main()
{
int arr[5] = { 0 };
int (*p1)[5] = &arr;
parr_t p2 = &arr;
return 0;
}
再来看函数指针类型
// 函数指针重命名
typedef void(*pf_t)(char*); //同样的 这个pf_t 等价于 void(*)(char*)
void test(char* s)
{
}
int main()
{
void(*p1)(char*) = test;
pf_t p2 = test;
return 0;
}
有了上面知识的丰富
我们再回过头来看之前那个代码
typedef void(*pf)(int);
pf signal(int, pf); // 这句代码和下面那个代码的功能是一样的
void (*signal(int, void(*)(int)))(int);
注意
typedef int* pint;
#define PINT int*
int main()
{
pint p1; // 这里是pint是一个完整的类型就是int*
PINT p2; // 这里是PINT 替换成int*
// 上面两个代码的效果是一样的
pint p1,p2;// 这里的两个变量都是int*类型
PINT p3,p4;// 这里p3 是指针 p4是指针
// 这两句话的效果就不一样了
return 0;
}
5.函数指针数组
我们知道 指针数组是用来专门存放指针的数组
- 字符指针数组: char* arr[5]; 存放字符指针
- 整形指针数组:int* arr[11]; 存放整形指针
因此 函数指针数组 就是存放函数的地址的数组 (注意是要相同的函数类型 才能放到一个数组里面)
格式 : 函数指针类型 数组名 [元素个数];
我们来看一个实例
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mid(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int (*p1)(int, int) = Add;
int (*p2)(int, int) = Sub;
int (*p3)(int, int) = Mid;
int (*p4)(int, int) = Div;
// 我们发现这四个函数指针变量都是同一个函数指针类型
// 我们就考虑将其放入函数指针数组里面
// 但是函数指针数组的 书写方式 和之前的函数声明一样都会不一样
//int (*)(int, int) arr[4] = { Add,Sub,Mid,Div }; // 按照以前的思路我们会这样写
// 实际上编译器并不支持 这样子
int (*arr[4])(int, int) = { Add,Sub,Mid,Div }; // 这个就是正确的写法 arr就是数组名 [4]就是元素个数
// 对这个函数指针数组进行使用
for (int i = 0; i < 4; i++)
{
int ret = arr[i](8, 4);
printf("%d\n", ret);
}
return 0;
}
那这个函数指针数组在什么时候会用到呢
我们来看一个实例 (做一个简易的计算器)
// 想写一个计算机器
// 完成两个整数的运算
// 1.加法 2.减法 3.乘法 4.除法
void menu()
{
printf("**************************************\n");
printf("**************简单计算器***************\n");
printf("******1.Add 2.Sub 3.Mid 4.Div******\n");
printf("****************0.exit****************\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, y;
int ret = 0;
do
{
menu();
scanf("%d", &input);
// 但是我们可以发现 我们写的代码有点冗余了
// 并且后期我们想添加功能,代码也会大量增加
switch (input)
{
case 1:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = Mul (x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数\n");
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;
}
我们可以发现 上面所有函数的类型都是一样的 返回的类型也是一样的 接受的参数也是一样的
并且我们以后拓展功能的时候 还有很多函数类型都是这个类型 int 函数名[int,int]
既然如此我们就可以用函数指针数组来优化这个代码
// 想写一个计算机器
// 完成两个整数的运算
// 1.加法 2.减法 3.乘法 4.除法
void menu()
{
printf("**************************************\n");
printf("**************简单计算器***************\n");
printf("******1.Add 2.Sub 3.Mid 4.Div******\n");
printf("****************0.exit****************\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, y;
int ret = 0;
int (*arr[5])(int, int) = { NULL,Add,Sub,Mul,Div }; // NULL 的原因是 让数组的下标和用户的选择对的上
menu();
scanf("%d", &input);
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = arr[input](x, y);
do
{
menu();
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = arr[input](x, y);
printf("%d\n", ret);
// 使用函数指针数组优化后 以后即便进行功能优化 代码也不会冗余 只需要在函数指针数组内多放几个函数地址就行了
}
else if (input == 0)
printf("已推出计算器\n");
else
printf("输入不合法,请重新输入\n");
} while (input);
return 0;
}
其实上述代码的函数指针数组的运用被称作转移表
我们来重温一下之前学的很多有关指针的概念
拓展:(指向函数指针数组的指针)
我们都知道指针本身是有内存空间的 也就是指针本身也是有地址的 那就可以有指针去指向指针
例如
int a = 10;
int* p1 = &a;
int* *p2 = &p1;
这个指针叫做二级指针 我们也学习过
那如果我想把函数指针数组的地址取出来放到一个指针变量中 我们要怎么做呢
char* test(int n, char* s) //一个函数
{
}
// 该函数的函数指针
char* (*pf)(int,char* s) = test; // pf是指针变量名字
// 该函数的函数指针数组
char* (*pfarr[])(int, char* s);
// 那么我们如果想要取出这个函数指针数组的地址 放到一个指针变量中 要怎么书写呢
char* (*(*pfarr)[])(int, char* s)
// (*pfarr) 这个指针指向了一个数组[] 这个数组里面放的元素类型都是char* (*)(int, char* s)
拓展:
我们上面使用了函数指针数组来解决了计算器代码 冗余的问题
那我们其实也可以用函数指针来解决
void menu()
{
printf("**************************************\n");
printf("**************简单计算器***************\n");
printf("******1.Add 2.Sub 3.Mid 4.Div******\n");
printf("****************0.exit****************\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;
}
// 其实我们还有一个思路
// 我们说在switch语句中 的冗余代码 我们可以考虑用函数去代替这个冗余代码
// 在把冗余代码编写成函数的时候我们说 按照以前的思路是不行的
// 如果我们按照以前的思路编写出下面这个函数
//void Calc()
//{
// int x, y;
// int ret = 0;
// printf("请输入两个操作数\n");
// scanf("%d %d", &x, &y);
// ret = Add(x, y);
// printf("%d\n", ret);
//}
// 我们发现 实际上 这个只能解决一个计算 比如加法 或者减法
// 但是在学习了函数指针数组之后 我们就可以解决这个问题
// 我们将函数指针作为参数传进去 就可以在函数中调用不同的函数了
void Calc(int (*p)(int, int))
{
int x, y;
int ret = 0;
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = p(x, y); // p代表传进来的函数的地址
printf("%d\n", ret);
}
int main()
{
int input = 0;
int x, y;
int ret = 0;
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");
break;
}
} while (input);
return 0;
}
ret);
//}
// 我们发现 实际上 这个只能解决一个计算 比如加法 或者减法
// 但是在学习了函数指针数组之后 我们就可以解决这个问题
// 我们将函数指针作为参数传进去 就可以在函数中调用不同的函数了
void Calc(int (*p)(int, int))
{
int x, y;
int ret = 0;
printf(“请输入两个操作数\n”);
scanf(“%d %d”, &x, &y);
ret = p(x, y); // p代表传进来的函数的地址
printf(“%d\n”, ret);
}
int main()
{
int input = 0;
int x, y;
int ret = 0;
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”);
break;
}
} while (input);
return 0;
}