14.1 C++类-成员函数、对象复制与私有成员
14.2 C++类-构造函数详解、explicit与初始化列表
14.3 C++类-inline、const、mutable、this与static
14.4 C++类-类内初始化、默认构造函数、“=default;”和“=delete;”
14.5 C++类-拷贝构造函数
14.6 C++类-重载运算符、拷贝赋值运算符与析构函数
14.7 C++类-子类、调用顺序、访问等级与函数遮蔽
14.8 C++类-父类指针、虚/纯虚函数、多态性与析构函数
14.9 C++类-友元函数、友元类与友元成员函数
14.10 C++类-RTTI、dynamic_cast、typeid、type-info与虚函数表
14.11 C++类-基类与派生类关系的详细再探讨
14.12 C++类-左值、右值、左值引用、右值引用与move
14.13 C++类-临时对象深入探讨、解析与提高性能手段
14.14 C++类-对象移动、移动构造函数与移动赋值运算符
14.15 C++类-继承的构造函数、多重继承、类型转换与虚继承
14.16 C++类-类型转换构造函数、运算符与类成员指针
文章目录
2.构造函数详解、explicit与初始化列表
2.1 称呼上的统一
(1)如果一个成员函数在class定义的内部将该成员函数完整地写出来,包括该成员函数的所有实现代码,对于这种写法的成员函数,称为“成员函数的定义”。
(2)如果一个成员函数在class定义的内部(一般位于一个.h文件中)只写出其声明,而具体的函数体代码写在了class定义的外部(一般位于一个.cpp文件中),那么,写在class内部的这部分称为“成员函数的声明”,写在class外部的这部分称为“成员函数的实现”,请注意区分。例如:
class A
{
public:
void myfunc();
};
void A::myfunc()
{
//实现代码写在这里
//......
}
当然,不管是(1)还是(2)中对成员函数的写法,都是允许的。
2.2 构造函数
上一节建立了一个Time类,写了一个public的initTime成员函数用于初始化成员变量的值,但问题是定义一个对象(也叫类对象)之后必须要手工调用这个成员函数,如果忘了调用,那么该对象里面的成员变量的值就变得不确定(未被初始化),如果不小心使用了这些不确定值的成员变量,就会出现代码编写错误。
在类中有一种特殊的成员函数,它的名字与类名相同,在创建类对象的时候,这个特殊的成员函数会被系统自动调用,这个成员函数叫作“构造函数”。显然,如果把一些成员变量的初始化代码放入构造函数中,就摆脱了需要手工调用initTime成员函数来初始化成员变量之苦了——因为构造函数会被系统自动调用。所以,可以简单理解成:构造函数的目的(存在的意义)就是初始化类对象的数据成员(成员变量)。
在Time.h中的Time类内部,声明(有时也把写在.h头文件中的成员函数的声明说成是“定义”)一个public类型的构造函数:
// Time.h
public:
Time(int tmphour, int tmpmin, int tmpsec);
// Time.cpp
void Time::initTime(int tmphour, int tmpmin, int tmpsec)
{
Hour = tmphour;
Minute = tmpmin;
Second = tmpsec;
initMillTime(0);
}
这里要注意几点:
(1)构造函数无返回值,以往书写无返回值函数时总在函数返回类型位置书写void,如void func(…),而构造函数是确确实实在函数头什么也不写,这也是构造函数的特殊之处。
(2)不可以手工调用构造函数,否则编译会报错。
(3)正常情况下,构造函数应该被声明为public,因为创建一个对象时系统要调用构造函数,这说明构造函数是一个public函数,能够被外界调用,因为class(类)默认的成员是private(私有)成员,所以必须说明构造函数是一个public函数,否则就无法直接创建该类的对象了(创建对象代码编译时报错)。
(4)构造函数中如果有参数,则在创建对象的时候也要指定相应的参数。
现在把所有的初始化代码放在了构造函数中,那么所有对象都会通过调用构造函数完成创建和初始化。因为构造函数的存在,类对象的初始化方式也就确定了,要带3个参数。看如下代码:
{
Time myTime = Time(12, 13, 52); //执行此行时调用构造函数
Time myTime2(12, 13, 52); //执行此行时调用构造函数
Time myTime3 = Time{ 12, 13, 52 }; //执行此行时调用构造函数
Time myTime4{ 12, 13, 52 }; //执行此行时调用构造函数
Time myTime5 = { 12, 13, 52 }; //执行此行时调用构造函数
Time myTime6(); //这不可以,没参数,可能被编译器误认为是函数声明
Time myTime7(12, 13); //不可以,缺少参数
}
上面代码中提供了多种Time对象的初始化方式。
在上一节内容中,如下这几种写法是进行对象的复制。对象的复制也是用来生成新对象,但是可以注意到,如下这些复制相关代码并没有调用传统意义上的构造函数,调用的实际是“拷贝构造函数”。下面的代码为防止对象重名,对对象名进行了适当的修改:
Time myTime2_l = myTime;
Time myTime3_l(myTime);
Time myTime4_l{ myTime };
Time myTime5_l = { myTime };
2.3 多个构造函数
一个类中是否可以同时存在多个构造函数呢?可以。如果提供多个构造函数,那么就可以为该类对象的创建提供多种创建的方法。但是,多个构造函数之间总要有些不同的地方,如参数数量上或者参数类型上的不同。
下面在Time.h中的Time类内部再声明一个public类型的构造函数:
Time();
在Time.cpp中实现这个新增的构造函数:
//构造函数
Time::Time()
{
Hour = 12;
Minute = 59;
Second = 59;
initMillTime(59);
}
现在换一种方法来创建类对象——创建对象时不再提供参数:
{
Time myTime10 = Time(); //执行此行时调用无参的构造函数
Time myTime12;//执行此行时调用无参的构造函数
Time myTime13 = Time{};//执行此行时调用无参的构造函数
Time myTime14{};//执行此行时调用无参的构造函数
Time myTime15 = {};//执行此行时调用无参的构造函数
}
从上面代码中可以发现,每次创建Time对象时,都会自动调用Time类的构造函数,但这次调用的是无参的构造函数。
2.4 函数默认参数
改造一下Time类中带3个参数的构造函数,在Time.h中,将第3个参数的默认值改为12。
Time(int tmphour, int tmpmin, int tmpsec=12);
此时,tmpsec参数就叫作函数的默认参数,也就是说如果生成Time对象时,不给这个参数传递值,那么,这个参数的值就是12。
在具有多个参数的函数中指定参数默认值时,默认参数都必须出现在非默认参数的右侧,一旦开始为某个参数指定默认值,则它右侧的所有参数都必须指定默认值。例如:
Time(int tmphour, int tmpmin = 59, int tmpsec = 12); //可以 这没问题
Time(int tmphour, int tmpmin = 59, int tmpsec); //不可以 这不行
有了默认参数之后,初始化对象的时候,就可以只给该对象提供两个参数:
Time myTime1_q{12, 13};
但是新问题来了:如果现在在Time.h中声明一个只有两个参数的构造函数会怎样呢?
Time(int tmphour, int tmpmin);
那么,提供两个参数生成Time对象时,编译会报错:系统搞不清楚构造这个对象时是应该调用带三个参数(含一个默认参数)的构造函数还是带两个参数的构造函数。
那怎么解决呢?其实很简单,在Time.h中,把带三个参数的构造函数中的默认参数值删掉即可:
Time(int tmphour, int tmpmin, int tmpsec);
当然,要想让Time myTime1_q{12,13};这行代码能够正确地编译链接并生成可执行程序,在Time.cpp中还要把带两个参数的Time构造函数实现一下:
//构造函数
Time::Time(int tmphour, int tmpmin)
{
Hour = tmphour;
Minute = tmpmin;
Second = 59;
initMillTime(59);
}
2.5 隐式转换和explicit
在C语言部分曾经学习过:一个float和一个int做运算的时候,系统会把int型转换成float型然后两者再做运算。在类中,这种情况也可能发生。
这里谈一谈单参数的构造函数带来的隐式转换,编译系统其实背着开发者在私下里还是做了很多事情的。如下两个对象定义和初始化,会发现出现语法错误:
Time myTime23 = 14; //现在语法错
Time myTime24 = (12, 13, 14, 15, 16); //现在语法错,不管()中有几个数字
但是,当声明了单参数的构造函数时(修改Time.h文件中的Time类定义):
Time(int tmphour);
修改.cpp增加单参数构造函数的实现代码:
//带一个参数的构造函数
Time::Time(int tmphour)
{
Hour = tmphour;
Minute = 59;
Second = 59;
initMillTime(0);
}
可以发现,上面两种定义对象的方式不再出现语法错误,而且每一个对象在定义和初始化时,都调用了单参数构造函数。
猜测一下上面的代码,把一个14给了myTime23,而myTime23是一个对象,14是一个数字,那么编译系统应该是有一个行为,把14这个参数类型转换成了Time类类型,这种转换被称为隐式转换或简称隐式类型转换。
现在再来写一个普通函数,它的形参类型就是Time类类型,格式如下:
void func(Time myt)
{
return;
}
现在可以发现,用一个数字就能够调用func函数:
func(16); //这显然也是含糊不清的代码,存在临时对象或者隐式转换的问题了
这说明系统进行了一个从数字16到对象myt(func函数的形参)的一个转换,产生了一个myt对象(临时对象),函数调用完毕后,对象myt的生命周期结束,所占用的资源被系统回收。
此外,接着上面的代码行继续书写,如下代码行也调用了Time类的单参数构造函数:
myTime23 = 16;//这句也调用了单参数的构造函数,生成一个临时对象,然后这个临时对象的值拷贝到了myTime23的成员变量去了
上面这些代码容易让人迷惑,也有点含糊不清,总结一下:
{
Time myTime100 = { 16 }; //这种写法认为正常,带一个参数12,可以让系统明确调用带一个参数的构造函数
Time myTime101 = 16; //这代码比较含糊不清,这就存在临时对象或者隐式转换的问题了
func(16); //这显然也是含糊不清的代码,存在临时对象或者隐式转换的问题了
}
上面这种隐式转换让人糊涂,是否可以强制系统明确要求构造函数不能做隐式转换呢?可以。如果构造函数声明中带有explicit(显式),则这个构造函数只能用于初始化和显式类型转换,来尝试一下。
现在,14.2.2节中构造的5个对象myTime、myTime2、myTime3、myTime4、myTime5都能够成功。此时,把带有3个参数的Time构造函数的声明前面加上explicit(修改Time.h),如下:
explicit Time(int tmphour, int tmpmin, int tmpsec);
此时,编译项目,发现如下这行代码出现语法错误:
Time myTime5 = {12, 13, 52};
把鼠标放到红色波浪线提示的错误处,可以看到,出现的错误提示信息如图14.1所示。
但是,myTime4这行代码比起myTime5只少了一个“=”,却能够成功创建对象。
Time myTime4 {12, 13, 52};
这说明一个问题:有了这个等号,就变成了一个隐式初始化(其实是构造并初始化),省略了这个等号,就变成了显式初始化(也叫直接初始化)。
现在,再理顺一下单参数的构造函数。目前如下调用都正常:
{
Time myTime100 = { 16 };
Time myTime101 = 16;
func(16);
}
此时,把带有单参数的Time构造函数的声明前面加上explicit(修改Time.h)。
explicit Time(int tmphour);
可以看到,上面三种写法都报错,这说明三种写法都进行了隐式转换。那如何进行修改呢?
Time myTime100 = Time(16); //或者Time{16}
func(Time(16)); //临时构造一个对象
建议:一般来说,单参数的构造函数都声明为explicit,除非有特别的原因。当然,explicit也可以用于无参或者多个参数的构造函数中。看如下代码:
explicit Time();
那么,对无参的构造函数使用了explicit之后,看看下面两行代码,分析一下:
Time time1{}; //可以,显式转换
Time time2 = {}; //不可以,隐式转换,就比上行多了一个=,等号就表示了一个隐式初始化了
继续看代码:
func({}); //不可以,隐式转换了
func({ 1,2,3 }); //不可以,隐式转换了
func(Time{}); //显式转换,生成临时对象,调用无参构造函数
func(Time{ 1,2,3 }); //显式转换,生成临时对象,调用三个参数的构造函数
2.6 构造函数初始化列表
在调用构造函数的同时,可以初始化成员变量的值,注意这种写法:笔者称它为冒号括号逗号式写法,位于构造函数定义(实现)中。注意,这种写法只能用在构造函数中。修改Time.cpp中的内容:
Time::Time(int tmphour, int tmpmin, int tmpsec)
:Hour(tmphour),Minute(tmpmin) //这就叫构造函数初始化列表
初始化列表的执行是在函数体执行之前就执行了的。上面这种写法和下面的写法类似,但下面的写法叫作函数体内赋值:
Time::Time(int tmphour, int tmpmin, int tmpsec)
{
Hour = tmphour;
Minute = tmpmin;
}
在这个范例中,用“构造函数初始化列表”和“函数体内赋值语句”都可以实现对数据成员的初始化。
但是提倡优先考虑使用构造函数初始化列表,原因如下:
(1)构造函数初始化列表写法显得更专业,有人会通过此来鉴别程序员的水平。
(2)一种写法叫作初始化,一种写法叫作赋值,叫法不同。对于内置类型如int类型的成员变量,使用构造函数初始化列表来初始化和使用赋值语句来初始化其实差别并不大。但是,对于类类型的成员变量,使用初始化列表的方式初始化比使用赋值语句初始化效率更高(因为少调用了一次甚至几次该成员变量相关类的各种特殊成员函数,如构造函数等)。这一点在后续的学习中可以更进一步体会。
避免写出下面的代码:
Time::Time(int tmphour, int tmpmin, int tmpsec)
:Hour(tmphour),Minute(Hour) //这就叫构造函数初始化列表
上面代码段的问题在于:某个成员变量(Minute)值不要依赖其他成员变量(Hour),因为成员变量的给值顺序并不是依据这里的初始化列表的从左到右的顺序,而是依据类定义中成员变量的定义顺序(从上到下的顺序),如果Hour定义在Minute下面(后面),那么,给Minute值的时候Hour值还尚未确定,所以上面的写法会让Minute中的值变得不确定。