拷贝构造函数和移动构造函数

目录

1. 拷贝构造函数和移动构造函数概念

2. 拷贝构造函数和移动构造函数调用时机

2.1 移动构造函数通常在以下情况被使用:

2.2 拷贝构造函数通常在以下情况被使用:

 2.3 如果没有移动构造函数呢 右值调用拷贝构造吗?            

 2.4 移动构造默认生成的条件?

2.5 拷贝构造默认生成的条件      

3. 为什么有移动构造函数?

4. 拷贝构造函数为什么使用const?

5. 非临时对象可以调用移动构造函数吗?

6. 返回局部对象和拷贝构造返回局部对象的区别


开始本篇文章前,请思考一个问题,一个空类默认生成哪些函数?

        在C++中,如果你定义了一个空类,编译器会为此类默认生成以下特殊成员函数:

1. 默认构造函数 (`Default Constructor`): 用于创建类的对象。如果没有定义任何构造函数,编译器会生成一个默认构造函数。

2. 拷贝构造函数 (`Copy Constructor`): 接收同类的另一个对象的引用,用于通过已存在的对象来初始化新对象的成员。

3. 拷贝赋值操作符 (`Copy Assignment Operator`): 用于将一个对象的内容复制到另一个已经存在的对象中。

4. 移动构造函数 (`Move Constructor`): C++11 引入。如果可能,用于将一个对象的资源“移动”到新创建的对象中,而非复制。

5. 移动赋值操作符 (`Move Assignment Operator`): C++11 引入。用于将一个对象的资源转移给另一个已经存在的对象。

6. 析构函数 (`Destructor`): 当对象的生命周期结束时被调用,用于执行清理工作,如释放资源。

        编译器只会在这些函数被需要时才生成它们。这意味着如果从未用到某个特殊成员函数,编译器可能不会实际生成它。例如,如果你从未复制过对象,编译器可能就不会生成拷贝构造函数和拷贝赋值操作符。

        这些自动生成的函数是公有的(`public`)和非虚的(`non-virtual`),除了析构函数,如果一个类是基类的话,析构函数将是虚的(`virtual`)。还要注意的是,如果为类定义了一个或多个构造函数,编译器将不会生成默认构造函数,需要显式定义它

        从C++11开始,可以使用`= default;`和`= delete;`来显式地要求编译器默认生成或禁用这些函数。如:

class MyClass {
public:
    MyClass() = default; // 显式要求编译器生成默认构造函数
    MyClass(const MyClass&) = delete; // 禁用拷贝构造函数
    // 其他成员...
};

        为什么要说明是一个空类,因为移动构造函数默认生成的条件,相比较于构造函数会更加的苛刻,稍后会做出解答。

1. 拷贝构造函数和移动构造函数概念

        在C++中,拷贝构造函数和移动构造函数都是用于创建一个新对象作为另一个对象的副本的构造函数,但它们在处理副本的方式上存在根本性的差异。

        拷贝构造函数:是一种特殊的构造函数,它用于根据同类型的另一个对象来初始化新对象的成员。拷贝构造函数通常接受一个对源对象的引用作为参数,并且这个引用通常是常量引用,以确保源对象不会被修改。

class MyClass {
public:
    int* data;

    // 普通构造函数
    MyClass(int value) {
        data = new int(value);
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data); // 分配新的内存并拷贝数据
    }

    // 析构函数
    ~MyClass() {
        delete data;
    }
};

        在这个例子中,拷贝构造函数分配了新的内存,并从`other`对象拷贝了数据,确保了深拷贝的进行,这样每个对象都有其自己的内存副本。

        移动构造函数:是C++11中引入的,用于支持移动语义。移动构造函数允许将资源(如动态分配的内存)从一个(临时)对象转移到另一个对象,而不是创建资源的副本。这通常更快,因为它避免了不必要的拷贝。移动构造函数接受一个右值引用作为参数

移动语义:移动语义是C++11中引入的一个概念,它允许资源(如动态内存、文件句柄、网络连接等)在对象之间转移,而不是复制。(稍后会有文章专门讲左值右值和移动语义完美转发)

class MyClass {
public:
    int* data;

    // 普通构造函数
    MyClass(int value) {
        data = new int(value);
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        data = other.data; // 直接接管other对象的资源
        other.data = nullptr; // 将other对象的指针置为空,避免析构释放内存
    }

    // 析构函数
    ~MyClass() {
        delete data;
    }
};

        在这个例子中,移动构造函数接管了`other`对象的资源,而`other`对象的指针被设置为`nullptr`,这样当`other`对象被销毁时,它的析构函数不会释放已经被当前对象接管的内存。

        拷贝构造函数和移动构造函数都可以被隐式调用,例如在函数参数传递、返回值、抛出异常时,或者用于初始化和赋值。但是,自C++11起,当可能的时候,编译器会优先使用移动构造函数,因为它通常提供更好的性能。

2. 拷贝构造函数和移动构造函数调用时机

2.1 移动构造函数通常在以下情况被使用:

        要注意的是,移动构造函数只有在对象是右值时才会被默认调用。右值是指临时的、无名的或即将被销毁的对象。左值则指持久的或有名的对象。

        1. 返回局部对象:当函数返回一个局部对象时,如果该对象的类型支持移动语义,编译器可能会使用移动构造函数来构造返回值,这是为了优化性能,避免不必要的拷贝。

MyClass createMyClass() {
    MyClass localObj;
    // ... 对localObj进行操作
    return localObj; // 可能会利用移动构造函数
}

        2. 对象初始化:使用花括号或等号初始化对象时,如果提供的是一个临时对象(右值),编译器可能会使用移动构造函数来初始化对象。

MyClass obj = MyClass(); // 使用临时对象初始化,可能会调用移动构造函数

        3. 容器操作:当将对象插入到一个容器中,如`std::vector`,当发生重新分配以容纳更多元素时,容器会使用移动构造函数来转移元素,而不是拷贝元素。

std::vector<MyClass> vec;
vec.push_back(MyClass()); // 插入一个临时对象,可能会调用移动构造函数

// 如果需要重新分配,已存在的元素可能会通过移动构造函数来转移

        4. 标准库算法:一些标准库算法,如`std::move`,可以将对象转换为右值,以便使用移动构造函数。

std::vector<MyClass> vec1, vec2;
// ... 假设vec1已经填充了数据
std::move(vec1.begin(), vec1.end(), std::back_inserter(vec2)); // 使用移动迭代器

        5. 异常处理:在异常处理中,当异常被抛出时,异常对象通常会被移动而不是被拷贝,以提高性能。

try {
    throw MyClass(); // 抛出一个临时对象,可能通过移动构造函数传递
} catch (MyClass obj) {
    // 异常对象obj可能是通过移动构造函数构造的
}

        6. 使用std::move:当显式地使用`std::move`来将对象标记为可移动时,可以强制使用移动构造函数。

MyClass a;
MyClass b = std::move(a); // 明确地使用移动构造函数

        7. 交换操作:在执行对象交换(如`std::swap`)时,如果对象支持移动构造函数,它将通过移动构造(以及移动赋值)来交换对象的内容。

MyClass a, b;
// ... 对a和b进行操作
std::swap(a, b); // 如果MyClass支持移动构造函数,则可能会使用它进行交换
2.2 拷贝构造函数通常在以下情况被使用:

        在C++中,拷贝构造函数是一个特殊的构造函数,它初始化一个对象作为另一个同类型对象的副本。拷贝构造函数的使用场景包括:

        1. 显式复制:
           当显式地使用一个已有对象来初始化一个新对象时,拷贝构造函数会被调用。例如:

 MyClass obj1;
 MyClass obj2 = obj1;  // 使用 obj1 初始化 obj2,调用拷贝构造函数

        2. 函数参数传递:
           当对象作为值传递给函数时,拷贝构造函数会被用来创建函数参数的副本。

void function(MyClass obj); // 函数参数按值传递
MyClass obj1;
function(obj1);  // 调用 function 时,使用 obj1 的拷贝构造函数

        3. 函数返回值:
           当函数返回一个对象时,拷贝构造函数用于从函数返回值创建对象。

 MyClass function() {
    MyClass obj;
    return obj;  // 返回 obj,可能调用拷贝构造函数,但通常会被优化
}

        这里会发现和移动构造函数返回对象一致,那编译器是如何分辨调用的是哪一个函数呢?请见目录6(返回局部对象和拷贝构造返回局部对象的区别)

        4. 从函数抛出异常:
           当对象作为异常被抛出时,拷贝构造函数用于创建异常对象的副本。

 void function() {
     MyClass obj;
     throw obj;  // 抛出 obj 时,调用拷贝构造函数
}

        5. 初始化数组或容器元素:
           当创建对象数组或将对象放入支持复制语义的容器时,例如`std::vector`,拷贝构造函数会用来初始化各个元素。

  MyClass array[3] = {obj1, obj2, obj3}; // 初始化数组,对每个元素调用拷贝构造函数
  std::vector<MyClass> vec;
  vec.push_back(obj1); // 将 obj1 插入到向量中,调用拷贝构造函数

        6. 对象赋值:
           虽然通常使用拷贝赋值运算符来处理对象的赋值操作,但如果赋值发生在变量初始化时,将使用拷贝构造函数。例如:

   MyClass obj1;
   MyClass obj2 = obj1; // 初始化时调用拷贝构造函数
   obj2 = obj1;         // 已经初始化后的赋值,调用拷贝赋值运算符
 2.3 如果没有移动构造函数呢 右值调用拷贝构造吗?            

        如果一个类没有提供移动构造函数,且其资源需要通过复制来转移,那么当尝试以移动语义来构造或赋值一个对象时(例如,使用`std::move`或返回一个局部对象),编译器将会退回到使用拷贝构造函数(如果提供了的话)。

        这是因为移动构造函数和拷贝构造函数都是对象初始化的重载版本。移动构造函数接受一个右值引用(通常是类型为`T&&`),而拷贝构造函数接受一个常量左值引用(通常是类型为`const T&`)。如果没有可用的移动构造函数,编译器会查找能否使用拷贝构造函数来实现初始化。

        这种回退到拷贝构造函数的行为可能不会导致编译时错误,但它将会导致性能上的损失,特别是对于那些拥有大量资源需要复制的对象。这就是在设计资源密集型的类时,实现移动构造函数和移动赋值操作符通常是一个好的实践,这样可以确保当对象作为右值时,资源能以更高效的方式转移,而不是进行成本较高的复制操作。

 2.4 移动构造默认生成的条件?

        在C++11及以后的版本中,移动构造函数可以被编译器默认生成,但这只会在某些特定条件下发生。编译器会默认生成移动构造函数当且仅当以下条件都满足:

1. 类没有声明任何拷贝构造函数、拷贝赋值操作符或析构函数。
2. 类没有声明任何移动赋值操作符。
3. 类的所有非静态数据成员和基类都可以被移动构造或没有移动构造函数被删除(`delete`)。

        如果上述条件中的任何一个不满足,编译器将不会自动生成移动构造函数。在这种情况下,如果需要移动语义,必须自己显式地声明和定义一个移动构造函数。

        此外,如果显式地声明了一个拷贝构造函数或拷贝赋值操作符,甚至是一个析构函数,编译器将不会自动生成移动构造函数和移动赋值操作符,因为这被视为以特定方式管理资源。在这种情况下,即使类的成员和基类都是可移动的,编译器也不会默认生成移动操作。

        如果想要编译器为类生成默认的移动操作,可以使用`= default;`语句来显式地告知编译器:

class MyClass {
public:
    MyClass(MyClass&&) = default; // 显式地要求编译器生成默认移动构造函数
    MyClass& operator=(MyClass&&) = default; // 显式地要求编译器生成默认移动赋值操作符
    // ...
};

        使用`= default;`的优点是即便声明了析构函数或其他构造函数,也可以指示编译器为类生成移动操作。这样可以保持默认的移动语义,同时允许自定义类的其他方面。

2.5 拷贝构造默认生成的条件      

        如果没有为类显式声明拷贝构造函数,C++ 编译器会默认生成一个拷贝构造函数。这个隐式的拷贝构造函数会逐个拷贝对象的非静态成员,使用其各自的拷贝构造函数来进行拷贝。

以下是隐式拷贝构造函数的行为:

- 对于基本数据类型的成员,它会进行简单的位复制。
- 对于类类型的成员,它会调用相应成员的拷贝构造函数。
- 对于数组类型的成员,它会逐一对数组中的每个元素调用拷贝构造函数。
- 对于继承的基类部分,它会调用基类的拷贝构造函数。

        隐式拷贝构造函数只会在没有提供任何构造函数时生成。如果已经声明了其他的构造函数(比如默认构造函数或参数化构造函数),但没有声明拷贝构造函数,编译器仍然会生成一个隐式拷贝构造函数。

请注意,有些情况下编译器不会生成隐式拷贝构造函数:

- 如果显式地声明了移动构造函数或移动赋值运算符,编译器将不会自动生成拷贝构造函数。
- 如果类中有成员变量是不可拷贝的(例如,如果成员变量的类型有删除的或不可访问的拷贝构造函数),编译器也不会生成默认的拷贝构造函数。

3. 为什么有移动构造函数?

        从上面讲述大家可以发现,移动构造函数的存在减少了不必要的对象拷贝,提高性能。

4. 拷贝构造函数为什么使用const?

        1. 保护源对象:使用`const`引用可以确保传递给拷贝构造函数的源对象不会被修改,可以避免在复制对象时意外改变源对象。

        2. 使得能够拷贝临时对象:临时对象(例如函数返回的对象)不能绑定到非`const`引用上,但可以绑定到`const`引用。如果拷贝构造函数接受一个非`const`引用,那么它就不能用于拷贝临时对象。例如:

 MyClass a;
 MyClass b = a; // a 是一个左值,可以绑定到 const 引用
 MyClass c = MyClass(); // MyClass() 产生一个右值,只能绑定到 const 引用

        3. 允许拷贝`const`对象:如果拷贝构造函数接受一个非`const`引用,将无法从一个`const`对象创建一个新对象。例如:

const MyClass a;
MyClass b = a; // 只有当拷贝构造函数接受 const 引用时,这行代码才有效

        4. 接口一致性:如果一个成员函数不打算修改对象,最好将参数声明为`const`。这使得类的接口更加清晰,用户可以了解哪些函数可以修改对象,哪些不会。

5. 非临时对象可以调用移动构造函数吗?

        非临时对象(或称为左值)也可以调用移动构造函数,但这不是自动发生的。在C++中,移动语义是为了优化临时对象(右值)的资源转移,因此,当使用临时对象或显式标记为右值的对象时,移动构造函数会自动被调用。

        但如果想对一个左值对象使用移动构造函数,需要使用`std::move`函数来将左值显式转换为右值引用,从而触发移动语义。`std::move`实际上并不移动任何东西,它只是将一个左值转换为一个右值引用,这样就可以使用移动构造函数或移动赋值运算符了。


#include <utility> // For std::move

class MyClass {
public:
    // ...

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        // 实现移动逻辑
    }

    // ...
};

MyClass a;
MyClass b(std::move(a)); // 使用std::move显式调用移动构造函数

        在上面的代码段中,尽管`a`是一个左值,通过`std::move(a)`我们可以调用`b`的移动构造函数来初始化它。务必注意,一旦进行了`std::move`操作,原对象`a`将处于一个有效但不确定的状态,不能再假设它包含有用的数据,唯一可以安全做的只是销毁它或者给它赋予新的值。因此,在使用`std::move`时应该非常小心,确保之后不再使用被移动的对象,或者立即给它赋予一个新的值以避免悬挂状态的发生。

6. 返回局部对象和拷贝构造返回局部对象的区别

        返回局部对象时,如果对象的类型支持移动语义,编译器可能会使用移动构造函数而不是拷贝构造函数来构造返回值。这主要是出于性能优化的考虑。

class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        // 这里会复制other到当前对象
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        // 这里会移动other到当前对象,通常是窃取资源
    }
};

MyClass createObject() {
    MyClass localObj;
    // ... 对localObj进行操作
    return localObj; // 返回局部对象
}

        在这个例子中,当`createObject()`函数被调用时:

1. 如果没有启用(或没有可用的)移动语义,将调用拷贝构造函数。
2. 如果启用了移动语义,且`MyClass`类型支持它,将调用移动构造函数。

        从C++11开始,编译器还会尝试在可能的情况下应用返回值优化(Return Value Optimization, RVO)或命名返回值优化(Named Return Value Optimization, NRVO),这可以进一步消除拷贝和移动。当RVO或NRVO被应用时,局部对象会直接在调用者的环境中构造,从而避免了拷贝或移动的需要。

        从C++17起,RVO已经成为语言规范的一部分,这意味着如果直接返回一个局部变量,编译器必须省略拷贝或移动操作,即使没有定义移动构造函数。但是如果返回的是一个条件表达式或者函数调用产生的临时值,那么移动构造函数可能会被调用,除非也能通过优化来省略。、

      `MyClass`类型支持它意味着该类型(在这个例子中是`MyClass`)实现了移动构造函数和/或移动赋值运算符。这些特殊的成员函数允许对象的资源(如动态分配的内存、文件句柄、套接字连接等)从一个即将被销毁的对象(通常是一个右值)转移至另一个对象,而无需进行资源的复制。

        移动构造函数通常具有以下形式,并且必须标记为`noexcept`来保证它不会抛出异常,这样编译器在某些情况下才会优先选择移动操作:

class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        // 将资源从other转移到this对象
    }
    // ...
};

        此构造函数接受一个右值引用参数(`MyClass&& other`),允许函数体内部从`other`“移动”资源到新创建的对象。

移动赋值运算符则允许将一个对象中的资源转移给另一个已经存在的对象:

要使`MyClass`支持移动语义,需要:

1. 定义移动构造函数和/或移动赋值运算符。
2. 确保它们被标记为`noexcept`(这不是必须的,但如果它们可能抛出异常,编译器可能不会在需要保证异常安全的上下文中使用它们)。
3. 确保类中所有资源可以安全地移动,并且移动后,源对象仍然保持一个有效但未定义的状态。

        如果类中的成员也支持移动语义(例如,它们有自己的移动构造函数和移动赋值运算符),那么类的移动操作将会调用成员的移动操作。如果类中的成员不支持移动,但支持拷贝,那么在移动操作期间,这些成员将会被拷贝。

        最后,如果没有为类定义移动操作,但定义了拷贝操作(并且成员也可以被拷贝),那么编译器会使用拷贝构造函数和拷贝赋值运算符来代替移动操作。如果类的成员或基类中有一个是不可移动的,那么类本身也会被视为不可移动的,除非显式定义了移动操作来处理这些特殊情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值