考虑以下程序:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
// 默认构造函数
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{
}
void print() const
{
std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
}
};
int main()
{
Fraction f { 5, 3 }; // 调用 Fraction(int, int) 构造函数
Fraction fCopy { f }; // 这里调用了哪个构造函数?
f.print();
fCopy.print();
return 0;
}
你可能会惊讶地发现,这个程序可以正常编译,并且输出结果为:
Fraction(5, 3)
Fraction(5, 3)
让我们更仔细地看看这个程序是如何工作的。
变量 f
的初始化只是一个标准的花括号初始化,调用了 Fraction(int, int)
构造函数。
但是下一行呢?变量 fCopy
的初始化显然也是一个初始化操作,而你知道构造函数是用来初始化类的。那么这一行调用了哪个构造函数呢?
答案是:拷贝构造函数。
拷贝构造函数
拷贝构造函数是一种用于用同类型的现有对象初始化一个对象的构造函数。拷贝构造函数执行完毕后,新创建的对象应该是作为初始化器传入的对象的副本。
隐式拷贝构造函数
如果你没有为你的类提供拷贝构造函数,C++ 将为你创建一个公共的隐式拷贝构造函数。在上面的例子中,Fraction fCopy { f };
这一行调用了隐式拷贝构造函数来用 f
初始化 fCopy
。
默认情况下,隐式拷贝构造函数将执行逐成员初始化。这意味着每个成员都将使用作为初始化器传入的类的相应成员进行初始化。在上面的例子中,fCopy.m_numerator
使用 f.m_numerator
(值为 5)进行初始化,fCopy.m_denominator
使用 f.m_denominator
(值为 3)进行初始化。
拷贝构造函数执行完毕后,f
和 fCopy
的成员具有相同的值,因此 fCopy
是 f
的副本。因此,无论是调用 f.print()
还是 fCopy.print()
,结果都是相同的。
自定义拷贝构造函数
我们也可以显式地定义自己的拷贝构造函数。在本课中,我们将让我们的拷贝构造函数打印一条消息,以便向你证明它确实在创建副本时被调用。
拷贝构造函数看起来就像你期望的那样:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
// 默认构造函数
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{
}
// 拷贝构造函数
Fraction(const Fraction& fraction)
// 使用参数的相应成员初始化我们的成员
: m_numerator{ fraction.m_numerator }
, m_denominator{ fraction.m_denominator }
{
std::cout << "Copy constructor called\n"; // 只是为了证明它被调用了
}
void print() const
{
std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
}
};
int main()
{
Fraction f { 5, 3 }; // 调用 Fraction(int, int) 构造函数
Fraction fCopy { f }; // 调用 Fraction(const Fraction&) 拷贝构造函数
f.print();
fCopy.print();
return 0;
}
运行此程序时,你将得到:
Copy constructor called
Fraction(5, 3)
Fraction(5, 3)
我们上面定义的拷贝构造函数在功能上与默认情况下会得到的拷贝构造函数完全等价,只是我们添加了一个输出语句来证明拷贝构造函数确实被调用了。当 fCopy
用 f
初始化时,会调用这个拷贝构造函数。
提醒
访问控制是按类定义的(而不是按对象定义的)。这意味着一个类的成员函数可以访问同类型的任何类对象的私有成员(而不仅仅是隐式对象的成员)。
我们在上面的 Fraction
拷贝构造函数中利用了这一点,以便直接访问分数参数的私有成员。否则,我们将无法直接访问这些成员(除非添加访问函数,而我们可能不想这么做)。
拷贝构造函数除了复制对象外,不应执行任何其他操作。这是因为编译器可能会在某些情况下优化掉拷贝构造函数。如果你依赖拷贝构造函数来实现除了复制之外的某些行为,那么这种行为可能会发生,也可能不会发生。我们将在第14.15课——类初始化和拷贝消除中进一步讨论这个问题。
最佳实践:拷贝构造函数除了复制对象外,不应有任何副作用。
优先使用隐式拷贝构造函数
与隐式默认构造函数(什么也不做,因此通常不是我们想要的)不同,隐式拷贝构造函数执行的逐成员初始化通常正是我们想要的。因此,在大多数情况下,使用隐式拷贝构造函数是完全可行的。
最佳实践:除非有特定理由需要自己创建拷贝构造函数,否则优先使用隐式拷贝构造函数。
我们将在讨论动态内存分配(21.13——浅拷贝与深拷贝)时看到需要重写拷贝构造函数的情况。
拷贝构造函数的参数必须是引用
要求拷贝构造函数的参数必须是左值引用或常量左值引用。由于拷贝构造函数不应修改参数,因此使用常量左值引用是首选。
最佳实践:如果你自己编写拷贝构造函数,参数应为常量左值引用。
按值传递与拷贝构造函数
当一个对象按值传递时,参数会复制参数。当参数和参数是同一类类型时,通过隐式调用拷贝构造函数来完成复制。
以下示例说明了这一点:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
// 默认构造函数
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{
}
// 拷贝构造函数
Fraction(const Fraction& fraction)
: m_numerator{ fraction.m_numerator }
, m_denominator{ fraction.m_denominator }
{
std::cout << "Copy constructor called\n";
}
void print() const
{
std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
}
};
void printFraction(Fraction f) // f 按值传递
{
f.print();
}
int main()
{
Fraction f{ 5, 3 };
printFraction(f); // 使用拷贝构造函数将 f 复制到函数参数 f 中
return 0;
}
在作者的机器上,这个示例打印:
Copy constructor called
Fraction(5, 3)
在上面的示例中,调用 printFraction(f)
是按值传递 f
。通过隐式调用拷贝构造函数,将 f
从 main
复制到函数 printFraction
的参数 f
中。
按值返回与拷贝构造函数
在第2.5课——局部作用域的介绍中,我们指出按值返回会创建一个临时对象(保存返回值的副本),该对象被传递回调用者。当返回类型和返回值是同一类类型时,临时对象通过隐式调用拷贝构造函数进行初始化。
例如:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
// 默认构造函数
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{
}
// 拷贝构造函数
Fraction(const Fraction& fraction)
: m_numerator{ fraction.m_numerator }
, m_denominator{ fraction.m_denominator }
{
std::cout << "Copy constructor called\n";
}
void print() const
{
std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
}
};
void printFraction(Fraction f) // f 按值传递
{
f.print();
}
Fraction generateFraction(int n