一、引用的基本概念
在介绍C++的引用机制之前,得先谈一下C++的前身,及C中的指针概念。如下图的c代码所示,所谓的指针,例如指针变量p,实际变量中保存的值是指向另一个变量a的地址,通过指针可以间接的操控真正的变量。通过这种间接的方式,可以减少变量在函数中的复制步骤,同时提供了一种突破变量空间的方法(即在函数内部依然可以修改函数外部的变量)。
在一般函数中,函数的形参实际是属于函数内部的局部变量,其作用域只限制于函数内部。在函数调用时,在未使用指针形参的情况下,函数的形参实际上对传入的变量复制了一份,如下图所示。将外部的变量a,b完全复制一份赋值给函数的内部变量i和j。复制操作是一种开销比较大的操作,如果要复制的变量是复杂的对象或或者结构,会消耗大量的存储空间,同时在函数内部无法修改函数外部的变量。
通过指针可以解决上述的变量复制问题以及内部修改外部变量的问题,如下图所示。通过指针,函数形参中的变量实际只是保存实参变量的地址,这样就避免了复制大数据的开销,同时指针可以在函数内部间接修改被指向的变量。
C++的引用,使用起来跟指针很像,使用引用变量就相当于在使用原变量一样,引用变量名好比是原变量的一个别名,和原变量具有同样的地址,并保存同一个内容,和指针不同,并不通过保存另一个变量的地址来间接指向。如下图所示,可以看到b是a的引用,引用b和a实际就是同一个变量,后续的代码既可以使用a也可以使用b来修改变量值,用法和普通变量完全相同,这是和指针不同的地方。
二、类对象复制机制
在c++中,引用分为左引用和右引用。
左引用申明方式为:&变量名;
右引用申明方式为:&&变量名;
通过如下的例子,先来看看左引用的使用,并观察在C++对象中存在复制的场景一般有哪些。
示例代码如下:
定义了一个简易的消类。
class Mymessage
{
public:
Mymessage(const char * msg="this is an default string!")
{
cout << " 构造函数开始处理" << endl;
size_t len = strlen(msg)+1;
p_messge = new char[len];
strcpy_s(this->p_messge, len, msg);
}
Mymessage(const Mymessage &msg)
{
cout << "复制构造函数开始处理" << endl;
size_t len = strlen(msg.p_messge) + 1;
p_messge = new char[len];
strcpy_s(this->p_messge, len, msg.p_messge);
}
Mymessage operator+(const Mymessage &msg)
{
cout << "加法操作函数处理" << endl;
size_t len = strlen(this->p_messge) + strlen(msg.p_messge) + 1;
Mymessage nmsg;
delete[] nmsg.p_messge;
nmsg.p_messge = new char[len];
strcpy_s(nmsg.p_messge, len, this->p_messge);
strcat_s(nmsg.p_messge, len, msg.p_messge);
return nmsg;
}
Mymessage & operator=(const Mymessage &msg)
{
cout << "赋值构造函数处理" << endl;
if (&msg != this)
{
size_t len = strlen(msg.p_messge) + 1;
delete[] this->p_messge;
this->p_messge = new char[len];
strcpy_s(this->p_messge, len, msg.p_messge);
}
return *this;
}
/* 使用右引用改写复制构造函数与赋值操作函数,如果是临时变量,会进入以下两个函数中
Mymessage(Mymessage &&msg)
{
cout << "复制移动构造函数开始处理" << endl;
this->p_messge = msg.p_messge;
msg.p_messge = nullptr;
}
Mymessage & operator=(Mymessage &&msg)
{
cout << "赋值移动构造函数处理" << endl;
if (&msg != this)
{
delete[] this->p_messge;
this->p_messge = msg.p_messge;
msg.p_messge = nullptr;
}
return *this;
}
*/
~Mymessage()
{
cout << "析构函数开始处理" << endl;
delete[] p_messge;
p_messge = nullptr;
}
void show()
{
cout << "the message is:" << this->p_messge << endl;
}
private:
char * p_messge;
};
可看到在类中定义了几个函数中都使用了左引用作为形参,如:Mymessage(const Mymessage &msg)(复制构造函数)、Mymessage operator+(const Mymessage &msg)(加法操作函数)、Mymessage & operator=(const Mymessage &msg)(赋值操作函数),其中在复制构造函数与赋值操作函数中,都使用了new函数来重新给对象申请空间,并使用了copy函数,这两个操作都是开销比较大的,可以看到使用做引用跟使用普通变量没有区别,并且使用引用可以直接修改。但上述例子应不需要修改传入的参数,因此加了const限制修改。
来看看运行的效果,如下是main函数及打印输出。
int main()
{
//std::cout << "Hello World!\n";
Mymessage msg1{"hello world!"};
Mymessage msg2{ "hello beutifaul world!" };
Mymessage msg3;
msg3 = msg1 + msg2;
msg3.show();
return 0;
}
打印如下:
我们感兴趣的 msg3=msg1+msg2这行代码,可以看上面的输出,在加法操作函数调用return语句时,实际上是将返回的结果复制给了该表达式的一个匿名临时变量,然后该变量又立即作为operator=()函数的参数,直到该表达式结束了,该匿名变量才销毁。试想下,调用一次operatro+()函数就会复制一个匿名变量,如果存在多次加法操作,那么复制的次数也会一同增加,同时内存消耗也一同增加。
通过上面的例子,可以总结出对象的复制一般出现在如下场景中:
构造函数中存在复制、赋值函数存在赋值、函数返回时存在复制;
要提升代码的性能,需要减少复制操作及空间分配操作,那么解决办法就是使用右引用。
三、右引用提升性能
还是使用上面的代码例子,只是将这部分的代码注释去除即可,如下:
上图表明,右引用之所以高效的原因,是因为在函数内部直接将指针指向了右引用变量中的有效据,好比发生了指针的移动,因此又把这类复制函数称为移动函数。
实际上,使用右引用的难点在于,如何保证程序能识别什么情况进入右引用的函数而不是进入到了其他函数中。这时需要对左右引用的变量做出解释,实际上左表示左操作数,而右表示右操作数。所谓左操作数,简单的理解为变量可以在=号左边并被赋值,而右操作数可简单理解为变量只能在=号右边并不能被赋值。那么很明显,普通变量(具有显式名称)会被识别为左引用,而匿名变量(一般是系统自动临时产生的)会被识别为右引用。
再次运行main函数,观察打印输出,可见符合预期。
四、使用右引用形参问题
前面的例子使用类对象比较简单,在其内部使用右引用的函数中,并没有继续将右引用参数或其内部的成员作为其他函数的参数进行传递。例如有如下函数
现通过完整代码与打印输出,来证明上述的结论:
class MyText
{
public:
MyText(const char * text = "this is an default string!")
{
cout << " 构造函数开始处理" << endl;
size_t len = strlen(text) + 1;
this->p_text = new char[len];
strcpy_s(this->p_text, len, text);
}
MyText(const MyText &text)
{
cout << "复制构造函数开始处理" << endl;
size_t len = strlen(text.p_text) + 1;
this->p_text = new char[len];
strcpy_s(this->p_text, len, text.p_text);
}
MyText operator+(const MyText &text)
{
cout << "加法操作函数处理" << endl;
size_t len = strlen(this->p_text) + strlen(text.p_text) + 1;
MyText ntext;
delete[] ntext.p_text;
ntext.p_text = new char[len];
strcpy_s(ntext.p_text, len, this->p_text);
strcat_s(ntext.p_text, len, text.p_text);
return ntext;
}
MyText & operator=(const MyText &text)
{
cout << "赋值构造函数处理" << endl;
if (&text != this)
{
size_t len = strlen(text.p_text) + 1;
delete[] this->p_text;
this->p_text = new char[len];
strcpy_s(this->p_text, len, text.p_text);
}
return *this;
}
// 使用右引用改写复制构造函数与赋值操作函数,如果是临时变量,会进入以下两个函数中
MyText(MyText &&text)
{
cout << "复制移动构造函数开始处理" << endl;
this->p_text = text.p_text;
text.p_text = nullptr;
}
MyText & operator=(MyText &&text)
{
cout << "赋值移动构造函数处理" << endl;
if (&text != this)
{
delete[] this->p_text;
this->p_text = text.p_text;
text.p_text = nullptr;
}
return *this;
}
~MyText()
{
cout << "析构函数开始处理" << endl;
delete[] p_text;
p_text = nullptr;
}
const char * getstr()
{
return this->p_text;
}
private:
char * p_text;
};
class MyMessage2
{
public:
MyMessage2(const char * text="this is a default string!")
{
cout << "Mymessage2默认构造函数开始处理" << endl;
m_text = MyText(text);
}
MyMessage2(const MyMessage2 &msg)
{
cout << "Mymessage2复制构造函数开始处理" << endl;
m_text = msg.m_text;
}
MyMessage2 & operator=(const MyMessage2 &msg)
{
cout << "MyMessage2赋值函数开始处理" << endl;
if (&msg != this)
{
this->m_text = msg.m_text;
}
return *this;
}
MyMessage2(MyMessage2 &&msg)
{
cout << "Mymessage2移动复制构造函数开始处理" << endl;
m_text = msg.m_text;
}
MyMessage2 & operator=(MyMessage2 &&msg)
{
cout << "MyMessage2移动赋值函数开始处理" << endl;
if (&msg != this)
{
this->m_text = msg.m_text;
}
return *this;
}
MyMessage2 operator+(const MyMessage2 &msg)
{
cout << "MyMessage2加法函数开始处理" << endl;
MyMessage2 nmsg;
nmsg.m_text = this->m_text + msg.m_text;
return nmsg;
}
void show()
{
cout << "the message is " << m_text.getstr() << endl;
}
~MyMessage2()
{
cout << "Mymessage2析构函数开始处理" << endl;
}
private:
MyText m_text;
};
int main()
{
MyMessage2 msg1{ "hello world!" };
MyMessage2 msg2{ "hello beutifaul world!" };
MyMessage2 msg3;
msg3 = msg1 + msg2;
msg3.show();
return 0;
}
我们重点是关注msg3=msg1+msg2这行代码,按照第三节的示例,在执行operator+()操作后会将返回值传递到一个匿名临时变量中,此时是进入了MyMessage2的移动复制函数中,MyMessage2(MyMessage2 &&msg)
{
cout << "Mymessage2移动复制构造函数开始处理" << endl;
m_text = msg.m_text; //会出问题,并不会触发移动函数
}
我们期望在该移动复制函数内部m_text=msg.m_text的这行代码,实际触发的也是MyText类的构造函数,但实际并没有。因为此时msg.m_text好比是一个非匿名变量,他系统默认为一个左值,因此实际出发的是MyText类的复制函数,如下图输出打印:
实际上不光是MyMessage2(MyMessage2 &&msg)内部在传递时会有问题,另一个函数 MyMessage2 & operator=(MyMessage2 &&msg)
{
cout << "MyMessage2移动赋值函数开始处理" << endl;
if (&msg != this)
{
this->m_text = msg.m_text; //会出问题,并不会触发移动函数
}
return *this;
}
要解决上面的问题,也很简单,通过引用std::move()函数,显示将变量标记为右引用即可,修正的代码如下: