C++构造函数用法

目录

定义Sales_data 的构造函数

构造函数初始值列表 

 在类的外部定义构造函数

构造函数的初始值有时必不可少 

 成员初始化的顺序


什么是构造函数呢?

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载

构造函数的任务是初始化对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

构造函数的名字与类名相同。与其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型有所区别。

不同于其他成员函数,构造函数不能被声明为const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其"常量"属性。因此,构造函数在const对象的构造过程中可以向其写值。

当我们的类没有定义任何的构造函数,程序仍能正常的编译和运行吗?答案是肯定的。但是我们并没有为这些对象赋初始值,因此它一定执行了默认初始化。我们将类通过一个特殊的构造函数来控制默认初始化的过程的这个函数叫做默认构造函数

默认构造函数在很多方面都有特殊性。其一是,如果我们的类没有显式地定义构造函数,那么编译器会为我们隐式地定义一个构造函数。我们又将编译器创建的构造函数称为合成的默认构造函数

那么我们能否仅仅依赖合成的默认构造函数呢?对于一个普通的类来说,必须定义自己的默认构造函数的原因有三点:

  1. 编译器只有发现类中不包含任何构造函数时才会替我们生成一个默认的构造函数,一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则将没有构造函数。
  2. 对于类来说,合成的默认构造函数可能执行错误的操作。如果定义在块中的类型或复合类型(比如数组或指针)的对象被默认初始化,则它们的值是未定义的。
  3. 有时编译器不能为某些类合成默认的构造函数。如果一个类中包含一个其他类型的成员且这个成员的类型没有默认的构造函数,那么编译器无法初始化该成员。

定义Sales_data 的构造函数

  • istream&,从中读取一条交易信息
  • 一个cosnt string&,表示ISBN编号;一个unsigned,表示售出的图书数量;以及一个double,表示图书的售出价格。
  • 一个const string&,表示ISBN编号;编译器将赋予其他成员默认值。
  • 一个空参数列表(即默认构造函数)
struct Sales_data
{
    //新增的构造函数
    Sales_data()=default;
    Sales_data(const std::string &s):bookNo(s){ }
    Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){ }
    Sales_data(std::istream&);
    //之前已有的其他成员
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

= default的含义

 因为改构造函数不接受任何实参,所以它是一个默认的构造函数。

Sales_data()=default;

我们定义这个构造函数的目的是什么呢?这个构造函数的目的仅仅是因为我们需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。像这种需要默认的行为,可以在参数列表后面写上=default来要求编译器生成构造函数。其中=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default在类的内部,则默认函数是内联的;如果它在类的外部,则函数在默认情况下不是内联的。

上面的默认构造函数之所以对Sales_data 有效,是因为我们为内置数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始化表来初始化类内的每个成员。 

构造函数初始值列表 

 接下来介绍类中定义的另外两个构造函数:

Sales_data(const std::string &s):bookNo(s){ }
Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){ }

我们将冒号以及冒号和花括号之间的代码(花括号定义了函数体)称为构造函数的初始值列表

 构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号括起来。

含有三个参数的构造函数分别使用它的前两个参数初始化bookNo和units_sold,revenue的初始化则通过将售出图书总数和每本书单价相乘计算得到。

 只有一个string类型的参数的构造函数使用这个string对象初始化bookNo,对于units_sold和revenue则没有显式初始化。当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。因此只接受一个string参数的构造函数等价于:

//与上面定义的那个构造函数效果相同
Sales_data(const std::string &s):bookNo(s),units_sold(0),revenue(0){ }

通常情况下,构造函数使用类内初始值不失为一种好的选择,因为只要这样的初始值存在我们就能确保为成员赋予一个正确的值。不过,如果编译器不支持类内初始值,则所以构造函数都应该显式地初始化每个内置类型的成员。

注意:上面两个构造函数中的函数体都是空的。因为构造函数的唯一目的就是为数据成员赋值,则所有构造函数都应该显式地初始化每个内置类型的成员。

 在类的外部定义构造函数

以istream为参数的构造函数需要执行一些实际的操作。在它的函数体内,调用了read函数以给数据成员赋以初始值:

Sales_data::Sales_data(std::istream &is)
{
    read(is,*this);//read函数的作用是从is中读取一条交易信息然后存入this对象中
}

构造函数没有返回类型,所以上述定义从我们指定的函数名字开始。和其他成员函数一样,当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。因此Sales_data::Sales_data的含义是我们定义Sales_data类的成员,它的名字是Sales_data。又因为该成员的名字与类名相同,所以它是一个构造函数。

这个构造函数没有构造函数初始值列表,或者讲的更准确一点,它的构造函数初始值列表是空的。尽管构造函数初始值列表是空的,但是由于执行了构造函数体,所以对象的成员仍然能被初始化。

没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初值化,或者执行默认初始化。对于Sales_data来说,这意味着一旦函数开始执行,则bookNo将被初始化成空的string对象,而units_sold和revenue将是0。

为了更好地理解调用函数read的意义,要特别注意read的第二个参数是一个Sales_data对象的引用,在此例中,我们使用*this将“this”对象作为实参传递给read参数。

构造函数的初始值有时必不可少 

有时我们可以忽略数据成员初始化和赋值之间差异,但是并非总能这样。如果成员是const或者是引用的话,必须将其初始化。类似的,当成员属于某种类型且该类型没有定义默认构造函数时,也必须将这个成员初始化。 例如:

class ConstRef{
public:
    ConstRef(int ii);
private:
    int i;
    const int ci;
    int &ri;
};

和其他常量对象或者引用一样,成员ci和ri都必须初始化。因此,如果没有为它们提供构造函数的初始值的话将引发错误:

//错误:ci和ri必须被初始化
ConstRef::ConstRef(int ii)
{//赋值
    i=ii;//正确
    ci=ii;//错误:不能给const赋值
    ri=i;//错误:ri没有被初始化
}

我们初始化const或者引用类型数据的唯一机会就是通过构造函数初始值,因此正确形式应该是:

//正确:显式地初始化引用和const成员
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i) { }

注意:如果成员是const、引用,或者某种未提供默认构造函数的类型,我们必须通过构造函数初始值列表为这些成员提供初始值。

 成员初始化的顺序

 成员的初始化顺序与它们在类中定义中出现的顺序保持一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

一般来说,初始化的顺序没有什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化位置就很关键了。

举个例子:

class X{
    int i;
    int j;
public:
    //未定义的:i在j之前被初始化
    X(int val):j(val),i(j) { }
};

从构造函数初始值的形式上仿佛是先用val初始化了j,然后用j初始化i。实际上,i先被初始化,因此这个初始值的效果是试图使用为定义的值j初始化i!

最好令构造函数初始值与成员声明的顺序保持一致。而且如果有可能的话,尽量避免使用某些成员初始化其他成员。

如果构造函数写成这样更好:

X(int val):i(val),j(val) { }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无限酸奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值