C++语言学习(十三)——C++对象模型分析

C++语言学习(十三)——C++对象模型分析

一、C++对象模型分析

1、类对象模型的内存布局

class是一种特殊的struct,class与struct遵循相同的内存对齐原则,class中的成员函数与成员变量是分开存放的,每个对象拥有独立的成员变量,所有的对象共享类中的成员函数。
运行时,类对象退化为结构体的形式:
A、所有成员变量在内存中依次排布
B、由于内存对齐的存在,成员变量间可能存在内存间隙
C、可以通过内存地址访问成员变量
D、访问权限关键字在运行时失效

#include <iostream>

using namespace std;

class A
{
    int i;
    int j;
    char c;
    double d;
public:
    void print()
    {
        cout << "i = " << i << ", "
             << "j = " << j << ", "
             << "c = " << c << ", "
             << "d = " << d << endl;
    }
};

struct B
{
    int i;
    int j;
    char c;
    double d;
};

int main(int argc, char *argv[])
{
    A a;
    //64 bit machine
    cout << "sizeof(A) = " << sizeof(A) << endl;    // 24
    cout << "sizeof(a) = " << sizeof(a) << endl;    // 24
    cout << "sizeof(B) = " << sizeof(B) << endl;    // 24

    a.print();

    B* p = reinterpret_cast<B*>(&a);

    p->i = 1;
    p->j = 2;
    p->c = ‘c‘;
    p->d = 3.14;
    a.print();

    return 0;
}

上述代码中,class A对象与struct B对象在内存中的排布相同。

2、派生类类对象模型

子类是由父类成员叠加子类成员得到的。

#include <iostream>

using namespace std;

class Parent
{
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d)
    {
        m_i = i;
        m_j = j;
        m_d = d;
    }
    void print()
    {
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
private:
    double m_d;
};

struct Test
{
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//8
    cout << sizeof(Child) << endl;//16
    Child child(1,2,3.14);
    child.print();
    Test* test = reinterpret_cast<Test*>(&child);
    cout << "i = " << test->i << endl;
    cout << "j = " << test->j << endl;
    cout << "d = " << test->d << endl;

    test->i = 100;
    test->j = 200;
    test->d = 3.1415;
    child.print();
    return 0;
}

二、C++多态的实现机制

1、C++多态的实现简介

当类中声明虚函数时,C++编译器会在类中生成一个虚函数表。虚函数表是一个用于存储virtual成员函数地址的数据结构。虚函数表由编译器自动生成与维护,virtual成员函数会被编译器放入虚函数表中。存在虚函数时,每个对象中都有一个指向类的虚函数表的指针。
由于对象调用虚函数时会查询虚函数表,因此虚函数的调用效率比普通成员函数低。
当创建类对象时,如果类中存在虚函数,编译器会在类对象中增加一个指向虚函数表的指针。父类对象中虚函数表存储的是父类的虚函数,子类对象中虚函数表存储的是子类对象的虚函数。虚函数表指针存储在类对象存储空间的开始的前4(8)个字节。

2、虚函数表

如果一个类包含虚函数,其类包含一个虚函数表。
如果一个基类包含虚函数,基类会包含一个虚函数表,其派生类也会包含一个自己的虚函数表。
虚函数表是一个函数指针数组,其数组元素是虚函数的函数指针,每个元素对应一个虚函数的函数指针。非虚成员函数的调用并不需要经过虚函数表,所以虚函数表的元素并不包括非虚成员函数的函数指针。?
虚函数表中虚函数指针的赋值发生在编译器的编译阶段,即在代码编译阶段虚函数表就生成。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "Child::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "Child::display()" << endl;
    }
private:
    double m_d;
};

struct Test
{
    void* vptr;
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//12
    cout << sizeof(Child) << endl;//24
    Child child(1,2,3.14);
    Test* test = reinterpret_cast<Test*>(&child);
    cout << "virtual Function Table Pointer:" << endl;
    cout << "vptr = " << test->vptr << endl;
    //虚函数表指针位于类对象的前4字节
    cout << "child Object address: " << &child << endl;
    cout << "Member Variables Address: " << endl;
    cout << "&vptr = " << &test->vptr << endl;
    cout << "&i = " << &test->i << endl;
    cout << "&j = " << &test->j << endl;
    cout << "&d = " << &test->d << endl;

    //函数指针方式访问类的虚函数
    cout << "Virtual Function Table: " << endl;
    cout << "Virtual print Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 0)) <<endl;
    cout << "Virtual sum Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 1)) <<endl;
    cout << "Virtual display Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 2)) <<endl;
    typedef void (*pPrint)();
    pPrint print = (pPrint)(*((long *)(*((long *)&child)) + 0));
    print();

    typedef double (*pSum)(void);
    pSum sum = (pSum)(*((long *)(*((long *)&child)) + 1));
    sum();

    typedef void (*pDisplay)(void);
    pDisplay display = (pDisplay)(*((long *)(*((long *)&child)) + 2));
    display();
    return 0;
}

上述代码中,通过类对象的虚函数表指针可以访问类的虚函数表,虚函数表顺序存储了类的虚函数的函数地址,通过函数指针的方式可以调用类的虚函数,包括声明为private的虚函数。但由于使用函数指针方式访问类的虚函数时,类的虚函数在执行过程中其this指针指向的对象是不确定的,因此访问到的类对象的成员变量的值是垃圾值。

3、虚函数表指针

虚函数表属于类,而不是属于某个具体的类对象,一个类只需要一个虚函数表。同一个类的所有对象都使用类的唯一虚函数表。?为了指定类对象的虚函数表,类对象内部包含一个指向虚函数表的指针,指向类的虚函数表。为了让每个类对象都拥有一个虚函数表指针,编译器在类中添加了一个指针*__vptr,用来指向虚函数表。当类对象在创建时便拥有__vptr指针,且__vptr指针的值会自动被设置为指向类的虚函数表。

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    void func()
    {

    }
protected:
    int m_i;
    int m_j;
};

上述代码中,类的虚函数表如下:
类Parent对象的内存布局中,虚函数表指针位于类对象存储空间的开头,其值0X409004是类Parent的虚函数表的首地址,虚函数表中的第一个数组元素是虚函数Parent::print的地址,第二个数组元素是虚函数Parent::sum,第三个数组元素是虚函数Parent::display,非虚函数不在虚函数表中。

4、类对象的内存布局

对于含有虚函数的类,虚函数表指针位于类对象内存布局的开始位置,然后依次排列类继承自父类的成员变量,最后依次排列类自身的非静态成员变量。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    void func()
    {

    }
protected:
    int m_i;
    int m_j;
    static int m_count;
};
int Parent::m_count  = 0;

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

class ChildB : public Parent
{
public:
    ChildB(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildB::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildB::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildB::display()" << endl;
    }
private:
    double m_d;
};

struct ParentTest
{
    void* vptr;
    int i;
    int j;
};

struct ChildTest
{
    void* vptr;
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//12
    cout << sizeof(ChildA) << endl;//24
    cout << endl;
    cout << "Parent..." <<endl;
    Parent parent(1,2);
    ParentTest* parenttest = reinterpret_cast<ParentTest*>(&parent);
    cout << "Member Variable Value:"<< endl;
    //虚函数表的首地址
    cout << parenttest->vptr << endl;//编译时确定
    cout << parenttest->i << endl;//1
    cout << parenttest->j << endl;//2
    cout << "Member Variable Address:" << endl;
    cout << &parenttest->vptr << endl;
    cout << &parenttest->i << endl;
    cout << &parenttest->j << endl;
    cout << endl;
    cout << "Child..." << endl;
    ChildA child(1,2,3.14);
    ChildTest* childtest = reinterpret_cast<ChildTest*>(&child);
    cout << "Member Variable Value:"<< endl;
    //虚函数表的首地址
    cout << childtest->vptr << endl;//编译时确定
    cout << childtest->i << endl;//1
    cout << childtest->j << endl;//2
    cout << childtest->d << endl;//3.14
    cout << "Member Variable Address:" << endl;
    cout << &childtest->vptr << endl;
    cout << &childtest->i << endl;
    cout << &childtest->j << endl;
    cout << &childtest->d << endl;

    return 0;
}

5、动态绑定的实现

Parent、ChildA、ChildB三个类都有虚函数,C++编译器编译时会为每个类都创建一个虚函数表,即类Parent的虚函数表(Parent vtbl),类ChildA的虚函数表(ChildA vtbl),类ChildB的虚表(ChildB vtbl)。类Parent、ChildA、ChildB的对象都拥有一个虚函数表指针*vptr,用来指向自己所属类的虚函数表。?
类Parent包括三个虚函数,Parent类的虚函数表包含三个指针,分别指向Parent::print()、Parent::sum()、Parent::display()三个虚函数函数。?
类ChildA继承于类Parent,因此类ChildA可以调用父类Parent的函数,但类ChildA重写Parent::print()、Parent::sum()、Parent::display()三个虚函数,因此类ChildA 虚函数表的三个函数指针分别指向ChildA::print()、ChildA::sum()、ChildA::display()。?
类ChildB继承于类Parent,因此类ChildB可以调用类Parent的函数,但由于类ChildB重写Parent::print()、Parent::sum()函数,类ChildB虚函数表有三个函数指针,第一个函数指针指向Parent::display()虚函数,第二个第三个依次指向ChildB::print()、ChildB::sum()虚函数。?

ChildA childA;
Parent* p = &childA;

当定义一个ChildA类的对象childA时,childA对象包含一个虚函数表指针,指向ChildA类的虚函数表。
当定义一个Parent类的指针p指向childA对象时,p指针只能指向ChildA对象的父类Parent部分,但由于虚函数表指针位于对象存储空间的开始,因此p指针可以访问childA对象的虚函数表指针。由于childA对象的虚函数表指针指向ChildA类的虚函数表,因此p指针可以访问类ChildA的虚函数表。
当使用指针调用print函数,程序在执行p->print()时,会发现p是个指针,且调用的函数是虚函数。?
首先,根据虚函数表指针p->vptr来访问对象childA对应的虚函数表。
然后,在虚函数表中查找所调用的虚函数对应的条目。由于虚函数表在编译阶段就生成,所以可以根据所调用的函数定位到虚函数表中的对应条目。对于 p->print()的调用,类ChildA虚函数表的第一项即是print函数指针对应的条目。?
最后,根据虚函数表中找到的函数指针,调用函数ChildA::print()。

Parent base;
Parent* p = &base;
p->print();

当base对象在创建时,base对象的虚函数表指针vptr已设置为指向Parent类的虚函数表,p->vptr指向Parent虚函数表。print在Parent虚函数表中相应的条目指向Parent::print()函数,所以 p->print()会调用Parent::print()函数。
虚函数的调用的三个步骤用表达式(*(p->vptr)[n])(p)可以概括。

三、虚函数经典问题

1、构造函数不能为虚函数

由于在构造函数执行完后,类对象的虚函数表指针才被正确初始化。因此构造函数不能为虚函数。类对象中的虚函数表指针是在调用构造函数的时候完成初始化的。因此,在构造函数调用前,虚函数表指针还没有完成初始化,无法调用虚的构造函数。
在构造函数进入函数体前,进行虚函数表指针的初始化,将虚函数表指针初始化为当前类的虚函数表地址,即在基类调用构造函数的时候,会把基类的虚函数表地址赋值给虚函数表指针,而如果进执行到子类的构造函数时,把子类的虚函数表地址赋值给虚函数表指针。因此,在派生类对象的构造时,虚函数表指针指向的虚函数表地址是动态变化的。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
        //虚函数表指针
        int* vptr = (int*)*((int*)this);
        cout << "vptr: " << vptr << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
        cout << "Child(int i, int j, double d): " << this << endl;
        //虚函数表指针
        int* vptr = (int*)*((int*)this);
        cout << "vptr: " << vptr << endl;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    ~Child()
    {
        cout << "~Child(): " << this <<endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent* p = new Child(1,2,3.14);
    p->print();
    delete p;
    return 0;
}

2、析构函数中可以为虚函数

析构函数可以为虚函数,可以发生多态。工程实践中,如果基类中有虚成员函数,建议将析构函数声明为虚函数,确保对象销毁时触发正确的析构函数调用,保证资源的正确回收。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j)" << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent()" << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
        cout << "Child(int i, int j, double d)" << endl;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    ~Child()
    {
        cout << "~Child()" <<endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent* p = new Child(1,2,3.14);
    p->print();
    delete p;
    return 0;
}

3、构造函数内不能发生多态行为

在调用基类的构造函数时,其虚函数表指针指向的是基类的虚函数表,而在调用派生类的构造函数时,其虚函数表指针指向的是派生类的虚函数表。因此,构造函数内不能发生多态行为。

4、析构函数内不能发生多态行为

在调用派生类的析构函数时,其虚函数表指针指向的是派生类的虚函数表;在调用基类的析构函数时,其虚函数表指针指向的是基类的虚函数表,并且派生类的虚函数表已经被销毁。

原文地址:http://blog.51cto.com/9291927/2148687

时间: 2024-11-10 04:28:19

C++语言学习(十三)——C++对象模型分析的相关文章

Go语言学习(十三)面向对象编程-继承

1.匿名组合 Go语言也提供了继承,但是采用了组合的方式,所以我们将其称为匿名组合: package main import "fmt" //定义基类 type Base struct { Name string } //基类相关的2个成员方法 func (base *Base) A() { fmt.Println("Base method A called...") } func (base *Base) B() { fmt.Println("Base

Dart语言学习(十三) Dart Mixins 实现多继承

Mixins Mixins(混入功能)相当于多继承,也就是说可以继承多个类,使用with关键字来实现Mixins的功能. 那么多个类中有相同的方法时候,会被覆盖吗?覆盖的先后是什么? class A{ void a(){ print("A.a()..."); } } class B{ void a(){ print("B.a()..."); } void b(){ print("B.b()..."); } } 现在新建一个类D,使得D继承A和B

Go语言学习笔记十三: Map集合

Go语言学习笔记十三: Map集合 Map在每种语言中基本都有,Java中是属于集合类Map,其包括HashMap, TreeMap等.而Python语言直接就属于一种类型,写法上比Java还简单. Go语言中Map的写法比Java简单些,比Python繁琐. 定义Map var x map[string]string x : = make(map[string]string) 写法上有些奇怪,map为关键字,右侧中括号内部为key的类型,中括号外部为value的类型.一般情况下使用逗号或者冒号

C++语言学习(十四)——C++类成员函数调用分析

C++语言学习(十四)--C++类成员函数调用分析 一.C++成员函数 1.C++成员函数的编译 C++中的函数在编译时会根据命名空间.类.参数签名等信息进行重新命名,形成新的函数名.函数重命名的过程通过一个特殊的Name Mangling(名字编码)算法来实现.Name Mangling算法是一种可逆的算法,既可以通过现有函数名计算出新函数名,也可以通过新函数名逆向推导出原有函数名.Name Mangling算法可以确保新函数名的唯一性,只要命名空间.所属的类.参数签名等有一个不同,那么产生的

C++语言学习(九)——多态

C++语言学习(九)--多态 C++中所谓的多态(polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应.    多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性.可以减轻系统升级,维护,调试的工作量和复杂度. 多态是一种不同层次分类下的重要联系,是一种跨层操作. 一.多态实现的前提 赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代.赋值兼容是一种默认行为,不需要任何的显式的转化步骤,只能发生在public继承方式中,是多态

关于c语言学习 谭浩强的书

2007-11-16 13:22:58|  分类: PROGRAMME |  标签: |举报 |字号大中小 订阅 广大有志于从事IT行业的同志们,在你们进入这一行之前千万请看这篇文章!太经典了!对你绝对有启发! 千万别买谭浩强和等级考试的书!!!!!! 整理别人的言论,请大家踊跃讨论!!!!!!!!!!!! 1:书皮上面有那么多的牛人题词,估计也许是自己的水平太低. 2:ANSI只给了两种方式:int main(void) {/*...*/}和 int main(int argc, char *

【转】朱兆祺教你如何攻破C语言学习、笔试与机试的难点(连载)

原文网址:http://bbs.elecfans.com/jishu_354666_1_1.html 再过1个月又是一年应届毕业生应聘的高峰期了,为了方便应届毕业生应聘,笔者将大学四年C语言知识及去年本人C语言笔试难点进行梳理,希望能对今年应届毕业生的应聘有所帮助. 2013年10月18日更新-->    攻破C语言这个帖子更新到这里,我不仅仅是为了补充大学学生遗漏的知识,我更重要的是希望通过我的经验,你们实际项目中的C语言写得漂亮,写出属于你的风格.“朱兆祺STM32手记”(http://bb

R语言学习笔记

參考:W.N. Venables, D.M. Smith and the R DCT: Introduction to R -- Notes on R: A Programming Environment for Data Analysis and Graphics,2003. http://bayes.math.montana.edu/Rweb/Rnotes/R.html 前言:关于R 在R的官方教程里是这么给R下注解的:一个数据分析和图形显示的程序设计环境(A system for data

EasyUI学习总结(二)——easyloader分析与使用

EasyUI学习总结(二)--easyloader分析与使用 使用脚本库总要加载一大堆的样式表和脚本文件,在easyui 中,除了可以使用通常的方式加载之外,还提供了使用 easyloader 加载的方式.这个组件主要是为了按需加载组件而诞生.什么情况下使用它呢? 你觉得一次性导入 easyui 的核心 min js 和 css 太大 你只用到 easyui 的其中几个组件 你想使用其中的一个组件,但是你又不知道这个组件依赖了那些组件. 如果你有以上三中情况,那么推荐你使用easyLoader.