#P2053. 在函数内修改实参
在函数内修改实参
1. 实参和形参
形参(形式参数) 在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数) 函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
我们通过具体的例子加以解释
#include<bits/stdc++.h>
using namespace std;
int gcd(int a,int b) //这里的 a 和 b 是形参
{
int c;
while(a%b!=0)
{
c = a % b;
a = b;
b = c;
}
printf("gcd 函数返回前的最后一刻,a=%d,b=%d\n",a,b);
return b;
}
int main(){
int a,b;
cin>>a>>b;
printf("最大公约数是:%d\n",gcd(a,b)); // a 和 b 是实参
printf("a=%d,b=%d\n",a,b);
}
假设我们运行上面的程序,输入 60 24,运行结果会是:
从运行结果我们可以看出,在 gcd 函数里,a 和 b 的值是被修改成了 24 和 12,但是,在 main 函数里,a 和 b 的值并没有发生变化,还是 60 和 24 。这是比较好理解的,上一课中,我们已经讲述的变量的作用域,我们现在看到的情况符合我们所掌握的知识。
但是,如果我们进一步深入,问个为什么,或者说,这个性质是如何实现的,那就要再增加一些知识了。
在 main 函数里调用 gcd(a,b),这里的 a 和 b 是实参。其实我们是拷贝了另外一对 a 和 b 出来传递给 gcd 函数。那个拷贝出来的 a 其实应该标记为 a' 和 b' ,多了这一撇,其实是告诉你这两个东西之间虽然有一些关系,但是并不是同一个东西。a' 和 b' 就是 gcd 函数里的传入参数 a 和 b,就是形参。所以,在 gcd 内部怎么修改 a 和 b,其实改的就是 a' 和 b'。a、b 和 a' 和 b' 之间并没有联动机制,所以,你改了 a' 并不影响 a。这就是原理。
上面这个特性挺好的,使得 c++ 的语法非常严谨,用 c++ 写的程序非常的稳健(当然,这也依赖于人的,如果写程序的人不严谨,那写出来的东西还是不严谨,C++ 只是在技术上留了足够的空间给程序员,你想严谨是可以很严谨的)。但是,有些时候我们也想换换口味,我们希望在程序里面修改形参,怎么办?
2. 引用传参
引用是 c++ 的一种语法,它要用到 & 符号,这个符号和取地址的符号是一样的,但是,因为它是出现在定义语句里,所以,c++ 编译器不会搞错(只要你不要搞错就行)。
引用的意思就是别名,你家有条小狗,中文名叫 聪聪,然后你妹妹喜欢叫它 Boby,聪聪是你家的那条小狗,Boby 也是你家的小狗,不同的名字,但是对应的是同一条小狗,这就是别名,也叫引用。
int a=6; // a 是一个变量
int &b = a; // 这里是定义了另外一个变量 b ,但是它其实是 a 的引用。也就是说, a 和 b 是一个东西。
b = 12; // 这里对 b 赋值,实际上也是修改了 a 的值,因为 a 和 b 是同一个东西
cout<<a; // 你可以看到输出的是 12 ,不是 6
所以,大家可以看到,这个 & 的位置是在定义的时候出现(也就是说,在等号的左边)。如果在等号的右边出现,& 就可能是取地址运算符(假如它是单目运算符),也可能是按位与运算符(假如它是双目运算符)。
接下来,我们介绍通过引用来传递参数。因为引用的意思就是同一个东西,所以,如果以引用方式传递参数,其实在函数里的参数(形参)就是调用函数的实际参数(实参),因为它们就是同一个东西。
#include<bits/stdc++.h>
using namespace std;
void work_ref(int &a,int &b){
int t = a;
a = b;
b = t;
}
void work_normal(int a,int b){
int t = a;
a = b;
b = t;
}
int main()
{
int x,y;
x = 100;
y = 27;
printf("一开始,x=%d,y=%d\n",x,y);
work_normal(x,y);
printf("调用完 work_normal 之后,x=%d,y=%d\n",x,y);
work_ref(x,y);
printf("调用完 work_ref 之后,x=%d,y=%d\n",x,y);
return 0;
}
3. 地址传参
如果,我们把参数以地址的形式传递,就可以通过间接引用的方法修改参数的值。
#include<bits/stdc++.h>
using namespace std;
int gcd(int *pa,int *pb) //
{
int c;
while( *pa % *pb!=0)
{
c = *pa % *pb;
*pa = *pb;
*pb = c;
}
printf("gcd 函数返回前的最后一刻,a=%d,b=%d\n",*pa,*pb);
return *pb;
}
int main(){
int a,b;
cin>>a>>b;
printf("最大公约数是:%d\n",gcd(&a,&b)); // 我传送的是 a 和 b 的地址
printf("a=%d,b=%d\n",a,b);
}
运行程序,我们继续输入 60 24,可以看到 main 函数里的 a 和 b 被 gcd 函数修改了。也就是说,a 和 b 的作用域算是 main 范围内的,但是却在超出作用域的范围中(gcd 函数)修改了。是不是很神奇?
如果上面这个程序能看明白,理解到位,应该就足够应对笔试了。下面我们来解释这个程序是怎么运行的。
首先,我们要看到 gcd 的定义发生了变化,它接收的参数不在是 int 类型,而是 int * 类型,也就是说,这两个参数是 int 指针,是内存地址。所以,我们在 main 函数里调用 gcd 的时候,传的不是 a 和 b 的值,而是传它们的地址。a 的地址是 &a,b 的地址是 &b,& 是取地址运算符,这是上一个课件的内容。
main 函数调用 gcd(&a,&b) 的时候,同样会采用实参转形参的方式,弄一个 int *pa 出来存放 &a ,弄一个 int *pb 出来存放 &b,然后这个 pa 和 pb 就会称为 gcd 函数的形参,对应的就是 gcd 里面的 pa 和 pb。定义 gcd 的时候,参数声明是有个星号的,表示的就是说这个参数是地址,是指针,而且是 int 的指针。这些信息都一定要写完整,如果你写成 double *pa 就错了。
gcd 里面的内容其实和之前没有变化,意思还是那个意思。只不过,我们每一次修改 a,b,是通过指针的间接引用去完成的。我们根据指针的内存地址,修改了参数 a 和 b,没毛病。
例如 c = *pa % *pb
,我们是通过间接引用去让 a 和 b 做模运算; *pa = *pb; *pb=c;
这两句也是通过间接引用去赋值。
反正,因为是根据地址去操作数据,这个地址的数据只有 1 份,修改了就是修改了,作用域拦不住。
4. 把数组名作为函数的参数
我们可以把数组名字作为函数的参数,这样做的好处是在函数内可以访问函数外面定义好的数组。我们设想一下,我们的程序如果比较复杂,就会在多个地方修改数组的内容,我们根据不同的需要来组织我们代码,最终可能是切割成很多个功能模块,每一个模块是一个函数,函数内需要修改数组的值就很正常。如果这个逻辑是固定的,但是修改的数组是不确定的(有时候要修改数组 A,有时候要修改数组 B),这时候就可以把数组名作为参数传递给函数。站在函数的角度看,我修改什么数组都可以,你让我修改那个数组我就修改什么数组,取决于外层怎么调用我。
先举一个简单的例子:
int a[10]={1,2,3,6,10,4,3,21,11,7};
int b[20]={1,2,3,4,5,6,7,8,9,10,33,34,35,36,37,38,39,40,41,42};
int work(int x[],int n){
int ret = x[0];
for(int i=1;i<n;i++)
ret = max(ret,x[i]);
return ret;
}
int main()
{
cout<<work(a,10)<<endl; // 计算并输出 a 数组最大值
cout<<work(b,20)<<endl; // 计算并输出 b 数组的最大值
cout<<work(&b[5],12)<<endl; // 计算并输出从 b[5] 到 b[16] 这 12 个数组元素的最大值
}
上面例子的 work 函数使用了数组名作为函数的第一个参数,第二个参数 n 可以理解为数组的大小。work 函数就是要计算数组的最大值,比较的范围取决于 n 。
然后 main 函数里 3 次调用了 work 函数。前两次调用都比较好理解,因为 n 和实参的数组大小一致。第 3 次调用,第一个参数的值是 b[5] 的内存地址,而第二个参数是 12 ,从 work 函数的角度看,就是一个 x 数组,x[0] 的内存地址相当于实参里的 b[5] 的内存地址,x[1] 的内存地址相当于实参里的 b[6] 的内存地址地址,......,x[11] 的 内存地址相当于实参的 b[16] d 内存地址。
二维数组作为函数的参数
二维数组作为函数的参数事,有一些语法注意事项:第二维(列)的大小一定要交待清楚。看下面这个错误例子
int work(int x[][],int n)
{
int ret = -100000000;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
ret = max(ret,x[i][j]);
return ret;
}
上面这个写法是错误的,因为形参写成 int x[][]
是不行的。第二维一定要写清楚(如果是 3 维数组,第二维第三维度都要写清楚,以此类推,只有第一维的大小是可写可不写)。
4. 更好的一个例子
题目描述
小华和小明在星期一全校大扫除时,被劳动委员分配到美术室去帮助美术老师整理物品。小明在帮助美术老师整理物品时,看到老师有一堆卡片,这些卡片都是正方形的,而且每张卡片的正反面都划有大小相等的格子,由于卡片的大小不同,每张卡片上划的格子数量也不同,最小的卡片上划有 2*2 的格,也有卡片划有 3*3 的格, 4*4 的格,... , 最大的卡片上竟划有 10*10 的格。小明发现这些卡片有些格子上涂满了颜色,而且卡片正反两面相对的格子涂的颜色是相同的。小明问小华知不知道这些卡片是用来做什么的,小华猜想这些卡片可能是用来拼字或画的吧。小华的猜想得到了老师的证实。老师让他俩把这堆卡片分类,相同的卡片放在一起。小华和小明立即行动起来。小华拿起一张卡片(如图 1 ),让小明帮手找相同的卡片。小明拿起一张卡片(如图 2 )递了过去,小华看了一眼:“别开玩笑了,赶快找和我这张相同的卡片”。
“这就是和你拿的卡片是一样的啊。”小明说,“你看,我把它反转过来,再转 90 度,不就和你的一摸一样了吗。”看来卡片是否相同,还真难一眼看出。现请你帮忙编一个程序,专门用来判断两张卡片是否相同的。
输入格式
第一行为第一张卡片的每边格数 n ,接下来是由 0 和 1 组成 n*n 的第一张卡片样式数阵, 0 表示没有涂颜色的格, 1 表示涂有颜色的格,横向数据之间有一空格隔开;
再下一行为第二张卡片的每边格数 m ,和 0 和 1 组第二张卡片样式数阵,表示方法同上。
输出格式
yes 或 no ,表示相同或不相同。
样例
4
0 0 1 0
0 0 0 0
0 1 0 0
0 0 1 1
4
0 0 0 0
0 0 1 0
1 0 0 1
0 0 0 1
yes
3
0 0 0
1 0 0
1 0 0
2
1 0
1 0
no
分析
我们学习二维数组的时候,已经说过,一个二维数组通过翻转和旋转,只有 8 个形态。我们就是先后得到这 8 个形态,然后和目标数组做比较就可以了。
本题的难点,就是如何把代码写得简单一点。有很多方法可以达到类似的功能的,以前我们教过用结构体来传参,让函数有返回值,返回的是整个结构体,数组是结构体的成员,所以返回结构体就把结构体下属成员也返回了。然后我们就可以不断的迭代。
今天教大家另外一个方法,没有结构体,但我们要做到在函数内部完成对原数组的修改。
#include<bits/stdc++.h>
using namespace std;
int n,n2,x[11][11],y[11][11],z[11][11];
void change(int a[11][11],int type) // type = 0 旋转,type = 1 翻转
{
int tmp[11][11];
memcpy(tmp,a,sizeof(x)); //拷贝数组 a 到 tmp
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(type==0)
a[i][j] = tmp[n+1-j][i];
else
a[i][j] = tmp[i][n+1-j];
}
bool check(int a[11][11],int b[11][11])
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(a[i][j]!=b[i][j])
return false;
return true;
}
void get_data(int a[11][11],int *pn)
{
scanf("%d",pn);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
}
int main()
{
get_data(x,&n);
get_data(z,&n2);
if(n!=n2)
{
printf("no");
return 0;
}
memcpy(y,x,sizeof(x)); // 把数组 x 拷贝 到 y 数组
change(y,1); // 翻转 y 数组
for(int i=0;i<4;i++)
{
change(x,0); //旋转 x
change(y,0); //旋转 y
if(check(x,z)||check(y,z)) //比较
{
printf("yes");
return 0;
}
}
printf("no");
return 0;
}
程序解释
-
一开始,要输入两个数组的内容。这两个事情是基本相同的,可以用一个函数来实现,就是 get_data 函数了。这个函数有 2 个参数,一个参数是数组的地址,一个参数是对应的 n 的地址。因为传的是地址,所以在数组里面可以修改数组的值,也可以修改 n 和 n2 的值。
-
change 函数是对数组进行旋转和水平翻转的函数,第二个参数 type 决定了究竟是旋转还是翻转。
-
对原数组(x)水平翻转,得到 y
-
对 x 和 y 旋转,各自得到 4 个形态,一共 8 个形态,如果 8 个形态中有任何一个和 z 数组一样,那就是相同的卡片。否则,就是不相同。
-
get_data 和 change 的参数都是指针(内存地址),我们说过,定义了一个数组之后,只写数组,不写下标,那就是数组的起始地址。所以,main 函数里面写
change(x,0);
就是取了 x[0][0] 的地址传给 change 函数 -
定义 change 函数的时候,形参可以写 *a,也可以写 a[11][11],语法上都正确。但是写 a[11][11] 会简单很多。因为 sizeof 函数要根据这个数组的定义来算这个数组一共有多少个元素,然后要拷贝多少个字节,形参写清楚了这个数组的规模,sizeof 就能自己计算,否则就要自己算(如果是自己手动填,应该用 11*11*4 来替代 sizeof 的计算,每个 int 4 个字节,所以最后要乘 4)。另外,用了二维数组之后,就不用自己计算指针地址偏移了,可以用常规的二维数组方式来访问数据。