C++中临时对象的常见产生情况及其解决的方案

目录
  • 前言
  • 1. 以值传递的方式给函数传参
    • 验证临时对象的而外开销(1)
    • 解决方案
  • 2. 类型转换成临时对象 / 隐式类型转换保证函数调用成功
    • 验证临时对象的而外开销(2)
    • 解决方案
  • 3. 函数返回对象时候
    • 验证临时对象的而外开销(3)
    • 解决方案

前言

在C++中很容易就写出一些代码,这些代码的特点就是偷偷的给你产生了一些临时对象,导致临时对象会调用拷贝构造函数,赋值运算符,析构函数,假如该对象还有继承的话,也会调用父类的拷贝构造函数,赋值运算赋函数等。这些临时对象所调用的函数,都是不必要的开销,也就是说,我本意不想你给我调用这些函数的,但你编译器却给我偷偷的调用了,就是由于我程序员写代码产生临时对象而产生的。

所以临时对象产生的话题也应运而生,这篇文章主要是探讨常见的临时对象产生的情况,及其如何避免和解决这种临时对象产生的方式。

1. 以值传递的方式给函数传参

这种是最常见的产生岭师对象的方式了。

以值传递的方式给函数传参这种方式会直接调用对象的拷贝构造函数,生成一个临时对象传参给函数。当临时对象销毁时候,也是函数形参销毁,也是函数执行完后,就会调用该临时对象的析构函数。此时,无论是调用拷贝构造函数和析构函数,都是额外的开销。

(验证是否调用拷贝构造函数和析构函数,可以在书写拷贝构造函数和析构函数验证)
(验证是否为临时对象可以通过再函数内部修改形参的值,在函数外部打印看看是否修改成功)

验证临时对象的而外开销(1)

# include<iostream>
using namespace std;

class Person{
public:
	Preson()
	{
		cout << "无参构造函数!" << endl;
	}
	Person(int a)
	{
		m_age = a;
		cout << "有参构造函数!" << endl;
	}
	Person(const Person &p)
	{
		m_age = p.m_age;
		cout << "拷贝构造函数!" << endl;
	}
	~Person()
	{
		cout << "析构函数!" << endl;
	}
	int fun(Person p) //普通的成员函数,注意参数是以值的方式调用的
	{
		p.m_age = 20; //这里修改对外界没有印象
		return p.m_age;
	}
	int m_age;
};

int main()
{
	Person p(10);//初始化
	p.fun(p);
	return 0;
}

先来预测一下调用函数的次数:也就是我们本意想调用的方式:
会执行一次 Person的有参构造函数;
会执行一次Person的析构函数;

于此同时我们看看,编译结果实际情况:

和我们预期并不一样!!! 多了一次拷贝构造函数和一次析构函数。这两个函数并不是我们希望要得,或者说,这个多余函数开销是不必要的;

产生的原因也很好理解:

由于 fun成员函数里面的形参是Person p,这样会导致在调用这个fun函数时候,会传递过去的是实参的复制品,临时对象,并不是外面main函数的实参,这里可以在fun函数里修改一样形参就可以发现,外面的实参没发生改变。

所以产生的临时对象给形参传参时候,在我们看来类似 Person p = p;实际上是Person p = temp;而这句 Person p = temp;就会发生拷贝构造函数啦,于此同时 fun函数调用结束后,p的声明周期也就结束,所以还会多调用析构函数。

解决方案

如何避免这种临时对象的产生呢?

只要把值传递的方式修改为引用传递的方式即可。这样既不会调用拷贝构造函数,也不会调用多一次临时对象的析构函数。减少额外不必要的开销。

所以我们在函数形参设计时候,能够用引用就用引用的方式,因为这样可以减少对象的复制操作,减少而外的开销。

代码不验证啦,因为比较简单,可以自行验证,修改 fun函数里形参为 Person& p;即可。

2. 类型转换成临时对象 / 隐式类型转换保证函数调用成功

这种方式就是并且把类型转化前的对象当作了形参传递给构造函数,生成临时对象临时对象结束后就会调用析构函数。

验证临时对象的而外开销(2)

代码依旧是上一个代码,只是在main函数做了不一样的动作

# include<iostream>
using namespace std;

class Person{
public:
	Preson()
	{
		cout << "无参构造函数!" << endl;
	}
	Person(int a)
	{
		m_age = a;
		cout << "有参构造函数!" << endl;
	}
	Person(const Person &p)
	{
		m_age = p.m_age;
		cout << "拷贝构造函数!" << endl;
	}
	~Person()
	{
		cout << "析构函数!" << endl;
	}
	int fun(Person p) //普通的成员函数,注意参数是以值的方式调用的
	{
		p.m_age = 20; //这里修改对外界没有印象
		return p.m_age;
	}
	int m_age;
};

int main()
{
	Person p;
	p = 1000;
	return 0;
}

首先预测一下该代码执行的结果:

首先 调用一次无参构造函数,一次析构函数。

其次看看编译器运行的结果:

为啥会多出一个有参构造函数呢和析构函数呢?

其实是由于 p = 1000;这句引起的,这里p的类型为 Person,而 1000为 int 类型,很明显类型不一致。
编译器其实偷偷的进行了类型转换,如何转换呢?看编译器的调用都可以发现,其实就是创建一个临时对象,这个临时对象调用了有参构造函数,并且把 这个1000作为形参,传入有参构造函数,当这个函数调用结束后,对象也就销毁了,所以临时对象会调用析构函数。

解决方案

其实很简单的:
只要把单参数构造函数的复制(复制)语句,改为初始化语句就行。
那什么是复制语句和初始化语句呢?
两者的区别就是
一个是创建对象同时赋值对象,也就是说创建时候就马上初始化,这就是初始化;
一个是创建对象时候不赋值对象,而是等对象创建好,过后使用再赋值对象,这就是赋值语句啦;

那么我们只需要把:

	Person p;
	p = 1000;
	修改为:
	Person p = 1000;

这样就不会有多一次的有参构造和析构的开销了。

3. 函数返回对象时候

在函数返回对象时候,会创建一个临时对象接收这个对象;从而调用了拷贝构造函数,和析构函数。
当你调用函数,没有接收返回值时候,就会调用析构函数,因为都没有人接收返回值了,自然而然析构了。当你调用时候,有接收返回值时候,这个时候,并不会多调用一次析构函数,而是直接把临时对象返回值,给了接受返回值的变量来接收。

验证临时对象的而外开销(3)

代码:

# include<iostream>
using namespace std;

class Person{
public:
	Preson()
	{
		cout << "无参构造函数!" << endl;
	}
	Person(int a)
	{
		m_age = a;
		cout << "有参构造函数!" << endl;
	}
	Person(const Person &p)
	{
		m_age = p.m_age;
		cout << "拷贝构造函数!" << endl;
	}
	~Person()
	{
		cout << "析构函数!" << endl;
	}
	int fun(Person p) //普通的成员函数,注意参数是以值的方式调用的
	{
		p.m_age = 20; //这里修改对外界没有印象
		return p.m_age;
	}
	int m_age;
};
Person test(Person & p)
{
	Person p1; //这里会调用无参构造函数和结束的一次析构函数
	p1.m_age = p.m_age;
	return p1; //这里会多调用一次临时拷贝和析构函数
}
int main()
{
	Person p;
	test(p);
	return 0;
}

看看执行结果:

其实很好理解:就是以值的方式返回时候,就会多调用一次拷贝构造和析构函数;
结果中的第一个析构时test函数里p1对象的析构,第二个析构时 返回值时候临时对象的析构;第三个析构时main函数里p对象的析构;

请注意我的test函数在调用时候,我并没有给返回值,此时;当我以返回只接受时候,就会有不一样结果:不一样的地方就是,少了一次析构函数,其实少的这次析构函数时test函数里返回值产生的临时对象,因为,当你有对象接收返回值时候,就会直接把test函数里返回值临时对象给初始化接收返回值对象;

即,我修改main函数的代码:

int main()
{
	Person p;
	Person p2 = test(p); //此时test返回值临时对象并不会析构,
						//因为这里把临时对象直接初始化了p2;
	return 0;
}

可以说时编译器优化手段吧。本来说 p2对象因该也是需要调用多一次拷贝构造函数的,但是由于有临时对象的初始化,所以p2对象就直接接管临时对象了。所以上面结果最后的析构函数,其实时p2对象的析构,并不是临时对象的析构。

解决方案

其实也很简单的解决办法:有两种:

  • 当我们在接收函数返回的对象时候,可以用右值引用接收,因为该函数返回值是一个临时变量,用一个右值引用接收它,使得它的生命周期得以延续,这样就少调用一次析构函数的开销。(当然普通的对象接收也是可以)
  • 当我们在设计函数里的return 语句中,不是返回创建好的对象,而是返回我们临时创建的对象,即使用retturn 类类型(形参); 这个时候,就可以直接避免 return 对象;返回时候又要调用多一次构造函数。

这两种行为就可以避免了构造函数和析构函数的产生。

但是,右值引用我还没有写到这文章,所以先不讲右值引用的方案,讲第二种方案:
也就是设计函数返回语句 return时候,不要直接返回对象,而是返回临时对象,这个临时对象。

把这个代码修改:
Person test(Person & p)
{
	Person p1; //这里会调用无参构造函数和结束的一次析构函数
	p1.m_age = p.m_age;
	return p1; //这里会多调用一次临时拷贝和析构函数
}

修改为:
Person test(Person &p)
{
	return Person(p.m_age);//直接返回临时对象,可以减少
}

其实,只要以值得形式返回对象都会调用多一次拷贝构造函数,所以我们尽量避免这种情况,用合适的方式解决它。

到此这篇关于C++中临时对象的常见产生情况及其解决的方案的文章就介绍到这了,更多相关C++ 临时对象内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入c++中临时对象的析构时机的详解

    c++中,临时对象一旦不需要,就会调用析构函数,释放其占有的资源:而具名对象则是与创建的顺序相反,依次调用析构函数. c++源码: 复制代码 代码如下: class X  {public:   int i;   int j;   ~X() {}   X() {} }; int main() {    X x1;    X();    x1.i = 1;    X x2; } 对应的汇编码: 复制代码 代码如下: _main    PROC ; 11   : int main() { push  

  • c++ 临时对象的来源

    首先看下面一端代码: 复制代码 代码如下: #include <iostream> void swap( int &a,int &b) {     int temp;     temp=a;     a=b;     b=temp; } int main(int argc,char** argv) {     int a=1,b=2;     swap(a,b);     std::cout<<a<<"-----"<<b&

  • 详解C++ 临时量与临时对象及程序的相关优化

    一.临时量与临时对象 临时量: 内置类型生成的临时量是常量(临时量,寄存器带出来). 自定义类型生成的临时量是变量 ,在内存中. 隐式生成生成的临时量是常量 ,显式生成生成的临时量是变量 . 临时对象: 临时对象是系统临时分配的对象,在没主动声明所需对象而又使用其功能时产生的 显示对象:出现类型名 隐式对象:不出现类型名 注意: 临时对象的生存周期只在本条语句,临时对象一旦被引用,它的生存周期就和引用相同. 对象如何生成? 先分配内存 在调用构造函数初始化对象的成员变量  产生对象对象析构了 对

  • 详解C++ 中的临时对象

    C++中临时对象(Temporary Object)又称无名对象.临时对象主要出现在如下场景. 1.建立一个没有命名的非堆(non-heap)对象,也就是无名对象时,会产生临时对象. Integer inte= Integer(5); //用无名临时对象初始化一个对象 2.构造函数作为隐式类型转换函数时,会创建临时对象,用作实参传递给函数. 例: class Integer { public: Integer(int i):m_val(i){} ~Integer(){} private: int

  • C++中临时对象的常见产生情况及其解决的方案

    目录 前言 1. 以值传递的方式给函数传参 验证临时对象的而外开销(1) 解决方案 2. 类型转换成临时对象 / 隐式类型转换保证函数调用成功 验证临时对象的而外开销(2) 解决方案 3. 函数返回对象时候 验证临时对象的而外开销(3) 解决方案 前言 在C++中很容易就写出一些代码,这些代码的特点就是偷偷的给你产生了一些临时对象,导致临时对象会调用拷贝构造函数,赋值运算符,析构函数,假如该对象还有继承的话,也会调用父类的拷贝构造函数,赋值运算赋函数等.这些临时对象所调用的函数,都是不必要的开销

  • vue 数组和对象不能直接赋值情况和解决方法(推荐)

    Vue 不能检测以下变动的数组: 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength 当第一种情况需求时,可以使用this.$set(this.arr,index,newVal) Vue 不能检测对象属性的添加或删除: 可以使用this.$set(this.person,'age',12) 当需要添加多个对象时,Object.assign({},this.pers

  • iOS中打包上传常见的错误与解决办法

    一.ERROR ITMS-90535 首先这个原因导入了其他第三方导致的问题,首先找到友盟库里面的腾讯API,找到其中的info.plist文件: 找到箭头所指向的一行,随后删掉 这一行 就可以了: 二.ERROR ITMS-90635 这个是由于项目中有使用到Cocoapods导入第三方的库使用bitcode造成的,此种错误我在网上找到了三种解决办法: 方法一 项目->targets->enable bitcode->no pods->project->enable bit

  • javascript中Date对象应用之简易日历实现

    前面的话 简易日历作为javascript中Date对象的常见应用,用途较广泛,本文将详细说明简易日历的实现思路. 效果演示 HTML说明  使用type=number的两个input分别作为年和月的输入控件,这样在高级浏览器下自带调节按钮  按照周日到周一的顺序进行星期的排列 <div class="box"> <header class='control'> <input id="conYear" class="con-i

  • 基于JavaScript将表单序列化类型的数据转化成对象的处理(允许对象中包含对象)

    表单序列化类型的数据是指url传递的数据的格式,形如"key=value&key=value&key=value"这样的key/value的键值对.一般来说使用jQuery的$.fn.serialize函数能达到这样的效果.如何将这样的格式转化为对象? 我们知道使用jQuery的$.fn.serializeArray函数得到的是一个如下结构的对象 [ { name: "startTime" value: "2015-12-02 00:00:

  • 浅析PHP编程中10个最常见的错误

    目前学习PHP很多朋友,在平时的日常程序开发工程中总会遇到各种各样的问题,本篇经验将为大家介绍PHP开发中10个最常见的问题,希望能够对朋友有所帮助. 错误1:foreach循环后留下悬挂指针 在foreach循环中,如果我们需要更改迭代的元素或是为了提高效率,运用引用是一个好办法: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6,

  • 解析c++中参数对象与局部对象的析构顺序的详解

    下面是c++的源码: 复制代码 代码如下: class X  {public:   int i;   int j;   ~X() {} };void f(X x) {  X x1;  x.i = 1;  x.j = 2; }int main() {    f(X());} 下面是main函数的汇编码: 复制代码 代码如下: _main    PROC ; 15   : int main() { push    ebp    mov    ebp, esp    sub    esp, 8;为临时

  • hibernate中的对象关系映射

    Hibernate的本质就是对象关系映射(ObjectRelational Mapping),ORM实现了将对象数据保存到数据库中,以前我们对关系表进行操作,执行增删改查等任务,现在我们不再对关系表进行操作,而是直接对对象操作.hibernate中的ORM映射文件通常以.hbm.xml作为后缀.使用这个映射文件不仅易读,而且可以手工修改,也可以通过一些工具来生成映射文档.下面将对hibernate中的映射进行介绍. Hibernate映射分类,如下图所示. 1 基本类映射 根据实体类创建相应的表

随机推荐