本篇文章主要讲解 多态 的相关内容
1. 多态的概念
1.1 概念
多态就是多种形态,就是当不同的对象执行某个行为,会产生不同的状态。
比如买票,普通人购买是全面票,学生购买是学生票.
2. 多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象
,去调用同一函数
,产生了不同的行为。比如Student
继承了Person
。Person
对象买票全价,Student
对象买票半价。
多态构成需要有两个条件:
1. 虚函数构成重写;
2. 父类的指针或者引用来调用虚函数。
2.2 虚函数
虚函数:被virtual
修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.3 虚函数的重写
重写:派生类有一个和基类相同的函数,即要满足三同:1. 函数名相同 2. 参数列表相同 3. 返回值相同
这时就称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void buyTickets()
{
cout << "Person::全价票" << endl;
}
};
class Student : public Person
{
public:
virtual void buyTickets()
{
cout << "Student::半价票" << endl;
}
};
void test1()
{
Person* p = new Person;
Person* st = new Student;
p->buyTickets();
st->buyTickets();
delete p;
delete st;
}
void test2(Person& p)
{
p.buyTickets();
}
int main()
{
test1();
Person p;
Student st;
test2(p);
test2(st);
return 0;
}
虚函数的三个例外
虚函数有三个例外:
1. 协变
2. 析构函数
3. 派生类的virtual
关键字可以省略
接下来是详细分解:
协变
我们之前要求虚函数要满足返回值的类型相同。
但是协变规定:虚函数返回值
可以不同
,但是
,返回值必须保证是父子关系的指针或者引用
,即基类
虚函数返回基类对象
的指针或者引用,派生类
虚函数返回派生类对象
的指针或者引用。
这里的基类对象和派生类对象的指针或者引用, 可以是其他类的,也可以是自己本身的类,如以下代码所示:
class A {};
class B : public A {};
class Person
{
public:
//virtual A* buytickets()
virtual Person* buytickets()
{
cout << "Person::全价票" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
//virtual B* buytickets()
virtual Student* buytickets()
{
cout << "Student::学生票" << endl;
return nullptr;
}
};
int main()
{
Person* p = new Person;
Person* st = new Student;
p->buytickets();
st->buytickets();
return 0;
}
析构函数
当父类的指针指向子类,在
delete
的时候,delete
会根据指针的类型调用对应的析构函数。
因此,如果说派生类中有动态开辟的空间,不会被释放,会导致内存泄漏。
比如这段代码:
class Person
{
public:
virtual void buytickets()
{
cout << "Person::全价票" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void buytickets()
{
cout << "Student::学生票" << endl;
}
~Student()
{
delete[] a;
cout << "~Student()" << endl;
}
protected:
int* a = new int[10];
};
int main()
{
Person* p1 = new Person;
// 父类指针指向子类
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
所以,为了解决这个问题,如果基类和派生类构成重写,指向父类的指针调用父类的析构,指向子类的指针调用子类的析构函数,就可以解决这个问题了,但是子类和父类的析构函数函数名称不相同,是否能构成重写呢?
是可以的,析构函数经过编译器的内部处理,函数名会被修饰为destructor
,因此,可以构成重写,只需要加上virtual
即可。
因此一般推荐具有继承的类的析构函数要构成重写,也就是加上virtual
关键字
代码如下:
class Person
{
public:
virtual void buytickets()
{
cout << "Person::全价票" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void buytickets()
{
cout << "Student::学生票" << endl;
}
virtual ~Student()
{
delete[] a;
cout << "~Student()" << endl;
}
protected:
int* a = new int[10];
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
这里为什么会出现两次
Person
的析构上一篇文章讲过,因为析构要保证先子后父,所以派生类的析构函数会自动在最后调用父类的析构函数。
关于virtual(不重要)
在重写基类虚函数时,派生类的虚函数可以不加
virtual
关键字,也构成重写,但是一般不推荐这样写。
class Person
{
public:
virtual void buytickets()
{
cout << "Person::全价票" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
// 不加virtual也可以
void buytickets()
{
cout << "Student::学生票" << endl;
}
// 不加virtual也可以
~Student()
{
delete[] a;
cout << "~Student()" << endl;
}
protected:
int* a = new int[10];
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
p1->buytickets();
p2->buytickets();
delete p1;
delete p2;
return 0;
}
2.4 override & final
从上面可以看出,
C++
对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug
会得不偿失,因此:C++11
提供了override
和final
两个关键字,可以帮助用户检测是否重写。
final
- final修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
// 这里不能重写了
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- final 修饰类,改类不能被继承
class Car final
{
public:
virtual void Drive() final {}
};
// 不能继承
class Benz :public Car
{
public:
// 这里不能重写了
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
// 检查是否重写成功
virtual void Drive() override{ cout << "Benz-舒适" << endl; }
};
2.5 重写、重载、隐藏的区别
重载:两个函数在同一个作用域,函数名相同,参数列表不同,构成重载(下图有点错误)。
重写:两个函数分别在派生类和基类作用域,三同,两函数必须是虚函数。
隐藏:两函数必须在派生类和基类作用域,函数名相同构成重写。
也就是说:两个基类和派生类的同名函数不构成重写就是隐藏。