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

相关文章

SpringBoot项目中Redis存储Session对象序列化处理

《SpringBoot项目中Redis存储Session对象序列化处理》在SpringBoot项目中使用Redis存储Session时,对象的序列化和反序列化是关键步骤,下面我们就来讲讲如何在Spri... 目录一、为什么需要序列化处理二、Spring Boot 集成 Redis 存储 Session2.1

Java实例化对象的​7种方式详解

《Java实例化对象的​7种方式详解》在Java中,实例化对象的方式有多种,具体取决于场景需求和设计模式,本文整理了7种常用的方法,文中的示例代码讲解详细,有需要的可以了解下... 目录1. ​new 关键字(直接构造)​2. ​反射(Reflection)​​3. ​克隆(Clone)​​4. ​反序列化

C++类和对象之初始化列表的使用方式

《C++类和对象之初始化列表的使用方式》:本文主要介绍C++类和对象之初始化列表的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C++初始化列表详解:性能优化与正确实践什么是初始化列表?初始化列表的三大核心作用1. 性能优化:避免不必要的赋值操作2. 强

C++迭代器失效的避坑指南

《C++迭代器失效的避坑指南》在C++中,迭代器(iterator)是一种类似指针的对象,用于遍历STL容器(如vector、list、map等),迭代器失效是指在对容器进行某些操作后... 目录1. 什么是迭代器失效?2. 哪些操作会导致迭代器失效?2.1 vector 的插入操作(push_back,

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

Java对象转换的实现方式汇总

《Java对象转换的实现方式汇总》:本文主要介绍Java对象转换的多种实现方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java对象转换的多种实现方式1. 手动映射(Manual Mapping)2. Builder模式3. 工具类辅助映

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

Python中判断对象是否为空的方法

《Python中判断对象是否为空的方法》在Python开发中,判断对象是否为“空”是高频操作,但看似简单的需求却暗藏玄机,从None到空容器,从零值到自定义对象的“假值”状态,不同场景下的“空”需要精... 目录一、python中的“空”值体系二、精准判定方法对比三、常见误区解析四、进阶处理技巧五、性能优化