内大量创建并销毁对象,就会造成内存抖动,严重影响系统的稳定性,而且,我们的真正目的只是将两个变量的值交换一下而已。所以相较于拷贝,我们还有更好的选择:移动。
左值和右值
说起移动,就不得不提到左值和右值。这里的左和右是相对于=
来说的。
我们知道=
是用来赋值的,这下面隐藏着三个动作:生,取,写。在内存中生成一个临时数据,读取变量保存位置,将临时变量内容写入保存位置。生就是指的右值,它保存在我们不知道的内存位置,在写动作完成后,它就被回收了。而取对应的就是左值,我们用变量名保存了它的内存位置,在它作用域内可以反复读写。所以右值最大的特点就是不知道地址,如i=i+1
就会先生成一个i+1
的临时对象,我们不知道地址,所以它是右值。与之相对的左值,是可以通过&
读到地址的。
接下来我们再来谈一谈引用。我们通常是用别名来理解引用的,但是可能会忽略一个小细节,别名也是需要有归属的,也就是它代表的地址在哪里。基于这个前提,我们就可以推导出凡是存在内存中的数据,理都是有地址的,而右值是存在内存中的,它也应该需要一种方式来获得地址,称之为右值引用,相对的一般变量的引用就称为左值引用。
说回到移动,前面的复制构造函数虽然能将数据和其他对象共享,但是大部分情况下,数据其实不需要共享的,只需要转移,也就是将数据的所有权移动到另一个对象上,原始对象就不再有效。所以C++提供了移动构造函数来完成这个操作。
class Sample {
private:
int* value;
public:
Sample(const int value) :value{ new int{value} } {
std::cout << "Create value = " << value << std::endl;
}
Sample(const Sample& sample) :value{ new int {*sample.value} } {
std::cout << "Copy create object" << std::endl;
}
Sample(Sample&& sample) :value{ sample.value } {
sample.value = nullptr;
std::cout << "Move create object" << std::endl;
}
~Sample() {
delete value;
std::cout << "destory sample" << std::endl;
}
friend std::ostream& operator<<(std::ostream& os, const Sample& sample) {
os << "Sample value is " << sample.value;
return os;
}
};
void use(Sample sample) {
std::cout << "Use sample " << sample << std::endl;
}
int main() {
// 普通变量,1被使用后马上销毁了
int a = 1;
//左值引用
int& b = a;
//右值引用,引用的就是1那个暂存的地址
int&& c = 1;
//可以修改引用的值
c = 2;
Sample sample{ 1 };
use(std::move(sample));
std::cout << sample << std::endl;
}
// 输出
// Create value = 1
// Move create object
// Use sample Sample value is 009B8E90
// destory sample
// Sample value is 00000000
// destory sample
在上面的代码里,我们真正使用sample
对象的是函数use
,use
执行完后,sample
就没用了。所以我们用std::move
将数据转移到了函数实参中,外部的sample
不再拥有那块内存的占用。很多场景其实都是类似的情况:外部配置参数后,传递给某个函数使用,所以这种情况下就没必要构造一个新的对象出来,假如业务很长的话,sample
对象就会一直占用内存,但是它是早就没用了的。所以移动构造函数就发挥了大作用。
数据共享
除了通过复制构造函数和成员函数共享数据外,还可以通过友元类和友元函数。它们都是一种特殊的访问数据的形式,可以直接访问到数据,不经过成员函数的调用。所以在有些时候友元能帮助减少函数调用的花销,有些时候则会引入不可预期的行为。
class FriendClass {
public:
void useSample(const Sample& sample) {
std::cout << "Sample value is " << sample.value << std::endl;
}
};
上面的例子,如果按照常规是无法通过编译的,因为sample
的value
是私有的。前面我们知道,成员函数是可以访问私有变量的,但是这个类是定义在Sample
外的,这个函数是另一个类的成员函数,完全没办法完成这种访问。当然,这种情况下,我们可以修改Sample
类的定义,添加一个成员函数就解决了。但是假如FriendClass
有多个成员函数都需要访问Sample
的私有成员呢,这个时候添加成员函数的方式就不再适用,所以出现了友元类。
实现友元类很简单,简单到只需要添加一条声明。首先友元类需要至少两个类,一个类是想要访问私有成员的源类,另一个是含有私有成员的目标类,然后我们把友元声明放在目标类里,源类就可以顺利访问到目标类的私有成员了。在上面的例子FriendClass
想要访问Sample
的私有成员,所以它是源类,是普普通通的类。Sample
含有FeiendClass
想访问的私有成员value
,所以它是目标类,声明需要添加到它的类定义里面。
Class Sample{
private:
int value;
friend class FriendClass;
//其余不变
}
加上这一条之后,前面的FriendClass
就可以正常通过编译了。这一句的威力很大,大到FriendClass
的所有成员函数都能访问到value
。假如这不是你的期望,但是还是想要直接访问到value
,那么就可以适用友元函数。
友元函数是普通的函数,虽然它声明在类里,但是不能直接访问到类的私有成员,而是通过函数参数的形式。为了和普通的成员函数区分开来,它的声明最前面需要添加关键字friend
。friend
仿佛像打开了权限控制的开关,可以使函数访问到参数的私有成员。
class Sample{
friend std::ostream& operator<<(std::ostr