这一节涉及到虚函数的知识。什么叫虚函数?
关于C++中虚函数的作用——这篇博客十分值得阅读,带你彻底理解虚函数的作用!
虚函数提及,能让你成员函数干嘛
- 默认情形下,成员函数的解析都是编译时静态进行。如果要让成员函数的解析在程序运行时动态进行,咋办啊?
就在成员函数的声明前加上关键字virtual,举个例子:
class LibMat{
public:
LibMat()
{
cout<<"LibMat::LibMat() default constructor!\m";
//LibMat类的默认构造构造函数
}
virtual ~LibMat()
{
cout<<"LibMat::~LibMat() destructor!\n";
//LibMat类的析构函数且是虚函数
}
virtual void print()const
{
cout<<"LibMat::print()--I am a LibMat object!\n";
//LibMat类的const成员函数且是虚函数
}
};
class Book : public LibMat {
public:
Book( const string &title, const string &author )
: _title( title ), _author( author ){
cout << "Book::Book( " << _title
<< ", " << _author << " ) constructor\n";
}
~Book(){
cout << "Book::~Book() destructor!\n";
}
virtual void print() const {
cout << "Book::print() -- I am a Book object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << endl;
}
const string& title() const { return _title; }
const string& author() const { return _author; }
protected:
string _title;
string _author;
};
class AudioBook : public Book {
public:
AudioBook( const string &title,
const string &author, const string &narrator )
: Book( title, author ), _narrator( narrator ){
cout << "AudioBook::AudioBook( " << _title
<< ", " << _author
<< ", " << _narrator
<< " ) constructor\n";
}
~AudioBook(){
cout << "AudioBook::~AudioBook() destructor!\n";
}
virtual void print() const {
cout << "AudioBook::print() -- I am a AudioBook object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << '\n'
<< "My narrator is: " << _narrator << endl;
}
const string& narrator() const { return _narrator; }
protected:
string _narrator;
};
void print(const LibMat &mat)
{
cout<<"in global print():about to print mat.print()\n";
mat.print();
//根据mat这个LibMat类类对象实际指向的对象,解析该
//执行哪一个类的print()成员函数
}
int main()
{
cout<<"\n"<<"Creating a LibMat object to print()\n";
LibMat libmat;//定义LibMat类对象同时由编译器自动调用该类默认构造函数
//来初始化这个类对象的数据成员
print(libmat);//在这个print(const LibMat&)函数中,mat.print()被解析为LibMat::print(),
//也就是会调用LibMat类的print()成员函数。输出指定的话到屏幕上(仅在这里)
cout<<"\n"<<"Creating a Book object to print()\n";
//注意,Book类是LibMat类的派生类,LibMat是基类,下面的AudioBook是Book类的派生类
Book b("The Castle","Franz Kafka");
//定义Book类对象同时由编译器自动调用该类带双参的构造函数
//来初始化这个类对象的数据成员
print(b);//在这个print(const LibMat&)函数中,mat.print()被解析为Book::print(),
//也就是会调用Book类的print()成员函数。输出指定的话到屏幕上(仅在这里)
cout<<"\n"<<"Creating an AudiBook = object to print()\n";
AudioBook ab("Man without Qualities","Robert Musil","Kenneth Meyer");
//定义AudioBook类对象同时由编译器自动调用该类带三参的构造函数
//来初始化这个类对象的数据成员
print(ab);
//在这个print(const LibMat&)函数中,mat.print()被解析为AudioBook::print()
}
虚函数引入
就着这个例子,我说明一下virtual关键字(虚函数)的作用以及代码执行流程。
首先我们知道这是一个三层的类体系。我们编译这个程序,首先从主函数开始执行,
cout<<"\n"<<"Creating a LibMat object to print()\n";
LibMat libmat;//定义LibMat类对象同时由编译器自动调用该类默认构造函数
//来初始化这个类对象的数据成员
print(libmat);//在这个print(const LibMat&)函数中,mat.print()被解析为LibMat::print(),
- 程序先输出一回车,然后输出一行话。紧接着定义了一个LibMat类类对象,对吧。然后自动调用这个类默认构造函数,给他的数据成员初始化一下子。
(截取部分代码)
class LibMat{
public:
LibMat()
{
cout<<"LibMat::LibMat() default constructor!\m";
//LibMat类的默认构造构造函数
}
//。。。
- 进入类默认构造函数,输出一行话证明LibMat类类对象的默认构造函数存在且已被执行。
然后出去这个构造函数。执行print()函数(这条语句)print(libmat);
print函数实现如下:
void print(const LibMat &mat)
{
cout<<"in global print():about to print mat.print()\n";
mat.print();
//根据mat这个LibMat类类对象实际指向的对象,解析该
//执行哪一个类的print()成员函数
}
- 输出一行话in global print(): about to print mat.print(),然后执行mat.print()函数(LibMat类的成员函数)
class LibMat{
public:
//...
virtual void print()const
{
cout<<"LibMat::print()--I am a LibMat object!\n";
//LibMat类的const成员函数且是虚函数
}
//。。。
-
注意这里用到了virtual!LibMat类的print()成员函数是虚函数!这里虚函数的作用体现的不是很明显,我们先往下执行程序,
mat.print();
调用函数要对号入座,输出这句话在屏幕上。结束。 -
然后执行LibMat类析构函数(为什么这里执行,应该是程序执行的原则,你可以认为是如果某类的类对象从内存中索取了内存空间给它的数据成员且接下来没他事不用它了(意味着类对象结束生命周期),那么析构函数就早早上场给它释放掉它所占的内存空间
-
tips:一个类有且只有一个析构函数。若未显式定义,系统会生成缺省的析构函数。
-
接着是这三行语句
cout<<"\n"<<"Creating a Book object to print()\n";
//注意,Book类是LibMat类的派生类,LibMat是基类,下面的AudioBook是Book类的派生类
Book b("The Castle","Franz Kafka");
//定义Book类对象同时由编译器自动调用该类带双参的构造函数
//来初始化这个类对象的数据成员
print(b);//在这个print(const LibMat&)函数中,mat.print()被解析为Book::print(),
输出一回车+一行话,然后定义Book类对象同时由编译器自动调用该类带双参的构造函数来初始化这个类对象的数据成员。Book类的类主体是什么样呢?来看一下
class Book : public LibMat {
public:
Book( const string &title, const string &author )
: _title( title ), _author( author ){
cout << "Book::Book( " << _title
<< ", " << _author << " ) constructor\n";
}
~Book(){
cout << "Book::~Book() destructor!\n";
}
virtual void print() const {
cout << "Book::print() -- I am a Book object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << endl;
}
const string& title() const { return _title; }
const string& author() const { return _author; }
protected:
string _title;
string _author;
};
如何定义派生类
class Book : public LibMat {
Book类是一个派生类,这是我们预先设想的,它的父类(基类)是LibMat类,所以,再定义Book类的时候,后面带一个:public 类名。所以
class 派生类(子类)类名:public 基类(父类)类名{
是定义并表明一个新的类(派生类)继承一个已存在的类(父类,基类)的格式。记住他!
public:
Book( const string &title, const string &author )
: _title( title ), _author( author ){
cout << "Book::Book( " << _title
<< ", " << _author << " ) constructor\n";
}
我们之前提到过定义了Book类类对象同时要调用Book类构造函数,但是,因为Book类是LibMat类的派生类,所以先要调用LibMat类构造函数,输出一句话。
LibMat()
{
cout<<"LibMat::LibMat() default constructor!\m";
//LibMat类的默认构造构造函数
}
再调用Book类构造函数!,传进来的两个实参"The Castle"和"Franz Kafka"初始化(成员逐一初始化)_title和_author这两个protected:
下的数据成员。
何谓protected:
这是一个关键字,protected:
的作用:被声明为protected的所有成员都可以被派生类(的类对象)直接访问!除了派生类之外,其余类什么的(类对象)都不可以直接访问protected成员!
- 好的,成员逐一初始化操作完毕后,输出Book::Book( The Castle, Franz Kafka ) constructor这句话到屏幕上,然后接着执行主函数的
print(b);
语句,
void print( const LibMat &mat )
{
cout << "in global print(): about to print mat.print()\n";
mat.print();
}
- 照常输出一句话,然后调用mat这个LibMat类类对象的print()成员函数,当然,这是我预先可以想到的,但是,程序却不是这样执行,他没有调用mat类对象的print()成员函数!而是调用了Book派生类里的public:里的print()成员函数! 即
virtual void print() const {
cout << "Book::print() -- I am a Book object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << endl;
}
- 为什么呢?在print(b);这里调用了非成员函数print(),在函数内执行了mat.print(),mat.print()会被解析为Book::print()!,这就是解析在程序运行时进行(即动态进行)!这就是关键字virtual在作怪!
虚函数(virtual)的作用
在某类的类成员函数定义/声明前加一个关键字virtual,
为了允许用基类的指针(隐含着的指针)来调用子类的这个函数。
为了效率,不是程序执行的效率,而是为了编码的效率。
另一种解释:
可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,
基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,
而不是基类中定义的成员函数(只要派生类改写了该成员函数)。
若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都
会调用基类中定义的那个函数。——关于C++中虚函数的作用
- OK,清楚了虚函数(virtual放在成员函数的定义/声明前)的作用,那我们可以解释一些现象。
- 执行了
mat.print();
因Book派生类的virtual void print()const;
所以调用了Book::print();
,在执行mat.print()之前先输出一句话:
in global print(): about to print mat.print()
然后输出了
Book::print() – I am a Book object!
My title is: The Castle
My author is: Franz Kafka
这些信息,紧接着,执行Book类的析构函数~Book(),注意! 只有LibMat基类的析构函数是虚函数(virtual ~LibMat(){//...};
),因为在派生类的类构造函数作用时首先调用了基类的构造函数,所以基类的析构函数我们让它是虚函数,这样我们就可以在派生类的析构函数作用完把派生类类对象的空间释放后让基类的析构函数作用,来对应上首先调用的基类的类构造函数! - 然后就到了下一个类的相关操作,AudioBook类,这个类是Book类的派生类,你看,一类套一类,对不对?派生类可以做基类啊也,Book类不就是?
class AudioBook : public Book {
public:
AudioBook( const string &title,
const string &author, const string &narrator )
: Book( title, author ), _narrator( narrator ){
cout << "AudioBook::AudioBook( " << _title
<< ", " << _author
<< ", " << _narrator
<< " ) constructor\n";
}
~AudioBook(){
cout << "AudioBook::~AudioBook() destructor!\n";
}
virtual void print() const {
cout << "AudioBook::print() -- I am a AudioBook object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << '\n'
<< "My narrator is: " << _narrator << endl;
}
const string& narrator() const { return _narrator; }
protected:
string _narrator;
};
- 主函数执行到这里
cout << "\n" << "Creating a AudioBook object to print()\n";
AudioBook ab( "Man Without Qualities", "Robert Musil", "Kenneth Meyer" );
print( ab );
依旧是输出一回车+一句话,然后定义AudioBook类对象并初始化ab类对象的数据成员。
AudioBook( const string &title,
const string &author, const string &narrator )
: Book( title, author ), _narrator( narrator ){
cout << "AudioBook::AudioBook( " << _title
<< ", " << _author
<< ", " << _narrator
<< " ) constructor\n";
}
- AudioBook类构造函数(三参数形式),然而,这里的构造函数采用成员逐一初始化操作,涉及到了AudioBook派生类的基类Book类的构造函数,注意这里涉及到了AudioBook派生类的基类Book类的构造函数是用来初始化Book类的protected下的数据成员(这些数据成员能被派生类(这里是AudioBook类)直接访问,还记得吗?在AudioBook类构造函数下面的cout就有所体现了!)而非是先调用哪个类的构造函数。AudioBook类构造函数在被调用前先调用Book类的构造函数,然后Book类的构造函数调用前先调用LibMat基类的构造函数,从而按顺序产生了下面的信息:
LibMat::LibMat() default constructor!
Book::Book( Man Without Qualities, Robert Musil ) constructor
AudioBook::AudioBook( Man Without Qualities, Robert Musil, Kenneth Meyer ) constructor
调用完LibMat类(基类)的构造函数和Book类(基类)的构造函数后,AudioBook类(派生类)的构造函数才开始执行!然后输出这个构造函数里cout<<后的信息。 - 接着
print( ab );
,先输出一句话。
in global print(): about to print mat.print() - 然后mat.print()被解析为AudioBook::print()函数,进入AudioBook类里的print()虚函数。 这下不难解释了mat这个基类存在着基类指针,通过传进非成员函数print()的类对象参数是哪个类的类对象来判别指向要调用的派生类或者基类自己。 然后执行AudioBook::print()函数,输出信息:
AudioBook::print() – I am a AudioBook object!
My title is: Man Without Qualities
My author is: Robert Musil
My narrator is: Kenneth Meyer - 注意,AudioBook::print()函数执行完后,程序执行即将进入尾声。AudioBook类的析构函数开始作用,然后是Book类的析构函数开始作用,然后是LibMat类的析构函数开始作用,为什么是这样呢?
是因为先调用的LibMat类的构造函数,然后是Book类的构造函数,然后是AudioBook类的构造函数,这就像是体现了程序栈(或说栈)如何工作,先进后出,最先调用的类的构造函数最后调用该类的析构函数
通过如下输出信息更好的解释这一现象!
AudioBook::~AudioBook() destructor!
Book::~Book() destructor!
LibMat::~LibMat() destructor!
总结
好的,程序到此执行完毕。
总体输出结果:
所以我们学习了三个知识点:
- 虚函数(关键字virtual)——让基类指针去指向其派生类从而调用派生类的某成员函数(这个派生类的某成员函数也一定是虚函数,基类的同名某成员函数也是个虚函数,注意,派生类要改写了该同名成员函数才可体现虚函数的作用,即让基类指针去指向派生类的同名成员函数从而调用这个派生类的同名成员函数。)
- 关键字protected——在protected:下定义的类数据成员,可以被其派生类(的类对象)直接访问,其他类类对象或其他变量/对象不不可直接访问protected:下定义的(基)类数据成员
- 派生类的构造函数作用时先调用基类的构造函数让其作用起来,派生类的析构函数先作用后再调用基类的析构函数让其作用起来,类似于栈的先进后出后进先出原则。这也是为什么
基类的析构函数(~类名(){//...;}
)前有virtual(即基类的析构函数是虚函数)的原因。因为派生类还要用到基类的析构函数,所以会有一个类成员函数指针指向基类的析构函数从而调用这个基类的析构函数!
补充关于基类virtual成员函数去掉virtual的分析
这个例子中,如果我把基类(LibMat)的print()函数的virtual去掉,派生类(Book,Audiobook)的print()(同名函数)的virtual保留,会发生什么?
去掉基类print函数的virtual前程序输出结果:
去掉基类print函数的virtual后程序输出结果:
发现这么几处不同:(前者为没去掉基类print函数的virtual的输出情况,后者为去掉基类print函数的virtual的输出情况
第一处:
第二处:
- 由程序输出结果可知,基类的同名成员函数如果没有在声明/定义前加virtual,
则在下面的情况下:(节选部分main()函数代码)
cout << "\n" << "Creating a Book object to print()\n";
Book b( "The Castle", "Franz Kafka" );
print( b );
cout << "\n" << "Creating a AudioBook object to print()\n";
AudioBook ab( "Man Without Qualities", "Robert Musil", "Kenneth Meyer" );
print( ab );
基类指针则不会调用派生类的print()同名函数,而是调用基类自身的print()同名函数!
-
如果基类的同名函数print()的virtual保留,而派生类的同名函数print()的virtual去掉,则不会发生上述情况,程序输出结果正常!
-
我把基类的析构函数的virtual去掉,派生类的析构函数本来就没有virtual(派生类的析构函数不是虚函数),则程序输出结果正常!
所以我们可以通过这个例子,更加深刻地理解虚函数的作用: