3.2 函数参数的传递

参数是调用函数与被调用函数之间交换数据的通道。函数定义首部的参数称为形式参数(简称形参),调用函数时使用的参数称为实际参数(实参)。

实际参数必须与形式参数在类型、个数、位置上相对应。函数被调用前,形参没有存储空间。函数被调用时,系统建立与实参对应的形参存储空间,函数通过形参与实参进行通信、完成操作。函数执行完毕,系统收回形参的临时存储空间。这个过程称为参数传递或参数的虚实结合。

C++语言有三种参数传递机制:值传递(值调用)、指针传递(地址调用)和引用传递(引用调用)。实际参数和形式参数按照不同传递机制进行通信。

3.2.1 传值参数

1.值传递机制

在值传递机制中,作为实际参数的表达式的值被复制到由对应的形式参名所标识的对象中,成为形参的初始值。完成参数值传递之后,函数体中的语句对形参的访问、修改都是在这个标识对象上操作的,与实际参数对象无关。

【例3-5】传值参数的测试。

            #include<iostream>
            using namespace std;
            void count(int x,int y)        //定义函数,x、y为传值参数,接收实参的值
            {  x=x*2;                      //在形参x上操作
              y=y*y;                       //在形参y上操作
              cout << "x = " << x << '\t';
              cout << "y = " << y << endl;
            }
            int main()
            {  int a=3,  b=4;
              count(a,b);                  //调用函数,a、b的值分别传递给x、y
              cout << "a = " << a << '\t';
              cout << "b = " << b << endl;
            }

程序运行结果:

            x=6    y=16
            a=3    b=4

main函数调用count函数时,系统建立形式参数对象x、y,把实参a、b值赋给x、y作为初始值。count函数对x、y的操作与实参a、b无关。返回main函数后,形参x、y被撤销,实参a、b的值没有变。如图3.1所示。

如果函数具有返回值,则在函数执行return语句时,系统将创建一个匿名对象临时存放函数的返回结果。这个匿名对象在返回调用之后撤销。

图3.1 函数的传值参数

【例3-6】求圆柱体体积。

            #include<iostream>
            using namespace std;
            double volume(double radius, double height);
            int main()
            {  double vol,r,h;
              cout<<"Input radius and height :\n";
              cin>>r>>h;
              vol=volume(r,h);                                //把r和h的值传递给形式参数
              cout<<"Volume = "<< vol << endl;
            }
            double volume(double radius,double height)        //在参数表中定义两个传值参数
            {  return 3.14*radius*radius*height;}             //返回表达式的值

volume函数用形参radius和height计算并返回圆柱体的体积。返回main函数时,匿名对象的值赋给变量vol。参数传递如图3.2所示。

图3.2 具有返回值的函数

因为在传值方式中,实际参数对形式参数进行赋值操作,所以实际参数可以是各种能够对形式参数标识对象赋值的表达式。如果实参值的类型和形参对象类型不相同,将按形参的类型进行强制类型转换,然后赋给形参。例如,有函数定义:

            int max(int a, int b)
            { return (a > b ? a : b); }

如果有调用:

            m = max(5.2/2, 1.5);

则m的值等于2。由于形参a、b都是整型的,接收实参值为int(5.2/2)和int(1.5),所以通过return返回的值是2。

【例3-7】已知,其中,max(x,y,z)为求x、y和z三个数中最大值函数。编写程序,输入a、b和c的值,求s的值。

            #include<iostream>
            using namespace std;
            double max(double, double, double);
            int main()
            {  double a,b,c,s;
              cout << "a, b, c = ";
              cin >> a >> b >> c;
              //三次调用max函数,表达式作为实际参数
              s = max(a,b,c)/(max(a+b,b,c)*max(a,b,b+c));
              cout << "s = " << s << endl;
            }
            double max(double x, double y, double z)
            {  double m;
              if(x>=y)  m=x;
                else  m=y;
              if(z>=m)  m=z;
              return m;
            }

程序首先从main函数开始执行。当执行到赋值语句:

            s = max(a,b,c)/(max(a+b,b,c)*max(a,b,b+c));

时,三次调用max函数。每次函数调用,都是先计算实际参数的值,把该值传送给相应的形式参数,然后执行函数体。当函数体执行到语句:

            return m;

时,把三个实际参数中的最大值m通过匿名对象返回函数调用处。

2.实际参数求值的副作用

C++没有规定在函数调用时实际参数的求值顺序。实际参数求值顺序的不同规定,对一般参数没有什么影响,但若实际参数表达式之间有求值关联,则同一个程序在不同编译器可能产生不同的运行结果。

例如,有一个函数定义为:

            int add(int a,int b)
            { return a+b; }

执行以下语句:

            x = 4;
            y = 6;
            cout << add(++x, x+y) << endl;

对于自左向右求实际参数的值的编译系统,首先计算++x,表达式的值为5,变量x的值也是5;然后计算表达式x+y,表达式的值为11;分别把5和11传递给形参a、b,得到的返回值是16。

但对于自右向左求实际参数的值的编译系统,首先计算x+y,表达式的值为10;然后计算表达式++x,表达式的值为5;分别把5和10传递给形参a、b,得到的返回值是15。

之所以产生这种语义歧义性,是因为实参中有“++x”这种赋值表达式,而另一个实参表达式又使用了x的值。

这种存在赋值依赖关系的传值参数称为有副作用的参数。为了避免这种情况,可以在调用函数之前先执行修改变量的表达式,以消除实参表达式求值的依赖关系,改写程序如下:

            x = 4;
            y = 6;
            ++x;
            cout << add(x, x+y) << endl;

3.默认参数

函数传值调用时,实际参数作为右值表达式向形式参数提供初始值。C++允许指定参数的默认值,当函数调用中省略默认参数时,默认值自动传递给被调用函数。

调用带参数默认值的函数时,如果显式指定实际参数值,则不使用函数参数的默认值。

【例3-8】定义并调用函数,求两坐标点之间的距离。如果省略一个坐标点,则表示求另一个坐标点到原点的距离;如果省略一个坐标点的一个参数,则表示纵坐标y的值等于0。

            #include<iostream>
            using namespace std;
            #include<cmath>
            //函数原型指定默认参数值
            double dist(double, double, double =0, double =0);
            int main()
            {  double x1,y1,x2,y2;
              cout << "Enter point (x1, y1) : ";
              cin >> x1 >> y1;
              cout << "Enter point (x2, y2) : ";
              cin >> x2 >> y2;
              cout << "The distance of (" << x1 << ", " <<y1<< ") to (" << x2 << ", " << y2 << ") : "
                    <<dist(x1,y1,x2,y2)<<endl;      //使用指定参数值
              cout << "The distance of (" << x1 << ", " << y1<< ") to (" << 0 << ", " << 0 << ") : "
                    <<dist(x1,y1)<<endl;            //使用默认参数值,x2、y2为0
              cout << "The distance of (" << x1 << ", " << y1<< ") to (" << x2 << ", " << 0 << ") : "
                    <<dist(x1,y1,x2)<<endl;         //使用默认参数值,y2为0
            }
            double dist(double x1, double y1, double x2, double y2)
            {  return sqrt(pow(x1-x2,2)+pow(y1-y2,2));  }

程序运行结果:

            Enter point(x1,y1):3  4
            Enter point(x2,y2):5  7
            The distance of (3,4) to (5, 7) : 3.60555
            The distance of (3,4) to (0, 0) : 5
            The distance of (3,4) to (5, 0) : 4.47214

在这个程序中,函数dist定义了4个形式参数,其中设置了两个默认值。在main函数中,三次调用函数 dist,每次调用的实际参数的个数都不一样。第一次调用时,全部实际参数不采用默认值。第二次调用时,实际参数采用两个默认值。第三次调用时,实际参数采用一个默认值。dist函数至少需要两个实际参数。

有关默认参数的说明如下。

① C++规定,函数的形式参数说明中设置一个或多个实际参数的默认值,默认参数必须是函数参数表中最右边(尾部)的参数。调用具有多个默认参数的函数时,如果省略的参数不是参数表中最右边的参数,则该参数右边的所有参数也应该省略。

② 默认参数应该在函数名第一次出现时指定,通常在函数原型中。若已在函数原型中指定默认参数,则函数定义时不能重复给出。

③ 默认值可以是常量、全局变量或函数调用,但不能是局部量。

④ 默认参数可以用于内联函数(参见3.5节)。

3.2.2 指针参数

函数定义中的形式参数被说明为指针类型时,称为指针参数。形参指针对应的实际参数是地址表达式。调用函数时,实际参数把对象的地址值赋给形式参数名标识的指针变量,被调用函数可以在函数体内通过形参指针来间接访问实参地址所指的对象。这种参数传递方式称为指针传递或地址调用。

【例3-9】通过函数及其指针参数来实现两个整型变量的值交换。

            #include<iostream>
            using namespace std;
            void swap(int *, int *);
            int main()
            {  int a=3,b=8;
              cout << "before swapping…\n";
              cout << "a = " << a << ", b = " << b << endl;
              swap(&a,&b);               //实际参数是整型变量的地址
              cout << "after swapping…\n";
              cout << "a = " << a << ", b = " << b << endl;
            }
            void swap(int*x,int*y)           //形式参数是整型指针
            {  int temp=*x;
              *x = *y;
              *y = temp;
            }

程序运行结果:

            before swapping…
            a = 3, b = 8
            after swapping…
            a = 8, b = 3

main函数执行函数调用语句:

            swap(&a, &b);

时,把变量a和b的地址分别传送给形式参数指针变量x和y,令x指向a,y指向b。执行函数体时,*x、*y通过间址访问对变量a、b进行操作,交换a、b的值。

图3.3 swap函数的指针传递

从上述讨论可知,形参指针可以通过获取对象地址来访问实参地址所指对象。指针参数的本质也是传值参数。对于一般传值参数,实际参数向形式参数传送的是数据表达式。而指针参数对应的实际参数是地址表达式,如果这个表达式是一个实际对象的地址值,则形式参数接收这个地址值后,可以间接访问这个地址所指的对象。

为了避免被调用函数对实参所指对象的修改,可以用关键字const约束形参指针的访问特性。

【例3-10】使用const限定指针,保护实参对象。

            #include<iostream>
            using namespace std;
            int func(const int * const p)
            {  int a=10;
              a += *p;
              //*p=a;       //错误,不能修改const对象
              //p=&a;       //错误
              return a;
            }
            int main()
            {  int x=10;
              cout << func(&x) << endl;
            }

程序运行结果:

            20

实参表达式是变量x的地址,形参p被定义为指向常量的常指针。调用函数时,用实参地址值初始化后,函数体对p和*p的访问都被约束为只读,从而保护了实参x不能通过p修改。

可以不约束p的访问:

            int func(const int * p);

使在函数体内对p的修改变为合法:

            p=&a;       //合法

但这一来,形参指针p就与实参所指对象失去关联了。

当将常对象的地址传递给形参指针时,形参必须用const约束。

【例3-11】传递常对象地址。

            #include<iostream>
            using namespace std;
            int func (const int * p)
            {  int a=10;
              a += *p;
              return a;
            }
            int main()
            {  const int M=10;
              cout << func(&M) << endl;
            }

main函数中,M是一个标识常量,其地址作为实参传递给形参指针p。p的间址访问被约束,函数不能通过*p修改M。在这个程序中,func确实没有修改*p,但是,不能因此而不对p加以约束。例如,func的函数原型改为:

            int func(int*p);        //参数p没有约束

编译器仅从函数原型分析,认为该函数有可能修改常实参M,从而报告错误。

3.2.3 引用参数

如果C++函数的形式参数被定义为引用类型,则称为引用参数。引用参数对应的实际参数应该是对象名。函数被调用时,形式参数不需要开辟新的存储空间,形式参数名作为引用(别名)绑定于实际参数标识的对象上。执行函数体时,对形参的操作就是对实参对象操作。直到函数执行结束,撤销引用绑定。

【例3-12】通过函数及其引用参数来实现两个整型变量的值交换。

            #include<iostream>
            using namespace std;
            void swap(int&, int&);
            int main()
            {  int a=3,b=8;
              cout << "before swapping…\n";
              cout << "a = " << a << ", b = " << b << endl;
              swap(a,b);              //实际参数是整型变量名
              cout << "after swapping…\n";
              cout << "a = " << a << ", b = " << b << endl;
            }
            void swap(int&x,int&y)      //形式参数是整型引用
            {  int temp=x;
              x = y;
              y = temp;
            }

调用函数swap后,形参x、y分别是实参a、b的引用,函数体内对x、y的操作实际上是对a、b的操作。

请注意,若把例3-9的swap函数改为:

            void swap(int x,int y)        //传值参数
            {  int temp=x;
              x = y;
              y = temp;
            }

则main函数对其调用方式不变,程序运行结果不会交换a、b的值。因为形参x、y只在调用时接收a、b的值作为初值,函数体内的操作与a、b无关。调用语句:

            swap(a, b);

对于传值参数,实际参数 a、b 是右值表达式。而当该调用对应引用参数时,a、b 作为标识名将与形式参数名绑定。

引用参数和指针参数都不需要像传值参数那样产生实参对象数据的副本,并且,引用参数不像指针参数那样通过间址访问实参对象,特别适用于大对象参数的高效操作。

和指针参数的情形一样,为了避免被调用函数对实参对象产生不必要的修改,可以使用const限定引用。

【例3-13】使用const引用参数。

            #include<iostream>
            #include<iomanip>
            using namespace std;
            void display(const int & rk) //定义const引用参数
            {  cout<<rk<<":\n"<<"dec:"<<rk<<endl<<"oct:"<<oct<<rk<<endl
                    << "hex : " << hex << rk << endl;
            }
            int main()
            {  int m=2618;
              display(m);              //实际参数是变量
              display(4589);           //实际参数是常数
            }

程序运行结果:

            2618 :
            dec : 2618
            oct : 5072
            hex : a3a
            4589 :
            dec : 4589
            oct : 10755
            hex : 11ed

注意,在本例main函数中第2次调用display函数时,用常数4589作为实际参数。C++规定,函数的const引用参数允许对应的实际参数为常数或者表达式。调用函数进行参数传递时将产生一个匿名对象保存实参的值。形参标识名作为这个匿名对象的引用,对匿名对象进行操作。匿名对象在被调用函数运行结束后撤销。这种const引用参数的使用效果与传值参数情况类似。

【例3-14】const引用参数的匿名对象测试。

            #include<iostream>
            using namespace std;
            void anonym (const int & ref)
            {  cout<<"The address of ref is:"<<&ref<<endl;
              return;
            }
            int main()
            {  int val=10;
              cout << "The address of val is : " << &val << endl;
              anonym(val);
              anonym(val + 5);
            }

程序运行结果:

            The address of val is : 0012FF4C
            The address of ref is : 0012FF4C
            The address of ref is : 0012FE80

main函数第1次调用anonym函数时,实参是变量名。形参ref与实参val绑定。在程序输出的第1行和第2行,实参和形参的地址值相同,说明引用参数与实参对象都是同一个存储单元,引用参数以别名方式在实参对象上进行操作。无论形参是否被约束,情形都一样。

第2次调用anonym时,实参是表达式。C++为const引用建立匿名对象用于存放val+5的值。第 3 行输出是匿名对象的地址。只有const引用对应的实参可以是常量或表达式,非约束的引用参数对应的实参必须是对象名。

3.2.4 函数的返回类型

C++函数可以通过指针参数或引用参数修改实际参数,从而获取函数的运行结果。return 语句也可以返回表达式的执行结果。return语句的一般格式为:

            return(表达式);

其中,圆括号可以省略。“表达式”的类型必须与函数原型定义的返回类型相对应,可以为数值型和字符型,也可以为指针和引用。

当函数定义为void类型时,return语句不带返回“表达式”,或者不使用return语句。

一个函数体内可以有多个 return 语句,但只会执行其中一个。return 语句的作用是,把“表达式”的值通过匿名对象返回调用点,并中断函数执行。

1.返回基本类型

如果函数定义的返回类型为基本数值类型,则执行return语句时,首先计算表达式的值,然后把该值赋给C++定义的匿名对象。匿名对象的类型是函数定义的返回类型。通过这个匿名对象,把数值带回函数的调用点,继续执行后续代码。

例如,有函数原型: int function();

函数体若有: return x;

则执行该语句时,把x的值赋给int类型的匿名对象,返回到函数调用点。

又如,若有: return a+b+c;

则首先对表达式求值,然后对int类型的匿名对象赋值,返回到函数调用点。

对匿名对象赋值时,如果表达式的值的类型与函数定义的返回类型不相同,将强制转换成函数的返回类型。

2.返回指针类型

函数被调用之后可以返回一个对象的指针值(地址表达式)。返回指针类型值的函数称为指针函数。指针函数的函数原型一般为:

            类型 * 函数名(形式参数表);

函数体中用return语句返回对象的指针。

【例3-15】定义一个函数,返回较大值变量的指针。

            #include<iostream>
            using namespace std;
            int*maxPoint(int*x,int*y)  //函数返回整型指针
            {  if(*x>*y)  return x;
              return y;
            }
            int main()
            {  int a,b;
              cout << "Input a, b : ";
              cin >> a >> b;
              cout << * maxPoint(&a, &b) <<endl;
            }

调用函数maxPoint后,两个形参指针分别指向main函数的变量a和b,即x的值是实参a的地址,y的值是实参b的地址。在maxPoint函数中,*x、*y间址访问a、b,函数通过匿名对象返回它们之中较大者的指针。匿名对象的类型就是函数的返回类型 int*,接收 return 语句的地址表达式的值。

main函数在输出语句中调用maxPoint函数。函数返回指针(地址值),然后用指针运算符访问所指对象,输出a、b之中的大值。

为了约束对实参的访问,函数maxPoint还可以写为:

            const int * maxPoint(const int * x, const int * y)
            {  if(*x>*y)  return x;
              return y;
            }

maxPoint返回对象的指针,不需要复制产生实际对象的值。如果函数需要对大对象进行操作,使用指针函数显然可以节省匿名对象生成的时空消耗。

注意,指针函数不能返回局部量的指针。例如,以下函数定义是错误的:

            int * f()
           {  int temp;
              //…
              return &temp;
           }

temp是在函数f运行时建立的临时对象,f运行结束,系统释放temp,因此,函数返回局部变量的地址是不合理的。

3.返回引用类型

C++函数返回对象引用时,不产生返回实际对象的副本,返回时的匿名对象是实际返回对象的引用。返回引用比返回指针更直接,可读性更好。

【例3-16】定义一个函数,返回较大值变量的引用。

            #include<iostream>
            using namespace std;
            int&maxRef(int&x,int&y)      //函数返回整型引用
            {  if(x>y)  return x;
              return y;
            }
            int main()
            {  int a,b;
              cout << "Input a, b : ";
              cin >> a >> b;
              cout << maxRef(a, b) <<endl;
            }

程序运行结果:

            Input a,b:  3  9
            9

从函数参数传递的规律可知,一旦调用maxRef,形参x、y分别是实参a、b的引用。函数体的if语句把x、y之中(a、b之中)大值的对象通过return返回。因为maxRef函数返回类型是整型引用,所以,C++无须建立返回用的匿名对象,函数调用的返回就是对象的引用。在上述运行示例中,函数返回变量名y,而y是b的引用,所以,在main函数的调用返回是b的引用。变量名在输出流中作为右值表达式,输出b的值为9。

函数返回引用需要依托于一个对象。显然,被依托的返回对象不能是函数体内说明的局部变量。其原因与返回指针的函数一样,被调用函数内定义的局部量是临时对象,函数返回时将被释放。例如,以下函数定义是错误的:

            int & r()
           {  int temp;
              //…
              return temp;
           }

返回对象可以是非局部对象或静态对象。

函数返回引用,使得函数调用本身是对象的引用,就像返回对象的标识别名。所以,返回引用的函数调用可以作为左值。

【例3-17】输入一系列正整数和负整数,以0结束,统计其中正整数和负整数的个数。

            #include<iostream>
            using namespace std;
            int &count(int);
            int a, b;
            int main()
            {  int x;
              cout << "Input numbers, the 0 is end : \n";
              cin >> x;
              while (x)
              { count(x)++;  //根据返回不同变量引用进行++运算
                  cin >> x;
              }
              cout << "the number of right: " << a << endl;
              cout << "the number of negative: " << b << endl;
             }
            int & count(int n)
            {  if(n>0)  return a;
              return b;
            }

运行程序,输入数据,显示结果为:

            Input numbers, the 0 is end :
            -2 5 8 -9 20 0
            the number of right:3
            the number of negative:2

函数count返回全局变量a或b的引用。当参数n的值大于0,函数返回变量a的引用, main函数中的调用:

            count(x)++

相当于: a ++

而当参数n的值小于0,函数返回变量b的引用, main函数中的调用:

            count(x)++

相当于: b ++

当然,这个程序的可读性并不好,这里仅作为一个简单的示例。事实上,返回对象引用的函数调用可以作为左值,这一点很有意义。我们将在第7章中看到,正是因为C++允许函数返回对象引用,使得在调用运算符重载函数时,可以实现运算符的连续操作。