构造函数语意学

2024-01-27 01:48
文章标签 构造函数 语意

本文主要是介绍构造函数语意学,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一、默认构造函数
    • 1.1、带有default constructor的成员类对象
    • 1.2、带有default constructor的基类
    • 1.3、带有一个虚函数的类
    • 1.4、带有一个虚基类的类
    • 1.5、小结
      • 1.5.1、两个误解
  • 二、拷贝构造函数
    • 2.1、default memberwise initialization
    • 2.2、不展现bitwise copy semantics
    • 2.3、带有虚函数的类
    • 2.4、带有虚基类的类
  • 三、程序转换语意学
    • 3.1、显式的初始化操作
    • 3.2、参数的初始化
    • 3.3、返回值的初始化
  • 四、成员初始化列表
  • 五、总结


一、默认构造函数

什么时候会合成一个default constructor呢?当编译器需要它的时候!此外,被合成的constructor只执行编译器所需的行动

C++标准规定,对于类X,如果没有任何用户声明的构造函数,那么它会有一个default constructor被隐式(implicit)声明出来。一个被隐式声明出来的default constructor将是一个trivial constructor

一个nontrivial default constructor才是编译器需要的那种,必要的话会由编译器合成。共有4种情况。

1.1、带有default constructor的成员类对象

如果一个类没有任何构造函数,但它内含一个成员对象,该对象有default constructor,那么这个类的implicit default constructor就是nontrivial,编译器需要为该类合成出一个default constructor。不过这个合成操作只有在constructor真正需要被调用时才会发生。

于是出现了一个问题:C++编译器如何避免合成出多个default constructor呢?解决方法是把合成的default constructor、copy constructor、destructor、assignment copy constructor都以inline方式完成。一个inline函数有静态链接,不会被文件以外者看到。如果函数太复杂,不适合做成inline,就会合成出一个explicit non-inline static实例。

举个例子:
在这里插入图片描述
编译器会为类Bar合成一个default constructor,被合成的Bar default constructor内含必要的代码,能够调用类Foo的default constructor来处理成员对象foo,但它并不产生任何代码来初始化Bar::str。将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。被合成的default constructor看起来可能像这样:
在这里插入图片描述
需要注意的是,被合成的default constructor只满足编译器的需要,而不是程序的需要。为了让程序正确执行,字符指针str也需要被初始化。假设程序员经由下面的default constructor提供了str的初始化操作:
在这里插入图片描述
现在程序的需求获得满足了,但是编译器还需要初始化成员对象foo。由于default constructor已经被显式地定义出来,编译器没办法合成第二个。编译器采取的行动是:如果类A内含一个或一个以上的成员类对象,那么类A的每一个构造函数必须调用每一个成员类的default constructor。编译器会扩张已经存在的构造函数,在其中安插一些代码,使得用户代码被执行之前,先调用必要的default constructor。在上述例子中,扩张后的constructor可能像这样:
在这里插入图片描述
如果有多个类成员对象都要求构造函数初始化操作,情况会如何呢?C++语言要求以成员对象在类中的声明顺序来调用各个构造函数。这一点由编译器完成,它为每一个构造函数安插程序代码,以成员声明顺序调用每一个成员所关联的default constructor。这些代码被安插在用户代码之前。

1.2、带有default constructor的基类

如果一个没有任何构造函数的类派生自一个带有default constructor的基类,那么这个派生类的default constructor会被视为nontrivial,因此需要被合成出来。它将调用上一层基类的default constructor(根据它们的声明顺序)。对一个后继派生的类而言,这个合成的构造函数和一个被显式提供的default constructor没有什么差异。

如果设计者提供多个构造函数,但其中都没有default constructor,情况如何呢?编译器会扩张现有的每一个构造函数,将用以调用所有必要的default constructor的程序代码加进去。它不会合成一个新的default constructor,因为存在其他由用户所提供的构造函数。如果同时存在着带有default constructor的成员类对象,那些default constructor 也会被调用——在所有基类构造函数都被调用之后。

1.3、带有一个虚函数的类

举个例子:
在这里插入图片描述
下面两个扩张行动会在编译期间发生:

  • 1、一个virtual function table(在cfront中被称为vtbl)会被编译器产生出来,其中包含类的虚函数地址
  • 2、在每一个类对象中,一个额外的指针(也就是vptr)会被编译器合成出来,内含相关vtbl的地址

此外,widget.flip()的虚调用操作(virtual invocation)会被重新改写,以使用widget的vptr和vtbl中的flip()条目:
在这里插入图片描述
有关虚函数表的内容,请参考Function语意学。

为了让这个机制发挥功效,编译器必须为每一个Widget对象的vptr设定初值,放置适当的virtual table地址。对于类所定义的每一个构造函数,编译器会安插一些代码来做这样的事情。对于那些未声明任何构造函数的类,编译器会为它们合成一个default constructor,以便正确地初始化每一个类对象的vptr。

1.4、带有一个虚基类的类

虚基类的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够于执行期准备妥当。在下面这个例子中:
在这里插入图片描述
编译器无法固定住foo()之中经由pa而存取的 X::i 的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变执行存取操作的那些代码,使 X::i 可以延迟至执行期才决定下来。上述foo()可能被改写为:
在这里插入图片描述
其中__vbcX 表示编译器所产生的指针,指向虚基类X。__vbcX是在类对象构造期间被完成的。对于类所定义的每一个构造函数,编译器会安插那些允许每一个虚基类的执行期存取操作的代码。如果类没有声明任何构造函数,编译器必须为它合成一个default constructor。

1.5、小结

有4种情况,会使编译器必须为未声明构造函数的类合成一个default constructor。C++标准把那些合成的构造函数称为implicit nontrivial default constructor。被合成出来的构造函数只能满足编译器(而非程序)的需要。它之所以能够完成任务,是借着"调用成员对象或基类的default constructor"或是"为每一个对象初始化其虚函数机制或虚基类机制"而完成的。至于不存在那4种情况而又没有声明任何构造函数的类,我们说它们拥有的是implicit trivial default constructor,它们实际上并不会被合成出来

在合成的default constructor中,只有基类的子对象和成员对象会被初始化。所有其它的非静态数据成员(如整数、整数指针、整数数组等等)都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则非必要。

1.5.1、两个误解

C++新手一般有两个常见的误解:

  • 1、任何类如果没有定义default constructor,就会被合成出一个来
  • 2、编译器合成出来的default constructor会显式设定类内每一个数据成员的默认值

如你所见,没有一个是真的!

二、拷贝构造函数

有3种情况,会以一个对象的内容作为另一个对象的初值。

  • 第一种情况是对一个对象做显式的初始化操作,像这样:
    在这里插入图片描述
  • 第二种情况是当对象被当做某个函数的参数时,例如:
    在这里插入图片描述
  • 第三种情况是当函数返回一个类对象时,例如:
    在这里插入图片描述
    假设类显式定义了一个copy constructor,像这样:
    在这里插入图片描述
    那么在大部分情况下,当一个类对象以另一个同类实例作为初值,上述构造函数会被调用。这可能会导致一个临时性类对象的产生或导致程序代码的蜕变(或两者都有)。

2.1、default memberwise initialization

如果类没有提供一个explicit copy constructor又当如何呢?当类对象以相同类的另一个对象作为初值,其内部是以所谓的default memberwise initialization手法完成的,也就是把每一个内建的或派生的数据成员的值,从某个对象拷贝一份到另一个对象身上。不过它并不会拷贝其中的成员类对象,而是以递归的方式施行memberwise initialization。

这样的操作是怎么完成的呢?就是由copy constructor完成的。当一个类不含copy constructor时,编译器会在必要时为其合成一个。"必要"的意思是指当类不展现bitwise copy semantics时

就像default constructor一样,C++ Standard上说,如果类没有声明一个copy constructor,就会有隐式的声明(implicitly declared)或隐式的定义(implicitly defined)出现。C++ Standard把copy constructor区分为trivial 和nontrivial两种。只有nontrivial的实例才会被合成于程序之中。决定一个copy constructor是否为trivial的标准在于类是否展现出所谓的"bitwise copy semantics"。

2.2、不展现bitwise copy semantics

共有4种情况,在这些情况下类不展现出bitwise copy semantics:

  • 1、当类内含一个成员对象而该对象的类声明有一个copy constructor时(不论是被类设计者显式地声明,或是被编译器合成)
  • 2、当类继承自一个基类而该基类存在一个copy constructor时(不论是被类设计者显式地声明,或是被编译器合成)
  • 3、当类声明了一个或多个虚函数时
  • 4、当类派生自一个继承串链,其中有一个或多个虚基类时

对于前两种情况,编译器必须将成员或基类的copy constructor调用操作安插到被合成的copy constructor中。以下详细说明后两种情况。

2.3、带有虚函数的类

编译期间的两个程序扩张操作:

  • 增加一个vtbl,内含每一个有作用的虚函数的地址
  • 一个指向vtbl的指针vptr,安插在每一个类对象内

很显然,如果编译器对于每一个新产生的类对象的vptr不能成功而正确地设定好其初值,将导致可怕的后果。因此,当编译器导入一个vptr到类中时,该类就不再展现bitwise semantics了。现在,编译器需要合成出一个copy constructor以求将vptr适当地初始化。

举个例子:
在这里插入图片描述
当ZooAnimal类对象以另一个ZooAnimal对象作为初值,或Bear类对象以另一个Bear类对象作为初值,都可以直接靠bitwise copy semantics完成。例如:
在这里插入图片描述
yogi会被default Bear constructor初始化。而在构造函数中,yogi的vptr被设定指向Bear类的虚函数表。因此,把yogi的vptr值拷贝给winnie的vptr是安全的。如下所示:
在这里插入图片描述
当一个基类对象以其派生类对象的内容做初始化操作时,其vptr复制操作也必须保证安全,例如:
在这里插入图片描述
franny的vptr不可以被设定指向Bear 类的虚函数表(但如果yogi的vptr被直接bitwise copy的话,就会导致此结果),否则当通过基类的指针或引用调用虚函数时,就会发生错误。如下所示:
在这里插入图片描述
也就是说,合成出来的ZooAnimal copy constructor会显式设定对象的vptr指向ZooAnimal类的虚函数表,而不是直接从等号右边的类对象中将其vptr值拷贝过来。
在这里插入图片描述

2.4、带有虚基类的类

虚基类的存在需要特别处理。一个类对象如果以另一个对象作为初值,而后者有一个虚基类子对象,那么会使bitwise copy semantics失效。

每一个编译器对于虚继承的支持承诺,都代表必须让派生类对象中的虚基类子对象位置在执行器就准备妥当。维护位置的完整性是编译器的责任。bitwise copy semantics可能会破坏这个位置,所以编译器必须在它自己合成出来的copy constructor中做出仲裁。

举个例子:
在这里插入图片描述
编译器所产生的代码(用以调用ZooAnimal的default constructor、将Raccoon的vptr初始化,并定位出Raccoon中的ZooAnimal子对象)被安插在两个Raccoon constructor之内,成为其先头部队。

一个虚基类的存在会使bitwise copy semantics无效。问题并不发生于一个类对象以另一个同类的对象作为初值,而是发生于一个类对象以其派生类的某个对象作为初值。例如让Raccoon类对象以一个RedPanda对象作为初值,如下所示:
在这里插入图片描述
如果以一个RedPanda对象作为little_critter的初值,编译器必须判断后续当程序员企图存取其ZooAnimal子对象时是否能够正确地执行。在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个copy constructor,安插一些代码以设定虚基类指针/偏移的初值,以及执行其他的内存相关工作。

三、程序转换语意学

3.1、显式的初始化操作

已知有这样的定义X x0;,下面的三个定义,每一个都明显地以x0来初始化其类对象:
在这里插入图片描述
必要的程序转化有两个阶段:

  • 1、重写每一个定义,其中的初始化操作会被剥除
  • 2、类的copy constructor调用操作会被安插进去

在明确的双阶段转化之后,foo_bar()可能看起来像这样:
在这里插入图片描述

3.2、参数的初始化

C++ Standard说,把一个类对象当做参数传给一个函数(或是作为一个函数的返回值),相当于初始化操作:X xx = arg;,其中xx代表形式参数(或返回值)而arg代表真正的参数值。

对于下面的代码:
在这里插入图片描述
将会要求局部实例x0以memberwise的方式将xx当做初值。在编译器实现技术上,有一种策略是导入所谓的临时对象,并调用copy constructor将它初始化,然后将此临时性对象交给函数。上述代码将被转换为:
在这里插入图片描述
然而这样的转换只解决了问题的一半。问题的另一半出在foo()的声明上。临时性对象先以类X的copy constructor正确地设定了初值,然后再以bitwise方式拷贝到x0这个局部实例中。foo()的声明因而也必须被转化,形式参数必须从原先的一个类对象改变为一个类对象的引用,像这样:void foo(X& x0);,其中类X声明了一个destructor,它会在foo()函数完成之后被调用,对付那个临时性的对象。

3.3、返回值的初始化

已知下面的函数定义:
在这里插入图片描述
bar()的返回值如何从局部对象xx中拷贝过来呢?Stroustrup在cfront中的解决做法是一个双阶段转化:

  • 1、首先加上一个额外参数,参数的类型是类对象的引用,这个参数将用来放置被"拷贝构建(copy constructed)"而得的返回值。
  • 2、在return指令之前安排一个copy constructor调用操作,以便将欲传回的对象内容当做上述新增参数的初值。

真正的返回值是什么?最后一个转化操作会重新改写函数,使它不传回任何值。bar()将会被转化为:
在这里插入图片描述
这样的编译器优化操作,有时候被称为Named Return Value(NRV)优化。

现在编译器必须转换每一个bar()调用操作,以反映其新定义。例如:
在这里插入图片描述
将被转换为:
在这里插入图片描述

四、成员初始化列表

当写下一个constructor时,就有机会设定类成员的初值。或者经由成员初始化列表,或者在constructor函数体内。除了下述4种情况必须使用成员初始化列表外,其他情况下,任何选择其实都差不多。

  • 1、当初始化一个引用成员时
  • 2、当初始化一个常量成员时
  • 3、当调用一个基类的构造函数,而它拥有一组参数时
  • 4、当调用一个成员类的构造函数,而它拥有一组参数时

一个明显的问题是:成员初始化列表中到底会发生什么事情?许多C++新手对于成员初始化列表的语法感到迷惑,他们误以为它是一组函数调用。当然不是!

编译器会一一操作成员初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何explicit user code之前。例如:
在这里插入图片描述
将被转化为:
在这里插入图片描述

五、总结

在这里插入图片描述

这篇关于构造函数语意学的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/648704

相关文章

C++中第一次听到构造函数

在C++中第一次听到构造函数这个名词,在C#中又遇到了。   在创建某个类时,由于对该对象的状态(数据)不是很明确,因此需要对其进行初始化。比如说我们要在长方形这个类中创建一个对象,或者说新建一个长方形,那么我们首先要确定他的长和宽,假如我们无法确定它的长和宽,那么我们是无法造出一个长方形来的。所以就要使用这个长方形类中一个用来构造该类所有对象的函数--构造函数。由于该函数要在创建一个新对象

《C++中的移动构造函数与移动赋值运算符:解锁高效编程的最佳实践》

在 C++的编程世界中,移动构造函数和移动赋值运算符是提升程序性能和效率的重要工具。理解并正确运用它们,可以让我们的代码更加高效、简洁和优雅。 一、引言 随着现代软件系统的日益复杂和对性能要求的不断提高,C++程序员需要不断探索新的技术和方法来优化代码。移动构造函数和移动赋值运算符的出现,为解决资源管理和性能优化问题提供了有力的手段。它们允许我们在不进行不必要的复制操作的情况下,高效地转移资源

为什么构造函数不能为虚函数

1,从存储空间角度     虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。 2,从使用角度         虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调

【JavaScrip】为什么箭头函数不能做构造函数

在 JavaScript 中,箭头函数(Arrow Functions)的设计初衷是为了简化函数声明,并引入了一些新的语法特性。其中一个关键特性就是箭头函数不能用作构造函数。下面我们详细探讨这个问题的原因。 1. 箭头函数的特点 箭头函数有一些独特的特点,其中最重要的是: ● 词法作用域的 this: 箭头函数内部的 this 值绑定到定义时所在的上下文环境,而不是调用时的上下文环境。 ● 简

C/C++ 拷贝构造函数

一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: [c-sharp]  view plain copy int a = 100;   int b = a;    而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。 下面看一个类对象拷贝的简单例子。 [c-sharp]  view p

5.程序转换语意学

目录 1.显示的初始化操作 2.参数的初始化 3.返回值的初始化 4.在使用者层面做优化 5.Copy Constructor要不要? 1.显示的初始化操作 已知有这样的定义: X x0; 下面的三个定义,每一个都明显地以x0来初始化其class object; void foo_bar(){X x1(x0); //译注:定义了x1X x2 = x0; //译注:定义

【自用19.1】C++构造函数

构造函数的作用 在创建一个新的对象时,自动调用的函数,用来进行“初始化”工作: 对这个对象内部的数据成员进行初始化。 构造函数的特点 自动调用(在创建新对象时,自动调用)构造函数的函数名,和类名相同构造函数没有返回类型可以有多个构造函数(即函数重载形式)   构造函数的种类 默认构造函数 自定义的构造函数 拷贝构造函数 赋值构造函数 默认构造函数 没有参数的构造函数

第二百一十二节 Java反射 - Java构造函数反射

Java反射 - Java构造函数反射 以下四种方法来自 Class 类获取有关构造函数的信息: Constructor[] getConstructors()Constructor[] getDeclaredConstructors()Constructor<T> getConstructor(Class... parameterTypes)Constructor<T> ge

第七章 构造函数this静态单例模式

7.1 构造函数 构造函数与类名相同,无返回值。 类中未定义构造函数时,默认使用空参构造函数。 7.2 关键字this 使用this可增强代码的可读性 成员变量与局部变量同名时,this.var指当前对象的成员变量。函数中指此函数所在的对象。 7.3 关键字static 可用来修饰成员遍历和函数。 成员被该类所有对象所共享 内存中存在方法区,因此优先于对象存在。 可

类实例化和构造函数

类实例化和构造函数 类如何创立,如何调用构造函数源码rv汇编行为分析 一般成员函数虚函数源码汇编行为分析 纯虚函数汇编行为分析 多态源码汇编行为分析 为什么构造函数不能是虚函数 类如何创立,如何调用构造函数 源码 #include <iostream>using namespace std;// 抽象父类class Base {int a,b;public:Base(){