最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
通常情况下,C++是以值传递(pass-by-value)的方式传递对象至函数,除非你另外指定,否则函数参数都是以实参的复件为初值,而调用函数返回的对象也是函数返回值的一个复件。这些复件是由对象的拷贝构造函数产出的,这使得值传递的成本很高(费时)。
class Person{
public:
Person();
virtual ~Person(); //见条款7为什么使用虚函数
....
private:
string name;
string address;
};
class Student : public Person{
public:
Student();
virtual ~Student();
...
private:
string schoolName;
string schoolAddress;
};
bool validateStudent(Student s); //值传递
int main()
{
Student stu;
bool platoOK = validateStudent(stu); //调用函数
return 0;
}
当上述代码中的 validateStudent(stu)
函数被调用时,Student
的 拷贝构造函数 会被调用,以 stu
为参数将 s
初始化;而函数结束后,当 validateStudent()
返回 ,s
会被销毁。因此,对函数而言,参数的传递成本是“一次 Student
拷贝构造函数 调用,加上一次 Student
析构函数 调用”。
但这还不是全部,Student
对象内还有两个 string
对象,所以每次构造一个 Student
对象也就构造了两个 string
对象。此外 Student
继承自 Person
,所以每次构造 Student
对象也必须构造出一个 Person
对象,而 Person
对象中又有两个 string
对象,因此每构造一个Person
对象又需要构造两个 string
对象。因此,以值传递方式传递一个 Student
对象会导致:Student
和 Person
的 拷贝构造函数 各调用一次, string
的 拷贝构造函数 调用了四次。当函数结束时,又需要释放 Student
对象,每一个 构造函数 调用都对应了一个 析构函数 的调用。因此,最终成本是:六次 拷贝构造函数 和六次 析构函数。
我们得保证函数的参数能够安全的初始化和销毁,但同样希望避免这些不必要的开销,那么就得使用常量引用传递(pass-by-reference-to-const)。
bool validateStudent(const Student& s);
使用引用传递表示在实参本身上进行读写,从而不用进行拷贝复件,那么拷贝构造函数和析构函数的调用也就省略了。若我们不想实参本身被修改,就需要加上 const
关键字,意思是这个实参是只读的。
引用传递传递参数可以避免对象切割问题。当一个函数的传入参数是基类,而你以值传递的方式传入一个派生类,参数初始化调用基类的拷贝构造函数,派生类派生出来的特性全部被“切割”掉了,仅留下一个基类对象。
class Window{ //定义一个图形操作界面的窗口类
public:
...
std::string name() const; //返回当前窗口名称
virtual void display() const; //显示窗口和内容,它是虚函数
};
class WindowWithScrollBar : public Window{
public:
...
virtual void display() const;
};
display()
是虚函数,这就意味着两个类对于这个函数有不同的实现。现在你要实现一个函数:先打印出窗口的名字,然后显示窗口。下面是错误示范:
void printNameAndDisplay(Window w){ //值传递
std::cout<<w.name();
w.display();
}
当你调用上述函数并传给它一个 WindowWithScrollBar
对象,会发生什么呢?
WindowWithScrollBar wwsb;
printNameAndDisplay(wwsb); //调用的永远是Window::display()
参数 w
会被构造成一个 Window
对象,因为该函数是值传递的,所以导致了 wwsb
之所以是 WindowWithScrollBar
对象的所有特性都被切割掉了。在 printNameAndDisplay()
内不论传递过来的对象原本是什么类型,参数 w
永远只是 Window
对象,因此在函数内调用 display()
调用的总是 Window::display()
,绝不会是 WindowWithScrollBar::display()
。
解决切割问题的方法就是使用引用传递。传进来的窗口是什么类型,w
就表现为那种类型。
void printNameAndDisplay(const Window& w){ //值传递
std::cout<<w.name();
w.display();
}
在C++编译器的底层,引用是用指针来实现的,即引用传递的本质是指针传递。因此对于内置类型来说,值传递往往比引用传递的效率高。STL中的迭代器和函数对象也适合使用值传递,因为它们是根据值传递效率高的规则来设计的,并且它们不受切割问题的影响。
选择值传递还是引用传递,取决于你使用哪一部分的C++(见条款01)。
选择值传递还是引用传递与类型的大小无关。
Note:
- 尽量以引用传递(若想参数只读,加 const)替换值传递,前者通常比较高效,并可避免切个问题
- 以上规则并不适用于内置类型以及STL的迭代器和函数对象,对它们而言,值传递更高效