C++--class
1.类实例化过程
内存分配 -> 初始化列表 -> 赋值
1.1 内存分配
全局对象、静态对象、分配在栈区域内的对象,在编译阶段进行内存分配;
存储在堆空间的对象,是在运行阶段进行内存分配。
1.2 初始化列表
首先明确一点:初始化不同于赋值
。
初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。
初始化列表先于构造函数体内的代码执行
,初始化列表执行的是数据成员的初始化过程
。
例:
//在类的构造函数名之后紧跟着冒号,冒号后面是要初始化的成员名,之后是圆括号或者花括号括起来的初始值。这样的结构就是构造函数的初始值列表,如下所示
// 自定义类A具有一个包含了初始值列表的构造函数,使用该构造函数创建对象时,该对象的成员变量a的值是1,b的值是2
class A
{
public:
A() :a(1),b(2) {};
int a;
int b;
};
1.3 赋值
对象初始化完成后,可以对其进行赋值。
对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。
当执行完该函数体,也就意味着类对象的实例化过程完成了。
1.4 类实例化总结
构造函数实现了对象的初始化和赋值两个过程
。
对象的初始化是通过初始化列表来完成
,而对象的赋值则才是通过构造函数的函数体来实现
。
1.5 类实例化堆栈
1.5.1 栈中分配实例化类
class Person
{
...;
}
/// 实例化类
Person person;
以上完成了不用 new 进行类的实例化,这个时候是在栈中分配内存的,使用完后不需要再手动进行内存的释放,类的析构函数会自动执行内存释放的动作。这样的类实例化方法适用于小类
,没有过多的内存管理的动作,使用起来比较便捷。
1.5.2 堆中分配实例化类
class Person
{
...;
}
/// 实例化类
Person* person = new Person();
/// 释放内存
delete person;
2.类的虚函数
在虚函数原型后加 =0
例:
virtual void funtion()=0
2.1 虚函数表
每个包含了虚函数的类都包含一个虚函数表。
虚函数表属于类,不属于对象
。
当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
例:
//类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
类A的虚表示意图如上。
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。
需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段
,也就是说在代码的编译阶段,虚表就可以构造出来了。
2.2 虚表指针
虚表是属于类的,而不是属于某个具体的对象
,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表
。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。
3.类的大小
说明:类的大小是指类的实例化对象的大小,用 sizeof 对类型名操作时,结果是该类型的对象的大小。
1.遵循结构体的对齐原则。
2.与普通成员变量有关,与成员函数和静态成员无关。
即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。
因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
3.虚函数对类的大小有影响,是因为虚函数表指针的影响。
4.虚继承对类的大小有影响,是因为虚基表指针带来的影响。
5.空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,
为了保证不同对象的地址不同,空类也占用存储空间。
注:虚函数的个数并不影响所占内存的大小,因为类对象的内存中只保存了指向虚函数表的指针。
4. 基类指针可以指向派生类对象
基类指针可以指向派生类对象。
原因是这样的:在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。
当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。
注:基类指针指向派生类对象时,该指针能访问到的范围是基类中的成员,派生类添加的成员该指针是访问不到的。若派生类重写了基类的虚函数,那该指针调用该虚函数时,实际调用的是派生类的虚函数实现。
5.类的动态绑定
例:
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
如上图,类 A,类 B,类 C 的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
类 A: 包括两个虚函数,故 A vtbl 包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类 B: 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了B::vfunc1()函数,
故 B vtbl 的两个指针分别指向B::vfunc1()和A::vfunc2()。
类 C: 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了C::vfunc2()函数,
故 C vtbl 的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
//假设我们定义一个类 B 的对象。由于 bObject是类 B 的一个对象,故bObject包含一个虚表指针,指向类 B 的虚表。
int main()
{
B bObject;
A *p = & bObject; //基类指针指向派生类对象(不懂看4)。虽然p是基类的指针只能指向基类的部分,但p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类 B 的虚表,所以p可以访问到 B vtbl
p->vfunc1(); //访问对象bObject对应的虚表中的函数vfunc1().
//为什么能访问到 bobject的虚表。原因:虚表构造是在编译阶段发生的,而基类A和派生类B中都存在方法vfunc1(),所以基类指针能访问到方法vfunc1,指针p赋的值是派生类B的,故调用的方法为类B的vfunc1()
}
int main()
{
A aObject;
A *p = &aObject; //基类指针指向派生类对象(不懂看4)
p->vfunc1(); //调用的是派生类A的vfunc1()
}
我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态
。
动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。
什么时候会执行函数的动态绑定?这需要符合以下三个条件。
通过指针来调用函数
指针 upcast 向上转型(继承类向基类的转换称为 upcast,关于什么是 upcast,可以参考本文的参考资料)
调用的是虚函数
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。