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;
};

image
类A的虚表示意图如上。

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。
需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

2.2 虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表
image

一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

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;
};

image

如上图,类 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,可以参考本文的参考资料)
调用的是虚函数
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。

posted @ 2023-03-03 13:12  Panor  阅读(35)  评论(0编辑  收藏  举报