C++学习之路:虚函数与多态
summery:主要有以下几个内容
1.多态
2.静态绑定与动态绑定
3.虚函数
4.虚表指针
5.object slicing与虚函数
6.overload override overwrite的区别
@什么是多态性?
#多态是面向对象程序设计的重要特征之一
#多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为
#多态的实现: $函数重载 $运算符重载 $模板 $虚函数
前三个都属于静态绑定, 虚函数属于动态绑定
tip:
{静态绑定}:绑定过程出现在编译阶段,在编译阶段就已确定要调用的函数。
{动态绑定}:绑定过程工作在程序运行时执行,在程序运行时才确定要调用的函数。
@虚函数概念?
1.在基类冠以关键字virtual的成员函数
2.定义 virtual TYPE func(para1, para2);
如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数
3.只有通过基类指针或者引用调用虚函数才能引发动态绑定
4.虚函数不能声明为静态。
那么上段代码操作一下吧。
#include <iostream> #include <string> #include <vector> using namespace std; class animal{ public: virtual void bark() { cout << "animal::bark..bark" << endl; } virtual void eat() { cout << "animal::eat..food" << endl; } void play(){ cout << "animal::play.." << endl; } }; class dog : public animal { public: /*virtual*/ void bark(){ cout << "dog::wang.." << endl; } /*virtual*/ void eat(){ cout << "dog::eat..bone" << endl; } void play(){ cout << "dog::bite.." << endl; } }; int main(int argc, const char *argv[]) { animal* p; dog d; p = &d; p->bark(); p->eat(); p->play(); //Func3非虚函数,根据p指针实际类型来调用函数 return 0; }
结果打印:
arch% ./test
dog::wang..
dog::eat..bone
animal::play..
解读:
函数是否为虚,关系到基类指针是否会动态识别调用派生类该函数的实现;
实际上这样理解十分的绕,一个函数不为虚,那么指针(的类型)去决定调用哪个类的函数,一个函数为虚,那么由被类类型觉得调用的函数。
在c++语义上很好理解,基类中虚函数是希望被继承之后重写的。 例如dog类继承了animal类,bark()方法是一个虚方法,animal类时抽象的,没有意义,用animal指针去调用bark()(假设非虚),那么就调用的是基类的方法。只有动态绑定成dog类的方法,指针才能正确的调用bark方法,dog才会正确的叫。
申明为虚函数,目的就是为了让基类指针去识别该类,并调用正确的方法。
那么析构函数可以是设置为虚吗?
答案是可以的,那么有什么作用呢?
#include <iostream> #include <string> #include <vector> using namespace std; class animal{ public: virtual void bark() { cout << "animal::bark..bark" << endl; } virtual void eat() { cout << "animal::eat..food" << endl; } void play(){ cout << "animal::play.." << endl; } animal(){ cout << "animal" << endl; } /*virtual*/ ~animal(){ cout << "~animal" << endl; } }; class dog : public animal { public: /*virtual*/ void bark(){ cout << "dog::wang.." << endl; } /*virtual*/ void eat(){ cout << "dog::eat..bone" << endl; } void play(){ cout << "dog::bite.." << endl; } dog(){ cout << "dog"<< endl; } virtual ~dog(){ cout << "~dog" << endl; } }; int main(int argc, const char *argv[]) { animal* p; dog d; p = &d; p->bark(); p->eat(); p->play(); //Func3非虚函数,根据p指针实际类型来调用函数 delete p; return 0; }
如果基类的析构函数没有设置为虚函数,在多重继承后,可能会出现这样一种情况,我们调用基类指针 去释放一个派生类,这个派生类可能掌握一些资源,但是却没有正确的调用派生类的析构函数,仅仅调用了基类的析构函数,那么就会内存泄露,memeroy leek。
@动态绑定是如何实现的($与虚基类表不同)
1.虚函数的动态绑定是通过虚表来实现的。
2.包含虚函数的类头4个字节存放指向虚表的指针
上一篇文章我们介绍了继承的内存结构中,类首地址会存在一个虚函数指针,这个指针的作用就是用来实现动态绑定的。
基类的虚函数结构如上所示。
我们写段小程序验证一下:
#include <iostream> #include <string> #include <vector> using namespace std; class Base{ public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } int data1_; }; class Dervied : public Base { public: /*virtual*/ void func1(){ cout << "Dervied::func1" << endl; } void Fun3(){ cout << "Dervied::func3" << endl; } int data2_; }; typedef void (*FUNC)(); int main(int argc, const char *argv[]) { cout << sizeof(Base) << endl; cout << sizeof(Dervied) << endl; Base b; long** p = (long**)&b; FUNC fun = (FUNC)p[0][0]; fun(); fun = (FUNC)p[0][1]; fun(); return 0; }
结果打印:
16 16 Base::func1 Base::func2
虚函数表结果如我们所推测一致。
上述内存打印不对,是因为gcc升级到了5.2的版本,编译器为我们做了优化。
我重新用4.6的版本编译一次。
➜ cpp gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/4.6/lto-wrapper Target: i686-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.3-1ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --enable-targets=all --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu Thread model: posix gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) ➜ cpp ➜ cpp ./test2 8 12 Base::func1 Base::func2
结果打印就正确了
我们在看一下派生类的情况(应该是dervied)
派生类的虚函数表应该如上图所示。
代码如下,不过增添几行派生类的测试代码。
#include <iostream> #include <string> #include <vector> using namespace std; class Base{ public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } int data1_; }; class Dervied : public Base { public: /*virtual*/ void func2(){ cout << "Dervied::func2" << endl; } virtual void Fun3(){ cout << "Dervied::func3" << endl; } int data2_; }; typedef void (*FUNC)(); int main(int argc, const char *argv[]) { cout << sizeof(Base) << endl; cout << sizeof(Dervied) << endl; Base b; long** p = (long**)&b; FUNC fun = (FUNC)p[0][0]; fun(); fun = (FUNC)p[0][1]; fun(); Dervied d; p = (long**)&d; fun = (FUNC)p[0][0]; fun(); fun = (FUNC)p[0][1]; fun(); fun = (FUNC)p[0][2]; fun(); return 0; }
结果打印:
8 12 Base::func1 Base::func2 Base::func1 Dervied::func2 Dervied::func3
结果和我们推测是一致的。
@基类指针如何通过虚函数表找到正确的调用?
例如:
Base* pp = &dd;
pp->Fun2();
pp首先找到【派生类】首地址,找到虚表指针,再利用虚表指针找到虚函数表.
再虚函数表中找到Fun2.这个入口编译期间是找不到入口的,是运行时决定的。这是动态绑定与静态绑定本质的区别。
如果是d.Func2();这种调用任然还是编译期间决定的静态绑定。
静态函数是不能申明为虚的,因为它根本就不是成员函数,不存在list指针。
@object slicing与虚函数
我们看一个例子
#include <iostream> #include <string> #include <vector> using namespace std; class CObject { public: virtual void Serialize(){ cout << "CObject::Serialize.." << endl; } }; class CDocument: public CObject { public: int data1_; void func(){ cout << "Document::func.." << endl; Serialize(); } virtual void Serialize(){ cout << "Document::Serialize.." << endl; } }; class CMydoc: public CDocument { public: int data2_; virtual void Serialize(){ cout << "CMydoc::Serialize.." << endl; } }; int main(int argc, const char *argv[]) { CMydoc mydoc; CMydoc* pmydoc = new CMydoc; cout << "#1 testing\n" << endl; mydoc.func(); cout << "#2 testing\n" << endl; ((CDocument*)(&mydoc))->func(); cout << "#3 testing\n" << endl; pmydoc->func(); cout << "#4 testing\n" << endl; ((CDocument)mydoc).func(); //将mydoc对象强制转换为CDocument对象 //进行了向上转型 return 0; }
结果打印:
#1 testing Document::func.. CMydoc::Serialize.. #2 testing Document::func.. CMydoc::Serialize.. #3 testing Document::func.. CMydoc::Serialize.. #4 testing Document::func.. Document::Serialize..
解读:
1.CMydoc没有定义自己的func函数,也就是没有重载,那么它调用的是基类CDocument的func,
func里又调用Serialize方法(虚函数),所以是从虚函数表里找到的,所以调用自己的Serialize方法。
2.第二种,相当于用基类指针指向派生类,一样的情况:调用func,没用重载,调用基类func,func中又调用Serialize,发现是虚函数,再去这个类的虚函数表找到Serialize方法
3.和2同理
4.发生截断:由派生类强制转换(向上转换)成基类,这种情况会导致完完全全将派生类转化为基类对象,
包括虚函数表(可能是编译器优化导致,尚不明确)
所以这个时候调用func,这个时候是基类的虚表,所以找到是基类(DOcument)的虚表,所以调用基类的Serialize的方法。
为了看的更清楚,我们对代码稍作更改。
#include <iostream> #include <string> #include <vector> using namespace std; class CObject { public: virtual void Serialize(){ cout << "CObject::Serialize.." << endl; } }; class CDocument: public CObject { public: int data1_; void func(){ cout << "Document::func.." << endl; Serialize(); } CDocument(){ cout << "CD" << endl; } CDocument(const CDocument&){ cout << "copy" << endl; } virtual void Serialize(){ cout << "Document::Serialize.." << endl; } }; class CMydoc: public CDocument { public: int data2_; virtual void Serialize(){ cout << "CMydoc::Serialize.." << endl; } }; int main(int argc, const char *argv[]) { CMydoc mydoc; CMydoc* pmydoc = new CMydoc; cout << "#1 testing\n" << endl; mydoc.func(); cout << "#2 testing\n" << endl; ((CDocument*)(&mydoc))->func(); cout << "#3 testing\n" << endl; pmydoc->func(); cout << "#4 testing\n" << endl; ((CDocument)mydoc).func(); //将mydoc对象强制转换为CDocument对象 //进行了向上转型 return 0; }
我们给CD这个类加了一个拷贝构造函数。
结果打印:
#1 testing Document::func.. CMydoc::Serialize.. #2 testing Document::func.. CMydoc::Serialize.. #3 testing Document::func.. CMydoc::Serialize.. #4 testing copy Document::func.. Document::Serialize..
我们发现进行了拷贝,一切都明白了。
第四种情况会导致对象完完全全的向上转型(安全的)。编译器默默地做出了奉献。
最后总结一下:
大概是一下几点: