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..

我们发现进行了拷贝,一切都明白了。

第四种情况会导致对象完完全全的向上转型(安全的)。编译器默默地做出了奉献。

 

最后总结一下:

三者有何区别

大概是一下几点:

时间: 2024-12-19 09:16:57

C++学习之路:虚函数与多态的相关文章

C++学习研究之虚函数、多态的实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数.2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的.3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性.4. 多态用虚函数来实现,结合动态绑定.5. 纯虚函数是虚函数再加上= 0.6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()=0:即抽象类!必须在子类实现这个函数!

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr).C++对象模型 一.虚函数表指针(vptr)及虚基类表指针(bptr) C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括: virtual function机制:用以支持一个有效率的"执行期绑定": virtual base class:用以实现多次在继承体系中的基类,有一个单一而被共享的实体. 1.虚函数表指针 C++中,有两种数据

C++学习笔记--从虚函数说开去

虚函数与纯虚函数: 虚函数:在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数,virtual  函数返回类型  函数名(参数表){函数体;} ,实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数.注意虚函数在基类中是有定义的,即便定义是空. 纯虚函数:在基类中是没有定义的,必须由派生类重定义实现,否则不能由对象进行调用. 看下面的例子: #include<iostream> using namespace std; class Cshape { p

C++ Primer Plus学习笔记之虚函数

C++ Primer Plus学习笔记之虚函数 C++语言的多态性有两种类型:静态多态性和动态多态性.函数重载和运算符重载就是静态多态性的具体表现,而动态多态性是指程序运行过程中才动态的确定操作所针对的对象,它是通过虚函数实现的: 1,虚函数的概念: 一个指向基类的指针可用来指向从基类派生的任何对象,这样就可以达到一个接口多个实现的访问了:虚函数是在基类中被冠以virtual的成员函数,它提供了一种接口界面.虚函数可以在一个或者多个派生类中被重新定义,但要求在派生类中从新定义时,虚函数的函数原型

你好,C++(37)上车的人请买票!6.3.3 用虚函数实现多态

6.3.3  用虚函数实现多态 在理解了面向对象的继承机制之后,我们知道了在大多数情况下派生类是基类的“一种”,就像“学生”是“人”类中的一种一样.既然“学生”是“人”的一种,那么在使用“人”这个概念的时候,这个“人”可以指的是“学生”,而“学生”也可以应用在“人”的场合.比如可以问“教室里有多少人”,实际上问的是“教室里有多少学生”.这种用基类指代派生类的关系反映到C++中,就是基类指针可以指向派生类的对象,而派生类的对象也可以当成基类对象使用.这样的解释对大家来说是不是很抽象呢?没关系,可以

linux学习之路之函数的使用

什么是函数呢? 函数就是将一些代码封装起来,实现代码重用的功能 函数是不能独立运行的,只有函数调用时才可以执行,而且可以调用多次 使用函数的好处可以避免代码多次重复使用,是程序代码更具有结构性. 那么该如何定义函数呢? 定义函数有二种结构 结构一: function FUNCNAME { COMMAND } 其中function为关键字,FUNCNAME为函数名,COMMAND为函数体内执行的命令 结构二: FUNCNAME() { COMMAND } 其中FUNCNAME为函数名 我们说过函数

C++学习笔记27,虚函数的工作原理

C++规定了虚函数的行为,但是将实现交给了编译器的作者. 通常,编译器处理虚函数的方法是给每一个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址. 例如:基类对象包含一个指针,该指针指向基类的虚函数表. 派生类对象包含一个指针,该指针指向一个独立的虚函数表.如果派生类提供了虚函数的新定义,虚函数表将保存新的函数地址. 如果派生类没有重新定义虚函数,虚函

C++中虚函数和多态

1.C++中的虚函数 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Ta