14.4 C++类-类内初始化、默认构造函数、“=default;”和“=delete;”

本文详细介绍了C++中类的相关概念,包括成员函数、对象复制、私有成员、构造函数的使用,特别是默认构造函数、初始化列表和=default、=delete的用法。还讨论了const成员变量的初始化、拷贝构造函数、友元函数等,并举例说明了如何使用这些特性。文章强调了在特定情况下编译器是否会生成默认构造函数,以及如何通过=default和=delete控制函数的行为。
摘要由CSDN通过智能技术生成

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++类-类型转换构造函数、运算符与类成员指针

4.类内初始化、默认构造函数、“=default;“和”=delete;”

  4.1 类相关非成员函数

    在实际编写代码中,有时候也会遇到一些额外的功能函数,例如某个功能函数是打印某个类中一个成员变量的值,这种额外的功能函数虽然和这个类有点关系,但感觉这种函数又不应该定义在类里面,那么这种函数应该怎样写,怎样调用呢?
    这种函数的定义可以放在该类成员函数实现的代码中(Time.cpp中)。看如下代码:

//注意这是普通函数,不是成员函数
void WriteTime(Time& mytime)
{
	std::cout << mytime.Hour << std::endl;
}

    在Time.h中,在Time类定义的后面,加入函数WriteTime的声明代码:

void WriteTime(Time& mytime); //函数声明,形参是引用,避免了对象拷贝产生的效率损耗

    在main主函数中加入如下代码:

{
	Time mytime(12, 15, 17);
	WriteTime(mytime);
}

  4.2 类内初始值

    在C++11新标准里,可以为新成员变量提供一个类内的初始值,那么在创建对象的时候,这个初始值就用来初始化该成员变量。对于没有初始值的成员变量,系统有默认的初始策略,如读者熟知的整型成员变量,系统会随便扔个值进去。
    在Time.h中的Time类内部,修改一下成员变量Second的定义:

int second{0};

    如果使用构造函数初始化列表或在构造函数中给Second值,该值会覆盖掉初始值:

Time::Time(int tmphour, int tmpmin, int tmpsec)
	:Second(tmpsec)		//通过初始化列表来给Second值或者
{
	Seond = tmpsec;		//通过赋值来给Second值
}

  4.3 const成员变量的初始化

    对于类的const成员,只能使用初始化列表来初始化,而不能在构造函数内部进行赋值操作。
在Time.h中的Time类内部,增加如下内容:

const int  testvalue;

    那么在Time.cpp类的构造函数中,代码应该如下:

Time::Time(int tmphour, int tmpmin, int tmpsec)
				:Hour(tmphour),Minute(tmpmin), //这就叫构造函数初始化列表
				testvalue(18)

{
	testvalue = 6;  //不可以在这初始化常量
}

    解释一下:上面创建testvalue这种常量属性的变量时,Time构造函数完成初始化之后,也就是Time::Time(…)…,testvalue(18)这行执行完后,testvalue才真正具备了const属性,在构造testvalue这个const变量(对象)过程中,Time构造函数可以向其内部写值,如上面的testvalue(18)。
    因为构造函数要进行很多看得见和看不见的写值操作,所以构造函数不能声明成const的,这一点读者朋友可以自己尝试。

  4.4 默认构造函数

    前面已经学习过了构造函数,并且知道了一个类中可以有多个构造函数,其中,没有参数的构造函数称为默认构造函数。
    创建Time2.h和Time2.cpp,并把Time2.cpp加入到当前项目中。特别留意Time2.h中类定义最后面的分号不能少,否则会出现语法错误。
    先定义一个无参的构造函数(Time2.h中):

#ifndef __MYTIME2__
#define __MYTIME2__
//类定义 有人也叫类声明
class Time2 {
public:
	explicit Time2();	// explicit禁止隐式转换
public:
	int Hour;
	int Minute;
	int Second{0};
};//分号不要忘记
#endif

    再看Time2.cpp文件:

Time2::Time2()//构造函数
{
	Hour = 12;
}

    在main主函数中增加如下代码:

Time2 mytime2;

    现在分别在Time2.h和Time2.cpp中把Time2类的构造函数注释掉,不难发现,上面的代码也能成功地生成mytime2对象。
    所以得到一个结论:如果一个类中没有构造函数,那么也是能够成功生成一个对象的
    这里要特别指出,有的资料认为(或者说:许多读者认为),如果一个类没有自己的构造函数,编译器会生成一个所谓“合成的默认构造函数”,正是因为这个“合成的默认构造函数”的存在,才使Time2 mytime2;这句代码能够执行成功。
    编译器只有在满足一定情形之下才会生成这个“合成的默认构造函数”。而即便一个类没有自己的构造函数,编译器也没有生成“合成的默认构造函数”,“Time2 mytime2;”这句代码依然能够执行成功。所以,创建类对象(mytime2)并不需要类(Time2)一定存在构造函数,这一点可能超出了读者以往的认知。如果读者要确认到底是否生成了构造函数,可以观察编译器生成的目标文件.obj(以及其他所有本项目相关的.obj文件)来寻找诸如Time2::Time2的字样。
    现在增加一个新的构造函数,带一个参数。在Time2.h中,代码如下:

Time2(int itmpvalue){};

    但此时可以发现,刚才的“Time2 mytime2;”代码行报错。报错截图如图所示。
在这里插入图片描述
    从图可以看到,提示的错误是类Time2不存在默认构造函数,也就是无参的构造函数。因为“Time2 mytime2;”这句代码的执行成功必须要有一个无参的构造函数存在,而现在的Time2类中并没有这样的构造函数。
    有的资料认为,一旦程序员自己写了一个构造函数,不管这个构造函数带几个参数,编译器就不会创建合成的默认的构造函数了,这里有两个意思,需要强调:
    (1)如果一个类没有自己的构造函数,编译器可能会生成一个“合成的默认构造函数”,也可能不生成一个“合成的默认构造函数”,生成与否取决于具体需要。但不管如何,生成该类的对象都会成功,例如“Time2 mytime2;”这句代码都会执行成功。
    (2)假设编译器因为需要,原本是能够生成一个“合成的默认构造函数”。但是,如果程序员自己写了一个构造函数,那么编译器就不会生成这个“合成的默认构造函数”。
    其实,编译器是否生成“合成的默认构造函数”并不需要去关心,但问题是,此时代码“Time2 mytime2;”无法执行成功了。所以,不难得出结论,一旦程序员书写了自己的构造函数,那么在创建对象的时候,必须提供与书写的构造函数形参相符合的实参,才能成功创建对象。那么,此时要成功创建Time2对象,就要写如下这样的代码:

Time2 mytime2(1);

    回头再说一说编译器只有在满足一定情形之下才会生成这个“合成的默认构造函数”这件事。现在把带一个参数的构造函数注释掉,使整个Time2类都不存在任何构造函数。main主函数中的代码“Time2 mytime2(1);”恢复为“Time2 mytime2;”,现在整个项目是能够编译成功的,证明目前代码没有错误。
    那么,此时此刻,编译器到底是否生成了“合成的默认构造函数”呢?答案是肯定的,编译器生成了“合成的默认构造函数”(观察编译器生成的目标文件.obj,会发现Time2::Time2的字样)。为什么?其实只是因为在Time2.h的Time2类定义中有一行代码“int Second{0};”,因为这行代码的存在,导致编译器需要生成“合成的默认构造函数”,并且往这个“合成的默认构造函数”中插入代码来把Second成员变量的值初始化为0。这就是这个“合成的默认构造函数”的使命。
    在Time2.h的最上面,Time2类定义之外,创建一个新类,取名OneClass。看如下代码:

class OneClass
{
public:
	OneClass(int)
	{}
};

    还是在Time2.h中,在Time2类定义中增加如下代码:

OneClass oc;

    此时编译工程能够发现,在main主函数中的“Time2 mytime2;”代码行会报错,如图所示。
在这里插入图片描述
    看得出来,如果Time2不提供构造函数,则生成Time2对象mytime2无法成功。究其原因,是因为无法构造成员变量oc(因为构造oc需要为OneClass的构造函数提供一个参数)。
    此时,必须为Time2提供自己的构造函数而无法再使用编译器创建的“合成的默认构造函数”。需要进行如下操作:
    (1)取消Time2.h中对默认构造函数声明行的注释:

explicit Time2();

    (2)取消Time2.cpp中对默认构造函数实现的注释并在初始化列表中为成员变量oc的创建提供一个实参:

<font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;Time2::Time2():oc(18)
{
	Hour = 12;
}

    此时编译整个项目,不会再出现问题。
    在14.2.6节中,笔者谈到过一些必须通过“构造函数初始化列表”来进行初始化的操作,那么14.4.3节中const成员变量的初始化以及本小节刚刚谈到的类类型成员变量oc的初始化,都属于必须通过“构造函数初始化列表”来进行初始化的操作。

  4.5 “=default;“和”=delete;”

(1)=default;

    为了顺利演示,现在调整一下代码。
(1)在Time2.h中,注释掉默认构造函数,取消对单参数构造函数的注释,注释掉成员变量oc定义这行。现在Time2.h有用的代码看起来如下:

class Time2{
public:
	Time2(int itmpvalue){};
public:
	int Hour;
	int Minute;
	int Second{0};
};

    (2)在Time2.cpp中,注释掉无参构造函数Time2::Time2的实现代码。
    (3)此时编译工程能够发现,在main主函数中的“Time2 mytime2;”代码行会报错。这种错误有两种解决方法,任选一种即可:
    ①在创建Time2对象的时候必须为该Time2的单参构造函数提供一个参数。例如,类似“Time2 mytime2(1);”是可以的。
    ②为Time2类写一个默认(不带参数)构造函数。看看这个默认构造函数如何书写:
    在Time2.h的Time2类定义中,增加下面这行语句:

Time2() = default;

    请注意这个写法,这相当于在默认构造函数声明的末尾,分号之前增加了一个“=default”。这样写了之后,Time2默认构造函数就带有了“=default”特性——编译器能够为这种“=default”的函数自动生成函数体(等价于空函数体“{}”)。此时,在main主函数中的“Time2 mytime2;”代码行就不会再报错了。可以看到,增加“=default”是一种偷懒写法,让开发者不用自己写默认构造函数的函数体。
    一般这种“=default”写法只适合一些比较特殊的函数,如默认构造函数(不带参数),普通成员函数就不能这样写,例如如下写法是错误的:

int ptfuncX() = default;

    而带参数的构造函数,系统也不认为是特殊函数,所以如下写法也是错误的:

Time2(int, int) = default;

    上面是把“=default”放在Time2类声明(Time2.h)中,所以,这个无参构造函数具备了inline特性。其实,把“=default”放在Time2类定义(Time2.cpp)中也可以。当然此时这个无参构造函数就不具备inline特性了。
    (1)在Time2.h的Time2类定义中,修改Time2默认构造函数。修改后如下:

Time2();

    (2)在Time2.cpp中,放默认构造函数的实现代码。如下:

Time2();

(2)=delete;

    现在再来看看“=delete;”,这个写法是用来让程序员显式地禁用某个函数而引入的。
    为了演示方便,在Time2.h和Time2.cpp中,注释掉所有和Time2类构造函数相关的代码,无论是带几个参数的构造函数。
    现在在main主函数中的代码行“Time2 mytime2;”是可以成功执行的。
    刚刚讲过,因为没有书写任何构造函数,又因为Time2.h中代码行“int Second{0};”的存在,导致编译器需要生成“合成的默认构造函数”,并且往这个“合成的默认构造函数”中插入代码来把Second成员变量的值初始化为0。
那如果想禁用编译器生成这个“合成的默认构造函数”,怎么做到呢?这就需要用到“=delete;”了。在Time2.h文件中书写如下代码:

Time2() = delete;

    此时会发现,main主函数中的代码行“Time2 mytime2;”再次报错,如图所示
在这里插入图片描述
    按照上面的讲解,在生成Time2对象时,因为要把Second成员变量初始化为0,所以编译器需要生成“合成的默认构造函数”,但是现在,通过“Time2()=delete;”代码行禁止编译器生成“合成的默认构造函数”,所以代码行“Time2 mytime2;”报错并不奇怪。
    但是,如果把类Time2定义中的“intSecond{0};”代码行注释掉,那么编译器就不需要生成“合成的默认构造函数”来初始化Second成员变量,那么是不是此时代码行“Time2 mytime2;”就不报错了呢?
    通过实践发现,代码行“Time2 mytime2;”依旧报错,而且错误提示和上图一样。
    这样看起来会给读者造成一种理解(误解):要成功构造mytime2对象,必须存在一个默认构造函数,如果程序员自己不写默认构造函数,编译器必然会生成“合成的默认构造函数”。
    但如果读者观察编译器生成的目标文件.obj,可以发现一个事实,即便去掉“Time2()=delete;”代码行,也只有在类Time2定义中存在代码行“int Second{0};”时编译器才会生成“合成的默认构造函数”,否则编译器不会生成“合成的默认构造函数”。
    所以笔者认为:“Time2()=delete;”代码行导致“Time2 mytime2;”代码行报错的问题焦点不在于编译器是否生成了“合成的默认构造函数”,而在于在编译器看来,这两行代码就是不能共存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值