20180312 C++ 请以pass-by-reference-to-const替换pass-by-value
缺省情况下C++以by value方式(一个继承自C的方式)传递对象至(或来自)函数,除非你另外指定,否则函数参数就是以实际实参的复件(副本)为初值,而调用端所获得的亦是函数返回值的一个复件。这些复件(副本)是由对象的copy构造函数产出,这可能使得pass-by-value成为昂贵的(费时的)操作。考虑以下class继承体系:
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;
};
现在考虑以下代码,其中调用函数validateStudent,后者需要一个Student实参(by value)并返回它是否有效:
bool validateStudent(Student s);//函数以by value方式接受学生
Student yanhui;//颜回,孔子的学生
bool yanhuiisOK = validateStudent(yanhui);//调用函数
上述函数被调用时,无疑的,Student的copy构造函数会被调用,以yanhui为蓝本将颜回初始化。同样明显地,当validateStudent返回s会被销毁。因此,对于函数而言,参数的传递成本是“一次Student 拷贝构造函数调用,加上一次Student析构函数被调用”。
详细过程为:Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象,此Student对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象。一个Person对象又有两个string对象在其中,因此每一次Person构造动作又需承担两个string构造动作。最终结果是,以by value方式传递一个Student对象会导致调用一次Student 拷贝构造函数,一次Person拷贝构造函数,四次string拷贝构造函数。当函数内的那个Student复件被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作。因此,以by value方式传递一个Student对象,总体成本是“六次构造函数和六次析构函数”。
如果想出一个方法可以回避所有构造和析构函数就好了。有的!就是pass by reference-to-const:
bool validateStudent(const Student& s);
现在Student以by reference方式传递,将它声明为const是必要的,因为不这样做的话调用者会忧虑validateStudent会不会改变他们传入的那个Student。
以by reference方式传递参数也可以避免对象切割(slicing)问题。假设在一组classes上工作,用来实现一个图形窗口系统:
class Window{
public:
...
std::string name() const;//返回窗口名称
virtual void display() const;//显示窗口和其他内容
};
class WindowWithScrollBars:public Window{
public:
...
virtual void display() const;
};
所有Window对象都带有一个名称,你可以通过name函数取得它。所有窗口都可显示,你可以通过display函数完成它。display是一个virtual函数,这意味着简易朴素的基类Window对象的显示方式和华丽WindowWithScrollBars对象的显示方式不同。(派生类是基类的扩展)。
现在假设写个函数打印窗口名称,然后显示窗口,下面的示范是错误的:
void printNameAndDisplay(Window w)//不正确! 参数可能被切割
{
std::cout<<w.name();
w.display();
}
当调用上述函数并交给他WindowWithScrollingBars对象 会怎样呢?
WindowWithScrollingBars wwsb;
printNameAndDiaplay(wwsb);
参数w会被构造成一个Window对象;它是pass by value,而造成wwsb“是个WindowWithScrollingBars对象”的所有特化信息都会被切除。在printNameAndDisplay函数内不论传递过来的对象原本是什么类型,参数w就像一个Window对象(因为其类型的Window)。因此在printNameAndDisplay内调用display掉用的总是Window::display,绝对不会是WindowWithScrollingBars::diaplay。
解决切割(slicing)问题的办法,就是以by reference-to-const的方式传递w:
void printNameAndDisplay(const Window& w)//很好,参数不会被切割
{
std::cout<<w.name;
w.dispaly();
}
现在,传进来的窗口是什么类型,w就表现出那种类型。
在C++底层实现中,reference往往以指针实现,因此pass by reference通常意味着真正传递的是指针,因此若对象属于内置类型,pass by value往往比pass by reference效率更高,这种情况也适用于STL的迭代器和函数对象。
注意:
1、尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可以避免切割问题(slicing problem)。
2、以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较合适。