这篇文章介绍的是一些关于指针的更加深入的知识。
1.字符指针
字符指针就是 char* 类型的指针变量,既可以存字符的指针,也可以存字符串的地址,当用来存字符串的地址时,存的是字符串首字符的地址,类似于数组名。
如 char* p ="abcdef" ,在这行代码中,p存的是 a 的地址。如果我们对p解引用想要改变字符串内容时,编译器会报错。这是因为在编译器"看来","abcdef"是一个常量字符串,常量是不能被修改的,因为常量存在内存中的常量区,常量区是只读区,只能够读取数据不能修改,当我们通过p解引用去修改常量表达式时,写入权限冲突,编译器会报错。所以我们在定义这种常量的指针时最好用 const 来修饰,表达他的不可修改性。
下面这一段代码的结果是什么?
int main()
{
char* p1 = "abcd";
char* p2 = "abcd";
char p3[] = "abcd";
char p4[] = "abcd";
if (p1 == p2)
{
printf("p1=p2\n");
}
else
{
printf("p1!=p2\n");
}
if (p3 == p4)
{
printf("p3=p4\n");
}
else
{
printf("p3!=p4\n");
}
return 0;
}
对于p1和p2,他们存的都是一个常量表达式的地址。在内存中有专门的一片区域用来存放常量和常量表达式,是只读区,当我们写下上面这样的代码时,编译器就会在只读区找一块内存来存放这个常量字符串,因为常量都是不能修改的,所以像这样的相同的常量只会存一份,所以p1和p2存的是同一块内存的地址,所以p1=p2.
对于p3和p4,他们是在内存中分别找了一块空间把这个常量字符串拷贝了一份,所以他们所在的内存空间不是同一块,所以p3!=p4.

2.指针数组
指针数组的本质是数组,是用来存放指针变量的数组。比如 int* arr[10]; 是一个一级指针数组,
int* * arr2[10];是一个二级指针数组。与数组是差不多的,只是指针数组存的是指针类型的数据。
3.数组指针
数组指针本质上是指针,是指向数组的指针。数组指针存的是整个数组的地址。
下面有两个代码:int* p1[10];
int (*p2)[10];
对于第一个,变量p1先于[ ] 结合,所以p1是一个数组,数组的元素类型是int* 。这是一个指针数组。
而对于第二种写法,p2在括号内先于 * 结合,所以p是一个指针,后面的 [10]表示 p2 指向的是一个存放十个元素的数组,int 表示数组的每个元素是int 类型的。p2是一个数组指针。
在之前我们讲到过 &数组名 得到的是整个数组的地址,存放整个数组的地址就要用数组指针来存。
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
return 0;
}
对于数组指针的定义如上,数组指针变量名首先要与 * 先结合表示他是一个指针,[10]表示它指向的数组有十个元素,int表示数组元素的类型。
对于上面的数组指针 p ,它的类型是什么呢?很简单,与指针定义时一样,我们把变量名去掉就是他的指针类型,上面的p的类型就是 int ( * ) [10];
数组指针的定义一般是 数组元素类型+( * 指针变量名)+[ 数组元素个数 ] 。在这里数组元素的个数是不能省略的,如果省略了编译器会把它默认为0,用这样一个指针类型去接受整个数组的地址时两边元素个数就不一样了,不是同一类型,编译器会报错。
数组指针的使用
int (*p ) [10] =&arr; 中p是指向数组的,*p就相当于是数组名,数组名又是数组首元素的地址,所以 *p 本质上是数组首元素的地址。还有一种更方便理解的解释就是,因为 p 是&arr,*p 的*
与& 相互抵消,于是 * p 就等于arr,这里的arr没有用&也不是单独放在sizeof内,所以是数组首元素的地址。
那这样我们可以用数组指针来访问数组的每个元素。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*p + i));
}
return 0;
}

但是当我们这样来用数组指针的话,会感觉十分的别扭,好像数组指针用起来不如一个普通的存放数组首元素地址的指针好用,确实是这样的。其实这不是数组指针的正确用法,数组指针一般都是用在二维数组和三维数组。
比如我们有一个二维数组 int arr[3][4]={ 1,2,3,4,5,6,7,8,9,10,11,12};,当我们用这个数组传参时,函数的形参部分可以写成数组形式,但是我们要知道传参传过去的本质上是这个二维数组的首元素的地址,二维数组的首元素是他的第一行,是个一维数组,而数组的地址就应该存在数组指针变量中,于是我们就可以这样写 int (*p)[4] ,这样就把二维数组整个第一行的地址表示了出来。p指向的是第一行,对p+1就是跳过一行的空间。*p指向的是第一行第一个元素,对*p +1就是跳过一个元素的大小,得到的是第一行第二个元素的地址,于是 *( * (p+i) +j) 就是取出二维数组第 i 行第 j 列的元素,等价于 p[ i ] [ j ] 。
void Print(int(*p)[4], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
Print(arr, 3, 4);
return 0;
}

数组指针的数组
数组指针数组就是一个存放数组指针的数组,在上面我们知道 数组指针类型的变量名是写在(*)中的,当变量名先和 * 结合他就是指针。当我们这样写的时候
int (* p[10] ) [ 5 ]={&arr1,&arr2,&arr3};
在这里,p先与 [ ] 相结合,所以这里的 p是个数组名,而数组中有10个元素。把p和[10]拿走就是数组中元素的类型,也就是说整个数组中的每个元素都是指向5个整型的数组的数组指针。
4.数组传参和指针传参
当我们用数组传参的时候,形参的部分可以写成数组的形式和指针的形式。
对于一维数组传参,形参部分可以写数组,数组的元素个数可以省略也可以不省略,都不影响,因为本质上是指针。
也可以写成指针的形式,写成数组元素类型的指针类型或者数组指针的形式。
一维数组中的数组指针传参是,形参部分可以写成二级指针的形式,因为数组名是首元素的地址,而首元素又是指针类型,相对于二级指针传参。
对于二维数组的传参,函数的形参部分可以写成二维数组的形式,可以省略行都是不能省略列。
形参写成指针的形式则可以写成数组指针,因为传过去的是一个一维数组的地址。不能写成二级指针,第一行的地址就是一级指针,不是二级指针。
5.函数指针
数组指针是指向数组的指针,那类比过来 函数指针就是指向函数的指针。
我们可以先验证一下函数是否有地址,
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", Add);
printf("%p\n", &Add);
return 0;
}

从这段代码我们就可以知道,函数也是有地址的,而函数的地址也和&函数名的地址数值是一样的。都是要注意的是,这里函数名和数组名是不一样的,函数名和&函数名是一样的性质,他们没有区别。因为函数地址没有什么首元素或者整个函数之分,函数的地址只是一个函数的入口,当我们对函数的地址解引用找到函数并传参时,就是调用函数。 函数的地址和全局变量一样,在编译期间就有他的地址了,函数只要声明或者定义了就有他的地址而不是要等到调用的时候才有地址,函数是存在代码区的,代码区也是只读区,只能读取不能写入修改。
那怎么创建一个函数指针呢?我们可以参考前面的数组指针的写法,对于函数来说,最重要的就是返回类型和函数参数。比如
int (* pf) (int , int ) = &Add ;在这里,前面的 int 表示函数的返回类型,pf先与 * 结合表示pf是一个指针变量,后面括号中的(int ,int )表示函数的参数类型,参数名可以写也可以不写。
函数指针的用法?
我们可以通过 pf 来找到函数,因为pf =&Add ,对pf 解引用就相当于找到了函数,如果再传参的话就是对函数进行调用。比如 int ret = (*pf)( 2 ,3 );这就是找到Add并传参调用,返回值赋给ret。 在前面我们说过,Add和&Add是一样的,没有任何区别,所以其实 pf 前面的 * 可以有也可以不写,甚至可以写多个,前提是在括号里与pf结合,* 就是一个摆设。
函数指针的作用是什么?
要知道函数指针的作用,我们先来看一段代码。我们要实现一个简易的计算器功能,如下
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");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
Menu();
do
{
printf("请选择_>\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret=Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出\n");
default:
printf("输入错误,请重新输入->\n");
break;
}
} while(input );
return 0;
}

在这里我们发现,在switch语句中有很多重复的代码块,那我们有什么办法能够精简代码呢,每一个代码块好像只有调用的函数有区别,而这几个函数的返回类型和参数都是相同的,那我们就可以用一个函数指针来表示这些函数,这样可以删去很多重复的代码。
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");
}
void Work(int input, int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数->");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
pf = Add;
break;
case 2:
pf = Sub;
break;
case 3:
pf = Mul;
break;
case 4:
pf = Div;
break;
}
ret = pf(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
Menu();
do
{
printf("请选择_>\n");
scanf("%d", &input);
switch (input)
{
case 1:
case 2:
case 3:
case 4:
int (*pf)(int ,int)=Add;
Work(input,pf);
break;
case 0:
printf("退出\n");
break;
default:
printf("输入错误,请重新输入->\n");
break;
}
} while (input);
return 0;
}
当我们用函数指针去写这个代码的时候就会省去很多重复的代码。
6.函数指针数组
函数指针数组就是每个元素都是函数指针的数组,定义方法可以参考数组指针数组和函数指针的定义。比如前面的简易计算器的代码还可以这样写:
int (* pf[4] )(int, int)={Add,Sub,Mul,Div};
这样写在Work函数中又可以把switch语句省略,直接有input-1作为下标来调用函数。
7.指向函数指针数组的指针
指向函数指针数组的指针的定义方法可以参考指向数组的指针。
比如上面的 int (* pf[4] )(int, int)={Add,Sub,Mul,Div};这是一个函数指针数组,我们可以取出数组的地址存放到函数指针数组的指针中
int ( (*pff) [4] ) ( int ,int)=&pf;
到这里逻辑已经很复杂了,而且用的也不多,他的用法也可以参考数组指针来类推。
指针和数组还可以继续套娃,但是已经没有意义了,我们不会用到这么复杂的代码,一般用的是函数指针数组。
8.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
比如在计算器的第二个版本中,我们在Work函数中通过函数指针来调用Add等函数,就是回调函数。