目录
[2. 数组指针变量](#2. 数组指针变量)
[2.1 数组指针变量是什么?](#2.1 数组指针变量是什么?)
[2.2 数组指针变量怎么初始化](#2.2 数组指针变量怎么初始化)
[3. ⼆维数组传参的本质](#3. ⼆维数组传参的本质)
[4. 函数指针变量](#4. 函数指针变量)
[4.1 函数指针变量的创建](#4.1 函数指针变量的创建)
[4.2 函数指针变量的使用](#4.2 函数指针变量的使用)
[4.3 两段有趣的代码](#4.3 两段有趣的代码)
[4.3.1 typedef关键字](#4.3.1 typedef关键字)
[5. 函数指针数组](#5. 函数指针数组)
[6. 转移表](#6. 转移表)
1.字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char*。
第一种使用方法:
cpp
#include <stdio.h>
int main()
{
char ch = 'a';
char* p = &ch;
*p = 'c';
printf("%c\n", *p);
return 0;
}
另一种使用方法:
cpp
#innclude <stdio.h>
int main()
{
/*
char ch[] = "abcdef";
char* p = ch;
*/
char* ch = "abcdef";
printf("%c\n", *ch);//首字符的地址解引用
printf("%s\n", ch);//使用%s打印字符串的时候,只需要传首字符的地址
return 0;
}
前面我们说常量字符串不能被修改,那我们也可以试一下。
ch是常量字符串,对它进行修改是不允许的, 既然内容不能修改,也就是不希望被修改,那我们可以使用const来做限制。
如果我们加上const,如果来修改ch指向的空间中的值的话编译器会报错,但是如果不加,语法上是没有任何问题的,但是运行起来程序会崩溃。
关于字符串的笔试题:
cpp
#include <stdio.h>
int main()
{
char str1[] = "hello world!";
char str2[] = "hello world!";
const char* str3 = "hello world!";
const char* str4 = "hello world!";
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;
}
运行结果:
这是为什么呢?
这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
2. 数组指针变量
2.1 数组指针变量是什么?
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量?还是数组? 答案是:指针变量。
类比:
字符指针 - char* - 指向字符的指针 - 字符指针变量中存放字符变量的地址。
cpp
char ch = 'a';
char* p = &ch;
整型指针 - int* - 指向整型的指针 - 整型指针变量中存放整型变量的地址。
cpp
int a = 2;
int* p = &a;
那么数组指针就是指向数组的地址,数组指针变量存放的是数组的地址,&数组名就得到数组的地址。
cpp
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//*说明p是指针,p指向的是[10]数组,指向的数组是10个元素,每个元素int类型
//p就是数组指针,p中存放的是数组的地址
int(*p)[10] = &arr;//括号不能去掉,去掉p就和[]结合,说明它是数组,是个元素,每个元素int*类型
//int(*)[10] = int(*)[10] - 数组指针类型
//arr - int* arr+1跳过4个字节
//&arr[0] - int* &arr[0]+1跳过4个字节
//&arr - int(*)[10] &arr+1跳过40个字节
//类型决定了+1跳过多少个字节
return 0;
}
cpp
int *p1[10];//指针数组
int (*p2)[10];//数组指针
p先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个大小为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。 这⾥要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
cpp
#include <stdio.h>
int main()
{
char* ch[5];
char* (*pc)[5] = &ch;
//(*pc)表示是指针变量,[5]表示pc指向的数组5个元素,
//char*表示pc指向的每个元素char*类型
//pc的类型就是char*(*)[5]
return 0;
}
2.2 数组指针变量怎么初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的&数组名 。
通过调试我们看到arr和p的类型完全一样
数组指针类型解析:
cpp
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
那么这个数组指针到底有什么用呢?我们能不能把放进去的值拿出来呢?
cpp
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int (*p)[10] = &arr;
//使用p这个数组指针来访问arr数组的内容
for (int i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);
}
return 0;
}
输出结果:
那么(*p)[i]应该怎么理解呢?
这里用数组指针的形式去访问一维数组太过别扭,这里只是便于理解。
一维数组的话这样访问更容易。
下面就看看数组指针的使用场景。
3. ⼆维数组传参的本质
之前我们把一个一维数组传递给函数,那么也可以把一个二维数组传递给函数。
cpp
#include <stdio.h>
void print(int arr[3][5], int x, int y)
{
for (int m = 0; m < x; m++)
{
for (int n = 0; n < y; n++)
{
printf("%d ", arr[m][n]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
return 0;
}
输出结果:
经过上面的理解,也可以不写成数组的形式,可以写成指针的形式。
cpp
#include <stdio.h>
void print(int (*arr)[5], int x, int y)
{
for (int m = 0; m < x; m++)
{
for (int n = 0; n < y; n++)
{
printf("%d ", *(*(arr + m)+n));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
return 0;
}
输出:
二维数组传参的话形参要写成数组指针,那么打印部分的*(*(arr + m)+n)应该怎么理解呢?
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
4. 函数指针变量
4.1 函数指针变量的创建
变量有地址,数组有地址。
cpp
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;//一级指针
int arr[5] = { '\0' };
int (*parr)[5] = &arr;//数组指针
return 0;
}
那么函数是否有地址呢?
&变量名就可以取出变量的地址,&数组名就可以取出整个数组的地址,那么&函数名是不是就取出函数的地址了。
cpp
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//数组名表示首元素的地址,&数组名取出的是整个数组的地址,这两个是有区别的
//但是&函数名和函数名都是函数的地址,没有区别
printf("&Add = %p\n", &Add);
printf("Add = %p\n", Add);
return 0;
}
输出结果:
函数是有地址的,&函数名和函数名的都是函数的地址,这两个是没有任何区别的。
以前取出数组的地址想要存起来就得创建数组指针变量,整型取出地址想要存起来就得创建整型指针变量,那我想把函数的地址存起来呢?那我该怎么写类型呢?
cpp
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int,int) = Add;//pf 函数指针变量,和数组指针的写法类似
int (*pf)(int x,int y) = Add;//x y写上或者省略都是可以的
//int (*)(int,int) 函数指针类型
return 0;
}
函数指针类型解析:
cpp
int (*pf) (int x, int y)
| | ------------
| | |
| | pf指向函数的参数类型和个数的交代
| 函数指针变量名
pf指向函数的返回类型
int (*) (int x, int y) //pf函数指针变量的类型
4.2 函数指针变量的使用
通过函数指针调用指针指向的函数。
cpp
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
int index = (*pf)(30, 40);
printf("%d\n", index);
return 0;
}
输出结果:
调试:
我们通过函数指针来调用函数,算出的结果也是合适的。
就算把取地址符号加上,结果也是没有问题的。
cpp
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//没有讲函数指针之前是这样调用函数的
int index1 = Add(30, 40);//Add是函数的地址,直接用函数的地址可以调用
printf("%d\n", index1);
int (*pf)(int, int) = &Add;
int index = pf(30, 40);//pf里面就存放的函数的地址,所以*不写也是可以的
printf("%d\n", index);
/*
(*pf)(30, 40) - *写上也行,不写也行,写多个也行,其实没有真的去解引用,
但是这样写更加容易理解,pf是指针变量,对应指针的理解,要解引用然后去调用它,
所以如果也写*就必须加上括号,如果不加就去调用函数,然后返回70,*70对70解引用
就会出问题
*/
return 0;
}
4.3 两段有趣的代码
代码1:
cpp
(*(void (*)())0)();
这段代码什么意思?
代码2:
cpp
void (*signal(int , void(*)(int)))(int);
这段代码什么意思?
这种代码看起来比较费劲,类型比较多,其实我们可以使用typedef关键字简化一下代码。
4.3.1 typedef关键字
typedef是用来类型重命名的,可以将复杂的类型简单化。
⽐如,unsigned int写起来不方便,如果能写成uint就方便多了。
cpp
typedef unsigned int uint;//将unsigned int 重命名为uint
int main()
{
unsigned int a;//写起来太麻烦
uint b;//写起来简单
//上面两句代码一个意思
return 0;
}
如果是指针类型,其实也是可以的,比如,将 int* 重命名为 pint_t 。
cpp
typedef int* pint_t;
int main()
{
int* p;
pint_t p2;
return 0;
}
但是对于数组指针和函数指针稍微有点区别:比如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t,那可以这样写:
cpp
//数组指针
typedef int(*parr_t)[10];//重命名
int mainn()
{
int arr[10] = { '\0' };
int (*p)[10] = &arr;//p是数组指针变量
parr_t p2;
return 0;
}
函数指针类型的重命名也是⼀样的,比如,将int(*)(int,int)类型重命名为pf_t ,就可以这样写:
cpp
typedef int(*pf_t) (int, int);
int Add(int x, int y)
{
return x + y;
}
int mainn()
{
int (*p)(int,int) = Add;
pf_t p1 = Add;
return 0;
}
那么我就可以使用typedef来对我们的代码2做出相应的简化。
cpp
typedef void(*pf_t)(int);
int main()
{
//简化前
void (*signal(int, void(*)(int)))(int);
//简化后
pf_t signal(int, pf_t);
//这样写就容易理解多了,函数名是signal,参数是int类型和函数指针类型,返回值是函数指针类型
return 0;
}
5. 函数指针数组
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组。
cpp
int* arr[10];//整型指针数组
char* arr2[10];//字符指针数组
函数指针我们也学习了,函数指针里面存放的是函数的地址。
cpp
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 (*p1)(int ,int) = Add;
int (*p2)(int ,int) = Sub;
int (*p3)(int ,int) = Mul;
int (*p4)(int ,int) = Div;
return 0;
}
我们会发现,这么写太麻烦了,而且每个类型都是一样的,int (*)(int ,int),我们知道,数组是存放相同元素类型的集合,那么现在就要创建一个函数指针数组,里面放函数的地址。
cpp
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(*parr[4])(int, int) = { Add,Sub,Mul,Div };
//是数组就有下标 0 1 2 3
return 0;
}
那么我们怎么通过下标来找到对应的数组呢?
cpp
#include <stdio.h>
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(*parr[4])(int, int) = { Add,Sub,Mul,Div };
//是数组就有下标 0 1 2 3
for (int i = 0; i < 4; i++)
{
int index = parr[i](6 , 3);
printf("%d\n", index);
}
return 0;
}
正是因为这个函数的参数个数,参数类型,返回值是一样的,所以才能写成函数指针数组的形式,如果这个各不相同,是不能这样写的。
要学会先写函数指针,在函数指针的基础上改造函数指针数组是最容易的。
6. 转移表
函数指针数组的用途 - 转移表
计算器的实现:
cpp
#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 index = 0;
do
{
menu();//菜单
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
index = Add(x, y);
printf("%d\n", index);
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
index = Sub(x, y);
printf("%d\n", index);
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
index = Mul(x, y);
printf("%d\n", index);
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
index = Div(x, y);
printf("%d\n", index);
break;
case 0:
printf("退出计算器!!!\n");
break;
default:
printf("输入错误,请重新输入!!!\n");
break;
}
} while (input);
return 0;
}
运行代码:
这种代码只能完成加减乘除,如果将来还要算%,&&,||,&,|,<<,>>,这类的运算,那么我们的菜单就得扩充,函数也得添加,这些都是必须的,但是switch中的case语句也得添加,这会使我们的代码越来越长,其实我们可以简化代码,把这些函数放到一个指针数组里面。
cpp
#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 x = 0;
int y = 0;
int input = 0;
int index = 0;
int (*pf[4])(int, int) = {0, Add,Sub,Mul,Div };
//下标 0 1 2 3 4 加0是为了对齐
do
{
menu();//菜单
printf("请输入:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
index = pf[input](x, y);
printf("%d\n", index);
}
else if (input == 0)
{
printf("退出计算器!!!\n");
}
else
{
printf("选择错误,请重新选择!!!\n");
}
} while (input);
return 0;
}
输出结果:
这样的写法如果今后我要在代码中加其他的运算,只需要修改菜单,添加函数体,在函数指针数组中添加函数,修改判断范围即可,do while循环中不会因为添加函数而导致代码变长。
函数指针数组 - 转移表