C++中的左值(lvalue)和 右值(rvalue),移动语义(move semantics)和完美转发(perfect forwarding)

C++中的左值(lvalue)和 右值(rvalue),移动语义(move semantics)和完美转发(perfect forwarding)

flyfish

在C++中,表达式可以是左值(lvalue)或右值(rvalue)。左值和右值的区别主要在于它们的值类别(value category)和它们在表达式中的使用方式。

左值(lvalue)

左值表示一个对象或内存位置,可以被赋值或取地址。左值通常是具名变量、数组元素或函数调用返回的左值引用。

右值(rvalue)

右值表示一个临时值或字面常量,不能被赋值,但可以用于初始化另一个对象或被移动。

左值示例

在这个示例中,x 和 y 都是左值,因为它们表示内存中的一个对象,可以被赋值和取地址。

c 复制代码
#include <iostream>

int main() {
    int x = 10;      // 变量 x 是左值,可以被赋值
    int y = x;       // 变量 x 是左值,可以用于赋值

    int* p = &x;     // 变量 x 是左值,可以取地址

    std::cout << "x: " << x << std::endl;
    std::cout << "y: " << y << std::endl;
    std::cout << "Address of x: " << p << std::endl;

    return 0;
}
c 复制代码
x: 10
y: 10
Address of x: 0000008C979DF694

右值示例

在这个示例中,表达式 x + 5 和字面常量 42 都是右值,因为它们表示临时值,不可以取地址。

c 复制代码
#include <iostream>

int main() {
    int x = 10;
    int y = x + 5;   // 表达式 x + 5 是右值,表示一个临时值, x + 5 不可以取地址
    int z = 42;      // 字面常量 42 是右值,表示一个临时值, 42不可以取地址

    std::cout << "x: " << x << std::endl;
    std::cout << "y: " << y << std::endl;
    std::cout << "z: " << z << std::endl;

    return 0;
}

左值引用和右值引用

printLvalue 函数接受左值引用参数,只能传递左值。

printRvalue 函数接受右值引用参数,只能传递右值。

调用 printRvalue(20) 和 printRvalue(a + 5) 都是传递右值,而调用 printLvalue(a) 是传递左值。

c 复制代码
#include <iostream>

// 左值引用参数
void printLvalue(int& x) {
    std::cout << "Lvalue: " << x << std::endl;
}

// 右值引用参数
void printRvalue(int&& x) {
    std::cout << "Rvalue: " << x << std::endl;
}

int main() {
    int a = 10;

    printLvalue(a);       // 传递左值
    printRvalue(20);      // 传递右值

    printRvalue(a + 5);   // 传递右值
    printRvalue(std::move(a)); // 将左值转换为右值

    return 0;
}

输出

c 复制代码
Lvalue: 10
Rvalue: 20
Rvalue: 15
Rvalue: 10

std::move

std::move 是 C++11 引入的一个标准库函数,用于将对象显式地转换为右值引用(rvalue reference)。它的主要作用是启用对象的移动语义,从而避免昂贵的拷贝操作,提升程序的性能。

std::move 的工作原理

std::move 本质上不会移动任何东西,它只是将其参数转换为右值引用,使其可以被移动构造函数或移动赋值运算符处理。这样可以避免深拷贝,提高效率。

下面的例子std::move 将 vec1 转换为右值引用,允许 vec2 通过移动构造函数获取 vec1 的内容。移动之后,vec1 的大小变为 0,因为其资源已被转移给 vec2。

cpp 复制代码
#include <iostream>
#include <utility>  // std::move
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2;

    // 使用 std::move 将 vec1 转换为右值引用,vec2 移动构造 vec1 的内容
    vec2 = std::move(vec1);

    std::cout << "vec1 size: " << vec1.size() << std::endl;  // 输出:vec1 size: 0
    std::cout << "vec2 size: " << vec2.size() << std::endl;  // 输出:vec2 size: 5

    return 0;
}

输出

cpp 复制代码
vec1 size: 0
vec2 size: 5

移动语义(Move Semantics)

移动语义是C++11引入的特性,用于优化对象的复制操作,避免不必要的深拷贝,提高程序的性能。它通过使用右值引用(&&)实现对象的移动,而不是复制。

下面是一个带有移动构造函数和移动赋值运算符的自定义类示例

MyClass 有一个带动态内存分配的构造函数。

移动构造函数将资源从源对象转移到目标对象,并将源对象的指针置空。

移动赋值运算符类似,将资源从源对象转移到目标对象,并释放目标对象的原有资源。

cpp 复制代码
#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int size) : size(size), data(new int[size]) {
        std::cout << "Constructor called\n";
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : size(other.size), data(other.data) {
        other.size = 0;
        other.data = nullptr;
        std::cout << "Move constructor called\n";
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;

            size = other.size;
            data = other.data;
            other.size = 0;
            other.data = nullptr;
            std::cout << "Move assignment operator called\n";
        }
        return *this;
    }

    ~MyClass() {
        delete[] data;
        std::cout << "Destructor called\n";
    }

private:
    int size;
    int* data;
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = std::move(obj1);  // 调用移动构造函数

    MyClass obj3(20);
    obj3 = std::move(obj2);          // 调用移动赋值运算符

    return 0;
}

输出

cpp 复制代码
Constructor called
Move constructor called
Constructor called
Move assignment operator called
Destructor called
Destructor called
Destructor called

完美转发(Perfect Forwarding)

完美转发允许函数模板将其参数完美地传递给另一个函数,而不改变参数的值类别(左值或右值)。这通常通过使用 std::forward 实现。

完美转发示例

wrapper 函数模板使用 std::forward 将参数完美转发给 process 函数。

process 函数有两个重载版本,一个接受左值引用,另一个接受右值引用。

wrapper 函数根据传入参数的值类别,正确调用对应的 process 函数。

cpp 复制代码
#include <iostream>
#include <utility>  // std::forward

// 泛型函数,用于完美转发参数
template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

// 被调用的函数
void process(int& x) {
    std::cout << "Processing lvalue: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "Processing rvalue: " << x << std::endl;
}

int main() {
    int a = 5;

    wrapper(a);          // 传递左值,调用 void process(int& x)
    wrapper(10);         // 传递右值,调用 void process(int&& x)
    wrapper(std::move(a)); // 传递移动的左值,调用 void process(int&& x)

    return 0;
}

输出

cpp 复制代码
Processing lvalue: 5
Processing rvalue: 10
Processing rvalue: 5
相关推荐
程与留1 小时前
15_国际化和本地化:tr()、ts 文件、QM 文件、多语言切换
c++·qt
程与留3 小时前
14_Qt 样式表(QSS)入门(语法、选择器、美化实战)
c++·qt
欧特克_Glodon12 小时前
OpenCV计算机视觉开发入门与实践<二十七>:图像分割概述
c++·人工智能·opencv·计算机视觉
蒸蒸yyyyzwd12 小时前
cpp 选手秋招学习笔记 day21
c++·面试·八股
「QT(C++)开发工程师」1 天前
C++ auto 用法详解
开发语言·c++
OPEN-F1 天前
C++STL教程:容器适配器与实用工具
开发语言·c++
OPEN-F1 天前
C++模板教程:变参模板、折叠表达式与SFINAE
java·开发语言·c++
有点。1 天前
C++二叉搜索树进阶
开发语言·c++
HugoStudio_SWAN1 天前
【擦除重绘】C++ 控制台动画:弹跳 Logo DVD 屏保效果
开发语言·c++·学习·程序人生
kyle~1 天前
C++_STL---迭代器失效
开发语言·c++