二、explicit关键字
1. int i = 0; 2. double d = i;//隐式类型转换
根据监视可以看出:
double d = i;并不是将i直接赋值给d,而是用i创建一个临时变量,再把临时变量的值给d,那么d改变的是临时变量的值,而不是i的值,因为程序执行完毕后,i的值并未发生改变。
如果d作为引用,那么必须加上const关键字进行修饰,因为d不是i的引用,是临时变量的引用,而临时变量具有常性,不允许引用权限放大。
1. int i = 0; 2. const double& d = i;//d引用了临时变量,临时变量具有常性,所以d也必须具有常性
正常的类对象初始化如下面的aa1,也可以使用拷贝构造初始化,如aa2。由于c++支持隐式类型转换,因此也支持单参数构造函数初始化,如aa3:
1. #include<iostream> 2. using namespace std; 3. 4. class A 5. { 6. public : 7. A(int a) 8. :_a(a) 9. {} 10. 11. private: 12. int _a; 13. }; 14. 15. int main() 16. { 17. A aa1(1);//构造aa1对象 18. A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝 19. 20. A aa3 = 3;//单参数的构造函数,会发生隐式类型转换 21. 22. return 0; 23. }
那么
A aa3 = 3;
是如何支持类型转换的呢?
对于自定义类型A,aa3是A类型,3是整形。编译器会先拿A构造一个临时对象temp,3作为参数传给这个临时对象temp,再拿aa3(temp)去拷贝构造,发生隐式类型转换,即先构造,再拷贝构造:
1. //A aa3 = 3; 2. A temp(3); //先构造 3. A aa3(temp); //再拷贝构造
不过现在的编译器已经优化过了,会直接调用构造函数A aa(3)。
如果不想让单参数的构造函数发生隐式类型转换,可以使用explicit关键字修饰构造函数,表明该构造函数是显式的,而不是隐式的,就会避免发生不期望的类型转换,使用场景如下:
1. #include<iostream> 2. using namespace std; 3. 4. class A 5. { 6. public: 7. A(int a) 8. :_a(a) 9. {} 10. 11. private: 12. int _a; 13. }; 14. 15. int main() 16. { 17. A aa1(1);//构造aa1对象 18. A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝 19. 20. A aa3 = 'x';//先拿A构造一个临时对象temp,字符x作为参数传给这个临时对象temp,会发生隐式类型转换,再拿aa3(temp)去拷贝构造 21. 22. return 0; 23. }
aa3作为A类的对象,构造时传参应该传int型,但却传了char型,由于发生隐式类型转换,因此编译也没毛病,但是它传参就是不伦不类。这时候可以给A的构造函数加上explicit声明不让该单参构造函数发生隐式类型转换,编译就会报错:
1. class A 2. { 3. public: 4. explicit A(int a) 5. :_a(a) 6. {} 7. 8. private: 9. int _a; 10. };
这时候只能乖乖给aa3传int型参数了。
三、匿名对象
没有名字的对象叫做匿名对象,A(3)跟aa1和aa2相比少了个对象名,没有名字,aa1和aa2的生命周期在main函数内,A(3)的生命周期只在当前行:
1. #include<iostream> 2. using namespace std; 3. 4. class A 5. { 6. public: 7. explicit A(int a) 8. :_a(a) 9. { 10. cout << "A(int a):"<< a << endl; 11. } 12. 13. A(const A& aa) 14. { 15. cout << "A(const A&)" << endl; 16. } 17. 18. ~A() 19. { 20. cout << "~A()" << endl; 21. } 22. 23. private: 24. int _a; 25. }; 26. 27. int main() 28. { 29. A aa1(1);//生命周期在main函数内 30. A aa2(aa1);//生命周期在main函数内 31. 32. A(3);//构造匿名对象,生命周期只在这一行 33. 34. return 0; 35. }
F10调试:当执行完A(3)还没执行return 0时,aa1和aa2的生命周期还没有结束,不会调用析构函数,此时打印的析构函数只能是匿名对象A(3)的析构函数:
所以A(3)这一行执行完就调析构函数了。
假设有一个函数f,且A类的构造函数全缺省:
1. #include<iostream> 2. using namespace std; 3. 4. class A 5. { 6. public: 7. A(int a = 0)//构造函数全缺省 8. :_a(a) 9. { 10. cout << "A(int a):"<< a << endl; 11. } 12. 13. A(const A& aa) 14. { 15. cout << "A(const A&)" << endl; 16. } 17. 18. ~A() 19. { 20. cout << "~A()" << endl; 21. } 22. 23. void f()//f函数 24. { 25. cout << "f()" << endl; 26. } 27. 28. private: 29. int _a; 30. }; 31. 32. int main() 33. { 34. A aa1(1);//生命周期在main函数内 35. A aa2(aa1);//生命周期在main函数内 36. 37. A(3);//构造匿名对象,生命周期只在这一行 38. 39. return 0; 40. }
调用f()函数时,需要定义一个A类对象,才能调用A类函数f,这就需要写两行:
1. int main() 2. { 3. A aa1(1);//生命周期在main函数内 4. A aa2(aa1);//生命周期在main函数内 5. 6. A(3);//构造匿名对象,生命周期只在这一行 7. 8. A aa4;//需要定义一个A类对象,才能调用f 9. aa4.f(); 10. 11. return 0; 12. }
对象aa4 在main函数结束后才会销毁。如果定义对象只是为了调用函数,那么可以考虑直接定义一个匿名对象:
1. int main() 2. { 3. A aa1(1);//生命周期在main函数内 4. A aa2(aa1);//生命周期在main函数内 5. 6. A(3);//构造匿名对象,生命周期只在这一行 7. 8. A aa4;//需要定义一个A类对象,才能调用f 9. aa4.f(); 10. 11. A().f();//定义匿名对象来调用函数f() 12. 13. return 0; 14. }
这个匿名对象就是为了调用函数f,这个匿名对象后边也没人用它,在当前行调用完f()函数就销毁了。