• 一文搞懂C++临时对象优化 附详细样例


    引入

    C语言和C++在程序执行中,都是通过调用一系列的函数来实现的。并且,很多时候,编译器会帮助我们做一系列的事情,比如(在编译类的成员方法的时候,编译器默认添加 this 指针,以此来确定是哪一个对象调用了该成员方法)。得益于编译器或者说系统帮助我们做的一系列事情,我们可以更加方便地使用C++。但是凡事有利必有弊,因为系统有时候会自己调用一系列的函数,从另一个角度来说,也一定程度上降低了效率。
    而我们想要提高C++的执行效率,就需要了解程序(主要是对象)使用过程中都调用了哪些方法。

    测试环境

    VS2019 x86环境下,编译器会影响最终结果的,如下面那个例子,VS2019 x86环境下是11个函数,而g++编译器则是9个函数,多余的优化下例有讲。

    例子

    #include <iostream>
    using namespace std;
    
    class Test
    {
    public:
    	Test(int a = 10)
    		:ma(a)
    	{
    		cout << "Test(int)" << endl;
    	}
    	~Test()
    	{
    		cout << "~Test()" << endl;
    	}
    	Test(const Test& t)
    		:ma(t.ma)
    	{
    		cout << "Test(const Test&)" << endl;
    	}
    	Test& operator=(const Test& t)
    	{
    		cout << "operator=" << endl;
    		ma = t.ma;
    		return *this;
    	}
    	int getdata()const { return ma; }
    private:
    	int ma;
    };
    
    Test GetObject(Test t) 
    {
    	int val = t.getdata();
    	Test tmp(val);
    	return tmp;
    }
    
    int main()
    {
    	Test t1;
    	Test t2;
    	t2 = GetObject(t1);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    对于上面的主函数,我们可以猜测一下一共调用了多少函数
    答案如下:

    Test(int)
    Test(int)
    Test(const Test&)
    Test(int)
    Test(const Test&)
    ~Test()
    ~Test()
    operator=
    ~Test()
    ~Test()
    ~Test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    共调用了11个函数
    我们可以来追踪一下

    Test GetObject(Test t) //3.Test(const Test& t)
    {
    	int val = t.getdata();
    	Test tmp(val);		//4.Test(int)
    	return tmp;			//5.Test(const Test&) 因为tmp是局部对象,是出不了该函数这个花括
                            //号的,故需要临时传给外面main函数中的一个临时对象才能传出。(最
                            //起码vs2019编译器是这样做的)经测试g++编译器会对此进行优化,少了
                            //main函数的那个临时对象,因此总共就9个函数
    }						//6.tmp析构 7.t的析构
    
    int main()
    {
    	Test t1;	//1.Test(int)
    	Test t2;	//2.Test(int)
    	t2 = GetObject(t1);//8.Test& operator=(const Test& t) 9.用来传递的那个临时对象的析构
    
    	return 0;
    }					//10和11分别为 t1 t2 的析构
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image.png
    接下来可以进行讲解:

    int main()
    {
    	Test t1;			//调用的是正常的构造函数
    	Test t2(t1);		//Test(const Test&)
    	Test t3 = t1;		//Test(const Test&)
    	Test t4 = Test(20); //显式生成临时对象,但是与Test t1(20)没有区别
    	t4 = t1;			//operator=
        t4=Test(30);		//Test(int)  operator=
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    Test(int)
    Test(const Test&)
    Test(const Test&)
    Test(int)
    operator=
    Test(int)
    operator=
    ~Test()
    ~Test()
    ~Test()
    ~Test()
    ~Test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    故对此可以总结:
    c++编译器对于对象的构造的优化:用临时对象生成新对象的时候,临时对象就不产生了,直接用临时对象的值去构造新对象。
    记住:只要是用临时对象去生成新对象的时候,临时对象都不会产生,不管是什么形式的临时对象。然而用临时对象去赋值已有对象时,临时对象便会生成。

    故基于如上讲解我们可以对刚开始的例子进行优化(注释的地方即为改动)

    Test GetObject(Test& t)
    {
    	int val = t.getdata();
    	//Test tmp(val);
    	return Test(val);
    }
    
    int main()
    {
    	Test t1;
    	//Test t2;
    	//t2 = GetObject(t1);
    	Test t2 = GetObject(t1);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 我们首先将函数的参数改为引用,对此节约了一个临时变量,函数-2
    2. 再取消tmp变量的构建,直接返回临时变量,同时把主函数中的t2的赋值操作改为初始化操作,触发优化条件:当用临时变量生成新变量的时候会进行优化,故省略掉了 tem变量 和 主函数中用来传递返回值的临时变量,函数-5(tmp 和 临时变量的构造和析构 t2的赋值)

    故得到如下结果:

    Test(int)
    Test(int)
    ~Test()
    ~Test()
    
    • 1
    • 2
    • 3
    • 4

    拓展

    Test *p = &Test(20);		//p指向的是临时对象了,到下一行时临时对象会析构,造成悬垂指针
    const Test &ref = Test(20);	//引用相当于给产生的临时对象赋予了别名,故临时对象不会销毁
    
    • 1
    • 2

    结论:用指针指向临时对象是不安全的,用引用指向临时对象是安全的。

    总结

    于是我们总结出对象优化的原则:

    1. 不能返回局部的或者临时对象的指针或引用
    2. 函数参数传递过程中,对象优先按引用传递,不要按值传递
    3. 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
    4. 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
  • 相关阅读:
    华为OD技术面试-最短距离矩阵(动态规划、广度优先)
    【微服务】SpringBoot启动流程注册FeignClient
    centos7 安装gcc boost 、cmake
    Android学习笔记 30. Service组件
    前端常见面试题
    【COMP329 LEC 2 Agent and Robot Architectures】
    事件循环的学习、执行上文、this、执行栈和任务队列
    Python argparse使用方法介绍
    版本特性 | Cloudpods v3.9重点功能介绍
    Go基础18-理解方法的本质以选择正确的receiver类型
  • 原文地址:https://blog.csdn.net/q2453303961/article/details/125532346