总结C++对传参和传返回值时构造的优化处理
这里用具体例子说明,简单定义一个日期类:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year = 0, int month = 0, int day = 0)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
//以下是下文举例子所用到的成员函数类中的声明,其定义在类外
void Func1(Date d);
void Func2(Date& d);
Date Func3();
Date Func4();
void Func5(Date d);
Date Func6();
Date& operator=(const Date& d);
Date Date::Func7();
Date Date::Func8();
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;//这里创建对象时调用了构造函数,初始化为缺省值;
Date d2(2017, 11, 18);//这里创建对象时调用了构造函数,初始化为所给值;
//以上创建两个对象,调用了两次构造函数;
d1.Print();
d2.Print();
return 0;
}
//程序结束时先析构都d2;再析构d1;因此这里会自动调用两次析构函数;
以下举例子所用成员函数均已在上述类中声明
例子一:形参加引用与不加的区别:
void Date::Func1(Date d)//这里很明显是函数的形参;则会去先调用拷贝构造函数,生成一个临时对象d;
{}
void Date::Func2(Date& d)//这里加了一个&;表示是实参的一个别名,因此在这里就不会去调用拷贝构造,d就相当于实参
{}
int main()
{
Date d3(2017, 11, 18);
Date d4;
d4.Func1(d3);
d4.Func2(d3);
return 0;
}
分析
结果很明显在调用Func1时多调用了一次拷贝构造和析构,而在调用Func2时并没有,因为Func2中的形参加了引用,就是实参d3的别名。
例子二:返回值加引用与不加的区别:
Date Date::Func3()//返回为Date
{
Date tmp;//创建临时对象tmp;
return tmp;//直接返回临时对象tmp;编译器会在这里自动创建一个临时变量,因此在这里会调用拷贝构造函数,函数结束后,该临时对象的生命周期结束;再调用一次析构函数。
}
Date& Date::Func4()//返回加引用Date&
{
Date tmp;//创建临时对象tmp;
return tmp;//返回的是tmp对象的别名,编译器就不会再创建临时变量;就少了一次拷贝构造函数和少了一次析构函数的调用;
}
int main()
{
Date d5;
d5.Func3();
d5.Func4();
return 0;
}
分析
很显然如果出了调用的函数的作用域时,返回的对象仍存在就需要返回引用,这样在返回时编译器就不用生成临时变量,就少了一次拷贝构造和析构的调用。
例三:匿名对象传参
void Date::Func5(Date d)
{}
int main()
{
Date d6;
d6.Func5(Date());//传参时传匿名对象,可以参照例一调用Func1()函数进行对比;
return 0;
}
分析
传参时传匿名对象,会调用一次构造函数,再传给形参时,形参又会调用一次拷贝构造函数,拷贝一份实参,函数调用结束后,会析构形参,析构匿名对象,但编译器在这里只调用了一次构造函数和析构函数,是因为编译器进行了优化,省去了形参的拷贝构造和析构函数,提高了效率。可以对比例一调用Func1()的传参。
例四:匿名对象返回
Date Date::Func6()
{
return Date();//返回一个匿名对象;
}
int main()
{
Date d7;
d7.Func6();
return 0;
}
分析
返回匿名对象时,会调用构造函数创建匿名对象,返回的并非引用,因此返回时编译器会自动创建一个临时对象进行返回,调用拷贝构造,调用结束后再析构临时对象和匿名对象;但编译器在这里只掉用了一次构造函数和析构函数,是因为编译器进行了优化,省去了返回时创建临时对象所要调用的拷贝构造函数和析构函数,可以对比例二调用Func3()函数的返回对象。
*例五:综合
Date& Date::operator=(const Date& d)
{
cout << "Date& Date::operator=(const Date& d)" << endl;
return *this;
}
Date Date::Func7()
{
Date tmp;
return tmp;//返回临时对象
}
Date Date::Func8()
{
return Date();//返回匿名对象
}
int main()
{
Date d8;
d8 = d8.Func7();
d8 = d8.Func8();
return 0;
}
分析
1、创建对象d8时调用构造函数
2、d8对象调用Func7(),创建对象tmp,调用构造函数
3、返回时生成临时对象,调用拷贝构造
4、析构临时对象tmp
5、【本来会再调用拷贝构造函数,再生成一个临时对象,进行接下来的赋值,但是由于编译器优化(省略了一次拷贝构造),将返回时生成临时对象与赋值时生成临时对象调用拷贝构造合二为一】
6、再调用赋值运算符重载
7、析构返回的临时变量
8、d8调用Func8(),创建临时对象tmp,调用构造函数
9、【返回时由于引用,所以直接返回对象tmp,不需要生成临时变量返回(省略了一次拷贝构造),接下来直接用tmp进行赋值,也并没有生成临时变量(又省略了一次拷贝构造),将创建对象tmp、返回时生成临时对象、赋值时生成临时对象调用拷贝构造合三为一】
10、调用赋值运算符重载
11、析构临时对象tmp
12、最后析构对象d8
例题
class AA
{};
AA f(AA a)
{
return a;
}
void Test1()
{
AA a1;
a1 = f(a1);
}
void Test2()
{
AA a1;
AA a2 = f(a1);
}
void Test3()
{
AA a1;
AA a2 = f(f(a1));
}
//Test1中调用了_ 1_次AA的拷贝构造函数, 1 次AA的赋值运算符函数的重载。
//Test2中调用了_ 2_次AA的拷贝构造函数, _0次AA的赋值运算符函数的重载。
//Test3中调用了_ 3_次AA的拷贝构造函数, 0_次AA的赋值运算符函数的重载。