函数按值传递时的传参机制和返回机制
前一段时间有一个误区,误以为函数的实参是任意能够作为形参初始值的表达式。
为什么会有这种错觉呢?我个人产生这种错觉是因为看到C++ Primer第三章学到string类的时候注意到这样一个事实:如果一个函数的形参类型为string,则可以用字符串字面值来作为实参。同时,初始化一个string对象时可以使用字符串字面值作为初始值(直接初始化和拷贝初始化都可以)。再后来学到第六章函数的时候,又了解到“实参用于初始化形参”这一概念。这几个概念一糅合,我就错误地得出了这样一个结论:实参可以是任何能够作为形参初始值的表达式。
但直到后面学到了类相关的知识,初步理解了构造函数以及“类类型”的隐式类型转换机制以后,不禁反思:之前的“实参是任意能够作为形参的初始值的表达式”这个结论是不是错误的?说不定只是编译器只是自动调用了一个构造函数完成了隐式类型转换,创建了一个string临时对象(作为“真正的”实参)来完成真正的(拷贝)初始化(string形参的)工作。
实际上“用实参去初始化形参”这种理解即对也不对。实参初始化形参变量这种说法肯定是站的住脚的,如果形参不是用实参来初始化的,那传递实参有什么意义呢。只不过初始化的形式是受到限制的,在按值传参这种方式下,只能是拷贝初始化而不能是直接初始化。举个例子:
1 //myComplex.h
2 #ifndef MYCOMPLEX_H
3 #define MYCOMPLEX_H
4 5 #include <iostream> 6 7 class myComplex 8 { 9 friend std::ostream& print(std::ostream&, const myComplex&); 10 public: 11 myComplex(double x, double y):re_(x),im_(y){} 12 explicit myComplex(double x):re_(x){} 13 myComplex() =default; 14 double real() const {return re_;} 15 double imag() const {return im_;} 16 myComplex& combine(myComplex); 17 private: 18 double re_ = 0.0; 19 double im_ = 0.0; 20 }; 21 22 inline myComplex& myComplex::combine(myComplex c) 23 { 24 re_ += c.re_; 25 im_ += c.im_; 26 return *this; 27 } 28 29 inline std::ostream& print(std::ostream& os, const myComplex &c) 30 { 31 os << '(' << c.re_ << ',' << c.im_ << ')'; 32 return os; 33 } 34 35 #endif 36
37 //demo.cpp
38 #include "myComplex.h" 39 #include <iostream> 40 41 int main() 42 { 43 myComplex c(1.0,0.0); 44 myComplex d(2.0); 45 //error: cannot convert ‘double’ to ‘myComplex 46 print(std::cout, c.combine(3.0)) << std::endl; 47 //error: cannot convert ‘double’ to ‘myComplex 48 print(std::cout, 5.0) << std::endl; 49 return 0; 50 }
我自行定义了一个复数类,目前只支持最简单的初始化操作和复合赋值操作。这个类有一个支持通过一个实参调用的构造函数,但这个构造函数被声明为explicit。
上述的代码在GNU 编译器中无法通过编译,错误理由见注释。可以看出,在对成员函数combine的调用中,combine的形参是myComplex类型。如果按照以前的理解(即认为实参是任意能够作为形参的初始值的表达式),这个函数调用应该是正确的才对,因为double确实可以作为myComplex的初始值(例如main中局部变量d的定义)。可见,并不是任意能作为形参变量的初始值的表达式就能作为实参。
那么如何判断什么样的表达式才能够作为实参?
个人猜测:用实参去初始化形参的过程能写成
形参类型 形参名 = 实参表达式;
这样一条定义语句的表达式才能作为实参。换言之,要求用实参去拷贝初始化形参。
myComplex c = 3.0; //错误,因为那个单形参的构造函数是explilcit
这一条定义是错误的,这里是拷贝初始化,要求等号右侧应该为一个myComplex对象。然而编译器有这样一条特性:如果某个类定义了一个只需要一个实参就能调用的构造函数,则定义了一个从该构造函数的形参类型到类类型的隐式转换机制(前提是这个构造函数不能是explicit)。编译器会将本应该使用这个类类型的对象实际却使用了那个构造函数形参类型对象的地方,自动调用那个构造函数,创建一个临时的类对象来完成工作。然而如果在这个构造函数的声明处使用了explicit关键字,那么这种类类型的“隐式类型转换”就被禁止了(也就是编译器不能在这种需要隐式类型转换的地方自动调用这个构造函数了)。
总结:如果函数func的形参类型为myComplex,则实际上该函数调用时,传入的实参应该得是一个myComplex对象(无论左值右值),只有当myComplex类存在一个适当的且不为explicit的构造函数时,才可以用这个构造函数的形参类型的表达式来作为func的实参。当然,如果这个构造函数是explicit时,非得用这个构造函数的形参类型的表达式来作为func的实参也是有办法的,但是得通过显式调用构造函数或者显式类型转换。
1 explicit myComplex(double d):re_(d){} 2 3 c.combine(myComplex(3.0)); 4 c.combine(static_cast<myComplex>(3.0)); 5 6 print(std::cout, myComplex(5.0)) << std::endl;
-------------------------------------------------------------------------------------------------------
更新:如果某个函数的返回值类型为类类型时也有类似的结论。
myComplex add_real(const myComplex &a, const myComplex &b) { double d = a.real() + b.real(); return d; }
这里,只有当myComplex(double d):re_(d){}这个构造函数不为explicit时这个return语句中的表达式才是正确的。以前也是想当然的以为return语句中的表达式只要能够用于初始化一个myComplex对象就行了,实际上不是的,和上面的按值传参的机制一样,要求return语句中的表达式能够用于拷贝初始化一个myComplex对象。
换言之,当一个函数的返回类型为类类型时,要求return语句中的表达式为这个类类型的对象(无论左值还是右值)。仅当这个class定义了一个只需要一个实参就能调用的构造函数且不为explicit时,return语句中的表达式才能够使用这个构造函数的形参类型的表达式,此时编译器会自动调用这个构造函数完成隐式类型转换。
------------------------------------------------------------------------
更新:
TNND,废了那么大劲,原来CPP primer的13章就有讲。

浙公网安备 33010602011771号