在默认情况下,C++函数传递参数是继承C的方式,是值传递(pass by value)。这样传递的都是实际实参的副本,这个副本是通过调用复制构造函数来创建的。有时候创建副本代价非常昂贵。例如一下继承体系
<span style="font-size:14px;">class Person{ public: Person(); virtual ~Person(); …… private: std::string name; std::string address; }; class Student:public Person{ public: Student(); ~Student(); …… private: std::string schoolName; std::string schoolAddress; };</span>
<span style="font-size:14px;">bool validateStudent(Student s);//pass by value Student plato; bool platIsOK=validateStudent(plato);</span>
pass by value是正确的,但是其效率低下。以pass by reference-to-const方式传递,可以回避所有构造函数和析构函数。
<span style="font-size:14px;">bool validateStudent(const Student& s);</span>这种方式传递,没有新对象创建,所以自然没有构造和析构函数的调用。参数中,以const修饰是比较重要的,原先的pass by value,原先的值自然不会被修改。现在以pass by reference方式传递,函数validateStudent内使用的对象和传进来的同同一个对象,为了防止在函数内修改,加上const限制。
以pass by reference方式传递,还可以避免对象切割(slicing)问题。一个派生类(derived class)对象以pass by value方式传递,当被视为一个基类对象(base class)时,基类对象的copy构造函数会被调用,此时派生类部分全部被切割掉了,仅仅留下一个base class部分。
在C++编译器的底层,reference往往以指针实现出来,所以pass by reference通常意味着真正传递是指针。但是对于内置类型,pass by value往往比pass by reference更高效。所以在使用STL函数和迭代器时,习惯上都被设计出pass by value。当设计迭代器和函数时,设计者有责任查看哪种传递方式更为高效,是否会有切割问题的影响。这个规则的改变适用于你使用C++的哪一部分。(条款1)
通常,内置类型都比较小,因此有人认小型types都适合pass by value,用户自己定义的class亦然。但是对象小并不意味着copy构造函数代价小。许多对象(包括STL容器),内涵的成员只不过是一两个指针,但是复制这种对象时,要复制指针指向的每一样东西,这个代价很可能十分昂贵。
还有一个理由,某些编译器对待内置类型和用户自定义类型的”态度“截然不同,即使两者有着相同的底层描述。例如,某些编译器拒绝把一个double组成的对象放进缓存器内,但是却乐意在一个正规基础上光秃秃的doubles上这么做。当这种事情发生时,应该以by reference方式传递此对象,因为编译器当然会把指针放进缓存器。
用户自定义的小型types,可能还会发生变化,将来也许会变大,其内部实现可能会改变,所以用户自定义的小型type在使用pass by value时要慎重。
一般情况下,可以假设内置类型和STL迭代器和函数对象以pass by value是代价不昂贵。其他时候最好以pass by reference to const替换掉pass by value。
在掌握了pass by reference后,刚开始一心一意想把所有pass by value替换为pass by reference。这时往往会犯下一个错误:传递一些reference指向不存在的对象。考虑一个用以表现有理数乘积的class。
<span style="font-size:14px;">class Rational{ public: Rational(int numerator=0, int denominator=1); …… private: int n, d; friend const Rational operator*(const Rational& lhs, const Rational& rhs); };</span>
一个对象的创建是指,在这个operator*函数中,创建一个新对象来保存结果,之后用这个用这个新对象返回,返回时用它初始化另一个对象。之后这个新对象析构。
但是如果用by reference方式传递就不会有任何代价。但是reference只是名称,代表一个已经存在的对象,任何时候看到reference都应该问自己,它的另一个名称是什么?如果上述operator*返回一个reference,那么它一定指向一个存在的Rational对象。
<span style="font-size:14px;">Rational a(1, 2);// 1/2 Rational b(3, 5);// 3/5 Rational c=a*b;// 3/10</span>
函数创建新对象有两种途径,在stack上或在heap上创建。如果定义local变量,那么就在stack上创建
<span style="font-size:14px;">const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational result(lhs.n* rhs.n, lhs.d* rhs.d); return result; }</span>
考虑在heap上创建对象
<span style="font-size:14px;">const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational* result=new Rational(lhs.n* rhs.n, lhs.d* rhs.d); return *result; }</span>
即使客户使用时谨慎,但是还是有可能造成以下内存泄露:
<span style="font-size:14px;">Rational w,x,y,z; w=x*y*z;</span>上述语句调用了两次operator*,使用了两次new,也就需要两次delete。但是没办法让使用者进行那些delete操作,因为没有让他们取得operator*返回的reference背后的指针。这会导致内存泄露。
在上面的两种做法中(在stack和在heap上创建对象),都因为operator*返回结果调用构造函数而受到惩罚。我们最初的目标是避免如此的构造函数的调用。还有一个办法避免任何构造函数的调用:让operator*返回一个指向在函数内部定义的static Rational对象:
<span style="font-size:14px;">const Rational& operator*(const Rational& lhs, const Rational& rhs) { static Rational result; result=……; return result; }</span>
<span style="font-size:14px;">bool operator==(const Rational& lhs, const Rational& rhs);//为比较Rational对象而写的 Rational a, b, c, d; …… if((a*b)==(c*d)) {//乘积相等时, doSomething(); } else//乘积不等时 { doOtherthing(); }</span>
一个必须返回新对象的函数的正确写法,就是让那个函数返回一个新对象。就这么简单。上述operator*正确的写法:
<span style="font-size:14px;">inline const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.n* rhs.n, lhs.d* rhs.d); }</span>
这样,你需要承受operator*返回值的构造和析构成本,但是从长远来看,那只是为了获得正确的行为而付出的一个小小的代价。但万一代价比较恐怖,承受不起,这是别忘了C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善产出代码的效率,却不改变其可观察的行为。如果编译器施行优化,你的程序将保持它们该有的行为,但运行起来比预期更快。
以上可以总结为:在返回一个reference和返回一个object之间抉择时,挑出行为正确的那个。让编译器厂商为你尽可能降低成本吧!