一.多态的概念
什么叫多态???
多态是指不同的情况下完成某件事会出现不同的状态,这就是多态
例如:购买火车票,不同的人买价钱不一样,这种多样的状态就是多态
在C++中,多态通常是指在不同继承关系的类对象,去调用同一函数,产生了不同的行为
二.继承中形成多态
在继承中要构成多态有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
首先,我们要先明白什么是虚函数???
虚函数定义:即被virtual修饰的类成员函数称为虚函数
如下:
class Student : public Person
{
public:
virtual void func()//这就是虚函数
{
cout << "Student:void func()" << endl;
}
protected:
};
现在我们知道什么是虚函数了,那么什么又是虚函数的重写呢???
下面看下面代码:
class Person
{
public:
virtual void func()
{
cout << "Person:void func()" << endl;
}
protected:
int _age;
};
class Student : public Person
{
public:
virtual void func()//这就是虚函数
{
cout << "Student:void func()" << endl;
}
protected:
};
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数,
(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)
称子类的虚函数重写了基类的虚函数。
看到这里,你可能能就开始想要去分清楚如下几个概念了:
重载,重定义,隐藏,重写,覆盖
这里我们就带大家来细分下:
重载:是指在同一作用域里,函数名相同,而参数不同所构成重载。
注意:只有返回值不同不是重载!!!
重定义:别名隐藏,是指在基类和子类两个不同的作用域中,出现了同名成员,子类就会隐藏基类成员
注意点:需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,,可以使用 基类::基类成员 显示访问基类同名成员
重写:别名覆盖,是指在基类和子类两个作用域中,两个虚函数:函数名,参数和返回值都相同所构成的
注意点:有特殊情况,例如:协变
对比图:
下面我们来看一个例子:
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
请问输出结果是什么???
你以为func函数调用的是Person,所以会是两个全票吗?看运行结果
出乎意料了吗?
这其实就是重写形成的多态。
我们调用了相同的func函数,结果却不一样,正是多态的情况
下面我们来解释下原因:
由于我们才开始学习多态,所以我们一点点引入,大家慢慢来,这里还是非常困难的。
首先这涉及虚函数表,当我们写一个如下类时,求其sizeof:
//x86环境
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
结果为8,我们知道int是4字节,内存对齐也不应该是8字节啊,那是哪里多开了4字节呢???
那是因为还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
现在再回归到上面的题目:
我们查看下图:
发现基类的__vfptr和派生类的并不一样,这是不是说明,虽然我们派生类是继承基类来的,但是虚表指针指向并不相同,即在多态调用时,我们虽然在func这里都是传Person,但是Student在切片之后还是调用的虚表地址是派生类的虚函数,所以输出不一样的结果
三.虚函数重写的两个例外
1. 协变
(基类与派生类虚函数返回值类型不同)
定义:
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A {};
class B : public A {};
class Person
{
public:
virtual A* f()
{
cout << "Person" << endl;
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
cout << "Student" << endl;
return new B;
}
};
2.析构函数的重写
(基类与派生类析构函数的名字不同)
定义:
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor
此时就脑海中忽然出现,如果析构可以那么构造函数是否也可以构成重写呢???
答案显然是不能的,为什么呢?
基类的构造函数是不能继承给子类的,所以派生类要自己创建一个构造函数。 该构造函数要对基类的数据成员以及派生类自己增加的数据成员进行初始化。 为了不在派生类的构造函数重复初始化基类的数据成员,为了减少代码量以及重写代码, 派生类在执行构造函数时,直接调用基类的构造函数
此时你可能就会有点晕了,为什么构造函数不能继承,而析构可以呢?
其实,这里的时候,你就已经错了,析构函数也是不能继承的
那么析构函数为什么可以形成重写呢?
这就是回到前面我们看到的析构函数的名称统一处理成destructor,所以无论是否加virtual关键字,都与基类的析构函数构成重写。
四.C++11 override 和 final
final:
用法:
1.修饰虚函数,表示该虚函数不能再被重写
2.修饰的类为最终类,不能被继承
用法:
//final用法1
class Person1
{
public:
virtual void func()final
{
//
}
protected:
};
//final用法2
class Person2 final
{
public:
protected:
};
override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Person
{
public:
virtual void func()
{
//
}
protected:
};
class Student: public Person
{
public:
void func() override
{
//
}
protected:
};
知识点补充:
如果基类的成员函数用了virtual,即是虚函数,那么派生类重写时可以不用写virtual,两者也构成重写关系,但是建议写上。
实现一个类,这个类不能被继承方法:
方法1:父类构造函数私有化,派生实例化不出对象
方法2:C++11,final修饰的类为最终类,不能被继承
下面我们对比不同调用和多态调用:
普通调用:编译时通过调用者类型来确定函数地址,关键可以看指针或者引用或者对象的类型
多态调用:运行时通过虚函数表去进行函数调用,基类调用基类函数,派生类调用派生类函数
关键看指针或者引用指向的对象
最后,感谢大家的支持!!!