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关键字,那它可以做基类去继承吗?
未完待续...
浙公网安备 33010602011771号