C++临时对象与移动构造函数

本文详细解释了C++中临时对象的概念,涉及构造函数、析构函数、深拷贝、移动构造函数以及编译器的返回值优化和复制消除。重点讨论了移动构造如何提高性能和资源管理,以及右值引用和移动语义在优化中的应用。
摘要由CSDN通过智能技术生成

临时对象

1、概念:

​ 没有对象名,用完立即释放

2、示例代码:
#include <iostream>
#include <string.h>
using namespace std;

class Person
{
private:
    char* name = nullptr;
    int age;
public:
    // 默认构造
    Person()
    {
        cout << "调用 默认构造函数" << endl;
    }

    // 带参构造
    Person(const char* name,int age):name(new char[strlen(name)+1]),age(age)
    {
        strcpy(this->name,name);
        cout << "调用 name-age 带参构造函数" << endl;
    }

    // 析构函数
    ~Person()
    {
        if(this->name != nullptr)
        {
            delete []this->name;
            this->name = nullptr;
        }
        cout << "调用 析构函数" << endl;
    }

    // 深拷贝
    Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
    {
        strcpy(this->name,p.name);
        cout << "调用 深拷贝" << endl;
    }

};

Person get_Person()
{
    return Person{"小龙",18};
}

int main()
{
    Person p = get_Person();
    return 0;
}
3.运行结果:

在这里插入图片描述

g++ 1-临时对象.cpp -std=c++11 -fno-elide-constructors	// 禁止优化
执行: Person p = GetPerson(); 这段代码会调用 2 次拷贝构造函数 和一次构造函数

g++ 1-临时对象.cpp -std=c++11
执行: Person p = GetPerson(); 这段代码会调用 1 次构造函数

-fno-elide-constructors:编译选项,用于禁用编译器的返回值优化和复制消除,
在C++中,编译器会尝试优化返回值的创建,尤其是在临时对象的情况下,
通过省略拷贝构造函数的调用来避免不必要的对象复制。这种优化称为返回值优化,它可以显著提高程序的性能,减少内存消耗。


Person p = GetPerson();过程分析
	1.进入 GetPerson() 函数,执行 Person{"小龙",18}; 这段代码将会调用 name-age 带参构造函数,定义初始化一个临时对象
	2.return 这个临时对象: 此时,刚刚创建的临时对象 会被拷贝构造函数将他赋值给 GetPerson() 函数的返回值对象(又一个临时对象)
	3.此时,第一次通过 name-age 带参构造函数 创建的这个临时对象作用结束,进行消亡,执行它的析构函数
	4.Person p = get_Person(); 当函数返回了一个临时对象时,又调用了一次拷贝构造函数,将返回的这个临时对象赋值给 新的对象p。
	5.赋值结束后,被返回的这个临时对象的作用结束,进行消亡,执行该对象的析构函数。
	6.对象p 的生命周期结束,调用 p 的析构函数。
4.编译器的返回值优化和复制消除
实质: 尽可能减少或消除不必要的对象复制操作,从而提高程序的性能和效率。 

具体实现: 通过在函数返回值的内部直接构造调用者要接收的对象,避免了中间对象的创建和拷贝,减少了不必要的内存和性能开销。
而复制消除是指编译器在编译过程中识别出不必要的对象复制,然后将其消除,避免了不必要的拷贝构造函数调用。

以 Person p = GetPerson(); 这段代码为例
eg:没有进行优化
①:
	调用get_Person(),创建临时对象1
	临时对象1.name = (char*)malloc(strlen("小龙")+1);
	strcpy(临时对象1.name,"小龙");
	临时对象1.age = 18;
	
②:
	进入return 创建临时对象2
	临时对象2.name = (char*)malloc(strlen(临时对象1.name)+1);
	strcpy(临时对象2.name, 临时对象1.name);
	临时对象2.age = 临时对象1.age;
	
③
	析构临时对象1
	free(临时对象1.name);
	
④	
	main 函数中 p 对象用来接收返回值
	p.name = (char*)malloc(strlen(临时对象2.name)+1);
	strcpy(p.name, 临时对象2.name);
	p.age = 临时对象2.age;
⑤
	析构临时对象2
	free(临时对象2.name);
	
⑥
	析构对象 p
	free(p.name);


eg:使用编译器优化
①
	调用 get_Person() 函数,不会产生临时对象的拷贝,返回的对象直接在 main 函数中的对象 p 的内存空间中构造。
	
②
	在 main 函数中,对象 p 的内存空间中直接存储了字符串 "小龙" 和年龄 18。
	
③
	在 main 函数结束时,对象 p 的析构函数被调用,释放 p 中的内存空间,不会有额外的析构步骤。
5.C++中右值引用
1.左值引用:
	指向左值的引用,使用符号 '&' 声明;
	左值: 指可以放在赋值运算符左边的表达式;
	左值引用可以绑定到左值,并且可以修改器所引用的对象。
2.右值引用:
	指向右值的引用,使用符号 '&&' 声明;
	右值:是临时性的,通常是临时对象、字面量、函数返回的临时结果等...;
	右值引用可以绑定到右值,但通常不可以修改其所引用的对象。
	
3.C++中左值引用和右值引用的区别:
1.左值引用指向左值,绑定到左值,右值引用指向右值,绑定到右值;
2.左值引用通常可以修改其引用的对象,用于传递可修改的对象,右值引用通常不可以修改其所引用的对象,用于支持移动语义和完美转发;

4.移动语义和完美转发:
移动语义和完美转发是 C++11 引入的两个重要概念,它们都涉及到改进和优化对象的传递和使用方式,提高了代码的效率和灵活性。
(1)移动语义:
	①允许在不进行深拷贝的情况下转移对象的资源所有权。
	②通过右值引用(Rvalue References)实现,允许程序员识别和利用临时对象(右值)。
	③移动构造函数和移动赋值运算符是实现移动语义的关键。通过这两个特殊的成员函数,对象可以从临时对象“窃取”资源,而不是进行昂贵的深拷贝操作。
	
(2)完美转发:
	①完美转发是一种技术,允许将参数以原样转发给其他函数,而不会丢失其值类别(左值还是右值)和常量性质。
	②在 C++ 中,使用模板和引用折叠等特性可以实现完美转发。通常结合模板函数或模板类来实现。
	③完美转发可以用于创建通用接口,例如,编写接受任意参数的函数或类模板时,可以使用完美转发来将参数转发给其他函数,从而保留其原始类型和特性。
	
移动语义使得资源管理更高效,而完美转发则允许编写更通用的代码,同时保留参数的类型信息和常量属性。
6.移动构造

移动构造的实质就是值传递,移动构造的过程中可能会改变当前对象中的值,所以,在移动构造中一般很少使用 const

右值引用就是为了去服务移动构造的

移动构造满足以下条件之后,编译器会给它写一个

1.没有用户声明的拷贝构造函数

2.没有用户声明赋值运算符重载

3.没有用户声明的移动构造函数

4.没有用户声明的析构函数

5.用户没有显示的声明移除移动构造

7.移动构造函数代码示例
#include <iostream>
#include <string.h>
using namespace std;

class Person
{
private:
    char* name = nullptr;
    int age;
public:
    // 默认构造
    Person()
    {
        cout << "调用 默认构造函数" << endl;
    }

    // 带参构造
    Person(const char* name,int age):name(new char[strlen(name)+1]),age(age)
    {
        strcpy(this->name,name);
        cout << "调用 name-age 带参构造函数" << endl;
    }

    // 析构函数
    ~Person()
    {
        if(this->name != nullptr)
        {
            delete []this->name;
            this->name = nullptr;
            cout << "释放堆空间" << endl;
        }
        cout << "调用 析构函数" << endl;
    }

    // 深拷贝
    Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
    {
        strcpy(this->name,p.name);
        cout << "调用 深拷贝" << endl;
    }

    // 移动构造函数
    Person(Person && p)
    {
        this->name = p.name;
        this->age = p.age;
        p.name = nullptr;
        cout << "调用移动构造函数" << endl;
    }

};

Person get_Person()
{
    return Person{"小龙",18};
}

int main()
{
    Person p = get_Person();
    return 0;
}
8.移动构造函数执行结果

在这里插入图片描述

9.结果分析
1.使用了移动构造函数:
	(1)调用带参构造函数创建了临时对象,然后临时对象被移动构造函数转移给了变量 p,因此会调用移动构造函数。
	(2)在移动构造函数中,指针 name 被转移,新对象的 name 指针指向旧对象 name 指针指向的空间,旧指针的 name 在使用完后直接指向 nullptr,
	通过两次调用移动构造函数,最终 p->name指向了最初的临时变量开辟的 name 空间。
	(3)最后,变量 p 的析构函数被调用,释放了 p 的资源。
	
2.未使用移动构造函数:
	(1)先调用带参构造函数创建了临时对象。
	(2)但由于没有移动构造函数,所以在返回临时对象时,会调用拷贝构造函数创建一个新的对象,这里开辟空间,进行深拷贝。
	(3)接着临时对象的析构函数被调用释放了临时对象的资源。
	(4)最后,变量 p 的析构函数被调用,释放了 p 的资源。
	
移动构造函数在使用过程中只是在更换指向堆空间的指针变量,而不需要每次都额外开辟堆空间,从而避免了不必要的深拷贝,提高程序的性能。
注意:在编写移动构造函数并涉及到堆空间的开辟时,一定要给该成员变量赋初值为 nullptr,并在临时变量使用完毕后将该成员变量指向 nullptr,
然后再 执行析构函数的时,对该成员变量进行 nullptr 值判断。否则,可能会造成调用析构函数,多次释放同一个空间!因为临时变量用完即销毁,
通过移动构造函数拿到的空间就是一个越界的空间!

移动构造函数在使用过程中只是在更换指向堆空间的指针变量,而不需要每次都额外开辟堆空间,从而避免了不必要的深拷贝,提高程序的性能。
注意:在编写移动构造函数并涉及到堆空间的开辟时,一定要给该成员变量赋初值为 nullptr,并在临时变量使用完毕后将该成员变量指向 nullptr,
然后再 执行析构函数的时,对该成员变量进行 nullptr 值判断。否则,可能会造成调用析构函数,多次释放同一个空间!
因为临时变量用完即销毁,通过移动构造函数拿到的空间就是一个越界的空间!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值