标签:右值 int 语义 左值 C++ 引用 test 构造函数
右值引用是C++11标准引入的特性,右值引用至少可以解决以下两个问题:
1、实现移动语义(move semantics)
2、完美转发(Perfect forwarding)
本次分享主要讲解移动语义。
讲上面的问题之前,先介绍一些相关的基础概念:
1)什么是左值和右值
左值:一个可以用来储存数据的变量,有实际的内存地址,表达式结束以后依然存在。
右值(非左值):是一个“匿名”的“临时变量,在表达式结束时生命周期终止。
我们见到的所有具名变量是左值,而匿名变量、字面常量是右值。左值可以出现在赋值符号的左边或右边,右值只能出现在赋值符号的右边。从定义也可以看出,左值具有一定的生命周期,右值的生命周期极其短暂。
左值示例:
void func()
{
int j = 0; // j 是左值
}
int main()
{
int i = 0; // i是左值。
void (*pFunc)() = func; // pFunc是左值。
int* ptr = nullptr; // ptr是左值
return 0;
}
右值示例:
int main()
{
// 字面量只能出现在赋值号的右边,因此它们都是右值。
// 比如说:1, 1.0, "abc"
int k = 1; // 1 是字面常量,是一个右值,这个表达式结束以后,1的生命周期就结束了
std::string("abcde"); // 一个匿名的临时变量,表达式结束生命周期结束,是一个右值
return 0;
}
int test(){
int a = 100;
return a;
}
// 函数test()的返回值是匿名临时变量,是一个右值
重要的注意事项:
对于一个变量来说,左右值是它的属性,和它的数据类型不是一码事。后面讲到的左值引用、常量左值引用和右值引用是变量的数据类型。
2)左值引用和右值引用的定义
左值引用的语法:
C++中左值引用和常量左值引用的语法是:
左值引用:类型 + & + 变量名
常量左值引用:const 类型 + & + 变量名
左值引用示例:
int main{
int a = 100; // a是一个左值
int &b = a; // b是左值引用
const int &c = a; // c是常量左值引用
}
左值引用的特点: 使用左值引用,相当于给左值变量起了一个别名。
常量左值引用的特点:左值和右值都可以用常量左值引用进行绑定,它是一个“万能”引用
右值引用的语法:
c++中右值引用的语法是:
类型 + && + 变量名
右值引用示例:
int main{
int &&b = 1000; // b是右值引用
return 0;
}
右值引用的特点:可以延长临时变量的生命周期,从而提升代码性能
在大部分的场景中,使用左值引用可以替代指针,C++11引入右值引用后,可以在有些场景中,优化代码,提升代码的运行效率。下面通过代码演示着重介绍右值引用的特点:
1)右值引用的第一个特点:
通过使用右值引用,右值可以“重获新生”,其生命周期与右值引用类型变量一样长,只要变量还活着,临时变量就会一直存在。下面通过例子来演示右值变量的生命周期:
class Test{
public:
Test() {
std::cout << "无参构造函数" << std::endl;
}
Test(const Test& test){
std::cout << "拷贝构造函数" << std::endl;
}
virtual ~Test() {
std::cout << "析构函数" << std::endl;
}
};
Test func(){
return Test();
}
我们通常使用如下方式调用函数func:
int main(){
Test test = func();
return 0;
}
因为g++编译时,会有返回值优化,所以编译的时候加上-fno-elide-constructors关闭返回值优化。输出结果如下:
无参构造函数
拷贝构造函数
析构函数
拷贝构造函数
析构函数
析构函数
通过输出结果可以看到拷贝构造函数调用了两次,一次是func函数返回对象创建临时对象产生的,一次是main函数中创建变量test时产生的,可以看出为了创建变量test总共需要调用三次构造函数。如果开启返回值优化,输出结果如下:
无参构造函数
析构函数
可以看出只进行了一次构造函数调用。但这个不是C++的标准,是各自编译器的优化规则。现在回到右值引用可以延长临时变量的生命周期,我们把上面的main函数代码修改为:
int main(){
Test&& test = func();
return 0;
}
输出结果:
无参构造函数
拷贝构造函数
析构函数
析构函数
这次创建test对象只进行了一次拷贝构造函数的调用,原因是:右值引用绑定了函数返回的临时对象(右值),让临时对象的生命周期得到了延长。利用这一点,我们可以对代码进行一些性能优化,即避免临时对象的构造和析构。补充:在没有引入右值引用之前,通常使用常量左值引用来做这个性能优化。main函数改成:
int main(){
const Test& test = func();
return 0;
}
输出结果:
无参构造函数
拷贝构造函数
析构函数
析构函数
这样可以达到和右值引用同样的效果,但缺点是无法修改对象的内部变量。
2)右值引用的第二个特点:
template<typename T>
void func(T&& t) {}
func(10); // t是右值引用类型
int a = 100;
func(a) // t是左值引用类型
T&&在发生自动类型推断的时候,它是未定义的引用类型,如果它被一个左值初始化,那么t的类型就是左值引用,如果它被一个右值初始化,那么t就是右值引用类型,即t的类型取决于初始化。特别提醒:T&&只有在自动类型推导的时候才是未定义引用类型。
移动语义
首先先看一个例子:
class Test{
public:
Test() : ptr(new int[20]{15}) {
std::cout << "调用无参构造函数" << std::endl;
}
Test(const Test& test) : ptr(new int[20]){
std::cout << "调用拷贝构造函数" << std::endl;
memcpy(ptr, test.ptr, sizeof(int)*20);
}
virtual ~Test() {
std::cout << "调用析构函数" << std::endl;
delete[] ptr;
}
private:
int *ptr;
};
Test GetTest(){
return Test();
}
调用函数GetTest创建对象:
int main(){
Test test = GetTest();
return 0;
}
输出结果如下:
调用无参构造函数
调用拷贝构造函数
调用析构函数
调用拷贝构造函数
调用析构函数
调用析构函数
结果显示,创建对象test共调用了两次拷贝构造函数,这意味着要进行两次资源的copy工作,虽然这样的代码是正确的,但有时资源的copy是不必要的,比如上面的例子。如果申请的堆内存很大的话,这种拷贝的代价会很大,带来的性能损失会很明显。右值引用的使用,可以解决这样的问题,为Test类添加移动构造函数,示例代码如下:
class Test{
public:
Test() : ptr(new int[20]{15}) {
std::cout << "调用无参构造函数" << std::endl;
}
Test(const Test& test) : ptr(new int[20]){
std::cout << "调用拷贝构造函数" << std::endl;
memcpy(ptr, test.ptr, sizeof(int)*20);
}
Test(Test&& test){
std::cout << "调用移动构造函数" << std::endl;
ptr = test.ptr;
test.ptr = nullptr;
}
virtual ~Test() {
std::cout << "调用析构函数" << std::endl;
delete[] ptr;
}
public:
int *ptr;
};
再次运行代码,输出结果如下:
调用无参构造函数
调用移动构造函数
调用析构函数
调用移动构造函数
调用析构函数
调用析构函数
在创建对象Test的过程中,调用了两次移动构造函数,意味着完成了两次资源的转移,避免了资源的深拷贝操作,这里的资源的转移,就叫做移动语义,其实就是资源的转移,从一个对象转移到另外一个对象。
通过上面的例子,我们知道移动语义是通过右值引用来实现的,那么普通的左值对象能否借助移动语义来完成资源的转移呢? 答案当然是可以的啦,C++11提供了std::move方法将一个左值转换为右值,从而实现移动语义。示例代码如下:
int main(){
Test test_1;
test_1.ptr[0] = 200;
Test test_2(std::move(test_1));
std::cout << test_2.ptr[0] << std::endl;
return 0;
}
输出结果如下:
调用无参构造函数
调用移动构造函数
200
调用析构函数
调用析构函数
这里需要注意对std::move方法的理解,move实际上并没有移动任何东西,它的唯一的功能是将左值强制转换成右值引用。通过上面的例子可以看出对于含有堆内存的对象,使用右值引用会变得很有意义。下面的例子是一个在实际场景中很常见的用法:
int main(){
std::string str_1 = "123456789";
std::string str_2(std::move(str_1));
std::cout << str_2 << std::endl;
std::cout <<"[" << str_1 <<"]" << std::endl;
return 0;
}
输出结果:
123456789
[]
可以看出str_1的资源转移到了str_2对象中,一般使用了std::move的对象进行资源转移以后,该对象就没有使用的价值了。
实际工程中,我们会频繁的把对象放入容器中,一般的操作如下示例:
class Test{
public:
Test() {
std::cout << "调用构造函数" << std::endl;
ptr = new int[20]{100};
}
Test(const Test& test){
std::cout << "调用拷贝函数" << std::endl;
x_ = test.x_;
y_ = test.y_;
z_ = test.z_;
ptr = new int[20];
memcpy(ptr, test.ptr, sizeof(int)*20);
}
int x_;
int y_;
int z_;
int *ptr;
};
int main(){
std::vector<Test> vec_test;
Test test;
test.x_ = 1;
test.y_ = 2;
test.z_ = 3;
vec_test.push_back(test);
return 0;
}
输出结果如下:
调用构造函数
调用拷贝函数
把一个左值对象放入vector里面,需要调用两次构造函数,一次发生在生成变量test,一次发生在调用push_back的时候。vector类自C++11以后,提供了支持右值引用的方法emplace_back,同时对push_back也重载了支持右值引用的方法。这是如果你直接使用vec_test.push_back(std::move(test))或者vec_test.emplace_back(std::move(test)),很遗憾的告诉你,结果和上面没什么区别。为什么呢?因为你没有在类中实现移动构造函数。现在让我们修改一下Test类,给它增加一个移动构造函数:
Test(Test&& test){
std::cout << "调用移动构造函数" << std::endl;
x_ = test.x_;
y_ = test.y_;
z_ = test.z_;
ptr = test.ptr;
test.ptr = nullptr;
}
main函数修改为:
int main(){
std::vector<Test> vec_test;
Test test;
test.x_ = 1;
test.y_ = 2;
test.z_ = 3;
vec_test.push_back(std::move(test));
// vec_test.emplace_back(std::move(test));
return 0;
}
输出结果如下:
调用构造函数
调用移动构造函数
可以看到调用了移动构造函数,test对象里面的ptr指向的资源得到了转移。
标签:右值,int,语义,左值,C++,引用,test,构造函数 来源: https://blog.csdn.net/weixin_42227520/article/details/110917997
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。