本期内容,将继续介绍C语言中的指针,带大家理解一些指针变量的特点,以及二位数组传参的本质和转移表的相关知识。
一. 字符指针变量
之前我们介绍过一种指针类型为字符指针:char*。它可以存放字符的地址,解引用字符指针就能得到这个字符:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'y';
return 0;
}
知道了基本用法,我们可以看以下代码;
#include <stdio.h>
int main()
{
char arr[10] = "abcdef";
char* p = &arr;
char* ps = "abcdef";
printf("%s\n", arr);
printf("%s\n", p);
printf("%s\n", ps);
printf("%p\n", p);
int a = 10;
int* pa = 10;
printf("%d\n", a);
printf("%d\n", pa);
printf("%p\n", pa);
}
运行结果:
上述代码中:
1.字符数组(arr[10])可以存放字符串,字符数组的内容可以被修改。
2. char* ps = "abcdef"; 这里的 abcdef 被称为常量字符串,和字符数组是非常相似的,也是在一个连续空间中存放多个字符,但是常量字符串不能被修改。
3. printf("%s\n", p); 是可以直接找到整个字符串并打印,但是对比下面的 printf("%d\n", pa); 就只能打印出变量a的地址的十进制数字。(事实上,任意类型的变量的地址,用%d都会打印出对应的十进制数字,而%s是打印到 \0 才会停止)
我们可以再去调试以下代码:
int main()
{
const char* p = "abcdef";
}
这里就很明显能看出来,当字符指针p指向一个常量字符串时,也是将常量字符串的首字符‘a’的地址存放在字符指针p中。
示意图
有了上述知识,我们就能来看一道和字符串有关的笔试题:(出自《剑指offer》)
#include <stdio.h>
int main()
{
char str1[] = "hello C.";
char str2[] = "hello C.";
const char* str3 = "hello C.";
const char* str4 = "hello C.";
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;
}
上述代码中,我们可以看到,str1 == str2 是在比较两个数组的首元素地址;str3 == str4 是在比较两个字符指针变量中存放的地址,那么选1/2/3/4呢?
同样的,我们可以调试起来:
这样子答案就很明显了,str3和str4一样,str1和str2不一样(选2,3)。运行一下:
果真如此。
这里str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
所以str1和str2不同,str3和str4相同。
示意图
二. 数组指针变量
2.1 基本介绍
我们在上起讲过,&arr其实是数组指针类型,parr = &arr,那么这里的parr就是数组指针变量。它实质上是指针变量,存放的是数组的地址,能够指向数组的指针变量。
我们需要先知道数组指针变量的表达形式:
int* p1[10];
int (*p2)[10];
思考一下:p1、p2分别是什么?
int* p1[10]; 这个表达形式我们的上期内容有提到(int* arr[3] = { &a,&b,&c }),所以这里的p1是指针数组变量,本质是数组,用来存放指针变量的。
那么这里的p2就是数组指针变量。int (*p2)[10] ,我们来分析一下:p2先和*结合,说明p是一个指针变量;然后指针指向的是是一个大小为10个整型的数组。
示意图
所以p2是一个指针,指向一个数组,这就是数组指针。
这里要注意,[ ] 的优先级要高于*,所以必须要加上()来保证p先和*结合。
如果想要存放数组的地址,就得存放在数组指针变量中:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr;
调试起来也会发现,&arr和p的类型是完全一致的。
2.2 二维数组传参的本质
上期内容我们用指针数组模拟实现了二维数组,现在有了数组指针的理解,我们就能进一步了解二维数组传参的本质。
如果我们需要一个函数来打印一个二维数组的每个元素,可以这样写:
#include <stdio.h>
void test(int arr[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 ", 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} };
test(arr, 3, 5);
return 0;
}
二维数组其实可以看做是每个元素是一维数组的数组,也就是二维数组的每一个元素都是一维数组,例如二维数组的首元素就是二维数组的第一行,是一个一维数组。
根据“数组名就是数组首元素的地址”这个规则,二维数组的数组名(arr)表示的就是第一行的地址,是一维数组的地址。
按上述代码,第一行的一维数组的类型就是int [5],所以第一行的地址的类型就是数组指针类型 int (*) [5]。(若有一数组int arr[20],则其类型为int [20])
那么形参部分是不是也能写成指针形式呢?如下:
#include <stdio.h>
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));//也可以写成p[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;
}
运行结果:
打印数组元素的时候,我们用 *(*(p + i) + j ,其实这句和 p[ i ][ j ]完全相同。*(p + i)就是第(i+1)行的数组名。
总结:
1.二维数组的传参本质上也是传递了地址,传递的是第一行这个一维数组的地址。
2.二维数组传参,形参部分可以写成数组,也可以写成指针形式。
三. 函数指针变量
3.1 函数指针变量的创建和使用
前面我们学习过整形指针、数组指针。类比一下,我们不难得出:函数指针变量应该是用来存放函数的地址,通过该地址能够调用函数。
可以写一段代码验证一下:
#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;
}
运行结果:
结论:函数是有地址的,函数名、&函数名都是函数的地址,没有区别。
如果我们将函数的地址存放起来,就得创建指针变量。函数指针变量的写法和数组指针非常类似:
int Add(int x, int y)
{
return x + y;
}
int (*pf1) (int, int) = Add;
int (*pf2)(int x, int y) = &Add;
这里需要注意:创建pf1和pf2的语句不一样,但实际上二者完全相同,“&”和“x/y”写上或者省略都可以。
代码中可以看出,函数指针的类型是:int ( * ) (int , int ),和数组指针的类型相似。解析示意图:
这样,我们就能通过函数指针调用指向的函数:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf1)(int, int) = Add;
printf("%d\n", (*pf1)(2, 3));
printf("%d\n", pf1(2, 4));
return 0;
}
运行结果:
(*pf1)和 pf1 两种调用方式都可以。
3.2 分析代码
有了以上的知识介绍,我们来看看两个很有趣的代码:(均出自《C陷阱和缺陷》一书)
1. (* ( void ( * ) ( ) 0 ) ) ( ) ;
看到这样奇怪的代码,肯定是一头雾水不知道它在干嘛,我们一步步分析即可。
1. void ( * ) ( ) ,这个结构是不是感觉一丝熟悉呢?是不是和 int ( * ) (int , int ) 非常相似?没错,这就是一个函数指针类型。
2.void ( * ) ( ) 0 ,void ( * ) ( ) 是函数指针类型,而 0 是整型。是不是和 int(3.14) 非常相似?没错,这就是在强制类型转换,让 0 从整型转化为函数指针类型。
3.* ( void ( * ) ( ) 0 ) ,这一步对当前的 0 的地址进行解引用,其实就是在调用 0 地址处的函数。
所以总的来说,上述代码本质上就是在调用函数。
这样一分析,是不是就感觉清晰多了?分析过程中,我们先从最里面的括号入手,依次向外展开;同时找出我们熟悉的结构,类比并加以判断,最终成功分析代码。
有了经验,我们再来看看第二题:
void ( *signal ( int , void ( * ) ( int ) ) ) ( int );
一一分析:
1. void ( * ) ( int ) 是函数指针类型。
2. signal 会先与后面的括号结合(而不是先和*结合),signal ( int , void ( * ) ( int ) ),其实就是一个函数的声明,signal是函数名,它有两个参数:一个是整型,一个是函数指针类型。
3.函数必然会有返回值,这里我们可以将signal ( int , void ( * ) ( int ) )删掉,就会更明显:void ( * ) ( int ) ,这是一个函数指针类型,说明函数signal的返回值是函数指针类型。
4.既然返回值是 void ( * ) ( int ) ,那为啥不写成void ( * ) ( int ) signal ( int , void ( * ) ( int ) ) 呢?这样写确实更明确,但是语法上并不支持,*后面必须接函数名。
所以这其实是一次函数的声明。
四. 函数指针数组和转移表
指针数组,即存放指针的数组,比如:
int * arr[10];//数组的每个元素是int*类型
如果将函数的地址存到一个数组中,那么这个数组就称为函数指针数组。
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
哪一个是函数指针数组呢?
既然是函数指针数组,那么该数组存放的元素的类型应该是函数指针类型,应该是 int ( * )( )这样的结构。但是 3.3分析代码 中我们提到“ *后面必须接函数名 ”,所以数组名应该在 * 后面()之内,很明显parr1就是函数指针数组。
parr1先和 [ ] 结合,说明parr1是数组。
[3] 说明数组有3个元素。
int ( * )( ) 说明元素类型是函数指针类型。
函数指针数组的用途之一就是做转移表。例如,我们需要一个可以做加减乘除等计算的计算器,就可以将各种运算方式写成不同函数,并存放在同一数组里,需要时再从数组中调用。
可以这样写:
#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;
}
void Menu()
{
printf("******************\n");
printf(" 1. Add 2. Sub \n");
printf(" 3. Mul 4. Div \n");
printf(" 0. exit \n");
printf("******************\n");
printf("请选择:\n");
}
int main()
{
int x, y;
int ret;
int input;
int(*pf[5])(int x, int y) = { 0,Add,Sub,Mul,Div };
do
{
Menu();
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("请输入操作数:\n");
scanf("%d %d", &x, &y);
ret = (*pf[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("非法输入,请重试\n");
}
} while (input);
return 0;
}
这样写,如果后续还要新增计算器功能时,就可以直接将函数存放在数组里,然后菜单新增选项即可。
其实写一个计算器也可以不用函数指针数组。用switch/case语句,将input与函数对应起来也是可以的,但是这样肯定没有使用函数指针数组方便。
五. 总结
夯实基础,无限进步!若有疑惑者欢迎讨论!
六.额外补充
上述我们实现计算器功能时用do/while循环让程序能一直运行下去,直到用户选择退出。
在测试过程,本人发现:在选择计算器功能时,如果输入 5、6、7这样的数字,确实会报非法输入并让用户重新输入。但是如果输入的是字符x、y、z等等,就会陷入死循环反复打印:
非法输入,请重试
******************
1. Add 2. Sub
3. Mul 4. Div
0. exit
******************
请选择:
询问师长才明白,可能是因为scanf出了问题:%d只能读取整型数据,scanf检测输入的字符不是整型就不会进行读取,依旧读取上次的值,但是输入的还残留在输入缓冲区,这样scanf再去检测、读取上次的值......就造成了死循环。有两个方法能证明:
1.调试起来就会发现,输入字符x后,input的值并没有被改动,依旧是随机值,说明scanf并没有读取。
2.我们可以写一个相似的代码:
#include <stdio.h>
int main()
{
int i= 1;
int j = 0;
while (i != 0)
{
scanf("%d", &i);
printf("%d\n", j++);
}
return 0;
}
输入x后,也同样会造成死循环。
那么怎么解决呢?
其实只需要清空输入缓冲区即可,可以用getchar读取输入缓冲区:
char c;
while ((c = getchar()) != '\n');
放在scanf后面,尽管scanf没有读取数据,getchar也能读取完输入缓冲区,达到清空缓冲区的目的。这样,就不会再出现死循环的情况了。
标签:arr,int,基础知识,数组,printf,函数指针,C语言,指针 From: https://blog.csdn.net/T_L_the_language/article/details/144073538