C++ 零散知识点速记 -- <深入探索C++对象模型> 读书笔记

错误认知


没有任何构造函数的时候, 编译器总会生成默认构造函数

编译器仅在必要的时候生成默认构造函数

析构函数同理

条件 :

  • 有带有默认构造函数的member
  • 有带有默认构造函数的base class
  • 有virtual function
  • 有virtual inherit

任何对象都带有vptr / 可以对任何对象指针进行 dynamic_cast 操作

只有对象具有 多态 属性的时候 , 才具有 vptr , 才可以对其指针进行 dynamic_cast .

对一个没有多态 属性的指针进行dynamic_cast会导致编译器报错.

    class GrandPa
    {
        public:
            int  a;
            char a1;
            // virtual void func(){}
    };

    class Father : public GrandPa
    {
        public:
            int b;
            char a2;
    };

    class Son : public Father
    {
        public:
            int i;
            char a3;
    };
    int main()
    {
        Son s;
        Father f ;
        GrandPa *  p1 = &s;
        GrandPa * p2 = &f;

        printf("The addr of p1 : %p\n" , dynamic_cast<Son*>(p1) ) ;
        printf("The addr of p1 : %p\n" , dynamic_cast<Father*>(p1) ) ;
        printf("The addr of p2 : %p\n" , dynamic_cast<Son*>(p2) ) ;
        return 0;
    }

报错:

c++_test.cpp: In function ‘int main()’:

c++_test.cpp:42:63: error: cannot dynamic_cast ‘p1’ (of type ‘class GrandPa*’) to type ‘class Son*’ (source type is not polymorphic)

c++_test.cpp:43:66: error: cannot dynamic_cast ‘p1’ (of type ‘class GrandPa*’) to type ‘class Father*’ (source type is not polymorphic)

c++_test.cpp:44:63: error: cannot dynamic_cast ‘p2’ (of type ‘class GrandPa*’) to type ‘class Son*’ (source type is not polymorphic)

取消对GrandPa的虚函数的屏蔽 , 结果:

The addr of p1 : 0x7fff8f017ee0

The addr of p1 : 0x7fff8f017ee0

The addr of p2 : (nil)




新知识点


C++ 语言保证”出现在派生类中的基类对象 有其原样完整性”

指向Data Member 的指针含义 : member 在对象中的偏移

例子 :
    class GrandPa
    {
        public:
            int a;
            char a1;
    };

    class Father : public GrandPa
    {
        public:
            char a2;
    };

    class Son : public Father
    {
        public:
            char a3;
    };

    #include <iostream>
    #include <stdio.h>
    using std::cout;
    using std::endl;
    int main()
    {
        cout<<"Sizeof GrandPa : "<<sizeof(GrandPa)<<endl;
        cout<<"Sizeof Father  : "<<sizeof(Father)<<endl;
        cout<<"Sizeof Son     : "<<sizeof(Son)<<endl;

        printf("The offset of a1 : %p\n", &Son::a1 );
        printf("The offset of a2 : %p\n", &Son::a2 );
        printf("The offset of a3 : %p\n", &Son::a3 );
        return 0;
    }
输出结果:

Linux / g++

命令

g++ test.cpp

输出

Sizeof GrandPa : 8

Sizeof Father : 12

Sizeof Son : 12

The offset of a1 : 0x4

The offset of a2 : 0x8

The offset of a3 : 0x9



Window / Visual Studio

Sizeof GrandPa : 8

Sizeof Father : 12

Sizeof Son : 16

The offset of a1 : 0x4

The offset of a2 : 0x8

The offset of a3 : 0xC


分析 :
  1. 由于地址对齐,GranPa的大小是8byte 而不是5byte
  2. a1 的偏移为4
  3. GranPa 类的大小为8byte , 派生类Father的member只能从第8地址(第九位)开始排。
  4. a2 的偏移为8 , 这显然是为了保持GrandPa类的原样完整性实现的
  5. 对于VS Son的大小16byte和a3 的偏移12显然也是保持Father类原样完整性 。
问题:

为什么Linux / GCC 下的 Son的大小也是12 ? 为什么 a3 的偏移是9而不是12

将代码改为:

  • Son类中添加了一个int , 声明先于a3 .

    class GrandPa
    {
        public:
            int  a;
            char a1;
    };

    class Father : public GrandPa
    {
        public:
            //int b;
            char a2;
    };

    class Son : public Father
    {
        public:
            int i;
            char a3;
    };

    #include <iostream>
    #include <stdio.h>
    using std::cout;
    using std::endl;
    int main()
    {
        cout<<"Size of GrandPa : "<<sizeof(GrandPa)<<endl;
        cout<<"Size of Father  : "<<sizeof(Father)<<endl;
        cout<<"Size of Son     : "<<sizeof(Son)<<endl;

        printf("The offset of a : %p\n", &Son::a);
        printf("The offset of a1 : %p\n", &Son::a1);
        printf("The offset of a2 : %p\n", &Son::a2);
        printf("The offset of a3 : %p\n", &Son::a3);
        printf("The offset of i : %p\n", &Son::i);
        return 0;
    }

输出:

Size of GrandPa : 8

Size of Father : 12

Size of Son : 20

The offset of a : (nil)

The offset of a1 : 0x4

The offset of a2 : 0x8

The offset of a3 : 0x10

The offset of i : 0xc

可以看到 g++ 对内存做了适当的优化 , 将a3 先于 i 存放.

  • 在Father 类中添加int

    对上面的代码中的 //int b 打开 , 得到输出:

Size of GrandPa : 8

Size of Father : 16

Size of Son : 24

The offset of a : (nil)

The offset of a1 : 0x4

The offset of a2 : 0xc

The offset of a3 : 0x14

The offset of i : 0x10

这一次没有之前的优化了 . 可以推断 :

g++ 在保证父类的原样完整性的时候, 如果父类也是个派生类, 那么将对父类仅保证 分段 的原样完整性,

也就是说 , 在第一个代码例子中, Son类中Father类的原样完整性是通过保证GrandPa类的原样完整行和Father类中独有的数据char a2 的原样完整性来进行的 . 这样虽然Father类的大小是12 byte, 但是原样完整性仅仅需要9byte !!!

时间: 2024-10-15 06:53:58

C++ 零散知识点速记 -- <深入探索C++对象模型> 读书笔记的相关文章

【C++】深度探索C++对象模型读书笔记--关于对象(Object Lessons)

前言中的内容: 1.什么是C++对象模型? 1.语言中直接支持面向对象程序设计的部分 2. 对于各种支持的底层实现机制 2. C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法再执行器动态增加或取代其中一个.这使得虚拟调用操作得以快速地派送结果,付出的成本则是执行期的弹性. 3. 全局对象在main()函数之前便完成初始化. 第一章 关于对象 1. 在C++中,有两种class data members:static 和 nonstatic,以及三种

【C++】深度探索C++对象模型读书笔记--Data语意学(The Semantics of data)

1. 一个空类的大小是1 byte.这是为了让这一类的两个对象得以在内存中配置独一无二的地址. 2. Nonstatic data member 放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据. 3. C++对象模型把nonstatic data members直接放在每一个classs object之中.对于继承而来的nonstatic data members(不管是virtual 还是nonvirtua

深入探索C++对象模型 读书笔记

第1章 关于对象 1.C++在布局以及存取时间上的主要的额外负担是由virtual引起的,包括: a.virtual function机制,引入vptr以及vtbl,支持一个有效率的"执行期绑定" b.virtual base class,用以实现"多次出现在继承体系中的base class,有一个单一而被共享的实例" c.多重继承下,派生类跟第二个以及后续基类之间的转换 2."指针的类型"会教导编译器如何解释某个特定地址中的内存内容以及其大小(

【C++】深度探索C++对象模型读书笔记--构造函数语义学(The Semantics of constructors)(四)

成员们的初始化队伍(member Initia 有四种情况必须使用member initialization list: 1. 当初始化一个reference member时: 2. 当初始化一个const member时: 3. 当调用一个base class的constructor,而它拥有一组参数时: 4.当调用一个member class的constructor,而它拥有一组参数时: 在这四种情况下,程序可以被正确编译运行,但是效率不高.例如: 1 class Word { 2 Stri

深度探索c++对象模型读书笔记:Data语意学-继承与Data member中内存对齐问题

书中在继承之后内存对齐问题上说道下面代码: 1 #include <bits/stdc++.h> 2 using namespace std; 3 class A 4 { 5 private: 6 int val; 7 char bit1; 8 }; 9 class B : public A 10 { 11 private: 12 char bit2; 13 }; 14 class C : public B 15 { 16 private: 17 char bit3; 18 }; 19 int

【C++】深度探索C++对象模型读书笔记--执行期语意学(Runtime Semantics)

对象的构造和析构: 全局对象 C++程序中所有的global objects都被放置在程序的data segment中.如果显式指定给它一个值,此object将以此值为初值.否则object所配置到的内容为0. 如果全局对象如果有构造函数或析构函数的话,我们说它需要静态的初始化操作和内存释放操作.编译器的执行步骤如下: 1.为每一个需要静态初始化的文件产生一个_sti()函数,内含必要的构造函数调用操作. 2. 在每一个需要静态的内存释放操作的文件中,产生一个_std()函数,内含必要的析构操作

【C++】深度探索C++对象模型读书笔记--Function(The Semantics of Function)

1. Nonstatic member function(非静态成员函数)的调用方式 编译器会将”member 函数实例“转换为对等的”nonmember函数实例“. 对于非静态成员函数 float Point3d::magnitude3d() const{...} 转换步骤如下: 1. 改写函数的signature(意指:函数原型)以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以将此函数调用.该额外参数被称为this指针: //non

深度探索C++对象模型 读书总结

Stanley B. Lippman 著 侯捷 译 这本书不是讲具体的C++编程技巧的, 涉及的内容并不在C++的语言层面, 而是探索一个C++编译器如何来实现C++的对象模型. 作者Lippman是世界上一个C++编译器cfront的主要开发者之一. C++最重要的特性继承和多态使如何来实现的呢? 使用多态带来的效率损失又是多少? 多重继承和虚拟继承又是如何实现的, 与单一继承和非虚拟继承之间的效率对比又是如何估算?  作者通过讲解当初他设计cfront编译器时遇到的这些问题及解决办法带领读者

《探索需求》读书笔记part1

“本书是现代需求技术的基石,我们强烈推荐需求工程师阅读本书.”这是UML China写在<探索需求>这本书后的一句话,这句话强烈的赞赏了这本书的价值.这个月我也开始了对这本书的阅读,到现在我已经阅读了这本书的第一篇,对这本书也有了初步的了解. <探索需求>这本书主要介绍了降低需求含混性的各种方法,强调的是通过不断的细化需求来最终减少实现的含混性.含混性,描述的是不清晰的程度.而对于软件开发来说,事实上含混性存在两个方面,其一就是本书讲的实现方式上的含混性:另一个就是对目标的含混性,