C++构造函数

1 构造函数的定义及特征

构造函数是类的一种特殊成员函数,其主要作用是对类的成员进行初始化,保证每个类成员都有一个合适的初始值。

构造函数有以下特征

  • 函数名与类名一致
  • 没有返回值
  • 对象实例化时自动调用对应的构造函数
  • 支持函数重载

构造函数语法:类名(){}

需要注意的是, 没有返回值和返回值为空是两码事。返回值为空指的是该函数有返回值,其中返回值为void类型。而支持重载则说明,在一个类中,可以同时存在多个构造函数。

在代码段1中,People类提供了两个构造函数,其中第一个是无参构造函数,第二个是有参构造函数。在test函数中创建对象时,father调用的是无参构造函数,而son调用的有参构造函数,说明构造函数重载为对象的实例化提供了多种初始化方式,而对象实例化时自动调用对应的构造函数,保证了对象一定可以被初始化。

代码段1:

class People
{
public:
    //无参构造函数 
    People(){} = 

    //有参构造函数
    People(int age, std::string sex, std::string name)
    {
        _age = age;
        _sex = sex;
        _name = name;
    }
private:
    int _age;
    std::string _sex = "男"; //C++11新标准规定,可以为数据成员提供一个类内初始值。
                        //创建对象时,类内初始值用于初始化数据成员
    stf::string _name;
};

void test()
{
    People father;       //不能写成People father(),否则会认为是函数的声明;
    People son(5, "男", "张三");
}

2 默认构造函数

默认构造函数是不用传参就可以调用的构造函数,其主要有三种:

  1. 在我们没有显式地定义类的构造函数时,编译器会自动提供一个构造函数,称为合成的默认构造函数
  2. 我们自己定义的无参构造函数,如代码1中的无参构造函数。
  3. 我们自己定义的全缺省的构造函数。

2.1 合成的默认构造函数

2.1.1 默认初始化

在讲合成的默认构造函数之前,需要先了解默认初始化

如果定义变量时没有指定初值,则变量会被默认初始化,赋予一个默认值。在C++中,变量有内置类型和自定义类型,内置类型如int、float、char等,而自定义类型是用class、struct、union自己定义的类型。变量的默认值是什么,取决于其变量类型

在代码段1中,People类中的_age变量为内置类型,如果调用无参构造进行初始化,该变量将不被初始化。在VS2019编译器中,报出如下警告:
在这里插入图片描述

执行之后,从监视窗口我们可以获知对象father和son的初始化结果,如下图:

在这里插入图片描述

可以看出,对象father的_age在初始化后,依然为随机值,也就是说_age并未初始化。而father的_sex被初始化为"男",是因为我们为_sex提供了类内初始值。C++11标准规定,可以为数据成员提供一个类内初始值,创建对象时,类内初始值用于初始化数据成员。father的_name初始化后为空串。C++中,string类规定若没有指定初始值,则默认初始化为空串

一个未初始化的内置类型变量的值是未定义的,如果对该变量进行拷贝或者以其他形式来访问,可能会导致错误。

2.1.2 合成的默认构造函数初始化类成员变量的规则

我们先来看一段代码:

代码段2

 class Cube
    {
    private:
        int _length;
        int _width;
        int _height;
    };
    
    void test()
    {
        Cube c1;
    }

该代码段定义了一个Cube类,其成员变量都是私有的int类型,初始化前后的值如下面两图所示。

初始化前:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EfjQXHFl-1628063908686)(C:\Users\Chenxu Gong\AppData\Roaming\Typora\typora-user-images\image-20210526013320882.png)]

初始化后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-08aPw4Wr-1628063908687)(C:\Users\Chenxu Gong\AppData\Roaming\Typora\typora-user-images\image-20210526013347470.png)]

可以发现,初始化前后数值都是随机值。那难道默认的构造函数不能有效初始化成员变量,其根本没有任何作用

答案是否定的。

根据我们在2.1.1中所述,默认初始化时,变量的默认值取决于变量类型。在Cube类中,三个成员变量均为int型,变量不会被初始化。如果成员变量中的类型是某种自定义类型,效果则完全不一样,且看代码段3。

代码段3

class People
{
public:
    //参数都为默认实参的构造函数,详见2.2
    People(int age = 22, std::string sex = "female", std::string name = "Sefanie")
    {
        _age = age;
        _sex = sex;
        _name = name;
    }
private:
    int _age;
    std::string _sex;
    std::string _name;
};

class Family
{
private:
    People mother;
    People Father;
    People Son;
};

int main()
{
    Family fa;
    return 0;
}

我们创建了一个Family类,其中有三个成员变量,三个成员变量均为People类,在创建对象fa后,其三个成员变量均被初始化。我们从监视窗口可以获知,fa的三个成员变量均得到有效初始化。这是因为Family类的合成的默认构造函数调用了People类的默认构造函数(1.2.2中将讲述),从而实现了三个成员变量的有效初始化、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6WmMeA0Y-1628063908688)(C:\Users\Chenxu Gong\AppData\Roaming\Typora\typora-user-images\image-20210527160643957.png)]

当类内变量为默认类型时,合成的默认构造函数按照如下规则进行初始化:

  1. 如果有类内初始值,则用它来初始化成员变量
  2. 否则执行默认初始化

当类内变量为自定义类型,且该自定义类型存在默认构造函数时,通过调用成员变量的默认构造函数来实现成员变量的初始化。

2.1.3 有些场合不能使用合成的默认构造函数

当类没有声明任何成员变量时,编译器才会提供合成的默认构造函数,然而在以下场合,不推荐使用合成的默认构造函数:

  • 类内存在内置类型或者复合类型(如数组和指针),且未提供类内初始值时。此时成员变量将不会被初始化,使用未初始化的变量可能会造成不可预计的后果。
  • 合成的默认构造函数是通过调用该自定义类型的默认构造函数来初始化类成员的,当类内存在自定义类型且该自定义类型没有默认构造函数时,编译器无法初始化该成员。若我们将代码段3改为代码段4,编译器将会报错。

代码段4

class People
{
public:
    //此处是和代码3的唯一区别,即少了默认实参,此时People不再有默认构造函数,该代码段将报错
    People(int age, std::string sex, std::string name)
    {
        _age = age;
        _sex = sex;
        _name = name;
    }
private:
    int _age;
    std::string _sex;
    std::string _name;
};

class Family
{
private:
    People mother;
    People Father;
    People Son;
};

int main()
{
    Family fa;
    return 0;
}

2.2 全缺省的默认构造函数(参数都为默认实参的构造函数)

在代码段3中,Family类的构造函数中,每个参数都有默认实参。根据我们在2.1.3中的描述,若类内存在自定义类型的成员变量,合成的默认构造函数必须通过调用这个成员变量的默认构造函数来实现对其的初始化。如果该成员变量不存在默认构造函数,编译器将报错。

而实际上,代码段3并未报错,也就是说,当构造函数的所有参数都有默认实参时,该构造函数也是默认构造函数。

在大多数情况下,我们都要自己写构造函数,推荐使用全缺省的方式来初始化成员变量,这样能适应大多数场景

3 构造函数初始化列表

3.1 初始化列表的书写方式

构造函数的唯一目的就是为数据成员赋初值,因此在实际应用中,构造函数的函数体往往用以下的方式来书写。我们将代码段3的People类修改如下:

代码段5

class People
{
public:
    //原始版本的构造函数
    //People(int age = 22, std::string sex = "female", std::string name = "Sefanie")
    //{
    //    _age = age;
    //    _sex = sex;
    //    _name = name;
    //}
    
    //初始化列表版本的构造函数
    People(int age = 22, std::string sex = "female", std::string name = "Sefanie"):_age(age),_sex(sex),_name(name){}
private:
    int _age;
    std::string _sex;
    std::string _name;
};

在这个定义中,在冒号和花括号之间出现了新的部分,花括号内为空实现,这个新出现的部分称为构造函数的初始化列表。其重要作用是为新创建的对象的几个成员变量赋初值。

语法:构造函数():属性1(值1),属性2(值2)... {}

为了巩固初始化列表的写法,我们再来看一个例子

代码段6

class Date
{
public:
    //_month和_day都初始化为1,_year用传入的参数进行初始化
    Date(int year):_year(year),_month(1),_day(1){}
    
    //_year, _month, _day都用传入的参数进行初始化
    Date(int year, int month, int day) :_year(year), _month(month), _day(day) {}
private:
	int _year;
	int _month;
	int _day;
};

3.2 为什么使用初始化列表?

在代码段1,3,4中的构造函数,是以赋值的方式实现构造函数的,而在代码段4,5中,是以初始化的方式实现构造函数的。如果没有在构造函数的初始化列表这种显式地初始化成员,那么在进入函数体之前,会先进行默认初始化,函数体内再进行赋值操作。

赋值的方式和初始化的方式与底层效率有关,前者是先初始化后赋值,而后者是直接初始化数据成员的。

其次,**如果类数据成员是const类型或者引用,必须将其初始化,而不能在函数体内才赋值。**因为const就表明该成员变量是只读属性,因此函数体内是无法改变const属性的成员变量的,而引用定义的时候,就需要初始化,如下例:

代码段7

class A
{
public:
    A(int i);
private:
    int _i;
    const int _j;
    int& _k;
};

//错误示例
//A::A(int i)
//{
//    _i = i;
//    _j = i; //函数体内无法修改const,错误
//    _k = _i; //引用应该在定义时或者初始化列表中进行初始化,错误
//}

//正确
A::A(int i) :_i(i), _j(i), _k(_i) {}

如果按照代码段7中的错误示例书写,在VS2019中将会报如下错误。也就是说,初始化const和引用类型的变量的唯一机会就是在初始化列表中构造初始值。
在这里插入图片描述

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值