目录
- 1、多态概念
- 1.多态性有两种表现的方式
- 2、联编(实现多态)
- 1.静态联编
- 2.动态联编
- 3、实现运行时多态
- 1.为何要使用运行时多态?
- 2.如何实现运行时多态
- 3.多态的例子
- 1.调用哪个同名虚函数?
- 2. 用途:可以用父类指针访问子类对象成员
- 4. 虚函数的传递性
- 虚函数的缺点:
- 4、代码示例
- 1、继承与重载函数的例子
- 任务1、2
- 任务3
- 2、使用运行时多态
- 3、override覆写
- 6、运行时多态的总结
- 1.Summary: static binding v.s. dynamic binding
- 2. Summary: 静态联编的简单示例
- 3. Summary: 通过基类指针访问同名函数的示例
- 4. Summary:动态联编的简单示例-指针形式
- 5. Summary:动态联编的简单示例-引用形式
- 7、C++11:使用override和final
- 1. override显式声明覆写
- 2. final 显式声明禁止覆写
1、多态概念
广义的多态:不同类型的实体/对象对于同一消息(可以理解为同一个名字的函数)有不同的响应,就是OOP中的多态性。
1.多态性有两种表现的方式
1. 重载多态:调用参数不同的同名函数,表现出不同的行为
class C {
public: int f(int x);int f( );
};
2.子类型多态:不同的对象调用同名重定义函数,表现出不同的行为
class A { virtual int f() {return 1;} };
class B: public A { virtual int f() {return 8;} };
A a; B b;
A* p = &b; //p虽然是a类型,但是它指向B类型的对象
a.f() // call A::f()
b.f() // call B::f()
p->f(); // call B::f()
//a和p是同类型,但是调用同名函数的响应是不一样的。
2、联编(实现多态)
确定具有多态性的语句调用哪个函数的过程称为联编。
1.静态联编
静态联编在程序编译时(Compile-time)确定调用哪个函数。
如:函数重载
2.动态联编
在程序运行时(Run-time),才能够确定调用哪个函数
用动态联编实现的多态,也称为运行时多态(Run-time Polymorphism)。
3、实现运行时多态
1.为何要使用运行时多态?
我们有三个类ABC,继承链如下:
A<-B<-C;
我们想使用print()调用toString()输出信息,就需要写三个重载函数:
void print(A obj);
void print(B obj);
void print(C obj);
这样比较麻烦。
我们可以使用运行时多态解决这个问题:
2.如何实现运行时多态
实现运行时多态有两个要素:
(1) virtual function (虚函数)
(2) Override (覆写) : redefining a virtual function in a derived class. (在派生类中重定义一个虚函数)
在正常的函数名字前加上virtual关键字,这个函数就会变为虚函数:
struct A{virtual std::string toString(){return "A";}
};
覆写是在派生类中定义一个与基类虚函数同名,参数一样,返回值一样的函数。
注意这里我们传递的是ABC类型的指针:
3.多态的例子
1.调用哪个同名虚函数?
(1) 不由指针类型决定;
(2) 而由指针所指的【实际对象】的类型决定
(3) 运行时,检查指针所指对象类型
2. 用途:可以用父类指针访问子类对象成员
注意print函数的参数是基类类型的指针。
然后将ABC类型的对象的地址作为函数参数传入。
后面两个print函数指针会做隐式类型转换。将派生类的地址转化为基类类型的指针,这个转换是安全的。
返回 的结果是不一样的,是和对象的实际情况相关的。
4. 虚函数的传递性
基类定义了虚同名函数,那么派生类中的同名函数自动变为虚函数。
注意:只需要在继承连上最顶端的基类上加上关键字即可:
虚函数的缺点:
1、类中保存着一个Virtual function table (虚函数表)。
2、运行时联编/动态联编,会有额外的逻辑。
所以调用虚函数比非虚函数开销大
4、代码示例
1、继承与重载函数的例子
任务1、2
1、创建A/B/C三个类,B继承A,C继承B,ABC均有toString函数
2、创建print函数,接受A类型的参数,调用A对象的toString()
不管传进来什么类型,都是打印基类类型的数据
//本部分要展示的内容如下:
//1、创建A/B/C三个类,B继承A,C继承B,ABC均有toString函数
//2、创建print函数,接受A类型的参数,调用A对象的toString()
//3、重载print函数,接受B/C类型参数,调用toStirng()#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A a) {cout << a.toString() << endl;
}
int main()
{A a;B b;C c;print(a);print(b);print(c);
}
任务3
重载print函数,接受B/C类型参数,调用toStirng()
//本部分要展示的内容如下:
//1、创建A/B/C三个类,B继承A,C继承B,ABC均有toString函数
//2、创建print函数,接受A类型的参数,调用A对象的toString()
//3、重载print函数,接受B/C类型参数,调用toStirng()#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A a) {cout << a.toString() << endl;
}
void print(B b) {cout << b.toString() << endl;
}
void print(C c) {cout << c.toString() << endl;
}
int main()
{A a;B b;C c;print(a);print(b);print(c);
}
通过三个重载函数,将打印的数据与类型匹配。
2、使用运行时多态
本部分要展示的内容如下:
1、将基类A的toStirng函数改为虚函数
2、将print函数参数改为基类指针类型
main()中调用print(),实参为指向对象的基类指针
3、添加一个print函数,参数是基类引用类型
在main()中调用print(),参数为对象的基类引用
//展示运行时多态的实现
#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:virtual std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A* a) {cout << a->toString() << endl;
}
void print(A& a) {cout << a.toString() << endl;
}
int main()
{A a;B b;C c;A* p1 = &a;A* p2 = &b;A* p3 = &c;print(p1);print(p2);print(p3);print(a);print(b);print(c);
}
可以发现参数是基类引用类型与参数是基类引用类型实现的效果是一样的。这些就是多态的具体展示。
注意如果在print函数中没有找到该对象的同名函数,那么就会顺着继承链,直到在基类中找到这个同名函数。
也就是说,在继承链中如果某个类的虚函数名字写错了就会出现许多BUG。
3、override覆写
为了解决上面的问题,c++提供了覆写操作。这在第7大点将详细讲述:
表明了对基类函数的覆写,如果函数名字或者参数或者返回值不一样,编译器就会报错,提醒程序员。
6、运行时多态的总结
1.Summary: static binding v.s. dynamic binding
基类与派生类中有同名函数
(1) 通过派生类对象访问同名函数,是静态联编
(2) 通过基类对象的指针访问同名函数,是静态联编
(3) 通过基类对象的指针或引用访问同名虚函数,是动态联编
2. Summary: 静态联编的简单示例
class P { public: f(){…} }; //父类
class C: public P { public: f(){…} }; //子类
main () {P p; C c;p.f(); //调用P::f()c.f(); //调用C::f()
}
说明: 对象是什么类型,就调什么类型
3. Summary: 通过基类指针访问同名函数的示例
class P { public: f(){…} }; //父类
class C: public P { public: f(){…} }; //子类
main () {P* ptr; P p; C c;ptr = &p;ptr->f(); // 调用P::f()ptr=&c;ptr->f(); // 调用P::f()
}
说明: 指针是什么类型,就调什么类型
4. Summary:动态联编的简单示例-指针形式
class P { public: virtual f(){…} }; //父类
class C: public P { public: f(){…} }; //子类,f自动virtual
main () {P* ptr; P p; C c;ptr = &p;ptr->f(); //调用P::f()ptr=&c;ptr->f(); //调用C::f()
}
说明: 函数虚,不看指针看真对象
5. Summary:动态联编的简单示例-引用形式
class P { public: virtual f(){…} }; //父类
class C:public P { public: f(){…} }; //子类,f自动virtual
main () {P p; C c;P& pr1 = p;pr1.f(); //调用P::f()P& pr2 = c;pr2.f(); //调用C::f()
}
说明: 函数虚,不看引用看真对象
7、C++11:使用override和final
1. override显式声明覆写
C++11引入override标识符,指定一个虚函数覆写另一个虚函数。
class A {
public:virtual void foo() {}void bar() {}
};
class B : public A {
public://此处foo为常函数,与基类的foo函数不是同名覆写函数void foo() const override { // 错误: B::foo 不覆写 A::foo} // (签名不匹配)void foo() override; // OK : B::foo 覆写 A::foovoid bar() override {} // 错误: A::bar 非虚
};
void B::foo() override {// 错误: override只能放到类内使用
}
override的价值在于:避免程序员在覆写时错命名或无虚函数导致隐藏bug
summary:
1、override标识符应该写到派生类同名虚函数的后面
2、非虚函数不能覆写
3、覆写只能在类的内部使用
2. final 显式声明禁止覆写
C++11引入final特殊标识符,指定派生类不能覆写虚函数。
struct Base {virtual void foo();
};struct A : Base
{ void foo() final; // A::foo 被覆写且是最终覆写void bar() final; // 错误:非虚函数不能被覆写或是 final
};
struct B final : A // struct B 为 final,不能被继承
{void foo() override; // 错误: foo 不能被覆写,因为它在 A 中是 final
};
final标识符的作用: