目录
1、对象的本质
class是—种特殊的struct ,在内存中class依旧可以看作变量的集合 ,class与struct遵循相同的内存对齐规则
class中的成员函数与成员变量是分开存放的 ,每个对象有独立的成员变量,所有对象共享类中的成员函数
// 对象内存布局初探test.cpp
#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()
{
A a;
cout << "sizeof(A) = " << sizeof(A) << endl; // 20 bytes
cout << "sizeof(a) = " << sizeof(a) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl; // 20 bytes
a.print();
B* p = reinterpret_cast<B*>(&a);
p->i = 1; // 访问了a对象的i私有成员部分???
p->j = 2;
p->c = 'c';
p->d = 3;
a.print();
p->i = 100;
p->j = 200;
p->c = 'C';
p->d = 3.14;
a.print();
return 0;
}
运行时的对象退化为结构体的形式 ,所有成员变量在内存中依次排布,成员变量间可能存在内存空隙
可以通过内存地址直接访问成员变量 ,访问权限关键字在运行时失效
类中的成员函数位于代码段中 ,调用成员函数时对象地址作为参数隐式传递 (this)
成员函数通过对象地址访问成员变量 ,C++语法规则隐藏了对象地址的传递过程 (具体过程:点击直达)
#include <iostream>
using namespace std;
class Demo
{
int mi;
int mj;
public:
Demo(int i, int j)
{
mi = i;
mj = j;
}
int getI()
{
return mi;
}
int getJ()
{
return mj;
}
int add(int value)
{
return mi + mj + value;
}
};
int main()
{
Demo d(1, 2);
cout << "sizeof(d) = " << sizeof(d) << endl; // 8
cout << "d.getI() = " << d.getI() << endl; // 1
cout << "d.getJ() = " << d.getJ() << endl; // 2
cout << "d.add(3) = " << d.add(3) << endl; // 6
return 0;
}
下面用C实现上面的面向对象程序
typedef void Demo; // 封装的关键
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); // 模拟析构函数
struct ClassDemo // 真正的类
{
int mi; // 成员变量
int mj;
};
Demo* Demo_Create(int i, int j) // 构造函数,初始化成员i,j
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->mi = i;
ret->mj = j;
}
return ret; // Demo* --> void* 即不能直接访问成员i,j,需要类型转换,实现信息隐藏
}
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;
}
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi + obj->mj + value;
}
void Demo_Free(Demo* pThis) // 析构函数,释放堆空间的对象
{
free(pThis);
}
int main()
{
Demo* d = Demo_Create(1, 2); // 通过构造函数创建对象,返回对象地址
printf("d.mi = %d\n", Demo_GetI(d)); // d->getI(); // 调用成员函数时传入对象的地址
printf("d.mj = %d\n", Demo_GetJ(d)); // d->getJ();
printf("Add(3) = %d\n", Demo_Add(d, 3)); // d->add(3);
// d->mi = 100; // 实现信息隐藏,error: request for member ‘mi’ in something not a structure or union
Demo_Free(d);
return 0;
}
2、继承对象模型
在C++编译器的内部类可以理解为结构体 ,子类是由父类成员叠加子类新成员得到的
#include <iostream>
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
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d); // 证明对象d的内存分布和Test结构体对象一致
cout << "Before changing ..." << endl;
d.print(); // 1 2 3
p->mi = 10; // 通过p指针改变d对象成员变量值
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print(); // 10 20 30
return 0;
}
3、多态对象模型
C++多态的实现原理
当类中声明虚函数时,编译器为类生成—个虚函数表 ,虚函数表是—个存储成员函数地址的数据结构 ,虚函数表是由编译器自动生成与维护的
virtual成员函数会被编译器放入虚函数表中 ,存在虚函数时,每个对象中都有—个指向虚函数表的指针
当类中有虚函数,创建对象时,在该对象中"塞"入一个指针成员变量(vptr),指向虚函数表
下图:编译两个类,编译器分别生成了两张虚函数表,并将虚函数放入虚函数表
void run(Demo* p, int v)
{
p->add(v);
}
编译器确认add函数是否为虚函数,若是:编译器在对象vptr所指向的虚函数表中查找add()的地址;若不是:编译器直接确定被调用函数地址;
通过p指针找到具体对象,通过具体对象的虚函数表指针找到虚函数表,在虚函数表找到函数地址,三次寻址,C++的多态通过牺牲效率实现
调用效率对比:虚函数 < 普通成员函数
虚函数表指针的存在,继承中的对象模型 test.cpp
#include <iostream>
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; // 4 + 8 = 12
cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 4 + 12 = 16
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d); // 证明对象d的内存分布和Test结构体对象一致
cout << "Before changing ..." << endl;
d.print();
p->mi = 10; // 通过p指针改变d对象成员变量值
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print();
return 0;
}
4、C实现C++的封装,继承,多态特性
下面用C实现C++的封装,继承,多态特性,深入理解C++对象模型
typedef void Demo; // 父类
typedef void 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); // 子类虚函数
/* 声明真正的虚函数,只能当前文件调用 */
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;
};
/* 定义父类虚函数表对象 */
/* 用父类真正意义的add虚函数初始化函数指针 */
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add
};
/* 定义子类类虚函数表对象 */
/* 用子类真正意义的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);
}
void run(Demo* p, int v)
{
// p指向父类对象时,调用Demo_Virtual_Add
// p指向子类对象时,调用Derived_Virtual_Add
int r = Demo_Add(p, v);
printf("r = %d\n", r);
}
int main()
{
// 创建父类对象,初始化父类的虚函数表指针,指向g_Demo_vtbl虚函数表,成员为函数指针,指向Demo_Virtual_Add函数
Demo* pb = Demo_Create(1, 2);
// 创建子类对象,初始化子类继承的虚函数表指针,指向g_Derived_vtbl,成员为函数指针,指向Derived_Virtual_Add函数
Derived* pd = Derived_Create(1, 22, 333);
printf("pb->add(3) = %d\n", Demo_Add(pb, 3)); // 6
printf("pd->add(3) = %d\n", Derived_Add(pd, 3)); // 336
run(pb, 3); // r = 6
run(pd, 3); // r = 336
Demo_Free(pb);
Demo_Free(pd);
return 0;
}
5、补充说明
面向对象程序最关键的地方在于必须能够表现三大特性:封装,继承,多态!
☛ 封装指的是类中的敏感数据在外界是不能访问的;
☛ 继承指的是可以对已经存在的类进行代码复用,并使得类之间存在父子关系;
☛ 多态指的是相同的调用语句可以产生不同的调用结果。
因此,如果希望用 C 语言完成面向对象的程序,那么肯定的,必须实现这三个特性;否则,最多只算得上基于对象的程序
封装
上文中通过 void* 指针保证具体的结构体成员是不能在外界被访问的,以此模拟 C++ 中 private 和 protected。因此,在头文件中定义了如下的语句:
typedef void Demo;
typedef void Derived;
Demo 和 Derived 的本质依旧是 void, 所以,用 Demo* 指针和 Derived* 指针指向具体的对象时,无法访问对象中的成员变量,这样就达到了“外界无法访问类中私有成员”的封装效果!
继承
继承的本质是父类成员与子类成员的叠加,所以在用 C 语言写面向对象程序的时候,可以直接考虑结构体成员的叠加即可。
文中的实现直接将 struct ClassDemo d 作为 struct ClassDerived 的第一个成员,以此表现两个自定义数据类型间的继承关系。
因为 struct ClassDerived 变量的实际内存分布就是由struct ClassDemo 的成员以及 struct ClassDerived 中新定义的成员组成的,这样就直接实现了继承的本质,所以说 struct ClassDerived 继承自 struct ClassDemo。
多态
多态在 C++ 中的实现本质是通过虚函数表完成的,而虚函数表是编译器自主产生和维护的数据结构。因此,接下来要解决的问题就是如何在 C 语言中自定义虚函数表?
文中认为通过结构体变量模拟C++中的虚函数表是比较理想的一种选择,所以有了下面的代码:
struct VTable
{
int (*pAdd)(void*, int);
};
必须要注意的是,若误将 pAdd 指针的类型定义成了int (*)(Derived*, int) ,这从 C 语言的角度算不上错误, 所以编译运行都没有问题。但是,从面向对象的角度,这里可以说是一种语义上的错误!
因为 pAdd 必须可以指向父类中定义的 Add 函数版本,也可以指向子类中定义的 Add 函数版本,所以说用 Derived* 作为第一个参数表示实际对象并不合适,应该直接使用 void* 。
有了类型后就可以定义实际的虚函数表了,在 C 语言中用具有文件作用域的全局变量表示实际的虚函数表是最合适的,因此有了下面的代码:
// 父类对象使用的虚函数表
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add
};
// 子类对象使用的虚函数表
static struct VTable g_Derived_vtbl =
{
Derived_Virtual_Add
};
每个对象中都拥有一个指向虚函数表的指针,而所有父类对象都指向g_Demo_vtbl,所以所有子类对象都指向 g_Derived_vtbl。
当一切就绪后,实际调用虚函数的过程就是通过虚函数表中的对应指针来完成的。