继上一篇目的内容,我们继续学习指针的相关知识。可能大家发现,越往后似乎越难了,那大家就更应该反复观看、巩固前几章的内容。
目录
1.字符指针变量
字符指针,顾名思义就是指向字符的指针,其类型为char*。我们学习过整型指针了,再来看字符指针相对就比较简单,如下面代码:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
上面的代码理解后,我们再来看一组代码:
int main()
{
const char* p = "hello bit.";//这⾥是把⼀个字符串放到p指针变量⾥了吗?
printf("%s\n", p);
return 0;
}
上面代码中定义了字符指针变量p,而且被在 * 左边的const修饰。我们在深入理解指针(1)中提到,const修饰指针变量时,放在*的左边,限制的是指针指向的内容,不能通过指针来修改,但是可以修改指针变量本身的值。忘记的赶紧去看看。那我们思考:这行代码是将一个字符串hello bit放到了指针变量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");
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和str3、str4的内容看似一样,其实不然,关键就在于str1和str2是用字符数组进行初始化,而数组每进行一次初始化,都需要开辟一块新的内存空间,这两块空间是不相同的;而str3和str4是利用具有const修饰的字符指针来初始化的, 是常量字符串,常量字符串会被存储在单独的一个内存区域,当多个指针同时指向它时,其实指向的是空一块空间,我们来看下面图解:
所以这段代码的运行结果应该是:
2.数组指针
2.1数组指针的概念
先问大家一个问题:数组指针是指针变量还是数组?
答案自然是指针变量。那这是个什么样的指针呢?我们可以类比,整型指针是指向整型的指针,存放的是整型变量的地址,字符指针是指向字符的指针,存放的是字符变量的地址,那数组指针就是指向数组的指针,存放的是数组的地址。
2.2数组指针变量的初始化
我们先来回顾一下下面这个代码:
int *p1[10];
int (*p2)[10];
上述第一个代码我们再上一篇目中学习过,它属于指针数组,其中每一个数组都是int*类型,其数组为int*[10]类型;而第二个就是我们今天的数组指针变量。*和p2在一起,说明p2是一个指针变量,它指向一个有10个元素的数组,其中每个元素都为int类型,该指针变量为int(*)[10]类型。
其实又上面两个例子也能看出,一定要加了括号*才会和p2结合,否则就先和[ ]结合了,说明[ ]的优先级是要高于*的,也说明这个括号是必不可少的。
那数组指针变量要怎么初始化呢?首先我们先明白,数组指针是指向数组的指针,即存放的是数组的地址,所以我们应该如下:
//数组指针
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;//指向数组的指针
//int* p1[10];//p1指针数组 - 存放指针的数组
//int(*p2)[10];//p2是数组指针变量 - 指向数组的指针
return 0;
};
我们在学习一级整型指针时,应该初始化指针变量时等式的右边放上数组名arr,而arr和&arr的区别我们也多次强调,他们进行+-整数时的增量是不一样的,尽管他们的初始位置相同,我们利用下面代码来加深对这两者的理解:
int main()
{
int arr[10] = { 0 };
int* p1 = arr;
//int* int*
printf("%d\n", p1);
printf("%d\n", p1 + 1);
int(*p2)[10] = &arr;
//int(*)[10] int(*)[10]
printf("%d\n", p2);
printf("%d\n", p2 + 1);
return 0;
}
arr只代表首元素地址,arr+1就代表第二个元素的地址(+1跳过一个int);&arr代表整个数组的地址,&arr+1代表的是跳过一整个数组后的地址(+1跳过一个int*[10])。
那我们能否利用数组指针来访问数组元素呢?自然可以:
//不好的示范
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
//& *
//*&ptr - ptr
for (int i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);
}
return 0;
}
但是这是个不好的示范,简单问题复杂化了,这里只是为了让大家知道可以这么使用。
3.二维数组传参的本质
有了数组指针作为基础,我们现在来探讨一下二维数组传参的本质。
我们之前说过一维数组传参的本质实际上是传递首元素的地址;⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。那对于二维数组呢?
//二维数组传参的本质
void showArr(int arr[][5], int rows, int cols)
{
int i = 0;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
printf("%d ", arr[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} };
//数组名是数组首元素地址 &ptr sizeof(ptr)
//
showArr(arr, 3, 5);
return 0;
}
我们能看到,上述用了函数showArr来打印二维数组的元素,形参部分写成了二维数组的形式(二维数组中的行可以省略),那如果我们要写成指针形式该怎么写呢?我们首先要明白,二维数组中的每一个元素都是一个一维数组,那这每一个一维数组,而我们想用指针访问的话,指针解引用得到的其实是数组,这正好符合我们数组指针的定义。我们再深入理解指针(2)也给大家留了一个问题,能否用指针变量来访问二维数组,那这里我们就能得到答案,可以,并且这个指针变量是数组指针,我们将深入理解指针(2)对指针访问二维数组这一小节的代码用数组指针来写:
#include<stdio.h>
int main()
{
int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
int(*p)[5] = arr;//p是指针数组,指向的数组为int[5]类型
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
我们对于二维数组传参如如何写成指针形式,我们也就清晰明了了,我们将代码进行修改:
//二维数组传参的本质
//void showArr(int arr[][5], int left, int right)
void showArr(int(*arr)[5], int rows, int cols)
{
int i = 0;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
printf("%d ", *(*(arr + i) + j));//arr[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} };
//数组名是数组首元素地址 &ptr sizeof(ptr)
//
showArr(arr, 3, 5);
return 0;
}
传入数组指针进入形参即可。而且这个数组指针指向的数组,应该是二维数组中的第一个元素,也就是第一行:
所以我们能总结二维数组传参的本质:
conclusion 1:二维数组传参的本质传递的也是地址,是第一行元素的这个一维数组的地址。
conclusion 2:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
4.函数指针
4.1函数指针的概念
先问大家一个问题:函数指针是指针变量还是函数?
答案自然是指针变量。那这是个什么样的指针呢?我们可以类比,整型指针是指向整型的指针,存放的是整型变量的地址,字符指针是指向字符的指针,存放的是字符变量的地址,我们上面刚接触的数组指针是指向数组的指针,存放的是数组的地址,所以函数指针就是指向函数的指针,存放的是函数的地址。
4.2函数指针变量的初始化
函数指针就是指向函数的指针,存放的是函数的地址,那么将来我对函数指针进行解引用,就可以调用这个函数,实现利用函数指针调用函数。我们来看看下面的这个例子:
//函数指针
int add(int x, int y)
{
return x + y;
}
int main()
{
printf("add = %p\n", add);
printf("&add = %p\n", &add);
int(*pf)(int, int) = &add;//pf是函数指针变量 - 指向函数的指针
int(*pf)(int, int) = add;
//int(*)(int int)
return 0;
}
我们能看到,我们定义了一个函数指针pf指向加法函数add,其*和pf结合,说明pf是一个指针,后面的括号内表示指向的函数add内部的参数为int和int类型;值得注意的是,函数的地址可以用&add表示,也可以直接用add,不需要取地址操作符。
int (*pf3) (int x, int y)
// | | ------------
// | | |
// | | pf3指向函数的参数类型和个数的交代
// | 函数指针变量名
//pf3指向函数的返回类型
int (*) (int x, int y)
4.2函数指针变量的使用
那取出了函数的地址,当我们需要使用该函数的时候,就可以通过该地址直接访问:
//函数名 &函数名 都是函数的地址,没有区别
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = add;
int c = add(2, 3);//函数名调用
printf("%d\n", c);
int d = (*pf)(3, 4);
printf("%d\n", d);//函数指针调用
int e = pf(4, 5);
printf("%d\n", e);//函数指针调用
return 0;
}
我们看上述代码,首先定义的函数指针pf指向函数add,那我们再调用函数时就可以有3中表达:①直接用add调用,如int c = add(2, 3) ②使用函数指针调用,如int d = (*pf) (3, 4) ③使用函数指针调用,在2的基础上省略解引用操作符,如int e = pf(4, 5)。
4.4理解两段代码
如果大家能明白并且理解函数指针的相关内容,我们尝试来读懂《C陷阱和缺陷》中的两段代码。
4.4.1代码1
(*(void (*)())0)();
看到代码大家首先不要慌,我们从外到内进行剖析:
首先,我们看外部的结构,其实是一个函数指针:
(* )();
(void (*)())0
我们将内部的内容取出,剩下函数指针的外壳,该指针指向的函数内部没有参数,所以后面的括号内部什么也没有。
那再细看,函数指针内部中的0是一个特殊的地址,前面部分在将括号去掉,又是一个函数指针类型:void(*)(),说明这是一个强转换,将0这个地址的类型强转为函数指针类型。
所以代码1的意思就是:将0强转化为函数指针类型得到这个指针指向的函数,再进行调用。
4.4.2代码2
void (*signal(int , void(*)(int)))(int);
对于代码2,我们再从外到内分析:
首先我们还是看外部结构:
void (*)(int);
signal(int , void(*)(int))
同理得到函数指针外壳,表示指向一个无返回值,有一个参数并且参数类型为int类型的函数。
再细看内部,内部是一个函数signal,内部有两个参数,一个是int类型,另一个也是函数指针类型,这个函数指针又指向了一个无返回值,有一个int类型参数的函数。
所以代码2的意思就是:声明了一个函数signal。
4.5 typedef关键字
我们在初始化数据变量时,需要说明它的类型,但有些变量的类型比较复杂,如函数指针、数组指针等。我们为了简化代码,让它看起来简洁明了,就可以用typedef关键字重定义。
4.5.1 typedef的使用
//typedef类型重定义
typedef unsigned int u_int;
typedef int* pint_t;
int* (p)[5];//p是数组指针变量
typedef int(*parr_t)[5];//pfarr_t就是int(*)[5]
typedef void(*pf_t)(int);//pf_t就是void(*)(int)
int main()
{
u_int a1;
unsigned int a2;
pint_t p1;
int* p2;
pf_t pf1;
int(*pf2)[5];
return 0;
}
我们看到这段代码开头将unsigned int用typedef重定义为u_nit,所以我们再使用unsigned int时就可以直接用u_nit就行了,如上述定义了a1和a2,类型是一样的。同理,我们将int* 重定义为pint_t,那我们再使用int*时直接用pint_t就行了,如上述定义的p1和p2数据类型就是一样的。
但是在重定义函数指针这样的类型时,我们要特别注意它变量名的位置,因为它的写法是将你重定义的名称直接放在原本类型中变量名的位置,如上述重定义了数组指针int(*)[5]为parr_t,注意parr_t是写在*后面这个位置,而不是后面。同理,函数指针也是一样的。
4.5.2 typedef和define的区别
那有些人就会好奇了,typedef和define有什么区别呢?不仅功能看起来没什么区别,就连写法上都差不多。但是大家一定要明白他两是不一样的,我们首先回顾一下define关键字:
define是宏定义,程序在预处理阶段将用define定义的内容进行了替换 。
我们抓住“替换这个词”,它只是替换,这就是最明显的区别。我们来看看:
typedef int* ptr_t;
#define PTR_T int*
ptr_t p1, p2;//p1,p2都是指针变量
PTR_T p1, p2;//p3是指针变量,p4是整型变量
我们重定义int*类型为ptr_t,有宏定义PTR_T,那下面的两行代码中,第一行相当于将p1,p2都定义成了指针变量,因为重定义ptr_t也是类型,所以每个变量前面都会带*来说明p是一个指针;而对第二行,由于仅仅是“替换”,只将p1定义为int*类型,而p2就不会带上那个*符号,因为它只是替换,所以p2只是整型变量。
5.函数指针数组
经过之前的阅读,我们很容易知道函数指针数组就是存放函数指针的数组,数组中的每一个元素都是一个函数指针,而这个指针又指向的一个函数。
5.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;
}
int main()
{
int(*pf1)(int, int) = add;//pf1是函数指针变量
int(*pfarr[4])(int, int) = { add,sub,mul,div };//pfarr是函数指针数组
//8 4
for (int i = 0; i < 4; i++)
{
int r = (*pfarr[i])(8, 4);
printf("%d\n", r);
}
return 0;
}
我们定义了加减乘除四个函数,而后定义了函数指针数组parr。这个写法我们注意,之前有说过[ ]的优先级要高于 * ,所以parr先和[ ]结合,即parr是数组,该数组内有四个元素,每一个元素都是函数指针变量。
int (*parr[4]) (int x, int y)
// | | ------------
// | | |
// | | parr中的指针所指向函数的参数类型和个数的交代
// | 函数指针数组变量名
//parr指向函数的返回类型
int (*[4]) (int x, int y)
5.2函数指针的用途
函数指针的用途 - 转移表
我们用函数指针实现计算器:
//转移表 - 计算器
void menu()
{
printf("*********************************\n");
printf("******* 1.add 2.sub ********\n");
printf("******* 3.mul 4.div ********\n");
printf("******* 0.exit ********\n");
printf("*********************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int z = 0;
//函数指针的数组
int(*pfArr[5])(int, int) = { 0, add,sub,mul,div };
// 0 1 2 3 4
do
{
menu();
printf("请选择:");
scanf("%d", &input);//3
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
z = pfArr[input](x, y);
printf("%d\n", z);
}
else if(input = 0)
{
printf("退出计算器\n");
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
我们发现,再需要使用加减乘除四种函数的时候,无需一个个add,sub,mul,div的去调用,而是直接用函数指针pfArr [input](x,y),通过input值的不同得到不同的函数指针,再通过函数指针调用函数。