右值引用
- MyString
- 浅拷贝与深拷贝
- 浅赋值与深赋值
- 左值与右值
- 左值概念
- 左值右值与函数的结合
- 移动构造函数
- 移动赋值函数
- 移动构造和移动赋值的应用
- 移动语义 有点问题
- 完美转发
- 引用叠加
MyString
浅拷贝与深拷贝
s1先在堆区申请了空间,然后将p指针指向的字符串复制到该堆区空间,
拷贝构造函数是s2指向了s1指向的空间,即指向了同一个堆区空间,会导致函数析构时,会对同一个空间释放两次,这就是浅拷贝
class MyString
{
private:
char* str;
public:
//strlen(p)只算了字符的长度,没有算上'\0'
MyString(const char* p = nullptr) :str(nullptr)
{
if (nullptr != p)
{
int len = strlen(p) + 1;
str = new char[len];
strcpy(str, p);
//strcpy_s(str,len,p); //C11 g++ 不支持
}
else
{
//相当一个空字符串
str = new char[1];
*str = '\0';
}
}
~MyString()
{
delete[]str;
str = nullptr;
}
MyString(const MyString& s) :str{s.str} {
cout << "Copy Create MyString :" << this << endl;
}
};
int main() {
MyString s1("yhpinghello");
MyString s2(s1);
return 0;
}
所以需要进行深拷贝,s2先申请堆区空间,然后再将原来s1的内容拷贝过去,这样析构s1,s2时就不会相互影响。
MyString(const MyString& s) :str{nullptr} {
int len = strlen(s.str) + 1;
str = new char[len];
strcpy(str, s.str);
cout << "Copy Create MyString :" << this << endl;
}
浅赋值与深赋值
MyString& operator=(const MyString& it) {
this->str = it.str;
return *this;
}
这个代码有两个问题,第一个是内存泄漏,str指向了it.str,那么str原本指向空间就找不到了,就无法释放了
第二个问题就是,这两个指针指向同一个地方,当调用析构函数释放空间时,当s2释放了他的空间后,s1仍然指向该空间,此时s1就是一个失效指针
浅赋值
深赋值
就是把s2指向的堆区资源释放,重新开辟一个和s1同样大小的堆区空间,再把s1内容拷贝过来
深赋值和深拷贝构造,的区别就是,多了一步,释放以前的资源
MyString& operator=(const MyString& it) {
if (this != &it) {
delete[]this->str;
int len = strlen(it.str) + 1;
str = new char[len];
strcpy(str, it.str);
}
return *this;
}
按照值返回,会构建将亡值对象,调用拷贝构造函数,深拷贝,回到主函数,调用深赋值函数,这个程序看,堆区开辟了三次,后文用移动拷贝,移动赋值优化。
MyString func(const char* p) {
MyString tmp(p);
return tmp;
}
int main() {
MyString s1("tulun");
s1=func("yhpinghello");
cout << s1 << endl;
return 0;
}
如果用引用返回的话,不构建将亡值,实质上就是将tmp的地址,用eax存着,再析构func,然后回到主函数,就是解引用了一次,将tmp深赋值给s1,但是tmp已经被释放了,从已经死亡的对象捞数据,这是不允许的
MyString& func(const char* p) {
MyString tmp(p);
return tmp;
}
int main() {
MyString s1("tulun");
s1=func("yhpinghello");
cout << s1 << endl;
return 0;
}
所以以值返回和引用返回具有巨大的差异,一般不要用引用返回。
左值与右值
左值概念
能取地址的量,对象,为左值
不能取地址为右值
将亡值:计算所产生的临时量,将亡值没有名字,就是右值,如果将亡值有名字就是泛型左值
纯右值:字面常量,由内置类型和指针运算产生的都是纯右值
类型加括号调用构造函数,创建不具名对象,就是右值
a++ 这是右值 返回的是不具名对象,
++a 这是左值 返回的是a本身
int main() {
MyString s1("yhping");//Left value
MyString& rs = s1;//左值引用
MyString("tulun");//不具名对象,右值
MyString &&rsa=MyString("tulun");//右值引用,引用不具名对象
return 0;
}
int main() {
const int& a = 10;
//int tmp=10;
//const int&a=tmp;
//a += 10;//error
int&& rr = 10;
//int tmp=10;
//int&&rr=tmp;
rr += 10;
return 0;
}
左值右值与函数的结合
void func(MyString &s){}//left ref
void func(const MyString &cs){}//const left ref
void func(MyString &&rs){}//right ref
int main(){
MyString s1("yhping");//Left value
func(s1);
return 0;
}
func(s1);中s1是一个左值,首先匹配void func(MyString &s){},没有该引用,退而求其次,匹配void func(const MyString &cs){},如果没有,就会报错,因为不能用s1去初始化右值引用
func(MyString("tulun"));
这里优先和右值引用匹配,没有的话,就和常左值引用匹配,再没有的话,不能匹配左值。
const MyString s1("yhping");//
只能和常性左值匹配,不能和左值匹配,不能和右值匹配
MyString&&rs=MyString("dataprint");
&rs
这里rs是右值引用,rs是不具名对象的别名,rs可以取地址,rs失去右值概念,所以这里变成了左值
常性左值引用是万能引用
移动构造函数
s1不就没有了
MyString(MyString&& s) {//移动拷贝构造
this->str = s.str;
s.str = nullptr;
}
int main()
{
MyString s1("yhping");
MyString s2(std::move(s1));
return 0;
}
move的作用就是将左值强转为右值
将s1的堆区资源移动给了s2
任何时候delete free一个空指针,都是安全的,它会先判断一下
移动赋值函数
问题:s2不就没有了
将堆区的地址移动给s1
MyString& operator=(MyString&& s)
{
if (this == &s)return *this;
delete[]str;
str = s.str;
s.str = nullptr;
return *this;
}
int main()
{
MyString s1("hello");
MyString s2("yhping");
s1=std::move(s2);
//s1=(MyString&&)s2;
return 0;
}
如果是深构造和深赋值
移动构造和移动赋值在实现上的区别还是,赋值需要先释放资源,防止内存泄漏
移动构造和移动赋值的应用
对于没有加static,const修饰的局部对象,return的时候直接将tmp转为右值,调用移动构造,s就是tmp一个别名,移动拷贝构造一个将亡值对象,将tmp的资源转移到将亡值对象,回到主函数,将亡值是右值,调用移动赋值,s就是将亡值的别名
用移动构造和移动赋值,创建对象的次数没有改变,但是对堆区的操作很简答,只用开辟一个空间。
MyString& operator=(MyString&& s)//移动赋值
{
if (this == &s)return *this;
delete[]str;
str = s.str;
s.str = nullptr;
return *this;
}
MyString(MyString&& s) {//移动构造
this->str = s.str;
s.str = nullptr;
}
MyString func(const char* p) {
MyString tmp(p);
return tmp;
}
int main() {
MyString s1("hello");
s1=func("yhping");
cout << s1 << endl;
return 0;
}
移动语义 有点问题
move实际上并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值,使我们可以通过右值引用使用该值,以用于移动语义。强制转换为右值的目的是为了方便实现移动构造。
using namespace std;
template<class _Ty>
struct my_remove_reference {
using type = _Ty;
my_remove_reference() {
type x;
}
};
template<class _Ty>
struct my_remove_reference<_Ty&> {
using type = _Ty;
my_remove_reference() {
type x;
}
};
template<class _Ty>
struct my_remove_reference<_Ty&&> {
using type = _Ty;
my_remove_reference() {
type x;
}
};
int main() {
my_remove_reference<int>();
my_remove_reference<int&>();
my_remove_reference<int&&>();
}
x 都是整型
template<class _Ty>
using my_remove_reference_t=typename my_remove_reference<_Ty>::type
int main() {
my_remove_reference_t<int>x;
my_remove_reference_t<int&>y;
my_remove_reference_t<int&&>z;
return 0;
}
xyz都是整型
template<class _Ty>
void fun(_Ty&& t) {
}
int main() {
int a = 10;
const int b = 20;
fun(a); // int &
fun(b); //const int&
fun(int(30));//int&&
return 0;
}
引用型别未定义,是模板的推演规则,不是右值引用,
fun传入什么类型的参数,t就是什么类型的引用
template<class _Ty>
my_remove_reference_t<_Ty>&& my_move(_Ty&& t) {
return static_cast<my_remove_reference_t<_Ty>&&>(t);
}
int main() {
MyString s1("yhping");
const MyString s2("tulun");
MyString s3, s4;
s3 = my_move(s1);
s4 = my_move(s2);
return 0;
}
s3 = my_move(s1); s1是左值,所以t就是 MyString& 然后删除_Ty的引用型别,static_cast<my_remove_reference_t<_Ty>&&>(t); =》 static_cast<MyString&&>(t);
我们就将左值强转成了右值
s4 = my_move(s2); 转成常性右值,就只能调用深赋值函数,因为常性左值引用是万能引用
如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用移动赋值函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。
这里也要注意对move语义的误解,move只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如int和char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move对于含资源的对象来说更有意义。
完美转发
void Print(int& a) { cout << "left value ref" << endl; }
void Print(const int& b) { cout << "const left value ref" << endl; }
void Print(int&& c) { cout << "right value ref" << endl; }
template<class _Ty>
void func(_Ty&& t) {
Print(t);
}
int main() {
int a = 10;
const int b = 20;
int&& c = 30;//
func(a);
func(b);
func(c);
func(40);
return 0;
}
c本来是右值引用,但是具名后,变成左值,当将亡值一旦具名变成了左值,因此我们要求在程序的连续调用过程中,值的类型不可变,所以就有完美转发的概念
void func(_Ty&& t) {
Print(forward<_Ty>(t));
}
所谓完美转发(Perfect Forwarding),是指在函数模板中,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的值类型转发。
引用叠加
template<class _Ty>
struct remove_reference {
using type = _Ty;
using _Const_thru_ref_type = const _Ty;
};
template<class _Ty>
struct remove_reference<_Ty&> {
using type = _Ty;
using _Const_thru_ref_type = const _Ty&;
};
template<class _Ty>
struct remove_reference<_Ty&&> {
using type = _Ty;
using _Const_thru_ref_type = const _Ty&&;
};
template<class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;
template<class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)noexcept {
return static_cast<remove_reference_t<_Ty> && (_Arg);
}
template<class _Ty>
_Ty&& my_forward(remove_reference_t<_Ty>& _Arg)noexcept {
return static_cast<_Ty&&>(_Arg);
}
template<class _Ty>
_Ty&& my_forward(remove_reference_t<_Ty>&& _Arg)noexcept {
return static_cast<_Ty&&>(_Arg);
}
void Print(int& a) { cout << "left value ref" << endl; }
void Print(const int& b) { cout << "const left value ref" << endl; }
void Print(int&& c) { cout << "right value ref" << endl; }
template<class _Ty>
void func(_Ty&& t) {
Print(my_forward<_Ty>(t));
}
int main() {
int a = 10;
const int b = 20;
int&& c = 30;
func(a);
func(b);
func(c);
func(40);
return 0;
}
func(a); _Arg => int& _Ty => int&
_Ty&& my_forward(remove_reference_t<_Ty>& _Arg)noexcept 删除_Ty的引用型别
将t给_Arg _Ty int& && 左值引用叠加右值引用仍然是左值引用 最后返回_Ty&& 继续叠加,就是左值引用 最后返回左值引用,调用void Print(int& a)
func(40); , _Ty是整型 _Arg 也是int _Ty&& 就是右值引用 返回叠加也是右值引用 最后_Ty&& 最后将_Arg 强转成右值引用,调用void Print(int&& c)