【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]

阅读导航

引言

当谈到C++的高级特性时,右值引用是一个不可忽视的重要概念。作为一种在C++11标准中引入的语言特性,右值引用为我们提供了更加灵活和高效的内存管理方式。它不仅可以优化代码性能,还可以改善对象拷贝行为,使得我们能够更好地处理临时对象和移动语义。通过深入理解右值引用的原理和使用方法,我们可以在C++编程中发挥出更大的威力,提升代码的效率和可维护性。本文将全面介绍右值引用的概念、用法和相关的重要概念,帮助读者更好地理解和应用这一关键特性。无论您是初学者还是有经验的程序员,都将从本文中获得对右值引用的深入认识,并能够在实际项目中灵活运用。让我们一起探索C++中右值引用的奇妙世界吧!🥰

一、左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,总的来说就是无论左值引用还是右值引用,都是给对象取别名。

1. 什么是左值?什么是左值引用?

在C++中,左值是指表达式结束后依然存在的数据对象,它可以出现在赋值操作的左边或右边。通常来说,变量、函数返回的引用、解引用操作等都是左值。简言之,左值可以被赋值,可以取地址。

左值引用是指对左值进行引用的方式。它使用&符号声明,可以绑定到一个左值上,从而允许我们通过引用修改原始的左值对象。左值引用就是给左值的引用,给左值取别名。左值引用在函数参数传递和函数返回值中经常被使用,能够避免不必要的复制,并且可以实现对原始对象的直接操作。左值引用也为后续引入右值引用打下了基础,是C++语言中非常重要的概念之一。

cpp 复制代码
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

2. 什么是右值?什么是右值引用?

在C++中,右值是指表达式结束后即将被销毁的临时数据对象,它通常不能出现在赋值操作的左边。字面上来说,右值就是"赋值运算符=右边的值"。比如,常量、临时对象、表达式的计算结果等都可以是右值。

右值引用是C++11引入的新特性,使用双&&符号声明,它可以绑定到一个右值或将要销毁的对象上。右值引用的引入使得我们能够实现移动语义,即将资源(如内存)的所有权从一个对象转移到另一个对象,而不需要进行深层的复制操作 ,从而提高了代码的效率和性能。右值引用还为移动构造函数和移动赋值运算符的实现提供了基础,这些特性在处理大型数据结构时非常有用。右值引用的引入使得C++语言能够更好地支持移动语义,从而更好地适应现代编程的需求。

cpp 复制代码
int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	
	// 这里编译会报错:error C2106: "=": 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

cpp 复制代码
int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	
	rr1 = 20;
	rr2 = 5.5; // 报错
	return 0;
}

3. move( )函数

std::move() 是C++11引入的一个函数模板,位于头文件 <utility> 中。它用于将传入的对象转换为右值引用,从而支持移动语义。在移动语义中,对象的资源所有权可以从一个对象转移到另一个对象,而不需要进行深层的复制操作,这可以提高程序的性能和效率。

std::move() 的定义如下:

cpp 复制代码
template <class T>
constexpr remove_reference_t<T>&& move(T&& t) noexcept;

其中,t 是一个通用引用,它可以绑定到左值或右值。std::move()t 转换为右值引用并返回,即使 t 是一个左值,也可以通过 std::move() 转为右值引用。

使用 std::move() 主要用于以下两个场景:

  1. 在实现移动构造函数和移动赋值运算符时,可以使用 std::move() 将成员变量转换为右值引用,从而实现资源的转移而非复制。
  2. 在标准库中,例如容器的 insertemplace 方法中,使用 std::move() 可以将对象的所有权转移到容器中,避免不必要的复制操作。

需要注意的是,std::move() 仅仅是将对象转换为右值引用,它本身并不会进行实际的资源移动操作。因此,在使用 std::move() 后,程序员仍需谨慎处理对象的生命周期,以避免悬挂指针或对象被多次释放等问题。

二、左值引用与右值引用比较

⭕左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值
cpp 复制代码
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

⭕右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值.
cpp 复制代码
int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: "初始化": 无法从"int"转换为"int &&"
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;
	
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

三、右值引用使用场景和意义

  1. 移动语义 :右值引用的最重要的使用场景之一就是实现移动语义。通过移动语义,可以避免不必要的深层复制操作,提高程序的性能和效率。移动语义通常在以下情况下使用:
    • 移动构造函数和移动赋值运算符:通过将资源的所有权从一个对象转移到另一个对象,而非进行深层的复制操作,来提高效率。
    • 标准库中的容器和算法:许多标准库中的容器和算法都利用了移动语义,例如移动构造和移动赋值来提高性能。

例如在下面这段代码中,使用了右值引用来实现移动语义,从而避免不必要的深层复制操作,提高了对象的构造和赋值效率。

移动构造函数的定义如下:

cpp 复制代码
string::string(string&& s)
    : _str(nullptr), _size(0), _capacity(0)
{
    cout << "string(string&& s) -- 移动语义" << endl;
    swap(s);
}

在移动构造函数中,接收一个右值引用作为参数,通过 && 标识符表示。在函数体内部,输出一条信息以表明这是移动构造函数,并且调用了 swap() 函数来交换资源,实现了移动语义。

移动赋值运算符的定义如下:

cpp 复制代码
string& string::operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}

在移动赋值运算符中,同样接收一个右值引用作为参数。在函数体内部,输出一条信息以表明这是移动赋值运算符,并且调用了 swap() 函数来交换资源,实现了移动语义。

  1. 完美转发:右值引用与通用引用(universal reference)结合使用时,可以实现完美转发。完美转发允许将函数参数按原样传递给其他函数,无论原始参数是左值还是右值。这对于泛型编程以及实现转发函数(forwarding function)非常有用。

  2. 优化临时对象:临时对象是在表达式求值过程中创建的临时值,它们的生命周期很短暂,并且通常在表达式结束后立即销毁。通过使用右值引用,可以避免不必要的拷贝构造和析构操作,提高代码的性能和效率。

  3. 移动语义和资源管理:右值引用在资源管理方面非常有用,例如管理动态分配的内存、文件句柄、网络连接等。通过使用右值引用,可以实现资源的移动而非复制,从而提高程序的性能和可维护性。

  4. 避免不必要的拷贝构造和析构:当需要返回临时对象时,通过使用右值引用可以避免不必要的拷贝构造和析构,提高代码的效率。

四、完美转发

模板中的&& 万能引用

cpp 复制代码
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

std::forward 函数

std::forward 是C++标准库中的一个函数模板,位于 <utility> 头文件中。它用于实现完美转发,将传入的参数以原样转发给其他函数。

std::forward 的函数模板定义如下:

cpp 复制代码
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept;

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& arg) noexcept;

这个函数模板有两个重载版本,接受一个通用引用作为参数。它使用了 typename std::remove_reference<T>::type 来移除参数的引用限定符,以保持参数的值类别(左值或右值)。

当传入一个左值时,std::forward 返回一个左值引用;当传入一个右值时,std::forward 返回一个右值引用。这样就可以保持参数在转发过程中的值类别不变。

std::forward 的主要应用场景是在模板函数中进行完美转发,将参数原样传递给其他函数。通过使用 std::forward,可以避免不必要的拷贝和移动操作,提高代码的性能和效率。

以下是使用 std::forward 进行完美转发的示例:

cpp 复制代码
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }

void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>

void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

完美转发实际中的使用场景

cpp 复制代码
template<class T>
struct ListNode
{
    ListNode* _next = nullptr;
    ListNode* _prev = nullptr;
    T _data;
};

template<class T>
class List
{
    typedef ListNode<T> Node;

public:
    List()
    {
        // 创建一个头节点,并将头节点的_next和_prev都指向自身,表示链表为空
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }

    void PushBack(T&& x)
    {
        // 在链表尾部插入一个右值
        Insert(_head, std::forward<T>(x));
    }

    void PushFront(T&& x)
    {
        // 在链表头部插入一个右值
        Insert(_head->_next, std::forward<T>(x));
    }

    void Insert(Node* pos, T&& x)
    {
        // 在指定位置之前插入一个右值

        // 获取pos节点的前一个节点
        Node* prev = pos->_prev;

        // 创建一个新的节点
        Node* newnode = new Node;
        
        // 使用完美转发将右值x赋值给新节点的_data
        newnode->_data = std::forward<T>(x);

        // 调整链表中的指针
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

    void Insert(Node* pos, const T& x)
    {
        // 在指定位置之前插入一个左值

        // 获取pos节点的前一个节点
        Node* prev = pos->_prev;

        // 创建一个新的节点
        Node* newnode = new Node;
        
        // 将左值x赋值给新节点的_data
        newnode->_data = x;

        // 调整链表中的指针
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

private:
    Node* _head;
};

int main()
{
    List<bit::string> lt;
    lt.PushBack("1111");
    lt.PushFront("2222");
    return 0;
}

上面这段代码是一个简化的链表实现,包括了节点结构 ListNode 和链表类 List。其中,链表类中的 PushBackPushFrontInsert 函数用于在链表中插入元素。

Insert 函数中,有两个重载版本,分别用于插入右值引用和左值引用。关键位置是对节点的 _data 成员赋值的地方。

对于右值引用版本,使用 std::forward<T>(x) 将参数 x 原样转发,保持其原始值类别。这样做可以避免不必要的拷贝操作,提高性能和效率。

对于左值引用版本,直接将参数 x 赋值给节点的 _data 成员。因为左值引用已经是一个具名对象,没有必要进行移动或拷贝操作。

在主函数中,创建了一个 List<bit::string> 类型的链表对象 lt,并通过 PushBackPushFront 函数向链表中插入元素。

总的来说,这段代码展示了如何使用完美转发和模板来实现一个简单的链表,并在插入元素时考虑了右值引用和左值引用的情况,以提高代码的灵活性和效率。

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

相关推荐
卓越小Y14 分钟前
配置jellyfin docker 硬件加速
java·spring cloud·docker
白萝卜弟弟17 分钟前
【JAVA】正则表达式中的捕获组和非捕获组
java·正则表达式
星光樱梦20 分钟前
24. 正则表达式
c++
fathing21 分钟前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#
袁庭新37 分钟前
LuaRocks如何安装数据库驱动?
java·数据库·redis·lua·luarocks·袁庭新
hummhumm1 小时前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
nukix1 小时前
Mac Java 使用 tesseract 进行 ORC 识别
java·开发语言·macos·orc
月光光心慌慌。1 小时前
新日撸java三百行` 新手小白java学习记录 `Day1
java
蘑菇丁1 小时前
ranger-kms安装
java·ide·eclipse
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】内存可见性问题 & volatile
java·开发语言·java-ee