C++的面向对象思想博大精深,现在到了面向对象的多态性。
强烈推荐:C++编程思想(第2版) 第1卷 标准C++引导 第15章 多态性和虚函数
我觉得这章讲得很清楚,包括编译器对虚函数的实现,还有C++的多态性,都挺不错的。看书就得取我所需,以免浪费太多时间。
一、多态性
多态性是指不同类型的对象接受相同的消息时产生不同的行为。在C++中,多态性可以分为两种:编译时的多态性和运行时的多态性。
编译时的多态性:函数的重载或运算符的重载;
运行时的多态性:虚函数来实现。
函数的重载和运算符的重载将在后面的博文中讨论,本文主要是讨论虚函数的使用。
二、动态绑定(晚捆绑)
动态绑定这词比较专业,在《C++ Primer中文版》书中看到。简单的说,动态绑定就是在程序运行过程中才绑定到具体的函数,编译器在编译过程中还无法确定要调用的具体类型对象的函数,所以只能动态绑定。
在C++中,要触发动态绑定,必须满足2个条件:
第一:只有指定为虚函数的成员函数才能进行动态绑定,使用virtual关键字修饰;
第二:必须通过基类类型的引用或指针进行函数调用。
这2点必须记住,非常重要的两点,下文会使用一个例子来说明,但在看例子之前,先讨论一下基类与派生类之间的转换。
三、派生类到基类的转换(将派生类转换为基类)
将派生类转换为基类是比较好理解的,因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,或者用指向基类的指针指向派生类对象。
1.引用转换不同于转换对象
a.引用转换:引用直接绑定到该对象;
b.对象转换:使用派生类对象的基类部分对基类对象进行初始化和赋值,派生类部分被切掉,简称"对象切片"。
2.派生类到基类转换的可访问性
派生类到基类的转换可能不可访问,具体规则如下(当然这是不太准确的,只是稍微提一下,具体在《C++ Primer中文版》书中):
public继承:可以转换
private或protected继承:不可转换
四、基类与派生类的转换(将基类转换为派生类)
就一句话:从基类到派生类的自动转换时不存在的,也是不安全的。
原因:基类对象只能是基类对象,它不能包含派生类型的成员。
五、再谈动态绑定
结合以上几点,写个小例子就能很清楚的理解多态性的概念和使用。一般我写例子都是小儿科级别的,只是体现一下概念和使用方法。
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase
{
public:
virtual void func() const
{
cout << "CBase::func" << endl;
}
};
class CDerived : public CBase
{
public:
void func() const
{
cout << "CDerived::func" << endl;
}
};
void test(CBase &obj)
{
obj.func();
}
int main()
{
CBase base;
CDerived derived;
test(base);
test(derived);
return 0;
}
执行结果:
CBase::func
CDerived::func
程序要求两点:
第一:只有指定为虚函数的成员函数才能进行动态绑定,使用virtual关键字修饰;
论证:
如果去掉基类func函数的virtual声明,那么执行结果:
CBase::func
CBase::func
第二:必须通过基类类型的引用或指针进行函数调用。
论证:
如果将"void test(CBase &obj)"改成"void test(CBase obj)",不使用引用,那么执行结果:
CBase::func
CBase::func
所以上述两个条件必须满足,才能够使用虚函数这一特性。使用虚函数,效率肯定会比较低,因为调用函数得需要2步,根据具体的对象的引用或指针找到实际的对象类型,然后再计算虚函数表中要调用的函数地址。但C++的成员函数默认不是虚函数,只能通过这两个条件来触发,所以给程序员提供了灵活性。
如果将基类中的成员函数声明为虚函数,那么使用基类的引用或指针来传递基类或派生类的对象时,将会根据引用或指针实际指向的对象类型来调用相应类中的函数。
再啰嗦:C++编程思想(第2版) 第1卷 标准C++引导 第15章 多态性和虚函数 这章介绍得很清楚。
六、虚析构函数
如果用基类类型的引用或指针绑定到派生类的对象,那么基类的析构函数将只能调用到基类自身的析构函数。但如果把基类中的析构函数也声明为虚函数,那么情况就完全不同了,基类的虚析构函数根据基类类型的引用或指针实际绑定的对象类型来调用析构函数。
在基类的析构函数声明为虚函数的情况下,若基类的引用绑定到派生类的对象,则在销毁对象的时刻,基类将会动态绑定到派生类对象,调用派生类的析构函数进行清除工作,随后再调用自身的析构函数。
来个小例子,就能看出区别了:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase1
{
public:
~CBase1()
{
cout << "CBase1::~Cbase1()" << endl;
}
};
class CDerived1 : public CBase1
{
public:
~CDerived1()
{
cout << "CDerived1::~CDerived1()" << endl;
}
};
class CBase2
{
public:
virtual ~CBase2()
{
cout << "CBase2::~Cbase2()" << endl;
}
};
class CDerived2 : public CBase2
{
public:
~CDerived2()
{
cout << "CDerived2::~CDerived2()" << endl;
}
};
int main()
{
CBase1 *base1 = new CDerived1;
cout << "析构函数" << endl;
delete base1;
CBase2 *base2 = new CDerived2;
cout << "虚析构函数" << endl;
delete base2;
return 0;
}
执行结果:
析构函数
CBase1::~Cbase1()
虚析构函数
CDerived2::~CDerived2()
CBase2::~Cbase2()
建议:如果要封装一个基类,最好将基类的析构函数声明为虚析构函数,以免后续的操作造成内存泄露。
七、纯虚函数和抽象类
在定义一个基类时,有时无法确定基类中虚函数的具体实现,那么就可以将虚函数声明纯虚函数。声明纯虚函数的格式:
virtual <函数类型> <函数名>(<形参表>) = 0;
抽象类:指至少包含一个纯虚函数的类,本身不能被实例化,称为抽象类。
纯虚函数的一些特性:
1.含有纯虚函数的类(抽象类),不能实例化对象;
2.纯虚函数有没有定义函数体都是允许的;
3.由抽象类派生出来的派生类,必须对基类中的纯虚函数重定义(重写)。
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CShape
{
public:
virtual void Show() = 0;
};
class CTriangle : public CShape
{
public:
void Show()
{
cout << "CTriangle::Show()" << endl;
}
};
class CCirrcle : public CShape
{
public:
void Show()
{
cout << "CCirrcle::Show()" << endl;
}
};
int main()
{
CShape *base1 = new CTriangle;
CShape *base2 = new CCirrcle;
base1->Show();
base2->Show();
return 0;
}
执行结果:
CTriangle::Show()
CCirrcle::Show()
八、纯虚析构函数
纯虚析构函数与纯虚函数有点区别:
1.必须为纯虚析构函数提供一个函数体;
2.由抽象类派生出来的派生类,可以不必重定义基类中的纯虚析构函数。
第1点的理由:
我们都知道,在类销毁进行析构的期间,会调用类的析构函数。如果不对一个纯虚析构函数进行定义,在析构函数就调用不到析构函数体了。
第2点的理由:
纯虚函数在派生类中必须要重新定义,但对于纯虚析构函数,在派生类中可以不必重定义??原因是:编译器如果发现一个类当中没有显示声明构造函数或析构函数,编译器将会自动的生成一个默认版本,所以如果用户没有重定义纯虚析构函数,编译器会帮用户生成一个。但这里还得注意几个细节,得写个程序来调调才知道纯虚析构函数到底有多纯?
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CAbstractBase
{
public:
virtual ~CAbstractBase() = 0;
};
CAbstractBase::~CAbstractBase()
{
cout << "请务必定义基类的纯虚析构函数" << endl;
}
class Derived : public CAbstractBase
{
};
int main()
{
Derived d;
return 0;
}
执行结果:
请务必定义基类的纯虚析构函数
举一反三:如果将基类中~CAbstractBase()的定义去掉,那么由CAbstractBase派生出来的Derived也将是抽象类,Derived d;声明就会失败。只有给基类的纯虚析构函数提供了函数体,Derived派生类才能够实例化对象。
九、虚函数其他的一些特性
1.C++的虚机制在构造函数和析构函数不工作
在类的构造函数,和非虚析构函数或许析构函数,C++的虚机制都不工作,就算调用了虚函数,也只能调用到本地版本。
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase
{
public:
CBase()
{
cout << "构造函数调用虚函数" << endl;
func();
}
virtual void func()
{
cout << "本地版本: CBase::func()" << endl;
}
~CBase()
{
cout << "析构函数调用虚函数" << endl;
func();
}
};
class CDerived : public CBase
{
public:
void func()
{
cout << "重写版本: CDerived::func()" << endl;
}
};
int main()
{
CBase *base = new CDerived;
cout << endl;
delete base;
return 0;
}
执行结果:
构造函数调用虚函数
本地版本: CBase::func()
析构函数调用虚函数
本地版本: CBase::func()
如果将基类的析构函数声明为虚析构函数,结果还是一样。
在博文,我只是稍微总结一下,这当然是非常浅的。不过我在学习C++面向对象这一部分,或者其他部分,都是结合好几本书的内容一起看,只有看我需要的那部分,这里的书包括:《C++程序设计语言_特别版》,《C++ Primer中文版(第4版)》,《C++编程思想(第2版)_第1卷_标准C++引导》,《C++入门经典(第3版)》。我这些书全是电子版的,都是在大家论坛下载的,大家论坛的资源真是数不甚数。因为看书我不可能把一整本书全看,所以买书对我来说很浪费。再者每本书都有自己的优缺点,只能取其精华。
强烈推荐:C++编程思想(第2版) 第1卷 标准C++引导 第15章 多态性和虚函数
我觉得这章讲得很清楚,包括编译器对虚函数的实现,还有C++的多态性,都挺不错的。看书就得取我所需,以免浪费太多时间。
一、多态性
多态性是指不同类型的对象接受相同的消息时产生不同的行为。在C++中,多态性可以分为两种:编译时的多态性和运行时的多态性。
编译时的多态性:函数的重载或运算符的重载;
运行时的多态性:虚函数来实现。
函数的重载和运算符的重载将在后面的博文中讨论,本文主要是讨论虚函数的使用。
二、动态绑定(晚捆绑)
动态绑定这词比较专业,在《C++ Primer中文版》书中看到。简单的说,动态绑定就是在程序运行过程中才绑定到具体的函数,编译器在编译过程中还无法确定要调用的具体类型对象的函数,所以只能动态绑定。
在C++中,要触发动态绑定,必须满足2个条件:
第一:只有指定为虚函数的成员函数才能进行动态绑定,使用virtual关键字修饰;
第二:必须通过基类类型的引用或指针进行函数调用。
这2点必须记住,非常重要的两点,下文会使用一个例子来说明,但在看例子之前,先讨论一下基类与派生类之间的转换。
三、派生类到基类的转换(将派生类转换为基类)
将派生类转换为基类是比较好理解的,因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,或者用指向基类的指针指向派生类对象。
1.引用转换不同于转换对象
a.引用转换:引用直接绑定到该对象;
b.对象转换:使用派生类对象的基类部分对基类对象进行初始化和赋值,派生类部分被切掉,简称"对象切片"。
2.派生类到基类转换的可访问性
派生类到基类的转换可能不可访问,具体规则如下(当然这是不太准确的,只是稍微提一下,具体在《C++ Primer中文版》书中):
public继承:可以转换
private或protected继承:不可转换
四、基类与派生类的转换(将基类转换为派生类)
就一句话:从基类到派生类的自动转换时不存在的,也是不安全的。
原因:基类对象只能是基类对象,它不能包含派生类型的成员。
五、再谈动态绑定
结合以上几点,写个小例子就能很清楚的理解多态性的概念和使用。一般我写例子都是小儿科级别的,只是体现一下概念和使用方法。
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase
{
public:
virtual void func() const
{
cout << "CBase::func" << endl;
}
};
class CDerived : public CBase
{
public:
void func() const
{
cout << "CDerived::func" << endl;
}
};
void test(CBase &obj)
{
obj.func();
}
int main()
{
CBase base;
CDerived derived;
test(base);
test(derived);
return 0;
}
执行结果:
CBase::func
CDerived::func
程序要求两点:
第一:只有指定为虚函数的成员函数才能进行动态绑定,使用virtual关键字修饰;
论证:
如果去掉基类func函数的virtual声明,那么执行结果:
CBase::func
CBase::func
第二:必须通过基类类型的引用或指针进行函数调用。
论证:
如果将"void test(CBase &obj)"改成"void test(CBase obj)",不使用引用,那么执行结果:
CBase::func
CBase::func
所以上述两个条件必须满足,才能够使用虚函数这一特性。使用虚函数,效率肯定会比较低,因为调用函数得需要2步,根据具体的对象的引用或指针找到实际的对象类型,然后再计算虚函数表中要调用的函数地址。但C++的成员函数默认不是虚函数,只能通过这两个条件来触发,所以给程序员提供了灵活性。
如果将基类中的成员函数声明为虚函数,那么使用基类的引用或指针来传递基类或派生类的对象时,将会根据引用或指针实际指向的对象类型来调用相应类中的函数。
再啰嗦:C++编程思想(第2版) 第1卷 标准C++引导 第15章 多态性和虚函数 这章介绍得很清楚。
六、虚析构函数
如果用基类类型的引用或指针绑定到派生类的对象,那么基类的析构函数将只能调用到基类自身的析构函数。但如果把基类中的析构函数也声明为虚函数,那么情况就完全不同了,基类的虚析构函数根据基类类型的引用或指针实际绑定的对象类型来调用析构函数。
在基类的析构函数声明为虚函数的情况下,若基类的引用绑定到派生类的对象,则在销毁对象的时刻,基类将会动态绑定到派生类对象,调用派生类的析构函数进行清除工作,随后再调用自身的析构函数。
来个小例子,就能看出区别了:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase1
{
public:
~CBase1()
{
cout << "CBase1::~Cbase1()" << endl;
}
};
class CDerived1 : public CBase1
{
public:
~CDerived1()
{
cout << "CDerived1::~CDerived1()" << endl;
}
};
class CBase2
{
public:
virtual ~CBase2()
{
cout << "CBase2::~Cbase2()" << endl;
}
};
class CDerived2 : public CBase2
{
public:
~CDerived2()
{
cout << "CDerived2::~CDerived2()" << endl;
}
};
int main()
{
CBase1 *base1 = new CDerived1;
cout << "析构函数" << endl;
delete base1;
CBase2 *base2 = new CDerived2;
cout << "虚析构函数" << endl;
delete base2;
return 0;
}
执行结果:
析构函数
CBase1::~Cbase1()
虚析构函数
CDerived2::~CDerived2()
CBase2::~Cbase2()
建议:如果要封装一个基类,最好将基类的析构函数声明为虚析构函数,以免后续的操作造成内存泄露。
七、纯虚函数和抽象类
在定义一个基类时,有时无法确定基类中虚函数的具体实现,那么就可以将虚函数声明纯虚函数。声明纯虚函数的格式:
virtual <函数类型> <函数名>(<形参表>) = 0;
抽象类:指至少包含一个纯虚函数的类,本身不能被实例化,称为抽象类。
纯虚函数的一些特性:
1.含有纯虚函数的类(抽象类),不能实例化对象;
2.纯虚函数有没有定义函数体都是允许的;
3.由抽象类派生出来的派生类,必须对基类中的纯虚函数重定义(重写)。
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CShape
{
public:
virtual void Show() = 0;
};
class CTriangle : public CShape
{
public:
void Show()
{
cout << "CTriangle::Show()" << endl;
}
};
class CCirrcle : public CShape
{
public:
void Show()
{
cout << "CCirrcle::Show()" << endl;
}
};
int main()
{
CShape *base1 = new CTriangle;
CShape *base2 = new CCirrcle;
base1->Show();
base2->Show();
return 0;
}
执行结果:
CTriangle::Show()
CCirrcle::Show()
八、纯虚析构函数
纯虚析构函数与纯虚函数有点区别:
1.必须为纯虚析构函数提供一个函数体;
2.由抽象类派生出来的派生类,可以不必重定义基类中的纯虚析构函数。
第1点的理由:
我们都知道,在类销毁进行析构的期间,会调用类的析构函数。如果不对一个纯虚析构函数进行定义,在析构函数就调用不到析构函数体了。
第2点的理由:
纯虚函数在派生类中必须要重新定义,但对于纯虚析构函数,在派生类中可以不必重定义??原因是:编译器如果发现一个类当中没有显示声明构造函数或析构函数,编译器将会自动的生成一个默认版本,所以如果用户没有重定义纯虚析构函数,编译器会帮用户生成一个。但这里还得注意几个细节,得写个程序来调调才知道纯虚析构函数到底有多纯?
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CAbstractBase
{
public:
virtual ~CAbstractBase() = 0;
};
CAbstractBase::~CAbstractBase()
{
cout << "请务必定义基类的纯虚析构函数" << endl;
}
class Derived : public CAbstractBase
{
};
int main()
{
Derived d;
return 0;
}
执行结果:
请务必定义基类的纯虚析构函数
举一反三:如果将基类中~CAbstractBase()的定义去掉,那么由CAbstractBase派生出来的Derived也将是抽象类,Derived d;声明就会失败。只有给基类的纯虚析构函数提供了函数体,Derived派生类才能够实例化对象。
九、虚函数其他的一些特性
1.C++的虚机制在构造函数和析构函数不工作
在类的构造函数,和非虚析构函数或许析构函数,C++的虚机制都不工作,就算调用了虚函数,也只能调用到本地版本。
来个例子:
[cpp] view plaincopy
#include <iostream>
using namespace std;
class CBase
{
public:
CBase()
{
cout << "构造函数调用虚函数" << endl;
func();
}
virtual void func()
{
cout << "本地版本: CBase::func()" << endl;
}
~CBase()
{
cout << "析构函数调用虚函数" << endl;
func();
}
};
class CDerived : public CBase
{
public:
void func()
{
cout << "重写版本: CDerived::func()" << endl;
}
};
int main()
{
CBase *base = new CDerived;
cout << endl;
delete base;
return 0;
}
执行结果:
构造函数调用虚函数
本地版本: CBase::func()
析构函数调用虚函数
本地版本: CBase::func()
如果将基类的析构函数声明为虚析构函数,结果还是一样。
在博文,我只是稍微总结一下,这当然是非常浅的。不过我在学习C++面向对象这一部分,或者其他部分,都是结合好几本书的内容一起看,只有看我需要的那部分,这里的书包括:《C++程序设计语言_特别版》,《C++ Primer中文版(第4版)》,《C++编程思想(第2版)_第1卷_标准C++引导》,《C++入门经典(第3版)》。我这些书全是电子版的,都是在大家论坛下载的,大家论坛的资源真是数不甚数。因为看书我不可能把一整本书全看,所以买书对我来说很浪费。再者每本书都有自己的优缺点,只能取其精华。