在实参传递给形参时(值传递),形参是实参的一份临时拷贝,函数传参的时候参数是需要压栈的,如果传递对象过大,参数压栈的系统开销就比较大,会导致性能的下降。因此,数组、结构体传参的时候,不可能传数组所有元素,那样太浪费空间了,所以实际中仅仅只是传的首元素地址。
指针变量作为函数参数的必要性:
函数的调用可以(而且只可以)得到一个返回值,而使用指针变量作参数,可以得到多个变化了的值。如果不使用指针变量是难以做到这一点的。要善于利用指针。
实参类型 | 变量名 | 数组名 |
要求形参的类型 | 变量名 | 数组名或指针变量 |
传递的信息 | 变量的值 | 实参数组首元素的地址 |
通过函数调用能否改变实参的值 | 不能改变实参变量的值 | 能改变实参数组的值 |
在C语言中,数组名代表的就是首元素地址(当然这不是绝对的),所以数组传参时一定是传的数组名,而形参有两种写法:数组形式和指针形式。因为一维数组名和二维数组名表示的含义不同:一个是变量指针,指向数组元素;一个是数组指针,指向数组。所以当形参是指针形式时一维数组和二维数组在函数参数设计时是有所差异的。
1、一维数组传参
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
return 0;
}
上面例程中test函数的形参写法有下面三种:
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
上面第一种和第二种写法是数组形式:这种写法是非常直观容易理解的,传数组用数组接收,此处数组大小可以不写,写了也没有用。第三种是指针形式:在C语言中,数组名代表数组中首元素地址,一维数组名作函数实参时传递的是数组首元素地址,地址是用指针接收的,所以形参一定是指针变量,数组形式的写法arr[ ]看似是数组,本质也是指针变量。
test2函数的形参写法也有下面三种:
void test2(int *arr[])
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
arr2是指针数组,所以形参肯定是可以写成指针数组来接收,也就是上面第一第二种写法。同时arr2数组名表示首元素地址,也就是指针数组第一个元素(整型指针)的地址,是一个二级指针,所以肯定是要用一个二级指针变量来接收,也就是第三种写法。
一维数组传参冒泡排序示例代码(形参采用数组形式写法):
#include<stdio.h>
//冒泡排序算法
void bubble_sort(int arr[])
{
int sz = 0;
int i = 0, j = 0, tmp = 0;
sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < (sz-1); i++)
{
for (j = 0; j < (sz - 1 - i); j++)
{
//降序
if (arr[j] < arr[j + 1])
{
tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;
}
}
}
}
int main()
{
int arr[10] = { 1,5,6,7,8,3,4,2,9,10 };
int i = 0;
printf("排序前:");
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
bubble_sort(arr);
printf("排序后:");
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
冒泡排序算法:
其核心思想是两个相邻的数比较,以10个数为例:一趟完整的冒泡排序,例如10个数比较9次就会让一个数来到最后它应该在的位置,然后将剩余8个数继续冒泡排序比较7次,然后是7个数比较6次,6个数比较5次~~~冒泡9趟即可完成整个的升序或是降序。不难想到利用二层循环嵌套即可实现,外层循环每完成一次内层循环次数减一。
运行结果:
冒泡排序算法肯定是没有问题的,为什么没有实现降序排序?F10调试程序结果如图2所示:
可以看到在实现冒泡排序函数中计算数组长度时,得出的结果不是10而是1,这是因为数组名作函数实参时传递的是数组首元素地址,地址是用指针接收的,所以形参arr[ ]看似是数组,本质是指针变量。而在32位计算机中指针变量的大小是4个字节,因此计算得到的长度是1,造成了冒泡排序未能实现。
获取数组长度一般用sizeof(arr) / sizeof(arr[0]),但是在数组传参时我们不能在被调函数中使用这个方法来计算数组长度,否则就会出现上面尴尬的局面。可以在主调函数中计算出数组长度然后跟数组一起传过去,上面代码稍加改造即可成功实现冒泡排序。
上述例程形参是指针形式的写法如下:
void bubble_sort(int* arr) //形参名可以和实参名相同,形参为指针时名称也可以相同
//fun(int arr[],int n) 等价与 fun(int *arr,int n)
在用数组名作为函数实参时,既然实际上相应的形参是指针变量,为什么还允许使用形参数组的形式呢?这是因为在C语言中用下标法和指针法都可以访问一个数组(如果有一个数组a,则a[i]和*(a+i)无条件等价),用下标法表示比较直观,便于理解。对C语言比较熟练的专业人员往往喜欢用指针变量作形参。
2、二维数组传参
二维数组传参也是用指针变量作形参接受实参传递来的地址,比一维数组要复杂,有两种方法:①用指向变量的指针变量(如实参arr[0]形参用int *类型);②用指向一维数组的指针变量(如实参arr形参用int (*p)[5],形参为数组指针类型)
实参与形参如果是指针类型,应当注意它们的类型必须一致。不应把int *类型的指针(即元素的地址)传给int (*)[4]型(指向一维数组)的指针变量,反之亦然。正如不应把“班长”传给“排长”一样,应该是“门当户对”。
同一维数组一样,二维数组传参时形参也可以用数组形式接收,本质上也是指针,当然也能用指针形式接收。只不过二维数组实参有两种指针变量,所以有两种数组和指针形式形参:一是实参为二维数组名arr,这是一个数组指针,指向行为0的一维数组,所以形参要用数组指针和二维数组接收;二是实参为指向列的数组元素,形参用一维数组和指针变量接收。
2.1 形参是数组形式
#include<stdio.h>
int main()
{
int arr[3][5] = { 0 };
test(arr);
test2(arr[0]);
return 0;
}
test函数实参为二维数组名(指向一维数组的指针变量),数组形式的形参可以是:
void test(int arr[][5])
{}
void test(int arr[3][5])
{}
需要注意:二维数组名作函数的形参时,对形参数组定义时可以指定每一维的大小,也可以省略第1维(行数)的大小说明。但是不能省略第2维(列数)的大小(跟定义二维数组一样),而且要和实参数组的第2维大小相同,这是因为二维数组是由若干个一维数组组成的,在内存中数组是按行存放的,因此在定义二维数组时必须指定列数(即一行包含几个元素)。在第2维大小相同的前提下,形参数组的第1维可以与实参数组不同。这时形参数组和实参数组都是由相同类型和大小的一维数组组成的。C语言编译系统不检查第一维的大小,熟悉指针对此会有更深入的认识。
形参数组的第1维与实参数组不同例程运行结果:
为了防止越界访问或是指定要访问的二维数组某行元素,可以把二维数组的行号一起传过去,如图4例程:
test2函数实参为arr[0](指向变量的指针变量),则形参的数组形式应为int arr[ ],如图5所示:
2.2 形参是指针形式
#include<stdio.h>
int main()
{
int arr[3][5] = { 0 };
test(arr);
test2(arr[0]);
return 0;
}
test函数的实参是数组指针,则形参应该是(5不能省略且必须与指向的数组的列数相同):
void test(int (*arr)[5])
{}
test2函数的实参是变量指针,则形参应该是:
void test(int *p)
{}
下面通过一个例程加深理解二者用法与区别:
#include<stdio.h>
void average(float *p,int n) //实参为元素地址,用float*指针类型接收
{
float sum = 0, aver = 0;
float* p_end;
p_end = p + n - 1; //指向最后一个元素的指针
for (; p <= p_end; p++)
sum = sum + (*p);
aver = sum / n;
printf("平均分 = % 5.2f\n",aver);
}
void search(float(*p)[4], int n) //实参为数组地址,指向行为0的一维数组地址,用float(*)[4]类型接收
{
float sum = 0, aver = 0;
int i = 0;
for (i = 0; i < 4; i++)
sum = sum + *(*(p + n) + i); //此处*(p+2)理解为score[2]一维数组首元素地址
aver = sum / 4;
printf("行号为2的平均分 = % 5.2f\n", aver);
}
int main()
{
float score[3][4] = { {65,67,70,60},{80,87,90,81},{90,99,100,98} };
average(*score, 12); //求12个分数的平均值,实参为score[0],指向score[0][0],是变量指针
search(score, 2); //求序号为2的学生的成绩,实参为score,指向一维数组score[0]的地址,类型为数组指针
return 0;
}
函数average中形参p被声明为float *类型(指向float型变量)的指针变量,实参用*score,即score[0],也就是&score[0][0],即score[0][0]的地址,p指向score[0][0],p每加1就指向score数组的下一个元素。
函数search的形参p的类型是float (*)[4],指向包含4个元素的一维数组的指针变量。所以实参传的是score(代表数组0行起始地址,数组指针,不同于上面是变量指针),p指向score[0],不再是指向score[0][0]了,而是指向行为0的数组,p+n是score[n]的起始地址,指向行为n的数组,*(p + n) 等价于p[n](二维数组的一维数组名),*(p + n) + i是score[n][i]的地址,*(*(p + n) + i)是score[n][i]的值。
二维数组名也是首元素地址,只不过二维数组的首元素是行为0的一维数组的地址,数组地址传参就要用数组指针类型来接收了,即int (*)[4]。关于数组指针的详细说明可以参考博客(C语言指针进阶:数组指针和函数指针_江东平的博客-CSDN博客)。
二维数组sizeof笔试题分析:
上面sizeof(a[3])也是16,sizeof操作不会造成越界访问,只是求数据类型的大小,就跟sizeof(int)一样,而 a[3]和a[0]是一种类型。
3、数组名
无论是一维数组还是二维数组,毫无疑问数组名就是首元素的地址,只不过是首元素的含义不同。但是有两个例外:
1)sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节,同样适用于二维数组。需注意sizeof(arr+0)就不再是整个数组大小了,而是首元素地址的大小。
2)&数组名,这里的数组名表示整个数组,取出的是整个数组的地址,也适用于二维数组。下图7例程及运行结果可以更好的理解这两个例外。
图8例程可以直观的说明二维数组arr[3][4]中arr[0]、arr[1]、arr[2]是一维数组名:
所以需要严格区分数组首元素地址和整个数组地址。除了上述两种情况外,其他所有情景中数组名都是数组首地址。
数组也是有类型的,跟其他变量一样,去掉名字就是类型,例int arr[10]的类型就是int[10],所以sizeof(arr) = sizeof(int [10])。
数组名和指针运算笔试题:
4、传值调用和传址调用
形参的类型决定了形参和实参交互的方式,一般来说参数的传递是值传递(单向),也就是说实参传给形参,形参发生改变时实参并不会改变,但是数组在传递的时候是地址传递,只要形参发生了变化,实参也会发生变化(双向)。
形式参数只有在函数调用的过程中才实例化(分配内存单元),所以叫形式参数。当实参传递给形参时(传值),形参是实参的一份临时拷贝(形参在函数调用完成之后就自动销毁了,所以是临时),通过程序调试可以发现实参和形参使用的不是同一空间。
传值调用:函数的形参和实参分别占用不同内存块,对形参的修改不会影响实参。
传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外部的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。