目录
一、右值引用
1.左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
// 左值:可以取地址
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0];
cout << &c << endl;
cout << &s[0] << endl;
// // 右值:不能取地址
// double x = 1.1, y = 2.2;
// // 以下几个都是常见的右值,常量临时对象,匿名对象
// 10;
// x + y;
// fmin(x, y);
// string("11111");
//
// cout << &10 << endl;
// cout << &(x+y) << endl;
// cout << &(fmin(x, y)) << endl;
// cout << &string("11111") << endl;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是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.左值引用与右值引用比较
左值引用总结:
✸ 左值引用只能引用左值,不能引用右值。
✸ 但是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;
}
右值引用总结:
✸ 右值引用只能右值,不能引用左值。
✸ 但是右值引用可以move以后的左值。
✸ move的本质是强制类型转换
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
// int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
string s("111111");//s是左值 “111111”是右值
string&& rrx4 = move(s);//左值move后给右值引用
// move的本质是强制类型转换
string&& rrx5 = (string&&)s;//与上句等同
return 0;
}
3.右值引用使用场景和意义
1️⃣ 传返回值
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
//构造函数
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 赋值重载
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;
}
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;//临时对象
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
bit::string s1;
s1 = bit::to_string(1234);
return 0;
}
输出结果:
string(char* str) //构造 bit::string s1;
string(char* str) //构造内部str对象
string& operator=(const string& s) -- 深拷贝 //赋值重载
对于返回值,编译器会生成临时对象,再对临时对象进行拷贝构造,对于临时对象进行拷贝构造这不就浪费了吗?为了更进一步优化,C++11新引入移动构造,以及移动赋值
// 移动构造
// 临时创建的对象,不能取地址,用完就要消亡
// 深拷贝的类,移动构造才有意义
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
swap(s);//转移资源
return *this;
}
int main()
{
bit::string s1;
s1 = bit::to_string(1234);
return 0;
}
输出结果:
string(char* str)
string(char* str)
string& operator=(string&& s) -- 移动拷贝
我们先看编译器做的几种优化
情况一:string 没有移动构造,只有拷贝构造也就是C++11之前的
优化前:
✸ 传值返回会生成临时对象
✸ 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
优化后
✸ 编译器认为两次拷贝太浪费了,直接优化临时对象,发生一次拷贝
情况二:string有拷贝构造,也有移动构造C++11之后
优化前: ✸ str是一个左值(可以取地址)
✸ 临时对象是一个右值
✸ 把一次拷贝构造,变为移动构造
优化后:
✸ 没有临时对象
✸ 理应来说str是一个左值,应进行拷贝构造,但是这里把str隐式转换为右值,直接进行移动构造
✸ 相当于传值返回,没有拷贝了
更激进的优化
int main()
{
bit::string s1 = bit::to_string(1234);
return 0;
}
输出结果:
string(char* str) 直接构造,没有移动
ps:这两种情况是不一样的
//编译器再厉害也不行,这种情况还是要靠移动赋值与移动构造
int main()
{
bit::string s1;
s1 = bit::to_string(1234);
return 0;
}
int main()
{
bit::string s1 = bit::to_string(1234);
// s1 = bit::to_string(1234);
return 0;
}
我们之前使用的一直都是C++11/C++11以后的编译器,所以我们以前也并未提起相关知识,而是直接用,但我们现在学习了之后,发现不影响使用,该随便用就随便用,本次也只是介绍如果没有C++11,而是以前的,我们对于这种情况就很麻烦处理
2️⃣ STL中的应用
对于库中list
int main()
{
cout << "-------1-------" << endl;
list<bit::string> lt;
cout << "-------2-------" << endl;
bit::string s1("111111111111111111111");
cout << "-------3-------" << endl;
lt.push_back(s1);
cout << "-------4-------" << endl;
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "-------5-------" << endl;
lt.push_back("3333333333333333333333333333");
cout << "-------6-------" << endl;
lt.push_back(move(s1));
return 0;
}
输出结果:
-------1-------
-------2-------
string(char* str)
-------3-------
string(const string& s) -- 深拷贝
-------4-------
string(char* str)
string(string&& s) -- 移动拷贝
-------5-------
string(char* str)
string(string&& s) -- 移动拷贝
-------6-------
string(string&& s) -- 移动拷贝
对于我们之前手搓链表我们加上移动版本push_back 和 insert
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T())
:_next(nullptr)
,_prev(nullptr)
,_data(data)
{}
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(move(data))
{}
};
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
int main()
{
cout << "-------1-------" << endl;
bit::list<bit::string> lt;
//这里会由于创造哨兵位头节点 发生构造+拷贝构造
cout << "-------2-------" << endl;
//左值
bit::string s1("111111111111111111111");
cout << "-------3-------" << endl;
//左值
lt.push_back(s1);
cout << "-------4-------" << endl;
//右值
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "-------5-------" << endl;
//右值 产生临时对象
lt.push_back("3333333333333333333333333333");
//右值
cout << "-------6-------" << endl;
lt.push_back(move(s1));
cout << "-------7-------" << endl;
bit::string&& r1 = bit::string("22222222222222222222222222222");
// r1(右值引用本身)的属性是左值还是右值?-> 左值
return 0;
}
输出结果:
-------1-------
string(char* str)
string(const string& s) -- 深拷贝
-------2-------
string(char* str)
-------3-------
string(const string& s) -- 深拷贝
-------4-------
string(char* str)
string(string&& s) -- 移动拷贝
-------5-------
string(char* str)
string(string&& s) -- 移动拷贝
-------6-------
string(string&& s) -- 移动拷贝
-------7-------
string(char* str)
注意:由于move以后本身的属性还是左值,所以只要使用到右值,我们都需要写一个右值版本,少一个都不行,比如这里的节点构造、push_back、insert
编译器是能过自动匹配右值还是左值
void func(const bit::string& s)
{
cout << "void func(bit::string& s)" << endl;
}
void func(bit::string&& s)
{
cout << "void func(bit::string&& s)" << endl;
}
int main()
{
cout << "左值" << endl;
bit::string s1("1111111");
func(s1);
cout << "右值" << endl;
func((bit::string&&)s1);
cout << "右值" << endl;
func(bit::string("1111111"));
//强转成左值
// func((bit::string&)bit::string("1111111"));
return 0;
}
输出结果
左值
string(char* str)
void func(bit::string& s)
右值
void func(bit::string&& s)
右值
string(char* str)
void func(bit::string&& s)
4.完美转发
比如我们写一个Fun()函数,我们既要写它的左值版本,又要写它的右值版本,这是不是不太合适?所以C++引入完美转发
模板中的&& 万能引用(引用折叠)
###
```cpp
//函数模版中
template <class T>
void Func(T&& x)
{
}
```
对于
void Func(T&& x)
中的T&& x
,如果我们进行传值✸ 如果传的是一个右值类型,那么
T&& x
就是一个右值引用✸ 如果传的是左值类型,那么
T&& x
就是一个左值引用
使用关键字
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
// 模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给Fun
// 使用std::forward完美转发参数到另一个函数
Fun(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;
}
输出结果:
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
二、 新的类功能
1.默认成员函数
原来C++类中,有6个默认成员函数:
✸ 构造函数
✸ 析构函数
✸ 拷贝构造函数
✸ 拷贝赋值重载
✸ 取地址重载
✸ const 取地址重载
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
✸ 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
✸ 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
✸ 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class Person
{
public:
Person(const char* name = "111111111111", int age = 0)
:_name(name)
, _age(age)
{}
这里我们没有写析构函数 、拷贝构造、拷贝赋值重载
// 自动生成拷贝构造和移动构造
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
移动赋值测试,原理同移动构造
//Person s4;
// s4 = std::move(s2);
return 0;
}
如果我们加上析构函数
~Person()
{}
输出结果:
string(char* str)
string(const string& s) -- 深拷贝
string(const string& s) -- 深拷贝
注:一个函数如果需要显示写析构函数,说明有资源需要释放
✸ 说明需要显示写拷贝构造和赋值重载
✸ 说明需要显示写移动构造和移动赋值
自动生成的移动构造,对于Date这样的类,其实没有什么意义,因为它和拷贝构造的功能是一样的
自动生成的移动构造,对于Person这样的类是很有意义的,因为Person是右值时,他内部的string也是右值,string就可以走移动构造,提高效率了
2.类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了。
3.强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成,也就是算显示生成,因此我们需要额外注意一下默认成员函数的条件。
Person(Person&& p) = default;
Person& operator=(Person && p) = default;
Person(const Person& p) = default;
Person& operator=(const Person& p) = default;
4.禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "111111111111", int age = 0)
:_name(name)
, _age(age)
{}
// 只声明不实现,声明为私有
// C++98的做法
//private:
// Person(const Person& p);
// Person& operator=(const Person & p);
//C++11的做法
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
5.继承和多态中的final与override关键字
这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多台章节吧。