前言:在对C语言指针进行初步入门之后,我们对指针——也就是地址有了基本的了解,如果还有对指针这部分知识不理解的同学可以看一下我写的关于指针入门的文章C语言——指针入门。
这里为大家引入两个名词,数组指针和指针数组,这两个名词有什么区别呢?
在指针入门阶段我们已经了解到,指针数组就是一个数组,一个存放指针类型元素的数组,而数组指针,顾名思义,它就是一个指针,一个指向数组的指针。
我们可以通过符号的优先级来进行判断:()>[]>* 所以:
(*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;
*p[n]:根据优先级,先看[],则p是一个数组,再结合*,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。
根据上面两个分析,可以看出,p是什么,则词组的中心词就是什么,即数组“指针”和指针“数组”。
数组指针的声明格式如下:
int main()
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
int (*p)[10]=&a;//指针指向数组
return 0;
}
(*p)代表它是一个指针,声明过后,p就代表a数组的地址。
总结:
指针数组:是一个数组,装着指针的数组
数组指针:是一个指针,指向数组的指针
我们已经知道了数组指针是指向数组的指针,那么我们应该如何用其进行对二维数组的遍历呢?
我们定义一个数组指针int (*p)[4]=&a代表一个指向一维数组的指针,该数组有5个元素
这里的p指向了二维数组a的首地址 ,也就是第0行
那么p+1等于多少呢,观察符号的优先级我们知道,p的大小是int*[5]也就是一行的大小,那么p+1也就指向了二维数组a的第1行,*(p+1)代表的是第1行的所有数据,也可以理解为第1行的数组名。所以大小为4个int字节的大小,为4x4=16
#include
int main(){ int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; int (*p)[4] = a; printf("%d\n", sizeof(*(p+1))); return 0; }
根据以上内容我们可以得到这些等价关系
a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
接下来使用数组指针进行二维数组的遍历
#include
int main() {
int a[3][4] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
int(*p)[4];
int i, j;
p = a;
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++) {
printf("%3d", *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
在写代码的时候难免要把数组或者指针的参数传给函数,那函数的参数应该如何设计呢?
这以下有这几种方式可以进行传参
可以直接将数组名直接传参,可以通过指针将数组的地址传入函数。
#include
void text(int *arr);
void text(int arr[]);
void text(int arr[10]);
void text(int* arr[20]);
void text(int** arr);
int main() {
int arr[10] = { 0 };
int* arr2[10] = { 0 };
text(arr);
text(arr2);
return 0;
}
我们首先要知道,二维数组的首地址是第0行,我们对其进行传参需要的函数参数应该是其一行的地址,而不是arr[0][0]的地址,所以有这两种方式进行传参。
#include
void text(int arr[3][5]); void text(int arr[][5]); void text(int(*arr)[5]); int main() { int arr[3][5] = { 0 }; text(arr); return 0; } 我们要注意,形参里面,二维数组可以没有行,但是必须要有列。所以arr[][5]是可以的,但是arr[ ][ ]和arr[3][ ]都是不行的。
这个问题其实大家很好理解,主要是在函数的应用上要注意参数的正确性
#include
void text(int* p); int main() { int a = 0; int arr[5] = {1, 2, 3, 4, 5}; int* p = &a; text(p); text(&a); text(arr); return 0; }
二级指针传参主要要注意指针数组也是可以的,因为指针数组储存的是指针类型的元素,那么直接把数组名传过去相对应把首元素的地址传过去,而首元素的地址也是一个储存地址的元素,所以也是一个二级指针,这是成立的。是
#include
void text(int** p); int main() { int a[3]={1,2,3}; int* p = a; int** p1 = p; int* arr[2]; text(p1); text(&p); text(arr); }
函数也是有地址的,我们可以通过指针变量去指向函数的地址
对于函数来说,&函数名和函数名都是函数的地址
声明函数指针的变量和函数数组类似,int(*p)(函数形参),*p指的是一个指针变量
为什么要用函数指针呢?
那么,有不少人就觉得,本来很简单的函数调用,搞那么复杂干什么?其实在这样比较简单的代码实现中不容易看出来,当项目比较大,代码变得复杂了以后,函数指针就体现出了其优越性。
举个例子,如果我们要实现数组的排序,我们知道,常用的数组排序方法有很多种,比如快排,插入排序,冒泡排序,选择排序等,如果不管内部实现,你会发现,除了函数名不一样之外,返回值,包括函数入参都是相同的,这时候如果要调用不同的排序方法,就可以使用指针函数来实现,我们只需要修改函数指针初始化的地方,而不需要去修改每个调用的地方(特别是当调用特别频繁的时候)。
最典型的应用就是函数回调
函数回调就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其他所指向的函数时,我们就说这是函数回调。
这里写一个最基本的计数器小程序给大家参考理解
#include
int Add(int a, int b) { return a + b; } int Cdd(int a, int b) { return a - b; } int Mdd(int a, int b) { return a * b; } int Ddd(int a, int b) { return a / b; } void cals(int(*p)(int, int)) { int x=0, y=0; scanf_s("%d %d", &x, &y); int ret = 0; ret = p(x, y); printf("%d\n", ret); } void meun() { printf("1.Add\n2.Cdd\n3.Mdd\n4.Ddd\n5.exit\n"); } int main() { int flag = 0; while (flag != 5) { meun(); scanf_s("%d", &flag); switch (flag) { case 1: cals(Add); break; case 2: cals(Cdd); break; case 3: cals(Mdd); break; case 4: cals(Ddd); break; } } return 0; } 我们可以看到通过函数回调我们提高了程序的可读性以及之后的可修改性。
感谢大家的阅读。