前言
指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
- 指针的运算。
这个章节,我们继续探讨指针的高级主题。
提示:以下是本篇文章正文内容,下面案例可供参考
一、字符指针
常规使用
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
return 0;
}
示意图如下: 特殊使用
#include<stdio.h>
int main()
{
char* p = "hello bit";
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
示意图如下:
注:"hello bit"这是一个常量字符串,而对于常量,我们是要求不能修改的,我们上述代码中是把h的地址给了P,那么通过*P是可以修改常量字符串内容的,一旦进行修改,系统就会报错。 所以,更加妥善的写法是const char * p = “hello bit”;
趁热打铁,我们来看一道字符指针的面试题:
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
char *str3 = "hello bit.";
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;
}
解释1: 数组是独立开辟空间的,str1和str2分别是两个字符数组的数组名,数组名是数组首元素地址,str1和str2分别是两个不同数组首元素的地址,自然是不一样的。
解释2: str3和str4都是const修饰的char*指针,这两个指针都指向一个常量字符串。
而常量字符串又是不能修改的,也就是它的内容永远是"hello bit",那么我其他的指针也想来指向这个不可修改的"hello bit"时,为了节省内存,就没必要再创建一块额外空间了,还指向原来的那个即可。
所以这里str3和str4指向的是同一个地址
二、指针数组
指针数组是指针还是数组? 这个问题你就想,好男孩,好男孩的本质是男孩啊 指针数组本质还是数组
举个例子:
int main()
{
int arr1[5];
char arr2[3];
int* parr[5];
char* pbrr[4];
return 0;
}
示意图如下 应用实例1:
int main()
{
int a = 123;
int b = 213;
int c = 312;
int* arr[3] = { &a,&b,&c };
for (int i = 0;i < 3;i++)
{
printf("%d\n", *arr[i]);
}
return 0;
}
应用实例2:
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
int* parr[] = { arr1,arr2,arr3 };
for (int i = 0;i < 3;i++)
{
printf("%d\n", *parr[i]);
}
return 0;
}
如果想得到三个数组的所有元素
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
int* parr[] = { arr1,arr2,arr3 };
for (int i = 0;i < 3;i++)
{
for(int j=0;j<3;j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
ps:还有一个老生常谈的话题: arr[i]=*(arr+i),因为arr是数组名(一个地址)嘛, arr+i就是这个地址往后i个单位,也就是arr[i]的地址, 对(arr+i)解引用,就可以得到arr[i]
应用实例3:
#include<stdio.h>
int main()
{
const char* arr[5] = { "abc","defg","hijkl","mn","opqish" };
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
三、数组指针
3.1 数组指针定义
数组指针是数组还是指针?
还是那个判别方法:好男孩,性质是好,本质是男孩 所以,数组指针本质上是个指针
int *p1[10];
int (*p2)[10];
解释:p2先和 * 结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。 所以p2是一个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于 * 号的,所以必须加上()来保证p先和*结合。
示例:
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char c = 'w';
char* pc = &c;
int arr[10] = { 0 };
int* p = arr;
int(*parr)[10] = &arr;
return 0;
}
3.2 &数组名vs数组名
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
printf("=====我是分割线====\n");
printf("%p\n", arr+1);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr+1);
return 0;
}
综上,我们可以了解到,数组首元素地址和整个数组地址的区别: 他们在进行±操作时,跳过的单位是不同的, 前者是跳过一个数组元素大小,后者则是跳过整个数组的大小。
数组名不是数组首元素地址的两种特殊情况:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", sizeof(arr));
return 0;
}
3.3 数组指针的使用
引子:
#include<stdio.h>
int main()
{
int* arr[10] = { 0 };
int* (*p)[10] = &arr;
int** p1 = arr;
return 0;
}
示例1:
#include <stdio.h>
void print(int(*parr)[10],int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", parr[0][i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(&arr,sz);
return 0;
}
解释如下:
示例2:
void print(int(*p)[5],int r,int c)
{
int i = 0;
for (i = 0;i < r;i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
printf("%d ", (*(p + i))[j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
print(arr, 3, 5);
return 0;
}
解释如下:
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
对于parr3,它首先是和[10]结合,我们就可以确定它本质是一个10元素的数组 然后把int ( * parr3[10] )[5]中的parr3[10]去掉就可以得到int ( * )[5], 也就是数组元素为数组指针,这种指针指向数组有5个元素,每个元素为int型
示意图如下:
四、数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1一维数组传参
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
int arr[10] = { 0 };
int *arr2[20] = { 0 };
test(arr);
test2(arr2);
return 0;
}
4.2二维数组传参
热知识:二维数组首元素指的是该二维数组的第一行
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int(*arr)[5])
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
4.3一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p, sz);
return 0;
}
4.4二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
五、函数指针
5.1引子
示例:我们常常接触的指针大多有如下几类: 整形指针-存放整形地址,指向整形 字符指针-存放字符地址,指向字符 数组指针-存放数组地址(注意不是数组首元素地址),指向数组
由以上三个例子,我们能总结指针的共同点:存放某个类型变量的地址,指向那个类型的变量,但是在讲函数指针首先有一个问题:函数也有地址吗?我们用一段简单的代码来验证一下即可。
#include<stdio.h>
int Add(int x,int y)
{
return x+y;
}
int main()
{
printf("%p\n",&Add);
return 0;
}
屏幕上打印出地址:
所以答案是有的,函数也存在地址,也就衍生出了今天的知识点-函数指针。
5.2使用步骤
5.2.1取函数地址
我们知道&数组名,取出的是数组的地址。单独一个数组名,取出的是数组首元素的地址。但是对于函数来说:函数名==&函数名
我们代码验证一下(示例):
#include<stdio.h>
int Add(int x,int y)
{
return x+y;
}
int main()
{
printf("%p\n",&Add);
printf("%p\n",Add);
return 0;
}
显然,打印出来的地址是一样的,但是这个时候也会有同学跳出来说:“那数组名和&数组名打印出来的地址还一样呢,但意义明显不一样啊”。但是你想想,函数也没有首元素等其他玩意啊,它就是它本身啊,它也不会出现什么函数首元素啊。
所以再次声明: 在函数指针这一块 函数名==&函数名,它的意义和值,都是一样的
5.2.2创建函数指针
我们知道,数组指针用来存放数组地址,整形指针用来存放整形地址。。。函数指针也不例外,它用来存放函数地址,我们现在定义一个p来存放Add地址,那它的类型怎么创建?我们来看一下具体步骤:
1.p是一个指针对吧,给它一个*是不是必须的 p变成了 * p。为了确保 * 和 p结合(如果没有括号,*或者p有可能会与其他的一些符号结合,具体参见符号优先级)那我在 * p外面加一个括号便于观看也没有问题吧,也就是(*p)
2.那函数总得有参数啊,比如这里是Add(int x,int y)。参数x和y的类型是int 你指针指向的函数是不是要找一下它的参数。所以(*p)(int,int)
3.那函数还有一个性质啊,有没有返回值,要是有的话,类型呢? 这里以Add为例,它是返回int型,所以我们指针也返回int 型 即int(*p)(int,int)
到这里Add函数指针的类型就创建完成啦即为*int(p)(int ,int)
需要注意的是:不同函数的参数类型和返回值类型是不一样的,到时候需要根据不同函数对类型进行转换,这里只是以Add函数为例,其他函数以此类推
ps:一个快速判别类型的方法——去掉变量的名字,剩下的就是类型 代码如下(示例):
int a = 10;
int arr[10] = { 0 };
int(*parr)[10] = &arr;
int(*pf)(int, int) = &Add;
5.2.3通过函数指针调用函数的两种方法
法一: 我们平时在调用函数的时候,一般就是函数名( ,)然后把参数传进括号即可,那我们现在有函数指针了呀,指针怎么使用?
p不是指向了函数Add嘛,我们用*解引用指针,得到的是地址里的东西,
也就是说 *p==Add,用 * p(,)来传参也可以实现Add函数的调用。 代码如下:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(2, 3);
printf("%d\n", ret);
int(*p)(int, int) = &Add;
ret = (*p)(3, 3);
printf("%d\n", ret);
}
法二: 我们在二.1取函数地址那一块介绍了,在函数指针这一块,函数名==&函数名, 也就是说创建函数指针的时候可以这样写:int(*p)(int, int) = Add,Add是赋给了p啊,你也可以认为:p就是Add。
你可以这样理解,法一是int(*p)(int, int) = &Add,是把Add的地址给p,所以用p来调用函数要解引用一下,但是法二p就是Add,那不用解引用了,直接调用。代码如下:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*p)(int, int) = Add;
int ret = p(3, 6);
printf("%d", ret);
}
5.3函数指针进阶
大家来看这样一个代码
( * (void(*)() ) 0)()
乍一看非常复杂,我们来细化一下
1 . ( * (void( * )() ) 0)() 我们抽出加粗部分 这是我们熟悉的老朋友:void( * )(),这不就是一个函数指针嘛,该函数无参,返回类型void
2 . (void( * )() ) 0是什么?我们联想一下(int)3.14,不就是对3.14强制类型转换嘛,将3.14这个浮点型强制转换成整形。这里同样的道理,是将整形0强制转换成类型为void( * )()的一个函数指针
3 .现在有了(void( * )() ) 0,我们在这个东西前面加一个 *,这个是什么意思,我们知道(void( * )() ) 0已经被转换成一个指针(指针即地址)了,地址前面加一个 *表示解引用,取出地址里的东西,也就是找到了那个函数
4 . (void( * )() ) 0表示那个函数那再在后面加一个()即是对函数的调用,也就是( * (void(*)() ) 0)()
六、函数指针数组
6.1引子
示例:以下这个代码,我们可以很清楚的看出,arr是一个整形指针数组,数组里的每个元素均为整形指针
int *arr[10];
那我们就会有接下来的两个问题: 整形指针能放入到一个数组内, 那么参数、返回类型相同的*函数指针能否也可以放入一个数组内? 如果可以,我们应该如何创建那个数组呢?
6.2使用步骤
6.2.1创建函数指针数组
先上代码(示例):
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*p1)(int, int) = Add;
int(*p2)(int, int) = Sub;
int(*parr[2])(int,int)={Add,Sub};
}
由以上代码,我们不难看出,函数Add和函数Sub除了名字和功能不同,它们的参数和返回类型均相同,我们可以写出它们共同的函数指针类型(详情见上一期的C语言快速入门):int(*)(int,int) ,也就是说,只要我们由Add和Sub两个函数的地址,我们都可以放到int( * )(int,int)这个类型中去
那么Add和Sub这两个函数指针数组类型怎么创建?我们举两个简单的例子:
int a;
int arr[10];
整形和整形数组类型的差别也就是多了一个[ ]这样的东西
这里找函数指针数组类型的创建法也是同样的道理, 我们仍以Add函数为例,已知了Add函数指针类型为int( * )(int,int),我们在 * 后跟一个[ ]即可
也就是如下代码,初始化和别的数组初始化没有什么区别, 我们正常把函数Add和Sub放入(函数名==&函数名,详情见上一期函数指针)
int(*parr[2])(int,int)={Add,Sub};
6.2.2函数指针数组的实际使用案例
比如我们这里写一个加减乘除的简易计算器 代码如下(示例): Add Sub Mul Div分别代表加减乘除
#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 ***\n");
printf("***2.Sub ***\n");
printf("***3.Mul ***\n");
printf("***4.Div ***\n");
printf("***0.exit ***\n");
}
int main()
{
int input = 0;
do
{
int x = 0;
int y = 0;
int z = 0;
menu();
printf("请选择:");
scanf("%d", &input);
int(*parr[5])(int, int) = { 0,Add,Sub,Mul,Div };
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入2个操作数\n");
scanf("%d %d", &x, &y);
z=parr[input](x,y);
printf("计算结果为%d\n", z);
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
使用示例:
该段代码有以下一些注意点: 1.在写函数指针数组时,因为下标的缘故,菜单中的选项和实际存放的函数指针是不匹配的,我们可以在Add前加一个0(该0本身是无意义的)将下标依次加1个
2.parr是存放函数指针的数组,有了下标也就有了某个函数指针(也就是函数地址)我们在函数指针那一块讲解过:&Add=Add,调用函数的时候可以直接用函数地址调用(详情见上一期C语言快速入门)
这里用函数指针数组写的好处是什么? 在日常的工作中,我们如果要写一个需要用到很多函数的程序,而这个程序将来也极其有可能增加新的或删减旧的函数。传统的更改方式,一旦不小心,可能会损伤到其他正常的代码,导致满盘皆输。如果用函数指针数组,我们只需要对函数指针数组大小变换一下,再将新的或旧的在函数指针数组初始化中修改一下即可,而在函数调用方面也不需进行更改,非常非常非常的方便。
七、指向函数指针数组的指针
这里开始就出现无限套娃了,其实方法都是一样的。 这里我们只展示如何定义,函数指针数组的指针也就是函数指针数组取地址
int add(int x, int y)
{
return x + y;
}
int (*pf)(int, int) = add;
int (*pfArr[5])(int, int);
int(*(*ppfArr)[5])(int, int) = &pfArr;
八、回调函数
回调函数就是一个通过函数指针调用的函数。 如果你把函数的指针(地址)作为参数传递给另一个函数, 当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
九、指针和数组笔试题详解
题库整理1
题库整理2
题库整理3
总结
本文着重介绍了指针数组、数组指针、函数指针、函数指针数组等知识点。指针作为C语言学习的大头,需要读者们认真细致的学习,而一些指针/或者数组的套娃定义也是其中的难点,建议读者们在学习完本文后,认真练习本文中第九节的笔试题,借此提升自己对指针的理解。
|