14-14 复制构造函数介绍

请考虑以下程序:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    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 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // What constructor is used here?

    f.print();
    fCopy.print();

    return 0;
}

你可能会惊讶地发现,这个程序编译完全没问题,并且产生了以下结果:

image

让我们仔细看看这个程序是如何工作的。

变量 f 的初始化只是标准的大括号初始化,调用了 Fraction(int, int) 构造函数。

但下一行呢?变量 fCopy 的初始化显然也是初始化操作,而你清楚构造函数用于初始化类。那么这一行调用了哪个构造函数?

答案是:复制构造函数。


复制构造函数

复制(拷贝)构造函数copy constructor是一种构造函数constructor,用于通过同类型现有对象来初始化新对象。执行复制构造函数后,新生成的对象应成为传入初始化参数对象的副本。


隐式复制构造函数

若未为类提供显式复制构造函数,C++ 将自动生成公共隐式复制构造函数。在上例中,语句 Fraction fCopy { f }; 调用了隐式复制构造函数,用 f 初始化 fCopy。

默认情况下,隐式复制构造函数将执行成员逐项初始化。这意味着每个成员都将使用作为初始化器传入的类对应成员进行初始化。在上例中,fCopy.m_numerator 使用 f.m_numerator(值为 5)初始化,fCopy.m_denominator 使用 f.m_denominator(值为 3)初始化。

复制构造函数执行后,f 与 fCopy 的成员值完全一致,因此 fCopy 是 f 的副本。对两者调用 print() 函数将得到相同结果。


定义自己的复制构造函数

我们也可以显式地定义自己的复制构造函数。在本节课中,我们将让复制构造函数打印一条消息,以便向您展示当进行复制时它确实正在执行。

复制构造函数的定义方式完全符合预期:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Copy constructor
    Fraction(const Fraction& fraction)
        // Initialize our members using the corresponding member of the parameter
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout << "Copy constructor called\n"; // just to prove it works
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };  // Calls Fraction(int, int) constructor
    Fraction fCopy { f }; // Calls Fraction(const Fraction&) copy constructor

    f.print();
    fCopy.print();

    return 0;
}

当这个程序运行时,你会得到:

image

我们上面定义的复制构造函数在功能上与默认构造函数等效,区别在于我们添加了输出语句来证明复制构造函数确实被调用了。当fCopy通过f进行初始化时,该复制构造函数会被调用。

重要提示
访问控制是基于类(而非对象)生效的。这意味着类成员函数可访问同类型任意类对象的私有成员(不仅限于隐式对象)。

我们在 Fraction 的复制构造函数中利用此特性直接访问 fraction 参数的私有成员。否则将无法直接访问这些成员(除非添加访问函数,而这可能并非我们所期望的)。

复制构造函数不应执行除复制对象外的任何操作。这是因为编译器在特定情况下可能优化掉复制构造函数。若依赖复制构造函数实现复制以外的行为,该行为可能发生也可能不发生。我们在第14.15节——类初始化和复制省略中将进一步讨论此问题。

最佳实践:
复制构造函数不应产生复制操作之外的副作用。


优先使用隐式复制构造函数

与什么都不做的隐式默认构造函数不同(因此很少是我们想要的),隐式复制构造函数执行的成员逐项初始化通常正是我们所需。因此在大多数情况下,使用隐式复制构造函数完全没问题。

最佳实践:
除非有特殊原因需要自定义构造函数,否则应优先使用隐式复制构造函数。

在讨论动态内存分配时(21.13节——浅拷贝与深拷贝),我们将看到需要重写复制构造函数的场景。


复制构造函数的参数必须是引用

复制构造函数的参数必须是左值引用或常量左值引用。由于复制构造函数不应修改参数,因此建议使用常量左值引用。

最佳实践:
若编写自定义复制构造函数,参数应采用常量左值引用形式。


按值传递与复制构造函数

当对象按值传递时,参数会被复制到参数中。当参数与参数类型相同类时,复制操作通过隐式调用复制构造函数实现。

以下示例说明了这一点:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // Copy constructor
    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 is pass by value
{
    f.print();
}

int main()
{
    Fraction f{ 5, 3 };

    printFraction(f); // f is copied into the function parameter using copy constructor

    return 0;
}

在作者的机器上,此示例输出:

image

在上例中,对 printFraction(f) 的调用是以值传递方式传递 f。复制构造函数被调用,用于将 f 从 main 复制到函数 printFraction() 的 f 参数中。


按值返回与复制构造函数

在第2.5课——局部作用域简介中,我们提到按值返回会创建一个临时对象(保存返回值的副本),该对象被传递回调用方。当返回类型与返回值属于同一类时,该临时对象通过隐式调用复制构造函数进行初始化。

例如:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // Copy constructor
    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 is pass by value
{
    f.print();
}

Fraction generateFraction(int n, int d)
{
    Fraction f{ n, d };
    return f;
}

int main()
{
    Fraction f2 { generateFraction(1, 2) }; // Fraction is returned using copy constructor

    printFraction(f2); // f2 is copied into the function parameter using copy constructor

    return 0;
}

当 generateFraction 将 Fraction 返回给主函数时,会创建一个临时 Fraction 对象并通过复制构造函数初始化。

由于该临时对象用于初始化 Fraction f2,这将再次调用复制构造函数将临时对象复制到 f2 中。

而当 f2 传递给 printFraction() 时,复制构造函数被第三次调用。

因此,在作者的机器上,此示例输出:

Copy constructor called
Copy constructor called
Copy constructor called
Fraction(1, 2)

这是我的输出结果,和作者的相差很大,为了谨慎起见, 我把自己写的删除掉,然后把作者的复制过来构建运行也是如此结果, 所以我就使用clang去调试(也就是debug)看看哪里出问题了image
结果就是generateFraction函数返回f给main函数f2对象作初始化时并没有调用拷贝构造函数, 这里我猜测是clang编译器太聪明了, 给优化掉了, 所以理论上还是以作者(Alex)的为主。
image
只有执行printFraction(f2);语句时才调用了拷贝构造函数一次image
在使用 拷贝构造函数 =delete语法删除后,发现f返回处确实报错了, 这说明此处确实在调用copy constructor, 但是被优化掉了。
image

若编译并运行上述示例,您会发现仅调用了两次复制构造函数。这是编译器优化机制——复制省略copy elision。我们将在第14.15节---类初始化与复制省略 中进一步探讨复制省略。


使用 = default 生成默认复制构造函数

若类未定义复制构造函数,编译器将隐式生成默认构造函数。若需自定义,可通过 = default 语法显式要求编译器创建默认复制构造函数:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Explicitly request default copy constructor
    Fraction(const Fraction& fraction) = default;

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };
    Fraction fCopy { f };

    f.print();
    fCopy.print();

    return 0;
}

image


使用 = delete 防止复制

有时我们会遇到某些类对象不应可复制的情况。可通过将复制构造函数标记为删除来实现,使用 = delete 语法:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // Default constructor
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // Delete the copy constructor so no copies can be made
    Fraction(const Fraction& fraction) = delete;

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f { 5, 3 };
    Fraction fCopy { f }; // compile error: copy constructor has been deleted

    return 0;
}

image

在示例中,当编译器试图寻找构造函数来用 f 初始化 fCopy 时,它会发现复制构造函数已被删除。这将导致编译器报错。

顺带一提……
通过将复制构造函数设为私有(因私有函数不可被外部调用),也可阻止用户复制类对象。但私有复制构造函数仍可被类内部其他成员使用,因此除非有此需求,不建议采用此方案。

进阶读者须知:
三原则rule of three是C++领域广为人知的设计法则:若类需要用户定义的复制构造函数、析构函数或复制赋值运算符,则通常需要同时实现三者。C++11中该原则扩展为五原则rule of five,新增移动构造函数与移动赋值运算符。

违反三原则/五原则极易导致代码异常。我们将在动态内存分配章节重新探讨这两条规则。

构造函数相关内容详见第15.4节 构造函数导论 与第19.3节 构造函数 ,复制赋值运算符详见第21.12节 赋值运算符重载。


测验时间

问题 #1

在上节课中,我们提到复制构造函数的参数必须是(const)引用。为什么不允许使用按值传递?

显示提示

提示:思考当我们按值传递类类型参数时会发生什么。

显示解答

当我们按值传递类类型参数时,会隐式调用复制构造函数将参数复制到参数中。

若复制构造函数采用值传递方式,则需调用自身来将初始化**参数**`argument`复制到构造函数**参数**`parameter`中。但该对复制构造函数的调用同样采用值传递,导致构造函数再次被调用以将参数复制到函数参数中。这将引发复制构造函数的无限递归调用链。
posted @ 2025-12-30 12:55  游翔  阅读(19)  评论(0)    收藏  举报