甘孜建设机械网站首页,贵州建设公司网站,免费网页设计制作网站,网站建设 模板网站1.多态的概念
1.1 概念
多态的概念#xff1a;通俗来说#xff0c;就是多种形态#xff0c;具体点就是去完成某个行为#xff0c;当不同的对象去完成时会 产生出不同的状态。
举个例子#xff1a;在现实当中#xff0c;我们去火车站买票#xff0c;一般都分三种情况通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。
举个例子在现实当中我们去火车站买票一般都分三种情况当普通人买票时是全价买票学生买票时可以销售半折优惠军人买票时可以享受优先购票
2.多态的定义与实现
2.1多态的构成条件
多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。在继承中要构成多态的条件有两个
1.必须通过基类的指针或者引用去调用虚函数
2.被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
被virtual修饰的成员函数称为虚函数只要成员函数才能变成虚函数
class person
{
public:virtual void buyticket(){cout 全价 endl;}
};
2.2虚函数的重写
虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数( 即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同 )称子类的虚函数重写了基类的虚函数。
class person
{
public:virtual void buyticket(){cout 全价 endl;}
};class student : public person
{
public:virtual void buyticket(){cout 半折 endl;}
};void buyticket(person* p)
{p-buyticket();
}void buyticket(person p)
{p-buyticket();
}int main()
{student s;person p;buyticket(s); //半折buyticket(P); //全价buyticket(s); //半折buyticket(p); //全价return 0;
}
以上派生类中就完成了重写覆盖了
注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范建议在派生类对的虚函数前加上virtual
虽然说虚函数的重写需要符合三同返回类型函数名参数列表主要是类型
但是存在两个例外
1.协变基函数与派生类虚函数放会值类型不同
C的语法允许派生类和基类中虚函数返回值类型不同但是要求返回值必须是基类和派生类关系的指针或者引用。满足前面要求就是协变
class person
{
public:virtual person* buyticket(){cout 全价 endl;return 0;}
};class student : public person
{
public:virtual student* buyticket(){cout 半折 endl;return 0;}
};
注意只要是父子关系的指针或者引用都可以父是父子是子指针或者引用要匹配
class A{};
class B : public A{};
class person
{
public:virtual A* buyticket(){cout 全价 endl;return 0;}
};class student : public person
{
public:virtual B* buyticket(){cout 半折 endl;return 0;}
};
析构函数可以是虚函数吗为什么是虚函数
答
析构函数加virtual是不是虚函数重写
是因为在类中析构函数被特殊处理成了destructor这个统一的名字。
为什么要这么处理
因为要让它们构成重写。
为什么要让他们构成重写
因为以下的场景
class person
{
public:~person(){cout ~person endl;}
};class student : public person
{
public:~student(){cout ~student endl;}
};int main()
{person* p new person;delete p;p new student;delete p;
}
运行之后我们来看看它调用析构情况 可以看到程序运行结束之后只调用了两次析构函数这里可以发现在派生类中少调用了一次析构函数析构派生类部分派生类的析构需要调用基类和它自己的析构函数来进行空间的释放原因就是在类中析构函数名被编译器特殊处理成了destructor导致基类中的析构函数将派生类中的析构函数隐藏/重定义了编译器找不到派生类中的析构函数所以就会造成以上的结果
在delete释放派生类空间的时候需要进行两个操作 1.调用析构函数destructor进行资源的释放 2. 调用operator delete() 释放整个空间 )我们这里所期望的是一个多态调用而不是一个普通调用
解决方法使用虚函数进行多态调用
class person
{
public:virtual ~person(){cout ~person endl;}
};class student : public person
{
public:virtual ~student(){cout ~student endl;}
};int main()
{person* p new person;delete p;p new student;delete p;
} 2.3 C11 override 和 final
final
对函数使用不允许该函数进行重写操作
class person
{
public:virtual void buyticket() final{cout 全价 endl;}
};class student : public person
{
public:virtual void buyticket(){cout 半折 endl;}
}; 对类使用不允许该类被继承
class person final
{
public:virtual void buyticket(){cout 全价 endl;}
};class student : public person
{
public:virtual void buyticket(){cout 半折 endl;}
}; override
帮助派生类检查是否完成重写如果没有就会报错
class person
{
public:virtual void buyticket(){cout 全价 endl;}
};class student : public person
{
public:void buyticket(int) override{cout 半折 endl;}
};
设计题设计一个类不想被继承应该怎么设计不能使用final
方法一将基类构造函数私有C98
class A
{
private:A(){}
};class B : A
{
public:int _a;
};int main()
{B a;return 0;
} 基类中构造函数被封装之后派生类就无法调用构造函数创建对象
如果要进行访问可以使用静态成员函数
class A
{
public:static A createObj(){return A();}int _a 1;
private:A(){}
};class B : A
{
public:int _a;
};int main()
{A p A::createObj();p._a;return 0;
}
方法二使用final不允许基类被继承C11
class A
{
private:A(){}
};class B : A
{
public:int _a;
};int main()
{B a;return 0;
}
2.4 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类
3.1 概念
在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口 类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生 类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。
class car
{
public:virtual void func1() 0; //纯虚函数
};int main()
{car a;return 0;
}
使用纯虚函数实例化对象编译器会直接报错 继承car的派生类对象也不能实例化对象
class car
{
public:virtual void func1() 0; //纯虚函数
};class BMW : public car
{
public:};int main()
{car a;BMW b;return 0;
} 抽象类的使用
class car
{
public:virtual void func1() 0; //纯虚函数
};class BMW : public car
{
public:virtual void func1(){cout 启动 endl;}
};class Benz : public car
{
public:virtual void func1(){cout 刹车 endl;}
};class BYD : public car
{
public:virtual void func1(){cout 减速 endl;}
};void func2(car* c)
{c-func1();
}int main()
{func2(new BMW);func2(new Benz);func2(new BYD);return 0;
}
抽象类也可以实现多态调用抽象类就是用来间接强制要求你重写虚函数的
3.2 接口继承和实现继承
普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。
4.多态的原理
4.1单继承的虚函数表
大家看下面这一道题目
class Base
{
public:virtual void Func1(){cout Func1() endl;}virtual void Func2(){cout Func1() endl;}void Func3(){cout Func1() endl;}
private:char _b 1;
};int main()
{cout sizeof(Base) endl;Base b1;return 0;
} 我们知道算类的大小不算成员函数我们只需要考虑成员变量只有一个成员变量但是最终的运行结果是8我们打开监视窗口看一下base中的成员变量 可以看到在类中多出了_vfptr(v代表virtualf代表function)它是一个指针这里就可以知道为什么sizeof(b1)算出来的值是8
_vfptr在vs下是放在第一个位置存放位置与编译器有关别的编译器可能会放到最后一位
_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 1;
};
class Derive : public Base
{
public:virtual void Func1(){}
private:int _d 2;
};
int main()
{Base b;Derive d;return 0;
}
观察b和d两个对象 可以看到在derive中重写了func1的实现之后d对象中虚函数表也将Base::Func1覆盖成了Derive::Func1所以重写也叫覆盖
通过虚指针指向的地址我们可以找到虚函数表在内存中的储存情况从图中我们可以清楚的看到虚函数表中的最后一个位置被处理为了nullptr这是编译器个性处理 此时我在Derive中新加了一个虚函数func4()看这个函数是否会放到虚函数表中
class Derive : public Base
{
public:virtual void Func1(){}virtual void func4(){}
private:int _d 2;
}; 在监视窗口中看不到函数func4的身影我们看看内存视图 内存视图中确实三个函数的地址所以我猜测这最后一个地址就是func4函数的地址
那如何验证这个猜测呢
我们这里做了一个小设计通过使用函数指针指向虚函数表来访问虚函数
//重定义函数指针为FUNC_TEST
typedef void(*FUNC_TEST) ();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:virtual void Func1(){}virtual void func4(){}
private:int _d 2;
};void _vfptr_Print(FUNC_TEST* _vfptr)
{for (int i 0; _vfptr[i] ! nullptr; i){cout _vfptr[i] ;}cout endl;
}int main()
{Base b;Derive d;//使用int*访问_vfptr然后通过解引用放问虚函数表第一个元素FUNC_TEST* ptr (FUNC_TEST*)(*(int*)b);_vfptr_Print(ptr);ptr (FUNC_TEST*)(*(int*)d);_vfptr_Print(ptr);return 0;
} 通过将地址打印出来来验证func4的存在可以看到func4确实是存在于虚函数表中的
那么虚表存在哪呢
栈 堆 动态区 数据段 代码段
同理我们可以通过代码实践得出结果
int a 1;
printf(栈区%p\n, a);int* b new int;
printf(堆区%p\n, b);static int c 1;
printf(静态区%p\n, c);const char* str nxbw;
printf(代码段%p\n, str);Base q;
printf(虚表1%p\n, *(int*)q); 可以看到虚表距离常量区几十个字节离常量区非常近其他区域相对常量区而言距离虚表很远的距离依此可以判断虚函数表可能是在常量区
多态调用的条件
1.基类的指针或者引用
为什么不能是派生类的指针或者引用
答因为使用派生类的指针或者引用只能接收派生类的指针或引用它不能多种形态
为什么不能是基类对象
person p s; //切割赋值
person* ptr s; //引用
person ref s; //指针有这样一个问题如果我们使用对象那就是将派生类切割拷贝赋值给基类这时如果想访问派生类的虚函数的话就需要将虚函数表也拷贝过去通过虚函数表来访问这个函数但是这样就会引发一个问题如果下次使用指针或者引用调用的虚函数时候就不知道调用的是父类还是子类的虚函数了就乱套了而且在vs下切割赋值时它不会拷贝虚函数表去基类中
2.在派生类中重写虚函数
答在派生类中重写虚函数是必要的在多态调用中我们需要传递不同的对象进行函数调用我们需要重写虚函数改变虚函数中指针的指向以以此找到访问相对应的虚函数
通过观察和测试我们发现了以下几点问题
1. 派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚 表指针也就是存在部分的另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表 中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函 数所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生 类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 虚函数存在哪的虚表存在哪的 答虚函数存在虚表虚表存在对象中。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现vs下是存在代码段的
4.2动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载
2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态。
5.多继承关系的虚函数表
需要注意的是在多继承关系中下面我们去关注的是派生类对象的虚表模型因为基类的虚表模型前面我们已经看过了没什么需要特别研究的
5.1 多继承中的虚函数表
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;
};int main()
{cout sizeof(Derive) endl;Derive d;return 0;
} 可以看到在Derive的模型中它有两个虚表指针在Derive中我添加了一个虚函数func4函数大伙可以猜猜这个函数被放到了那个虚函数表中1.Base1的函数表 2.Base2的虚函数表 3.放在Base1和Base2的函数表中
开始测试
//重定义函数指针为FUNC_TEST
typedef void(*FUNC_TEST) ();void _VFPTR_PRINT(FUNC_TEST* _vfptr)
{for (int i 0; _vfptr[i] ! nullptr; i){cout i : _vfptr[i] ;}cout endl;
}int main()
{Derive d;FUNC_TEST _vfptr1 (FUNC_TEST)*(int*)d;Base2* b2 d;FUNC_TEST _vfptr2 (FUNC_TEST) * (int*)b2;_VFPTR_PRINT((FUNC_TEST*)_vfptr1);_VFPTR_PRINT((FUNC_TEST*)_vfptr2);return 0;
} 可以看到在Derive中加入的虚函数func4放在Base1的虚函数表中
typedef void(*FUNC_TEST) ();void _VFPTR_PRINT(FUNC_TEST* _vfptr)
{for (int i 0; _vfptr[i] ! nullptr; i){FUNC_TEST f _vfptr[i];cout _vfptr[i] ;f();}cout endl;
}int main()
{Derive d;FUNC_TEST _vfptr1 (FUNC_TEST)*(int*)d;Base2* b2 d;FUNC_TEST _vfptr2 (FUNC_TEST) *(int*)b2;_VFPTR_PRINT((FUNC_TEST*)_vfptr1);_VFPTR_PRINT((FUNC_TEST*)_vfptr2);return 0;
} 在Derive中重写func1后为什么Derive和Base1中调用的func1地址会不一样
转到汇编看看底层是怎么操作的
转到底层汇编之后发现ptr1调用func1时会直接跳到func1的地址处调用func1而ptr经过不断的跳转之后也会去调用func1在跳转期间我们可以看到最关键的一步操作就是ecx减88就是Base1类的大小ecx是this指针ptr2 因为ptr2指向的是Base2这个指针并不能调用func1所以编译器将this指针减8修正ptr2所指向的位置让它指向的Derive的开头修正之后它就可以调用func1函数了
为什么ptr1不需要修正指向
在内存中编译器不会去看数据是什么类型类型只是代码层的叫法它只会看你的地址指向所以ptr1不会去做任何修改
int main()
{Derive d;Base1* ptr1 d;ptr1-func1();Base2* ptr2 d;ptr2-func1();Derive* ptr3 d;ptr3-func1();return 0;
} ptr1和ptr2使用的是多态调用它们满足重写和父子关系指针或引用
ptr3是普通调用因为ptr3是一个基类的指针它并不满足多态调用条件
5.2. 菱形继承、菱形虚拟继承
菱形继承
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a 1;
};class B : public A
{
public:int _b 1;
};class C : public A
{
public:int _c 1;
};class D : public B, public C
{
public:int _d 1;
};
在菱形继承中我们在A类中写下一个虚函数C类B类D类中没有重写并且都没有函数观察D的对象模型是怎么样 由上图可以看到B和C类都继承了A类的虚表并且带到了D类对象模型中所以D类带有两个虚表
菱形虚拟继承在B和C类中重写A的虚函数
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a 1;
};class B : virtual public A
{
public:virtual void func1(){cout B::func1() endl;}int _b 1;
};class C : virtual public A
{
public:int _c 1;
};class D : public B, public C
{
public:int _d 1;
}; 报错了A类中func1继承不明确因为B和C都继承了A可以理解为A现在是B和C的共享又因为B和C中都重写了func1编译器不知道是谁重写了func1这样就会导致继承不明确而报错
解决方法在D类中重写func1
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a 1;
};class B : virtual public A
{
public:virtual void func1(){cout B::func1() endl;}int _b 1;
};class C : virtual public A
{
public:virtual void func1(){cout C::func1() endl;}int _c 1;
};class D : public B, public C
{
public:int _d 1;
};
我们来看看它的对象模型是怎么样的 使用了菱形虚拟继承并且重写函数之后可以看到编译器将A类的虚表和成员变量单独放到了D类对象模型的最后并且给B和C类中加入了虚基表在D类中重写的func1放到了A类的虚表中
在B和C中重写func1有意义吗
有在你单独使用B类或C类中的func1的时候我们就需要在它们中进行重写。
将它们看作有符号ffffffff是-1fcffffff是-4
由上图可知B和C类中的虚函数并没有放进A类虚表中而是在B和C类中新增了一个虚表储存它们的虚函数
最后的最后在实际中尽量不要使用菱形虚拟继承太麻烦了可能会把自己给绕进去
下面有一道很经典的面试题
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;
}
在main函数中我们new了一个B的对象然后使用该对象去调用基类的虚函数使用派生类对象调用A中的成员函数时派生类对象需要通过切割基类部分来调用test函数这时候test当中它隐藏的this指针的类型为A*基类的指针我们上面所说多态调用成立的条件1.重写虚函数 2.子和父的指针或者引用前面我们分析this指针是基类指针在这段代码中也可以看到func函数被重写派生类中重写的虚函数符合三同大家注意函数表中只要参数类型一样就符合参数列表相同上面提到过基类中的虚函数写过virtual后派生类重写基类的虚函数可以不用带virtual经过上一轮的分析我们可以知道this-func()是一个多态调用这个题目使用的是派生类的对象进行多态调用所以这里调用的是派生类的虚函数这道题的输出结果为B-1
为什么会是这个结果呢
因为我们在派生类中重写的是基类虚函数的实现而不是重写它的函数头返回值函数名参数列表所以这里虚函数的的调用其实是 基类的函数头 派生类重写的实现 所以这题的输出结果为B-1