1.什么是多态
我们都知道面向对象语言的三大特性,封装,继承,多态;
- 封装:封装就是将数据封装在一个类里面,提供对数据更好的管控;
- 继承:继承就是类设计层次的代码复用。
那多态是什么呢?多态是一种现象,这种现象要通过封装和继承才能实现。多态就是在同一继承体系下,不同的类的对象 调用相同的函数,表现出不同的的结果。
2.如何实现多态
2.1多态的条件
多态的构成是有条件的:
- 条件一:子类必须完成父类中虚函数的重写(多态调用下的重写是实现重写)
- 条件二:通过父类的指针or引用调用对应的虚函数。
- (被virtual修饰的类成员函数叫做虚函数)
2.2如何重写父类中的虚函数
子类中如何完成虚函数的重写呢?虚函数的重写需要满足三同,函数名相同、函数参数相同、函数返回值类型相同。注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写 (因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性) ,但是不建议这样写。多态实现示例代码:
class person
{
public:
virtual void func()
{
cout << "person:" << endl;
}
};
class student : public person
{
public:
virtual void func()
{
cout << "student:" << endl;
}
};
int main()
{
person* ptr1 = new student;
person* ptr2 = new person;
ptr1->func();
ptr2->func();
delete ptr1;
delete ptr2;
return 0;
}
程序运行结果:
student:
person:
2.2.1虚函数重写的两个例外
虚函数的返回值不同:完成虚函数重写的时候,如果子类中重写的虚函数的返回值是子类对象的指针,父类中被重写的虚函数的返回值是父类对象的指针。此时,虽然父子类中虚函数的返回值不同,但是也构成重写,这种情况叫做协变。示例代码如下:
class A
{};
class B : public A
{};
class person
{
public:
virtual A* func()
{
return new A;
}
};
class student : public person
{
public:
virtual B* func()
{
return new B;
}
};
虚函数的函数名不同:只适用于父子类的析构函数;如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写;虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成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;
}
2.2.2虚函数重写的检查
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。
final关键字:如果一个类不想被其他类继承,可以使用final关键字;同样,如果一个虚函数不想被重写,也可以使用final关键字修饰。示例代码如下:父类中的func虚函数不能被重写。
class Person
{
public:
virtual void func() final {}
};
class Student : public Person
{
public:
virtual void func() {cout << "不能重写" << endl;}
};
override关键字:如果我们明确要对哪个虚函数进行重写,需要检查是否对该虚函数进行了重写,我们可以使用override关键字帮助我们检查,示例代码如下:如果该虚函数没有被重写,会报错。
class Person
{
public:
virtual void func() {}
};
class Student : public Person
{
public:
virtual void func() override {}
};
3.抽象类
3.1抽象类简介
什么是抽象类?在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类使用代码如下:
class Person // 抽象类
{
public:
virtual void func() = 0; // 纯虚函数
};
class Student : public Person
{
public:
virtual void func()
{
cout << "hello" << endl;
}
};
3.2接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.一个例题
请问下面这段程序输出什么?
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();
p->func();
return 0;
}
- p->test():p是一个子类对象的指针,调用test()函数,B类中没有实现test函数,但是,B类中继承了A类的test函数,所以调用的是A类中的test函数(注:B类继承A类,并不是说A类中的成员变量和成员方法都要拷贝一份给B类,而是B类的对象可以使用A类中的成员变量和成员方法),A类中的test函数的参数是 A* this;在test函数中,通过this指针(父类的指针)调用func函数,同时,B类中完成了func函数的重写(多态调用的重写是实现重写),多态的两个条件都满足,所以此时的func是多态调用,也就是说父类的指针指向谁就调用谁。所以调用的是B类中的func函数,那是不是输出B->0呢?不是的,因为虚函数的继承是接口继承,所以使用的还是A类中的func函数的声明。最后输出B->1。
-
p->func():此时,通过子类的指针调用重写的虚函数,不满足多态调用的所有条件,所以是普通调用,普通调用看的是指针或者引用或者对象的类型,所以此时调用的是B类中的func函数;因为不是多态调用,所以不需要使用父类中的func函数的声明。所以输出B->0。
5.区分三个概念
重载:重载通常是指函数重载;
- 重载的条件:同一作用域中,函数名相同,参数不同,就构成重载。
重写:重写也叫覆盖,重写是语法层的概念,覆盖是原理层的概念;
- 重写的条件:分别在父子类的作用域中,两个函数都是虚函数,且函数名,函数参数类型,函数返回值类型都相同。(除了那两个例外)
隐藏:隐藏也叫重定义;
- 父子类中的同名成员如果不构成重写,就构成隐藏。