目录
一、左右值引用和移动语义
1、左值引用和右值引用
传统的
C++
语法中就有引用的语法,而
C++11
中新增了的右值引用语法特性,在C++11之前我们所学习使用的引用通常是指左值引用。
无论左值引用还是右值引用,都是给对象取别名
。
1)左值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式
(
如变量名或解引用的指针
)
,
我们可以获取它的地址
+
可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边
。定义时
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;
}
2)右值引用
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值
(
这个不能是左值引
用返回
)
等等,
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址
。右值引用就是对右值的引用,给右值取别名。
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是左值。如果不想
rr1
被修改,可以用
const int&& rr1
去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
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;
}
3、右值引用的使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么
C++11
还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
1)左值引用的使用场景及短板
左值引用的使用场景:
左值引用做参数和做返回值都可以提高效率。
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:
bit::string to_string(int value)
函数中可以看到,这里只能使用传值返回,
传值返回会导致至少
1
次拷贝构造
(
如果是一些旧一点的编译器可能是两次拷贝构造
)
。
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
int main()
{
// 在string to_string(int value)函数中可以看到,这里
// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
string ret1 = to_string(1234);
string ret2 = to_string(-1234);
return 0;
}
2)右值引用和移动语义解决左值引用的短板
在
上述string
中增加移动构造,
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
int main()
{
string ret2 = to_string(-1234);
return 0;
}
再运行上面
to_string
的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
不仅仅有移动构造,还有移动赋值:
在
string
类中增加移动赋值函数,再去调用
to_string(1234)
,不过这次是将
to_string(1234)
返回的右值对象赋值给
ret1
对象,这时调用的是移动构造。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
string ret1;
ret1 = to_string(1234);
return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。
to_string
函数中会先用
str
生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把
str
识别成了右值,调用了移动构造。然后在把这个临时
对象做为
to_string
函数调用的返回值赋值给
ret1
,这里调用的移动赋值。
大家可以去官网看一下STL的容器,都增加的有移动构造和移动赋值。
4、右值引用引用左值及其更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。
当需要用右值引用引用一个左值时,可以通过
move
函数将左值转化为右值
。
C++11
中,
std::move()
函数
位于 头文件中,该函数名字具有迷惑性,
它
并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
。
int main()
{
string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
string s3(move(s1));
return 0;
}
STL容器接口函数中也插入了右值引用版本。
void push_back(value_type&& val);
int main()
{
list<string> lt;
string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(move(s1));
return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
5.完美转发 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; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(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;
}
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;
}
完美转发实际中的使用场景:
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
二、新的类功能
默认成员函数
原来
C++
类中,有
6
个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前
4
个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11
新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
● 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。● 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载 中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。( 默认移动赋值跟上面移动构造 完全类似)● 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
类成员变量初始化
C++11
允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化
强制生成默认函数的关键字
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:
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:
string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
觉得这篇博客有帮助到你的话不妨点个赞支持一下,后面我会继续讲解有关C++11的知识,我们一起学习进步❤️