1.什么是多态
在C++中,多态(Polymorphism)是指通过基类指针或引用来访问派生类对象的一种机制。简单来说,它允许我们在基类类型的指针或引用上调用派生类对象的成员函数。
通俗来说,就是多种形态,或者说完成某个行为时,当不同的对象去完成会产生出不同的状态。
例如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。不同的对象去完成同一个行为——买票,会产生不同的状态。
在代码中的具体体现则依赖于虚函数(Virtual Function)。在基类中,可以将某个成员函数声明为虚函数,而在派生类中重写该函数。通过使用基类指针或引用指向派生类对象,并调用该虚函数,实际上在运行时会根据对象的实际类型调用合适的函数。
虚函数:即被virtual
修饰的类成员函数称为虚函数
class Person {
public:
//虚函数
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
//虚函数
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//虚函数重写
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
在重写基类虚函数时,派生类的虚函数在不加virtual
关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,所以不建议这样使用
注意这里与继承中的隐藏区分一下,隐藏是只要在派生类中有与基类函数名相同的函数就构成,而重写则需要返回值、函数名和参数列表完全相同才构成
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上述代码Student类继承了Person。而Person对象买票全价,Student对象买票半价。
在继承中要构成多态有两个条件:
例如:
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 ps;
Student st;
Func(ps);
Func(st);
return 0;
}
多态的实现结果如下:
通过使用基类引用p指向派生类对象,并调用虚函数
BuyTicket()
,实际上在运行时会根据对象的实际类型调用合适的函数来实现多态,也就是不同的对象完成同一个行为会有不同的状态——普通人买票全价,学生买票半价
这种通过基类指针或引用调用派生类对象的成员函数的行为称为多态。它使得我们可以在不同类型的对象上使用相同的接口,提供了更高的灵活性、可扩展性和代码复用性。
前面我们学习过虚函数重写必须要求基类与派生类除了函数体以外其它完全相同,但是对于协变,基类与派生类的返回值类型可以不同,但基类与派生类的函数的返回类型必须是继承关系
//协变
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }//基类虚函数返回值类型为基类指针或引用
};
class Student : public Person {
public:
virtual B* f() { return new B; }//派生类虚函数返回值类型为派生类指针或引用
};
当然基类虚函数返回值类型可以是别的基类也可以是自己,派生类虚函数返回值类型可以是别的派生类也可以是自己,与基类相呼应:
class Person {
public:
virtual Person* f() { return new Person; }//基类虚函数返回值类型为基类指针或引用
};
class Student : public Person {
public:
virtual Student* f() { return new Student; }//派生类虚函数返回值类型为派生类指针或引用
};
这是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成
destructor
。
例如:
//析构函数重写
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
//构成多态条件二:基类虚函数并且派生类虚函数重写
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
//构成多态条件一:使用基类指针或引用调用虚函数(这里是析构函数)
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
结果如下:
这里要注意派生类的析构调用完之后会自动调用基类对象的析构函数,所以这里基类的析构函数调用了两次
我们可以对比一下,当没有实现多态时,对于delete对象调用析构函数是不会根据所指向的对象调用相应的析构函数,而是直接调用Person类的析构函数:
//没有实现多态
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
结果如下:
如果是这样,那么如果子类Student类中动态开辟了一块空间而没有调用合适的析构函数就会造成内存泄漏:
//内存泄漏
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
int* ptr = new int[10];//动态开辟空间
~Student() {
delete[] ptr;//释放空间
cout << "~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
在类的继承关系中,派生类的析构函数会自动调用基类的析构函数。因此,可以重写(覆盖)基类的析构函数,以处理派生类特有的资源清理需求。
尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态。
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
class Person {
public:
virtual void BuyTicket() final { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
结果如下:
此外final也可以修饰类表明该类不可被继承。
class Person {
public:
virtual void BuyTicket(){ cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() override{ cout << "买票-半价" << endl; }
};
重写了BuyTicket(),编译不报错
如下图所示,屏蔽了基类的虚函数,派生类的函数没有重写,编译报错:
重载与重定义主要区别在于作用域,而重定义与重写主要区别在于函数返回值与函数参数列表是否相同。
重写是重定义的一种特殊形式,重定义中包括重写
在虚函数的后面写上 =0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
纯虚函数是在基类中声明的虚函数,但没有给出具体的实现,也就是没有函数体。抽象类只能用作其他类的基类,不能被直接实例化。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
派生类必须实现基类中的所有纯虚函数,否则派生类也会成为抽象类。
例如:
//抽象类
class Person {
public:
virtual void BuyTicket() = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
在上面的示例中,Person是一个抽象类,定义了一个纯虚函数:BuyTicket()。Student是Person的派生类,必须实现基类中的纯虚函数
注意:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
对于多态,我们可以将其理解为同一件事,不同的对象去做会有不同的状态,这就构成了多态,这与我们的现实生活息息相关,比如上文中举例的买票行为,再比如价格歧视等等。而我们在编程中要实现多态就必须要满足两个条件:一个是基类的指针或引用来调用虚函数,另一个则是基类中定义虚函数并且在派生类中对该虚函数进行重写;这两个条件缺一不可,这与多态实现的底层原理有关,我们后续再了解。对于虚函数重写的两个例外中析构函数的重写要掌握清楚,此外对于重载、重写与重定义的区别我们也要弄明白。