C++中virtual关键字解决了什么问题(一)

一、继承引发的问题
在《C++ Primer Plus》类继承那一章中,将发生类继承这一行为的两个类之间的关系定义为“is-a关系”,即“子类对象是一个父类对象”。可能这么说不太容易理解,以书中水果的例子来讲:水果类为基类,香蕉类为水果类的派生类。香蕉作为水果的一种,我们可以讲香蕉是水果,所以香蕉对象也是水果对象。在代码中直观的体现就是,子类具有父类的所有属性和方法。

class A
{
public:
    A(){cout << "Create A" << endl;}
    void selfintroduce(){cout << name << endl;}
    ~A(){cout << "Destroy A" << endl;}
private:
    string name = "I am A";
};

class B : public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
};

int main(int argc, char* argv[])
{
    B* b = new B;
    b->selfintroduce();
    delete b;
    return 0;
}

以上代码执行结果如下:

Create A //先执行父类构造函数
Create B //然后执行子类构造函数
I am A   //继承自父类的属性和方法
Destroy B //按与构造函数相反的执行顺序析构对象
Destroy A

可以看到对象b能够正常访问到A类中的属性和方法。但是前面讲“子类对象是一个父类对象”,那我用一个父类的指针去创建一个子类对象也很合理吧,对上面的代码的main函数稍作修改:

int main(int argc, char* argv[])
{
    A* b = new B; //仅修改指针类型
    b->selfintroduce();
    delete b;
    return 0;
}

编译过程没有产生任何警告和错误:

make
gcc main.cpp -Wall -Werror -m64 -g -c -o main.o -Wall -Werror -m64 -g
gcc main.o -Wall -Werror -m64 -g -lstdc++  -o virFunTable
执行结果:
 ./virFunTable 
Create A
Create B
I am A
Destroy A

这时我们发现一个问题:对象b在析构时没有执行B类中的析构函数!众所周知,如果没能正确执行析构函数可能会导致内存泄漏等问题,是个比较严重的问题。如何解决?
二、virtual关键字
针对上面通过父类指针销毁子类对象时,子类析构函数未执行的问题,只需将父类的析构函数声明为虚函数即可解决。

class A
{
public:
    A(){cout << "Create A" << endl;}
    void selfintroduce(){cout << name << endl;}
    virtual ~A(){cout << "Destroy A" << endl;} //增加virtual关键字
private:
    string name = "I am A";
};

class B : public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
};

int main(int argc, char* argv[])
{
    A* b = new B; //仅修改指针类型
    b->selfintroduce();
    delete b;
    return 0;
}

修改后的执行结果如下:

./virFunTable 
Create A
Create B
I am A
Destroy B //子类析构函数正常执行
Destroy A

三、思维扩展
上面的这种问题除了增加virtual关键字之外还有别的办法吗?答案是肯定的。因为通过delete关键字析构对象时,执行哪个析构函数与指针的类型相关,所以我们把delete时的指针强转回子类好像也能解决这个问题。

class A
{
public:
    A(){cout << "Create A" << endl;}
    void selfintroduce(){cout << name << endl;}
    ~A(){cout << "Destroy A" << endl;} //没有virtual关键字
private:
    string name = "I am A";
};

class B : public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
};

int main(int argc, char* argv[])
{
    A* b = new B; 
    b->selfintroduce();
    delete (B*)b; //父类指针强转子类
    return 0;
}

执行结果:

./virFunTable 
Create A
Create B
I am A
Destroy B //子类析构函数正常执行
Destroy A

确实可以。但是回过头来,从理性的角度讲这种做法违背了继承中“is-a关系”,既然“子类对象是一个父类对象”,那通过父类指针操作子类对象应当同操作父类对象一样别无二致,在delete时不应与父类对象有所区别。
再逆向思考一下,倘若一个类中没有virtual关键字,那它可以做基类去继承吗?
未完待续...

posted on 2022-03-04 21:32  OrangeGLC  阅读(98)  评论(0)    收藏  举报

导航