c++详解移动语义,右值引用与move函数的作用,noexcept关键字在移动构造与移动赋值运算符中的重要性

这是c++ 11新添加的特性,设计的目的是为了避免不必要的拷贝开销,通过资源所有权的转移提升性能,它可以在不拷贝的情况下移动大型资源,它所做的就是将现有资源的所有权交给其他对象,而传统的拷贝需要进行一次内存分配这是很大的开销,移动语义相较于拷贝性能提升相当明显,移动后的源对象处于有效但未指定状态,即保证可安全析构和重新赋值,但具体成员值由实现决定(可能为空、残留值等)。继续访问其值前必须先重置状态。

  • 标准规定,移动后的原对象必须仍然可安全析构,但其内容可能为空或残留旧数据(具体取决于标准库实现)。
std::string str = "hello world";
std::string str1 = std::move(str);
std::cout << "str1: " << str1 << std::endl;
std::cout << "str: " << str << std::endl;
str = "test";
std::cout << "str: " << str << std::endl;

// 输出
str1: hello world
str:
str: test

我们可以看到触发移动语义后,原始的str被置为空了,这时候是不建议再访问它的,因为这是未定义的,不过我们可以为被移动的对象重新赋值,这是合法且安全的行为
在继续之前我们先讲两个基础概念

右值引用与std::move函数

c++中有左值与右值的区分,左值可以理解为一个长期存在的对象,而临时对象即将销毁的对象就是右值,像我们创建的变量与new出来的对象都是左值,而函数的返回值与由std::move标记的对象等都是右值
在c++ 11引入了一个新的类型概念,右值引用使用&&符号定义,例如 int&& rref,作用就是绑定到 临时对象(右值)即将销毁的对象,标识可以安全“移动”资源的对象,从而优化资源管理。

int func1(){
    return 1;
}

int a = 10;       // a 是左值
int& ref = a;     // ref 是左值引用
42;               // 字面量是右值
a + 5;            // 表达式结果是右值
std::move(a);     // 返回右值引用
int&& p = std::move(a);// 右值引用
int&& pp = func1();// 右值引用

需要注意std::move函数的作用是,将对象 强制转换为右值,表示其资源可被移动,此函数并没有实现移动,只是标记了某个对象为可移动的,这一点很重要,不要移动调用了std::move就实现了移动
上面的代码只是作为演示,在实际使用中int 等基本类型的移动与拷贝无区别,直接赋值即可。

移动构造与移动赋值运算符

几个关键的知识点了解完了,下一步就需要来实现移动了,我们需要实现移动构造与移动赋值运算符,来实现移动对象的功能

class Test {
public:
    // 构造函数
    Test(const std::string& s) : str(s) {
        std::cout << "Test(const std::string&)" << std::endl;
    }
    // 移动构造函数(标记 noexcept)
    Test(Test&& s) noexcept
        : str(std::move(s.str)),  // 调用 string 的移动构造
          i(s.i) {                // 直接拷贝 int
        std::cout << "Test(Test&&)" << std::endl;
    }
    // 拷贝构造函数
    Test(const Test& s) : str(s.str), i(s.i) {
        std::cout << "Test(const Test&)" << std::endl;
    }
    // 移动赋值运算符(标记 noexcept)
    Test& operator=(Test&& s) noexcept {
        std::cout << "operator=(Test&&)" << std::endl;
        // 自赋值检查
        if (this != &s) {
            str = std::move(s.str);  // string 的移动赋值
            i = s.i;                 // 直接拷贝 int
        }
        return *this;  // 正确返回 Test&
    }
    ~Test() = default;  // 默认析构
    std::string getStr() const { return str; }
private:
    std::string str;
    int i = 15;  // 默认值
};
// 测试代码
int main() {
    Test t("hello");
    Test t1(t);
    std::cout << "t1: " << t1.getStr() << std::endl;
    std::cout << "t: " << t.getStr() << std::endl;
    
    Test t2(std::move(t1));
    std::cout << "t2: " << t2.getStr() << std::endl;
    std::cout << "t1: " << t1.getStr() << std::endl;
    std::cout << "t: " << t.getStr() << std::endl;

    t1 = std::move(t);
    std::cout << "t2: " << t2.getStr() << std::endl;
    std::cout << "t1: " << t1.getStr() << std::endl;
    std::cout << "t: " << t.getStr() << std::endl;
    return 0;
}

// 输出
Test(const std::string&)
Test(const Test&)
t1: hello
t: hello
Test(Test&&)
t2: hello
t1:
t: hello
operator=(Test&&)
t2: hello
t1: hello
t:

这里我们实现了Test的移动构造与移动赋值函数,他们的参数都是Test&&一个右值引用,注意赋值运算符的,返回值是左值引用不是不是右值引用,函数的实现很简单对于基础类型,我们直接赋值后置0就可以,当然也可以不置0
在测试代码的输出中可以看出,使用移动语义构造t2后t1内部的str空了,并且t2获得了hello,移动赋值也实现了同样的效果,我们成功实现了移动

noexcept

在代码中出现的noexcept,很多人可能没见过,这也是移动语义的重要一环noexcept 是 C++11 引入的关键字,用于指示函数不会抛出异常。它的核心作用是优化代码性能明确无异常
它的重要性主要体现在两方面:

  1. 编译器
    编译器知道函数不会抛异常后,可省略生成异常处理代码(如栈展开逻辑),生成更高效的机器码。
  2. 标准库优化
    这是最重要的,如果不知道这个和没学移动是一样的,下面我们通过一段代码来讲解
// 为标记 noexcept
Test(Test&& s): str(std::move(s.str)),i(s.i) {
    std::cout << "Test(Test&&)" << std::endl;
}

int main() {
    std::vector<Test> vec;
    vec.push_back(Test("hello"));
    vec.push_back(Test("hello"));
    vec.push_back(Test("hello"));
    return 0;
}

// 输出
Test(const std::string&)
Test(Test&&)
Test(const std::string&)
Test(Test&&)
Test(const Test&)
Test(const std::string&)
Test(Test&&)
Test(const Test&)
Test(const Test&)

我先来简单的介绍一下vector,它是一个可扩容的数组,当我们向其添加新内容时,如果已有大小不够了会自动扩容,而扩容就需要开辟新的内存空间,这个新空间是什么都没有的,需要将新老资源迁移过来
在看到输出后你可能会很惊讶,我们不是都实现了移动构造函数了吗,为什么vector在扩容的时候,会调用拷贝构造而不是移动构造吗,对于vector的扩容这件事来说移动构造完美太合适了呀,但是为什么没有使用移动构造呢。
问题就出在没有使用noexcept,我们为将移动构造函数标记为无异常std::vector 在扩容时,是否使用移动构造函数取决于移动操作是否标记为 noexcept。若未标记,标准库会优先选择拷贝构造函数以保证异常安全。这是 C++ 标准(C++11 及后续版本)的强制要求。
我们假设在扩容是调用了无noexcept的移动构造函数,中途发送了异常导致扩容失败,这是时候就会出现严重的数据安全问题,可以会有部分数据为被迁移到新内存,并且老的那一部分已经被清除,标准库要求容器操作(如 vector::push_back)在发生异常时,保持原有数据不变,那为什么会默认选择拷贝构造呢,因为拷贝构造函数通常不修改源对象,即使抛异常,原数据仍完整。

// 将移动构造函数加上noexcept
// 输出
Test(const std::string&)
Test(Test&&)
Test(const std::string&)
Test(Test&&)
Test(Test&&)
Test(const std::string&)
Test(Test&&)
Test(Test&&)
Test(Test&&)

移动构造函数和移动赋值运算符应标记 noexcept,以便标准库(如 std::vector)优先选择移动而非拷贝。
最后总结几点注意事项

  • 移动后的源对象:有效但不可依赖
  • 移动操作必须标记 noexcept
  • 避免自移动赋值
  • 默认生成的移动操作可能不符合预期
  • 移动语义不总是优于拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值