首页 > 后端开发 > C++ > 正文

C++构造函数有哪些 默认拷贝移动构造函数

P粉602998670
发布: 2025-08-20 13:17:01
原创
307人浏览过
答案:C++构造函数包括普通、默认、拷贝和移动构造函数,分别用于初始化、默认创建、复制和移动对象。默认构造函数在无自定义构造函数时由编译器生成,否则需用= default显式声明;拷贝构造函数处理对象复制,需避免浅拷贝导致的资源冲突;移动构造函数通过转移资源提升性能,使用std::move触发。= delete可禁用特定构造函数以防止不期望的操作。理解这些机制对资源管理、性能优化和代码正确性至关重要。

c++构造函数有哪些 默认拷贝移动构造函数

C++中,构造函数是类的一个特殊成员函数,它在对象创建时被自动调用,主要用于初始化对象。我们通常会遇到几种类型的构造函数:用户自定义的普通构造函数(包括带参数和不带参数的),以及编译器可能自动生成或我们需要显式定义的特殊成员函数——默认构造函数、拷贝构造函数和移动构造函数。它们各自在对象的生命周期管理中扮演着不可或缺的角色,尤其是在资源管理和性能优化方面。

解决方案

谈到C++的构造函数,这东西真是对象生命周期的起点,也是许多设计模式和资源管理(比如RAII)的基石。我个人觉得,真正理解它们,尤其是那几个“特殊”的,才能写出健壮且高效的代码。

首先是普通构造函数,这可能是大家最熟悉的。你写一个类,通常会给它定义一个或多个构造函数来初始化成员变量。它们可以带参数,也可以不带参数。比如:

class MyClass {
public:
    int value;
    std::string name;

    // 无参数构造函数
    MyClass() : value(0), name("default") {
        // 简单初始化
    }

    // 带参数构造函数
    MyClass(int v, const std::string& n) : value(v), name(n) {
        // 使用初始化列表是好习惯
    }
};
登录后复制

接着是默认构造函数。这个有点意思,它是指那种不需要任何参数就能构造对象的构造函数。如果你没给类定义任何构造函数,编译器通常会“好心”地为你生成一个公共的、无参数的默认构造函数。但一旦你定义了任何一个其他的构造函数(比如带参数的),那么编译器就不会再自动生成这个默认构造函数了。这常常是新手容易踩的坑,导致

MyClass obj;
登录后复制
这样的代码编译不过去。

立即学习C++免费学习笔记(深入)”;

然后是拷贝构造函数。它的签名通常是

ClassName(const ClassName& other)
登录后复制
。顾名思义,它用来“拷贝”一个已存在的对象来创建另一个新对象。这在很多场景下都会被调用:比如你用一个对象去初始化另一个对象 (
MyClass obj2 = obj1;
登录后复制
),或者函数参数按值传递 (
void func(MyClass obj)
登录后复制
),再或者函数返回一个对象 (
MyClass func() { return MyClass(); }
登录后复制
)。这里面最让人头疼的就是深拷贝和浅拷贝的问题。如果你的类管理着堆内存或其他资源,那么简单的成员变量复制(浅拷贝)往往会带来双重释放的灾难。

最后,也是C++11引入的重磅成员——移动构造函数。它的签名通常是

ClassName(ClassName&& other)
登录后复制
。这个函数是为了解决拷贝构造函数在某些场景下效率低下的问题。想象一下,如果你有一个大对象,里面包含一大块堆内存,每次拷贝都要重新分配内存并复制数据,这开销可不小。移动构造函数允许你“窃取”源对象的资源,而不是复制。它把源对象的内容转移过来,然后把源对象置于一个有效但未指定的状态(通常是清空资源指针)。这大大提升了处理临时对象或右值时的性能。
std::move
登录后复制
就是用来将左值转换为右值引用,从而触发移动语义的。

有时我们还需要显式控制这些特殊成员函数。C++提供了

= default
登录后复制
= delete
登录后复制
语法。
= default
登录后复制
可以强制编译器生成一个默认的特殊成员函数(比如即使你定义了其他构造函数,也想保留默认构造函数)。而
= delete
登录后复制
则可以显式地禁用某个特殊成员函数,比如你不想让你的类可以被拷贝,就可以
ClassName(const ClassName&) = delete;
登录后复制
。这对于设计单例模式或者限制对象行为非常有用。

为什么理解C++构造函数如此重要?

深入理解C++构造函数,尤其是那几个特殊的,在我看来,是掌握C++对象生命周期管理的关键。这不仅仅是语法层面的东西,它直接关系到你代码的健壮性、资源管理效率,甚至是程序的性能瓶颈。

首先,资源管理。C++不像Java或Python那样有垃圾回收机制,内存和其他系统资源(文件句柄、网络连接等)的释放完全依赖程序员。构造函数是资源获取的理想场所,而析构函数则是资源释放的理想场所,这正是RAII(Resource Acquisition Is Initialization)原则的核心。如果你对拷贝构造函数或移动构造函数理解不到位,很可能导致资源泄漏、重复释放,甚至野指针问题。比如,一个简单的浅拷贝就可能让两个对象共享同一块堆内存,当其中一个对象析构时释放了这块内存,另一个对象再尝试访问或释放时,程序就崩溃了。

其次,性能优化。特别是移动构造函数的引入,彻底改变了我们处理大型对象或临时对象的方式。在C++11之前,许多返回大对象的函数都面临着巨大的拷贝开销。有了移动语义,我们不再需要昂贵的深拷贝,而是直接转移资源的所有权,这对于高性能计算和处理大数据量的应用来说,是质的飞跃。如果你不理解移动语义,可能会在不知不觉中写出很多低效的代码,或者为了避免拷贝而采取一些不必要的复杂设计。

再者,正确性与可维护性。一个设计良好的类,其构造函数应该清晰地表达对象的创建意图和初始状态。如果你没有正确地定义或禁用某些构造函数,可能会允许用户以你意想不到的方式创建对象,从而引入潜在的bug。例如,一个不允许拷贝的类,如果其拷贝构造函数没有被

= delete
登录后复制
,就可能在某个地方被隐式调用,导致逻辑错误。理解这些特殊成员函数的行为,能让你更好地控制类的行为,提升代码的可预测性和可维护性。在我看来,这就像是给你的类设定了“规矩”,让它在各种场景下都能按你期望的方式工作。

默认构造函数何时“消失”?以及如何显式控制?

默认构造函数这个概念,初学时确实容易让人有点迷糊。它不是你写出来的,而是编译器“送”给你的。但这份“好意”是有条件的,一旦你打破了它的条件,这份“礼物”就会消失。

具体来说,当你在类中定义了任何一个构造函数(无论是带参数的还是不带参数的),编译器就不会再为你自动生成那个公共的、无参数的默认构造函数了。举个例子:

class MyData {
public:
    int id;
    // MyData() { id = 0; } // 如果我写了这一行,编译器就不会生成默认构造函数

    MyData(int i) : id(i) {} // 定义了一个带参数的构造函数
};

// 尝试创建对象
// MyData d1; // 编译错误!因为MyData(int)的存在,编译器不会再生成MyData()
MyData d2(10); // OK
登录后复制

这种“消失”往往会导致一个常见的问题:你可能希望能够无参数地创建对象,但因为你定义了其他构造函数,这个能力就被剥夺了。

那么,如何显式地控制它呢?C++11引入的

= default
登录后复制
= delete
登录后复制
语法就是为此而生的。

即构数智人
即构数智人

即构数智人是由即构科技推出的AI虚拟数字人视频创作平台,支持数字人形象定制、短视频创作、数字人直播等。

即构数智人 36
查看详情 即构数智人

如果你希望即使定义了其他构造函数,也强制编译器生成默认构造函数,你可以这样做:

class MyData {
public:
    int id;

    MyData() = default; // 显式请求编译器生成默认构造函数
    MyData(int i) : id(i) {}
};

MyData d1; // OK,现在可以无参数创建了
MyData d2(10); // OK
登录后复制

= default
登录后复制
不仅适用于默认构造函数,也适用于拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。它告诉编译器:“嘿,按你默认的方式生成这个特殊成员函数吧!”这在某些情况下非常有用,比如当你有一个复杂的类,并且你希望编译器为你生成默认的移动构造函数,而不是自己手动实现时。

反过来,如果你想显式禁用默认构造函数,或者任何其他特殊成员函数,你可以使用

= delete
登录后复制

class NonDefaultConstructible {
public:
    int value;
    NonDefaultConstructible(int v) : value(v) {}

    NonDefaultConstructible() = delete; // 显式禁用默认构造函数
};

// NonDefaultConstructible obj; // 编译错误!不能无参数构造
NonDefaultConstructible obj2(100); // OK
登录后复制

使用

= delete
登录后复制
可以明确地表达你的设计意图,防止用户以你不想允许的方式构造、拷贝或移动对象。这对于实现单例模式、不可拷贝的类(如
std::unique_ptr
登录后复制
)或限制对象创建方式非常有效。这两种机制给了我们对特殊成员函数行为更细粒度的控制,让类设计的意图更加清晰。

拷贝构造函数和移动构造函数:何时选用与陷阱?

拷贝构造函数和移动构造函数,它们都涉及“复制”对象,但在底层机制和适用场景上有着本质的区别。理解它们何时被调用以及各自的陷阱,是写出高效且无错C++代码的关键。

拷贝构造函数:创建副本

  • 何时选用? 当你需要一个现有对象的独立副本时,或者说,你希望新对象拥有与源对象完全相同的数据,但两者之间没有共享资源,互不影响。典型的场景包括:

    • 用一个对象初始化另一个新对象:
      MyClass newObj = oldObj;
      登录后复制
      MyClass newObj(oldObj);
      登录后复制
    • 函数参数按值传递:
      void process(MyClass obj);
      登录后复制
    • 函数返回一个对象:
      MyClass createObject();
      登录后复制
      (尽管现代C++编译器通常会进行RVO/NRVO优化,避免实际的拷贝)
    • 容器操作,如
      std::vector
      登录后复制
      扩容时可能需要拷贝元素。
  • 陷阱:浅拷贝问题。这是最常见的坑。如果你的类内部管理着堆内存或其他资源(比如文件句柄、网络连接),而你没有自定义拷贝构造函数,那么编译器生成的默认拷贝构造函数只会进行成员变量的按位复制(浅拷贝)。这会导致两个对象指向同一块资源。

    class ResourceHolder {
    public:
        int* data;
        ResourceHolder(int size) : data(new int[size]) { /* ... */ }
        ~ResourceHolder() { delete[] data; } // 析构函数释放内存
        // 默认拷贝构造函数:只复制data指针,不复制指针指向的内容
    };
    
    ResourceHolder r1(10);
    ResourceHolder r2 = r1; // 浅拷贝!r1.data 和 r2.data 指向同一块内存
    
    // 当 r2 析构时,释放了 data 指向的内存
    // 当 r1 析构时,再次尝试释放同一块内存 -> 双重释放,程序崩溃!
    登录后复制

    为了避免这个问题,你需要提供一个深拷贝的拷贝构造函数,即在构造新对象时,为新对象分配独立的资源,并将源对象的内容复制过来:

    class ResourceHolder {
    public:
        int* data;
        int size;
        ResourceHolder(int s) : size(s), data(new int[s]) { /* ... */ }
        ~ResourceHolder() { delete[] data; }
    
        // 深拷贝构造函数
        ResourceHolder(const ResourceHolder& other) : size(other.size) {
            data = new int[size];
            std::copy(other.data, other.data + size, data); // 复制内容
        }
    };
    登录后复制

移动构造函数:转移所有权

  • 何时选用? 当你不再需要源对象,或者源对象是一个即将销毁的临时对象(右值),并且你希望将源对象的资源转移给新对象,而不是复制一份。这是一种“窃取”资源的优化策略,避免了昂贵的内存分配和数据复制。典型的场景包括:

    • 从函数返回一个临时对象(通常与RVO/NRVO结合,但移动语义是更通用的备选)。
    • 使用
      std::move
      登录后复制
      显式地将左值转换为右值引用,以触发移动语义:
      MyClass newObj = std::move(oldObj);
      登录后复制
    • 在容器操作中,比如
      std::vector::push_back
      登录后复制
      插入右值时,如果元素类型支持移动构造,会优先使用移动而不是拷贝。
  • 优势:性能提升。对于包含大量数据或管理堆内存的类,移动构造函数可以显著减少资源分配和数据复制的开销。它将源对象的内部指针直接赋给新对象,然后将源对象的指针置空,这样就避免了重新分配内存和复制数据的过程,大大提高了效率。

    class ResourceHolder {
    public:
        int* data;
        int size;
        // ... 构造函数、析构函数、拷贝构造函数同上
    
        // 移动构造函数
        ResourceHolder(ResourceHolder&& other) noexcept : data(other.data), size(other.size) {
            other.data = nullptr; // 源对象指针置空
            other.size = 0;       // 源对象大小置0,确保其处于有效但空的状态
        }
    };
    登录后复制
  • 陷阱:源对象状态。移动操作之后,源对象的内容被“掏空”了。虽然它仍然是一个有效的对象(你可以安全地调用它的析构函数),但它的状态已经不再是原始状态。你不能再指望它拥有原来的资源或数据。如果你在移动后仍然尝试使用源对象,可能会导致未定义行为或逻辑错误。因此,在移动操作后,最好不要再依赖源对象的数据,除非你明确知道它已经被重置到了一个安全的状态。

总结来说,拷贝构造函数用于创建独立的副本,适用于需要保持源对象不变的场景;而移动构造函数用于转移资源所有权,适用于源对象不再需要或即将销毁,且追求性能优化的场景。正确地选择和实现它们,是C++内存管理和性能调优的重要一环。

以上就是C++构造函数有哪些 默认拷贝移动构造函数的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号