个人主页:救赎小恶魔
欢迎大家来到小恶魔频道
好久不见,甚是想念
今天我们要深入讲述类与对象的初始化列表以及隐式类型转换
目录
引言:
我们已经学习了初始化,可有时候我们利用函数去初始化后仍然决绝不了问题。
当有这个烦恼的时候,我们就可以开始深入学习初始化了,也就是学习初始化列表。
1.初始化列表
1.1 构造函数体赋值
什么事构造函数体赋值呢?
答:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造 函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内 可以多次赋值。
1.2 初始化列表
当我们在写Stack或者MyQueue时,我们进行构造
比如:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
但注意:这个不是默认构造,默认构造是不出传参的
不具备默认构造的话就无法进行代码得到正常运作,所以为了解决这个问题,C++引入了初始化列表。
MyQueue(int n)
:_pushst(n)
,_popst(n)
{
_size = 0;
}
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
同时初始化列表可以和函数体初始化同时进
写完初始化列表后,我们又有了一个问题,他有什么优势?只是在为stack,queue这种服务???
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
public:
MyQueue(int n, int& rr)
:_pushst(n)
,_popst(n)
,_x(1)
//,_ref(rr)
{
_size = 0;
//_x = 1;
}
private:
// 声明
Stack _pushst;
Stack _popst;
int _size;
// 必须在定义时初始化
const int _x;
必须在定义时初始化
//int& _ref;
};
看这个代码,我们会发现n是不可以初始化的。
但是我们可以把它放到初始化列表中
为什么呢???
因为const变量只有一次初始化的机会,也就是在定义的时候 ,而它定义的时候只有初始化列表
还有个特殊的是引用
如果我们不初始化就会报错
所以我们也需要在初始化列表中初始化。
也就是说:
所有的成员都可以在初始化列表初始化,也可以在函数体中初始化,但是有三类只能在列表中初始化
- 引用
- const变量
- 没有默认构造自定义类型的成员(必须显示传参调用),如stack,queue等
但后人有发现了,如果我们将以上三个注释掉,我们依旧可以运行
class MyQueue
{
public:
MyQueue(int n, int& rr)
/*:_pushst(n)
,_popst(n)
,_x(1)
,_ref(rr)*/
{
_size = 0;
//_x = 1;
}
private:
// 声明
Stack _pushst;
Stack _popst;
int _size;
};
为什么呢?
因为初始化列表,不管你写不写,每个成员变量都会先走一遍,很多事都是编译器帮你做了。
联系之前的知识点。我们曾会在声明中给缺省值,这个缺省值就是给我们初始化列表用的
但如果我们在声明中给了缺省值,又在初始化列表中给了一个值呢?
这时候就没有缺省值的事了,就是在初始化列表中初始化了
这时候可能有人会问,可不可以在初始化列表中调用函数呢?
实际上是可以的
所以在这里,都是先走初始化列表,然后再走函数体,没有初始化列表也会先走初始化列表。
我们在这里也是推荐
尽可能的使用初始化列表初始化,不方便的在使用函数体去初始化。
什么情况切不方便使用初始化列表呢?
比如size++,或者将数组进行初始化成为1等情况。
再看下一段代码
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
}
int main() {
A aa(1);
aa.Print();
}
我们会发现我们去运行后只有第一个是1,第二个变成了随机值。
这样的原因原因是因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
我们调试,它会先走a2,a2传的是a1,a1还没初始化,所以是随机值,也就是a2随机值,然后再走a1,a1传a,a是1,所以a1是1。
1.3隐式类型转换
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A a1(1);
A a2 = a1;
A a3 = 3;
return 0;
}
像这个代码,主函数中得到a1a2a3的式子分别与什么有关呢?
- 第一个a1它这个是给值
- 第二个a2也是拷贝构造
- 第三个a3则是隐式类型转换(内置类型转换为自定义类型)
隐式类型转换是在中间建立一个临时变量。
所以在思考一下这个代码,这个代码行不行呢?
A& a4 = 4;
我们会发现这个代码不行,原因是是什么呢?
3要想隐式类型转换成A类型就需要创建中间临时变量,而临时变量具有常性,所以我们加一个const就可以。
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A a1(1);
A a2 = a1;
A a3 = 3;
const A& a4 = 4;
return 0;
}
运行代码会发现a1和a2和我们预期的一样。
但是a3则是进行了构造没有进行拷贝构造。
原因是这里被编译器优化了,同一个表达式连续步骤的构造,一般会被合并为一个
也就是说编译器遇到先构造再拷贝构造时会直接优化为构造
但如果不产生中间变量是没法编译进行的,毕竟3怎么能直接给A类型呢。
隐式类型转换是有很多好处的,比如在stack中
class Stack
{
public:
void Push(A st)
{
//
}
};
int main()
{
Stack st;
A a1(1);
st.Push(a1);
return 0;
}
我们会先构造,然后传参然后再拷贝构造,就麻烦些
所以加上引用和const
class Stack
{
public:
void Push(const A& st)
{
//
}
};
加上后,我们就可以进行隐式类型转换
int main()
{
Stack st;
A a1(1);
st.Push(a1);
st.Push(2);
return 0;
}
这样依旧可以进行编译
* explicit关键字
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换
class Date
{
public:
Date(int year)
:_year(year)
{}
explicit Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2018);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2019;
}
单参构造函数,没有使用explicit修饰,具有类型转换作用