1. 字符指针
一般来说,字符指针使用char*来表示。在学习的过程中,如果所传的参数是数组,那么指向的其实是数组首元素的地址。在以下例子中,理论上p里面应该存的是4个字符的地址大小,但字符串大小是7个字节。其中不是把字符串赋给p,而是把首字符地址赋给p。
#include<stdio.h>
int main()
{
char* p = "abcdef"; //不是把字符串赋给p,而是首字符地址赋给p
printf("%c\n",*p); //a
printf("%s\n",p); //abcdef
return 0;
}
理论上来讲,那既然是字符串的话,是不是也可以像往常一样,对其中的内容进行更改呢?我们做出了以下尝试。可以发现,我们更改p指向的首字符地址中内容时,报错了。主要是因为字符串“abcdef”是一个常量字符串,不可以进行更改。所以正常来说,使用const char* p = “abcdef”是最合理的。
练习:分析下列程序
char arr1[] = "abcdef";
char arr2[] = "abcdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (arr1 == arr2)
{
printf("hehe\n");
}
else{
printf("haha\n");
}
if (p1 == p2)
{
printf("hehe\n");
}
else{
printf("haha\n");
}
答案:
haha
hehe
分析: 首先新建两个数组,两个数组开辟两块不同的空间,因此两个空间地址必然不同。而p1和p2都是常量字符串,不能被更改,因此打印hehe
2. 指针数组
指针数组的本质就是数组,可以通俗理解为存放指针元素的数组。常用场景:
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* parr[] = {arr1,arr2,arr3}; //数组名表示的是首元素地址。
int i = 0;
for ( i = 0; i < 3; i++)
{
int j = 0;
for ( j = 0; j < 5; j++)
{
printf("%d ",*(parr[i] + j));
}
printf("\n");
}
3. 数组指针
数组指针本质上是指针,是可以指向数组的指针。再复习一下先前学习过的:
arr - 首元素地址
arr[0] - 首元素地址
&arr - 数组地址
那什么是数组指针呢?如果想要存放数组的地址又需要如何表示呢?
//要把数组的地址存起来
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr; //数组指针 - p相当于是数组指针的名字,不是*p
//需要打印出arr中的每个元素
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for ( i = 0; i < sz; i++)
{
printf("%d ",(*p)[i]);
// printf("%d ",*(*p + i)); //p = &arr - *p == arr
}
分析:
&arr - 数组的地址
int* p = &arr - p是个指向整型的指针,但是&arr是数组,所以不可以用p来存放
int* p[10] = &arr - 这是个指针数组 - 因为[ ]的结合优先性高于*
从程序的优化性来说,数组指针更适合用于二维数组。
往常如果我们想要打印出二维数组,最常使用的就是参数为数组的形式。那如果把参数换成指针的形式,应该如何操作呢?
分析:
- 二维数组本质上可以看作是一维数组。数组名字表示为首元素的地址,于是在二维数组中,数组名字表示第一行元素的地址,而第一行元素中又由5个元素组成。因此创建数组指针p作为是第一行数组指针——int (*p)[5]
在print2()中,使用到了两种打印方式,分别是
- 方式1:printf("%d ",*(*(p + i) + j));——p+i找到每行元素地址,解引用找到每行元素,加j表示找到每列元素地址,解引用找到元素。
- 方式2:printf("%d ",(*(p + i))[j]);
int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; int* p = arr; for ( i = 0; i < 10; i++) { printf("%d ",p[i]); printf("%d ",*(p + i)); printf("%d ",*(arr + i)); printf("%d ",arr[i]); }
其中p是指向arr的指针,表示数组首元素的地址。arr本身就表示数组首元素地址。于是可以得出 p == arr,第二三句打印指令的结果是一致的。
其次,arr[i] == *(arr + i) == *(p + i) == p[i]。于是方式2的打印语句可以再次调整简化为
printf("%d ",p[i][j]);
#include<stdio.h>
//参数是数组的形式
void print1(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for ( i = 0; i < row; i++)
{
for ( j = 0; j < col; j++)
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
//参数是指针的形式
void print2(int (*p)[5], int row, int col) //相当于是个一维数组
{
int i = 0;
for ( i = 0; i < row; i++)
{
int j = 0;
for ( j = 0; j < col; j++)
{
printf("%d ",p[i][j]);
printf("%d ",*(*(p + i) + j));
printf("%d ",(*(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}};
//打印二维数组
print1(arr,3,5);
print2(arr,3,5);
return 0;
}
练习:分析以下语句的含义
- int arr[5]; —— arr是一个数组,该数组有5个元素,每个元素的类型是int
- int *parr1[10]; —— parr1是一个数组,该数组有10个元素,每个元素的类型是int*,因此 parr1是一个指针数组
- int (*parr2)[10]; —— parr2是一个指针,该指针指向一个数组,该数组有10个元素,每个元素的类型是int,因此parr2是个数组指针。
- int (*parr3[10])[5]; —— parr3 是个数组,该数组有10个元素,每个元素都是一个数组指针,该数组指针有5个元素,每个元素都是int类型。具体图示分析如下。
4. 数组传参、指针传参
在写代码的过程中,难免要把【数组】或者【指针】传给函数,那函数的参数要如何设计呢?
一维数组传参
在移位数组的传参过程中,以下方法都可以使用。可以直接用数组传参,也可以把数组名看作是首元素地址进行指针传参。
#include<stdio.h>
void test(int arr[]){}
void test(int arr[10]){}
void test(int *arr){}
void test2(int *arr[]){}
void test2(int *arr[20]){}
void test2(int **arr){}
int main()
{
int arr[10];
int *arr2[20];
test(arr);
test2(arr2);
return 0;
}
二维数组传参
使用数组传参需要注意: 二维数组只能省略行,坚决不能省略列。
使用地址传参的思想,参考(3. 数组指针)中的内容。
void test(int (*arr)[5]){}
void test(int arr[3][5])
int main()
{
int arr[3][5] = {0};
test(arr);
return 0;
}
一级指针传参
在一级指针传参中,① 能直接传入变量地址 ② 能传入存放地址的变量。
#include<stdio.h>
void test(int *p){}
int main()
{
int a = 10;
int* p = &a;
test(&a);
test(p);
return 0;
}
二级指针传参
在二级指针传参中,和一级指针有相似之处。① 能直接传入变量地址 ② 能传入存放地址的变量。此外,③ 能传入指针数组
#include<stdio.h>
void test(int **p){}
int main()
{
int* arr [10];
test(arr);
return 0
}
5. 函数指针
我们知道数组指针是指向数组的指针,那函数指针就是指向函数的指针。在数组指针中&arr ==数组地址 arr == 首元素地址,而在函数指针中&函数名 == 函数名 == 函数地址
定义函数指针和定义数组指针的规律也相似。
- 返回值类型 (*指针名称)(参数1类型,参数2类型...)= 函数名称
- 例如:int(*p)(int,int) = Add;
例如:
#include<stdio.h> void Print(char* str) { printf("%s\n",str); } int main() { void (*p)(char*) = Print; //函数指针 (*p)("hello,world!"); return 0; }
练习:分析以下代码
(*(void (*)())0)(); —— 把0强制转换成void (*)( )函数指针类型 - 0就是一个函数的地址,然后调用0地址处的该函数。实际上就是一次函数调用。
void (*signal(int, void (*)(int)))(int); —— signal是一个函数声明,有两个参数,一个是int类型,一个是函数指针类型。该函数指针指向的函数参数是int,返回类型是void。signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void
如果按照理解来说,原来的语句应该写成这样的形式void(*)(int) signal(int, void (*)(int));但实际上来说是不对的。那为了更容易能正确书写,还可以有以下编写方式。把void(*)(int)替换成pfun_t
需要注意的是书写规范。
typedef void(* pfun_t)(int) pfun_t signal(int,pfun_t) (int)
最后,实际上在函数指针的调用中,无论是否有解引用符号,都是可以的。这里的解引用符号实际上没有作用。
int (*pa)(int,int) = Add;
printf("%d\n",(pa)(2,3));
printf("%d\n",(*pa)(2,3));
6. 函数指针数组
要把函数的地址存到一个数组中,则这个数组就叫做函数指针数组。函数指针数组本质是数组,元素类型是函数指针。在以下的例子中,四个函数的参数类型相同,返回类型相同,所以都可以用同样的函数指针*pa进行表示。此时就需要一个数组来存放这些函数地址,即函数指针的数组。int (*parr[4])(int,int) = {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;
}
int main()
{
//函数指针数组
int* arr[5];
//需要一个数组,这个数组可以存放4个函数的地址 - 函数指针的数组
int (*pa)(int,int) = Add; // Sub/Mul/Div
int (*parr[4])(int,int) = {Add,Sub,Mul,Div}; //函数指针数组
int i = 0;
for ( i = 0; i < 4; i++)
{
printf("%d\n",parr[i](2,3));
}
return 0;
}
练习:
char* my_strcpy(char* dest, const char* src);
要求:
1. 写一个函数指针pf,能够指向my_strcpy
char* (*pf)(char*,const char*) ;
2. 写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址。
char*(*pfArr[4])(char*,const char*) ;
实际来说,函数指针数组会常用做转移表。实例:计算器。
#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 XOR(int x,int y)
{
return x ^ y;
}
void menu()
{
printf("*****************************\n");
printf("*****1. add******2. sub******\n");
printf("*****3. mul******4. div******\n");
printf("*****5. xor******0. exit******\n");
printf("*****************************\n");
}
int main()
{
//实现计算器
int input = 0;
int x = 0;
int y = 0;
//pfArr 是一个函数指针数组 - 转移表
int (*pfArr[6])(int,int) = {0,Add,Sub,Mul,Div,XOR};
do
{
menu();
printf("请选择:>");
scanf("%d",&input);
if (input >=1 && input <= 5)
{
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
int ret = pfArr[input](x,y);
printf("%d\n",ret);
}
else if(input == 0){
printf("退出\n");
}else
{
printf("选择错误\n");
}
}while(input);
return 0;
}
7. 指向函数指针数组的指针
指向函数指针数组的指针本质是一个指针,指向的是一个数组,数组的元素类型都是函数指针。其中ppfArr就是指向函数指针数组的指针。
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,用于对该事件或条件进行响应。
对于6中的实例计算器来说的话,能想到的最简单的代码就是用switch...case语句,但是在实现的过程中会有冗余的情况发生。或许想说那不然就把case事件执行的前两句语句分装成函数,但其实还是需要多次重复调用,本质上是没有什么区别的。那到底要怎样解决这样的冗余问题呢?使用回调函数
switch (input)
{
case 1:
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n",Add(x,y));
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n",Sub(x,y));
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n,",Mul(x,y));
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n",Div(x,y));
break;
case 5:
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n",XOR(x,y));
break;
case 0:
printf("退出\n");
break;
default:
break;
}
其中我们在函数Calc中,把函数指针作为参数传入。很好的解决了代码冗余的问题。
void Calc(int (*pf)(int,int))
{
int x = 0;
int y = 0;
printf("请输入两个操作数:>");
scanf("%d%d",&x,&y);
printf("%d\n",pf(x,y));
}
do
{
menu();
printf("请选择:>");
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 5:
Calc(XOR);
break;
case 0:
printf("退出\n");
break;
default:
break;
}
}while(input);