C++学习之路-对象类型的参数和返回值

什么叫对象类型参数

顾名思义,参数类型是我们定义的类。我们知道int型参数,char型参数等,对象类型原理相同,即传入某个函数的参数是对象类型的。

class Car
{

public:
	Car() 	//构造函数
	{
		cout << "Car() - " << this << endl;
	}

	Car(const Car& car)	   //拷贝构造函数
	{
		cout << "Car(const Car& car) - " << this << endl;
	}

};

void test(Car car)
{
	//do something
}

对象类型参数的合理使用

但是,使用对象类型作为函数参数的情况下,可能会产生一些不必要的中间对象。下方,我们创建一个新的car1对象,然后传入函数test中

int main()
{
	Car car1; 
	test(car1);

	getchar();
	return 0;
}

打印下方(调用了构造函数和拷贝构造函数)

Car() - 000000B7F12FF974
Car(const Car& car) - 000000B7F12FFA54

调用构造函是因为Car car1,创建了一个新对象,所以调用构造函数。调用拷贝构造函数是因为在函数传参时执行了下面操作

void test(Car car = car1)
{
	//do something
}

很明显就是拷贝操作,利用已存在的对象car1创建新的对象car。其实,我们的目的就是把对象参数传进test,然后函数内部可以调用对象的内容就可以了,我们并不想调用拷贝构造函数。所以,在实际开发过程中,我们通常这样做:函数的参数设为引用形式,这样就是直接引用对象,而不是拷贝对象了

void test(Car &car)
{
	//do something
}

正好借此说一个以前的一个知识点:拷贝构造函数(博文)的形参形式是固定的,也就是下面这样:

Car(const Car &car)	 // 拷贝构造函数,参数是const类型的对象引用
{
	cout << "Car():(const Car &car)" << endl;
}

形参的格式是引用,且const修饰。再了解了上面的知识点后,想想为什么要这样写。

因为,如果这样写

Car(Car car)	 // 拷贝构造函数,参数是const类型的对象引用
{
	cout << "Car():(const Car &car)" << endl;
}

那我在对象传进去的过程中(形参传递过程中),执行了这样的操作:Car car = car1,即进行了拷贝操作。

Car car = car1;
Car(Car car = car1)	 // 拷贝构造函数,参数是const类型的对象引用
{
	cout << "Car():(const Car &car)" << endl;
}

这样就会导致一件事情的发生:调用拷贝构造函数时,参数传递过程中完成了拷贝操作会调用拷贝构造函数,然后。。。即陷入了死循环,所以拷贝构造函数的参数必须是引用,因为引用是直接引用对象而不会拷贝对象

对象类型返回值

顾名思义,函数返回的是对象类型的。即修饰函数的也是对象类型(在函数test2内部,创建一个Car对象,然后作为返回值)。然后在main函数中,创建一个car2对象用于接受这个返回值。

Car test2()
{
	Car car;
	// car do something
	return car;
}


int main()
{
	Car car2;
	car2 = test2();

	getchar();
	return 0;
}

我们运行一下,看看执行了哪些构造函数:可以看到,运行了两次构造函数,运行了一次拷贝构造函数

Car() - 0000001B7C71F6C4
Car() - 0000001B7C71F5A4
Car(const Car& car) - 0000001B7C71F7A4

执行了两次构造函数我可以理解。第一次是main函数中创建car2对象,第二次是test2函数中创建car对象。但是哪里进行了拷贝操作呢?

我们先分析一下,test2执行的过程:调用test2函数之后,由于car对象是在栈空间,所以会被销毁,car对象都被销毁了,怎么作为函数返回值传递给car2对象呢?所以,C++进行了这样一个操作:凡是在普通函数内部返回对象的,都先将对象拷贝至调用函数处的栈空间,从而达到调用函数完毕之后,对象的内容还在(对象不在了,但是对象已经把内容拷贝给该区域的一块内存了)。

所以tset2内部返回car的时候,就将car对象拷贝到main函数的某块内存区域,即使test2调用后销毁了,依然可以把car对象的内容保存下来赋值给car2。

底层是怎么干的呢?首先main函数在调用test2时,将main函数栈空间的一块内存区域的地址传递给test2函数,然后test2函数内部接收这个地址,

Car test2()
{
	Car car;
	// car do something
	*mian函数某块内存区域地址 = car
	return car(返回的其实是存储在main函数栈空间的一块内存,里面是car的内容);
}

int main()
{
	Car car2;
	car2 = test2(mian函数某块内存区域地址);

	getchar();
	return 0;
}

但是,如果代码这样写,就会发生不一样的情况(test2函数的返回值,直接赋值给一个新创建的对象car3,而不是像上面一样赋值给已经存在的car2对象)

Car test2()
{
	Car car;
	// car do something
	return car;
}


int main()
{
	Car car3 = test2();

	getchar();
	return 0;
}

按照之前的分析,这里应该是调用一次构造,调用两次拷贝构造。调用一次构造是因为test2函数内部的car对象创建,调用两次拷贝构造是因为test2函数内部会默认执行一次拷贝,而mian函数处Car car3 = test2();又是一次拷贝操作(利用已存在的test2产生的对象,创建新对象car3)。但是结果确实只调用了一次拷贝构造:

Car() - 0000004D6ECFF844
Car(const Car& car) - 0000004D6ECFF964

这是因为什么呢?我们可以看到两种调用函数的方式有一处不同就是:car2是提前创建好,然后等待接收test2返回值;car3是新创建的对象,然后等待接受test2返回值。

car2对象由于是提前创建好,所以在main函数中有自己的内存地址。而car3是新创建的,所以C++就将car3的地址直接传递给test2函数,然后test2函数内部直接将返回值拷贝给car3就行了。而不是像car2一样,需要传递一个额外区域,然后再将额外区域赋值给car2。

总结就是Car car3 = test2();这样调用,编译器会优化一步拷贝操作。

总结

尽量不要将对象作为函数的返回值,尽量不要直接将对象作为函数参数传递。返回值尽量不要用,对象作为参数时,要用引用或者指针,而不是直接对象值传递。不要用的目的就是,避免产生一些中间变量(也就是拷贝操作产生的多余变量)

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值