1. 多态的概念
多态是多种形态,当不同的对象去完成同一行为时会产生出不同的状态。比如当我们和外国人吃饭时,我们多用筷子和勺子,他们多用叉子和手抓。还比如不同年龄段,对某款游戏的喜爱程度等等。
2. 多态的定义和实现
多态是在继承关系中不同的类对象,去调用同一函数,产生了不同的行为。多态有两个条件:1.必须通过基类的指针或者引用调用虚函数;2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class Tableware//餐具
{
public:
virtual void WhatTableware()
{
cout << "餐具" << endl;
}
};
class EasternAsia:public Tableware//东亚人的餐具
{
public:
virtual void WhatTableware()
{
cout << "筷子" << endl;
}
};
void func(Tableware& t)//以基类的指针或者引用接受
{
t.WhatTableware();
}
void test1()
{
Tableware t;
EasternAsia e;
func(t);
func(e);
}
3. 虚函数和虚函数的重写
- 虚函数是被virtual修饰的类成员函数。
- 虚函数的重写(也叫覆盖)是指派生类中有一个与基类完全相同的虚函数(即派生类虚函数与基类虚函数的函数名相同、返回值相同、参数列表完全相同),则称派生类的虚函数重写了基类的虚函数。
- 派生类的虚函数可以不加virtual构成重写,因为继承后基类的虚函数也被继承下来,其在派生类中依旧保持虚函数的性质。但不建议这么写,因为写法不规范。
class Tableware//餐具
{
public:
virtual void WhatTableware()
{
cout << "餐具" << endl;
}
};
class EasternAsia:public Tableware//东亚人的餐具
{
public:
void WhatTableware()//没+virtual也构成重写,但不建议。
{
cout << "筷子" << endl;
}
};
- 但也有一些特殊情况:协变
协变:派生类的虚函数返回值类型与基类的返回值类型不同。且派生类的虚函数返回值类型是派生类的指针或者引用,基类的虚函数的返回值类型是基类的指针或者引用。
class A
{
public:
virtual A* New()
{
return new A;
}
};
class B :public A
{
virtual B* New()
{
return new B;
}
};
- 析构函数+virtual,也可以构成重写。
基类的析构函数是虚函数,派生类的析构函数不管有没有virtual,都构成重写。为什么?因为编译器对析构函数做了特殊处理,统一处理为destructor(),这样函数名就相同了。
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
看起来析构函数的重写意义不大,但在一种情况下就必须构成重写
- final和override
final放在基类虚函数后面,表示这个虚函数不能被重写。
override放在派生类虚函数后面,检验该虚函数是否重写了基类的虚函数,没有则会报错。
拓展
final还有一个功能:类不能被继承
除此之外,还可以把父类的构造或者析构改成private成员,这样子类在调用构造或者析构时由于不能调用父类的构造或者析构,就会报错。
- 重载、重定义和重写
重载:重载的函数作用域相同;函数名相同,参数不同(参数类型、参数个数、参数顺序不同)。
重定义(隐藏):函数的作用域是不同(父类和子类的作用域是独立的);函数名相同。
重写(覆盖):函数的作用域不同;函数名、参数、返回类型必须相同(协变除外);两个函数必须是虚函数;两个基类和派生类的同名函数不构成重定义就是重写。
4. 多态的原理
例子1
例子2
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
void Func2()
{
cout << "A::Func2()" << endl;
}
int _a = 1;
};
class B :public A
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
virtual void Func3()
{
cout << "B::Func3()" << endl;
}
int _b = 2;
};
void test5()
{
A a;
B b;
}
例子3
void test5()
{
A a;
B b;
A* aa = &b;
aa->Func1();
}
调用结果
原理
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
int _a = 1;
};
class B :public A
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
int _b = 2;
};
void test6()
{
A a;
B b;
A* a1 = &a;
A* a2 = &b;
}
补充
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
拓展
(1) 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
(2)动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
5. 多继承的虚函数表
- 前面我们已经看到单继承的虚函数表,父类的虚函数表被继承后,子类如果重写父类的虚函数,对应虚表中的虚函数就被子类覆盖。子类自己的虚函数也会出现在虚表中,只不过没有显示。
- 例子
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
int _a = 1;
};
class B
{
public:
virtual void Func2()
{
cout << "B::Func2()" << endl;
}
int _b = 2;
};
class C :public A, public B
{
public:
virtual void Func1()
{
cout << "C::Func1()" << endl;
}
virtual void Func2()
{
cout << "C::Func2()" << endl;
}
virtual void Func3()
{
cout << "C::Func3()" << endl;
}
int _c = 3;
};
void test7()
{
C c;
}
- 菱形虚拟继承
没有重写虚函数
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C:virtual public A
{
public:
int _c;
};
class D:public B,public C
{
public:
int _d;
};
void test10()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
B和C重写了A的虚函数后
class A
{
public:
virtual void func1()
{
cout << "A::func1" << endl;
}
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
virtual void func1()
{
cout << "B::func1" << endl;
}
virtual void func2()
{
cout << "B::func2" << endl;
}
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1" << endl;
}
virtual void func2()
{
cout << "C::func2" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1" << endl;
}
virtual void func3()
{
cout << "D::func3" << endl;
}
public:
int _d;
}
void test10()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
6. 抽象类
- 有纯虚函数的类叫做抽象类(也叫接口类)。纯虚函数是在父类虚函数后面加上=0。
class A
{
public:
virtual void Func1() = 0;
int _a = 1;
};
- 抽象类对象必须重写父类的纯虚函数,才能进行实例化。
这也可以用来检查是否重写了虚函数。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。也就是说派生类继承的是基类的虚函数的接口,而不是其虚函数的内容。(有例题)
例题
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
输出结果是:B->1
//子类的虚函数继承的是父类虚函数的壳(void func1(int val = 1)),重写的是子类虚函数的实现(内容)。
7. 练习题
- 内联函数可以是虚函数。不过因为内联函数会被展开,没有地址,编译器会忽略其属性,使其不再是inline,才能将其放到虚函数表。
- 静态成员不能是虚函数。因为静态成员函数没有this指针,所以不能用基类的指针或者引用去调用静态成员函数,得使用类型::成员函数的调用方式,但这种方式无法访问虚函数表,不满足多态的条件。无法实现多态也就没有意义,所以语法会强制检查。
- 构造函数不能是虚函数。因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 对象访问普通函数快还是虚函数更快?首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
答案是D。虚函数表存在于代码段,与普通成员函数一样,都只有一份。