C++多态

多态

虚函数的概念

  • 作用:在 公有继承层次中的一个或多个派生类中对虚函数进行 重定义。当派生类的对象 使用它基类的指针(或引用)调用虚函数时,将调用 该对象的成员函数。
  • 当使用类的指针调用成员函数时,普通函数由指针类型(引用类型)决定,而虚函数由指针指向的实际类型(引用的的实际类型)决定
  • 虚函数的实现过程: 通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。
class Animal
{
public :
        virtual void eat()
       {
              cout << "I like food!" << endl;
       }
};
class Cat : public Animal
{
public :
        void eat()
       {
              cout << "I like fishes!" << endl;
       }
};
void whatyouprefer( Animal & animal )
{
        animal .eat();
}
int main()
{
        Cat cat;
       whatyouprefer(cat);
        return 0;
}
输出: I like fishes!
class Animal
{
public :
         void eat()
       {
              cout << "I like food!" << endl;
       }
};
class Cat : public Animal
{
public :
        void eat()
       {
              cout << "I like fishes!" << endl;
       }
};
void whatyouprefer( Animal & animal )
{
        animal .eat();
}
int main()
{
        Cat cat;
       whatyouprefer(cat);
        return 0;
}
输出: I like food!
class Animal
{
public :
        virtual void eat()
       {
              cout << "I like food!" << endl;
       }
};
class Cat : public Animal
{
public :
        void eat()
       {
              cout << "I like fishes!" << endl;
       }
};
void whatyouprefer( Animal  animal )
{
        animal .eat();
}
int main()
{
        Cat cat;
       whatyouprefer(cat);
        return 0;
}
输出: I like food!
“一旦为虚,永远为虚”
• 含义1:在公有继承层次中,某个类的成员函数声明为 virtual。则其直接或间接派生类中的 相同签名(返回类型/函数名/参数)的函数,都是 虚函数。不论是否用关键字virtual 再次声明。值得注意的是, 虚下不虚上,
若一个函数在派生过程中变成了虚函数,则其基类的相同签名的函数依旧是普通成员函数。
• 含义2:任何虚函数,都有可能被其派生类再次重新定义,重新定义后依然是虚函数。因此, 虚函数既可以被继承(派生类中没有与其相同签名的成员函数),也可以被重新定义。

虚函数的实现(内存布局)

   虚函数表中只存有一个虚函数的指针地址, 存放普通函数或是构造函数的指针地址。 只要有虚函数,C++类都会存在这样的一张虚函数表 ,不管是普通虚函数亦或是纯虚函数,亦或是派生类中隐式声明的这些虚函数都会生成这张虚函数表。
  虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置 这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。  这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数 虚函数表其实就是函数指针的地址。
类只是限定了静态函数的可见性,基类和派生类共享一个静态函数,无法被重载和覆盖。
友元函数不是成员函数,只有成员函数才可以是virtual,因此友元函数不可以是虚函数,但是可以通过让友元函数对虚函数的调用来解决友元函数的虚拟问题。
类外定义虚函数只能在声明虚函数时在虚函数前面加上一个virtual关键字,在类外定义时,此时就不能在其前面加上virtual关键字

1、无继承情况

class Base
{
public :
    Base() { cout << "Base construct" << endl; }
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
    virtual ~Base() { cout << "haha" << endl; }
};
int main()
{
    typedef void (* Fun )();  //定义一个函数指针类型变量类型 Fun
    Base * b = new Base ();
    //虚函数表存储在对象最开始的位置
    //将对象的首地址输出
    cout << "首地址:" << *( int *)(&b) << endl;
    //地址内的值 即为函数指针的地址,将函数指针的地址存储在了虚函数表中了
     Fun funf = ( Fun )(*( char **)*( char ***)b);
    Fun fung = ( Fun )(*(( int **)*( int ***)b + 1));
    Fun funh = ( Fun )(*(( int **)*( int ***)b + 2));
    //其中int可以换成任何其他的类型,
    //只是可以让编译器根据实际情况选取指针的字节数,
    //从而在+1、+2时能够偏移到正确的地址,
    //*(int*)b相当于把b当成指向int类型的指针,
    //在b所指的地址内取sizeof(int)个字节的数据,标以int型。
    funf();
    fung();
    funh();
    //最后一个位置为0 表明虚函数表结束 +4是因为定义了一个虚析构函数
    cout << ( Fun )(*(( int **)*( int ***)b + 4)) << endl;
    delete b;
    return 0;
}
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“ /0 ”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在 WinXP+VS2003 下,这个值是 NULL 。而在 Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3 下,这个值是如果 1 ,表示还有下一个虚函数表,如果值是 0 ,表示是最后一个虚函数表。

2、单继承情况(无虚函数覆盖)

  假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
【Note】:

3、单继承情况(有虚函数覆盖)

  • 虚函数按照其声明顺序放于表中
  • 父类的虚函数在子类的虚函数前面
   覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
【Note】:
  • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置
  • 没有被覆盖的函数依旧在原来的位置
这样,我们就可以看到对于下面这样的程序,
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代 ,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

4、多重继承情况(无虚函数覆盖)

  下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
【Note】:
  • 每个父类都有自己的虚表(有几个基类就有几个虚函数表)
  • 子类的成员函数被放到了第一个父类的表中 。(所谓的第一个父类是 按照声明顺序来判断的 )。

5、多重继承情况(有虚函数覆盖)

  下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:

我们可以看见, 三个父类虚函数表中的f()的位置被替换成了子类的函数指针 。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

虚函数的相关问题

1、构造函数为什么不能定义为虚函数

   构造函数不能是虚函数。
   首先,我们已经知道 虚函数的实现则是通过对象内存中的vptr来实现的 。而 构造函数是用来实例化一个对象的 ,通俗来讲就是为对象内存中的值做初始化操作。 那么在构造函数完成之前,vptr是没有值的 ,也就无法通过vptr找到作为虚函数的构造函数所在的代码区。

2、析构函数为什么要定义为虚函数?

   析构函数可以是虚函数且推荐最好设置为虚函数。
class B
{
public :
    B() { printf( "B()\n" ); }
    virtual ~B() { printf( "~B()\n" ); }
private :
    int m_b;
};
class D : public B
{
public :
    D() { printf( "D()\n" ); }
    ~D() { printf( "~D()\n" ); }
private :
    int m_d;
};
int main()
{
    B * pB = new D ();
    delete pB;
    return 0;
}
C++中有这样的约束: 执行子类构造函数之前一定会执行父类的构造函数 ;同理, 执行子类的析构函数后,一定会执行父类的析构函数 ,若B的析构函数不是虚函数,则会先执行B的析构函数,导致报错。
好处:
• 虽然析构函数是不继承的,但若基类声明其析构函数为 virtual,则派生的析构函数始终覆盖它。
这使得可以通过指向基类的指针 delete 动态分配的多态类型对象
• 任何包含虚函数的基类的析构函数必须为公开且虚,或受保护且非虚。否则很容易导致内存泄

安全性

一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到 Base1 的虚表中有 Derive 的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
          Base1 *b1 =  new  Derive();
            b1->f1();   // 编译出错
任何妄图使用父类指针想调用子类中的 未覆盖父类的成员函数 的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反 C++ 语义的行为。
二、访问 non-public 的虚函数
另外,如果父类的虚函数是 private 或是 protected 的,但这些非 public 的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些 no n-public 的虚函数,这是很容易做到的。
如:

覆盖与隐藏

1.覆盖(override)- 修改基类函数定义
• 基类或非直接基类,至少有一个成员函数被 virtual 修饰
• 派生类虚函数必须与基类虚函数有同样签名, 即函数名,参数类型,返回类型(基类指针和派生类指针视为相同的返回值),顺 序和数量都必须相同。
隐藏(overwrite)- 屏蔽基类的函数定义
• 派生类的函数与基类的函数 同名,但是参数列表有所差异。
• 派生类的函数与基类的函数 同名,参数列表也相同,但是基类函数没有virtual关键字。
2.对象指针(引用)被向上转型基类指针(引用)
• 覆盖:调用该对象的虚函数
• 隐藏:调用基类的函数
派生类对象赋值到 基类对象
• 按基类行为调用该 基类函数
作用域
是否虚函数
函数名
函数签名
重载
相同
O
相同
不相同
隐藏/屏蔽
不同
O/否
相同
不同/O
覆盖
不同
相同
相同
O表示都可以

多态概念

程序语言中,多态特指 一个标识符指代不同类型的数据。或者说,由具体的数据类型决定该标识符的语义。
例子:
函数重载(Function Overload)
• 函数名,它可为不同类型函数(注:函数类型声明为 f(t1,t2…))
方法覆盖(Method Override)
• 有 虚函数基类的指针或引用,它可指代派生类的对象
泛型(Generics)/模板(Template),即“参数化类型”
• 模板名,例如 vector 可泛化指代各种类型数据的数组
多态 标识符必须指派具体的函数或方法以实现规定的语义。
静态绑定:如果在运行前由编译完成这个指派,称为静态绑定
动态绑定:如果在运行期间完成这个指派,称为动态绑定
定义:
动态绑定是在运行期间发生的绑定,发生动态绑定的函数的运行版本由传入的 实际参数类型决定,在运行时觉得函数的版本,所以动态绑定又称运行时绑定,动态绑定是C++的多态实现的一种形式。
在C++中,当使用基类的引用或指针调用一个虚函数时将发生动态绑定。
实现原理
C++中的动态绑定通过虚函数实现。而虚函数是通过 一张虚函数表(virtualtable)实现的,拥有虚函数的类在实例化时会创建一张虚函数表。这个表中记录了虚函数的入口地址,当派生类对虚函数进行重写时,虚函数表中相关虚函数的地址就会被替换,以保证动态绑定时能够根据对象的实际类型调用正确的函数。
优点
1、能够提高代码的可用性,使代码更加灵活。
2、多态是设计模式的基础,能够降低耦合性。
缺点
1.需要查找虚函数表来,性能比静态函数调用低。
2.虚函数表本身需要存储空间

纯虚函数

纯虚函数在基类中是没有定义的,必须在子类中加以实现( virtual void Eat() = 0; 直接=0,不要在cpp中定义就可以了)
1.类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了
纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
class A{
protected:
void foo();//普通类函数
virtual void foo1();//虚函数
virtual void foo2() = 0;//纯虚函数
}
2.虚函数在子类里面也可以不覆盖;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现
3.虚函数的类用于“实作继承 ,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于 介面继承 ,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,介面类中只有纯虚函数的。
4.错误:带纯虚函数的类叫虚基类(改正:抽象类),这种基类 不能直接生成对象,而只有被继承,并重写其纯虚函数后,才能使用。
  • 抽象类不能被实例化

抽象类:

  • 纯虚函数没有实现部分,不能产生对象,所以含有 纯虚函数的类是抽象类
  • 抽象类 不能定义对象,在实际中为强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限
  • 能仅能作为基类指针或引用,因为不可能存在抽象对象
  • 不能申明抽象类的对象。例如:Animal a
  • 不能被显式转为抽象类对象。例如:(Animal)dog
  • 不能作为函数参数类型或者返回的值。例如:func(Animal)
  • 能申明为指针或引用,指代自己派生类对象
  • 抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来提供一个根,相关的子类是从这个根派生出来的。
  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类只是继承基类的纯虚函数,而派生类仍然是一个抽象类。如果派生类给出了基类 所有的纯虚函数的实现, 那么该派生类将不再是抽象类。(它是一个可以建立对象的类。)
虚函数是为了继承接口和默认行为
纯虚函数只是继承接口,行为必须重新定义
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值