多态了解一下

多态

1.概念:

按其字面意思为“同一事物具有多种形态”,可以这样理解:向不同的对象发送同一个消息,不同的对象接收消息后会产生不同的行为(即调用不同的函数),也就是说,每个对象可以用自己的方式去相应共同的消息。
多态性是面向对象程序设计的基本特征,若一个类不支持多态,只能说明它是基于对象的,而不是面向对象的,c++中的多态体现在编译运行两个方面,分别对应静态多态动态多态

静态多态:又称编译期多态,我们前面所学过的函数重载,运算符重载都属于静态多态,静态多态是编译器在编译期间根据函数的参数确定要调用哪一个函数,即静态绑定,早绑定。
动态多态:也称运行期多态,是动态链编,动态绑定,晚绑定。即在程序运行时才能动态的确定所要操作的对象,动态多态是通过虚函数实现的。

那么虚函数是什么呢?
虚函数就是指那些被virtual关键字修饰的成员函数,它出现的作用就是为了在基类中被声明在派生类中被重写,通过指向派生类对象的指针或引用来访问这些虚函数,从而实现多态。


动态多态的实现条件:
- 基类中必须包含虚函数,并且派生类一定要对基类中的虚函数进行重写
- 通过基类对象的指针或引用调用虚函数

class B
{
public:
virtual void Fun1()
{
    cout << "B::Fun1()" << endl;
}
 void Fun2()
{
    cout << "B::Fun2()" << endl;
}
virtual void Fun3(int x)
{
    cout << "B::Fun3()" << endl;
}
virtual B* Fun4()
{
    cout << "B::Fun4()" << endl;
    return this;
}
};

class D :public B
{
virtual void Fun1()
{
    cout << "C::Fun1()" << endl;
}
 void Fun2()
{
    cout << "C::Fun2()" << endl;
}
virtual void Fun3(char c)
{
    cout << "C::Fun3()" << endl;
}
virtual D* Fun4()
    {
    cout << "C::Fun4()" << endl;
    return this;
}
};

void test(B& b,string s)
{
cout << s << endl;
b.Fun1();
b.Fun2();
b.Fun3('a');
b.Fun4();
}

int main()
{
B b;
D d;

test(b,"b::fun");
test(d,"d::fun");
}

输出结果:
这里写图片描述

代码分析:主函数中第一次对test函数调用,传递的参数是基类对象,因此,理所当然调用的是基类的Fun1、2、3、4这四个函数。第二次调用test函数我们传递的是派生类D的对象,但是test函数是通过对基类对象的引用调用的成员函数,Fun1函数在基类B中声明的是虚函数,派生类中我们对其进行了重写,相当于覆盖掉了基类中的Fun1函数,因此调用派生类中的Fun1;Fun2函数我们未声明为虚函数,即为普通的成员函数,通过基类引用调用基类的Fun2;Fun3函数我们虽然在基类中声明了虚函数,但是派生类中Fun3参数与基类不同,未构成重写,因此还是调用基类中的Func3;Fun4函数在基类和派生类的返回值不同,也不满足重写的条件,但它属于协变,是重写条件中的一个特例,因此Fun4也会调用派生类中的Fun4。

区分函数重载,重写和同名隐藏

  • 函数重载:同一作用域,函数名相同,但参数列表不同(参数个数/类型/次序)
  • 重写(覆盖)
    a.一个在基类,一个在派生类,基类中必须声明为虚函数(派生类中可不添加virtual 关键字,但以防再次派生,最好加上)
    b.函数名相同(析构函数除外),参数相同,返回值相同(协变除外)
    【协变:是指两个虚函数的返回值分别为在基类中是基类指针和在派生类中是派生类指针(见上述Fun4函数)】
    c.访问限定符可以不一致(虚函数只需要在基类中给成公有的,派生类中可以给成私有的 是因为编译器在编译期间通过基类的指针或引用调用的虚函数)

  • 同名隐藏:基类和派生类中定义了同名的函数,派生类对象在调用该名字的函数时,只会调用派生类的(基类和派生类的成员函数名相同但参数列表不同的成员函数也会被隐藏)


纯虚函数与抽象类

纯虚函数
在声明为虚函数的成员函数的参数列表后面加上=0,这样的成员函数叫做纯虚函数,声明虚函数的一般形式是:

virtual 函数类型 函数名 (参数列表)=0

注意:纯虚函数只有函数名没有函数体,因此不具备函数的功能,不能被调用,它后面的=0不代表返回值为0,只是告诉编译器这是一个纯虚函数,该语句是一条声明语句,后面应该加上分号。

抽象类
包含纯虚函数的类叫做抽象类,也称作接口类,抽象类不能实例化出一个对象,纯虚函数在派生类中被重写后,其派生类才能实例化出一个对象,若派生类未对抽象类的纯虚函数进行重写,该派生类也是抽象类。
这种不用来定义对象只作为基本类型用来继承的类称作抽象类,由于它常用作基类,也成为抽象基类。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。
虽然抽象类不能实例化出一个对象但是可以定义指向抽象类数据的指针变量,当派生类成为具体类后i,就可以用这个指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。


多态调用原理

首先我们来看这样一段代码:

class B
{
public:
virtual void Fun1()
{
    cout << "B::Fun1()" << endl;
}
virtual void Fun2()
{
    cout << "B::Fun2()" << endl;
}
public:
int _b;
};

int main()
{
B b;
cout << sizeof(b) << endl;
b._b = 1;
}

输出结果:Alt text

我们都知道类对象大小的计算方式是非静态成员变量的总和,因此B类对象b的大小理应为4,而这里的打印结果却多了四个字节,我们通过内存看到其后四个字节放的是成员变量_b,而前四个字节放了一个很大的数,猜想它是一个地址,再看一看该地址中放了什么内容,由于这块空间中有两个看起来像地址的数,我们暂且猜想他们为B类中两个虚函数的地址,如图:
这里写图片描述

为了验证我们的猜想,我们在B类中多加了一个虚函数Fun3(),再次查看其在内存中的布局:

这里写图片描述

我们的猜想似乎成立,那么既然这块空间保存了类中虚函数的地址,即就可以通过访问函数地址对函数进行调用,我们小小的实现一下:

typedef void(*PVFT)();
void PrintFun(B& b)
{

PVFT *pVFT = (PVFT*)(*(int*)&b);
while (*pVFT)
{
    (*pVFT)();
    pVFT++;
}
}

在主函数中我们对这个打印函数进行调用,输出结果如图:

这里写图片描述

因此,可以得出结论,这块空间的确放的是虚函数的地址。
我们称这张表为虚表,对象前四个字节指向这张虚表,称为虚表指针
注意:虚表中函数地址的顺序=虚函数在类中的声明次序

了解了这些内容,我们再来回到之前的问题,多态究竟是如何实现的,还是先上代码:
在上面基类的基础上我们定义了派生类D,通过传递基类对象和派生类对象分别调用打印函数

class D:public B
{
public:
virtual void Fun2()
{
    cout << "D::Fun2()" << endl;
}
virtual void Fun3()
{
    cout << "D::Fun3()" << endl;
}
public:
int _d;
};
int main()
{
B b;
D d;
PrintFun(b);
PrintFun(d);
}

输出结果:
这里写图片描述

其分析大致如图:

这里写图片描述

派生类虚表的构建过程:
  • 首先将基类虚表中的虚函数拷贝过来
  • 其次,检查派生类是否存在对基类中的虚函数进行重写,若存在,则用派生类中的虚函数替换基类中对应的虚函数。
  • 最后,再将派生类中新添加的虚函数加入派生类的虚表中。

注:同一个类所有的对象共享同一张虚表
派生类和其基类对应不同的虚表


那么为什么通过基类的指针或引用调用虚函数,传递的对象不同,便会自己调用相应的虚函数呢?这就是赋值兼容规则的功劳了

赋值兼容规则

在public继承权限下,基类和派生类之间的关系,包括以下几种情况:
- 派生类对象可以赋值给基类对象(该基类对象拿到的只是派生类对象中对应的基类那部分,切片)
- 派生类对象可以初始化基类的引用(引用也是如此)
- 派生类对象的地址可以赋值给基类指针,即基类指针可以指向派生类对象(该基类指针只能指向从基类中继承下来的部分,只能访问派生类中的基类成员,而不能访问派生类中新增的成员)

问:通过基类指针和引用调用虚函数,可以实现多态,那么通过传值可以吗?
答案是否定的,因为如果我们使用基类对象调用虚函数,当传递的参数是派生类对象时,在传参时会形成一个临时的基类对象,而基类对象所能看到的只是基类的虚表,因此只能调用基类的虚函数,不能够达到我们预期的效果,实现多态。
而我们通过基类的指针和引用调用虚函数,当传递的参数是派生类对象时,不会生成临时对象,本质上我们拿到的还是原来该对象的那块空间,只是指针或引用指向的是其对应基类的部分,当中虚表指针所指向的虚表已经被改写,已经成为了派生类的虚表,因此,当调用虚函数时,可以调对应的虚函数。


那些函数不能定义为虚函数呢

  • 构造函数
    构造函数的作用就是为了初始化一个对象,而虚函数的调用是通过对象中的虚表来查找并调用的,若对象未构造出来,如何取其虚表,显然矛盾。
  • 静态成员函数
    静态成员函数对每个类来讲只有一份,通常我们调用它不需要通过对象或this指针来调,直接通过该函数的地址找到并调用即可。若将静态成员函数声明为一个虚函数,那么调用它也要通过对象的虚表去查找,当我们未创建对象时,就无法调用它,违背了静态成员函数的初衷,并且它也没有动态绑定的需要。
  • 赋值运算符重载函数
    原则上可以将基类的赋值运算符重载函数给成虚函数,当在派生类中重写该函数时,就遇到了问题,因为赋值操作符需要一个与类本身类型相同的形参,形参上不能保持一致,重写条件也就无法满足,若执意将派生类中的该函数形式改为与基类中的相同,构成重写,那它原本的赋值功能就无法实现,因此,建议最好不要将赋值运算符重载函数定义为虚函数。
  • 友元函数
    友元函数还是普通函数,只是赋予了它一定的特权,使他能够访问类中的私有成员,但它还不属于类的成员函数,当然不能定义为虚函数。

注意:最好将基类中的析构函数定义成虚函数,具体原因看例子:

class B
{
public:
    B()
    {
        cout << "B()" << endl;
    }
    /*virtual*/ ~B()
    {
        cout << "~B()" << endl;
    }
};
class D:public B
{
public:
    D()
    {
        p = new int[10];
        cout << "D()" << endl;
    }
    ~D()
    {
        delete[] p;
        cout << "~D()" << endl;
    }
    int *p;
};
int main()
{
    B *pb = new D;
    delete pb;
}

我们定义了一个基类B和一个派生类D,D公有继承自B,在D类的构造函数中我们申请了十个整型大小的空间,D的析构函数中将这块空间释放,主函数中,我们定义了一个B类指针pb指向一个D类对象,根据赋值兼容规则,pb指向的只是D类对象中属于B类的部分,之后通过delete释放pb,运行结果如下图,很明显未调用D类的析构函数,申请的空间未释放,存在内存泄漏问题。
这里写图片描述

当我们将基类B的析构函数声明为虚函数时,就可以解决内存泄漏问题,看运行结果

这里写图片描述

原因也很简单,将基类析构函数定义为虚函数后,在派生类中再给出析构函数就构成重写(协变,上面已进行说明),通过基类指针调用虚函数,调用的就是被改写的虚表中的派生类析构函数,完美!!

猜你喜欢

转载自blog.csdn.net/shidantong/article/details/80424572