C++ 继承语义下构造函数中的对象指针
2015-03-24 00:48
148 查看
展示一段程序,对其做一些讨论。
这段是测试在继承语义下对象构造时 this 指针的程序。首先在 main 函数中声明了一个X对象x和一个Y对象y,然后x和y分别通过指针成员ptr调用func函数。看X和Y的定义我们知道Y是继承自X的,是X的子类。
X中有自定义默认构造函数,一个数据成员ptr和一个虚拟成员函数func。 在Y中除了有一个"空的"构造函数外,还对继承自X的func重写(override)。那程序的输出是什么呢?
程序的输出取决于第9行中的 *this 和第10行中的 this 。
下面基于GCC(g++)编译器实现,对这段程序做较深入的讨论。先普及一下知识:
1. C++ 中类对象在构造之前,要为其在堆(malloc new 等操纵的内存)或栈(通常指函数栈帧)上分配内存空间,并将这块内存空间的地址作为第一个参数传递给相应的构造函数,构造函数负责对这块内存初始化。示例程序中构造函数里出现的 this 即构造函数的参数。
2. 在继承语义下,子类对象在构造时需要调用父类的构造函数来初始化子类对象中属于父类的部分(以下称之子对象),调用父类构造函数时以相应子对象的地址作为参数。
3. 一个类中如果出现虚函数,那么这个类对象的第一个机器字被用作虚指针(vptr), 它指向该类的虚函数列表中第一个虚函数入口地址所在的位置。在多继承或虚拟继承等复杂情形下,一个对象中可能包含多个虚指针。(有机会专门写一篇博客来讨论GCC中多继承和虚继承的一些实现细节。)
4. 一个类的虚函数列表被放在一个叫虚表(vtable)的数据结构(由编译器生成)中。虚表中除了保存虚函数的入口地址外,还保存指向类型信息数据结构的指针和一些用于处理多继承和虚继承的偏移值(如top_offset, vbase_offset)
5. 如果一个类中存在虚函数,编译器会在其构造函数在插入初始化 vptr 的代码,这部分代码通常在父类构造函数之后,用户代码之前。
现在开始分析 X::X(), 它所做的工作是初始化 vptr、打印类型信息和初始化 ptr。 X::X() 接受一个 X* 类型的指针,它是需要初始化的对象(或子对象)的地址。所以 *this 的类型一定是 X,第9行将打印X的类型信息。这一行中 typeid(*this).name 实际上调用 typeinfo::name(), 后者以指向X类型信息的指针(& typeinfo for X) 作为参数, 这个地址被保存在X的虚表中。
下面给出在64位机器上 X 对象的内存布局、X 类的虚表以及 X::X() 在汇编语言层面上的伪代码(为了使说明更清晰借用了C/C++语言操作符, 地址偏移以字节为单位, 赋值操作是8字节操作):
在构造y时,先将y的地址作为参数传给Y::Y(), 由 Y::Y() 来对其初始化。 Y::Y() 将y 中 X 子对象的初始化委托给了 X::X(), 最后再在Y::Y() 中对 vptr 赋值。 我这里说"赋值"
是因为Y对象的地址与其X子对象的地址相同(这是最简单的情形,子对象地址的偏移值为 0),在X::X()中已经将vptr这个机器字初始化为X的第一个虚函数的位置,在 Y::Y() 中需要覆盖这个不合理的vptr值,将其设置为指向存在于Y的虚表中的第一个虚函数的位置。通过Y对象访问的ptr指针在调用Y::Y()后被初始化为Y对象的地址。
下面给出 Y 对象的内存布局、Y 类的虚表 和 Y::Y() 的伪代码:
现在很容易知道 x.ptr->func() 和 y.ptr->func() 的输出是什么,这是每个C++程序员都熟悉的动态绑定。 x.ptr 和 y.ptr 均为类型为X*的指针,x.ptr 指向 x,y.ptr 指向
y, 但他们调用 func() 时, 通过一致的表达式(在汇编语言中,是一致的命令序列)就可以实现多态, 这得益于基于虚指针和虚表的编译器实现。
体现汇编语言层面语义的伪代码:
#include <iostream> #include <typeinfo> using namespace std; class X { public: X* ptr; X() { cout << typeid(*this).name() << endl; ptr = this; } virtual void func() { cout << "X func" << endl; } }; class Y : public X { public: Y() {} virtual void func() { cout << "Y func" << endl; } }; int main() { X x; Y y; x.ptr->func(); y.ptr->func(); return 0; }
这段是测试在继承语义下对象构造时 this 指针的程序。首先在 main 函数中声明了一个X对象x和一个Y对象y,然后x和y分别通过指针成员ptr调用func函数。看X和Y的定义我们知道Y是继承自X的,是X的子类。
X中有自定义默认构造函数,一个数据成员ptr和一个虚拟成员函数func。 在Y中除了有一个"空的"构造函数外,还对继承自X的func重写(override)。那程序的输出是什么呢?
程序的输出取决于第9行中的 *this 和第10行中的 this 。
下面基于GCC(g++)编译器实现,对这段程序做较深入的讨论。先普及一下知识:
1. C++ 中类对象在构造之前,要为其在堆(malloc new 等操纵的内存)或栈(通常指函数栈帧)上分配内存空间,并将这块内存空间的地址作为第一个参数传递给相应的构造函数,构造函数负责对这块内存初始化。示例程序中构造函数里出现的 this 即构造函数的参数。
2. 在继承语义下,子类对象在构造时需要调用父类的构造函数来初始化子类对象中属于父类的部分(以下称之子对象),调用父类构造函数时以相应子对象的地址作为参数。
3. 一个类中如果出现虚函数,那么这个类对象的第一个机器字被用作虚指针(vptr), 它指向该类的虚函数列表中第一个虚函数入口地址所在的位置。在多继承或虚拟继承等复杂情形下,一个对象中可能包含多个虚指针。(有机会专门写一篇博客来讨论GCC中多继承和虚继承的一些实现细节。)
4. 一个类的虚函数列表被放在一个叫虚表(vtable)的数据结构(由编译器生成)中。虚表中除了保存虚函数的入口地址外,还保存指向类型信息数据结构的指针和一些用于处理多继承和虚继承的偏移值(如top_offset, vbase_offset)
5. 如果一个类中存在虚函数,编译器会在其构造函数在插入初始化 vptr 的代码,这部分代码通常在父类构造函数之后,用户代码之前。
现在开始分析 X::X(), 它所做的工作是初始化 vptr、打印类型信息和初始化 ptr。 X::X() 接受一个 X* 类型的指针,它是需要初始化的对象(或子对象)的地址。所以 *this 的类型一定是 X,第9行将打印X的类型信息。这一行中 typeid(*this).name 实际上调用 typeinfo::name(), 后者以指向X类型信息的指针(& typeinfo for X) 作为参数, 这个地址被保存在X的虚表中。
下面给出在64位机器上 X 对象的内存布局、X 类的虚表以及 X::X() 在汇编语言层面上的伪代码(为了使说明更清晰借用了C/C++语言操作符, 地址偏移以字节为单位, 赋值操作是8字节操作):
X object layout Vtable for X 0 vptr ----------+ vtable for X: 3u entries 8 ptr | 0 (int (*)(...))0 // top_offset | 8 (int (*)(...))(& typeinfo for X) // ptr to typeinfo +-------> 16 (int (*)(...))X::func // slot for the first vfunc /* pseudo code * & : address-of, -> : member access through pointer */ X::X(this): // this is a pointer of type X* this->vptr = (& vtable for X) + 16 // (& this->vptr) : this + 0 print typeinfo::name(& typeinfo for X) // (& this->ptr) : this + 8 this->ptr = this
在构造y时,先将y的地址作为参数传给Y::Y(), 由 Y::Y() 来对其初始化。 Y::Y() 将y 中 X 子对象的初始化委托给了 X::X(), 最后再在Y::Y() 中对 vptr 赋值。 我这里说"赋值"
是因为Y对象的地址与其X子对象的地址相同(这是最简单的情形,子对象地址的偏移值为 0),在X::X()中已经将vptr这个机器字初始化为X的第一个虚函数的位置,在 Y::Y() 中需要覆盖这个不合理的vptr值,将其设置为指向存在于Y的虚表中的第一个虚函数的位置。通过Y对象访问的ptr指针在调用Y::Y()后被初始化为Y对象的地址。
下面给出 Y 对象的内存布局、Y 类的虚表 和 Y::Y() 的伪代码:
Y object layout Vtable for Y 0 vptr ----------+ vtable for Y: 3u entries 8 ptr | 0 (int (*)(...))0 | 8 (int (*)(...))(& typeinfo for Y) +-------> 16 (int (*)(...))Y::func Y::Y(this): // this is a pointer of type Y* X::X(this + 0) this->vptr = (& vtable for Y) + 16 // (& this->vptr) : this + 0
现在很容易知道 x.ptr->func() 和 y.ptr->func() 的输出是什么,这是每个C++程序员都熟悉的动态绑定。 x.ptr 和 y.ptr 均为类型为X*的指针,x.ptr 指向 x,y.ptr 指向
y, 但他们调用 func() 时, 通过一致的表达式(在汇编语言中,是一致的命令序列)就可以实现多态, 这得益于基于虚指针和虚表的编译器实现。
体现汇编语言层面语义的伪代码:
// pseudo code // *(ptr) : extract 8 bytes from the address pointed by ptr px = /* x.ptr or y.ptr */ vptr = *(px) // get the vptr pfunc = *(vptr) // get the function pointer pfunc(px) // call func()
相关文章推荐
- [C++]派生类构造函数举例(多继承、含有内嵌对象)
- C++继承与对象指针
- .第04章 CORE C++_指针(II)_动态内存_引用_类_对象_构造函数_析构函数
- 【C++继承与派生之二】有子对象的派生类的构造函数
- C++学习笔记(六)-- 类和对象 构造函数和析构函数 const成员函数 this指针 对象数组 堆栈管理变量
- 理解C++存在继承和组合的对象构造函数调用顺序
- Boost.Bind用法详解(一) 2008-05-09 15:50:50| 分类: C++ |字号 订阅 Boost.Bind 为函数和函数对象提供了一致的语法,对于值语义和指针语义也一样。
- 【c++继承】继承关系中派生类对象构造函数和析构函数调用顺序
- 【C++继承与派生之二】有子对象的派生类的构造函数
- 计算机程序设计(C++)第10周编程作业数据的抽象和封装——类(2)——构造函数、析构函数和指向对象的指针
- c++(一) 类 对象 重载 继承 多态 构造函数 虚函数 覆盖 纯虚函数等
- 关注C++细节——含有本类对象指针的类的构造函数、析构函数、拷贝构造函数、赋值运算符的例子
- [C++]变量存储类别,指针和引用,类与对象,继承与派生的一些摘要
- C++虚拟继承中_对象内存的分布_虚继承会多余分配虚表v-tab的指针vptr_图1-1清楚的描述了虚继承类对象内存的分布_转载淘宝共享数据平台
- C++中的临时对象,对临时对象的引用,和临时对象的指针
- C++指针探讨 (四) 函数对象
- 21天学通c++之第二周 指针 8.6 访问自由存储区中对象的成员数据
- [C++对象模型][3]指针与数组
- 21天学通c++之第二周 指针 8.10 使用指向const对象的指针
- C++对象的内存布局---单继承