目录
一、指针与数组
先来看以下代码及运行的结果
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
这说明,数组名所表示的含义就是数组首元素的地址。
但是在两个操作符下,数组名表示的不是首元素地址,而是整个数组。
sizeof(数组名),此时数组名表示整个数组,计算得出的结果是整个数组的大小
&数组名,此时数组名也表示整个数组,取出的是整个数组的地址
除此之外,一般数组名就是首元素地址,因此,可以建立指针与数组的联系,并利用指针对整个数组进行遍历或修改等一系列操作
int arr[5] = {1,2,3,4,5};
int* p = arr; // p中存放首元素地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(int i = 0 ; i < sz ; i++)
{
priintf("%d ", *(p+i)); //遍历数组
}
指针p首先指向数组第一个元素,再通过+i的方式向后跨越相应的步长,再解引用得到对应下标的元素
指针遍历操作也可以用下一种方法表示
for(int i = 0 ; i < sz ; i++)
{
printf("%d ", p[i]);
}
而我们最初遍历数组时,最常用方法的就是
for(int i = 0; i < sz ; i++)
{
printf("%d ",arr[i]);
}
对比上面两段代码,很可能会不太理解为什么指针p能够用[]下标引用操作符来访问数组成员,但是实际上,正如上文所述,一般情况下,数组名表示数组的首元素,也就是说,数组名本身也是一个指针,指向数组首元素。既然数组名这个指针可以使用[],指针p当然也可以使用[]来访问其成员。
二、数组传参
2.1一维数组传参
一般情况下,我们不会在主函数内对数组进行修改,而是调用其他函数来完成,将数组作为参数传递给函数。为了简单,以下均以遍历数组为例
void print1(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
}
void print2(int pa[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int sz = sizeof(arr) / sizeof(arr[0]);
print1(arr, sz);
printf("\n");
print2(arr, sz);
return 0;
}
如上文所述,数组名本质是一个指向首元素的指针,所以在print函数中,需要用一个指针来接收数组,如函数print1。也可以像print2那样,创建一个名为pa的数组,[]内可以填任意正数(写太大也会报错的),但是填写数字没有意义。
调用print2上面这段代码的处理是,创建一个int* 类型的名为pa的指针,指向实参数组的首元素地址,和调用print1时一样。所以[]内没必要填入任何数字,没有意义。
数组传参时接收用print1和print2中两种方法都可以,一种更体现本质,一种更容易理解,具体用那种看个人喜好。
2.2二维数组传参
所谓二维数组,其实是多个一维数组拼接起来形成的。
二维数组的数组名也是一个指针,也指向数组中的首元素,但是指针类型是一个数组指针。上代码
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
int (*p)[3] = arr;
//第二行代码中必须加括号
//因为[]操作符的优先级大于*
//不加括号会导致p成为一个指针数组。
这里的arr是一个二维数组,由三个容量为3的一维数组拼接而成。
数组名表示首元素地址。这里的首元素不是1,而是第一行的整个一维数组地址。即arr为指向一个容量为3,元素类型为int的数组的数组指针。如下图所示
知道了二维数组数组名表示的是什么,就可以用相应类型的变量来接收它,仍以遍历为例
void print1(int (*p)[3], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", *(*(p+i)+j) );
}
}
}
void print2(int p[][3], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", p[i][j]);
}
}
}
int main()
{
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
print1(arr, 3, 3);
printf("\n");
print2(arr, 3, 3);
return 0;
}
对一个三行三列的二维数组进行遍历。
print1中,*(p+i)先定位到行,如当i为0时,定位到二维数组第一行,之后 *( *(p+i) + j),再定位到列。一开始,p是一个数组指针,其步长为所指一维数组的大小,因此对p+i后会跨越一行。在第一次解引用后,p是一个int类型的指针,指向某行的第一个元素,对其+j即可确定列。这样,p就可以指向 i+1 行 j+1 列的元素(因为数组下标是从0开始,所以要+1),再对其解引用,即可获取该元素
print2要好理解的多,p[i][j],先由 i 确定行,再由 j 确定列,然后获取元素。需要注意的是,参数中的int p[][3],后面[3]中数字不可省略且必须与实参一致,因为这关系到指针的步长。前面[]中的数字无意义,不需要填写
如print2这样写读起来也比较方便,推荐二维数组操作时像print2这样用下标进行访问,可读性强
三、函数指针及应用
函数也有其地址,也可以用&函数名取出函数的地址,但是不加&符,一样会得到函数的地址
void f(int* a)
{
*a = 10;
}
int main()
{
printf("%p\n",f);
printf("%p\n",&f);
}
//二者打印完全一样,没有区别
这里不能类比数组。对函数而言,获取其地址时,这两种方式的意义完全一样,没有区别
还要说明的是,函数指针p在使用时,加*与不加*效果也完全一样,看如下代码及结果,编译器没有提示错误。
void f(int* p)
{
*p = 10;
}
int main()
{
void (*p)(int*) = f;
int a = 0;
p(&a);
printf("第一次调用后a = %d", a);
a = 0;
(*p)(&a);
printf("第二次调用后a = %d", a);
}
甚至于可以这样写
void f(int* p)
{
*p = 10;
}
int main()
{
void (*p)(int*) = f;
int a = 0;
p(&a);
(*p)(&a);
(&f)(&a);
(*********p)(&a);
}
对于函数名来说,加*或加&与否都无所谓,摆设而已。但是*可以无限加,&只能加一个,因为两个&&就是按位与了
函数指针的应用是函数回调,如在c语言内置库函数快速排序qsort中,就用到了函数回调,下面是快速排序的函数声明
第一个参数是待排序数组,第二个参数是数组元素个数,第三个参数是数组单个元素大小,第四个参数就是函数指针
这个函数指针是一个指向返回值为int,参数为两个const void*的函数。
这里需要的是一个比较函数,当返回值为正数时,p1会排到p2前,返回负数时,p1排到p2后,返回0时,不动。
所以,我们要排序一个数组,可以这样来调用
int cmp(const void* p1, const void* p2)
{
return (*(int*)p2) - (*(int*)p1); //降序
//return (*(int*)p1) - (*(int*)p2); //升序
}
int main()
{
int arr[9] = {2,321,41,1,45,6,78,43,2};
qsort(arr,9,4,cmp);
}
cmp函数之所以会用const void*做参数,是因为这种类型可以接收所有指针,可以比较更多类型的元素,不会局限于某一种。传入参数之后,我们对p1,p2进行相应的类型转换,再解引用即可得到其原本的数值
这样,我们就可以得到降序排列的数组,如果修改cmp为注释掉的返回方法,则会得到升序排列的数组。快排的具体实现相对复杂一点,这里先简单介绍用法。
快排的内部就是调用了我们的cmp函数,通过函数指针的方式来回调cmp函数,从而知道如何对元素进行排序,即排升序还是降序。
函数指针并不是很常用,一般做到认识即可
四、函数指针数组
数组可以存放相同类型的元素,当然也可以用来存放函数指针,只不过这个用法也少见,了解一下即可
int (*parr1[10])();
这是一个容量为10,存储返回值为int,参数为空的函数指针的一个函数指针数组,听起来有点绕。换一种方法,去掉数组名parr1,及容量[10],还剩下int (*) (),前面的int是返回值类型,后面的()是参数列表,通过这种方法可以相对容易的判断所存储的函数指针类型。
函数指针数组也叫转移表,其用途之一是计算器,若是感兴趣可以查一下,因为转移表本身不常见不常用,在此就不写了。