深入探索C++对象模型-关于对象 阅读笔记

深入探索C++对象模型-关于对象 阅读笔记


什么是C++对象模型

包含两方面

  1. C++语言层面面向对象程序设计。比如相对C语言而言,支持了封装、继承、多态等性质。这些都是C++标准规定的,但是这些特性是如何实现的呢?即第二方面;
  2. 编译器底层实现模型。比如类对象的内存布局,虚函数的底层实现等。原书作者lippman参与设计了第一套C++编译器cfront。

C++对象模型

单继承

请添加图片描述

通过书中内容以及我自己的理解,绘制了这份C++对象模型图,只考虑单继承情况,且虚基类中没有虚函数。由于底层实现并不唯一,这只是其中一种实现方式。

重点:

  1. 在单继承情况下,子类会共享父类的vptr,指向子类的虚函数表。子类的虚函数表与父类共享,所谓共享是指用父类指针引用子类对象,或者通过子类指针引用子类对象时,调用虚函数时都会经由这个虚函数表进行转发(dispatch)。两者区别是用父类指针引用时,只能调用父类中声明的函数(即静态类型是Parent, 动态类型是Child),从虚函数表角度看,父类指针只能使用其中“父类的那部分”,比如上图中func1 func2是父类中声明的虚函数,myfunc1是子类中声明的虚函数,因此如果用Parent指针无法调用myfunc1。

  2. 虚基类在内存中的位置会随着继承体系的改变而改变,因此同虚函数一样,在多态场景下,虚基类的偏移量需要在运行时才能决定。比如用Parent指针访问虚基类的中的成员,Parent指针可以指向Parent对象,也可以指向Child对象,不同的对象偏移量不同。图中的实现方式是复用虚函数表,正数slot表示virtual functions的地址,负数slot表示虚基类的偏移量。

  3. RTTI(RunTime Type Identification)放在虚函数表的第0个slot,因此如果该类没有虚函数表时,是无法表现出运行时对象的概念的,typeid的结果由编译器静态决定。很好理解,如果一个类中没有虚函数,也就没有多态行为,即父类指针无论指向子类对象还是父类对象,其行为是固定的。同时,由于dynamic_cast依赖RTTI,在没有多态行为时,dynamic_cast会出现编译错误(cannot dynamic_cast ‘p’ (of type ‘struct Parent*’) to type ‘struct Child*’ (source type is not polymorphic))

多继承

请添加图片描述

实际测试

在这里插入图片描述

(可以通过g++ -std=c++11 -fdump-class-hierarchy test.cpp查看,更多细节参考 https://www.cnblogs.com/hamwj1991/p/12907683.html )

继承关系为:

struct Base {
    int base_data;
    virtual void BaseFunc() {}
};

struct Parent1 : virtual Base {
    void BaseFunc() override {}
    virtual void Parent1Func() {}
};
struct Parent2 : virtual Base {
    void BaseFunc() override {}
    virtual void Parent2Func() {}
};
struct Derived : Parent1, Parent2 {
    virtual void DerivedFunc() {}
    void BaseFunc() override {}
    void Parent1Func() override {}
    void Parent2Func() override {}
};

int main() {
    Derived d;
    return 0;
}

重点

  1. 在多继承场景下,第一个父类的行为跟单继承一致(如Parent1)。
  2. vptr指向的虚函数表的内容由vptr所属的静态类型决定。比如vptr2指向的虚函数表包含了继承自Parent2的虚函数以及继承自Base的虚函数(即Parent2指针所能调用的函数), 因为vptr1由Child和Parent1共享,因此需要从Child的角度和Parent1角度分别去看。虚函数表在编译期准备完成。
  3. 在多继承继承下,虚函数调用可能会发生指针偏移 。比如Parent2* ptr = new Child(); ptr->Parent2Func(); ptr一开始指向Child对象中的Parent2子对象(该过程发生了指针偏移,在编译期完成),由于Parent2Func被子类重写,为了能够正确执行,ptr指针需要偏移,重新指向Child对象的起始处。由于ptr指向的实际类型在编译期并不可知,因此具体的偏移量只能在运行期才能确定(通过thunk技术)。

指针偏移

我理解为什么需要偏移:类成员名中保存了该成员变量基于对象首地址的偏移量(可从成员指针中解释),且固定不变。因此在访问成员变量前,需要确保对象首地址是正确的。

#include<stdio.h>
#include <iostream>
using namespace std;

struct Parent1
{
    int val1;
    virtual ~Parent1() {
        cout << "~Parent1" << endl;
        printf("this: %p\n", this);
    }
};
struct Parent2
{
    int val2;
    virtual ~Parent2() {
        cout << "~Parent2" << endl;
        printf("this: %p\n", this);
    }
};
struct Child: Parent1, Parent2
{
    int val3;
    ~Child() {
        cout << "~Child" << endl;
        printf("this: %p\n", this);
    }
};

int main()
{
    Child *c = new Child;
    Parent2 *p = c;
    printf("Parent2::val2: %d\n", &Parent2::val2);
    p->val2 = 1;
    printf("c: %p, p: %p\n", c, p);
    delete p;
    return 0;-
}

/* 输出:
Parent2::val2: 8 (偏移量为8,这里gcc将vptr放在对象首部)
c: 0xed6010, p: 0xed6020 (地址相差16,即Parent1的大小,考虑了内存对齐)
~Child
this: 0xed6010
~Parent2
this: 0xed6020
~Parent1
this: 0xed6010
*/

C++多态

C++支持三种程序设计范式:

  1. 程序模型,即与C语言一样的,面向过程。数据和函数直接没有关联性,函数处理的是共同的外部数据。
  2. 抽象数据类型,Abstract Data Type (ADT),只使用了C++封装、继承特性,没有多态。函数调用在编译期就能决定。
  3. 面向对象模型,Object-oriented (OO),与ADT的主要区别是,使用了C++多态特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值