7、拷贝构造函数
在C++中有一种叫做拷贝构造函数的特殊的构造函数,它允许生成另一个对象的拷贝的对象。下面是在SpreadsheetCell类中的拷贝构造函数的声明:
export class SpreadsheetCell
{
public:
SpreadsheetCell(const SpreadsheetCell& src);
// Remainder of the class definition omitted for brevity
};
拷贝构造函数使用一个对源对象的常量引用。与其他构造函数类似,它并不返回值。拷贝构造函数从源对象中拷贝所有的数据成员。当然了,从技术上来说,在拷贝构造函数中可以做你想做的任何事,但是,执行期待的行为并初始化新的对象为原来对象的一个拷贝是个不错的主意。下面是一个实现SpreadsheetCell拷贝构造函数的例子。注意构造函数初始化器的使用。
SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src)
: m_value { src.m_value }
{
}
如果你自己不定拷贝构造函数,c++就会为你生成一个,初始化每个从与其等价的源对象中的数据成员的新的对象的数据成员。对于对象的数据成员,这种初始化意味着调用拷贝构造函数。给定一些叫做m1,m2,...mn的数据成员,编译器生成的拷贝构造函数可以表示如下:
classname::classname(const classname& src)
: m1 { src.m1 }, m2 { src.m2 }, ... mn { src.mn } { }
因此,在大部分情况下,没有必要指定拷贝构造函数!
注意:SpreadsheetCell拷贝构造函数只是为了演示的需要。实际上,在这种情况下,拷贝构造函数可以省略,因为缺省的编译器生成的已足够好。然而,在有些情况下,编译器生成的拷贝构造函数是不够的。我们以后再讨论。
7.1、什么时候调用拷贝构造函数
在c++中缺省的传递给函数的参数语法是传值。这就意味着函数接收到的是值或者对象的一个拷贝。这样的话,每当传递对象给函数的时候,编译器就会调用新对象的拷贝构造函数来进行初始化。例如,假设你有下面的printString()函数,接收std::string参数的值:
void printString(string value)
{
println("{}", value);
}
回忆一下,std::string实际上是一个类,而不是一个内建的类型。当代码传递string参数调用printString()是,string参数的值是用调用其拷贝构造函数来进行初始化的。拷贝构造函数的参数就是传递给printString()的string。在下面的例子中,string的拷贝构造函数对于printString()的value对象以name作为其参数执行:
string name { "heading one" };
printString(name); // Copies name
当printString()成员函数执行完毕时,value就被破坏掉了。因为它只是name的一个拷贝,而name保持不变。当然了,你可以通过传递一个指向常量的引用作为参数来避免掉拷贝构造函数的开销,这个我们下面讨论。
当从函数中返回对象的值时,拷贝构造函数也可能被调用。这个也比较复杂,我们专门讨论吧。
7.2、显式调用拷贝构造函数
你也可以显式使用拷贝构造函数。能够构造一个对象完全是另一个对象的拷贝通常是很有用的。例如,你可能会想像这样生成一个SpreadsheetCell对象:
SpreadsheetCell myCell1 { 4 };
SpreadsheetCell myCell2 { myCell1 }; // myCell2 has the same values as myCell1
7.3、传递对象引用
为了避免在传递对象给函数时发生拷贝,应该声明函数接收对象的引用。传递对象引用通常比传值更高效,因为只有对象的地址被拷贝,而不是整个对象。另外,引用传递避免了对象动态内存分配的问题。这个也留待后面讨论吧。
当传递对象引用时,使用对象引用的函数可能会改变原来的对象。如果使用引用传递只是为了效率,也可以通过声明对象为常量来排除这种可能性。这就是有名的通过常量引用传递对象,会贯穿在我们整个的博客中。
注意:基于性能的原因,最好是传递常量引用对象而不是值。在介绍了move的语法后会稍稍改动一下这个规则,允许在特定情况下传递对象的值。
注意SpreadsheetCell类有许多接收std::string_view作为参数的成员函数。我们以前讨论过,string_view只是一个指针与长度,所以对其拷贝开销不会太大,通常都是传值。
还有对于像int,double等等的原始类型也应该传值。对这样类型的传递常量引用也得不到什么额外的好处。
SpreadsheetCell类的doubleToString()成员函数总是返回string值,因为该成员函数的实现生成了一个本地的string对象,在返回给调用者的成员函数的结尾。返回一个对该string的引用是不灵的,因为当函数退出时,它指向的string已经被破坏掉了。
7.4、显式缺省与删除拷贝构造函数
就像可以显式缺省与删除编译器生成类的缺省构造函数一样,也可以显式缺省与删除编译器生成的拷贝构造函数,如下:
SpreadsheetCell(const SpreadsheetCell& src) = default;
SpreadsheetCell(const SpreadsheetCell& src) = delete;
删除了拷贝构造函数,对象就不能再被拷贝了。这可以被用于禁止传值给对象,这个我们以后讨论。
注意:如果拥有数据成员的类有一个删除了的或者私有的拷贝构造函数,该类的拷贝构造函数也自动删除了,即使你显式地给了一个缺省的情况下也不行。