C++左值,右值,std::move,移动构造函数

一.左值和右值

左值和右值的概念偶尔就会听到,经常性碰到的是 “表达式是不可修改的左值”,这个问题对于初学者来说会经常遇到。那么左值和右值究竟是什么呢?

左值:从字面意思理解就是 ,出现在赋值语句左边的内容,它要代表一个地址

int i=10;

我们就但看这行代码,我们定义了一个int型变量,系统为它在内存中分配了一块空间(地址),i就是一个左值,再来看看右值,右值是为了更好的解释左值而引入的,常量就是一个右值,比如上面的10;

i=i+1;

再来看这行代码,根据上面的说法,i出现在赋值语句的左边,所以i一定是左值,但是它又出现了赋值语句的右边,这又该怎么去理解呢,这里我们能不能说i也是右值呢,显然是不可以的。注意:这里的i出现在等号的右侧,可以称i具有右值属性(右值属性不是右值),当它出现在等号的左边时,我们又称它具有左值属性,此时我们可以称左值既可以有左值属性,也可以有右值属性。

 我们来进一步了解一下自增运算符(前置、后置)哪个是左值,哪个是右值,或者还有其他的情况,我们都知道前置自增是先进行自增,然后再进行别的运算,它的这个自增是对变量本身进行操作的,所以前置加加是左值,后置自增是先进行别的运算,再自增,后置自增实际上是先生成一个临时变量,对这个临时变量进行加一的操作,最后返回的是这个临时变量,因此后置自增是一个右值,每个左值在内存中都有自己对应地址,我们可以通过取地址运算发来判断是否是左值。函数的返回值(因为函数的返回值是一个局部变量,一旦离开函数,函数体内的临时变量就会被自动销毁,因为返回的值也一样)也是一个右值

int reference()
{
	int a = 10;
	return a;
}
void test5()
{
	int b = reference();//函数的返回值(右值)
}

另外还有左值表达式和右值表达式,不要被其字面意思搞混了,左值表达式就是左值,右值表达式就是右值。

二.左值和右值引用

左值引用就是绑定到左值的引用,用一个&符号

int a = 10;
int& ref = a;//把ref和a绑定到一起
ref = 10;//可以通过ref来修改a的值

右值引用就是绑定到右值的引用,一般来说,就是要和那些临时变量/即将要销毁的变量绑定,通过&&实现

	int&& vai = 10;//右值引用,两个&&符号

    string &&str{"I love China."};//将str和字符串绑

再看下面的一行代码

int &&r1=10;
int &&r2=r1;//错误

此处应该注意,虽然&&r1和10绑定了,但是r1本身是左值,编译器会报错,无法将右值绑定到左值

三.std::move

move是c++11标准库里面的一个新函数,move翻译成移动,在这里就指的是把一个左值强制转换成右值,后面的移动构造函数中也会用到。

void func(int&& x)//定义一个需要传右值的函数
{

}
void test5()
{
	int r1 = 10;
	int&& r2 = 20;
	func(r1);//报错
	func(move(r1));//
	func(move(r2));
}

看上面的代码,我们先定义一个空的需要传入右值的函数,然后调用它,这时候就需要用std::move将r1或r2转换成右值传入,否则就会报错。

下面让我们来看看std::move对string类能否实现移动呢

void test6()
{
	string str1 = "I love China";
	const char* p = str1.c_str();//转换成c类型的指针
	string str2 = move(str1);
	const char* q = str2.c_str();
	cout << p << endl;
	cout << q << endl;

}

 观察运行结果,我们发现了str1变成了空串,str1的内容移动到str2里面去了,实际上这里的移动操作是触发了string类的移动构造函数,而不是move的功劳,我们继续看范例

string s1 = "I love China";
move(s1);//此处不会触发移动构造
cout << s1 << endl;


string &&s2 = move(s1);//此处不会触发移动构造函数
cout << s1 << endl;
cout << s2 << endl;

上面又列举了两种不会触发移动构造函数的写法,同时也说明move不具有移动的作用。 

四.移动构造函数:

移动构造函数是把一块内存中的数据从原来的所有者标记为新的所有者,如果原来的这块所有者是A,那么移动后这块数据的所有者就变成B了,此时对象A变得残缺了,原则上不要再去使用A对象。

在移动构造函数中,函数的形参传入的是右值引用。移动构造函数的写法:在完成移动构造函数时要完成资源的移动,一块对象中的内容移动到另一块中后,原来块中的数据将不在使用,下面通过代码来实现:

class Human
{
public:
	int m_age;
	string m_name;
	Human() :m_name("Tom"), m_age(10)
	{
		cout << "Human构造函数调用" << endl;
	}
	Human(const Human& h):m_age(h.m_age),m_name(h.m_name)
	{
		cout << "human的拷贝构造函数调用" << endl;
	}
	virtual~Human()
	{
		cout << "human析构函数调用" << endl;
	}
};
class Person:public Human
{
public:
	Person():hum(new Human())
	{
		cout << "Person的构造函数执行" << endl;
	}
	Person(Person&& p)noexcept :hum(p.hum)//移动构造函数
	{
		p.hum = nullptr;
		cout << "Person的移动构造函数"<<endl;
	}
	Person(const Person& p) :hum(new Human(*(p.hum)))
	{
		cout << "Person的拷贝构造函数" << endl;
	}
	Person& operator=(Person& p)
	{
		if (&p == this)
			return *this;
		delete hum;
		hum = new Human(*(p.hum));
		cout << "Person的普通=重载函数" << endl;
		return *this;
		
	}
	Person& operator=(Person&& p)noexcept
	{
		if (&p == this)
			return *this;
		delete hum;
		hum=p.hum;
		p.hum = nullptr;
		cout << "Person的=移动重载运算符调用" << endl;
		return *this;
	}
	~Person()
	{
		if (hum)
		{
			delete hum;
			hum = NULL;
		}
		cout << "Person的析构函数执行" << endl;
	}
private:
	Human* hum;
	
	
};
void test4()
{
	Person p;
	cout << endl;
	Person p1(move(p));
	cout << endl;
	
	Person p3;
	p3= move(p);
	cout << endl;
}

在这里打个断点观察一下,发现运行到此处时,p的内容已经变为空了,可以说p处于一种被释放的状态,不能再使用p,

这里说明一下,person类以public方式继承human类,human类的析构函数最好每次都写成虚函数,不管有没有父类指针指向子类对象的情况,万一有时候自己没写成虚函数而有父类指针指向子类对象的情况,此时就无法调用子类的析构函数。

        通常我们会在移动构造函数中加入关键字 noexcept 需要注意它放置的位置,第三块那里的析构函数是因为在调用重载=时,delete了hum,导致调用了Human的析构函数

五.万能引用和完美转发

        下面再来看看最后一个左右值的应用,那就是完美转发,就是在传递参数的时候依然能保持自己的左右值属性,先说明一下这里的万能引用T&&.下面看测试。

void func(int& m)
{
	cout << "这是一个左值" <<m<< endl;
}
void func(int&& m)
{
	cout << "只是一个右值" << m<< endl;
}

        先准备好上面这两个重载的普通函数, 首先说明一下这两个函数是可以根据传入的参数区分左右值的。

int main()
{
	int a = 9;
	func(a);
	func(8);
	return 0;
}

        在主函数中进行调用,很明显第一个是左值,第二个是右值。

         这个时候我想去实现用一个函数实现识别这种区分左右值的功能,于是产生了下面两种想法。

void func1(T &val)
{
	func(val);
}

        和

void func1(T &&val)
{
	func(val);
}

         这时候再去运行测试,发现 & 会报错,而&&能够接收传入的左值和右值参数。 

        接下来就是要引入完美转发的关键字forward<>() ,完美转发是使用模板实现的。

template <typename T>
void func1(T&& val)
{
	func(forward<T>(val));
}

        这就是完美转发,能够识别左值右值并将其发送到合适的函数。 

六.总结:

        在有必要的情况下尽量添加移动构造函数和移动构造赋值运算符,达到减少拷贝构造和赋值运算的目的,当然,一般只有在new分配了大量内存的这种类才需要移动构造函数和移动赋值运算符。不抛出异常的移动构造函数、移动赋值运算符都应该加上关键字noexcept。一个本该调用移动构造函数和移动赋值运算符的地方,如果类中没有提供,系统会调用拷贝构造函数和拷贝赋值运算符代替。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
std::move是C++11中的一个标准库函数,用于将一个强制转换为引用。通过使用std::move,可以告诉编译器一个对象可以被移动而不是复制,从而提高程序的性能。std::move是一个类型转换函数,它不会真正移动数据,只是将转换成引用。 std::forward也是C++11中的一个标准库函数,用于完美转发参数。当我们希望将一个函数的参数传递给另一个函数时,我们可以使用std::forward来保持参数的属性。std::forward根据传入的参数类型来决定是将参数作为引用还是引用进行传递。 引用是C++11中引入的一个新的引用类型。引用指向一个具名的对象,而引用则可以绑定到一个临时对象或将要销毁的对象。引用的一个重要应用是移动语义,通过将资源所有权从一个对象转移到另一个对象,避免了昂贵的资源拷贝操作。 移动构造函数是一种特殊的构造函数,用于在对象的移动操作中进行资源移动而不是拷贝。在C++11中,当一个对象被移动时,编译器会首先尝试调用其移动构造函数移动构造函数需要一个引用作为参数,并将其它对象的资源移动到当前对象中,然后将原来的对象置为有效的但未知的状态。 综上所述,C++11中的std::move和std::forward以及引用与移动构造函数都是为了实现移动语义而引入的新特性。它们可以提高程序的性能,避免不必要的资源拷贝,以及实现更高效的对象移动操作。但是在使用时需要注意正确的使用方式和避免潜在的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值