目录
1.练习题1:初始化列表中的初始化顺序
2.练习题2:成员变量声明时的缺省参数和初始化列表
问题1.下面代码的运行结果是什么?
分析
问题2:修改上方代码的Myclass带参的构造函数为下方代码,执行myobj2的构造函数时,在执行 _val = 10;前_val的值是多少?
分析
3.类型转换
观察下列代码,问myobj1和myobj2初始化有什么区别?
分析
临时对象具有常性
★结论:对象(初始值)和对象 = 初始值的区别
4.用好隐式类型转换可以简化代码
5.explicit关键字
承接CD23.【C++ Dev】类和对象(14) 取地址重载函数和初始化列表(上)文章
1.练习题1:初始化列表中的初始化顺序
下面代码的运行结果是什么?
#include <iostream>
using namespace std;
class Myclass
{
public:Myclass(int data):_val1(data), _val2(_val1){}void Print() {cout << "_val1:"<<_val1<<endl<< "_val2:" << _val2 << endl;}
private:int _val2;int _val1;
};int main()
{Myclass myobj(123);myobj.Print();
}
分析:
先看答案再推原因:
_val2是随机值,说明myobj中先初始化_val2,再初始化_val1,1._val2(_val1)可以看出,_val2初始化的是_val1的值,但_val1此时还没有初始化,操作系统为_val1内存空间赋的是随机值,导致_val2是随机值
2._val1(data)可以看出:_val1初始化的值是data,而data值为123,因此正常打印"_val1:123"
从反汇编也可以看出:
结论:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
建议:声明的顺序和定义的顺序保持一致
2.练习题2:成员变量声明时的缺省参数和初始化列表
问题1.下面代码的运行结果是什么?
#include <iostream>
using namespace std;
class Myclass
{
public:Myclass(){}Myclass(int data):_val(10){}int GetVal(){return _val;}
private:int _val = 0;
};int main()
{Myclass myobj1;Myclass myobj2(1);cout << myobj1.GetVal() << endl;cout << myobj2.GetVal() << endl;return 0;
}
分析
显然构造函数Myclass被重载了,Myclass()是默认构造函数(没有传任何参数),Myclass(int data)不
是默认构造函数(传了参数data)
则Myclass myobj1;初始化时会调用Myclass(),初始化成员变量val时会使用int _val = 0的缺省参数0,
Myclass myobj2(1)传了一个参数,因此初始化时会调用Myclass(int data),导致_va的值l被初始化为10
运行结果:
问题2:修改上方代码的Myclass带参的构造函数为下方代码,执行myobj2的构造函数时,在执行 _val = 10;前_val的值是多少?
Myclass(int data)
{_val = 10;
}
分析
由CD23对缺省参数和初始化列表的关系的说明可知:所有成员变量在初始化时都要走初始化列表,_val给了缺省参数0,则在执行_val = 10前,_val的值应该为缺省参数0
可以下断点调试看看,如下:
再看看反汇编代码,更明确:
3.类型转换
观察下列代码,问myobj1和myobj2初始化有什么区别?
#include <iostream>
using namespace std;
class Myclass
{
public:Myclass(int data):_val(data){}
private:int _val;
};int main()
{Myclass myobj1(1);Myclass myobj2 = 2;return 0;
}
分析
Myclass myobj1(1)是正常初始化,向构造函数Myclass正常传参数,用参数初始化_val的值:
Myclass myobj2 = 2;在写法上与myobj1不同,2是一个整型,却要赋值给自定义类型myobj2,编译器会做如下处理:隐式类型转换,将整型转换为自定义类型,即用2去构造一个临时对象,再将这个临时对象拷贝构造给myobj2,其实是构造函数+拷贝构造函数(这个是未优化的情况)
画示意图为:
实际上较新的编译器不允许这样做,编译器会进行优化,将连续的构造转换为用2直接构造,不使用拷贝构造函数,可以手动写一个拷贝构造函数看看是否会调用:
#include <iostream>
using namespace std;
class Myclass
{
public:Myclass(int data)//构造函数:_val(data){cout << "Myclass(int data)" << endl;}Myclass(const Myclass& myobj)//拷贝构造函数:_val(myobj._val){cout << "Myclass(const Myclass& myobj)" << endl;}
private:int _val;
};int main()
{Myclass myobj2 = 2;return 0;
}
VS2022上的运行结果: 只调用构造函数,属于优化后的
没有优化的运行结果:使用Linux g++关闭优化的指令:
g++ -fno-elide-constructors test.cpp
(未优化:构造+拷贝构造)
临时对象具有常性
如果改成:
Myclass& myobj2 = 2;
编译出错:
分析:用2构造临时对象,再对临时对象引用,这里需要注意:临时对象具有常性,引用需要用const修饰,改成下面这样就行了:
const Myclass& myobj2 = 2;
★结论:对象(初始值)和对象 = 初始值的区别
之前在CC12.【C++ Cont】string类字符串的创建、输入、访问和size函数文章中提到过string类对象的两种初始化方式,如下:
string str1="hello world";
string str2("hello world");
在那篇文章中认为:两种初始化的效果一样,其实初始化的方式是有区别的
1. string str2("hello world");只调用构造函数
2. string str1="hello world";调用了构造函数和拷贝构造函数,可能会被编译器优化成直接构造
4.用好隐式类型转换可以简化代码
例如写一个List类,实现向链表中尾插字符串:
#include <iostream>
#include <string>
using namespace std;
class List
{
public:void push_back(const string& s){//省略具体实现代码}
};int main()
{List ls;//发生隐式类型转换//因为push_back接收的是const string& s,是string对象的引用//用const修饰原因:临时对象具有常性ls.push_back("teststring1");string s("teststring2");ls.push_back(s);return 0;
}
显然ls.push_back("teststring1")利用了隐式类型转换,只需写一行,在写法上比string s("teststring2");和ls.push_back(s);简洁
5.explicit关键字
如果不想发生隐式类型转换,可以使用explicit关键字,加在构造函数的前面
explicit表明该构造函数是显式的,而非隐式的.当使用explicit修饰构造函数时,它将禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数
代码如下:
#include <iostream>
using namespace std;
class Myclass
{
public://explicit修饰explicit Myclass(int data):_val(data){cout << "Myclass(int data)" << endl;}Myclass(const Myclass& myobj):_val(myobj._val){cout << "Myclass(const Myclass& myobj)" << endl;}
private:int _val;
};int main()
{Myclass myobj2 = 2;const Myclass& myobj3 = 2;return 0;
}
这样编译就会报错,myobj2和myobj3都会报错,都无法创建临时对象