文章目录
- C++11
- 5.右值引用和移动语义
- 5.1左值引用和右值引用
- 5.2左值引用与右值引用比较
- 5.3右值引用使用场景和意义
- 5.4右值引用引用左值及其一些更深入的使用场景分析
- 5.5完美转发
- 6.新的类功能
C++11
5.右值引用和移动语义
右值引用是C++11引入的一个新特性,用于支持移动语义和完美转发。
在C++中,左值和右值是根据它们在表达式中的位置来定义的。左值是指在表达式中位于赋值符号左侧的对象,而右值是指位于赋值符号右侧的对象。例如,在表达式 a = b 中,a 是左值,b 是右值。
在C++11之前,左值和右值都是使用普通的引用(T&)来处理的。但是,这在处理某些情况时会造成一些不便,例如在实现移动构造函数和移动赋值运算符时。
为了解决这个问题,C++11引入了右值引用(T&&)的概念。右值引用是一种特殊的引用类型,只能绑定到右值上。这使得我们可以实现移动语义,即将资源从一个对象移动到另一个对象,而不是进行深拷贝。
右值引用还可以用于实现完美转发,即在函数模板中将参数以原样传递给其他函数,保持其原始的左值或右值性质不变。这需要使用 std::forward 函数来实现。
5.1左值引用和右值引用
什么是左值什么是右值?
左值和右值是根据它们在表达式中的位置来定义的。
左值是指可以位于赋值符号左侧的对象,它表示一个对象的身份(例如,一个具有名称的变量)。左值必须在内存中有实体,可以使用取地址符&获取其地址。
右值指的是位于赋值符号右侧的对象,它表示一个对象的值。右值可以是临时对象(临时变量),也可以是在内存或CPU寄存器中的值。当一个对象被用作右值时,使用的是它的内容(值),而不是它的身份。
简单来说,左值和右值的区别在于:左值表示一个对象的身份,而右值表示一个对象的值。左值可以取地址,右值不能取地址。
左值示例:
int a = 10; // a 是左值,因为它是一个具有名称的变量,可以在赋值符号左侧使用
int b = a; // a 是左值,因为它在赋值符号左侧
右值示例:
在这个例子中,a + b 的结果是一个临时值,没有具体的身份(变量名),只能在赋值符号右侧使用,因此它是右值。
int a = 10;
int b = 20;
int c = a + b; // a + b 的结果是右值,因为它是一个临时值,没有具体的身份(变量名)
什么是左值引用什么是右值引用?
左值引用(lvalue reference)就是对一个左值进行引用的类型。 具体来说,它是一个具有名称的变量的别名,可以通过该别名访问和修改该变量的值。左值引用使用 T& 表示,其中 T 是被引用的变量的类型。
右值引用(rvalue reference)就是对一个右值进行引用的类型。 由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用使用 T&& 表示,其中 T 是被引用的变量的类型。
左值引用示例:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。 定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
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;
}
右值引用示例:
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。 右值引用就是对右值的引用,给右值取别名。
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;
}
5.2左值引用与右值引用比较
左值引用总结:
(1)左值引用只能引用左值,不能引用右值。
(2)但是const左值引用既可引用左值,也可引用右值
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以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
5.3右值引用使用场景和意义
左值引用的场景:
(1)做参数:
当函数需要传递大的对象或复杂的数据结构时,可以使用左值引用来做参数传递,以避免对象的复制。这样可以提高函数的执行效率,并减少内存占用。例如:
在这个例子中,ComplexObject 是一个复杂的数据结构,使用左值引用来做参数传递可以避免对象的复制。
void foo(const ComplexObject& obj) {
// ...
}
(2)做返回值:
当函数需要返回一个大的对象或复杂的数据结构时,可以使用左值引用来做返回值,以避免对象的复制。这样可以提高函数的执行效率,并减少内存占用。例如:
在这个例子中,bar 函数返回一个 ComplexObject 类型的左值引用,以避免对象的复制。由于返回的是一个引用,调用者可以直接访问原始对象,而不是一个复制的副本。
ComplexObject& bar() {
static ComplexObject obj;
return obj;
}
综上,无论是做参数还是做返回值,它们都可以减少拷贝,提高效率。
以String的实现为例, 左值引用被用于实现字符串类的拷贝构造函数和赋值重载运算符。
在拷贝构造函数中,左值引用被用来接收一个传入的字符串对象(const string& s),这样可以避免对象的复制。通过创建一个临时对象(string tmp(s._str)),然后使用 swap 函数将临时对象的资源交换到当前对象,实现了深拷贝的效果。这里的左值引用作为参数传递方式,使得我们可以直接访问传入的字符串对象的成员变量。
在赋值重载运算符中,左值引用同样被用来接收一个传入的字符串对象(const string& s)。这里的左值引用的作用是使得我们可以将返回的对象赋值给当前对象。通过创建一个临时对象(string tmp(s)),然后使用 swap 函数将临时对象的资源交换到当前对象,实现了深拷贝的效果。最后,返回当前对象的引用(return *this),使得赋值操作可以连续进行。
综上,在这个代码片段中,左值引用被用于实现深拷贝的拷贝构造函数和赋值重载运算符,它使得我们可以直接访问传入的对象,避免对象的复制,提高了程序的性能。
// s1.swap(s2)
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;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。
下面是一个代码示例,演示了左值引用的短板:
class String {
public:
String(const char* str = "")
: _str(nullptr)
{
std::cout << "String(const char* str) -- 深拷贝" << std::endl;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& other)
: _str(nullptr)
{
std::cout << "String(const String& other) -- 深拷贝" << std::endl;
_str = new char[strlen(other._str) + 1];
strcpy(_str, other._str);
}
String& operator=(const String& other)
{
std::cout << "String& operator=(const String& other) -- 深拷贝" << std::endl;
if (this != &other) {
delete[] _str;
_str = new char[strlen(other._str) + 1];
strcpy(_str, other._str);
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
void printString(const String& str)
{
std::cout << "printString(const String& str)" << std::endl;
std::cout << str._str << std::endl;
}
int main()
{
String s1("Hello");
String s2 = "World";
printString(s1 + s2); // 这里会创建一个临时对象,无法用左值引用捕获
return 0;
}
在上面的代码中,我们定义了一个简单的 String 类,并实现了拷贝构造函数、赋值重载运算符和析构函数。在 main 函数中,我们创建了两个 String 对象 s1 和 s2,然后调用 printString 函数打印它们的拼接结果。
然而,在 printString 函数中,我们尝试通过左值引用来捕获传入的字符串对象,但是这里的传入参数实际上是一个临时对象(由 s1 + s2 创建),无法用左值引用捕获。这就是左值引用的短板之一,它无法绑定到临时对象或右值上,导致我们无法直接访问这些对象的资源。为了解决这个问题,C++11引入了右值引用的概念。
右值引用和移动语义解决上述问题:
使用右值引用和移动语义可以解决上述问题。具体做法是在 String 类中实现移动构造函数和移动赋值运算符,利用右值引用来捕获临时对象,并将其资源移动到当前对象中,避免深拷贝。
下面是修改后的代码示例:
我们在 String 类中实现了移动构造函数和移动赋值运算符,使用右值引用来捕获临时对象,并将其资源移动到当前对象中。在 main 函数中,临时对象就可以使用右值引用捕获,以便调用移动构造函数和移动赋值运算符。这样就可以避免深拷贝,提高程序的性能。
class String {
public:
String(const char* str = "")
: _str(nullptr)
{
std::cout << "String(const char* str) -- 深拷贝" << std::endl;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& other)
: _str(nullptr)
{
std::cout << "String(const String& other) -- 深拷贝" << std::endl;
_str = new char[strlen(other._str) + 1];
strcpy(_str, other._str);
}
String(String&& other)
: _str(nullptr)
{
std::cout << "String(String&& other) -- 移动构造" << std::endl;
_str = other._str;
other._str = nullptr;
}
String& operator=(const String& other)
{
std::cout << "String& operator=(const String& other) -- 深拷贝" << std::endl;
if (this != &other) {
delete[] _str;
_str = new char[strlen(other._str) + 1];
strcpy(_str, other._str);
}
return *this;
}
String& operator=(String&& other)
{
std::cout << "String& operator=(String&& other) -- 移动赋值" << std::endl;
if (this != &other) {
delete[] _str;
_str = other._str;
other._str = nullptr;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
void printString(const String& str)
{
std::cout << "printString(const String& str)" << std::endl;
std::cout << str._str << std::endl;
}
int main()
{
String s1("Hello");
String s2 = "World";
printString(s1 + s2); // 临时对象可以使用右值引用捕获
return 0;
}
5.4右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
但是有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。 C++11中,std::move()函数位于 头文件中,该函数可以将一个左值强制转化为右值引用,然后实现移动语义。
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
bit::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
bit::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
bit::string s3(std::move(s1));
return 0;
}
move函数使用示例:
void push_back (value_type&& val);
int main()
{
list<bit::string> lt;
bit::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
5.5完美转发
完美转发(Perfect Forwarding)是C++11引入的一种特性,它允许函数模板按照参数原来的形式(类型、值类别)将参数转发给其他函数,从而实现更灵活的编程。
完美转发的核心在于使用 std::forward 函数模板,它可以将左值转换为左值引用,将右值转换为右值引用,从而实现参数的完美转发。下面是一个简单的示例:
template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
在这个示例中,flip 函数模板接受一个函数对象 f 和两个参数 t1 和 t2,并将它们转发给函数对象 f,但是转发的顺序与原始顺序相反。通过使用 std::forward,我们可以保证参数的类型和值类别得到正确的保留和转发。
完美转发在很多情况下都非常有用,比如在实现代理函数、工厂函数、回调函数等场景中,它可以让我们更灵活地处理参数,并避免不必要的拷贝或移动操作。
完美转发在传参的过程中保留对象原生类型属性:
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;
}
完美转发的示例:
void print(int&& i) {
std::cout << "rvalue: " << i << std::endl;
}
void print(const int& i) {
std::cout << "lvalue: " << i << std::endl;
}
template<typename T>
void forwardPrint(T&& t) {
print(std::forward<T>(t));
}
int main() {
int i = 42;
forwardPrint(i); // lvalue: 42
forwardPrint(std::move(i)); // rvalue: 42
return 0;
}
6.新的类功能
默认成员函数:
原来C++类中,有6个默认成员函数:
(1)构造函数 (2)析构函数
(3)拷贝构造函数 (4)拷贝赋值重载
(5)取地址重载 (6)const 取地址重载
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
(1)如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
(2)如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
(3)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}