导读:本期聚焦于小伙伴创作的《C++拷贝构造函数什么时候调用?对象复制场景全面汇总》,敬请观看详情。不少初学者写C++类时遇到过程序莫名多调用了一次构造函数的状况,根源往往在于没弄清拷贝构造的触发条件。拷贝构造本质是用已存在对象去初始化新对象,并非所有赋值都会引发。当函数以值传递接收对象、从函数返回局部对象、用同类型实例显式初始化新对象,以及编译器做隐式类型转换时,都会调用它。若类里包含指针成员却未自定义深拷贝,默认逐位复制会让多个对象指向同一块内存,析构时重复释放直接崩溃。理清这些场景,才能准确控制资源归属,避免浅拷贝陷阱,也能在需要移动语义时及时禁用或改写拷贝行为。

在C++中,拷贝构造函数是一种特殊的构造函数,其形参通常为同类型的常量引用。它的核心职责是用一个已存在的对象去构造一个全新的对象。很多运行时行为和资源管理问题,都和拷贝构造在何时被编译器自动调用紧密相关。理解这些触发点,是写出正确且高效C++代码的基础。

C++拷贝构造函数什么时候调用?对象复制场景全面汇总

一、拷贝构造函数的基本定义

拷贝构造函数的一般形式如下:ClassName(const ClassName& other)。如果程序员没有显式定义,编译器会合成一个默认版本,该版本执行成员逐一复制,也就是常说的浅拷贝。对于只包含基本类型或没有动态资源的类,默认拷贝构造通常够用;但一旦类内部通过new分配了堆内存,浅拷贝就会带来多个对象共享同一资源的问题。

下面给出一个最简单的自定义拷贝构造示例,用于观察调用时机:

#include <iostream>
using namespace std;

class Demo {
public:
    int val;
    Demo(int v = 0) : val(v) {
        cout << "普通构造函数被调用" << endl;
    }
    Demo(const Demo& other) : val(other.val) {
        cout << "拷贝构造函数被调用" << endl;
    }
};

int main() {
    Demo a(10);      // 普通构造
    Demo b(a);       // 拷贝构造:用a初始化b
    Demo c = a;      // 拷贝构造:定义时初始化,不是赋值
    return 0;
}

上面的代码中,Demo b(a)Demo c = a都会在对象定义阶段触发拷贝构造,而不仅仅是在等号出现时。这一点和普通的赋值运算符不同,后者不会调用拷贝构造。

二、函数传值引发的拷贝

当函数以值传递的方式接收类类型参数时,编译器必须根据实参创建一个副本,此时拷贝构造函数必然被调用。这是实际开发中最容易被忽略的场景之一,尤其是对象体积较大或构造代价较高时,会带来不必要的性能开销。

观察下面这段程序,函数printDemo的参数是值类型,每次调用都会发生一次拷贝:

#include <iostream>
using namespace std;

class Demo {
public:
    int val;
    Demo(int v = 0) : val(v) {}
    Demo(const Demo& other) : val(other.val) {
        cout << "拷贝构造:函数传值" << endl;
    }
};

void printDemo(Demo d) {
    cout << d.val << endl;
}

int main() {
    Demo a(5);
    printDemo(a);   // 实参a拷贝给形参d
    return 0;
}

如果希望避免这次拷贝,可以将参数改为常量引用const Demo&,这样不会创建新对象,也就不会调用拷贝构造。在接口设计中,对自定义类型优先使用引用传递是普遍共识。

三、函数返回对象时的拷贝

当函数返回局部对象且返回类型不是引用时,理论上需要把局部对象拷贝到调用处。不过现代C++编译器通常会实施返回值优化,省略掉这次拷贝。但在没有优化或优化无法生效的情况下,拷贝构造仍会被调用。

以下示例展示了返回局部对象的可能拷贝行为:

#include <iostream>
using namespace std;

class Demo {
public:
    int val;
    Demo(int v = 0) : val(v) {}
    Demo(const Demo& other) : val(other.val) {
        cout << "拷贝构造:返回对象" << endl;
    }
};

Demo createDemo() {
    Demo temp(20);
    return temp;   // 可能触发拷贝构造
}

int main() {
    Demo d = createDemo();
    return 0;
}

在C++11之后,如果类中定义了移动构造函数,且返回的是即将销毁的局部对象,编译器往往优先使用移动语义而非拷贝,效率更高。但若没有移动构造,且优化被关闭,就会回退到拷贝构造。

四、显式初始化与同类型赋值区别

用已有对象去声明并初始化另一个对象,属于拷贝构造场景;而对已经存在的对象使用等号,则是赋值运算,调用的是拷贝赋值运算符而非拷贝构造。两者容易混淆,但机制完全不同。

通过代码对比可以更清楚地看到差异:

#include <iostream>
using namespace std;

class Demo {
public:
    int val;
    Demo(int v = 0) : val(v) {}
    Demo(const Demo& other) : val(other.val) {
        cout << "拷贝构造" << endl;
    }
    Demo& operator=(const Demo& other) {
        val = other.val;
        cout << "拷贝赋值" << endl;
        return *this;
    }
};

int main() {
    Demo a(1);
    Demo b(a);     // 拷贝构造
    Demo c;
    c = a;         // 拷贝赋值,不调用拷贝构造
    return 0;
}

因此,判断是否调用拷贝构造,关键看新对象是否在定义的同时被同类型对象初始化。如果对象已经存在,后续的任何等号操作都只是赋值。

五、容器与算法中的拷贝

标准库容器如vector在扩容、插入元素时,也会大量使用拷贝构造。例如vector::push_back在容量不足时会重新分配内存,并把原有元素逐个拷贝到新空间。如果元素类型拷贝代价高,就会明显影响性能。

下面演示了vector扩容导致的拷贝:

#include <iostream>
#include <vector>
using namespace std;

class Item {
public:
    int id;
    Item(int i = 0) : id(i) {}
    Item(const Item& other) : id(other.id) {
        cout << "Item拷贝构造 id=" << id << endl;
    }
};

int main() {
    vector<Item> v;
    for (int i = 0; i < 3; i++) {
        v.push_back(Item(i));
    }
    return 0;
}

为了避免频繁拷贝,C++11引入了emplace_back,它直接在容器内存中构造对象,省去临时对象的拷贝。同时移动语义也让很多容器操作从拷贝转为移动,降低了开销。

六、浅拷贝风险与应对

当类中含有指针并指向动态内存时,默认拷贝构造只复制指针值,导致两个对象的指针指向同一块内存。任一对象析构后,另一对象再访问或析构就会出错。这种问题在拷贝构造被自动调用时尤其隐蔽。

正确的做法是实现深拷贝,或者在不需要复制时把拷贝构造和拷贝赋值设为私有并删除:

#include <iostream>
#include <cstring>
using namespace std;

class Buffer {
public:
    char* data;
    int size;
    Buffer(const char* s) {
        size = strlen(s) + 1;
        data = new char[size];
        strcpy(data, s);
    }
    // 深拷贝拷贝构造
    Buffer(const Buffer& other) {
        size = other.size;
        data = new char[size];
        strcpy(data, other.data);
    }
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer a("hello");
    Buffer b(a);   // 深拷贝,各自拥有独立内存
    return 0;
}

如果类代表唯一资源拥有者,比如文件句柄管理类,应明确禁用拷贝,使用= delete修饰拷贝构造和赋值运算符,从语言层面防止误用。

七、总结各类复制场景

归纳起来,拷贝构造主要在以下情况被调用:用同类型对象显式初始化新对象、函数以值方式传参、函数返回非引用局部对象、标准库容器内部元素复制、以及某些隐式类型转换。掌握这些触发条件,有助于在设计中合理控制资源,也能结合移动语义减少不必要的性能损耗。

在实际工程中,建议为所有管理资源的类显式定义拷贝行为,或明确删除拷贝接口,同时使用引用传递和emplace系列接口来规避多余复制,从而保证程序既安全又高效。

copy_constructorC++object_copy修改时间:2026-08-03 09:45:43

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。