【C++】——再谈构造函数
@[TOC]
这篇文章呢,我们来再来对类和对象做一些补充,进行一个最后的首尾!
1. 再谈构造函数
那上一篇文章呢,我们学了类的6个默认成员函数,其中我们第一个学的就是构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。 也就是说,构造函数其实就是帮我们对类的成员变量赋一个初值。
举个栗子:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
对于像这样的一个类来说:
虽然经过上述构造函数的调用之后,对象中的成员变量已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋值。 因为初始化只能初始化一次(即在定义的时候赋初值),而构造函数体内可以对成员变量进行多次赋值。 这里注意初始化(定义的时候赋初值)和赋值的区别。
那我们现在来看这样一个类:
class A
{
private:
int _a1;
int _a2;
};
<font color = black>问大家一个问题:这里面的
int _a1; int _a2;
是对成员变量
_a1、 _a2
的声明还是定义? 这里是不是声明啊,只是声明一下A这个类里有这样两个成员变量。
那它们在哪定义呢?
<font color = black>🆗是不是在这个时候:
但是,这里不是对对象整体的定义嘛。
那对象的每个成员变量什么时候定义呢? <font color = black>可是变量整体定义了的话,它的成员不都也定义了吗? 这些成员不都是属于这个对象的吗?
我们运行也没出什么问题。
道理好像是这样的,但是呢?看这种情况:
<font color = black>我们现在给这个类里面再增加一个const的成员变量。 那这时我们再去运行程序:
哦豁,发生错误了,这么回事? 为什么会这样呢? 🆗,大家来想一下,const修饰的变量有什么特点: const修饰的变量必须在定义的时候赋初值(初始化) 而我们现在有对
_b
进行初始化吗? 是不是没有啊,我们构造函数都没写,那编译器是会默认生成一个,但是,我们知道默认生成的根本就不会对内置类型进行处理。
那我们是不是自己写个构造函数就行了:
但是我们发现还不行,为什么呢? 因为const变量必须是在定义的时候赋初值,而我们上面说了构造函数里面只是对其赋值,并不是初始化。
那大家可能想到了:
<font color = black>之前文章里我们在讲解构造函数的时候说了,C++11不是允许内置类型成员变量在类中声明的时候可以给缺省值嘛。
我们来试一下:
🆗,这样确实不报错了。
但是,这是C++11之前才提出来的,那C++11之前呢? 如何解决这样的问题呢?
<font color = black>现在的问题是什么:
_b必须初始化,即在定义的时候赋初值,但是现在是不是没法搞啊,构造函数里只能对其赋值,并不是初始化。 那我们是不是要给成员变量也找一个定义的位置,不然像const这样的成员变量不好处理。
那成员变量的定义到底是在哪里呢?
我们可以认为,对象定义的时候,其成员变量也就定义了,但是一个对象可能有多个成员,在对象定义的地方也没法给某个成员初始化啊。 怎么办? 🆗,这就是我们接下来要学的东西——初始化列表。
1.1 初始化列表
那面对上面的问题,我们的祖师爷就要去给成员变量找一个定义的地方,那最终找来找去呢,还是把目标锁定在了构造函数。 在构造函数里面呢又搞了一个东西叫做——初始化列表。
初始化列表:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
举个栗子:
对于上面类中
const int _b
的初始化我们就可以放在初始化列表进行处理:
class A
{
public:
A(int a1, int a2, int b)
:_a1(a1)
,_a2(a2)
,_b(b)
{
}
private:
int _a1;
int _a2;
const int _b;
};
int main()
{
A a(1, 1, 1);
return 0;
}
这下我们再运行程序:
就可以了。
当然:
在构造函数体内我们还可以再为成员变量赋值
注意这里成员_b被const修饰,不能再被赋值了。
然后呢,对于初始化列表,还有一些需要我们注意的地方:
<font color = red>每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
<font color = red>以下三种类成员变量,必须放在初始化列表位置进行初始化: 引用成员变量 const成员变量 没有默认构造函数的自定义类型成员
首先const成员变量:
我们上面举的例子就是const成员变量,它必须在定义的时候赋初值,所以必须在初始化列表对其进行初始化(定义的时候赋初值),<font color = red>当然C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。
然后还有引用成员变量:
这个我们在之前学习引用的时候就说了:
引用也必须在定义的时候初始化。
最后就是没有默认构造函数的自定义类型成员:
因为默认生成的构造函数对内置类型不做处理,对自定义类型会去调用它对应的默认构造函数(不需要传参的构造函数都是默认构造函数),所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。
举个栗子:
class B
{
public:
private:
int _b;
};
class A
{
private:
int _a1;
int _a2;
B _bb;
};
int main()
{
A a;
return 0;
}
<font color = black>大家看运行这个程序有问题吗?
没有问题,因为对于成员
B _bb;
来说,会调用它对应的默认构造,类B我们虽然没写构造函数,但是有编译器默认生成的构造函数。 当然如果我们写了不用传参的构造函数,也可以。 但是如果这样:此时类B是不是没有默认构造函数了。
那这时就不行了。
![]()
让_bb在初始化列表调用其构造函数进行初始化,这样就可以了。
<font color = red>尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会在初始化列表定义。当然我们说了C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。
<font color = red>成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
看这个程序:
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();
}
大家思考一下结果是啥?
构造函数参数a接收传过来的1,1先初始化a1,然后a1去初始化_a2,所以都是1,是吗?
结果是1和一个随机值。
为什么是这样?
原因就是成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
所以先初始化a2,然后是a1
所以是1和随机值。
1.2 explicit关键字
我们先来看这样一个类:
class A
{
public:
A(int a)
:_a1(a)
{}
private:
int _a2;
int _a1;
};
那我们现在想用A这个类去创建对象:
int main()
{
A a1(1);
return 0;
}
这样肯定是可以的,去调它带参的构造函数。
那除此之外呢,其实还可以这样搞:
欸,这种写法是怎么回事?
<font color = black>这个地方为什么
A a2 = 1;
这样也可以,1是一个整型,怎么可以直接去初始化一个类对象呢? 🆗,那要告诉大家的是这里其实是一个隐式类型转换。 就跟我们之前提到的内置类型之间的隐式类型转换转化是一样的,会产生一个临时变量,我们再来回顾一下:那这里
A a2 = 1
是如何转换的呢? 这里呢也会产生一个临时变量,这个临时变量就是用1去构造出来的一个A类型的对象,然后再用这个临时对象去拷贝构造我们的a2。
那我们可以来证明一下,是不是如我所说的那样:
<font color = black>我们来再写一个拷贝构造:
注意拷贝构造也是有初始化列表的,因为拷贝构造函数是构造函数的一个重载形式。 那我们现在运行程序,看
A a2 = 1
是不是先用1调构造函数创建一个临时变量,然后再调拷贝构造构造a2。 如果是那就跟我们上面说的一样了。哦豁,构造确实调了,但是后面没去调拷贝构造啊。
是我们上面说的不对吗?
<font color = black>🆗,那其实呢,C++编译器针对自定义类型这种产生临时变量的情况,会进行优化 编译器看到你这里先拿1构造一个对象,然后再去调拷贝构造,有点太费事了,干脆优化成一步,直接拿1去构造我们要创建的对象。 当然,不一定所有的编译器都会优化,但是一般比较新一点的编译器在这里都会优化。
但是呢?口说无凭欸!
<font color = black>你凭什么说这里没有优化的话是会产生临时变量的,说不定人家本来就是直接去构造了呢? 那我们再来看这个代码:
A& c = 10;
这样可以吗?🆗 ,不行直接报错了。 但是:
加个const就行了。
为什么呢?
🆗,这是不是我们之前在常引用那里讲过的啊: 这里产生了临时变量,而临时变量具有常性,所以我们加了const就行了。 欸,那不是说优化了嘛,但是这里是引用就没优化了,直接拿10去构造一个临时对象,然后c就是这个临时对象的引用,所以只有一步构造,就不用优化了。 所以这里确实是会产生临时变量的,上面那种情况确实是进行了优化。 还有一个点就是,一般来说,C++ 中的临时变量在表达式结束之后 (full expression) 就被会销毁,而这里引用去引用一个临时变量的话会延长它的声明周期的。
那上面说了这么一大堆,想告诉大家的是什么呢?
就是 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,是支持隐式类型转换的(C++98就支持这种语法了)。
这里就可以这样:
那如果我们这里不想让它支持类型转换了,有没有什么办法呢?
<font color = black>🆗,这就要用到我们接下来要学的一个关键字——
explicit
我们只需在对应得构造函数前面加上explicit
关键字:然后:
这样写就不行了。
🆗,但是呢,我们刚才说的是对于单参数的构造函数是支持这种类型转换的,那多参数的构造函数呢?
<font color = black>首先我们肯定是可以这样用的:
A a(1, 1);
那这里能不能也像上面那样支持隐式类型转换呢,两个参数的构造函数,那这样去用?那首先要告诉大家C++98是不支持多参数的构造函数进行隐式类型转换的。 不过呢,C++11对这块进行了扩展,使得多参数的构造函数也可以进行隐式类型转换,但是,要这样写:
用一个大括号括起来。 那同样的道理,如果我们不想让这里支持这种类型转换,对于多参数的构造函数,也是在前面加一个
explicit
关键字:用explicit修饰构造函数,将会禁止构造函数的隐式转换。
- 点赞
- 收藏
- 关注作者
评论(0)