类和对象——拷贝对象时的一些编译器优化

拷贝对象时的一些编译器优化

拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化(也就是说有的不做优化),减少对象的拷贝,这个在一些场景下还是非常有用的。

这里只举几个案例,详细见书籍《深度探索c++对象模型》。

在20世纪末流行的编译器(例如,vc++6.0)不会对这种情况进行优化。

案例1:仅使用类中的成员函数

很多时候,生成这个对象的目的仅仅是为了调用类中的某个函数。此时没必要生成一个对象,特别是生成一个对象作为实参上传给普通函数。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}
	void print() {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

//若调用拷贝构造仅仅是为了调用这个函数,完全没必要传值传参
void f1_1(A a) {
	a.print();
}

//所以直接加引用
void f1_2(A& a) {
	a.print();
}

void f1() {
	A a;
	f1_1(a);
	cout << endl;
	f1_2(a);
	cout << endl;
}

int main() {
	f1();
	return 0;
}

案例2:案例1减少一次拷贝构造

首先,const对象不能调用非const成员函数。所以const对象也要准备对应的const函数重载。

其次,引用和const一般在一起,为了避免别名修改原来的对象(变量)。

最后,形参使用引用可以减少一次拷贝构造。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

//为了支持生成临时对象,使用const引用
void f2_1(const A& a) {
	a.print();
}

void f2_2(A& a) {
	a.print();
}

void f2_3(A& a) {//非const形参,不具有常属性
	a.print();
}

void f2() {
	A a;
	f2_1(a);//权限缩小
	cout << endl;

	f2_2(a);//权限平移
	cout << endl;

	//f2_3(A());//权限放大
	f2_1(A());//形参也具有常属性时权限平移,可以调用
	cout << endl;
}

int main() {
	f2();
	return 0;
}

输出:

cpp 复制代码
A(int a)
6

6

A(int a)
6
~A()

~A()

f2_3(A());无法编译通过,因为临时对象、匿名对象都有常属性,上传无常属性形参的函数,权限放大。

案例3:临时对象也具有常属性

在案例2已经证明匿名对象具有常属性。隐式类型转换的临时对象也具有常属性。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

//const引用能很好的支持生成临时对象
void f3_1(const A& a) {//这个地方引用和const一般同时出现防止不小心修改
	a.print();
}

void f3() {//少调用一次拷贝构造
	f3_1(A());//匿名对象有常属性
	cout << endl;
	f3_1(A(4));
	cout << endl;
	f3_1(3);//临时对象也具有常属性
	cout << endl;
}

int main() {
	f3();
	return 0;
}

输出:

cpp 复制代码
A(int a)
6
~A()

A(int a)
4
~A()

A(int a)
3
~A()

它们都被优化成了只调用一次构造函数。

案例4:const引用延长生命周期

const引用可以延长临时对象的生命周期,本质是将临时对象变成有名对象,这样临时对象就可以像有名对象一样生命周期在局部。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

//缺省值为匿名对象
//const延长生命周期使得匿名对象存在于局部
void f4_1(const A& a = A()) {
	a.print();
}

void f4() {
	f4_1();
	cout << endl;

	//这里只有ref出了作用域,
	//临时对象的生命周期才终止
	const A& ref = A();
	cout << endl;

	ref.print();//还在{}也就是作用域内,可以使用
	cout << endl;
}

int main() {
	f4();
	return 0;
}

案例5:传匿名对象传参

编译器优化情况1:隐式类型转换作为实参,此时会调用两次构造。编译器将连续的两次构造(构造+拷贝构造)优化为直接构造。

c++标准并没有对这种情况进行优化说明,这个其实还是编译器本身的行为。在一些年代比较久远的编译器(比如20世纪末)就不会。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

void f5_1(A a) {
	a.print();
}//析构

void f5_2(const A a) {
	a.print();
}

A f5_3() {
	A a;
	return a;
}

//隐式类型,连续构造(两次及以上)->优化为直接构造
void f5() {
	//传值传参
	//正常情况
	A a;//构造
	f5_1(a);//拷贝构造
	cout << endl;

	// 一个表达式中,构造+拷贝构造->优化为一个构造
	f5_1(A());//匿名对象构造+拷贝构造被优化
	cout << endl;

	f5_1(A(3));
	cout << endl;
	
	f5_1(4);//隐式类型转换
	cout << endl;

	//这个也是构造+拷贝构造
	A b = A(3);
	cout << endl;
}

int main() {
	f5();
	return 0;
}

输出:

cpp 复制代码
A(int a)
A(const A& aa)
6
~A()

A(int a)
6
~A()

A(int a)
3
~A()

A(int a)
4
~A()

A(int a)

~A()
~A()

分析:

f5_1(A());f5_1(A(3));:匿名对象调用构造函数,加拷贝构造生成形参。

f5_1(4);:隐式转换,一次构造加拷贝构造。

A b = A(3);:一次构造加拷贝构造。

这三种情况,都被优化为一次构造。

案例6:函数传值返回时的优化

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

A f6_1() {
	A a;//构造
	return a;//拷贝构造生成临时对象
}

A& f6_2() {
	A a;
	return a;
}

void f6() {
	A a;
	cout << endl;

	f6_1();
	cout << endl;

	a = f6_1();
	cout << endl;
	
	A ret = f6_1();
	cout << endl;

	A ret2 = f6_2();
	cout << endl;
}

int main() {
	f6();
	return 0;
}

输出:

cpp 复制代码
A(int a)

A(int a)
A(const A& aa)
~A()
~A()

A(int a)
A(const A& aa)
~A()
A& operator=(const A& aa)
~A()

A(int a)
A(const A& aa)
~A()

A(int a)
~A()
A(const A& aa)

~A()
~A()
~A()

单独看A ret = f6_1();这种情况:

A f6_1()return语句会生成临时对象,但编译器进行了优化,直接将这个a在生命周期结束前拷贝给ret

所以在一个表达式的连续两个步骤里,局部对象构造 + 传值返回生成临时对象调用拷贝构造,两次调用构造被优化为一次。

A ret2 = f6_2();因为f6_2是传引用返回,所以直接省去了return语句的一次拷贝构造,在析构前生成临时对象,之后通过拷贝构造将对象拷贝给ret2

案例7:优化的条件

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

A f7_1() {
	A a;
	return a;
}

void f7() {//这种情况编译器不会再优化
	A ret2;
	ret2 = f7_1();
}

int main() {
	f7();
	return 0;
}

f7()这种情况不能优化,两个原因:

  • 同类型才能优化(都是构造或都是拷贝构造才能优化,这里是构造和赋值)。
  • 不在同一步骤(声明对象和赋值重载是两个语句或者说步骤)。

案例8:隐式类型转换的优化

和案例6的情况相似,都是构造临时对象并返回,只是存在隐式类型转换。所以被优化为一次构造。

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;

class A {
public:
	A(int a = 6)
		:a(a) {
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:a(aa.a) {
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa) {
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa) {
			a = aa.a;
		}
		return *this;
	}

	~A() {
		cout << "~A()" << endl;
	}

	//相应函数也要对这个类的成员函数进行限制防止权限放大
	void print() const {
		using std::cout;
		cout << a << "\n";
	}
private:
	int a;
};

//被优化为直接构造
//构造匿名对象加临时对象,两次构造被优化为1次
A f8_1() {
	return A();
}

A f8_2() {
	return 8;
}

A f8_3() {
	return A(1);
}

void f8() {
	A a1 = f8_1();
	cout << endl;
	A a2 = f8_2();//隐式类型转换
	cout << endl;
	A a3 = f8_3();
	cout << endl;
}

int main() {
	f8();
	return 0;
}

所以就有了这样一个特性:局部对象都只能传值返回,因此可以的话尽可能使用临时对象返回或隐式类型转换,可以减少拷贝调用次数。

再次理解封装

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。

比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象------即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。

  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面向对象的语言(比如:c++、java、python等)将洗衣机用类来进行描述,并输入到计算机中。

  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。

  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

所以类是对某一类实体对象来进行描述的 ,描述该对象具有那些属性 ,那些方法 ,描述完成后就形成 了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。

相关推荐
c-c-developer2 分钟前
C++ Primer 特定容器算法
c++
宇寒风暖1 小时前
侯捷 C++ 课程学习笔记:Spaces in Template Expression、 nullptr and stdnull
开发语言·c++·笔记·学习
明月看潮生1 小时前
青少年编程与数学 02-010 C++程序设计基础 13课题、数据类型
开发语言·c++·青少年编程·编程与数学
Awkwardx1 小时前
C++初阶—list类
开发语言·c++
2501_902556232 小时前
C++ 中 cin 和 cout 教程
数据结构·c++
萌の鱼2 小时前
leetcode 73. 矩阵置零
数据结构·c++·算法·leetcode·矩阵
Duramentee3 小时前
C++ 设计模式 十九:观察者模式 (读书 现代c++设计模式)
c++·观察者模式·设计模式
了不起的杰3 小时前
【c++语法基础】c/c++内存管理
java·c语言·c++
Chasing追~4 小时前
SQLite数据库从0到1
数据库·c++·qt·sqlite
追烽少年x4 小时前
C++中tuple的用法
开发语言·c++