当前位置: 首页 > news >正文

做陶瓷公司网站建工社官网

做陶瓷公司网站,建工社官网,ui设计培训班的学费一般是多少钱?,wordpress 文章和tag文章目录 一、多态的概念二、多态的定义及实现2.1 多态的构成条件2.2 虚函数2.3 虚函数的重写2.4 虚函数重写的两个例外2.4.1 协变#xff08;基类与派生类虚函数返回值类型不同#xff09;2.4.2 析构函数的重写#xff08;基类与派生类析构函数的名字不同#xff09; 2.5 … 文章目录 一、多态的概念二、多态的定义及实现2.1 多态的构成条件2.2 虚函数2.3 虚函数的重写2.4 虚函数重写的两个例外2.4.1 协变基类与派生类虚函数返回值类型不同2.4.2 析构函数的重写基类与派生类析构函数的名字不同 2.5 C11 override 和 final2.5.1 final修饰虚函数表示该虚函数不能再被重写2.5.2 override 三、重载、隐藏重定义、覆盖重写的对比四、多态的原理4.1 虚函数表4.2 派生类对象中的虚函数表4.2.1 编写程序去访问虚函数表4.2.2 虚表存储位置的验证 4.3 多态的原理4.3.1 为什么不能是派生类的指针或者引用4.3.2 为什么不能是父类的对象呢4.3.3 派生类中为什么要对父类的虚函数进行重写4.4 动态绑定与静态绑定 五、多继承关系的虚函数表5.1 普通的多继承5.2 菱形继承、菱形虚拟继承5.2.1 普通菱形继承5.2.2 菱型虚拟继承 六、抽象类6.1 概念6.2 接口继承和实现继承 七、多态常见面试题7.1 快问快答 八、结语 一、多态的概念 多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。 举个栗子比如买票这个行为当普通人买票时是全价学生买票时是半价军人买票时是优先买票。再举个例子想必大家都参与过支付宝的扫红包-支付-给奖励金的活动那么大家想一想为什么有人扫的红包金额很大8块、10块而有的人扫出来的红包金额都是1毛5毛。其实这背后就是一个多态行为。支付宝首先会分析你的账户数据比如你是新用户、或者你没有经常的使用支付宝等等那么你需要被鼓励使用支付宝那么你扫码的金额就 random % 99如果你是经常使用支付宝支付或者支付宝账户中常年有钱那么就不需要太鼓励你去使用支付宝那么你的扫码金额就 random % 1。总结一下同样是扫码动作不同的用户去扫得到不一样的红包这也是一种多态行为。 二、多态的定义及实现 2.1 多态的构成条件 多态是在不同继承关系的类对象去调用同一个函数产生了不同的行为。比如 Student 继承了 Person。Person 对象买票全价Student 对象买票半价。因此多态的前提是要在继承体系中在继承中要构成多态还有两个条件 必须通过基类的指针或者引用调用虚函数 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 class Person { public:virtual void BuyTicket() const//虚函数{cout 买全价票 endl;} };class Student : public Person { public:virtual void BuyTicket() const//虚函数{cout 买半价票 endl;} };void Func(const Person people) {people.BuyTicket(); }int main() {Person Jack;//普通人Func(Jack);Student Mike;//学生Func(Mike);return 0; }小Tips多态调用看的是基类指针或引用指向的对象基类的指针或引用如果指向一个基类对象那就调用基类的成员函数如果指向派生类对象就调用派生类的成员函数。 class Person { public:virtual void BuyTicket() const{cout 买全价票 endl;} };class Student : public Person { public:virtual void BuyTicket() const{cout 买半价票 endl;} };void Func(const Person people) {people.BuyTicket(); }int main() {Person Jack;//普通人Func(Jack);Student Mike;//学生Func(Mike);return 0; }小Tips上面这段代码中 Fun 函数的形参变成了一个普通的基类对象 people在函数体中通过 people 去调用成员函数 BuyTicket此时因为 people 不是基类的指针或引用因此 people.BuyTicket(); 函数调用不满足多态调的条件此时无论传进来的是基类对象还是派生类对象调用的都是基类中的 BuyTicket因为在不满足多态的条件下调用成员函数取决于当前调用对象的类型当前的 people 是一个基类对象这就意味着它只能调用基类中的成员函数所以我们不管是传基类对象 Jack 还是派生类对象 Mike最终打印结果都是“买全价票”。传派生类对象的 Mike 的时候会发生切片会用 Mike 对象中继承自基类的那部分成员变量去构造基类对象 people。如果 Fun 函数的形参 people 就是基类的指针或者引用去掉基类中 BuyTicket 函数前面的 virtual此时还是不满足多态的条件无论传基类对象还是派生类对象最终调用的都是基类中的 BuyTicke因为 people 的类型是基类。总结多态的两个构成条件缺一不可。 2.2 虚函数 虚函数被 virtual 修饰的类成员函数称为虚函数。 class Person { public:virtual void BuyTicket() const{cout 买全价票 endl;} };小Tips只能是类的成员函数才能变成虚函数在全局函数前面是不能加 virtual 的。 2.3 虚函数的重写 虚函数的重写覆盖派生类中有一个跟基类完全相同的虚函数即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同称子类的虚函数重写了基类的虚函数。 class Person { public:virtual void BuyTicket() const//虚函数{cout 买全价票 endl;} };class Student : public Person { public:virtual void BuyTicket() const//虚函数{cout 买半价票 endl;} };void Func(const Person people) {people.BuyTicket(); }int main() {Person Jack;//普通人Func(Jack);Student Mike;//学生Func(Mike);return 0; }小Tips在重写基类虚函数时派生类的虚函数在不加 virtual 关键字时虽然也可以构成重写因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性但是该种写法不是很规范不建议这样使用。 2.4 虚函数重写的两个例外 2.4.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.4.2 析构函数的重写基类与派生类析构函数的名字不同 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加 virtual 关键字都与基类的析构函数构成重写虽然基类与派生类析构函数的名字不同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成 destructor。 class Person { public:virtual ~Person() { cout ~Person() endl; } };class Student : public Person { public:virtual ~Student() { cout ~Student() endl;delete[] pi;pi nullptr;} protect:int* pi new int[10]; };void Test() {Person* p1 new Person;Person* p2 new Student;delete p1;delete p2; }int main() {Test();return 0; }小Tips编译器之所以将所有类的析构函数都统一处理成 destructor目的是为了让父子类的析构函数构成重写只有派生类 Student 的析构函数重写了 Person 的析构函数上面代码中 delete 对象才能构成多态才能保证 p1 和 p2 指向的对象正确的调用析构函数。假如子类中并没有重写父类的析构函数那么 delete p2; 就会出问题它就调不到派生类 Student 的析构函数。因为 delete 分两步先去调用析构函数再去调用 operator delete而这里 p2 是一个基类 Person 的对象最终 delete p2 就是变成p2-destructor operator delete(p2)。如果派生类 Student 没有重写基类 Person 的析构函数那 p2-destructor 就不构成多态调用就是普通的调用成员函数此时会根据调用对象的类型去判断到底是调用基类中的成员函数还是调用派生类中的成员函数具体规则是基类对象调用基类的成员函数派生类对象调用派生类中的成员函数这里的 p2 是一个基类对象的指针所以 p2-destructor 调用的一定是基类的析构函数但是当前 p2 指向一个派生类 Student 的对象而我们希望调用派生类 Student 的析构函数去清理该派生类 Student 对象中的资源。 这种情况下我们希望的是 p2 指向谁就去调用谁的析构这不就是多态嘛。所以我们要让基类的析构函数变成虚函数然后派生类去重写虚函数这样才能满足多态的条件重写编译器已经帮我们实现了编译器将析构函数统一处理成同名函数且析构函数没有返回值和参数完美的满足三通我们只需要在基类析构函数的前面加上 virtual让析构函数变成虚函数即可。这里建议大家在写代码的过程中对于可能会被继承的类最好在它的析构函数前面加上 virtual让它变成一个虚函数。 2.5 C11 override 和 final 从上面可以看出C 对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名的字母顺序写反而无法构成重写而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来 debug 会得不偿失因此 C11 提供了 override 和 final 两个关键字可以帮助用户检测是否重写。 2.5.1 final修饰虚函数表示该虚函数不能再被重写 final修饰虚函数表示该虚函数不能再被重写 class Car { public:virtual void Drive() final {} }; class Benz :public Car { public:virtual void Drive() { cout Benz-舒适 endl; } };小Tips虚函数如果不能被重写是没有什么意义的。这里在补充一个知识点一个类不想被继承该怎么做C98 中的方法将该类的构造函数私有私有在子类中是不可见的而派生类的构造函数又必须调用父类的构造函数。但是这种做法会导致创建该类对象时也无法调用构造函数了私有在类外面不可见但是在类里面是可见的所以此时可以在该类里面写一个静态成员函数专门用来创建对象。在 C11 引入 final 关键字后对于一个类如果不想让它被继承我们可以在该类的后面加上 final 关键字进行修饰。 2.5.2 override override检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。 class Car { public:virtual void Drive() {} }; class Benz :public Car { public:virtual void Drive() override{ cout Benz-舒适 endl; } };三、重载、隐藏重定义、覆盖重写的对比 四、多态的原理 4.1 虚函数表 // 这里常考一道笔试题sizeof(Base)是多少 class Base { public:virtual void Func1(){cout Func1() endl;} private:int _b 1; };int main() {cout sizeof(Base) endl;return 0; }小Tips通过上面的打印结果和调试我们发现一个 Base 对象是 8 bytes除了 _b 成员还多了一个 _vfptr 放在对象成员变量的前面注意有些平台可能会放到对象成的最后面这个跟平台有关系。_vfptr 本质上是一个指针这个指针我们叫做虚函数表指针v 代表 virtualf 代表 function。一个含有虚函数的类中都至少有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数本质上是存在代码段的虚函数表也简称虚表。 4.2 派生类对象中的虚函数表 上面我们看了一个普通类对象中的虚表下面我们再来看看派生类中的虚表又是怎样的。 // 针对上面的代码我们做出以下改造 // 1.我们增加一个派生类Derive去继承Base // 2.Derive中重写Func1 // 3.Base再增加一个虚函数Func2和一个普通函数Func3 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; }通过监视窗口我们发现了以下几个问题 派生类对象 d 中也有一个虚表但是这个虚表是作为基类成员的一部分被继承下来的。总的来说d 对象由两部分构成一部分是父类继承下来的成员d 对象中虚表指针就是就是这部分成员中的一个。另一部分则是自己的成员。 基类 b 对象和派生类 d 对象的虚表是不一样的上面的代码中 Func1 完成了重写所以 d 的虚表中存的是重写后的 Derive::Func1所以虚函数的重写也叫做覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法层面的叫法覆盖是原理层面的叫法。 另外 Func2 继承下来后是虚函数所以放进了虚表Func3 也继承下来了但是不是虚函数所以不会放进虚表。 虚函数表本质上是一个存虚函数地址的函数指针数组一般情况下这个数组最后面放了一个 nullptr。 总结一下派生类虚表的生成 先将基类中的虚表内容拷贝一份到派生类虚表中。 如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数。 派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。在 VS 监视窗口显示的虚表中是看不见的下面将通过程序带大家来验证 这里还有一个比较容易混淆的问题虚函数存在哪虚表存在哪很多小伙伴会觉得虚函数存在虚表虚表存在对象中注意这种回答是错的。这里再次强调虚表存的是虚函数的地址不是虚函数虚函数和普通的成员函数一样都是存在代码段的只是它的地址又存到了虚表中。另外对象中存的不是虚表存的是虚表的地址。那虚表是存在哪儿呢通过验证在 VS 下虚表是存在代码段的。Linux g 下大家可以自己去验证。 同一个程序中同一类型的对象共用一个虚表。 4.2.1 编写程序去访问虚函数表 上面提到派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。但是在 VS 的监视窗口中是看不到以下面的代码为例 class Person { public:virtual void func1() const{cout virtual void Person::fun1() endl;}virtual void func2() const{cout virtual void Person::fun2() endl;}virtual void func3() const{cout virtual void Person::fun3() endl;}//protected:int _a 1; };class Student : public Person { public:virtual void func1() const{cout virtual void Student::fun1() endl;}virtual void func3() const{cout virtual void Student::fun3() endl;}virtual void func4() const{cout virtual void Student::fun4() endl;}//protected:int _b 2; };int main() {Person Mike;Student Jack; }小Tips监视窗口中展现的派生类对象的虚函数表中并没有派生类自己的虚函数 func4。但是我们从内存窗口可以看到第四个地址我们可以大胆的猜测这个就是派生类自己的虚函数 func4 的地址但是口说无凭下面我们来写一段代码验证一下我们的猜想。 typedef void (*VFPTR) ();//VFPTR是一个函数指针//vf是一个函数指针数组vf就是指向虚表 //虚表本质上就是一个函数指针数组 void PrintVfptr(VFPTR* vf) {for (int i 0; vf[i] ! nullptr; i){printf(vfptr[%d]:%p-----, i, vf[i]);VFPTR f vf[i];//函数指针和函数名是一样的可以去调用该函数f();}printf(\n); }int main() {Person Mike;int vfp1 *(int*)Mike;PrintVfptr((VFPTR*)vfp1);Student Jack;int vfp2 *(int*)Jack;PrintVfptr((VFPTR*)vfp2);return 0; }小Tips通过上图可以看出我们程序打印出来的地址和监视窗口中显示的地址是一样的并且成功的调用了派生类中的虚函数 func4上图显示的结果完美的验证了我们的猜想。这里也说明了一个问题VS 的监视窗口是存在 Bug 的以后我们在调试代码过程中也不能完全相信监视窗口展现给我们的内容比起监视窗口我们更应该相信内存窗口展现给我们的内容。这里也侧面反映了一个问题只要我们能拿到函数的地址就能去调用该函数正常情况下我们只能通过派生类对象去调用虚函数 func4这里我们直接拿到了这个函数的地址去调用这里的问题在于函数的隐藏形参 this 指针接收不到实参因为不是派生类对象去调用该函数。函数中如果去访问了成员变量那么我们这种调用方式就会出问题。 4.2.2 虚表存储位置的验证 //虚表存储位置的验证class Person { public:virtual void func1() const{cout virtual void Person::fun1() endl;} //protected:int _a 1; };class Student : public Person { public:virtual void func1() const{cout virtual void Student::fun1() endl;} //protected:int _b 2; };int main() {Person Mike;Student Jack;//栈区int a 10;printf(栈区:%p\n, a);//堆区int* pa new int(9);printf(堆区:%p\n, pa);//静态区(数据段)static int sa 8;printf(静态区(数据段):%p\n, sa);//常量区(代码段)const char* pc hello word!;printf(常量区(代码段):%p\n, pc);//虚表printf(基类的虚表:%p\n, (void*)*(int*)Mike);printf(派生类的虚表:%p\n, (void*)*(int*)Jack); }小Tips上面取虚表地址是通过强制类型转化来实现的通过上面的监视窗口我们可以看出虚表的地址永远是存储在对象的前四个字节所以这里我们先取到对象的地址然后将其强转为 int* 类型为什么要强转为 int* 呢因为一个 int 型的大小就是四个字节而指针的类型决定了该指针能够访问到内存空间的大小一个 int* 的指针就能够访问到四个字节再对 int* 解引用这样就能访问到内存空间中前四个字节的数据这样就能取道虚表的地址啦。通过打印结果我们可以看出虚表的地址和常量区代码段的地址最为接近因此我们可以大胆的猜测虚表就是存储在常量区代码段的。 4.3 多态的原理 上面说了这么多那多态的原理究竟是什么呢 小Tips此时再来分析下上面这个图当 people 指向基类对象 Jack 时people.BuyTicket() 在 Jack 的虚表中找到的虚函数是 Person::BuyTicket()当 people 指向派生类对象 Mike 时people.BuyTicket() 在 Mike 的虚表中找到的虚函数是 Student::BuyTicket()。这样就实现了不同对象去完成同一行为时展现出不同的形态。其次通过对汇编代码的分析可以发现满足多态的函数调用不是在编译时确定的是在运行起来以后到对象中取的。而不满足多态的函数调用则是在编译时就确定好了。 小Tips通过上面两张图可以看出在满足多态的条件下无论传递的是基类对象还是派生类对象最终转化成汇编代码都是一样的。最终的函数调用是在代码运行起来后去对象里面取的。 小Tips普通函数调用在编译时就确定好了直接去 call 那个函数。call 的这个函数和调用该函数对象的类型有关这里调用 BuyTicket 的对象是一个 Person 类型这就决定了调用的 BuyTicket 函数一定是基类中的。 4.3.1 为什么不能是派生类的指针或者引用 答因为只有基类的指针和引用才能做到既可以指向基类对象也可以指向派生类对象。而一个派生类的指针或者引用只能指向派生类对象不能指向基类对象。 4.3.2 为什么不能是父类的对象呢 答因为如果是一个父类对象假定为 A那么将一个派生类对象赋值给父类对象 A 时会发生切片会用该派生类中父类的那部分成员变量的值去初始化该父类对象 A但是并不会把该派生类对象中的虚表拷贝给父类对象所以不管是将基类对象赋值给基类对象 A还是将一个派生类对象赋值给基类对象 A该基类对象 A 中的虚表永远都是基类自己的去调用的始终是基类自己的虚函数无法做到传基类调用基类的虚函数传派生类调用派生类的虚函数多态就无法实现。而父类的指针和引用之所以能够实现是因为父类对象的指针和引用指向一个父类对象当然是没问题的指向派生类对象时会发生形式上的切片即这种切片并不是真的切片假设这里有一个基类的指针 p此时它指向一个派生类对象这里的切片本质上是限定了 p 指针的“视野范围”即 p 指针只能“看到”该派生类对象中继承自父类的那部分成员并没有像前面那样去实实在在的重新创建一个基类对象。而且根据 4.2 小节那张监视窗口的截图我们可以发现派生类的虚表本质上是作为父类成员的一部分继承下来的但是会对该虚表中的内容稍作修改具体如何修改请看 4.2 小节使之称为派生类自己的虚表所以 p 指针指向一个派生类对象的时候就能去根据派生类的虚表去调用派生类自己的虚函数。这样才能满足多态的要求。 class Person { public:virtual void func1() const{cout virtual void Person::fun1() endl;}virtual void func2() const{cout virtual void Person::fun2() endl;}virtual void func3() const{cout virtual void Person::fun3() endl;} //protected:int _a 1; };class Student : public Person { public:virtual void func1() const{cout virtual void Student::fun1() endl;}virtual void func3() const{cout virtual void Student::fun3() endl;}virtual void func4() const{cout virtual void Student::fun4() endl;} //protected:int _b 2; };int main() {Person Mike;Student Jack;Jack._a 9;Mike Jack; }小Tips这里如果把虚函数表也拷贝过去那就乱套了如果真拷贝过去了那当一个基类的指针Person*指向 Mike 时去调用的就是派生类的虚函数而且有一些虚函数是派生类自己的那这也太离谱了吧一顿操作下来一个基类的指针既然能去调用一个自己这个类里面没有的函数。太离谱了太离谱了千万不能这样搞。这里总结一下就是想告诉大家将一个派生类对象赋值给基类对象的过程中会涉及到切片但是不会把虚表拷贝过去的。 4.3.3 派生类中为什么要对父类的虚函数进行重写 答派生类中的虚表本质上是继承自父类的会先把父类的虚表拷贝一份如果对父类的虚函数进行重写了那么就会对拷贝的虚表进行修改存派生类重写的虚函数地址。如果派生类没有对基类的虚函数进行重写那么派生类的虚表中存的就是从基类虚表中拷贝过来的基类虚函数的地址这就失去了多态原本的目的还没有意义的。 4.4 动态绑定与静态绑定 静态绑定又称为前期绑定早绑定在程序编译期间确定了程序的行为也称为静态多态比如函数重载。 动态绑定又称后期绑定晚绑定是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。 4.3 小节中汇编代码的截图就很好的展示了什么是静态编译器绑定和动态运行时绑定。 五、多继承关系的虚函数表 5.1 普通的多继承 上面我们都是在单继承体系中去探究虚函数表的那多继承关系中的虚函数表是怎么样的呢下面我们就来一探究竟。根据前面的经验监视窗口展示给我们的内容已经不能再相信了所以这里我们直接通过程序去打印内存空间中虚表里面的虚函数地址。 class Base1 { public:virtual void func1() { cout Base1::func1 endl; }virtual void func2() { cout Base1::func2 endl; } private:int b1 0; };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 d1 3; };typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]) {cout 虚表地址: vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf(vTable[%d]:0X%p---------, i, vTable[i]);VFPTR f vTable[i];f();}cout endl; } int main() {Derive d;VFPTR* vTableb1 (VFPTR*)(*(int*)d);PrintVTable(vTableb1);VFPTR* vTableb2 (VFPTR*)(*(int*)((char*)d sizeof(Base1)));PrintVTable(vTableb2);return 0; }小Tips通过打印结果可以发现对于多继承的派生类 Derive 来说它的对象里面会有两张虚表因为它继承了两个类这两个类中一张继承自 Base1另一张继承自 Base2派生类自己的虚函数地址会存放在继承的第一个基类的虚表中。此外还有一个值得注意的地方两个基类中都有 func1 函数并且它们的返回值类型函数名、参数都完全相同派生类中对这个 func1 函数进行了重写原本继承下来的虚表中存的都是他们自己内部 func1 函数的地址派生类进行重写后两张虚表中 func1 函数的地址就应该被覆盖成派生类中 func1 函数的地址但是通过打印结果可以看出两张虚表中存的 func1 函数的地址并不相同但是最终调用的却是同一个函数都去调用了派生类中重写的 func1 函数这是为什么呢通过下面这段代码的反汇编来给大家解释原因。 int main() {Derive d;Base1* p1 d;p1-func1();Base2* p2 d;p2-func1(); }小Tips通过反汇编我们可以看出p1 是直接去调用的p2 则进行了多层封装。p2 调用进行多层封装的主要目的就是为了执行 sub ecx , 8这里的 ecx 是一个寄存器它存的是 this 指针的值那为什么要对它减 8 呢我们先来看看在减 8 之前 ecx 中存的是什么值。 小Tips我们可以发现 ecx 本来存的是 p2 指针的值那为什么要对这个值减 8 呢因为 p2 本来是一个基类的指针而 fucn1 函数中的隐藏形参 this 是一个派生类的指针。一个基类指针是不能赋值给派生类的指针换句话说就是一个派生类的指针不能指向一个基类对象原因是指针的类型决定了该指针可以访问到的内容一个派生类指针应该可以访问到派生类中的所有成员而当一个派生类指针指向一个基类对象的时候由于基类对象中不可能有派生类中的成员所以派生类指针再去访问这些成员的时候就会出错。这里的 8 本质上是一个 Base1 类对象的大小所以这里减 8 的目的就是为了让 p2 中存 d 对象的首地址这样 p2 就相当于指向了一个派生类Derive对象此时再去调用 func1 函数就没有什么问题啦。所以总结一下Derive 中只重写了一份 func1 函数这里 sub ecx , 8 的目的就是为了修正 this 指针。p1 不用修正的原因是 p1 中原本存的就是 d 对象的首地址去调用 func1 是没有任何问题的。其次补充一点这里的 p1 和 p2 去调用 func1 函数都属于多态调用。上面这种是 VS 下的解决办法其他编译器的处理方法可能会有所不同。 5.2 菱形继承、菱形虚拟继承 实际中我们不建议设计出菱形继承和菱形虚拟继承一方面太复杂容易出问题另一方面这样的模型访问基类成员有一定的性能损耗。所以菱形继承、菱形虚拟继承的虚表我们就不需要研究的很清楚因为始终很少使用。若果对这方面感兴趣的小伙伴这里我给大家推荐两篇文章C虚函数表解析、C对象的内存布局 5.2.1 普通菱形继承 class A { public:virtual void func1(){cout A::func1() endl;}virtual void func2(){cout A::func2() endl;} protected:int _a 1; };class B : public A { public:virtual void func1(){cout B::func1() endl;}virtual void func3(){cout B::func3() endl;} protected:int _b 2; };class C : public A { public:virtual void func1(){cout C::func1() endl;}virtual void fun4(){cout C::func4() endl;} protected:int _c 3; };class D : public B, public C { public:virtual void func1(){cout D::func1() endl;}virtual void func3(){cout D::func3() endl;}virtual void fun5(){cout D::func5() endl;} protected:int _d 4; };typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]) {cout 虚表地址: vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf(vTable[%d]:0X%p---------, i, vTable[i]);VFPTR f vTable[i];f();}cout endl; }int main() {D d;B* p1 d;PrintVTable((VFPTR*)*(int*)p1);C* p2 d;PrintVTable((VFPTR*)*(int*)p2); }小Tips普通菱形继承的虚表和多继承是如出一辙的没有什么区别。 5.2.2 菱型虚拟继承 class A { public:virtual void func1(){cout A::func1() endl;}virtual void func2(){cout A::func2() endl;} //protected:int _a 1; };class B : virtual public A { public:virtual void func1(){cout B::func1() endl;}virtual void func3(){cout B::func3() endl;} //protected:int _b 2; };class C : virtual public A { public:virtual void func1(){cout C::func1() endl;}virtual void fun4(){cout C::func4() endl;} //protected:int _c 3; };class D : public B, public C { public:virtual void func1(){cout D::func1() endl;}virtual void func3(){cout D::func3() endl;}virtual void fun5(){cout D::func5() endl;} //protected:int _d 4; };typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]) {cout 虚表地址: vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf(vTable[%d]:0X%p---------, i, vTable[i]);VFPTR f vTable[i];f();}cout endl; }void Test() {D d;B* p1 d;PrintVTable((VFPTR*)*(int*)p1);C* p2 d;PrintVTable((VFPTR*)*(int*)p2);A* p3 d;PrintVTable((VFPTR*)*(int*)p3); }int main() {D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;Test(); }小Tips从打印结果和上图可以看出在菱形虚拟继承体系中 A 类中的成员被独立出来了不再是 B 类和 C 类中各存一份了因此在 B类、C类、D类对象中各自有一份 A 类的虚函数表。这里有一个问题就是如果 B 类和 C 类中同时重写了 A 类中的虚函数那么 D 类中一定也要重写这个虚函数如上面代码中的 func1 函数因为如果 D 类中不进行重写的话那 D 类对象中到底存 B 类中重写的那个还是存 C 类中重写的那个呢此时就会产生歧义只要 D 类中也对这个虚函数进行重写就不会产生歧义了。其次D 类中并没有自己的虚表即对 D 类自己的虚函数来说编译器会把这个函数的地址存入 D 类继承的第一个类的虚表中这里就是存入 B 类虚表中。 补充上一篇文章中提到虚基表中存的是偏移量目的是为了找到被分离出去的基类成员这里也就是 A 类成员但是当时通过内存窗口看到这个偏移量存在虚基表的第二个字节中那虚基表的第一个字节存的是什么呢答案是存的也是偏移量存这个偏移量的目的是为了找到该类在内存中的首地址还是以上面的代码为例因为一个类如果有虚表那么虚表地址都是被存储在这个类对象的最开始位置虚基表中第一个字节存储的偏移量是用来找到该对象的首地址虚基表中第二个字节存储的偏移量是用来找到基类的首地址。 六、抽象类 6.1 概念 在虚函数的后面写上 0则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 //抽象类(接口) class Car { public:virtual void Drive() const 0; };class Benz : public Car { public:virtual void Drive() const{cout Benz-舒适 endl;} };class Bmw : public Car { public:virtual void Drive() const{cout Bmw-操控 endl;} };void Advantages(const Car car) {car.Drive(); }int main() {Benz be;Advantages(be);Bmw bm;Advantages(bm); }6.2 接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类的函数可以使用函数继承的函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 七、多态常见面试题 //下面这段代码的运行结果是什么 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() {B* p new B;p-test();return 0; }分析这道题要想做对我们必须了解以下几点。首先就是我们对继承的理解B 类继承了 A 类因此一个 B 类的指针 p 去调用 test 是没有问题的B 类中把 A 类的 test 函数给继承了下来。在 test 函数中又去调用了 func 函数这个 func 函数本质上是又 this 指针去调用的而 func 函数是一个虚函数并且子类对其进行了重写那这里调用 func 函数是否是多态调用呢是不是多态调用取决于这里调用 func 函数的是谁前面说过这里的 func 本质上是 this 指针去调用那这里的 this 指针究竟是什么类型呢如果是基类A类型那么这里就符合多态调用如果是派生了B类型那就不符合多态调用。所以这里的 this 究竟是什么类型呢这就要考察大家对继承的理解了。先说答案这里的 this 指针是 A* 类型。可能会有很多朋友觉得B 类继承了 A 类那么就要在 B 类中就会重新生成一份 test 函数然后这里的 p 指针就去调用 B 类中字节生成的 test 函数所以这里的 this 指针因该是 B* 类型但事实并非如此编译器并不会这样做。继承中派生类对象模型是按照下面的方式来生成的对于成员变量来说创建一个派生类这里就是B类对象它分为两个部分第一部分是父类第二部分是自己他会把继承自父类中的那些成员变量凑在一起当成一个父类对象然后又把这个对象当成是派生类的一个成员变量因此在派生类构造函数的初始化列表中要去调用父类的构造函数在派生类的析构函数中要去调用父类的析构。这就是一个派生类对象在内存中的存储模型对象的存储模型只和成员变量有关和成员函数无关。所有编译好的函数都是放在代码段的由于派生类 B 中并没有对 test 函数进行重写所以 test 函数的代码并不会生成两份从始至终这个 test 函数就只有一份即基类 A 生成的所以这里的 test 函数中的 this 指针是 A*。p 指针在调用 test 函数的时候先进行语法检查先在派生类 B 中去找 test 函数没找到接着去父类 A 中去找最后找到了语法上没有任何问题然后在链接阶段这个 test 函数是父类的编译器就拿着这个经过函数名修饰规则修饰产生的名字去找这个函数。前面说了这么多就是想告诉大家这里的 this 指针是 A*所以这里满足多态调用。这就意味着不同类型的对象去调用 test 函数会产生不同的效果基类对象去调用 test 函数最终会去调用基类中的 func 函数派生类对象去调用 test 函数最终会去调用派生类中的 func 函数。而这里是一个派生类的指针 p 去调用 test 函数所以最终调用的是派生类中的 func 函数此时就会有小伙伴产生疑问了派生类中 func 函数的形参 val 的缺省值明明是 0 呀为什么打印出来的是11 不是父类中 func 函数形参的缺省值嘛。这就涉及到本题的第二个“坑点”了虚函数重写重写的是实现只重写了函数体这就是为什么派生类中重写的虚函数可以不加 virtual。对于重写的虚函数编译器会检查是否满足三同即返回值类型、函数名、参数列表是否相同参数列表相同指的是参数的个数相同、类型顺序相同。只要符合三同编译器就不管了派生类中重写的虚函数的整个壳子即函数声明那一套使用的是父类中的。所以派生类中重写的虚函数 func它的函数体中使用的 val就应该是父类中 val 的缺省值在派生类重写的虚函数 func 的形参列表给缺省值是没有任何意义的。 7.1 快问快答 ● inline 函数可以是虚函数嘛 答可以不过编译器会忽略 inline 属性这个函数就不再是 inline因为虚函数要放进虚函数表中。 ● 静态成员可以是虚函数嘛 答不能因为静态成员函数没有 this 指针使用类型::成员函数的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。 ● 构造函数可以是虚函数嘛 答不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 ● 析构函数可以是虚函数嘛什么场景下析构函数是虚函数 答可以并且最好把基类的析构函数定义成虚函数。集体场景参考 2.4.2 小节。 ● 对象访问普通函数块还是虚函数更快 答首先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数更快。因为构成多态运行时调用虚函数要到虚函数表中去查找。 ● 虚函数表是在什么阶段生成的存在哪 答虚函数表是在编译阶段就生成的一般情况下存在代码段常量区。 ● 什么是抽象类抽象类的作用 答什么是抽象类请参考 6.1 小节。抽象类强制重写了虚函数另外抽象类体现出了接口继承关系。 八、结语 今天的分享到这里就结束啦如果觉得文章还不错的话可以三连支持一下春人的主页还有很多有趣的文章欢迎小伙伴们前去点评您的支持就是春人前进的动力
http://www.hkea.cn/news/14265073/

相关文章:

  • 烟台开发区建设局网站做家电网是什么网站
  • 模板网站源码烟台网站建设技术托管
  • 网站开发公司安心加盟wordpress登陆ip唯一
  • 门户类网站费用怎样做自己的视频网站
  • 外国网站的浏览器下载整站优化外包服务
  • 做网站服务器配置应该怎么选岳阳建站公司
  • 淘客那些网站怎么做的郑州网站开发公司电话
  • php网站开发流程逻辑西安网站seo分析
  • 南昌网站建设制作与维护阿里云wordpress外网访问不了
  • 商城类网站建设报价一般app开发费用
  • 做网站建设的公司有哪些wordpress it
  • 网站关键词排名外包房产网站案例
  • 网站如何更换图片做一个网页难不难
  • 网站开发中 视频播放卡官方网站开发用什么语言
  • 做网站熊掌号唐山滦县网站建设
  • 中国建设银行网站用户名重庆网站建设公司怎么做
  • 网站建设工作计划表网站访问频率
  • 做网站 如何 挣钱网站建设 还有需求吗
  • 上海网络做网站公司php网站开发自学
  • 网站空间分销搜索引擎技术包括哪些
  • 加强协会网站建设意义国外优秀设计网站有哪些
  • 网站更换服务器wordpress ajax error
  • 易名中国网站金融网站建设方法
  • 购物网站前台模板58同城石家庄网站建设
  • 嘉兴秀洲区建设局网站wordpress模板导出
  • 个人备案可以做哪些网站php做网站需要学的东西
  • 制作的网站游戏公司官方网站模版
  • 高级网站开发公司网站地址
  • 个人可以做行业网站吗电子信息工程移动互联网就业方向
  • 网站备案制作怎么在手机上设计网站