隐式类型转换/匿名对象/拷贝和赋值/编译器优化

概念区分

本篇文章主要是对于隐式类型转换和匿名对象的理解,其次重点是为了区分拷贝构造和赋值的区别,并以此引出对应的编译器优化

1.隐式类型转换

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用

接收单个参数的构造函数具体表现:

  1. 构造函数只有一个参数
  2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
  3. 全缺省构造函数

特别注明:C++11支持多参数的隐式类型转换

class Point
{
public:
	Point(int val)
		:_val(val)
	{
		cout << "Point(int val) -- 构造" << endl;
	}

	Point(const Point& p)
	{
		cout << "Point(const int& p) -- 拷贝构造" << endl;
		_val = p._val;
	}
	
	Point& operator=(const Point& p)
	{
		cout << "Point& operator=(const Point& p) -- 赋值" << endl;

		if (this != &p)
		{
			_val = p._val;
		}
	}

private:
	int _val;
};

int main() 
{
	// 正常方式 - 有名对象 - 生命周期在当前函数局部域
	Point p1(10);
	// 隐式类型转换 - C++支持单参数构造函数的隐式类型转换
	// 隐式类型转换生成的临时变量具有常性,所以引用要加const,引用传参也需要加const
	Point p2 = 10;
	const Point& p3 = 10;
	
	return 0;
}

隐式类型的转换作用体现在string

void Push_back(const string& s1)
{
	//
}

int main()
{
	// 如果不支持隐式类型转换,只能这样传参
	string s1("ZCDL");
	Push_back(s1);

	// 支持隐式类型转换 - 内部走了构造的隐式类型转换
	Push_back("ZCDL"); 

	return 0;
}

C++11支持多参数的隐式类型转换,注意:隐式类型转换的过程中会产生临时变量,可以直接传参,但是如果想要引用或者引用传参,那么需要加const

class Point
{
public:
	Point(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "Point(int val) -- 构造" << endl;
	}

	Point(const Point& p)
	{
		cout << "Point(const int& p) -- 拷贝构造" << endl;
		_a1 = p._a1;
		_a2 = p._a2;
	}
	
	Point& operator=(const Point& p)
	{
		cout << "Point& operator=(const Point& p) -- 赋值" << endl;

		if (this != &p)
		{
			_a1 = p._a1;
			_a2 = p._a2
		}
	}

private:
	int _a1;
	int _a2
};


int main()
{
	Point p1(1, 2);

	Point p2 = { 1, 2 };

	const Point& p3 = { 1, 2 };
	
	return 0;
}

如果想要禁止隐式类型转换,就要在构造函数加关键字explicit,禁止类型转换

2.匿名对象

匿名对象是在隐式类型被禁用后有作用,其特点为生命周期仅在这一行

有名对象特点:生命周期在当前局部域
匿名对象特点:生命周期只在这一行

void Push_back(const string& s1)
{
	//
}

int main()
{
	// 如果不支持隐式类型转换,只能这样传参
	string s1("ZCDL");
	Push_back(s1);

	// 支持隐式类型转换 - 内部走了构造的隐式类型转换
	Push_back("ZCDL"); 

	// 当隐式类型转换被禁用,使用匿名对象
	Push_back(string("ZCDL"));

	return 0;
}

匿名对象也常用于题目中调用函数Solution(). - LeetCode

3.拷贝和赋值的区别

拷贝和赋值不能靠是否是=来判断,有等号不一定时赋值

拷贝还是赋值要通过被赋值对象是否初始化来确定,用已经初始化的对象去初始化未初始化的对象为拷贝构造,即使有等号,对象是未初始化的也是拷贝构造
当对象已经初始化,再用已经初始的对象给等号为赋值

class Point
{
public:
	Point(int a1)
		:_a1(a1)
	{
		cout << "Point(int val) -- 构造" << endl;
	}

	Point(const Point& p)
	{
		cout << "Point(const int& p) -- 拷贝构造" << endl;
		_a1 = p._a1;
	}
	
	Point& operator=(const Point& p)
	{
		cout << "Point& operator=(const Point& p) -- 赋值" << endl;

		if (this != &p)
		{
			_a1 = p._a1;
		}
	}

private:
	int _a1;
};

int main()
{
	// 构造
	Point p1(10);

	// 拷贝构造的两种方式:
	// 1. 拷贝构造 - p2未初始化,用初始化的p1去初始化p2为拷贝构造(即使有等号也为拷贝构造)
	Point p2 = p1;
	// 2. 拷贝构造 - 直接方式
	Point p3(p1);

	// 赋值 - p4已经初始化,再用p1去初始化为赋值
	Point p4(20);
	p4 = p1;

	return 0;
}

结论:判断依据不能仅靠等号去判断是赋值还是拷贝,要看=对面的对象是否初始化,如果初始化就为赋值,未初始化就为拷贝

4.编译器优化

编译器优化:构造+拷贝构造优化成直接构造,拷贝构造+拷贝构造优化成直接拷贝构造(传参返回的深拷贝会出先两次拷贝构造,一次是销毁产生临时对象时会拷贝构造一次,另一次是给赋值的时候会进行一次拷贝构造,两次都是深拷贝,防止内存泄漏)

条件:同时性,要在同一行,即构造和拷贝构造同时进行(同一行即可),才会发生优化(同时性)
赋值:不会发生优化,因为赋值一定是给初始化对象的,所以赋值的过程中不会出现构造(两个不能写在同一行,写在同一行为拷贝构造),所以赋值和构造一定是分开进行的,一定不会出现优化

// 默认生成拷贝构造,对于内置类型完成值拷贝,对于自定义类型(自己写的类就是自定义类型)去调用它的拷贝构造
class Point
{
public:
	Point(int a1)
		:_a1(a1)
	{
		cout << "Point(int val) -- 构造" << endl;
	}

	Point(const Point& p)
	{
		cout << "Point(const int& p) -- 拷贝构造" << endl;
		_a1 = p._a1;
	}
	
	Point& operator=(const Point& p)
	{
		cout << "Point& operator=(const Point& p) -- 赋值" << endl;

		if (this != &p)
		{
			_a1 = p._a1;
		}

		return *this;
	}

private:
	int _a1;
};

int main()
{
	// 构造 * 2
	Point p1(10);
	Point p2(20);
	// 赋值和构造一定是分开进行的, 先构造后赋值
	p1 = p2;

	// 拷贝构造 - 因为p1没有进行构造,只有p3的拷贝构造,编译器不会优化
	Point p3 = p1;

	// 构造+拷贝构造的过程就是隐式类型转换
	// 用10调用Point构造函数生成一个临时对象,再用这个对象去拷贝构造p3
	// 10进行了构造,生成了临时对象拷贝构造p3,编译器直接优化成构造‘
	// 同时性:构造和拷贝构造同时发生 - 优化成直接构造
	Point p4 = 10;

	return 0;
}

在这里插入图片描述

补充点:两次拷贝构造会被优化成直接拷贝构造,减少一次深拷贝,后续还会有更好的优化方法,为右值引用的移动语义(移动构造和移动赋值),可以将深拷贝直接优化成移动构造,减少内存的使用
两次拷贝构造的场景

End!!!

### 方法返回对象时的拷贝与引用行为 在 C++ 中,当一个函数返回一个对象时,默认行为是进行拷贝构造,即返回的是原对象的一个副本。这种行为适用于 Qt 中的值类型类,例如 `QString`、`QVector` 等。这些类通常采用隐式共享(implicit sharing)机制,返回副本时并不会立即执行深拷贝,而是通过引用计数共享内部数据,直到其中一个对象被修改时才会触发深拷贝[^1]。 例如: ```cpp QString createString() { QString str = "Hello"; return str; // 返回 str 的副本,但使用隐式共享优化 } ``` 在这种情况下,虽然返回的是副本,但实际内存操作是高效的,因为只有在写入时才会复制数据(Copy-on-Write)[^1]。 ### 返回引用的场景 如果希望避免拷贝,可以将函数返回类型声明为引用(`T&`)或常量引用(`const T&`)。这种情况下,函数返回的是对象的引用,不会触发拷贝构造或析构操作。然而,必须确保返回的引用在其生命周期内有效,否则会导致悬空引用。 例如: ```cpp class MyClass { public: const QString& getName() const { return name; // 返回成员变量的引用,避免拷贝 } private: QString name; }; ``` 上述方法适用于返回类内部对象或传入参数的引用,但不适用于返回局部变量的引用,因为局部变量在函数返回后即被销毁,引用将无效。 ### QObject 派生类的特殊处理 对于继承自 `QObject` 的类,Qt 明确禁止拷贝构造赋值操作,因此不能通过值传递的方式返回 `QObject` 派生类的对象。如果尝试这样做,编译器会报错。因此,这类对象通常通过指针或引用来传递。 例如: ```cpp class Cat : public QObject { Q_OBJECT }; Cat* createCat(QObject* parent = nullptr) { return new Cat(parent); // 返回堆对象的指针,避免拷贝 } ``` 在这种情况下,返回指针或智能指针(如 `QScopedPointer` 或 `std::unique_ptr`)是常见的做法,以确保对象生命周期的正确管理。 ### 总结 Qt 中函数返回对象时的行为取决于返回类型: - **按值返回**:触发拷贝构造,但对隐式共享类(如 `QString`)而言,实际拷贝是延迟执行的,具有性能优化。 - **按引用返回**:不执行拷贝,直接操作原对象,需确保引用有效性。 - **按指针返回**:适用于 `QObject` 派生类,避免拷贝并支持动态内存管理。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值