到目前为止,我们都是使用class关键字来定义类,其实在C++中,class是一种特殊的struct
- 在内存中class依旧像struct一样,可以看作变量的集合
- class与struct遵循相同的内存对齐规则
- class中的成员函数与成员变量是分开存放的
- 每个对象拥有独立的成员变量
- 所有对象共享类中的成员函数
#include <iostream>
using namespace std;
class A
{
int item1;
int item2;
char item3;
};
struct B
{
int item1;
int item2;
char item3;
};
void main()
{
cout << "sizeof(A) = " << sizeof(A) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl;
system("pause");
}
对于以上代码,我们分别使用class与struct来定义了A跟B,并且在main函数中计算A跟B所占的内存空间
从输出结果,我们看到,A跟B在内存中所占的内存大小是一样的,也就是说,在某种意义上,class与struct没有什么实质性的区别
- class对象在运行时会退化为结构体形式
- 所有成员变量在内存中依次排布
- class由于遵循内存对齐规则,成员变量之间可能存在内存空隙
- 可以通过内存地址直接访问成员变量
- 访问权限关键字在运行时失效
- 类中的成员函数位于代码段中
- 调用成员函数时对象地址作为参数隐式传递(this指针)
- 成员函数通过对象地址访问成员变量
- C++语法规则隐藏了对象地址的传递过程
#include <iostream>
using namespace std;
class Test
{
private:
int m_value;
public:
void fun(int i)
{
this->m_value = i; //this指针传递当前的对象地址
//m_value = i; //本质与上一句一样,只是语法规则隐藏了this指针
cout << "m_value = " << m_value << endl;
}
};
void main()
{
Test T1, T2;
T1.fun(10);
T2.fun(20);
system("pause");
}
上边的代码,就是在成员函数fun中,通过this指针,将当前对象地址传入,从而能访问当前对象中的成员变量,也可以不写this指针,通过隐式方式直接访问类对象的成员变量
上边我们说了普通对象的模型,那么对于继承对象的模型又是什么样?在C++中,子类是由父类成员叠加子类新成员得到的,如下
#include <iostream>
using namespace std;
class parent
{
int m_item1;;
};
class child : public parent //继承自父类
{
int m_item2;
int m_item3;
};
void main()
{
cout << "sizeof(parent) = " << sizeof(parent) << endl;
cout << "sizeof(child) = " << sizeof(child) << endl;
system("pause");
}
上边我们定义了一个parent父类,一个继承自parent的子类child,我们从子类是由父类成员叠加子类新成员得到的,可以猜测,父类占有1个成员变量m_item1,子类有m_item1、 m_item2、m_item3三个成员变量组成,其中m_item1是从父类继承而来,那么sizeof(parent) = 4;sizeof(child) = 12;实际编译输出如下,刚好符合我们的猜想
对于上边的的子类child,内存结构其实如下,由父类成员叠加子类新成员。
C++多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一种存储成员函数地址的数据结构,用于存储类中的虚函数地址
- 虚函数表是由编译器自动生成及维护
- 类中有虚函数时,每个具体对象中都有一个指向虚函数表的指针,以便实现多态
上图中定义的两个类,一个父类一个子类,并且父类中的虚函数add(int value)在子类中重写,那么对于父类Demo,编译器就会自耦东生成一个虚函数表,里边存放父类的虚函数地址void Demo::add(int value);同理,子类Derived子类中也会自动生成一个虚函数表,存放子类中的虚函数void Derived::add(int value)。
在生成上边两个类的具体对象时,每个对象中就会包含一个虚函数表指针,指向各自类中的虚函数表
当具体对象指针或引用传递给父类指针或引用时,假如调用的函数是虚函数,那么编译器就在所传入的具体对象的虚函数表指针所指的虚函数表中去寻找调用函数的地址。比方传入的是子类的对象地址,那么编译器就会在子类对象中的虚函数指针所指的虚函数表(也就是子类的虚函数表)中查询调用函数的地址,这时候执行的就是子类中的重写的函数。这样就实现了多态,执行的函数是所传进来指针对象里的函数,而不会退化成去调用父类对象中的同名函数。
因为调用虚函数时会有一个虚函数表的查找,所以调用效率会比直接调用普通函数低。
#include <iostream>
using namespace std;
class parent
{
public:
virtual void fun() //虚函数,parent类对象中会生成一个虚函数表指针,占4字节
{
cout << "I'm Parent" << endl;
}
};
class child : public parent
{
public:
virtual void fun() //虚函数,child类对象中会生成一个虚函数表指针,占4字节
{
cout << "I'm Child" << endl;
}
};
void main()
{
cout << "sizeof(parent) = " << sizeof(parent) << endl; //输出的大小为对象中虚函数表指针的大小,4个字节
cout << "sizeof(child) = " << sizeof(child) << endl; //输出的大小为对象中虚函数表指针的大小,4个字节
parent* p = NULL;
child c;
p = &c; //父类指针指向子类对象
p->fun(); //因为实现了虚函数,所以这里会去查子类中的虚函数表,
//得到子类中的fun()函数地址,所以执行的是子类中的fun, 继而实现了多态
system("pause");
}
编译执行如下
上边的代码中,两个类中我们都没有定义任何成员变量(成员函数不属于具体类对象),所以表面看来两个类对象不占内存空间,但是输出结果却是都占了4字节的内存空间,这是因为我们在里边定义了虚函数,这就使得其具体对象生成一个虚函数指针,指针是占4字节的(32位系统)。同时我们拿父类指针指向的子类对象,这里就实现了面向对象中的多态。
总结
- C++中的类对象在内存布局上与结构体相同
- 成员变量和成员函数在内存中分开存放
- 访问权限关键字在运行时失效
- 调用成员函数时对象地址作为参数隐式传递
- 继承的本质是父子间成员变量的叠加
- C++中的多态是通过虚函数表实现的
- 虚函数表是由编译器自动生成与维护的
- 虚函数的调用效率低于普通成员函数