2.1 多态的构成条件
多态必须是在继承的条件下面,去调用同一个函数(返回值,函数名,参数都相同),产生了不同的行为
2.1.1 实现多态的必要条件
实现多态必须要满足以下两个条件:
- 必须是指针或引用调用虚函数
- 被调用的函数必须是虚函数,派⽣类必须完成对基类的虚函数重写/覆盖
说明:要实现多态的效果:第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类又指向派生类对象;第二派生类必须对基类的虚函数进行重写/覆盖,如此派生类才能有不同的函数,多态的不同形态效果才能达到。
多态的现本质就是调用虚函数
那什么又是虚函数呢?我们一起来看看
2.1.2 虚函数
类成员函数前面加
v
i
r
t
u
a
l
virtual
virtual(返回值前面),那么这个成员函数被称为 虚函数。
注:非成员函数不能加 virtual 修饰
注:在继承
章节,我们曾讲过虚继承也是用
v
i
r
t
u
a
l
virtual
virtual 关键字,但这里的
v
i
r
t
u
a
l
virtual
virtual 和虚继承中的
v
i
r
t
u
a
l
virtual
virtual 功能上没有任何关系
。这里属于一鱼两吃。
那么说明中虚函数的重写
和覆盖
又是什么意思呢?我们一起来看看
虚函数的重写/覆盖:
子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。
我们再来理清楚各个条件之间的关系:
多态有两个条件:必须是指针或引用调用虚函数;调用虚函数必须完成重写/覆盖。
而虚函数重新/覆盖又要满足:必须是基类和派生类中的两个虚函数,两个虚函数必须完全相同(只有函数体可以不同)
2.1.3 感受多态
class Person
{public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-打折" << endl;
}
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
为什么void Func(Person* ptr)
的参数要基类的指针或引用呢?在继承章节,我们曾讲过切片,只有参数是基类,才能既可以传父类又可以传子类对象(详情请看:【C++】—— 继承(上))
这时就达到了一个效果:指向谁调用谁。传递 p s ps ps,指向的是基类,就调用基类的虚函数;传递 s t st st,指向派生类,调用派生类的虚函数。传递不同的对象,调用不同的函数。
运行结果:
如果没有学习多态,实际调用的类型是根据表达式的类型来确定的。
f
u
n
c
func
func 函数参数类型是
P
e
r
s
o
n
Person
Person*,因此不论传递父类还是子类指针,传递给形参
p
t
r
ptr
ptr 都会强转成
P
e
r
s
o
n
Person
Person*,最终调用的都是父类的函数。
现在有了多态,实际调用的函数实在运行时根据对象的实际类型来确定的。指向谁调用谁,指向父类调父类,指向子类调子类我们传递不同的对象,实现调用不同的函数。
当然,引用也是可以的
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
运行结果:
2.1.4 判断是否满足多态
class Person {
public:
void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
不满足第二个条件,基类BuyTicket()
不是虚函数
运行结果:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person ptr)
{
ptr->BuyTicket()
}
不满足第一个条件,不是基类的指针或应用调用
运行结果:
不满足多态编译器看调用的参数类型void Func(Person ptr)
,类型父类是父类,类型子类是子类
满足多态则看起指向的对象
这种情况下满足多态吗
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
int main()
{
Cat* pcat = new Cat;
Dog* pdog = new Dog;
pcat->talk();
pdog->talk();
return 0;
}
不满足,因为多态要求是基类的指针/引用来调用
,这里
p
c
a
t
pcat
pcat 和
p
d
o
g
pdog
pdog 都是派生类的指针来调用。但基类和派生类是相对的,如果有
A
A
A 类
B
B
B 类继承了
p
c
a
t
pcat
pcat 和
p
d
o
g
pdog
pdog,那么他们同时还是基类,这样调用就构成多态了。
那可不可以是子类的指针或引用呢
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Student* ptr)
{
ptr->BuyTicket();
}
也是不行的,必须是父类的指针或调用
。因为如果是子类的,那就不能传递父类的对象,也就没有多态的概念了
那这样满足多态的条件吗
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
是满足的
注意:在重写基类虚函数时,派生类的虚函数在不加
v
i
r
t
u
a
l
virtual
virtual 关键字时,虽然也可以构成重写
(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该钟写法不是很规范,不建议这样使用
,不过在考试选择题中,经常会故意挖这个坑,让你判断是否构成多态。
2.1.5 多态场景的一道选择题
- 以下程序输出结果是什么()
A : A A: A A:A -> 0 B : B B: B B:B -> 1 C : A C: A C:A -> 1 D : B D: B D:B ->0 E E E: 编译出错 F F F: 以上都不正确
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;
}
首先 f u n c func func函数是
虚函数
,并完成了重写
接着 n e w new new 了一个子类对象 B B B,并通过 p p p 来指向 B B B
再接着, p p p 调用 子类 B B B 从父类 A A A 继承过来的 t e s t ( ) test() test() 函数,这是个普通调用
t e s t ( ) test() test() 函数中去调用 f u n c ( ) func() func() 函数。这里 t e s t ( ) test() test() 中的
this指针
是 A A A* 还是 B B B* 呢;如果是 A A A*, f u n c func func函数是通过父类的指针调用,满足多态的条件,如果是 B B B* 不满足多态的条件这里 t e s t test test函数中的 t h i s this this是 A A A*,为什么呢? B B B 不是继承了 A A A 吗,现在是 B B B 调用 t e s t test test,不应该是 B B B*吗?其实继承是一个形象的说法,继承的意思是我可以用,不会将函数中的参数给改了。编译器不会真的将 A A A 中的成员变量和不是重写成员函数都拷贝一份到 B B B 中。
因此,是
满足多态的条件
的,调用 B B B 类中的 f u n c func func 函数那答案是
B->0
了吗?还没完虚函数重写,只重写函数体的实现,不重写声明! 也就是说继承而来的缺省值是不会重写的,可以认为
重写后的虚函数 = 基类的函数声明 + 派生类的函数体
。也就是说 B B B 类中的虚函数本质是:virtual void func(int val = 0) { std::cout << "A->" << val << std::endl; }
所以正确答案是
B->1
那如果是这样调用,答案又选什么呢
int main(int argc, char* argv[])
{
B* p = new B;
p->func();
return 0;
}
这里是子类
B
B
B 去调用,不满足多态的条件,因此
B
B
B 中的
f
u
n
c
func
func函数 也不用去组合。这里是普通调用,直接调用
B
B
B 类原本的
f
u
n
c
func
func 函数。答案: B->0
2.1.6 虚函数重写的一些其他问题
2.1.6.1 协变
上述说到完成虚函数的重新必须满足三同(返回参数、函数名、参数类型),但是也有例外,那就是协变
的情况。
协变的概念
- 派生类重写基类虚函数时,可以与基类虚函数返回值类型不同。但要求
基类虚函数返回基类对象的指针或引用
,派生类虚函数返回派生类对象的指针或引用
,称为协变。
注:返回的基类和派生类可以不是自己本身
,但他们必须是一对基类和派生类
协变的实际意义并不大,我们了解一下即可
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
运行结果:
2.1.6.2 析构函数的重写
当基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加
v
i
r
t
u
a
l
virtual
virtual 关键字,都与基类的析构函数构成重写。
虽然基类与派生类析构函数名字看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成
d
e
s
t
r
u
c
t
o
r
(
)
destructor()
destructor(),所以基类的析构函数加了
v
i
r
t
u
a
l
virtual
virtual 修饰,派生类的析构函数就构成重写
class A
{
public :
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
传统的意义上,哪怕 ~B() 加上
v
i
r
t
u
a
l
virtual
virtual 也不会构成重写(不符合三同中的函数名相同),但经过编译器将析构函数函数名处理成
d
e
s
t
r
u
c
t
o
r
(
)
destructor()
destructor() 后,再加上
v
i
r
t
u
a
l
virtual
virtual 就构成了重写了。
但 C++ 为什么设计成这样呢?
我们实践中会遇到这样的问题
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0
}
现在我有一个父类对象的指针,他可以指向父类对象也可以指向子类对象。当我们进行
d
e
l
e
t
e
delete
delete 时,delete p1;
没问题,但delete p2;
是调用父类对象的析构还是子类对象的析构呢?
我们期望的是delete p2;
调用的是子类的析构函数,但是正常来说(不构成多态情况下)delete p2;
是调用父类的析构函数,因为
p
2
p2
p2 是
A
A
A* 类型。
这种情况只有构成多态才能解决问题。构成多态,指针调用时不是跟 p1 和 p2 的类型有关,而是跟他们指向的对象有关,指向父类调父类;指向子类调子类。
编译器将析构函数统一处理为 d e s t r u c t destruct destruct,就是为了能够实现析构函数的重写,这样只要在父类析构函数前加 v i r t u a l virtual virtual 就构成多态。只有这样才能正确释放资源
运行结果:
为什么最后会多调用一次父类的?
这一点我们在【C++】—— 继承(上)中提到过:这是为了保证析构的顺序是先子后父
不构成多态的情况:
class A
{
public :
~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
运行结果:
p
1
p1
p1 和
p
2
p2
p2 都是调用父类的析构函数
,
p
2
p2
p2 无法调到子类的析构函数
2.1.7 override 和 final 关键字
从上面可以看出,C++ 对函数重写的要求比较严格
,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间时不会报出的,只有在程序运行时没有得到预期结果才来
d
e
b
u
g
debug
debug 会得不偿失,因此 C++11 提供了
o
v
e
r
r
i
d
e
override
override关键字,可以帮助用户检测是否重写,如果没有完成重写,在编译时会报错
。
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override{ cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
函数名不同(
D
i
r
v
e
Dirve
Dirve 和
D
r
i
v
e
Drive
Drive),没有构成重写,编译报错。不加
o
v
e
r
r
i
d
e
override
override,编译时是检查不出来的。
如果我们不想让派生类重写这个虚函数,那么可以用
f
i
n
a
l
final
final 去修饰
在继承章节中我们曾提过:如果一个类我们不想让他被继承,我们可以用
f
i
n
a
l
final
final 去修饰他;被 final 修饰了他就是最终类。
多态这里是不能被重写:如果一个虚函数不想被重写 就可以用
f
i
n
a
l
final
final 去修饰
class Car {
public:
virtual void Dirve() final
{}
};
class Benz :public Car {
public:
virtual void Dirve() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
同时,既然析构函数函数名都被编译器处理成 d e s t r u c t o r destructor destructor,那也意味着他们构成隐藏,如果想要显式调用父类的析构函数需指定类域。详情请看:【C++】—— 继承(上)
2.1.8 重载/重写/隐藏的对比重载
重载
- 两个函数在同一作用域
- 函数名相同,但参数类型或个数不同,返回类型可同、可不同
重写/覆盖
- 两个函数分别在继承体系的父类和子类不同作用域
- 函数名、参数、返回值
都必须相同
。协变除外- 两个函数都必须是虚函数
隐藏
- 两个函数分别在继承体系的父类和子类不同作用域
- 函数名相同
- 两函数只要不构成重写就是隐藏
父子类的成员变量相同也叫隐藏
重载/重写/隐藏都有一个共同点:函数名相同