C++基础_关于拷贝构造函数(复制构造函数)RVO

1. 基本概念

拷贝构造函数,或者叫复制构造函数,是构造函数的一种,本质作用是将对象初始化为一个特定的初始状态。只不过是一种特殊的构造函数,用已经存在的对象去初始化同类型的新对象。

在理解这个基本概念的基础上,再去看调用时机

2. 调用时机(触发对拷贝构造函数的调用)

一般而言有三种,前面两种都很好理解,第三种是这次要特别讨论的

  1. 定义一个对象时,以本类已存在的一个对象的状态初始化当前正在定义的对象
  2. 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象
  3. 如果函数的返回值是类的对象(非引用),函数执行完成返回主调函数时,将使用 return 的对象初始化一个临时无名对象,传递给主调函数

假如我们有一个Point类

class Point
{
private:
	int m_x, m_y;
public:
	// 普通构造函数
	Point(int x = 0, int y = 0) :m_x(x), m_y(y)
	{
		cout << "Constructor called for Point (" << m_x << "," << m_y << ")\n";
	}

	// 拷贝构造函数
	Point(const Point& p) :m_x(p.m_x), m_y(p.m_y)
	{
		cout<<"Copy Cosntructor called for Point("<< m_x << "," << m_y << ")\n";
	}
	// 基本信息显示函数
	void print()const
	{
		cout<<"Point("<< m_x << "," << m_y << ")\n";
	}
};

这是第一种调用时机,定义一个对象时,以本类已存在的一个对象的状态初始化当前正在定义的对象:定义 p2,且用 p1 初始化;定义 p3,且用 p1 初始化;
在这里插入图片描述
这是第二种调用时机,如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象。
因为函数的参数按值传递,所以传递的是对象本身,调用函数时会生成形参对象,且用实参初始化,此时会发生对拷贝构造函数的调用。
在这里插入图片描述

3. 关于第三种调用时机的讨论

关于第三种调用时机:如果函数的返回值是类的对象(非引用),函数执行完成返回主调函数时,将使用 return 的对象初始化一个临时无名对象,传递给主调函数。

3.1 低版本的两次拷贝构造函数的调用

在 C++ 11 之前的版本时候是这样的。以下代码在低版本中,会带来两次拷贝构造函数的调用:

Point createPoint()
{
	Point p(100, 100); 
	return p;          
}

int main()
{
	Point p4 = createPoint(); 
	return 0;
}

第一次,在函数的return语句中,用 p 初始化一个临时创建的返回对象中;
第二次,在 Point p4 = createPoint(); 语句中,用临时对象初始化新建的 p4。
这个过程因为发生局部对象的构造和析构,两次初始化,低效

3.2 返回值优化(RVO,Return Value Optimization)

所以后面发展出来了返回值优化(RVO,Return Value Optimization)的方案,并且在发展的过程中,编译器可以选择优化与不优化。

移动构造

最开始的优化思路是用“移动构造函数”,如果存在移动构造函数,编译器会优先使用移动构造函数,而不是调用拷贝构造函数。

先简单解释移动构造函数,为了避免上面复制资源构造对象的消耗和低效,那么可以转移资源而不是复制资源来构造对象:

移动构造转移资源的思路是:
1. 假设 Point 类使用动态内存来存储数据,类似于一个大的数据数组
2. 传统的拷贝构造会分配一块新的内存,并将每个数据从源对象复制过去
3. 移动构造函数只需“窃取”源对象的内存指针,避免了额外的内存分配和数据拷贝

如此,对于大对象、数组或动态内存,移动构造函数就可以避免耗时的复制

如果在返回对象的地方用 std::move(p),可以观察到这个场景下的一次拷贝构造函数调用
在这里插入图片描述
这里的 return move(p); 告诉编译器尽量移动对象 p 而不是拷贝它。
但是,Point 类没有定义移动构造函数
编译器会回退调用拷贝构造函数,所以有了一行拷贝构造函数的输出
在这里插入图片描述
如果Point 类定义了移动构造函数,在遇到 std::move(p) 时,会优先调用移动构造函数,如果有移动构造函数的定义,结果将看不到拷贝构造函数的调用,只有移动构造函数的调用,完整代码如下:

#include <iostream>
using namespace std;

class Point
{
private:
	int m_x, m_y;
public:
	// 普通构造函数
	Point(int x = 0, int y = 0) :m_x(x), m_y(y)
	{
		cout << "Constructor called for Point (" << m_x << "," << m_y << ")\n";
	}

	// 拷贝构造函数
	Point(const Point& p) :m_x(p.m_x), m_y(p.m_y)
	{
		cout<<"Copy Cosntructor called for Point("<< m_x << "," << m_y << ")\n";
	}

	// 移动构造函数
	Point(Point&& p) noexcept :m_x(p.m_x), m_y(p.m_y)
	{
		cout<<"Move Constructor called for Point(" << m_x << "," << m_y << ")\n";
	}

	// 基本信息显示函数
	void print()const
	{
		cout<<"Point("<< m_x << "," << m_y << ")\n";
	}
};

Point createPoint()
{
	Point p(100, 100); 
	return move(p);      // 调用移动构造函数          
}

int main()
{
	Point p4 = createPoint(); 
	return 0;
}

在这里插入图片描述

VS2022的强制优化,既无两次拷贝也无移动构造

但是,如果Point 类定义了移动构造函数,但是 也没有 去显式调用 移动构造函数,即没有 return move(p); 而是 return p;

代码回到最初的状态,VISUAL STUDIO 2022 编译器,强制优化,既无两次拷贝也无移动构造,而是直接在目标位置构造局部变量 p,避免拷贝或移动。

Point createPoint()
{
	Point p(100, 100); 
	return p;          
}

int main()
{
	Point p4 = createPoint(); 
	return 0;
}

return p; 位置,VISUALSTUDIO2022 编译器知道返回的 p 将要赋值给 main 中的 p4,于是就直接在 p4 内存位置上构造 p

所以只有一行普通构造函数的输出,这行输出发生在 普通函数createPoint() 函数中的 Point p(100, 100);

编译器在 p4 的内存地址上直接构造了 p,输出普通构造函数的消息,反映的是 p4 的构造过程。

完整代码如下:

#include <iostream>
using namespace std;

class Point
{
private:
	int m_x, m_y;
public:
	// 普通构造函数
	Point(int x = 0, int y = 0) :m_x(x), m_y(y)
	{
		cout << "Constructor called for Point (" << m_x << "," << m_y << ")\n";
	}

	// 拷贝构造函数
	Point(const Point& p) :m_x(p.m_x), m_y(p.m_y)
	{
		cout<<"Copy Cosntructor called for Point("<< m_x << "," << m_y << ")\n";
	}

	// 移动构造函数
	Point(Point&& p) noexcept :m_x(p.m_x), m_y(p.m_y)
	{
		cout<<"Move Constructor called for Point(" << m_x << "," << m_y << ")\n";
	}

	// 基本信息显示函数
	void print()const
	{
		cout<<"Point("<< m_x << "," << m_y << ")\n";
	}
};

Point createPoint()
{
	Point p(100, 100); 
	return p;  //           
}

int main()
{
	Point p4 = createPoint();  // 
	return 0;
}

在这里插入图片描述
虽然看起来像是有两个不同的构造函数调用(一个在 createPoint(),一个在 main() 中接收返回值),实际上只有一次构造调用的输出。编译器在 p4 的内存地址上直接构造了 p。

3.3 小结

随着发展,现代编译器往往会默认启用优化,有的编译器还可以通过一定方式去选择开启和禁用优化。但VS2022,会直接跳过拷贝构造或移动构造,直接在目标对象内存中构造返回对象,优化性能。具体视编译器而定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dotdotyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值