C++多态相关

本文深入探讨了C++中虚函数表的概念及其在实现多态中的作用,解释了构造函数不能是虚函数的原因,以及为何不应在构造函数中调用虚函数。

虚函数表

当通过指针访问类的成员函数时:
如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数。
如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable。

我们以下面的继承关系为例进行讲解:

#include <iostream>
#include <string>
using namespace std;

//People类
class People{
public:
    People(string name, int age);
public:
    virtual void display();
    virtual void eating();
protected:
    string m_name;
    int m_age;
};
People::People(string name, int age): m_name(name), m_age(age){ }
void People::display(){
    cout<<"Class People:"<<m_name<<"今年"<<m_age<<"岁了。"<<endl;
}
void People::eating(){
    cout<<"Class People:我正在吃饭,请不要跟我说话..."<<endl;
}

//Student类
class Student: public People{
public:
    Student(string name, int age, float score);
public:
    virtual void display();
    virtual void examing();
protected:
    float m_score;
};
Student::Student(string name, int age, float score):
    People(name, age), m_score(score){ }
void Student::display(){
    cout<<"Class Student:"<<m_name<<"今年"<<m_age<<"岁了,考了"<<m_score<<"分。"<<endl;
}
void Student::examing(){
    cout<<"Class Student:"<<m_name<<"正在考试,请不要打扰T啊!"<<endl;
}

//Senior类
class Senior: public Student{
public:
    Senior(string name, int age, float score, bool hasJob);
public:
    virtual void display();
    virtual void partying();
private:
    bool m_hasJob;
};
Senior::Senior(string name, int age, float score, bool hasJob):
    Student(name, age, score), m_hasJob(hasJob){ }
void Senior::display(){
    if(m_hasJob){
        cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,并且顺利找到了工作,Ta今年"<<m_age<<"岁。"<<endl;
    }else{
        cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,不过找工作不顺利,Ta今年"<<m_age<<"岁。"<<endl;
    }
}
void Senior::partying(){
    cout<<"Class Senior:快毕业了,大家都在吃散伙饭..."<<endl;
}

int main(){
    People *p = new People("赵红", 29);
    p -> display();

    p = new Student("王刚", 16, 84.5);
    p -> display();

    p = new Senior("李智", 22, 92.0, true);
    p -> display();

    return 0;
}

运行结果:
Class People:赵红今年29岁了。
Class Student:王刚今年16岁了,考了84.5分。
Class Senior:李智以92的成绩从大学毕业了,并且顺利找到了工作,Ta今年22岁。

在这里插入图片描述

图中左半部分是对象占用的内存,右半部分是虚函数表 vtable。在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

仔细观察虚函数表,可以发现基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

当通过指针调用虚函数时,先根据指针找到 vfptr,再根据 vfptr 找到虚函数的入口地址。以虚函数 display() 为例,它在 vtable 中的索引为 0,通过 p 调用时:
p -> display();

编译器内部会发生类似下面的转换:
( *( *(p+0) + 0 ) )§;

下面我们一步一步来分析这个表达式:
0是 vfptr 在对象中的偏移,p+0是 vfptr 的地址;
(p+0)是 vfptr 的值,而 vfptr 是指向 vtable 的指针,所以(p+0)也就是 vtable 的地址;
display() 在 vtable 中的索引(下标)是 0,所以( *(p+0) + 0 )也就是 display() 的地址;
知道了 display() 的地址,( *( *(p+0) + 0 ) )§也就是对 display() 的调用了,这里的 p 就是传递的实参,它会赋值给 this 指针。

可以看到,转换后的表达式是固定的,只要调用 display() 函数,不管它是哪个类的,都会使用这个表达式。换句话说,编译器不管 p 指向哪里,一律转换为相同的表达式。

转换后的表达式没有用到与 p 的类型有关的信息,只要知道 p 的指向就可以调用函数,这跟名字编码(Name Mangling)算法有着本质上的区别。

再来看一下 eating() 函数,它在 vtable 中的索引为 1,通过 p 调用时:
p -> eating();

编译器内部会发生类似下面的转换:
( *( *(p+0) + 1 ) )§;

对于不同的虚函数,仅仅改变索引(下标)即可。

构造函数为什么不能是虚函数

  1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
  2. 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
  3. 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

为什么不要在构造函数中调用虚函数

先看一段在构造函数中直接调用虚函数的代码:

#include <iostream>

class Base
{
public:
    Base() { Foo(); }   ///< 打印 1

    virtual void Foo()
    {
        std::cout << 1 << std::endl;
    }
};

class Derive : public Base
{
public:
    Derive() : Base(), m_pData(new int(2)) {}
    ~Derive() { delete m_pData; }

    virtual void Foo()
    {
        std::cout << *m_pData << std::endl;
    }
private:
    int* m_pData;
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

这里的结果将打印:1。

这表明第6行执行的的是Base::Foo()而不是Derive::Foo(),也就是说:虚函数在构造函数中“不起作用”。为什么?

当实例化一个派生类对象时,首先进行基类部分的构造,然后再进行派生类部分的构造。即创建Derive对象时,会先调用Base的构造函数,再调用Derive的构造函数。

当在构造基类部分时,派生类还没被完全创建,从某种意义上讲此时它只是个基类对象。即当Base::Base()执行时Derive对象还没被完全创建,此时它被当成一个Base对象,而不是Derive对象,因此Foo绑定的是Base的Foo。

C++之所以这样设计是为了减少错误和Bug的出现。假设在构造函数中虚函数仍然“生效”,即Base::Base()中的Foo();所调用的是Derive::Foo()。当Base::Base()被调用时派生类中的数据m_pData还未被正确初始化,这时执行Derive::Foo()将导致程序对一个未初始化的地址解引用,得到的结果是不可预料的,甚至是程序崩溃(访问非法内存)。

总结来说:基类部分在派生类部分之前被构造,当基类构造函数执行时派生类中的数据成员还没被初始化。如果基类构造函数中的虚函数调用被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,将导致程序出现一些未定义行为和bug。

<think>好的,我需要处理用户关于C++多态的笔试题的查询。首先,用户提供了四个引用内容,其中包含不同的代码示例和问题。我需要分析这些引用,找出与C++多态相关的关键点,并生成相关问题。 首先,引用[1]讨论了解构函数未重写的问题,导致派生类的析构函数未被调用,可能引发资源泄漏。这涉及到虚析构函数的重要性。 引用[2]和[3]是关于虚函数默认参数的问题。在C++中,虚函数的默认参数是静态绑定的,因此当通过基类指针调用派生类函数时,使用的默认参数来自基类。这可能导致输出与预期不符,如问题中的答案B显示。 引用[4]展示了构造函数中虚函数调用的风险。由于在基类构造函数期间,派生类尚未初始化,虚函数表指针可能指向基类的虚函数表,导致调用基类的虚函数而非派生类的。 接下来,我需要根据这些内容生成相关问题。问题应覆盖虚析构函数、默认参数的静态绑定、构造函数中的虚函数调用,以及虚函数表机制。同时,要确保问题符合用户要求,与多态相关,并基于提供的引用内容。 最后,需要检查生成的问题是否清晰,是否覆盖了各个关键点,并且没有超出引用范围。确保问题能够帮助用户深入理解C++多态的不同方面。</think>### C++多态编程笔试题解析 #### 1. 虚析构函数的重要性 当基类指针指向派生类对象时,若基类析构函数未声明为虚函数,`delete`操作仅调用基类析构函数,导致派生类析构函数未被调用,可能引发资源泄漏[^1]。 **示例代码问题点**: ```cpp Person* p1 = new Student; delete p1; // 仅输出~Person() ``` **解决方法**:将基类析构函数声明为`virtual`: ```cpp virtual ~Person() { ... } ``` #### 2. 虚函数默认参数的静态绑定特性 虚函数的多态性**仅作用于函数体**,默认参数值在编译期根据指针类型静态确定[^2][^3]。 **示例分析**: ```cpp B* p = new B; p->test(); // 调用A::test(),实际执行B::func(val=1) ``` **输出结果**:`B->1`。原因:`test()`在基类A中定义,其调用`func()`时使用A的默认参数`val=1`,但实际调用派生类B的`func()`实现。 #### 3. 构造函数中调用虚函数的风险 在构造函数执行期间,虚函数表指针(vptr)尚未指向派生类的虚函数表,因此虚函数调用不会触发多态[^4]。 **代码示例**: ```cpp Base() { clear(); // 清空虚函数表指针 show(); // 调用Base::show() } ``` **关键结论**:构造函数中调用虚函数会直接执行当前类的实现,无法实现动态绑定。 #### 4. 虚函数表vtable)机制 虚函数通过虚函数表实现动态绑定。对象内存布局首地址存储虚函数表指针,指向类的虚函数地址列表[^4]。 **内存布局示例**: ```cpp Derive obj; // 内存结构:vfptr -> &Derive::vftable ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值