餐饮营销网站建设,外贸seo网站搭建,主机网站,手机优化大师官方免费下载文章目录 多态的构成条件虚函数虚函数的重写#xff08;覆盖#xff09; 虚函数重写的两个例外C11 override和final区分重载、覆盖(重写)、隐藏(重定义)抽象类接口继承和实现继承多态的原理虚函数表 动态绑定和静态绑定动态绑定静态绑定 单继承中的虚函数表多继承中的虚函数表… 文章目录 多态的构成条件虚函数虚函数的重写覆盖 虚函数重写的两个例外C11 override和final区分重载、覆盖(重写)、隐藏(重定义)抽象类接口继承和实现继承多态的原理虚函数表 动态绑定和静态绑定动态绑定静态绑定 单继承中的虚函数表多继承中的虚函数表 多态的构成条件
在继承中要构成多态还有两个条件 1、必须通过基类的指针或者引用调用虚函数。
2、被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写。
//父类
class Person
{
public://父类的虚函数virtual void BuyTicket() const{cout 买票-全价 endl;}
};//子类
class Student : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket() const{cout 买票-半价 endl;}
};引用
//void func(const Person p)
//{
// p.BuyTicket();
//}//指针
void func(const Person * p)
{p-BuyTicket();
}int main()
{ //多态条件// 1、调用函数必须是重写的虚函数//基类必须是指针或者引用//多态,不同对象传递过去调用不同参数//多态调用看指向的对象//普通对象看当前类型//引用/*func(Person());func(Student());*///指针Person pp;func(pp);Student st;func(st);return 0;
}虚函数
被virtual修饰的类成员函数被称为虚函数。
class A
{
public:virtual void func(){cout virtual void func() endl;}
};注意 只有类的非静态成员函数前可以加virtual普通函数前不能加virtual
虚函数这里的virtual和虚继承中的virtual虽然是同一个关键字但是它们之间没有任何关系。
虚函数的virtual是为了实现多态 虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
虚函数的重写覆盖
如果派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同这里所说的参数列表是指参数类型要相同) 此时我们称该派生类的虚函数重写了基类的虚函数。
//父类
class Person
{
public://父类的虚函数virtual void BuyTicket(){cout 买票-全价 endl;}
};
//子类
class Student : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout 买票-半价 endl;}
};
//子类
class Soldier : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout 优先-买票 endl;}
};
通过父类Person的指针或者引用调用虚函数BuyTicket此时不同类型的对象调用的就是不同的函数产生的也是不同的结果进而实现了函数调用的多种形态。
void Func(Person p)
{//通过父类的引用调用虚函数p.BuyTicket();
}
void Func(Person* p)
{//通过父类的指针调用虚函数p-BuyTicket();
}
int main()
{Person p; //普通人Student st; //学生Soldier sd; //军人Func(p); //买票-全价Func(st); //买票-半价Func(sd); //优先买票Func(p); //买票-全价Func(st); //买票-半价Func(sd); //优先买票return 0;
}
注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用
虚函数重写的两个例外
协变基类与派生类虚函数的返回值类型不同
派生类重写基类虚函数时与基类虚函数返回值类型不同。 即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用称为协变。
//基类
class A
{};
//派生类
class B : public A
{};//基类
class Person
{
public://虚函数virtual A* fun(){cout A* Person::f() endl;return new A;}
};
//派生类
class Student : public Person
{
public:// 虚函数virtual B * fun(){cout B* Person::f() endl;return new B;}};
int main()
{Person p;Student st;//基类指针指向基类对象Person* ptr1 p;//基类指针指向子类对象Person* ptr2 st; //切片//父类指针ptr1指向的p是父类对象调用父类的虚函数ptr1-fun();//父类指针ptr2指向的st是子类对象调用子类的虚函数ptr2-fun();return 0;
}析构函数的重写基类与派生类析构函数的名字不同 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。
//析构函数加上virtual 是虚函数重写为什么
// 因为析构函数都被处理成了destructor这个统一的名字为什么统一处理成destructor
//因为统一处理成destructor是要将派生类和基类的析构函数构成重写,而重写是构成多态的一个重要条件
class Person
{
public:virtual ~Person(){cout ~Person() endl;}
};
//子类
class Student : public Person
{
public:virtual ~Student(){cout ~Student() endl;delete[]ptr;}
protected : int* ptr new int[10];
};int main()
{//Person p;//Student s;//析构顺序先子后父Person* p new Person;delete p;p new Student;delete p;//p-destuctor() operator delete (p)//这里我们希望p-destuctor()是一个多态调用 而不是普通调用return 0;
}在继承当中子类和的析构函数和父类的析构函数构成隐藏的原因就在这里这里表面上看子类的析构函数和父类的析构函数的函数名不同但是为了构成重写编译后析构函数的名字会被统一处理成destructor();
C11 override和final
C11提供了override和final两个关键字可以帮 助用户检测是否重写。
final修饰虚函数表示该虚函数不能再被重写。
//基类
class Person
{
public://虚函数//final修饰虚函数表示该虚函数不能再被重写virtual void BuyTicket() final{cout 买票-全价 endl;}
};//派生类
class Student : public Person
{
public:virtual void BuyTicket()//err{cout 买票-半价 endl;}
};override检查派生类虚函数是否重写了基类的某个虚函数如果没有重写则编译报错
//基类
class Person
{
public://虚函数virtual void BuyTicket() {cout 买票-全价 endl;}
};//派生类
class Student : public Person
{
public://override派生类完成基类的重写就不报错virtual void BuyTicket() override{cout 买票-半价 endl;}
};区分重载、覆盖(重写)、隐藏(重定义) 抽象类
在虚函数的后面写上0则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。
class Car
{
public://纯虚函数virtual void Drive() 0;
};
int main()
{Car c; //errreturn 0;
}派生类继承抽象类后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。
//抽象类class Car
{
public://纯虚函数virtual void Drive() 0;
};//派生类继承抽象类class Benz : public Car
{
public://重写纯虚函数virtual void Drive(){cout Benz-舒适 endl;}
};int main()
{//派生类重写了纯虚函数可以实例化对象Benz b1;Car* p1 b1;p1-Drive();return 0;
}抽象类不能实例化出对象那抽象类存在的意义是什么
抽象类体现了虚函数的继承是一种接口继承强制子类去重写纯虚函数因为子类若是不重写从父类继承下来的纯虚函数那么子类也是抽象类也不能实例化出对象。
接口继承和实现继承
实现继承 普通函数的继承是一种实现继承派生类继承了基类函数的实现可以使用该函数。
接口继承 虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态。
建议 所以如果不实现多态就不要把函数定义成虚函数。
多态的原理
虚函数表
看下面的代码
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};int main()
{Base b;cout sizeof(b) endl;return 0;
}b对象当中除了_b成员外实际上还有一个_vfptr虚函数表指针简称虚表指针放在对象的前面有些平台可能会放到对象的最后面这个跟平台有关。虚函数 的地址要被放到虚函数表中虚函数表也简称虚表
#include iostream
using namespace std;
//父类
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 1;
};
//子类
class Derive : public Base
{
public://重写虚函数Func1virtual void Func1(){cout Derive::Func1() endl;}
private:int _d 2;
};
int main()
{Base b;Derive d;return 0;
}
通过观察我们发现 基类对象b和基类对象d当中除了自己的成员变量之外基类和派生类对象都有一个虚表指针分别指向属于自己的虚表。 实际上虚表当中存储的就是虚函数的地址
派生类虽然继承了基类的虚函数Func1和Func2但是派生类对基类的虚函数Func1进行了重写。所以派生类对象d的虚表当中存储的是基类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖覆盖就是指虚表中虚函数地址的覆盖重写是语法的叫法覆盖是原理层的叫法。
注意Func2是虚函数所以继承下来后放进了子类的虚表而Func3是普通成员函数继承下来后不会放进子类的虚表。并且虚函数表本质是一个存虚函数指针的指针数组一般情况下会在这个数组最后放一个nullptr。
总结派生类的虚表生成步骤如下 1、先将基类中的虚表内容拷贝一份到派生类的虚表
2、如果派生类重写了基类中的某个虚函数则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址
3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
虚表是什么阶段初始化的虚函数存在哪里 虚表实际上是在构造函数初始化列表阶段进行初始化的注意虚表当中存的是虚函数的地址不是虚函数虚函数和普通函数一样都是存在代码段的只是他的地址又存到了虚表当中。另外对象中存的不是虚表而是指向虚表的指针。
那虚表是存在哪里的
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}virtual void Func1() { }virtual void Func2() { }
//protected:int _a 0;
};
class Student : public Person
{
public://派生类重写基类虚函数virtual void BuyTicket(){cout 买票-半价 endl;}virtual void Func3() { }
protected:int _b 1;
};
int main()
{Person ps;Student st; //栈int a 0;printf(栈%p\n, a);//静态区static int b 0;printf(静态区%p\n, b);//堆int* p new int;printf(堆%p\n, p);const char* str hello world;printf(常量区(代码段):%p\n, str);printf(虚表1%p\n,*( (int*)ps ) );printf(虚表2%p\n, *( (int*) st ) );return 0;
}从上述代码可以发现虚表地址与代码段的地址非常接近由此可以得出虚表是存在代码段的。
详细分析下面的代码 为什么当父类Person指针指向的是父类对象Mike时调用的就是父类的BuyTicket 当父类Person指针指向的是子类对象Johnson时调用的就是子类的BuyTicket?
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl;}int _a 1;
};class Student : public Person
{
public://派生类重写基类虚函数virtual void BuyTicket() { cout 买票-半价 endl;}int _b 1;
};int main()
{Person Mike;Student Johnson;Johnson._b 3; //以便观察是否完成切片Person* p1 Mike;Person* p2 Johnson;p1-BuyTicket(); //买票-全价p2-BuyTicket(); //买票-半价return 0;
}对象Mike中包含一个成员变量_a和一个虚表指针对象Johnson中包含两个成员变量_a和_b以及一个虚表指针这两个对象当中的虚表指针分别指向自己的虚表。 通过上图可分析 1、父类指针p1指向Mike对象p1-BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。
2、父类指针p2指向Johnson对象p2BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。
这样就实现出了不同对象去完成同一行为时展现出不同的形态即多态
多态构成的两个条件 1、完成虚函数的重写 2、必须使用父类的指针或者引用去调用虚函数。 完成虚函数的重写是因为需要完成子类虚表当中虚函数地址的覆盖这样才能做到指针指向父类调用父类对象指针指向子类调用子类对象
为什么多态的设计必须使用父类的指针或者引用不使用父类的对象 指针和引用的切片不存在拷贝问题 但是对象的切片需要拷贝 子类赋值给父类对象切片不会拷贝虚表如果拷贝虚表那么父类对象虚表中是父类虚函数还是子类虚函数就不确定了 使用父类的指针或者引用时实际上是一种切片行为切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。 用p1和p2调用虚函数时p1和p2通过虚表指针找到的虚表是不一样的最终调用的函数也是不一样的。
Person p1 Mike;
Person p2 Johnson;使用父类对象时切片得到部分成员变量后会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表他们的虚表指针指向的虚表是一样的。
总结
对象的调用 如果是普通对象的调用不符合多态看调用者的类型普通对象的调用在编译时就确定好了地址 如果调用符合多态看指向的对象在运行时到指向对象的虚函数表中找调用函数的地址从而完成调用
动态绑定和静态绑定
动态绑定
动态绑定又称为后期绑定(晚绑定)在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。
静态绑定
静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也成为静态多态比如函数重载
普通对象的调用
//基类
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
};//派生类
class Student : public Person
{
public:virtual void BuyTicket(){cout 买票-半价 endl;}
};int main()
{Student st;Person p st;//不构成多态,函数的调用是在编译时就确定的p.BuyTicket();return 0;
}将调用函数的那句代码翻译成汇编就只有以上两条汇编指令也就是直接调用的函数。
使用多态调用
//基类
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
protected:int _a 0;
};//派生类
class Student : public Person
{
public:virtual void BuyTicket(){cout 买票-半价 endl;}
protected:int _b 1;
};int main()
{Student st;Person p st;//构成多态,看指向的对象p.BuyTicket();return 0;
}构成多态时调用函数的那句代码翻译成汇编后就变成了八条汇编指令原因就是我们需要在运行时先到指定对象的虚表中找到要调用的虚函数然后才能进行函数的调用。
体现了静态绑定是在编译时确定的而动态绑定是在运行时确定的。
单继承中的虚函数表
//基类
class Base
{
public:virtual void func1() { cout Base::func1() endl;}virtual void func2() {cout Base::func2() endl; }
private:int _a 0 ;
};
//派生类
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 1 ;
};int main()
{Base b;Derive d;return 0;
}派生类和基类的内存分布 在单继承关系当中派生类的虚表生成过程如下
1、继承基类的虚表内容到派生类的虚表。 2、对派生类重写了的虚函数地址进行覆盖比如func1。 3、虚表当中新增派生类当中新的虚函数地址比如func3和func4。
在调试过程中某些编译器的监视窗口当中看不到虚表当中的func3和func4可能是编译器的监视窗口故意隐藏了这两个函数也可以认为这是一个小bug此时想要看到派生类对象完整的虚表有两个方法。
使用内存监视窗口 使用代码打印虚表内容
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{printf(虚表地址:%p\n, ptr);for (int i 0; ptr[i] ! nullptr; i){printf(ptr[%d]:%p--, i, ptr[i]); //打印虚表当中的虚函数地址ptr[i](); //使用虚函数地址调用虚函数}printf(\n);
}
int main()
{Base b;PrintVFT((VFPTR*)(*(int*)b)); //打印基类对象b的虚表地址及其内容Derive d;PrintVFT((VFPTR*)(*(int*)d)); //打印派生类对象d的虚表地址及其内容return 0;
}
多继承中的虚函数表
//基类1
class Base1
{
public:virtual void func1() { cout Base1::func1() endl; }virtual void func2() {cout Base1::func2() endl;}
private:int _b1;
};//基类2
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;
};int main()
{Base1 b1;Base2 b2;Derive d;return 0;
}多继承中派生类的虚表生成过程如下
1、分别继承各个基类的虚表内容到派生类的各个虚表当中。 2、对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖)比如func1。 3、在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址比如func3。
看到派生类对象完整的虚表有两种方法。 一、使用内存监视窗口 二、使用代码打印虚表内容 在派生类第一个虚表地址的基础上向后移sizeof(Base1)个字节即可得到第二个虚表的地址。
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{printf(虚表地址:%p\n, ptr);for (int i 0; ptr[i] ! nullptr; i){printf(ptr[%d]:%p--, i, ptr[i]); //打印虚表当中的虚函数地址ptr[i](); //使用虚函数地址调用虚函数}printf(\n);
}
int main()
{Base1 b1;Base2 b2;PrintVFT((VFPTR*)(*(int*)b1)); //打印基类对象b1的虚表地址及其内容PrintVFT((VFPTR*)(*(int*)b2)); //打印基类对象b2的虚表地址及其内容Derive d;PrintVFT((VFPTR*)(*(int*)d)); //打印派生类对象d的第一个虚表地址及其内容PrintVFT((VFPTR*)(*(int*)((char*)d sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容return 0;
}