文章目录
前言
俗话说得好,好记性不如烂笔头,本文主要是重温了一下经典书籍《深度探索C++对象模型》,然后一些内容摘录,主要记录的是我自己的精简版,建议读者有相关的知识需要还是慢慢去啃一下原书比较好。
思维导图
第一章 关于对象
首先介绍了 C++ 类的情况, C++类的内存存储方式是:只包含非静态成员变量的大小,成员函数(静态,非静态),静态成员变量都是单独存储的;有虚函数的则在内存中多一个虚函数表。
第二章 构造函数语意学
一般都会认为编译器会自动生成一些我们需要的构造函数,包括构造函数,析构函数,拷贝构造函数,赋值构造函数等。并且一般编译器自动生成的都是叫做 trival 的,这个主要是为了满足编译器自己的需求,而不是为了满足程序员的要求,所以程序变量没有初始化之类的,编译器也不会自动帮你初始化的,因为这是程序员的任务!
只有下面4种情况下才会真正的去合成相关的构造函数,并且是 nontrival 的否则很多是不会生成的。
构造函数
- 带有其他 default 构造的类成员对象
- 带有 default 构造基类的子类
- class 声明/继承里有虚函数
- 继承链里有虚继承
以上 4 种情况只有是在处理,带有或者继承有默认构造函数的类对象,因为要调用它们的初始化函数初始化它们。其次就是处理虚函数表。
拷贝构造函数
基本也是同构造函数的 4 种情况。
- 当 class 内带有其他声明有一个 copy constructor 时,不论是显式声明还是被编译器合成
- 当 class 继承自一个 base class 而其存在一个copy constructor 时
- 当 class 声明一个或对个虚函数时
- 当 calss 派生自一个继承链,其中有一个或多个虚基类时
前两种就还是编译器需要将 member 或者 base class 的拷贝构造函数安插到被合成的拷贝构造中,后面两种主要还是要处理好虚函数表的位置,而不就是简单的 copy 。
初始化列表
必须使用初始化列表的情况:
- 初始化一个引用对象时
- 初始化一个常量成员时
- 调用一个基类的构造函数,它有一组参数
- 调用一个类成员的构造函数,它有一组参数
1,2 点因为进入构造函数体内时,实际上变量都已经初始化完毕了,引用和 const 都是一旦初始化就不可改变的,构造函数能做的只有赋值,而 const 和 引用类型是不可以赋值的。
3,4 则是可能从效率部分考虑,不用初始化列表赋值,构造函数有参数的情况下,编译器会自动编程,生成一个临时对象,然后拷贝构造,然后析构临时对象;初始化列表则直接调用相关的构造函数了。
- 初始化列表顺序与成员声明顺序一致
- 初始化列表执行在构造函数之前
第三章 Data 语意学
- 空的类会有一个 bit 大小,使它的对象可以在内存中被分配一个独一无二的地址。
- non static data members 在类对象中的排列顺序和其被声明的顺序一致
- 编译器会在 name-mangling 来解决命名冲突的问题
- Static data member 存在 class 之外,所以可以直接访问,而 non static 直接放在每个类内,所以要有类对象来存取,直接存取由 this 指针来完成,由偏移位置 offset 来访问所有的对象,编译器就可知。
- 继承对象的 member 访问,多态可能运行期才确定,所以引用和指针在这个时候效率还是有差距的。
- 直接继承并不会增加空间/存取时间上的额外负担, base_calss = derive_class 对象赋值会发生切割。
- 单一继承 + 虚函数: 会有很多额外负担,多一个虚函数表也会继承下去
- 多重继承也是分别内存叠加,包括相关的虚函数表
- 虚继承是父类就只有一份,子类都指向同一块地方,多了一个指向父类的指针
第四章 Function 语意学
类成员函数分为 3 类:静态类成员函数,非静态类成员函数,虚成员函数。
非静态成员函数
编译器要求至少调用非静态成员函数的效率要跟直接调用一般的函数有一致的效率,所以实际调用编译器会通过 name_mangling 和返回值优化等,将类成员函数转成类似普通函数的形式(类名 + 函数名 + 参数列表 + 字符)
静态成员函数
特别之处是:
-
静态成员函数存储在类外,所以没有this 指针,因为 this 只能操作非静态类成员变量,所以静态成员函数只能操作静态类成员变量。
-
不能用 const,vilatile 或者 virtual ,这些关键字的操作前提都是需要 this 指针,例如 const 表示不修改类的成员变量,static 都访问不了普通的类成员变量,何来 const 一说呢,虚函数也需要 this 指针来索引虚函数的位置。
-
不需要类对象就可以被直接调用,可以称为 callback 函数,直接用在线程函数是。
虚成员函数
C++ 中多态是指“以一个 public base class 的指针(引用)寻址出一个 derived class object” 的意思。
所以实现多态又不能直接让指针上加载多态必要的信息,这些信息是:指针所指的正确类型,低啊用虚函数的地址等。否则效率可想很低。
单一继承虚函数
通过虚函数表来实现,一个 class 只会有一个虚函数表,继承子类再重新实现虚函数,则子类的虚函数表中地址直接替换成子类自己的地址。
多重继承下的虚函数
会继承两个表, this 指针需要调整:
// 基类1的虚函数表
Base1 -> Base1::~Base1()
-> Base1::A()
-> Base1::B()
// 基类2的虚函数表
Base2 -> Base2::~Base2()
-> Base2::A()
-> Base2::C()
// 子类的虚函数表1,重新实现了虚函数 A
Derived -> Derived::~Derived
-> Derived::A()
-> Base1::B()
-> Derived::D() // 有新增的默认直接放在第一个表后面
// 子类的虚函数表2,重新实现了虚函数 A
-> Derived::~Derived
-> Derived::A()
-> Base2::C()
第五章 构造、析构、拷贝语意学
这部分内容主要就是对第 2 章的进一步具体讲解,主要是继承关系中构造、拷贝、析构函数的一些行为特点,例如当拷贝函数是 trival 时候其实编译器的行为。
后面6, 7两章主要是针对运行期,new delete,包括模板实例化的一些内容介绍,
RTTI
下面记录学习到了之前一直只闻其名,不知其意的名词 RTTI:
后面主要记录的就有一个 RTTI(Runtime Type identification) 执行期类型识别。主要就是虚函数表一般除了存储各个虚函数的地址,开头还有一个 type_info 的数据结构,这个数据结构就是多态时类型推导用的。包括 dynamic_cast 就是会利用这个 type_info 的信息,确认当前的指针是否可以进行转换,不能转换则返回 Nullptr。
模板函数的实现逻辑
编译器并不是把模板生成能处理任意类的函数,其会对函数模板通过具体的类型产生不同的函数,编译器对会函数模板进行两次编译:
- 第一次是编译其声明,即模板代码本身
- 在调用的地方,对参数替换后的代码进行再编译
所以一般模板的声明和定义都在头文件里,因为函数模板只有被实例化才是真正的函数,如果头文件中只有声明,没有定义,那么编译器无法实例化该模板,最终导致链接错误。
上面 5,6,7 三章这里就不详细记录了,感兴趣的可以阅读原书。