文章目录
1.左值、右值、左值引用、右值引用与move
1.1 左值和右值
左值:可以被取地址的值
右值:不可以取地址的值,临时对象
(1)赋值运算符=
(2)取地址运算符&
int a = 5;
&a;//&肯定要作用与一个左值,但它返回的是一个地址(指针),这个指针是一个右值,如0x008ffdd4
//如&123肯定不成立
(3 ) string和vector都用到[ ]下标访问运算符
(4)很多运算符都要用到左值
1.2 引用分类
1.2.1 左值引用(用&绑定到左值)
int value = 10;
int& refval = value;//value的别名是refval
1.2.2 const 引用(常量引用)
- 也是左值引用
- 引用那些不希望被改变的值
const int& refval2 = value;
refvl2 = 10;//错误,表达式是不可修改的左值
1.2.3 右值引用
- C++11中的新概念
int&& refrightvalue = 3;//绑定到一个常量
refrightvalue = 5; //还可以修改值
1.3 左值引用
- 左值引用就是引用左值的,换句话说,就是绑定到左值的引用
1.4 右值引用
- 用来绑定哪些“即将销毁/临时的对象”上
例如
int &r2 = i++;//i++右值表达式,右值表达式不能绑定到左值引用
- 引入目的:把复制对象编程移动对象从而提高程序运行效率
1.5 std::move函数
- 作用:把一个左值强制转换成右值(结果:一个右值引用可以绑定到这个转换成的左值上去了)
范例一
void fff(int&& vrc){}//形参是右值引用,需要绑定右值
//下面的函数可以放在主函数
int i = 10;
int&& ri20 = i; //不可以,因为i是左值,不能绑定到右值上去
int&& ri20 = std::move(i); //可以,所以move是把一个左值转换为一个右值
ri20 = 15; //可以
i = 25; //可以
范例二
int&& ri6 = 100; //可以
int&& ri8 = ri6; //不可以,ri8是右值引用,ri6是左值
int&& ri8 = std::move(ri6); //可以,move是把左值转换成右值
ri6 = 68; //可以
ri8 = 52; //可以
范例三
string st = "I love China!";
const char* p = st.c_str(); //0x008ff9d8
string def = std::move(st); //string的移动构造函数把st的内容转移到了def中去
//这句话和string def = st;一样,没提高效率
const char* q = def.c_str(); //0x008ff9b4
范例四
string st = "I love China!";
string&& def = std::move(st);//这个不会触发string的移动构造函数,st值不为空,只是将st转成右值绑定到def上
std::move可能的源代码
template<typename T>
decltype(auto)move(T&& param)
{
using ReturnType = remove_reference_t<T> &&;//一个右值引用类型
return static_cast<RReturnType>(param);
}
2.临时对象深入探讨、解析与提高性能手段
2.1 临时对象的概念
- 减少产生临时对象,提升程序性能和效率
2.2 产生临时对象的情况和解决方案
2.2.1 以传值的方式给函数传递参数
#include <iostream>
using namespace std;
class CTempValue
{
public:
int val1;
int val2;
public:
CTempValue(int val1 = 0,int val2 = 0); //构造函数
CTempValue(const CTempValue&t)
: val1(t.val1)
, val2(t.val2)
{
std::cout<<"调用了拷贝构造函数"<<std::endl;
}
virtual ~CTempValue()
{
std::cout<<"调用了析构函数"<<std::endl;
}
public:
int Add(CTempValue tobj); //普通成员函数
};
CTempValue::CTempValue(int v1,int v2)
: val1(v1)
, val2(v2)
{
std::cout<<"调用了构造函数"<<std::endl;
std::cout<<"val1 = "<<val1<<std::endl;
std::cout<<"val2 = "<<val2<<std::endl;
}
int CTempValue::Add(CTempValue tobj)
{
int tmp = tobj.val1 + tobj.val2;
tobj.val1 = 1000;//这里修改值对外界没影响
return tmp;
}
int main()
{
CTempValue tm(10,20); //调用构造函数
int Sum = tm.Add(tm); //导致执行拷贝构造函数
std::cout<<"Sum = "<<Sum<<std::endl;
std::cout<<"tm.val1 = "<<tm.val1<<std::endl;//tm.val1 = 10
return 0;
}
- 把成员函数修改下,减少拷贝构造函数的使用
int CTempValue::Add(CTempValue& tobj)
{
int tmp = tobj.val1 + tobj.val2;
tobj.val1 = 1000;//这里修改值对外界直接产生影响
return tmp;
}
2.2.2 类型转换生成的临时对象/隐式类型转换以保证函数调用成功
- 类型转换生成的临时对象
CTempValue& operator=(const CTempValue& tmpv)
{
//不能初始化列表,只有构造函数才有初始化列表
val1 = tmpv.val1;
val2 = tmpv.val2;
std::cout<<"调用了拷贝赋值运算符!"<<std::endl;
return *this;
}
int main()
{
CTempValue sum = 1000; //这里的=表示的是定义时初始化的概念
//这里定义了sum对象,系统就为sum对象创建了预留空间,然后用1000调用构造函数来
//构建临时对象的时候,这种构造就是为sum对象创建的预留空间进行的,所以并没有产生临时对象
return 0;
}
- 隐式类型转换以保证函数调用成功
int calc(const string& strsource,char ch)
{
const char* p = strsource.c_str();
int icount = 0;
//计算逻辑
return icount;
}
int calc(string& strsource,char ch)
{
const char* p = strsource.c_str();
int icount = 1;
//计算逻辑
return icount;
}
int main()
{
{
char mystr[100] = "I love China,oh,yeah!";
int result = calc(mystr,'o');
std::cout<<result<<std::endl;
}
{
string mystr = "I love China,oh,yeah!"; //不用产生string的临时对象,提升了效率
int result = calc(mystr,'o');
std::cout<<result<<std::endl;
}
{
const string mystr = "I love China,oh,yeah!";//C++会给const引用产生临时对象
int result = calc(mystr,'o');
std::cout<<result<<std::endl;
}
return 0;
}
2.2.3 函数返回对象的时候
CTempValue Double(CTempValue& ts)
{
CTempValue tmpm; //这里会消耗一次构造函数和析构函数
tmpm.val1 = ts.val1*2;
tmpm.val2 = ts.val2*2;
return tmpm; //这里调用了拷贝构造函数和析构函数,表示生成了临时对象
}
int main()
{
CTempValue ts1(10,20);
Double(ts1);
}
- 优化
CTempValue Double(CTempValue& ts)
{
return CTempValue(ts.val1*2,ts.val2*2);
}
int main()
{
CTempValue ts1(10,20);
CTempValue&& ts3 = Double(ts1);//优化:一次构造函数,一次拷贝构造函数,两次析构函数
//优化成:一次拷贝构造函数和一次析构函数
return 0;
}
2.2.4 类外的运算符重载之中的优化
class mynum
{
public:
int num1;
int num2;
public:
mynum()
{
cout<<"调用了构造函数"<<endl;
}
mynum(const mynum& t) //拷贝构造函数
{
cout<<"调用了拷贝构造函数"<<endl;
}
virtual ~mynum()
{
cout<<"调用了析构函数"<<endl;
}
};
mynum operator+(mynum& tmpnum1,mynum& tmpnum2) //类外的运算符重载
{
mynum result;
result.num1 = tmpnum1.num1 + tmpnum1.num2;
result.num2 = tmpnum1.num2 + tmpnum2.num2;
return result;
}
int main()
{
mynum tm1;
tm1.num1 = 10;
tm1.num2 = 100;
mynum tm2;
tm1.num1 = 20;
tm1.num2 = 200;
mynum tm3 = tm1 + tm2;
return 0;
}
- 优化
class mynum
{
public:
int num1;
int num2;
public:
// mynum()
// {
// cout<<"调用了构造函数"<<endl;
// }
mynum(const mynum& t) //拷贝构造函数
{
cout<<"调用了拷贝构造函数"<<endl;
}
virtual ~mynum()
{
cout<<"调用了析构函数"<<endl;
}
mynum(int x= 0,int y = 0)
: num1(x)
, num2(y)
{
cout<<"调用了构造函数"<<endl;
}
};
mynum operator+(mynum& tmpnum1,mynum& tmpnum2) //类外的运算符重载
{
return mynum(tmpnum1.num1 + tmpnum1.num2,tmpnum1.num2 + tmpnum2.num2);
}
- 这样少调用了一次构造和析构函数
3 对象移动、移动构造函数与移动赋值运算符
3.1 对象移动的概念
1)产生原因:
- 临时对象的产生肯定要面临大量数据的复制,例如把数据复制给临时对象,然后又把数据从临时对象复制出来等。这种复制显然会极大地影响程序运行效率,所以C++11会引入对象移动这个概念
2)作用:
- 把临时对象中有用的数据如new出来的一块内存接管过来,那么创建该类新对象就不用再new一块新内存。所有权转移之后,销毁临时对象时,已经转移出去的数据当然就不需要随之一起销毁了
3.2 移动构造函数和移动赋值运算符概念
- 转换数据的所有权从A到B
拷贝构造函数:
Time::Time(const Time& tmptime){...}
移动构造函数:形参是一个右值引用 &&
3.3 移动构造函数演示
#include <iostream>
class B
{
public:
B() //构造函数
: m_bm(100)
{
std::cout<<"类B的构造函数执行了"<<std::endl;
}
B(const B& tmp)
{
m_bm = tmp.m_bm;
std::cout<<"类B的拷贝构造函数执行了"<<std::endl;
}
virtual ~B()
{
std::cout<<"类B的析构函数执行了"<<std::endl;
}
int m_bm;
};
class A
{
public:
A()
:m_pb(new B()) //调用B类B的构造函数
{
std::cout<<"类A的构造函数执行了"<<std::endl;
}
A(const A& tmpa) //拷贝构造函数
: m_pb(new B(*(tmpa.m_pb))) //这要调用类B的拷贝构造函数
{
std::cout<<"类A的拷贝构造函数执行了"<<std::endl;
}
A(A&& tmpa)noexcept //移动构造函数(函数声明和函数实现而非你开的话,声明和实现都加noexcept,避免抛出异常)
: m_pb(tmpa.m_pb)
{
tmpa.m_pb = nullptr;//打断元对象a1中m_pb的指向内存
std::cout<<"类A的移动构造函数执行了"<<std::endl;
}
private:
B* m_pb;
};
static A getA()
{
A a;
return a;
}
int main()
{
//第一版本
A a = getA();
A a1(a);//调用了类A的拷贝构造函数
//第二版本
A a2 = (std::move(a));//调用类A的移动构造函数,a2指向的内存不用重新开辟了
//第三版本
A &&a3 = getA();//绑定从getA返回的临时对象到a3上
return 0;
}
3.4 移动赋值运算符演示
#include <iostream>
class B
{
public:
B() //构造函数
: m_bm(100)
{
std::cout<<"类B的构造函数执行了"<<std::endl;
}
B(const B& tmp)
{
m_bm = tmp.m_bm;
std::cout<<"类B的拷贝构造函数执行了"<<std::endl;
}
virtual ~B()
{
std::cout<<"类B的析构函数执行了"<<std::endl;
}
int m_bm;
};
class A
{
public:
A()
:m_pb(new B()) //调用B类B的构造函数
{
std::cout<<"类A的构造函数执行了"<<std::endl;
}
A(const A& tmpa) //拷贝构造函数
: m_pb(new B(*(tmpa.m_pb))) //这要调用类B的拷贝构造函数
{
std::cout<<"类A的拷贝构造函数执行了"<<std::endl;
}
A(A&& tmpa)noexcept //移动构造函数(函数声明和函数实现而非你开的话,声明和实现都加noexcept,避免抛出异常)
: m_pb(tmpa.m_pb)
{
tmpa.m_pb = nullptr;//打断元对象a1中m_pb的指向内存
std::cout<<"类A的移动构造函数执行了"<<std::endl;
}
A& operator=(const A& src) //第一个不加&传出去再执行一次拷贝构造函数
{
if(this == &src) //&表示取地址
return *this;
delete m_pb; //把原来内存释放掉
m_pb = new B(*(src.m_pb));//重新分配一块,加*是因为传入的是一个指针,解引用指向一个对象
std::cout<<"类A的拷贝赋值运算符执行了"<<std::endl;
return *this;
}
A& operator=(A&& src)noexcept
{
if(this == &src)
return *this;
m_pb = src.m_pb;
src.m_pb = nullptr;
std::cout<<"类A的移动赋值运算符执行了"<<std::endl;
return *this;
}
private:
B* m_pb;
};
static A getA()
{
A a;
return a;
}
int main()
{
//第一版本
A a = getA();
A a1(a);//调用了类A的拷贝构造函数
//第二版本
A a2 = (std::move(a));//调用类A的移动构造函数,a2指向的内存不用重新开辟了
//第三版本
A &&a3 = getA();//绑定从getA返回的临时对象到a3上
A a = getA(); //移动构造,临时对象直接构造在a
A a2; //普通构造
//a2 = a; //拷贝赋值运算符
a2 = std::move(a); //移动赋值运算符
return 0;
}
3.5 总结
- 1)如有必要,尽量给类添加移动构造函数和移动赋值函数,达到减少拷贝构造函数和拷贝赋值运算符调用的目的。当然一般来讲只有使用了大量内存的这种类才比较需要调用移动构造函数和移动赋值运算符
- 2)不抛出异常的移动构造函数、移动赋值运算符都应该加上noexcept,用于通知编译器该函数本身不抛出异常
- 3)一个对象移动完数据后当然不会自主销毁,但是程序员有责任使这种数据被移走的对象处于一种可以被释放(析构)的状态。例如 tmpa.m_pb = nullptr;
- 4)一个本该由系统调用移动构造函数和移动赋值运算符的地方,如果类没有提供,则系统会调用拷贝构造函数和拷贝赋值运算符代替
4.万能引用
4.1 类型区别基本概念
template<typename T>
void func(const T& abc){}
//主函数
func(10);
- 这里的T是int类型,abc是const int&类型
4.2 universal referrence基本认识(万能引用)
- 万能引用是一个类型
- 万能引用需要的语境:
1.必须是函数模板
2.必须是发生了模板类型推断并且函数模板形参长这样: T&&(类型模板参数,T跟&&必须挨着)
4.3 万能引用资格的剥夺与辨认
4.3.1 万能引用剥夺
- const修饰词会剥夺一个引用成为万能引用的资格,被打回原形成右值引用
template<typename T>
void myfunc(const T&& tmprv)
{
cout<<tmprv<<endl;
return;
}
//main
int i = 100;
myfunc(i);//不可以,只能传右值进去,必须是myfunc(std::move(i));
4.3.2 万能引用辨认(涉及类型推断才是万能引用)
template<typename T>
class mytestc
{
public:
void tescFunc(T&&c){};//这个不是万能引用,没有涉及到类型推断
};
//main主函数
mytestc<int> mc;
int i = 100;
mc.testFunc(i);//错,传入的是左值,要求用mc.testFunc(std::move(i));传入右值
- 优化
template<typename T>
class mytestc
{
public:
void testcFunc(T&& x){};
template<typename T2>
void testFunc2(T2&& x){}//T2类型是独立的,跟T没关系
};
//main函数
mytestc<int> myoc;
int i = 10;
myoc.testFunc2(i);//万能引用可以传左值
5.引用折叠、转发、完美转发与forward
5.1 引用折叠规则
- 编译器合并&时有个合并规则
& - & 左值引用
& - && 左值引用(举例)
&& - & 左值引用
&& - && 右值引用
-
所以:
void myfunc(int& &&tmprv){…}传入左值折叠后变成左值引用:
void myfunc(int& tmprv){…} -
总结规则:如果任意一个引用为左值引用,结果就为左值引用,否则为右值引用
5.2 转发与完美转发
template<typename F,typename T1,typename T2>
void myFuncTemp(F f,T1&& t1,T2&& t2)
{
f(t1,t2);
}
void myfunc(int&& v1,int& v2) //要运行的的话,这里的&&要去掉
{
++v2;
std::cout<<v1+ v2<<std::endl;
}
int main()
{
int i =100;
int&& youzhi = 80;//&&youzhi是右值引用,youzhi是左值
myFuncTemp(myfunc,20,i);//20是右值,j是左值
//现在需要解决:myFuncTemp中的v1变成右值才行,这样才能绑到myfunc2的v1上去
return 0;
}
- 所以引出完美转发这个换题,把myFuncTemp的v1变成右值才行
5.3 std::forward
- std::forward专门为转发而生,要么返回一个左值,要么返回一个右值
//完美转发
template<typename F,typename T1,typename T2>
void myFuncTemp(F f,T1&& t1,T2&& t2)
{
//针对myFuncTemp(myfunc2,20,j);调用:
//T1 = int,t1 = int&& 但t1本身是左值,因为是形参
//T2 = int& ,t2 = int&,
//f(t1,t2);
f(std::forward<T1>(t1),std::forward<T2>(t2));//T1和T2有很重要的信息,就是原始的实参到底是左值还是右值
//std::forward就是保证原始实参的左值或右值性
}
void myfunc(int&& v1,int& v2)
{
++v2;
std::cout<<v1+ v2<<std::endl;
}
int main()
{
int i =100;
myFuncTemp(myfunc,20,i);//20是右值,j是左值
//现在需要解决:myFuncTemp中的v1变成右值才行,这样才能绑到myfunc2的v1上去
return 0;
}
- 总结:20本来是右值,到了myFuncTemp里面去之后,向myfunc2转发的时候,f(t1,t2)中的t1变成了左值,是std::forward把它恢复成了右值身份
- 追加
int ix = 12;
int&& def = std::forward<int>(ix);//把左值绑定到右值才能绑定到def上
<>里面是int表示转成右值,为int&表示转为左值
5.4 std::move和std::forward的区别
- std::move无条件把左值转成右值
- std::forward在某种条件下强制执行,把转成右值或左值的恢复成左值或右值,但是通常需要配合模板类型参数,又要提供一个普通参数