类的构造函数,拷贝构造函数

C++ Primer 类的构造函数,拷贝构造函数,

在说这些内容之前,先说以下几个内容:
内置类型:算术类型(整型(字符,布尔型),浮点型)和空类型(空类型不对应具体的值,仅用于一些特殊的场合)
1、初始化:当对象再创建时获得了一个特定的值,我们说这个对象被初始化。
初始化和赋值是两个完全不同的操作,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
2、默认初始化
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型和定义变量的位置决定:
①内置类型:如果是内置类型的变量而未被显式初始化,它的值由定义的位置决定。定义于任何函数之外的变量被初始化为0;定义在函数体内部的内置类型变量将不被初始化,一个未被初始化的内置类型变量的值是未定义的(此时,程序可能继续工作、可能崩溃,也可能生成拉基数据),如果试图拷贝或以其他形式访问此类值将引发错误。
②类类型:执行类的默认构造函数(不严谨)。
3、值初始化
①内置类型:初始值自动设为0.
②类类型:执行类的默认构造函数(不严谨)。
4、字面值常量
主要说明字符和字符串字面值。字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾添加一个空字符‘\0’,因此字符串字面值的实际长度要比它的内容多1.例如,字面值’A’表示的就是单独的字符A,而字符串"A"则代表了一个字符的数组,该数组包含两个字符:一个是字母A,另一个是空字符。


先说类的构造函数
1、默认构造函数:类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参(一定没有吗?)。
合成的默认构造函数:如果我们的类没有显式地定义任何构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,称为合成的默认构造函数。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:
?如果存在类内的初始值(只能使用拷贝初始化(即使用=时)或使用花括号的形式初始化),用它来初始化成员。
?否则,默认初始化。
某些类不能依赖于合成的默认构造函数:
合成的默认构造函数只适合非常简单的类,对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:
?第一个原因也是最容易理解的一个原因就是编译器只有在发现类内不包含任何构造函数的情况下才回替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制,也就无法执行类对象的默认初始化过程。
?第二个原因是对于某些类来说,合成的默认构造函数很可能执行错误的操作。如果定义在块中的内置类型复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。该准则同样适用于默认初始化的内置成员。因此,含有内置类型或复合类型成员的类应该在类的内部提供初始值,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。
?第三个原因是有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。

3、= default
在C++11 新标准中,我们可以通过将拷贝控制成员定义为 = default来显式地要求编译器生成合成的版本。其中,= default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果= default 在类的内部,则默认构造函数是内联的。

4、= delete:
对于某些类来说,合成的拷贝控制操作没有合理的意义。再次情况下,定义类时必须采用某种机制阻止编译器自动生成合成的拷贝控制操作,例如iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。在新标准下,我们可以通过在函数的参数列表后面加上 = delete 来指出我们希望将此函数定义为删除的函数
与=default不同,=delete必须出现在函数第一次声明的时候,这个差异与这些声明的含义在逻辑上是吻合的。与=default的另一个不同之处是,我们可以对任何函数指定=delete(我们只能对编译器可以合成的拷贝控制成员使用=default)。
值得注意的是,我们不能删除析构函数对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时变量。而且,如果有一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时变量。

4、构造函数初始值列表
它负责为新创建的对象的一个或几个数据成员赋初值,每个名字后面紧跟括号(或者花括号)括起来的成员初始值。当某个数据成员被构造函数初始值列表忽略时,该成员将在构造函数函数体之前执行默认初始化。即先在初始值列表中进行初始化,如果没有初始值列表,则执行默认初始化,随后在构造函数函数体内赋值给数据成员。 成员初始化的顺序与它们在类定义中出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置不会影响实际的初始化顺序。
构造函数的初始值有时必不可少:
有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。如果数据成员是const或者引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
建议使用构造函数初始值:(我的理解是:必须使用构造函数初始值)
在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。
除了效率问题外更重要的是,一些数据成员必须被初始化。建议养成使用构造函数初始值的习惯,这样能避免某些意想不到的编译错误,特别是遇到有的类含有需要构造函数初始值的成员时。

5、this:
当我们调用成员函数时,实际上是在替某个对象调用它,成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。对于我们来说,this形参是隐式定义的,实际上,任何自定义名为 this 的参数或变量的行为都是非法的。默认情况下,this的类型是指向类类型非常量版本的常量指针(底层const)。尽管this是隐式的,但它仍需要遵循初始化规则,意味着(默认情况下)我们不能把this绑定到一个常量对象上(56)。这一情况下也就使得我们不能在一个常量对象是调用普通的成员函数。为解决这个问题,C++的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。通过区分成员函数是否是const的,我们可以对其进行重载,其原因与我们之前根据指针参数是否指向const(208)而重载的原因差不多。具体说来,因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数;另一方面,对于非常量对象上,我们既可以调用常量版本也可以调用非常量版本,但显然非常量版本是一个更好的匹配。

6、类型别名:除了定义数据和函数成员之外,类还可以定义某种类型在类中的别名。由类定义的某种类型名字和其他数据成员一样存在访问限制,可以是public或者private中的一种。


在定义任何C++类时,拷贝控制操作都是必要部分。包括拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值预算符和析构函数。其中有三个基本操作可以控制类的拷贝操作:拷贝构造函数,拷贝赋值运算符和析构函数。而且在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。拷贝构造函数和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋值给同类型的另一个对象时做什么。对初学C++的程序员来说,必须定义拷贝控制操作,这常常令他们感到困惑,因为如果我们不显示定义这些操作,编译器也会为我们定义,但编译器定义的版本的行为可能并非我们所想。通常实现拷贝控制操作最困难的地方就是首先认识什么时候需要定义这些操作。

1、拷贝构造函数:
如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数哦都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数的第一个参数必须是引用类型(见博客https://www.cnblogs.com/this-543273659/archive/2011/09/18/2180575.html)。虽然我们可以定义一个接受非const引用拷贝构造函数,但此参数几乎总是一个const的引用。如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数,与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们定于合成拷贝构造函数。对某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象(见=delete)。而一般情况,合成拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中:对于内置类型直接拷贝;对于类类型,会使用其拷贝构造函数来拷贝。

现在我们可以完全理解直接初始化和拷贝初始化之间的差异了。
当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。
当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到我们正在创建的对象中,如果需要的话还要进行类型转化。

以上我还不能理解,我的理解是:直接初始化就是使用构造函数构造,拷贝构造函数是右侧对象先通过构造函数构造,然后通过拷贝构造函数(形参是引用)共享一个对象。

拷贝初始化不仅在我们使用=定义变量时会发生,在一下情况也会发生:
?将一个对象作为实参传递给一个非引用类型的形参
?从一个返回类型为非引用类型的函数返回一个对象
?用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
?某些类类型还会对它们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或是调用其insert或push成员时,容器会对其元素进行拷贝初始化。与之相对,用emplace成员创建的元素都进行直接初始化。

2、拷贝赋值运算符:
先介绍重载运算符,本质上是函数,其名字有operator关键字后接表示要定义的运算符号组成。某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式指针this参数。对于一个二元运算符,例如赋值运算符,其右侧运算对象作为显式参数传递。为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
合成拷贝赋值运算符:
如果我们没有为一个类定义拷贝赋值运算符,编译器会为我们定义一个合成拷贝赋值运算符,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值。

3、析构函数:
如果我们没有为一个类定义析构函数,编译器会为我们定义一个合成析构函数。
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化的顺序逆序销毁。
在一个析构函数中,不存在类似构造函数中初始值列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。认识到析构函数函数体自身并不直接销毁成员是非常重要的。成员是在析构函数函数体之后隐式的析构阶段中被销毁的。在整个对象销毁过程中,析构函数函数体是作为成员销毁步骤之外的另一部分而进行的
隐式销毁一个内置类型的指针类型不会delete它所指向的对象。与普通指针不同,智能指针是类类型,所以具有析构函数。

什么时候会调用析构函数:
当变量离开其作用域时被销毁。
当一个对象被销毁时,其成员被销毁。
容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
对于动态分配的对象,当对指向它的指针应用delete时被销毁。

三五法则:
当我们决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本的原则是首先确定这个类是否需要一个析构函数。如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和滚吧赋值运算符。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

且放白鹿青涯间

你的打赏将是我最大的鼓励

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

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

打赏作者

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

抵扣说明:

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

余额充值