C++对象模型:第2章(1)-构造函数

2024-08-28 04:08
文章标签 c++ 模型 对象 构造函数

本文主要是介绍C++对象模型:第2章(1)-构造函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、默认构造函数

以下两个观点是错误的:

(1)任何类,如果没有定义默认构造函数,编译器就会自动合成一个(但是作者Stanley B. Lippman在Primer中说“任何类没有显式定义构造函数就会默认生成一个”,这两种说法有点冲突)。

(2)编译器合成的默认构造函数会显式将类的每个成员设定为默认值。


        有四种情况,会造成编译器必须为未声明构造函数的类合成一个默认构造函数。C++标准将这些合成的默认构造函数称作“隐式有用构造函数(implicit nontrivial default constructors)”。被合成的构造函数只能满足编译器(而不是程序)的需求;它之所以能完成任务是因为以下四种情况。

       如果类不是以下四种情况又没有声明任何构造函数,那么它们拥有的是implicit trivial default constructor,它们实际上并没有被合成出来。


 C++ standard:对于class X ,如果没有任何user-declared constructor,那么会有一个default constructor 被暗中(implicitly)声明出来,一个被暗中声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor


        默认情况下,全局变量的内存会保证在程序启动时被初始化为0,但是局部变量位于程序的堆栈中,堆对象位于自由空间中,都不会被初始化为0,它们的内容是内存上次被使用时的“遗迹”

#include <iostream>
#include <string>
using namespace std;class A {
public:int val; 
};int main() {A a1;  // A a1 = A();cout << a1.val << endl;
}
上面的程序在VS2010下能够通过编译但是在执行时出错:提示“The variable ‘a1' is being used without being initialized”;但是在GCC下会输出负的随机值。

如果将main函数中第一个语句改为注释的那样:创建一个空的A的对象,然后将其赋值给a1,则会有如下结果:在VS2010下和在GCC均输出0.


要是将class A中再增加一个string成员,如下面代码:

#include <iostream>
#include <string>
using namespace std;class A {
public:int val; string str;
};int main() {A a1;  // A a1 = A();cout << a1.val << endl;
}
此时在VS2010下会输出随机值(而不是如第一段程序那样运行时错误);但是在GCC下会输出正的随机值。

如果将main函数中第一个语句改为注释的那样:创建一个空的A的对象,然后将其赋值给a1,则会有如下结果:VS2010会输出随机值,而GCC会输出0.



这两个程序的运行结果似乎表明,默认构造函数的行为很复杂,和编译器相关,具体看下面:

情况1:一个类的某成员带有默认构造函数时

         如果一个类没有任何构造函数,但是它的一个成员对象有默认构造函数,例如上面第二段程序:A类没有构造函数,但是它的成员string类型的str有默认构造函数,那么这个类的implicit default constructor就是“nontrivial”(有用的)的,编译器需要为该类合成一个默认构造函数。不过这个合成操作指引在构造函数真正需要调用时才会发生。被合成的默认构造函数包含必要的代码,能够调用A的成员str的默认构造函数,但是它并不产生任何代码来初始化A::val【因此本文开篇第二种说法错误】。是的,初始化str是编译器的责任,但初始化val则是程序员的责任

那么,程序员可以定义一个显式的构造函数来初始化val,如:

<pre name="code" class="cpp"><pre name="code" class="cpp">class A {
public:A() {  val = 0; }int val; string str;
};

 
 

         这样的话,程序的需求满足了,但是编译器还需要初始化类成员str。由于默认构造函数被显式定义了,那么编译器没办法合成第二个,此时编译器的行动是:“如果类A内含有一个或一个以上的类成员对象,那么类A的每个构造函数必须调用每个类成员的默认构造函数”。编译器会扩张已经存在的构造函数,在其中插入一些代码,使得用户代码执行前,必须先调用必要的默认构造函数,那么扩张后的构造函数类似这样:

<pre name="code" class="cpp">class A {
public:A() {  val = 0; str = string(); }int val; string str;
};

 

情况2:带有默认构造函数的基类

         和情况1类似:如果一个没有任何constructor的class派生自一个带有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。

        如果设计者提供了多个构造函数,但其中没有默认构造函数,编译器同样会扩张现有的每个构造函数,将“用以调用所有必要之构造函数”的程序代码加进去。它不会合成新的默认构造函数,因为存在设计者显式定义的构造函数。

情况3:带有虚函数的类

        当一个类声明了或继承的类里有虚函数时,同样要合成默认构造函数。

当没有显式声明一个构造函数时,编译器会进行两个扩张行为:

(1)编译器会产生一个虚函数表(vtbl,virtual function table),表内存放虚函数地址;

(2)在每一个类对象中,编译器会额外合成一个指针成员(vptr,pointer member),该指针成员指向虚函数表的地址

        编译器必须为每个这种类的对象的vptr设定初值,放置适当的虚函数表的地址。对于类所定义的每个构造函数,编译器会安插一些代码来做这样的事情。对于那些未声明任何构造函数的类,编译器会为它们合成一个默认的,以便正确地初始化每个类对象的vptr。

情况4:带有虚基类的类

多重继承和虚继承的内存布局

如下的代码,Top类是一个虚基类:

#include <iostream>
#include <string>
using namespace std;class Top {
public:Top():a(1) {}int a;
};class Left: virtual public Top {
public:Left(): b(2) {}int b;
};class Right: virtual public Top {
public:Right():c(3){}int c;
};class Buttom: public Left, public Right {
public:Buttom(): d(4){}int d;
};void foo(const Right *pa) {pa->a = 1024;
}int main() {//这里输出类的大小,分别为4,12,12,24cout << sizeof(Top) << " " << sizeof(Left) << " " << sizeof(Right) << " " <<sizeof(Buttom) << endl;foo(new Right);foo(new Right);
}
在编译的时候函数foo无法确定pa->Top::a的实际位置,因为pa的真正类型是可变的。编译器只有在执行的时候才决定其位置。foo函数可能会被编译器写成这样:

void foo(const  Right *pa) { pa->__vbcX->a = 1024; }

__vbcX是编译器所产生的指针,指向虚基类Top,该指针时再类对象构造期间完成的。对于类定义的每个构造好桉树,编译器都会安插那些“允许每一个虚基类的执行期存取操作”的代码。如果类没有声明任何构造函数,编译器必须为它合成一个默认的。

这篇关于C++对象模型:第2章(1)-构造函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

C++中NULL与nullptr的区别小结

《C++中NULL与nullptr的区别小结》本文介绍了C++编程中NULL与nullptr的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编... 目录C++98空值——NULLC++11空值——nullptr区别对比示例 C++98空值——NUL

C++ Log4cpp跨平台日志库的使用小结

《C++Log4cpp跨平台日志库的使用小结》Log4cpp是c++类库,本文详细介绍了C++日志库log4cpp的使用方法,及设置日志输出格式和优先级,具有一定的参考价值,感兴趣的可以了解一下... 目录一、介绍1. log4cpp的日志方式2.设置日志输出的格式3. 设置日志的输出优先级二、Window

Python实现对阿里云OSS对象存储的操作详解

《Python实现对阿里云OSS对象存储的操作详解》这篇文章主要为大家详细介绍了Python实现对阿里云OSS对象存储的操作相关知识,包括连接,上传,下载,列举等功能,感兴趣的小伙伴可以了解下... 目录一、直接使用代码二、详细使用1. 环境准备2. 初始化配置3. bucket配置创建4. 文件上传到os

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函