2. 多态的定义及实现
首先多态实现的前提必须是继承!
多态实现的两个条件:
1.必须使用父类(基类)的指针或者引用调用虚函数;
2.被调用的函数必须是虚函数,且子类(派生类)必须对虚函数进行重写;
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了
Person 。 Person 对象买票全价, Student 对象买票半价。
2.1多态调用
class Person { public: virtual void Buyticket() { cout << "Person:全价" << endl; } }; class Student:public Person { public: virtual void Buyticket() { cout << "Student:半价" << endl; } }; void func(Person& p) //切片 { p.Buyticket(); } int main() { Person p; Student s; func(p); func(s); }
2.2普通调用:
不符合多态条件即可:
1.
void func(Person p)//不是指针或者引用,就是对象 { p.Buyticket(); } int main() { Person p; Student s; func(p); func(s); }
那么我们可以发现:
普通调用跟调用对象的类型有关;
多态调用必须是父类的指针或者引用,无论是是哪个对象传,他都会指向该对象中父类的那一部分(切片),进而调用该对象中的虚函数。一句话,多态调用跟 指针/引用 指向的对象有关
2.3析构函数建议加virtual吗?
我们看一个例子
class Person { public: ~Person() { cout << "Person delete" << endl; delete _p; } protected: int* _p = new int[10]; }; class Student :public Person { public: ~Student() { cout << "Student delete" << endl; delete _s; } protected: int* _s = new int[20]; }; int main() { //Person p; //Student s; Person* ptr1 = new Person; Person* ptr2 = new Student; delete ptr1; delete ptr2; }
我们都知道,析构函数自动调用,在继承中,子类会先析构,调用子类的析构函数以后,自动再调用父类的析构函数。
但这用情况还适用吗?
先看一下结果:
我们发现,居然调用了两次父类的析构函数 !!!
这种情况就会造成子类对象中的成员变量没有释放,导致内存泄露!!
我们知道:
delete有两种行为:1.使用指针调用析构函数;2.operator delete(ptr)
所以使用指针调用析构函数是普通调用(不满足多态调用的条件),普通调用是跟调用的对象类型有关,类型都是Person,所以只会调用person的析构函数
但此时我们更希望的是多态调用,所以建议加virtual,指针指向的对象是哪个,就调用哪个的析构函数。但此时我们会想,析构函数名字都不一样,这能构成重写吗?当然可以,那是因为编译器会自动把父类子类的析构函数名字换成一样的:ptr->destructor()。
那么就可以实现我们预期的效果:
所以我们建议:再写析构函数时,可以无脑给父类的析构函数加virtual,防止出现上面的情况,导致内存泄露 。
普通调用时,时普通调用;父类的指针或者引用调用时,时多态调用,互不影响!
2.4抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car { public: virtual void Drive() = 0; }; class BMW :public Car { public: virtual void Drive() { cout << "BMW-操控" << endl; } }; void Test() { Car* pBMW = new BMW; pBMW->Drive(); } int main() { Test(); }
总结:有些类不需要类的对象,可以在写成纯虚函数。
2.5接口继承和实现继承
接口继承针对虚函数;实现继承针对普通函数。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。
学了这么多,来做一道题温习一下: (很坑)
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; } A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确 若是ptr->func(),就是B类对象直接调用,就是普通调用,普通调用跟对象类型有关。 普通调用在编译时就会静态绑定,在编译时调用的函数以及函数的默认值就已经确定,子类调用子类自己的函数,跟父类没有任何关系,函数都是子类编译时就已经静态绑定的,所以缺省值依然是0。最终结果是B->0
答案选哪个??
首先我们了解的第一点是,继承父类的成员,会原封不动的继承到子类;
我们接下来看:创建了一个B对象的指针,指针来调用p->test(),这时候,会直接调用父类中的test,再this->func(),此时的this的类型是A*,因为test处于A类中,继承到B中,也会原封不动的继承过去,this依然是A*,所以父类的指针调用虚函数,满足多态的调用,多态调用是看指针指向的对象,又因为p调用的test,所以指针指向B对象,所以会调用B的重写的func虚函数,所以最终答案是B->1.(其实多态调用一直是调的父类的接口,再根据指向的对象去调用具体的实现,后面会详细讲到)
当B对象自己调用函数func时,当不是多态调用时,就会直接调用自己的func(),缺省值还是自己的val=0.