自学内容网 自学内容网

c++ 左值和右值

在 C++ 中,右值(rvalue)左值(lvalue) 是两个基础概念,它们主要用来区分表达式中的对象的“值类别”,即对象是“可以被赋值的”还是“仅用于计算的”。

左值和右值的区别

  • 左值(lvalue):指向内存中的某个位置,可以获取其地址。左值通常是具有持久性的内存对象,它可以位于赋值语句的左边。通常,变量、数组元素、引用等都是左值。

    示例

  • int a = 10; // a 是左值,因为它表示一个持久的对象,可以被赋值

  • a = 20; // 可以给左值 a 赋值

  • 右值(rvalue):指的是不存储在内存中的临时值,只用于表达式的计算结果,通常不能获取其地址。右值是“临时的”,它通常位于赋值语句的右边,像字面量或表达式的结果。

    示例

  • int a = 10; // 10 是右值,它是一个临时的字面量

  • int b = a + 5; // (a + 5) 的结果是右值,它是一个表达式的结果

右值的操作

右值的操作主要与如何处理临时对象、转移资源和优化性能有关,特别是在 C++11 引入右值引用移动语义之后,右值的处理方式得到了极大的增强。

1. 右值引用(Rvalue Reference,T&&
  • 定义:C++11 引入了一种新的引用类型,叫做右值引用,其语法是 T&&。右值引用允许我们绑定右值,并对它们进行操作。通过右值引用,可以避免不必要的复制操作,从而实现更高效的代码。

  • 主要用途

    • 移动语义(Move Semantics):右值引用用于实现移动构造函数移动赋值运算符,从而转移资源的所有权,而不进行深拷贝。
    • 完美转发(Perfect Forwarding):右值引用配合模板用于在泛型编程中转发参数,避免不必要的拷贝或移动。

    示例

  • class MyClass {
    public:
        MyClass(int size) : data(new int[size]) {}         // 构造函数
        ~MyClass() { delete[] data; }                      // 析构函数

        // 移动构造函数:通过右值引用,转移资源所有权
        MyClass(MyClass&& other) noexcept : data(other.data) {
            other.data = nullptr;  // 将资源的所有权转移后,清空原对象
        }

    private:
        int* data;
    };

2. 右值和移动语义
  • 移动语义通过右值引用来优化性能。在函数中,如果参数是右值引用(如 T&&),则可以通过移动资源而非复制资源来避免性能损耗。移动语义在涉及到资源管理(如动态内存、文件句柄、网络连接)时尤为重要。

    示例:移动语义通过 std::move 来触发:

  • std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = std::move(v1);  // v1 的数据被“移动”到 v2,v1 变为空

3. 右值的常见操作
  • 移动构造函数和移动赋值运算符:通过右值引用来优化类的资源管理。使用右值引用避免不必要的深拷贝。

    移动构造函数移动赋值运算符的定义一般如下:

  • MyClass(MyClass&& other) noexcept;         // 移动构造函数

  • MyClass& operator=(MyClass&& other) noexcept;  // 移动赋值运算符
  • 临时对象的优化:右值是临时对象,它们会在不再使用时销毁。在某些情况下,编译器会优化掉这些临时对象的创建和销毁(如返回值优化 (RVO) 和命名返回值优化 (NRVO))。

  • std::move将左值显式地转换为右值,以便触发移动语义而非复制。例如:

        std::string str1 = "Hello";

        std::string str2 = std::move(str1);  // str1 被移动到 str2,避免拷贝

  • std::forward:用于模板中的完美转发,保持右值或左值的属性,用于函数模板参数传递时的转发。

        template<typename T>
        void func(T&& arg) {
            someFunction(std::forward<T>(arg));  // 保持原有的左值或右值属性
          }

5、右值操作总结
  1. 右值引用(T&&:允许绑定右值,并可以通过它来进行资源转移。
  2. std::move:将左值显式地转换为右值,以便触发移动语义。
  3. 移动构造函数和移动赋值运算符:利用右值引用来避免资源的复制,而直接转移资源。
  4. 完美转发:利用右值引用和 std::forward 实现高效的参数传递。

右值和右值引用在现代 C++ 中极大地提升了性能,特别是在处理大对象和复杂资源时,能够避免大量不必要的复制操作。

std::move 和swap

1、介绍

  • std::move 是将对象转换为右值引用,允许资源移动,减少资源的复制。移动后,源对象的状态通常是不确定的,意味着它的资源已经被转移。
  • std::swap 是交换两个对象的内容,通过调用构造函数(移动或复制)来交换两个对象的内部资源。

2. std::move

  • 功能std::move 并不移动对象本身,而是将对象转变为一个右值引用(rvalue reference)。这允许对象的资源被“移动”,而不是“复制”。

  • 主要用途std::move 通常用于移动语义,以减少不必要的复制操作,尤其是在处理资源(如内存、文件句柄)时。通过移动语义,可以避免拷贝资源而直接转移所有权。

  • 用法:通过 std::move,你可以将对象变成右值,从而触发其移动构造函数或移动赋值运算符。

  • 注意事项

  • 使用 std::move 后,源对象的状态通常是不确定的(但必须保持可析构的状态)。因此,除非明确重新赋值或初始化源对象,否则不要再使用它。
  • std::move 本身并不会移动数据,它只是将对象转换为右值引用。真正的移动是在移动构造函数或移动赋值运算符中发生的。
  • 注意事项

  1. 使用 std::move 后,源对象的状态通常是不确定的(但必须保持可析构的状态)。因此,除非明确重新赋值或初始化源对象,否则不要再使用它。
  2. std::move 本身并不会移动数据,它只是将对象转换为右值引用。真正的移动是在移动构造函数或移动赋值运算符中发生的。

3. std::swap

  • 功能std::swap 是用于交换两个对象的内容。它不涉及右值引用,而是将两个对象的值互换。通常使用对象的复制构造函数或移动构造函数来交换其内部资源。

  • 主要用途std::swap 用于简洁地交换两个对象的状态,尤其在某些算法中(如排序算法)被频繁使用。

  • 用法std::swap 可以交换两个同类型对象的内容。

  • 注意事项

  • std::swap 对所有可移动或可复制的对象都可以使用,且性能依赖于对象的移动构造函数或赋值运算符的实现。
  • 在自定义类中,如果对象较大,最好提供一个高效的移动构造函数和移动赋值运算符以便优化 swap 的性能。

不可复制不可移动对象(单例模式开发

C++11 引入了移动语义,但通过控制拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符的访问性,开发者可以禁用这些操作,从而防止对象的复制和移动。

1. 不可复制的对象

为了创建不可复制的对象,可以将拷贝构造函数拷贝赋值运算符声明为 delete 或者将它们声明为 private,这样在编译期就会阻止复制行为。

在这个例子中:

  • 拷贝构造函数 NonCopyable(const NonCopyable&) = delete 被删除,因此不能通过拷贝构造另一个对象。
  • 拷贝赋值运算符 NonCopyable& operator=(const NonCopyable&) = delete 被删除,因此不能通过赋值来复制对象。

class NonCopyable {
public:
    NonCopyable() = default;  // 默认构造函数
    ~NonCopyable() = default; // 默认析构函数

    // 删除拷贝构造函数和拷贝赋值运算符
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

int main() {
    NonCopyable obj1;
    // NonCopyable obj2 = obj1;  // 错误!拷贝构造函数被禁用
    // NonCopyable obj3;
    // obj3 = obj1;              // 错误!拷贝赋值运算符被禁用
    return 0;
}

2. 不可移动的对象

为了创建不可移动的对象,可以将移动构造函数移动赋值运算符删除或声明为 private。这样可以在编译期阻止移动操作。

在这个例子中:

  • 移动构造函数 NonMovable(NonMovable&&) = delete 被删除,因此不能通过移动构造另一个对象。
  • 移动赋值运算符 NonMovable& operator=(NonMovable&&) = delete 被删除,因此不能通过赋值进行移动。

class NonMovable {
public:
    NonMovable() = default;  // 默认构造函数
    ~NonMovable() = default; // 默认析构函数

    // 删除移动构造函数和移动赋值运算符
    NonMovable(NonMovable&&) = delete;
    NonMovable& operator=(NonMovable&&) = delete;
};

int main() {
    NonMovable obj1;
    // NonMovable obj2 = std::move(obj1);  // 错误!移动构造函数被禁用
    // NonMovable obj3;
    // obj3 = std::move(obj1);             // 错误!移动赋值运算符被禁用
    return 0;
}

3. 不可复制和不可移动的对象

为了创建既不可复制不可移动的对象,可以同时删除拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。这种设计常用于某些具有唯一性或需要保证单一实例的类。

在这个例子中,NonCopyableNonMovable 类既不能被复制也不能被移动,因为它的所有相关函数都被删除。

class NonCopyableNonMovable {
public:
    NonCopyableNonMovable() = default;
    ~NonCopyableNonMovable() = default;

    // 删除拷贝构造函数和拷贝赋值运算符
    NonCopyableNonMovable(const NonCopyableNonMovable&) = delete;
    NonCopyableNonMovable& operator=(const NonCopyableNonMovable&) = delete;

    // 删除移动构造函数和移动赋值运算符
    NonCopyableNonMovable(NonCopyableNonMovable&&) = delete;
    NonCopyableNonMovable& operator=(NonCopyableNonMovable&&) = delete;

};

int main() {
    NonCopyableNonMovable obj1;
    // NonCopyableNonMovable obj2 = obj1;          // 错误!拷贝构造函数被禁用
    // NonCopyableNonMovable obj3 = std::move(obj1); // 错误!移动构造函数被禁用
    return 0;
}

4. 实际应用场景

  • 单例模式:在实现单例模式时,通常会禁用对象的复制和移动,以确保系统中始终只有一个实例。

  • class Singleton {
    public:
        static Singleton& getInstance() {
            static Singleton instance;  // 唯一实例
            return instance;
        }

        // 禁用复制和移动
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
        Singleton(Singleton&&) = delete;
        Singleton& operator=(Singleton&&) = delete;

    private:
        Singleton() = default;  // 私有构造函数
    };

    int main() {
        Singleton& s1 = Singleton::getInstance();
        // Singleton s2 = s1;   // 错误,复制被禁用
        // Singleton s3 = std::move(s1);  // 错误,移动被禁用
        return 0;
    }

  • 在单例模式中,复制和移动被禁用以保证系统中只有一个 Singleton 实例。

  • 文件句柄、网络连接等系统资源管理类:为了防止资源不正确地被多次释放或移动到无效状态,一些管理系统资源的类(如文件句柄类)也通常禁止复制和移动操作。

5. 总结

  • 不可复制的对象:通过删除拷贝构造函数和拷贝赋值运算符,可以防止对象的复制。
  • 不可移动的对象:通过删除移动构造函数和移动赋值运算符,可以防止对象的移动。
  • 不可复制和不可移动的对象:通过同时删除这些函数,可以完全阻止对象的复制和移动。

这些特性常用于单例模式、资源管理类等场景,以确保对象的唯一性和控制对象的生命周期。


原文地址:https://blog.csdn.net/weixin_43881088/article/details/143083674

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!