注:博客中内容主要来自《狄泰软件学院》,博客仅当私人笔记使用。
测试环境:Ubuntu 10.10
GCC版本:9.2.0
一、继承对象模型
1)在C++编译器的内部类可以理解为结构体
2)子类是由父类成员叠加子类新成员得到的
编程实验
继承对象模型初探
51-1.cpp
#include <iostream>
#include <string>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
public:
void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
};
class Derived : public Demo
{
int mk;
public:
Derived(int i, int j, int k)
{
mi = i;
mj = j;
mk = k;
}
void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << ", "
<< "mk = " << mk << endl;
}
};
struct Test
{
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl; //8
cout << "sizeof(Derived) = " << sizeof(Derived) << endl; //12 = 8 + 4 ,叠加得到
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d); //强制转换成结构体Test类型
cout << "Before changing ..." << endl;
d.print();
p->mi = 10; //这里可以修改数据成功,因为结构体Test和对象d内存布局相同!
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print();
return 0;
}
操作:
1) g++ 51-1.cpp -o 51-1.out编译正确,打印结果:
sizeof(Demo) = 8
sizeof(Derived) = 12
Before Changing ...
mi = 1, mj = 2, mk = 3
After changing ...
mi = 10, mj = 20, mk = 30
分析:
1. 子类继承父类,数据在内存中如何分布的:父类数据在前,子类数据在后。
2. 类可以转换成结构体,因为类中的数据存储方式和结构体中数据存储方式相同。
二、多态对象模型
1)C++多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储成员函数地址的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 存在虚函数时,每个对象中都有一个指向虚函数表的指针(4字节)
多态是面向对象中的一个概念,和具体程序设计语言没有关系。多态指的是相同的行为方式,不同的行为结果。通过同一种行为方式,表现结果是不同的。(多态的概念)
多态的这种方式,是通过虚函数来进行实现的。(多态实现的方法)
两个类生成两个虚函数表(VTABLE) ,一个给子类一个给父类。
红色箭头代表寻址操作。由于效率问题,对于使用虚函数要慎重。
证明虚函数表存在
#include <iostream>
#include <string>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
public:
virtual void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
};
class Derived : public Demo
{
int mk;
public:
Derived(int i, int j, int k)
{
mi = i;
mj = j;
mk = k;
}
void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << ", "
<< "mk = " << mk << endl;
}
};
struct Test
{
void* p; //虚函数列表指针放在最初位置。
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d);
cout << "Before changing ..." << endl;
d.print();
p->mi = 10;
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print();
return 0;
}
操作:
1) g++ test.cpp -o test.out编译正确,打印结果:
sizeof(Demo) = 12
sizeof(Derived) = 16
Before changing ...
mi = 1, mj = 2, mk = 3
After changing ...
mi = 10, mj = 20, mk = 30
分析:
因为有虚函数,所以每个类大小+4(因为存储指向虚函数表的指针)。struct Test的指向虚函数表的指针放在了最前边,这证明了class在多态时内存布局。
将子类地址给结构体p,通过修改结构体p中的数据,竟然改变了子类中的数据,证明子类内存排布方式和结构体内存排布方式相同。
编程实验
多态本质分析
51-2.h
#ifndef _51_2_H_
#define _51_2_H_
typedef void Demo;
typedef void Derived; //新添加(derived:派生)
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);
Derived* Derived_Create(int i, int j, int k); //创建对象
int Derived_GetK(Derived* pThis); //新添加
int Derived_Add(Derived* pThis, int value); //虚函数
#endif
51-2.c
#include "51-2.h"
#include "malloc.h"
//模拟多态:声明两个“同名”函数
static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);
struct VTable // 2. 定义虚函数表数据结构
{
int (*pAdd)(void*, int); // 3. 虚函数表里面存储什么???(函数指针)
};
struct ClassDemo
{
struct VTable* vptr; // 1. 定义虚函数表指针 ==》 虚函数表指针类型???
int mi;
int mj;
};
struct ClassDerived //定义结构对象
{
struct ClassDemo d; //继承
int mk;
};
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向父类累加函数
static struct VTable g_Demo_vtbl = //父类虚函数表
{
Demo_Virtual_Add
};
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向子类累加函数
static struct VTable g_Derived_vtbl = //子类虚函数表
{
Derived_Virtual_Add
};
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->vptr = &g_Demo_vtbl; // 4. 关联对象和虚函数表(父类虚表)
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Demo_GetI(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi;
}
int Demo_GetJ(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mj;
}
// 6. 定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Demo_Virtual_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi + obj->mj + value;
}
// 5. 分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->vptr->pAdd(pThis, value);
}
void Demo_Free(Demo* pThis)
{
free(pThis);
}
Derived* Derived_Create(int i, int j, int k) //创建对象
{
struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
if( ret != NULL )
{
ret->d.vptr = &g_Derived_vtbl;
ret->d.mi = i;
ret->d.mj = j;
ret->mk = k;
}
return ret;
}
int Derived_GetK(Derived* pThis)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk;
}
//定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Derived_Virtual_Add(Demo* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk + value;
}
//分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Derived_Add(Derived* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->d.vptr->pAdd(pThis, value);
}
main.c
#include "stdio.h"
#include "51-2.h"
void run(Demo* p, int v)
{
int r = Demo_Add(p, v); //这里调用父类累加函数,模拟子类退化成父类(Add函数内部会进行强制转换,对父类没有影响,但是会使子类丢失自己专属的数据(地址截断),对遗留下的数据没有任何破坏。不过虚函数表总是放在内存分布的最前边,不会影响找到想调用的子类函数。)
printf("r = %d\n", r);
}
int main()
{
Demo* pb = Demo_Create(1, 2);
Derived* pd = Derived_Create(1, 22, 333);
printf("pb->add(3) = %d\n", Demo_Add(pb, 3)); //父类Add函数
printf("pd->add(3) = %d\n", Derived_Add(pd, 3)); //子类Add函数
run(pb, 3); //6
run(pd, 3); //336
Demo_Free(pb);
Demo_Free(pd);
return 0;
}
操作:
1) gcc main.c 51-2.c -o 51-2.out编译正确,打印结果:
pb->add(3) = 6
pd->add(3) = 336
r = 6
r = 336
分析:
1. 头文件:
#ifndef _51_2_H_
#define _51_2_H_
typedef void Demo;
typedef void Derived; //新添加(derived:派生)
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);
Derived* Derived_Create(int i, int j, int k); //创建对象
int Derived_GetK(Derived* pThis); //新添加
int Derived_Add(Derived* pThis, int value); //虚函数
#endif
void是空类型,void*可以被任意类型指针赋值,但是用void指针之前必须被强制转换成原来类型指针,否则无法访问数据。
这里设计上对外提供void*类型,因为内存模型不同,导致void*指针不能正常访问数据(模拟私有数据不能被外界直接访问),有如下测试:
#include <stdio.h>
#include "51-2.h"
void run(Demo* p, int v)
{
int r = Demo_Add(p, v);
printf("r = %d\n", r);
}
int main()
{
Demo* pb = Demo_Create(1, 2);
Derived* pd = Derived_Create(1, 22, 333);
printf("%d\n", pb->mi); //编译器这里报错
printf("pb->add(3) = %d\n", Demo_Add(pb, 3));
printf("pd->add(3) = %d\n", Derived_Add(pd, 3));
run(pb, 3);
run(pd, 3);
Demo_Free(pb);
Demo_Free(pd);
return 0;
}
编译其报错:
main.c:16:20: warnging: dereferencing 'void *' pointer [enabled by default]
printf("%d\n", pb->mi);
警告:对void *指针取值
main.c:16:19: error: request for member 'mi' in someting not a structure or union
错误:在一些不是结构体或者联合体类型中请求成员'mi'
如果想正常访问数据,必须将void*指针强制转换成数据原来的指针,还原内存模型,进而访问数据。但是这个操作只能在函数中进行(封装、私有数据能被成员函数访问,不能被外界直接访问),这个函数模拟的是公有成员函数,如:Create和数据获取等函数。
2. .C文件:
#include "51-2.h"
#include "malloc.h"
//模拟多态:声明两个“同名”函数
static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);
struct VTable // 2. 定义虚函数表数据结构
{
int (*pAdd)(void*, int); // 3. 虚函数表里面存储什么???(函数指针)
};
struct ClassDemo
{
struct VTable* vptr; // 1. 定义虚函数表指针 ==》 虚函数表指针类型???
int mi;
int mj;
};
struct ClassDerived //定义结构对象
{
struct ClassDemo d; //继承
int mk;
};
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向父类累加函数
static struct VTable g_Demo_vtbl = //父类虚函数表
{
Demo_Virtual_Add
};
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向子类累加函数
static struct VTable g_Derived_vtbl = //子类虚函数表
{
Derived_Virtual_Add
};
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->vptr = &g_Demo_vtbl; // 4. 关联对象和虚函数表(父类虚表)
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Demo_GetI(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi;
}
int Demo_GetJ(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mj;
}
// 6. 定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Demo_Virtual_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi + obj->mj + value;
}
// 5. 分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->vptr->pAdd(pThis, value);
}
void Demo_Free(Demo* pThis)
{
free(pThis);
}
Derived* Derived_Create(int i, int j, int k) //创建对象
{
struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
if( ret != NULL )
{
ret->d.vptr = &g_Derived_vtbl;
ret->d.mi = i;
ret->d.mj = j;
ret->mk = k;
}
return ret;
}
int Derived_GetK(Derived* pThis)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk;
}
//定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Derived_Virtual_Add(Demo* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk + value;
}
//分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Derived_Add(Derived* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->d.vptr->pAdd(pThis, value);
}
static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);
这两个函数被修饰为static函数,只属于这个文件。这两个函数为了模拟多态的同名函数(为虚函数)。
// 6. 定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Demo_Virtual_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis; //必须强转
return obj->mi + obj->mj + value;
}
//定义虚函数表中指针所指向的具体函数(定义静态函数:模拟类的私有属性)
static int Derived_Virtual_Add(Demo* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis; //必须强转
return obj->mk + value;
}
参数为Demo*(void*)类型,为了保存数据起始地址,但想访问数据,必须进行强制转换成数据原来的类型才能访问(上边已经证明)。
这两个函数可以被再次封装成普通函数,为了对外界提供访问接口:
// 5. 分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->vptr->pAdd(pThis, value);
}
//分析具体的虚函数!!!!(对外提供的累加接口:模拟公有属性)
int Derived_Add(Derived* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->d.vptr->pAdd(pThis, value);
}
struct VTable // 2. 定义虚函数表数据结构
{
int (*pAdd)(void*, int); // 3. 虚函数表里面存储什么???(函数指针)
};
这是模拟虚函数表,表中声明的是函数指针,这个指针可以指向两个专属的Add函数。
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向父类累加函数
static struct VTable g_Demo_vtbl = //父类虚函数表
{
Demo_Virtual_Add
};
//虚函数表里存放的是一组函数:给函数指针初始化,指针指向子类累加函数
static struct VTable g_Derived_vtbl = //子类虚函数表
{
Derived_Virtual_Add
};
这两个表(结构体)在对象创建之初就被绑定好:
//父类
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->vptr = &g_Demo_vtbl; // 4. 关联对象和虚函数表(父类虚表)
ret->mi = i;
ret->mj = j;
}
return ret;
}
//子类
Derived* Derived_Create(int i, int j, int k) //创建对象
{
struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
if( ret != NULL )
{
ret->d.vptr = &g_Derived_vtbl;
ret->d.mi = i;
ret->d.mj = j;
ret->mk = k;
}
return ret;
}
3. main.c(多态发生的地方:多态是发生代码运行期间)
main.c
#include "stdio.h"
#include "51-2.h"
void run(Demo* p, int v)
{
int r = Demo_Add(p, v); //这里调用父类累加函数,模拟子类退化成父类(Add函数内部会进行强制转换,对父类没有影响,但是会使子类丢失自己专属的数据(地址截断),对遗留下的数据没有任何破坏。不过虚函数表总是放在内存分布的最前边,不会影响找到想调用的子类函数。)
printf("r = %d\n", r);
}
int main()
{
Demo* pb = Demo_Create(1, 2);
Derived* pd = Derived_Create(1, 22, 333);
printf("pb->add(3) = %d\n", Demo_Add(pb, 3)); //父类Add函数
printf("pd->add(3) = %d\n", Derived_Add(pd, 3)); //子类Add函数
run(pb, 3); //6
run(pd, 3); //336
Demo_Free(pb);
Demo_Free(pd);
return 0;
}
void run(Demo* p, int v)
{
int r = Demo_Add(p, v);
printf("r = %d\n", r);
}
分析:
这里调用父类累加函数,模拟子类退化成父类(Add函数内部会进行强制转换,对父类没有影响,但是会使子类丢失自己专属的数据(地址截断),对遗留下的数据没有任何破坏。不过虚函数表总是放在内存分布的最前边,不会影响找到想调用的子类函数。),因为Demo*指针可以指向父类或者子类内存地址,又因为强制转换成统一的父类内存模型没虚函数表,仍然可以访问子类或者父类虚函数表指向的累加函数。这个过程模拟了多态的发生。
转换之前的子类和父类模型:
转换之后的子类和父类模型:
虚函数表并没有被破坏。
小结
1)继承的本质就是父子间成员变量的叠加
2)C++中的多态是通过虚函数表实现的
3)虚函数表是由编译器自动生成与维护的
4)虚函数的调用效率低于普通成员函数