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++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

javaSE类和对象进阶用法举例详解

《javaSE类和对象进阶用法举例详解》JavaSE的面向对象编程是软件开发中的基石,它通过类和对象的概念,实现了代码的模块化、可复用性和灵活性,:本文主要介绍javaSE类和对象进阶用法的相关资... 目录前言一、封装1.访问限定符2.包2.1包的概念2.2导入包2.3自定义包2.4常见的包二、stati

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3