C++多态
多态
多态的概念
多态就是就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
多态的实现和实现
不同功能的函数用同一个函数名可以调用不同的内容的函数
在继承中的2个必要的条件:
1.必须通过基类的指针或引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写
虚函数
虚函数:即被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 void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;//普通人
Student st;//学生
Func(ps);
Func(st);
return 0;
}
普通人调用则是全价票而学生则是半价。
注意:
虚继承的virtual是解决数据冗余和二义性的,这里的virtual是实现多态的二者没有关联
虚函数的重写的2个例外
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A {};
class B : public A {};
class Person {
public:
virtual A* f()
{
cout << "A* f()" << endl;
return new A;
}
};
class Student : public Person {
public:
virtual B* f()
{
cout << "B* f()" << endl;
return new B;
}
};
int main()
{
Person p;
Student s;
Person* ptr1 = &p;
Person* ptr2 = &s;
ptr1->f();
ptr2->f();
return 0;
}
也完成了父类调用输出的父类,子类调用输出子类的。
析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
在普通场景下父子类构不构成重写不重要
class Person {
public:
~Person() { cout << "~Person()" << endl; }
// vortual ~Person() { cout << "~Student()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
// vortual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
//普通场景
Person p;
Student s;
return 0;
}
加不加virtual的效果是一样的,下面的场景:
Person* p1 = new Person;
Person* p2 = new Student;
//调用父类,完成父类的析构
//调用子类,完成子类的析构完成多态的行为
delete p1;
delete p2;
不加virtual
就都是调用父类的析构函数,而子类却没有完成析构会造成内存泄漏
加上virtual
子类的析构完成后会自动调用父类的析构函数
在继承的时候父类和子类的析构函数是构成隐藏的原因就在这里,表面上父类和子类的析构函数名不同但是编译器统一改成了destructor就是为了重写
C++11 override 和 final
final:修饰虚函数,表示该虚函数不能再被继承
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
//不能被重写
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
重载、覆盖(重写)、隐藏(重定义)的对比(重点)
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
抽象类不可以实例化对象。重写之后:
抽象类的价值:
1.更好的表示现实中没有实例对象对应的抽象类型
2.强制子类重写虚函数
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base a;
cout << sizeof(a) << endl;
return 0;
}
通过监视窗口发现a对象里有个vfptr.
接下来看看虚表中放着什么。
B类中有2个虚函数和1个普通的成员函数,子类D对Func2进行了重写
class B
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
virtual void Func2()
{
cout << "B::Func2()" << endl;
}
void Func3()
{
cout << "B::Func3()" << endl;
}
private:
int _b = 1;
};
class D : public B
{
public:
virtual void Func1()
{
cout << "D::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
B b;
D d;
return 0;
}
总结一下子类中虚表生成:先把父类的虚表拷到子类的虚表中,子类如果重写了父类中的虚函数,用子类自己的虚函数覆盖表中的父类的虚函数,自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。
原理
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
int _p = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _a = 2;
};
void Func(Person& p) {
p.BuyTicket();
}
int main()
{
Person p;
Func(p);
Student s;
Func(s);
return 0;
}
当指向s的时候BuyTicke在虚表中找到的是Person::BuyTicke,指向m对象时,BuyTicke在虚表中找到的是Student::BuyTicke. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态.
满足多态的条件。
为什么是父类的指针或者是引用调用虚函数呢?普通对象不行吗?
如果给的是父类对象,那指针或引用就到父类中找到虚函数,如果是子类对象,那切的子类中父类的那部分,指向的是父类的那部分,或者是引用找到虚函数。再来看看普通对象
静态绑定和动态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
面试题:虚表存在哪?
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
int main()
{
Base b;
Base* p = &b;
printf("vfptr的地址:%p\n", *((int*)p));
int i;
printf("数据段的地址:%p\n", &i);
int* k = new int;
printf( "堆上的地址: %p\n" ,k);
const char* c = "hello";
printf("代码段的地址: %p\n" ,c);
return 0;
}
通过打印知道虚表存在代码段。
单继承和多继承关系的虚函数表
单继承:
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
监视窗口只能看见func1和func2的地址,在通过内存可以看到func3和func4的地址,通过代码来打印func3和func4的地址
//这里可以用typedef,但再别出不能乱用
typedef void(*VFPTR) ();
void PrintVTable(VFPTR *ptr)
{
// 依次取虚表中的虚函数指针打印并调用。
//调用就可以看出存的是哪个函数
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :%p->" ,i,ptr[i]);
ptr[i]();
}
printf("\n");
}
int main()
{
Base b;
Derive d;
//先强转成int*,在解引用,再强转成VFPTR*
//因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组
PrintVTable((VFPTR*)(*(int*)&b));
PrintVTable((VFPTR*)(*(int*)&d));
return 0;
}
和内存中的地址是一样的。子类继承了父类的func1和func2并且对func1进行了重写,还有自己的func3和func4.
多继承中的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
多继承反馈的:子类中有2张虚表,子类自己没重写的虚函数放在继承第一个父类的虚表中和子类重写的func1和Base1的func2放在第一张虚表中,第二张虚表中放着对Base2的func1的重写和继承和的func2.
菱形继承和菱形虚拟继承比较复杂,感兴趣的小伙伴可以自己探讨
本篇的多态就到这里。作者水平有限,如有错误还请私信博主,感谢!!!