个人主页:@Weraphael
✍作者简介:目前学习C++和算法
✈️专栏:C++航路
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注✨
多态是面向对象三大基本特征中的最后一个。概念:通俗来说,就是多种形态,具体点就是当不同的对象去完成某个行为,就会产生出不同的状态。
比如在购买高铁票时,成人原价,学生半价,而军人可以优先购票,对于购票这一相同的动作,需要根据不同的对象提供不同的方法
#include
using namespace std;
class Adult // 成人
{
public:
virtual void Buyticket()
{
cout << "成人-原价" << endl;
}
};
class Student : public Adult
{
public:
virtual void Buyticket()
{
cout << "学生-半价" << endl;
}
};
class Soldier : public Adult
{
public:
virtual void Buyticket()
{
cout << "军人-优先" << endl;
}
};
void Buyticket(Adult& At)
{
At.Buyticket();
}
int main()
{
Adult at;
Student s;
Soldier sd;
Buyticket(at); // 成人
Buyticket(s); // 学生
Buyticket(sd); // 军人
return 0;
}
【输出结果】
可以看到,不同对象调用同一函数,执行结果是不同
虚函数:即被virtual
修饰的类成员函数称为虚函数。
虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数,即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同,称子类的虚函数重写了父类的虚函数。
// 父类
class Adult // 成人
{
public:
virtual void Buyticket()
{cout << "成人-原价" << endl;}
};
// 子类
class Student : public Adult
{
public:
virtual void Buyticket()
{cout << "学生-半价" << endl;}
};
在继承中要构成多态还有两个条件:
注意:上述两个构成多态的条件缺一不可!缺少其中任意一个条件,都不构成多态!
// 父类
class Adult // 成人
{
public:
virtual void Buyticket()
{cout << "成人-原价" << endl;}
};
// 子类
class Student : public Adult
{
public:
// 子类必须对父类的虚函数进行重写
virtual void Buyticket()
{cout << "学生-半价" << endl;}
};
// 1. 通过父类的指针或者引用调用虚函数
// void Buyticket(Adult* At) - 指针
void Buyticket(Adult& At) // 引用
{
// 2. 被调用的函数必须是虚函数
At.Buyticket();
}
virtual
修饰虽然这个例外在语法上是支持的,但是建议不要省略,因为会破坏代码的可阅读性,可能无法让别人一眼看出多态。
子类重写基类虚函数时,与父类虚函数返回值类型不同。即 父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
第一种:返回各对象的指针
class Adult // 成人
{
public:
// 父类虚函数返回父类对象的指针
virtual Adult* Buyticket()
{
cout << "成人-原价" << endl;
return 0;
}
};
// 子类
class Student : public Adult
{
public:
virtual Student* Buyticket()
{
cout << "学生-半价" << endl;
return 0;
}
};
第二种:返回各对象的引用
class Adult // 成人
{
public:
// 父类虚函数返回父类对象的引用
virtual const Adult& Buyticket()
{
cout << "成人-原价" << endl;
return Adult(); // 返回匿名对象
}
};
// 子类
class Student : public Adult
{
public:
virtual const Student& Buyticket()
{
cout << "学生-半价" << endl;
return Student();
}
};
注意:父子类关系的指针/引用,不是必须是自己的,也可以是其他类的,但是要对应匹配子类和父类。
class A // 父类
{};
class B : public A // 子类
{};
class Adult // 成人
{
public:
// 父类虚函数返回父类对象的引用
virtual const A& Buyticket()
{
cout << "成人-原价" << endl;
return A(); // 返回匿名对象
}
};
// 子类
class Student : public Adult
{
public:
virtual const B& Buyticket()
{
cout << "学生-半价" << endl;
return B();
}
};
还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用
有个问题:析构函数加上virtual
是不是虚函数重写?
答案:是。虽然父类与子类析构函数名字不同(不满足三重),看起来违背了重写的规,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
#include
using namespace std;
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A a;
B b;
return 0;
}
【输出结果】
接下来就是面试官的连续“攻击”:为什么要这样处理呢?— 因为要构成重写
那么为什么要让它们构成重写呢?其实不加virtual
关键字也是可以的
但如果不对析构重写的话,那么下面有一个场景是过不了的(记住此场景)
#include
using namespace std;
class A // 父类
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A // 子类
{
public:
~B()
{
cout << "~B()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[3];
};
int main()
{
A* a = new A;
delete a;
a = new B;
delete a;
return 0;
}
【输出结果】
我们发现,不加virtual
没有调用子类的析构函数,发生了内存泄漏。那为什么没有调到子类的析构呢?第一次释放了a
指向的空间,然后又改变了指向
在前面说过,类的析构函数都被处理成了destructor
这个函数。而delete
对于自定义类型的原理是:
operator delete
函数释放对象的空间(operator delete
本质就是调用free
函数)即对于delete a
,先调用了析构函数a->destructor()
,然后再调用operator delete
函数释放对象的空间。
但由于编译器将析构函数名处理成一样的函数名destructor
,因此构成了隐藏/重定义了。而且a
刚好是A
类型的指针,是一个普通的调用,不是多态调用。对于普通调用,看的是当前者的类型。因此delete a
就会再次调用A
类的析构函数。
但我们想的是指向什么类型,就去调用对应的析构函数,因此这是就得用到多态了。多态调用:看的是其指向的类型,指向什么类型,调用什么类型。
virtual
修饰?答案:为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏。
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override
和final
两个关键字,可以帮助用户检测是否重写。
作用:修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错
先以正确代码为例,override
要写在子类函数括号的后面
#include
using namespace std;
class A
{
public:
virtual void Print()
{cout << "class A" << endl;}
};
class B : public A
{
public:
virtual void Print() override
{cout << "class B : public A" << endl; }
};
void Print(A& a)
{
a.Print();
}
int main()
{
A a;
B b;
Print(a);
Print(b);
return 0;
}
【输出结果】
以下故意在子类的虚函数加个参数(不构成三重:子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),看看是否会报错
作用:修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态
对父类的虚函数加上final
:无法构成重写
除此之外,final
在某些场景下很实用:final
还可以修饰父类,修饰后,父类不可被继承。
注:final
可以修饰子类的虚函数,因为子类也有可能成为父类;但override
无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写。
面试题中也喜欢考这三者的区别
重载:即函数重载。在同一个作用域中,通过参数的类型、个数或顺序不同来定义多个具有相同函数名但不同参数列表的方法。重载方法在编译时根据调用的参数匹配最合适的方法。
重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则(函数名、参数列表和返回类型必须与父类中的方法一致)时,则会发生重写(覆盖)行为
重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数。如果想使用父类的同名成员,可以通过::
指定调用。
重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义。注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)