这里写目录标题
一. 右值引用
右值引用本质就是移动(挪动)资源,原因在于右值一般是马上要被销毁的资源,同时在函数返回值中可以减少拷贝问题。
1.1 什么是右值与左值
右值一般包括字面值常量,创建临时对象,如10,fmin(x,y),iterator(cur,nullptr)等,具有临时性。
左值是一个变量名,解引用指针如int a=1,*p=10等,一般具有持久性。
- 不同点:左值可以取地址,右值不可以取地址。
1.2 左值引用和右值引用
两个引用都是取别名,前者是给左值取别名,后者是给右值取别名,左值引用不可以直接引用右值,原因:右值具有常性,涉及权限放大,可以使用const左值引用可以引用右值。
右值引用不可以直接引用左值,右值引用可以引用move(左值)。
结论:引用和指针在汇编实现都是一样的。
1.2.1 move
move是一个函数模版,本质就是类型强制转换。
template <class T>
typename remove_reference<T>::type&& move (T&& arg);
虽然右值引用引用是右值,但&rr是允许的,因为右值引用本身是左值,左值是允许取地址的,右值是可以对该临时资源进行修改的。
int a = 10;
//左值引用 引用 右值
const int& a = 20;
//右值引用 引用 左值
int&& rr = move(a);
给右值加上const 就不允许修改它了。
注意:使用move左值语句后,可能会导致左值中的资源被挪动,所以在使用该move前须确保左值的资源不再使用。
string str = "hello C++!";
cout << "str: " << str << endl;
string rr = move(str);
cout << "str: " << str << endl;
输出结果:
1.3 右值使用场景
右值分为:纯右值和将亡值(说白了就是马上要被销毁的资源,与其它被销毁,不如二次回收,将所要被销毁的资源转移给需要的其它系统等)。将亡值的意义很大,尤其体现在拷贝场景下。
下面通过场景分析一下右值的用处。
下面是自己模拟实现的string类,未实现移动构造及移动赋值。
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
main.cpp
int main()
{
A::string str= "hello!";
//str为左值
A::string s = str;
//str + '\n'为右值
A::string s1 = str + '\n';
return 0;
}
输出结果:
发生了三次构造,一次是"hello"构造,一次是s的构造,另一次是s1的构造。
给上述代码假如移动构造。
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
在运行代码,结果如下:
1.4 右值引用意义
右值引用是个非常奇妙的想法,它可以利用临时资源,避免低效率拷贝问题。
- 左值引用:直接引用对象资源,本身该对象资源仍然存在。
- 右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝
二. 完美转发
2.1 模版中的万能引用
泛型编程的核心在于 模版根据传入参数类型推导函数,当我们分别传入左值引用,右值引用时,模版是否能正确推导?
下面这段代码的含义是 分别传入 左值、const 左值、右值、const 右值,并设计对应参数的回调函数,将参数传给模板,看看模板是否能正确回调函数。
void func(int& a)
{
cout << "func(int& a) 左值引用" << endl;
}
void func(const int& a)
{
cout << "func(const int& a) const 左值引用" << endl;
}
void func(int&& a)
{
cout << "func(int&& a) 右值引用" << endl;
}
void func(const int&& a)
{
cout << "func(const int&& a) const 右值引用" << endl;
}
template<class T>
void perfectForward(T&& val)
{
// 调用函数
func(val);
}
int main()
{
int a = 10;
const int b = 10;
// 左值
perfectForward(a);
perfectForward(b);
// 右值
perfectForward(move(a));
perfectForward(move(b));
return 0;
}
- 输出结果:
从结果可以看出全是左值相关的func函数,模版出问题了吗???实则不然。
模版是不会出现问题的,问题必出现在模版传入参数的属性上。val不管是左值,还是右值,传给func函数后都是左值属性了,左值传给func不言而喻了,为啥右值传给func后,属性变成左值???右值引用后的变量是具有指向资源和地址的,它本质就是左值,因为右值不可以取地址,左值可以取地址。这样就说的通了。如何保持保持值得属性,不改变它,这就需要使用完美转发,完美转发本质是函数模版。即forward函数。
template<class T>
void perfectForward(T&& val)
{
// 调用函数
func(forward<T>(val));
}
运行结果:
可以发现结果与预期相同。
注意:forward为函数模版,需制定函数模版类型T,确保参数正确传递。
2.2 实际应用
完美转发实际应用中较为广泛,特别是在链表中,可以规避较多无意义的拷贝行为。
下面来看看它的实际应用。
#pragma once
#include<assert.h>
namespace A
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> self;
node* _node;
__list_iterator(node* n)
:_node(n)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
template <class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
//it = erase(it);
erase(it++);
}
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
void insert(iterator pos, const T& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._node->_prev;
node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
return iterator(next);
}
private:
node* _head;
};
}
主函数中只需创建一个 list 对象,,查看 移动构造是否被正确调用
测试移动构造是否被正确调用
int main()
{
A::list<A::string> l;
l.push_back("Hello World!");
return 0;
}
运行结果:为两次深拷贝
第一次是构造本身构造,第二次是插入时传参的构造。
// 右值引用版
void push_back(T&& x)
{
// 完美转发
insert(end(), std::forward<T>(x));
}
// 右值引用版
void insert(iterator pos, T&& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
// 完美转发
node* new_node = new node(std::forward<T>(x));
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
list_node(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
要想让我们之前模拟实现的 list 成功进行 移动构造,需要增加:一个移动构造、两个右值引用版本的函数、三次完美转发,并且整个 完美转发 的过程是层层递进、环环相扣的,但凡其中有一层没有进行 完美转发,就会导致整个传递链路失效,无法触发 移动构造。但凡任何一层有左值属性,直接诶调用拷贝构造。
三. 新增类功能
3.1 默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器
会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
生成条件:
- 如果没有显示实现移动构造函数,且没有显示实现析构函数,拷贝构造函数,拷贝赋值重载任意一个。那么编译器会自动生成一个移动构造函数,默认生成的移动构造函数对于内置类型成员会逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动构造,如果实现调用移动构造,没有就调用拷贝构造。
- 如果没有显示实现移动赋值重载函数,且没有实现析构函数,拷贝构造函数,拷贝赋值重载中任意一个函数,那么编译器会自动生成一个默认移动赋值函数,默认生成的移动赋值函数对于内置类型逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动赋值函数,如果实现调用移动赋值重载,没有就调用拷贝赋值。
- 显示实现移动构造或移动赋值,编译器不再主动生成拷贝构造和拷贝赋值。
如何实现移动语义函数呢???
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
- 移动语义是否能延长临时对象(将亡值)的生命周期?
移动语义本身不会直接延长临时对象(将亡值)的生命周期,但通过特定机制(如绑定到右值引用)可以间接实现生命周期的延长。
- const引用延长生命周期问题
#include<iostream>
using namespace std;
int Add(int& x, int& y)
{
int m = x + y;
return m;
}
int main()
{
int a = 10, b = 30;
const int& ret = Add(a, b);//临时对象被绑定到了ret对象上,临时对象与ret对象共存亡。
//被const修饰的临时对象不可修改
//ret += 2;//error
return 0;
}
const引用不可以延长局部对象的生命周期,局部对象在函数早就被销毁了,不可访问。
3.2 新增关键字
default 关键字
功能:显式要求编译器生成默认的特殊成员函数(如默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等)。因为一些特殊原因导致默认函数没有生成,我们的确想要它,就可以使用default关键字,强制编译器生成。
class MyClass {
public:
MyClass() = default; // 显式要求编译器生成默认构造函数
MyClass(int x) {} // 自定义构造函数
// 编译器会自动生成析构函数、拷贝构造函数等(除非被删除)
};
delete关键字
功能:禁用类的某些特殊成员函数(如禁用拷贝构造、赋值运算符等),或禁用特定函数重载。在Linux中就见过单例类模式,只允许生成有一个类对象,此时我们需要将拷贝构造,赋值等具有生成对象功能的函数禁用,就可以使用delete关键字。
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁用拷贝构造
NonCopyable& operator=(const NonCopyable&) = delete; // 禁用拷贝赋值
};
// 禁止将int隐式转换为类类型
void func(double) {}
void func(int) = delete; // 调用func(1)会编译错误
额外:声明成员变量时,是可以给缺省值的,不给则是随机值。
手动简单实现一个Date类,此时成员变量没有给缺省值。
class Date
{
public:
Date()
{
//cout << "Date()" << endl;
}
~Date()
{
//cout << "~Date()" << endl;
}
int GetYear()
{
return _year;
}
private:
int _year;
};
int main()
{
Date d1;
cout << "d1.GetYear() = " << " " << d1.GetYear() << endl;
return 0;
}
输出结果:
可以看出是随机值。
此时我们给成员变量 _year 一个缺省值,设置为2025。再看下结果。
int _year = 2025;
输出结果:
随机值的危害:导致逻辑错误,代码维护性下降 等。建议:声明成员变量时,给缺省值。
四. 可变参数
可变参数允许程序员传递任意个参数,函数都可以接收和作出相应的行为。C语言的scanf,printf函数就设计可变参数,需要用户指定数据类型及参数数量,这样使用相当复杂和繁琐,假如我有100个甚至上万个参数呢,都显示写吗???显然太烦杂了,此时C++11引用可变参数模版,将上述繁琐工作交给编译器去完成。
4.1 可变参数包
求参数包个数:sizeof…(参数包名)
//sizeof...运算符去计算参数包中参数的个数
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}
可变参数包使用万能引用模版,既可以引用左值,又可以引用右值。
4.2 可变参数包解析
通过递归展开参数包,逐个处理每个参数,直至参数包为空时终止递归。解析可变参数包的核心方法是模板递归和折叠表达式(C++17)。
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
return 0;
}
输出结果:
图解:
注意:不可以递归这样操作:
if (args[0] == 0) return;
4.3 emplace系列函数
emplace系列函数增加参数为可变参数模版的版本函数。
emplace系列函数通过完美转发(Perfect Forwarding)将参数直接传递给元素的构造函数,在容器内存中直接构造对象,而非先创建临时对象再拷贝/移动到容器中。
总之:emplace函数功能比push_back函数更高效。
最佳实践:合理使用emplace系列函数可显著提升性能,但需权衡代码可读性与构造参数的明确性。在元素构造简单或需兼容旧代码时,insert系列函数可能更直观。
五. 最后
本文深入解析C++11核心特性:右值引用通过移动语义减少拷贝开销,结合move实现资源所有权转移;完美转发利用模板万能引用与std::forward保留参数值类别,优化链表等场景性能;新增类功能如默认移动语义、default/delete关键字提升代码健壮性;可变参数包与emplace函数通过模板递归和折叠表达式实现类型安全的高效参数处理。这些特性共同推动C++向零开销抽象迈进,适用于高性能库开发与资源敏感场景。