C++随心记(二)

本文概述了动态类型和静态类型的区别,讨论了私有和公有继承在C++中的作用,重点讲解了虚继承、虚函数表及构造顺序,揭示了动态绑定在实现运行时多态中的核心作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 动态类型和静态类型

        动态类型:对象在运行时的类型。

        静态类型:对象被定义的类型或表达式产生的类型,该类型在编译时是已知的。

        对象的静态类型和动态类型不一致:当我们使用引用和指针操作一个对象时,基类的指针或引用可以指向派生类,此时,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。

class Base {/* int func() */}  

class A : public Base {/* int func() */} 

// !函数参数的静态类型是Base,具体的动态类型需要运行时才能知道
int Function(const Base &item) {/* 
    int ans = item.func();
*/}

// 正式调用
// 1. 传入Base对象,调用的函数版本是 Base::func()
Base b = new Base();
func(b);
// 2. 调用的版本是A::func()
A a = new A();
func(a);

        在这段代码中,Function函数内要调用func,但具体调用哪个类的func(),在运行时由item的具体类型决定。

2. 私有继承和公有继承

        这里涉及到继承的概念,不同的继承方式,主要改变了派生类对基类成员的访问权限。

私有继承:

        基类的公有成员和保护成员在被派生类继承后,将变为派生类的私有成员,能够被派生类的成员函数直接访问;不能被这个派生类的子类访问。

        同时基类的私有成员也会被派生类继承,但是这些成员不能被派生类的成员函数 or 派生类的对象(指针,引用)直接访问。

公有继承:

        基类的公有成员和保护成员原样继承给派生类,能够直接被派生类中定义的成员函数访问;私有成员不能被派生类的成员函数 or 派生类的对象(指针,引用)直接访问。

3. 多重继承

        从多个直接基类中产生派生类的能力。

        构造函数调用顺序:最终基类(根部)—— 向派生类顺序遍历基类 —— 直接基类 —— 派生类。

        析构函数:只负责清除派生类本身分配的资源。调用顺序与构造函数相反。

4. 虚继承

        虚继承是多重继承的一种形式,基类被继承了多次,但是派生类共享该基类的唯一副本。

        具体操作,使用virtual关键字,令某个类做出声明,承诺愿意共享它的基类,其中共享的基类子对象称为虚基类。在此策略中,无论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

// 表达等价
class A : public virtual Base {/* */}
class A : virtual public Base {/* */}

// Base是A,B的虚基类
class A : virtual public Base {/* */}  //  虚继承
class B : virtual public Base {/* */}  //  虚继承
class C : public A, public B {/* */}

// 这样C中只有一个Base基类部分

5. 虚继承的对象的构造顺序

        构造函数调用顺序:根部虚基类 — 向派生方向依次构建间接虚基类 — 直接虚基类 — 第一个非虚基类 — 向派生方向依次构建非虚基类 — 派生类

析构顺序:相反

6. 虚函数表

        定义:虚函数表属于类,不属于具体对象,本质是一个指针数组,数组中每个元素是类中虚函数的函数指针,调用虚函数需要经过虚函数表,它是动态绑定技术的核心。

        6.1 C++实现了动态绑定的技术,即在运行中识别对象的具体类型,该技术核心是虚函数表。

        6.2 每个包含虚函数的类都包含了一个虚函数表,派生类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

        6.3 虚表是属于类的,不属于某个具体的对象,一个类只需要一个虚表,同一个类的所有对象共享一个虚表。

        6.4 虚函数表是一个指针数组(数组中每个元素都是指针),其元素是虚函数的函数指针,每个元素对应一个虚函数的函数指针。

        6.5 虚函数表指针 *__vptr

        编译器在类中添加了一个指针,*__vptr,进一步告知每个对象需要指向的虚函数表,对象内部包含一个指针,指向所使用的虚函数表。

// 类A的虚函数表,有a(),b()两个虚函数的函数指针
class A {
public:
    virtual void a();
    virtual void b();
    void func();
}

// 类B继承自类A,因此也有自己的虚函数表
// B重写了虚函数a(),虚函数表指针分别指向A::b(), B::a()
class B : public A {
public:
    virtual void a();

}


int main(){
// 当执行到这里,会发现a是一个指针,并且调用的是虚函数
// 此时会根据a->__vptr来访问B的虚函数表,进一步调用B中的a()
    B b = new B();
    A *a = &b;
    a->a();
}

总结:把经过虚函数表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。

!!!需要注意的是:普通函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素不包括普通函数的函数指针。

7. 虚函数表和构造函数的调用 / 初始化顺序

        7.1 在单独一个类、没有继承的情况

        分配内存 — 虚函数指针赋值 — 列表初始化 — 执行构造函数体

        7.2 有继承的情况

        分配内存 — ( 基类虚函数表指针赋值 — 基类列表初始化 — 基类构造函数体执行 ) — 派生类虚函数指针赋值 — 派生类列表初始化 — 执行派生类构造函数体

!! 将虚表指针的赋值过程放置在初始化列表之前,防止在初始化列表出现调用虚函数的情况!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值