C++的复制构造函数三种用法

本文详细探讨了C++中复制构造函数的调用情况,包括对象初始化、函数参数传递和函数返回值时的使用。特别强调了现代编译器对返回值优化(RVO)的实现,解释了为何有时函数返回对象时不调用复制构造函数。通过实例展示了全局对象作为返回值、函数参数为对象及函数返回值为对象时如何触发复制构造函数的调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

如果真的想明白,为什么你写的函数的返回值是对象时,有的时候调用了复制构造函数,而有的时候,没有调用复制构造函数。需要明白一件事:函数的返回值是对象时,什么情况下,函数的返回是return by value,即生成了临时对象。这两个问题是等价的,因为按照道理来讲,对象作为函数的返回值时,就应该生成临时对象,调用复制构造函数,初始化这个临时对象,但是,现在的编译器,对于return by value有优化。有时候,不生产临时对象。编译器总是将栈里面的 结果对象 直接给调用者,避免了多一次的析构和构造。即使在关闭编译器优化的时候,它依然给你做了这个动作。
现在的我,还不明白具体什么情况下,生成临时对象。什么情况下,不生成临时对象。
留给以后解决
可以参考的文章参考1参考2

三种用法

我们知道,类、struct结构体的 复制构造函数 在三种情况下会被调用,分别是:

1、创建对象a时,使用对象b去初始化对象a
2、函数fun( )的形参是对象时,传递参数时,形参 是用 复制构造函数 初始化的
3、函数的返回值是对象时,生成的临时对象,是用 复制构造函数初始化的

接下来依次看一下这三种情况。

首先是类的定义

class node {
    public:
        int x, y;
        
        node () {} // 默认构造函数
        node (int _x, int _y) : x(_x), y(_y) {} // 自定义的构造函数

        node (const node& temp) { // 自定义的复制构造函数
            x = temp.x;
            y = temp.y;
            printf("复制构造函数被调用\n"); 
            // 打印语句,只要该复制构造函数被调用,就会有输出
        }
};

1、创建对象a时,使用别的对象b去初始化a

int main()
{
    node b(8, 10); 
    // 创建了node对象 b,并且调用了构造函数,初始化对象b
    node a(b); // 创建对象a, 并且使用对象b来初始化a
    // node a = b; 
    // 这样也可以,只要是创建对象同时,用其余对象初始化就可以。
    cout<< a.x << " " << a.y << endl;

    return 0;
}

运行结果

复制构造函数被调用
8 10

可以看到,先输出了"复制构造函数被调用",之后又执行了打印a.x、a.y的语句。说明,在创建对象a时,使用对象b初始化对象a,是会调用复制构造函数的。调用了对象a的复制构造函数,复制构造函数的实参是对象 b。
但是下面的这种情况就不是创建对象的同时使用另一个对象初始化了。

int main()
{
    node b(8, 10);
    node a; // 先创建了对象 a,使用默认构造函数初始化了对象 a

    a = b; // 这是赋值,不是初始化
    
    cout << a.x << " " << a.y << endl;

    return 0;
}

运行结果

8 10

可见,首先定义一个对象a,之后再进行赋值,就不是初始化了。

2、函数fun( )的形参是对象时,传递参数时,形参 是用 复制构造函数 初始化的

void show(node temp) { // show()函数的参数是对象
    cout << temp.x << " " << temp.y << endl;
}

int main()
{
    node a(8, 10); 
    // 创建了node对象 a,并且调用了构造函数,初始化数据

    show(a);
    // 将对象a作为函数的实参传入

    return 0;
}

运行结果

复制构造函数被调用
8 10

可以看到,首先打印的是复制构造函数中的语句“复制构造函数被调用”,之后执行的是函数体里面的打印语句。所以,对象作为函数的参数时,传递参数时,是会调用复制构造函数的。即进行了值复制。形参temp是使用实参 a 初始化的。初始化形参temp时,调用了temp的复制构造函数,这个函数的参数是a。
更进一步,如果参数是引用:

void show(node& temp) { // show()函数的参数是 对象的引用
    cout << temp.x << " " << temp.y << endl;
}

int main()
{
    node a(8, 10); 
    // 创建了node对象 a,并且调用了构造函数,初始化数据

    show(a);
    // 将对象a作为函数的实参传入

    return 0;
}

此时函数的参数不是对象本身,而是对象的引用。再来看一下运行结果:

8 10

如果函数的参数是 对象的引用时,传入参数的时候,没有调用复制构造函数,没有输出复制构造函数中的打印语句“复制构造函数被调用”。
所以这种情况下,就没有复制这一步,因为传入的是实参的引用。如果大量数据要作为实参传入时,最好传入的是引用。速度快。

3、 函数的返回值是对象时,生成的临时对象,是用 复制构造函数 初始化的

我们知道,不同的函数,是在不同的内存空间中运行的,调用某个函数fun()时,去往另一片内存空间运行,当该函数执行结束时,这一片内存空间要被释放,其中的变量要消亡。但是,如果该函数有返回值(无论是基本类型,还是对象),则该函数要生成一个临时的值temp,用于返回给调用函数的地方。因为该函数执行结束之后,该函数里面的任何局部变量都要消失。
重点就是,该临时值temp,如果是对象的话,就是使用复制构造函数进行初始化的。

node create_node() { // 函数的返回值是对象
    node temp(3, 5); // 创建一个对象
    return temp; // 返回对象
}

int main()
{
    node a = create_node();// a 用于接收函数返回值

    cout << a.x << " " << a.y << endl;

    return 0;
}

运行结果:

3 5

可以看到,并没有想象的那样输出“复制构造函数被调用”,也就是说明,此种情况下,并没有调用复制构造函数。
原因在于现在的编译器对于“对象作为函数的返回值”这种情况有所优化。返回值为对象时,有的情况下不再产生临时对象,因而不再调用复制构造函数。因此也就不存在临时对象被初始化。

有几种情况下,还是会调用复制构造函数。
全局对象 cur 作为函数返回值,对象 a 在接收 返回值 cur 时,调用复制构造函数。

#include <bits/stdc++.h>
using namespace std;

class node {
    public:
        int x, y;

        node() {}
        node(int _x, int _y) : x(_x), y(_y) {}

        node(const node& temp) {
            x = temp.x, y = temp.y;
            printf("复制构造函数被调用\n");
        }
};

node cur(1, 2);

node fun() { // 函数的返回值是对象,并且是全局对象
    return cur;
}

int main()
{
    node a ; // a用于接收函数返回值
    a = fun(); // a 接收函数返回值
    cout << a.x << " " << a.y << endl;

    return 0;
}

运行结果

复制构造函数被调用
1 2

可以看到,当全局对象 cur 作为函数返回值时,对象 a 在接收 返回对象 cur时,调用了复制构造函数。

详细地分析一下这个例子。fun( ) 函数执行结束后,会生成一个临时对象,假设为temp,该对象是使用对象cur进行初始化的。在这个初始化过程中,调用了temp的复制构造函数,其实参是 return 语句中的 cur。在这之后,fun( )函数彻底消失,占用的内存被释放。
之后,在main( ) 函数中的 a = fun(); 语句,该临时对象temp,被赋值给 a, 这是一个赋值语句,并不是初始化语句。该语句执行结束之后,临时对象 temp 消亡。

此外,如果有一个函数,它的参数是对象,返回值也是对象,则,接收该函数的返回值时,调用了复制构造函数。

#include <bits/stdc++.h>
using namespace std;

class node {
    public:
        int x, y;

        node() {}
        node(int _x, int _y) : x(_x), y(_y) {}

        node(const node& temp) {
            x = temp.x, y = temp.y;
            printf("复制构造函数被调用\n");
        }
};

node fun(node& temp) { // 此处是引用,否则调用两次赋值构造函数。
    return temp;
}

int main()
{
    node a(1, 2), b;
    b = fun(a);
    cout << a.x << " " << a.y << endl;

    return 0;
}

运行结果

复制构造函数被调用
1 2

可以看到,fun( ) 函数的参数是对象、返回值也是对象,此时,对象 b 接收 返回值时,就调用了复制构造函数。

总结

复制构造函数,只在对象被 初始化 时被调用。而初始化意味着某对象在创建的同时,被初始化。
具体就三种情况:
1、对象被创建时,被其余对象初始化
2、函数fun( ) 的形参是对象,则调用函数 fun()时,形参会被初始化
3、函数的返回值是 对象,则生成的临时对象会被 return 语句中的数据初始化。

### C++ 复制构造函数 使用详解 #### 什么是复制构造函数复制构造函数是一种特殊的构造函数,用于通过已存在的对象初始化一个新的对象。它通常在以下情况下被调用: - 当一个对象以值传递方式传入函数时; - 当一个对象以值返回方式从函数返回时; - 当创建一个对象并将其初始化为另一个同类型的对象时。 如果没有显式定义复制构造函数,编译器会自动生成一个默认的复制构造函数[^4]。然而,默认的复制构造函数仅执行浅拷贝操作,即简单地将源对象的数据成员逐个赋值给目标对象的数据成员。这可能导致某些问题,特别是在涉及动态内存管理的情况下[^2]。 --- #### 浅拷贝与深拷贝的区别 ##### 浅拷贝 浅拷贝是指仅仅复制指针或其他资源的地址,而不是实际的内容。这意味着两个对象可能会共享同一块动态分配的内存区域。这种行为可能导致未定义的行为,尤其是在析构函数释放内存时发生双重删除的情况[^5]。 ##### 深拷贝 深拷贝则是完全独立地复制整个对象及其所拥有的资源。这样可以确保每个对象都有自己的数据副本,从而避免潜在的风险。 --- #### 自定义复制构造函数的实现方法 为了防止浅拷贝带来的问题,在设计类时应考虑是否需要提供自定义的复制构造函数来完成深拷贝。以下是具体实现的一个示例: ```cpp #include <iostream> using namespace std; class MyClass { public: int* data; // 构造函数 MyClass(int value) : data(new int(value)) {} // 复制构造函数 (深拷贝) MyClass(const MyClass& other) { data = new int(*other.data); cout << "Deep copy performed!" << endl; } // 析构函数 ~MyClass() { delete data; } void display() const { cout << "Value: " << *data << endl; } }; int main() { MyClass obj1(10); MyClass obj2(obj1); // 调用了复制构造函数 obj1.display(); obj2.display(); return 0; } ``` 在这个例子中,`MyClass` 的复制构造函数实现了深拷贝逻辑,确保 `obj1` 和 `obj2` 各自有其独立的动态分配整型变量。 --- #### 编译器生成的默认复制构造函数的特点 如果用户没有定义任何特殊形式的构造函数(如复制构造函数或移动构造函数),则编译器会自动合成一个默认版本。此默认版本会对所有非静态成员逐一进行位级复制。需要注意的是,一旦开发者提供了自定义的复制构造函数,编译器就不会再生成默认版本[^3]。 --- #### 示例分析:解决浅拷贝引发的问题 以下是一个典型的因浅拷贝而导致崩溃的例子以及如何修复它的解决方案: ```cpp // 原始代码存在浅拷贝问题 class StringWrapper { private: char* buffer; public: StringWrapper(const char* str) { size_t length = strlen(str); buffer = new char[length + 1]; strcpy(buffer, str); } ~StringWrapper() { delete[] buffer; } // 添加自定义复制构造函数以支持深拷贝 StringWrapper(const StringWrapper& other) { size_t length = strlen(other.buffer); buffer = new char[length + 1]; strcpy(buffer, other.buffer); } void printBuffer() const { cout << buffer << endl; } }; void testCopyConstructor() { StringWrapper original("Hello"); StringWrapper copied(original); original.printBuffer(); // 输出 Hello copied.printBuffer(); // 输出 Hello } int main() { testCopyConstructor(); return 0; } ``` 在此修正后的程序里,新增加了一个复制构造函数用来处理字符串缓冲区的深层复制过程,有效规避了原始方案中的隐患[^5]。 --- #### 总结 C++ 中的复制构造函数是非常重要的概念之一,尤其当涉及到复杂数据结构或者外部资源管理时更是如此。合理运用它可以保障程序的安全性和稳定性;反之,则容易造成难以追踪的错误。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值