C++“=“操作符背后运行了哪些的代码

        C++的 “=” 实际有很多陷阱。对于内置类型来说,“=” 并没有什么特别。C背景的C++开发者,因为先入为主的印象,在类对象的使用上,仍然遵循了内置类型的方法,因此导致非常普遍的误用。

0 引子

“=” 名称

赋值运算符

“=” 功能

在不同的场景下被赋予不同的功能。包含初始化、赋值、复制、构造、移动

“=”可以操作的类型

  • 内置类型
  • 类类型

1 语义

对于上述2中类型的使用场景进行语义分析,可以形成如下表格

初始化

复制初始化

复制

移动初始化

移动

内置类型

初始化

初始化

赋值

初始化

赋值

类类型

构造函数

拷贝构造

拷贝重载

移动构造

移动重载

  • 内置类型

对于内置类型来说没有什么特殊,仅有两种语义初始化和赋值。

  • 类类型

#include <iostream>

class A{
public:  
    explicit A(int a) {
        std::cout << "ctor" << std::endl;
    };

    A(const A& a)
    {
        std::cout << "copy ctor" << std::endl;
    }    

    A& operator=(const A& a) {
        std::cout << "copy operator =" << std::endl;
        return *this;
    }

    A(A&& a)noexcept 
    {
        std::cout << "move ctor" << std::endl;
    }

    A& operator=(A&& a) noexcept {
        std::cout << "move operator =" << std::endl;
        return *this;
    }
};

int main()
{
    auto a0 = A(0);  // 构造
    auto a1 = a0;    // 拷贝构造
    a0 = a1;        // 拷贝
    auto a3 = std::move(a0); // 移动构造
    a3 = std::move(a1); // 移动
    return 0;
}

2 效率

        移动语义是在C++11标准中引入的,它的出现是为了解决传统的复制操作在性能上可能存在的问题。在C++之前,通过复制构造函数和拷贝赋值运算符来进行复制操作,这对于大型对象或者资源密集型对象来说可能会导致性能损失。

        让我们用一个简单的类来进行说明,这个类内部包含一个动态分配的资源,比如一个指向动态分配数组的指针。假设有一个类 Resource:

class Resource {
private:
    int* data;
public:
    Resource() : data(nullptr) {}
    Resource(int size) : data(new int[size]) {}
    ~Resource() { delete[] data; }
    // 移动构造函数
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr; // 避免资源被释放两次
    }
};

现在我们来比较传统的值语义和移动语义的操作:

  •  传统的值语义

        对象的复制意味着需要分配新的内存,并将数据复制到新的内存空间中。在传统的值语义中,每次复制都会导致动态分配资源的重新分配和复制,这可能会导致性能损失。

  •  移动语义

        对象的移动意味着将资源的所有权从一个对象转移到另一个对象,而不是复制资源。移动语义通过将指针从一个对象转移到另一个对象,而不是对数据进行复制,避免了不必要的资源分配和复制操作,从而提高了性能。

        在移动语义中,资源的指针被简单地转移给目标对象,而源对象不再拥有资源,这样就避免了资源的复制和额外的内存分配。这种转移是通过移动构造函数和移动赋值运算符来实现的。

3 = default 表达式

        在C++中,当我们声明一个类的构造函数、析构函数、拷贝构造函数、拷贝赋值运算符或移动构造函数时,可以使用= default语法来指示编译器生成默认的实现。

对于拷贝构造函数和拷贝赋值运算符:

        当我们声明它们为= default时,编译器会自动生成逐成员复制的拷贝构造函数和拷贝赋值运算符。这意味着它们的行为与默认情况下编译器生成的行为完全相同。

对于移动构造函数:

        当我们声明移动构造函数为= default时,编译器会生成默认的移动构造函数,这个默认移动构造函数会将成员变量逐个地移动(使用std::move)。

        使用= default的主要好处是可以让编译器为我们自动生成对应的特殊成员函数,避免手动编写这些函数。这在很多情况下能够简化代码,并且保证了正确性,因为编译器生成的函数通常都是符合预期的。

4 自定义与空实现

        如果你提供了一个空的移动构造函数,即使成员类提供了自定义的移动构造函数,它们也不会被调用。空的移动构造函数不会触发自动成员移动,而是会进行成员的逐位拷贝。这就是为什么在实践中,如果你的类需要移动语义,但不需要对成员进行特殊处理时,你可以使用= default来生成默认的移动构造函数,这将确保成员按照它们的移动构造函数进行移动。

#include <iostream>

class MemberClass {
public:
    MemberClass() {}
    MemberClass(MemberClass&& other) {
        std::cout << "MemberClass move constructor called" << std::endl;
    }
};


class MyClass {
public:
    MemberClass member;
    // 空实现移动构造函数
    MyClass(MyClass&& other) noexcept {
        std::cout << "MyClass move constructor called" << std::endl;
        // 这里虽然空,但不会调用成员的移动构造函数
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1);
    return 0;
}

输出结果将会是:

MyClass move constructor called

        所以,即使成员类提供了自定义的移动构造函数,空的移动构造函数也不会调用它们。如果你想使用默认移动,那么请不要进行空实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值