去完成某个行为,当不同的对象去完成时会产生出不同的状态。
静态多态:通过和函数重载和模板实现。
动态多态:通过继承中虚函数的重写+基类指针(基类的引用)调用虚函数实现,同样的函数,指向不同的对象,调用不同的函数实现。
两个条件:
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; } /*void BuyTicket() //再重写基类的虚函数时,派生类重写的虚函数可以不加>virtual关键也,也可以构成重写(因为继承后基类的虚函数被继承下来了在派>生类依旧保持虚函数属性),这种写法不是很规范,不建议这样使用 { cout << "买票半价" << endl; }*/ };
虚函数重写的两个例外:
1.协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。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; } };
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名字不同,但是这里编译器对析构函数的名称做了特殊化处理,编译后析构函数的名称是统一处理成destructor。class Person { public: virtual ~Person() { cout << "~Person()" << endl; }; }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; }; }; int main() { Person* p = new Person; Person* s = new Student; delete p; delete s; return 0; }
1.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就编译报错。
class Car { public: virtual void Drive() {} }; class Benz : public Car { public: virtual void Drive() override { cout << "Benz-舒适" << endl; } //virtual void func() override {} //error,基类中没有func这个虚函数 };
2.final:修饰虚函数表示该虚函数不能被重写。修饰类表示该类不能被继承。
class Car { public: virtual void Drive() final {} }; class Benz : public Car { public: /* error,基类该虚函数不能被重写 virtual void Drive() override { cout << "Benz-舒适" << endl; }*/ };
class A final {}; class B : public A {}; //error,A类不能被B类继承
在虚函数的后面写上=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; } }; int main() { Car* pBenz = new Benz; Car* pBMW = new BMW; pBenz->Drive(); pBMW->Drive(); return 0; }
在类中普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不是先多态,不要把函数定义成虚函数。
class Base
{
public:
virtual void func1()
{
cout << "func1()" << endl;
}
private:
int _b = 2;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
在b对象中处理_b成员,还多了一个__vfptr放在成员的前面,对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也称虚表。
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
void func3()
{
cout << "Base::func3()" << endl;
}
private:
int _b = 2;
};
class Drive : public Base
{
public:
virtual void func1()
{
cout << "Drive::func1()" << endl;
}
private:
int _d = 3;
};
int main()
{
Base b;
Drive d;
return 0;
}
通过观察和测试
1.派生类d中也有一个虚表指针,d对象由两部分构成一部分是基类继承下来的成员,虚表中指向的函数包含了基类的虚函数,和派生类重写的虚函数,可以这样理解,虚表从基类复制一份到派生类,然后派生类将重写的虚函数的地址覆盖到虚表中去。
2.基类b对象和派生类d对象虚表是不一样的,func1完成了重写,所以d对象的虚表中存的是重写的Drive::func1(),所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.func2继承下来是虚函数,放进虚表,func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组的最后放了一个nullptr。
5.派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6.虚表在vs下是存在代码段的。
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 p;
func(p);
Student s;
func(s);
return 0;
}
- 观察下图的红色箭头我们看到,p是指向Person对象时,p->BuyTicket在p的虚表中找到虚函数是Person::BuyTicket。
- 观察下图的橙色箭头我们看到,p是指向Student对象时,p->BuyTicket在s的虚表中找到虚函数是Student::BuyTicket。
- 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
- 派生类对象赋值给基类对象时,会发生切片,切片时就会发生拷贝,但是虚表是不能拷贝的,因为如果将派生类的虚表指针的地址拷贝给基类,那么基类对象就可以调用派生类的成员函数,会乱套,所以派生类的虚表不会拷贝到基类的对象中去,这样也就无法实现基类对象调用派生类对象。
5.满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中找到的。不满足多态的函数调用时编译时确认好的。
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
virtual void func4() { cout << "Derive::func3()" << endl; }
private:
int _b = 2;
};
观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。使用代码打印
出虚表中的函数.
typedef void(*VFPTR)();
void PrintVTable(VFPTR v[])
{
cout << "虚表地址:" << v << endl;
for (int i = 0; v[i]; ++i)
{
printf("[%d]:%#xp->", i, v[i]);
v[i]();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
PrintVTable(*(VFPTR**)&b);
PrintVTable(*(VFPTR**)&d);
return 0;
}
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
private:
int _b1 = 1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void func2() { cout << "Base2::func2()" << endl; }
private:
int _b2 = 2;
};
class Derive : public Base1,public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
private:
int _d = 3;
};
typedef void(*VFPTR)();
void PrintVTable(VFPTR v[])
{
cout << "虚表地址:" << v << endl;
for (int i = 0; v[i]; ++i)
{
printf("[%d]:%#xp->", i, v[i]);
v[i]();
}
cout << endl;
}
int main()
{
Derive d;
PrintVTable(*(VFPTR**)&d);
Base2* p = &d;
PrintVTable(*(VFPTR**)p);
return 0;
}
观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
而且从第一个继承的虚表和第二个继承的虚表中,可以看到在Derive中重写func1在两张虚表中的函数地址不同。