四川省建设厅工地安全网站,建设网站注意什么,网站每天点击量多少好,app用什么开发软件好文章目录 多态的概念多态的定义和实现多态的构成条件虚函数重写的两个例外协变(基类和派生类虚函数返回值类型不同)析构函数的重写(基类和派生类析构函数名字不同) c11 override 和 final关键字 重载#xff0c;重写(覆盖)#xff0c; 隐藏(重定义)对比抽象类(纯虚函数)多态的… 文章目录 多态的概念多态的定义和实现多态的构成条件虚函数重写的两个例外协变(基类和派生类虚函数返回值类型不同)析构函数的重写(基类和派生类析构函数名字不同) c11 override 和 final关键字 重载重写(覆盖) 隐藏(重定义)对比抽象类(纯虚函数)多态的原理虚表派生类虚表行为多态实现细节动态绑定与静态绑定 多继承的虚函数表菱形继承菱形虚继承 关于多态使用的小细节 多态的概念
多态通俗来说就是多种形态就是当去完成某种行为时不同的对象会发生不同的行为。 就像学生和普通成人去景区买票同样是买票学生和普通成人所要花费的资金是不一样的。 多态的定义和实现
多态的构成条件
多态是在不同继承关系的类对象去调用同一函数产生的不同的行为。如下面的例子student继承了personstudent买票半价person买票全价。
在继承中要构成多态需要三个条件
必须通过基类的指针或者引用调用虚函数被调用的函数在基类必须用virtual关键字声明并且派生类必须对基类的虚函数进行重写注意这里的重写和继承中函数的隐藏(重定义)是两个概念 被virtual定义的函数叫做虚函数 重写形成的条件相对重定义更加苛刻需要派生类虚函数和基类虚函数的返回值类型函数名字参数列表完全相同。 **注意:**关于在符合重写条件的情况下可以只在基类将函数用virtual关键字修饰而派生类该函数不用加virtual但不能只在派生类该函数加上virtual(一般情况下建议两边都加上virtual) 虚函数重写的两个例外
协变(基类和派生类虚函数返回值类型不同)
派生类重写虚函数时有一种情况允许其于基类虚函数返回值不同那就是协变。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用。 注意只用同时为指针或者同时为引用能完成协变其他类型都不行一个指针一个引用也不行基类和派生类返回顺序相反也不能构成协变。即基类返回派生类的指针或引用派生类返回基类的指针或引用也是不行的 class person
{
public:virtual void buyTicket() { cout 买票——全价 endl; }virtual person f() { return *this; }
};class student : public person
{
public:virtual void buyTicket() { cout 买票——半价 endl; }virtual student f() { return *this; }
};析构函数的重写(基类和派生类析构函数名字不同)
如果基类的析构函数为虚函数此时其和派生类的析构函数一定构成重写虽然派生类和基类的函数名一定不相同看起来违背了重写的规则但实则不然在底层编译器都会将析构函数的名称做统一的特殊处理编译后析构函数的名称将会统一处理成destructor()。 那么为什么要支持析构函数多态呢我们看下面的场景
void test()
{person* p1 new person;person* p2 new student;delete p1;delete p2;
}正是由于这个场景一定要支持虚函数多态由于基类指针可以指向派生类指针如果不支持析构函数多态上面的这段代码将不能正常调用派生类析构函数清理多余资源将会导致内存泄漏问题因此只有通过多态才能正常释放资源。
class person
{
public:virtual void buyTicket() { cout 买票——全价 endl; }//virtual person f() { return *this; }virtual ~person() { cout 析人\n; }
};class student : public person
{
public:virtual void buyTicket() { cout 买票——半价 endl; }//virtual student f() { return *this;}virtual ~student() { cout 析学\n; }
};void test()
{person* p new person;person* s new student;//将会调用基类析构delete p;//调用派生类析构释放派生类资源//然后调用基类析构释放基类资源delete s;
}
int main()
{test();return 0;
} c11 override 和 final关键字
从上面我们知道虚函数对重写的要求很严格需要三同(函数名相同参数列表相同返回值相同)以及基类指针或引用调用但是在有些情况下容易疏忽容易出现错误因此c11提供了这两个关键字帮助用户检查是否重写。 **final:**修饰虚函数表示该虚函数不能再被重写(该关键字放在函数名括号之后)
**override:**检查派生类虚函数是否重写了某个虚函数如果没有重写编译报错。
重载重写(覆盖) 隐藏(重定义)对比 抽象类(纯虚函数)
在虚函数后面加上0则这个函数就叫做纯虚函数。包含纯虚函数的类叫做抽象类接口类抽象类不能实例化出对象。派生类继承之后也不能实例化出对象只有重写了纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现了接口继承。 override的作用是检查重写而纯虚函数的作用是强制重写。 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 多态的原理
在探究 多态原理之前我们先来看一道常考的面试题
//请问sizeof(base)是多少
//32位平台下
class base
{
public:virtual void func(){cout func() endl;}
private:int _b 1;
}虚表
通过测试我们可以发现base对象是8bytes32位平台下,除了b成员还有一个 _vfptr放在对象的最前面(与平台有关) 对象中的这一指针叫做虚函数表指针(v——virtualf——function)一个含有虚函数的类中至少都有一个虚函数指针因为虚函数的地址要放到虚函数表中虚函数表也简称为虚表。 注意这里的虚表要和虚继承中解决菱形继承问题的虚基表区分开两者是截然不同的概念如果有不清楚虚基表和虚继承是什么的可以看看博主的另一篇博客链接如下 c_深究继承 里面关于菱形继承的部分就有为大家讲解虚继承是什么。 派生类虚表行为
那么了解了这个之后我们继续看看派生类在这个表中做了什么又是如何实现多态的。 针对上面的代码我们进行如下的改造
class base
{
public:virtual void func1(){cout func1() endl;}virtual void func2(){cout func2() endl;}void func3(){cout func3() endl;}
private:int _b 1;
};class derive : public base
{
public:virtual void func1(){cout next::func1() endl;}
private:int _c 2;
};int main()
{base b;derive n;return 0;
}通过观察和测试我们发现了几点问题:
派生类对象n中也有一个虚表指针n对象由两部分构成一部分是父类继承下来的成员以及虚表指针另一部分是自己的成员基类b对象和派生类对象虚表是不一样的我们发现func1完成了重写所以n的虚表里面存储的是derive::func1而func2在派生类中并没有重写所以派生类虚表中仍然是base::func2()因此重写也可以叫做覆盖覆盖就是指虚表中虚函数的覆盖重写是语法层的叫法覆盖是原理层的叫法。虚表中存放的只有虚函数也就是被声明为virtual的函数因此在该例子中func3并没有在虚表内。虚函数表本质上是一个存虚函数指针的指针数组有些编译器的虚表数组最后面放了一个nullptr。接下来总结一下派生类虚表是生成过程a. 先将基类的虚表内容拷贝一份到派生类虚表 b. 如果派生类重写了基类的某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在派生类中的声明顺序依次加到派生类虚表的最后面。 对于最后一步vs的监视窗口可能有一点小bug无法直接看到需要用一些小技巧才能看到。 接下来还有一个很多同学都容易混淆的问题:虚表存在哪里呢? 网上有很多种说法很大一部分说法说虚表存在数据段中但这种说法真的对吗我们通过比较实验的方法来观察一下。 首先通过刚才的测试我们知道在derive类中虚表指针是放在对象开始的所以我们先将derive对象强转成int*然后对齐解引用就拿到了虚表的地址通过对四个区域的数据进行比对我们可以发现虚表的位置和代码段数据的位置相隔最近与数据段的位置看似不远但是16进制的第四位差别已经接近上万字节了和虚表还是有点距离的所以我们可以推荐虚表并不放在数据段静态区而放在**代码段常量区)**中。 其实放在常量区中也是一个比较合理的选择因为虚表是不能被随意修改的。 多态实现细节
接下来有了虚表这个概念后我们就可以更容易的理解多态了。 回顾一下多态需要的条件
基类指针调用派生类虚函数满足三同构成重写
在学习了虚表之后多态这个过程也就不那么神秘了其实就是在用基类指针调用重写函数时编译器会直接进入虚表内拿到所要调用的函数地址也就是说在满足多态以后的函数调用不是在编译的时候确定的是运行起来以后到对象的虚表中去查找的。而不满足多态的函数调用在编译的时候早已确认好。
那么再来思考一个问题为什么一定要是**基类指针或引用调用**直接用基类对象调用不行吗 这里我们需要理解的一个至关重要的点就是引用或指针不会修改原来对象的虚表正是由于这个原因才必须用引用或者指针如果函数参数是基类对象那么将派生类对象传入时就会修改对象虚表从而不能达到多态的效果 动态绑定与静态绑定
上面的内容又引出了一个概念就是动态绑定和静态绑定。
静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也成为静态多态,函数重载就是经典的静态多态。动态绑定又称为后期绑定(晚绑定)是在程序运行期间根据拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。
多继承的虚函数表
看如下多继承
class base1
{
public:virtual void func1(){cout func1() endl;}virtual void func2(){cout func2() endl;}void func3(){cout func3() endl;}
private:int _b 1;
};class base2
{
public:virtual void func1(){cout base2::func1()\n;}virtual void func2(){cout base2::func2()\n;}
};class derive : public base1, public base2
{
public:virtual void func1(){cout next::func1() endl;}virtual void func4(){cout func4() endl;}
private:int _c 2;
};
对于多继承来说派生类将有多个虚表(有几个带虚函数的基类就有几个虚表)如果两个基类有构成重写的函数并且派生类也有构成重写的该函数那么派生类的该函数指针将会同时覆盖两个基类函数的虚表内的该函数指针另外如果派生类中有自己新增的虚函数将会放进第一个继承的基类的需表中同样可以通过监视窗口操作看到。下图可以更好的说明
菱形继承菱形虚继承
在继承的学习中我们知道为了解决菱形继承的数据冗余和二义性问题引入了虚继承而虚继承是用虚基表实现的而多态是由虚表实现的那将这两者结合起来之后就越能感觉到c的恐怖了在实际中我们并不建议设计出菱形虚拟继承一方面太复杂容易出问题另一方面这样庞大的模型访问基类成员有一定的性能损耗。
因此菱形虚拟继承的虚表我们也不需要进行深究这里带大家简单的了解一下即可。 可以看到虚继承虚函数是非常复杂的另外通过观察得知最终类的虚函数同样被放在了第一个继承的类中而不是放在person类当然这也跟编译器有关本编译器是vs2022的结果。 另外还有一个疑点就是虚基表中的第一行存放的是0xfffffc翻译成十进制是-4博主对于-4的作用还未能得知如果有知道的佬欢迎在评论区解答。 由于菱形虚继承过于复杂所以在实际应用中一定要尽量避免使用菱形虚继承否则会造成很大的麻烦。
关于多态使用的小细节
inline函数可以是虚函数但是在编译器会忽略inline这一属性。 很合理因为内联函数没有地址没办法放进需表中 静态成员函数不可以是虚函数因为静态成员函数没有this指针无法访问虚函数表所以不能通过运行时确定调用对象因此没办法放入虚函数表构造函数不能是虚函数因为虚函数指针是在初始化列表中初始化的(和先有鸡还是先有蛋的问题很想)虚表是在编译期间就生成了一般存放在代码段中。