网站制作思路深圳市官网网站建设哪家好

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

网站制作思路,深圳市官网网站建设哪家好,wap网站推荐,做手机网站用什么程序好文章目录 一、多态二、对象的静态类型和动态类型三、虚函数和纯虚函数1、虚函数2、虚析构函数3、抽象基类和纯虚函数4、多态的原理 四、重载、覆盖(重写)、隐藏(重定义)的对比 一、多态 OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语#xff0c;其含义是“多… 文章目录 一、多态二、对象的静态类型和动态类型三、虚函数和纯虚函数1、虚函数2、虚析构函数3、抽象基类和纯虚函数4、多态的原理 四、重载、覆盖(重写)、隐藏(重定义)的对比 一、多态 OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C语言支持多态性的根本所在。 当我们使用基类的引用或指针调用基类中定义的一个函数时我们并不知道该函数真正作用的对象是什么类型因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数则直到运行时才会决定到底执行哪个版本判断的依据是引用或指针所绑定的对象的真实类型。 另一方面对非虚函数的调用在编译时进行绑定。类似的通过对象进行的函数虚函数或非虚函数调用也在编译时绑定。对象的类型是确定不变的我们无论如何都不可能令对象的动态类型与静态类型不一致。因此通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。 ❕ 当且仅当对通过指针或引用调用虚函数时才会在运行时解析该调用也只有在这种情况下对象的动态类型才有可能与静态类型不同。 二、对象的静态类型和动态类型 ⚠️在C语言中当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。 当我们使用存在继承关系的类型时必须将一个变量或其他表达式的静态类型static type与该表达式表示对象的动态类型dynamic type区分开来。表达式的静态类型是在编译时确定的它是变量声明时的类型或表达式生成的类型动态类型则是变量或表达式表示的内存中的对象类型在运行时才可知。 ❕ 如果表达式既不是引用也不是指针则它的动态类型永远与静态类型保持一致。 因此我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定我们直到运行时才知道到底调用了哪个版本的虚函数所以所有的虚函数都必须有定义。但是我们必须为每一个虚函数都提供定义而不管它是否被用到了这是因为连编译器也无法确定到底会使用哪个函数。 派生类可以继承其基类的成员然而当遇到与类型相关的操作时派生类必须对其重新定义。换句话说派生类需要对这些操作提供自己的新定义以覆盖override从基类继承而来的旧定义。我们来看如下代码 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 bb;B* p bb;p-test(); //B-1p-func(); //B-0bb.func(); //B-0A a bb;a.test(); //B-1a.func(); //B-1return 0; }当我们使用指向bb的指针p调用 test函数时test函数中隐含的传入了 A* this因此在此处我们是多态调用。派生类用自己的新定义覆盖了从基类继承而来的旧定义但是调用时仍使用的是基类的声明。下面 A a 到底调用哪个版本的 func完全依赖于运行时绑定到它上面的动态类型。 虚函数与其他函数一样虚函数也可以用有默认实参如果某次虚函数调用使用了默认实参则该实参指由本次调用的静态类型决定。 换句话说如果我们通过基类的引用或指针调用函数。则使用基类中定义的默认实参即使实际运行的是派生类中的函数版本也是如此。此时传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参则程序结果将与我们的预期不符。 class A{ public: void test(float a) { cout a; } }; class B :public A{ public: void test(int b){ cout b; } }; int main() { A *a new A; B b new B; a b; a-test(1.1); //输出1.1 }如果虚函数使用默认实参则基类和派生类中定义的默认实参最好一致。 调用的虚函数在运行时才会被解析当某个虚函数通过指针或引用被调用时编译器产生的代码直到运行时才会确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。 那么如果我们使用普通类型非指针非引用的表达式调用虚函数在编译时就会将调用的版本确定下来。 那么我们就产生疑问inline函数可以是虚函数吗答案当然是可以当我们使用普通类型调用虚函数时具有inline属性。如果是多态调用这个函数酒不再是inline因为虚函数要放进虚表中去。 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载 。动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态。 三、虚函数和纯虚函数 1、虚函数 在C语言中基类必须将它的两种成员函数区分开来一种是基类希望其派生类进行覆盖的函数另一种是基类希望派生类直接继承而不要改变的函数。对于前者基类通常将其定义未虚函数virtual。当我们使用指针或引用调用虚函数时该调用将被动态绑定。根据引用或指针所绑定的对象类型不同该调用可能执行基类的版本也可能执行某个派生类的版本。 基类通过在其成员函数的声明语句之前加上关键字 virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。一个派生类的函数如果覆盖了某个继承而来的虚函数则它的形参类型必须与被它覆盖的基类函数完全一致。 同样的派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在两个例外。 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。 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; } };如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统-处理成destructor。析构函数的重写我们将在后文再进行叙述
❕ 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同称子类的虚函数重写了基类的虚函数。 ⚠️关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。 而且 virtual不能与static同时使用。静态成员一定是不被包含在对象中的静态成员属于整个类不属于任何对象所以在整体体系中只有一份。 ⚠️静态成员函数与具体对象无关属于整个类核心关键是没有隐藏的this指针可以通过类名::成员函数名 直接调用此时没有this无法拿到虚表就无法实现多态因此不能设置为虚函数。 当然我们也可以会批虚函数的机制在某些情况下我们可能希望对虚函数的调用不进行动态绑定而是强迫其执行虚函数的某个特定版本。我们可以使用作用域运算符实现此目的 class Base { public:virtual void func() {cout Base::func endl;} }; class Derived :public Base { public:virtual void func() {cout Derived::func endl;} }; int main(){Derived d;Base* pb d;pb-func(); //Base::func pb-Base::func(); //Derived::func return 0; }运行时的多态性可通过和虚函数实现。不可通过模板实现因为模板属于编译时多态。编译时的多态性可通过函数重载实现。 class A { public:virtual void f() { cout A::f() endl; } };class B : public A { private:virtual void f() {cout B::f() endl;} }; int main() {A* pa (A)new B; //或 A pa new B;均合法pa-f(); //B::f() }此段代码编译正确虽然子类函数为私有但是多态仅仅是用子类函数的地址覆盖虚表最终调用的位置不变只是执行函数发生变化。不强制也可以直接赋值因为赋值兼容规则作出了保证。 2、虚析构函数 继承关系中对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数这样我们就能动态分配继承体系中的对象了。 当我们delete一个动态分配的对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。 例如Qutoe是Bulk_quote 的父类。我们 delete一个 Quote类型的指针则该指针有可能实际指向了一个Bulk_quote 类型的对象。如果这样的话编译器就必须清楚它应该执行的是Bu1k_quote的析构函数。和其他函数一样我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本 class Quote { public:如果我们删除的是一个指向派生类对象的基类指针则需要虚析构函数virtual ~Quote() default; 动态绑定析构函数 };和其他虚函数一样析构函数的虚属性也会被继承。因此无论Quote的派生类使用合成的析构函数还是定义自己的析构函数都将是虚析构函数。只要基类的析构函数是虚函数就能确保当我们 delete基类指针时将运行正确的析构函数版本 Quote itemP new Quote; //静态类型与动态类型一致 delete itemP; //调用 Quote的析构函数 itemP new Bulk_quote; //静态类型与动态类型不一致 delete itemP //调用Bulk guote的析构函数⚠️如果基类的析构函数不是虚函数则delete一个指向派生类对象的基类指针将产生未定义的行为。 析构函数需要构成重写那么编译器会对析构函数名进行特殊处理处理成destrutor()所以父类析构函数不加virtual的情况下子类析构函数和父类析构函数构成隐藏关系。 如前所述在析构函数体执行完成后对象的成员会被隐式销毁。类似的对象的基类部分也是隐式销毁的。因此和构造函数及赋值运算符不同的是派生类析构函数只负责销毁由派生类自己分配的资源 class D :public Base { public://Base::~Base被自动调用执行~D() {/该处由用户定义清除派生类成员的操作/ } };对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行然后是基类的析构函数以此类推沿着继承体系的反方向直至最后。 那么我们在构造函数和析构函数中调用虚函数会发生什么呢 如我们所知派生类对象的基类部分将首先被构建。当执行基类的构造函数时该对象的派生类部分是未被初始化的状态。类似的销毁派生类对象的次序正好相反因此当执行基类的析构函数时派生类部分已经被销毁掉了。由此可知当我们执行上述基类成员的时候该对象处于未完成的状态。 为了能够正确地处理这种未完成状态编译器认为对象的类型在构造或析构的过程中仿佛发生了改变一样。也就是说当我们构建一个对象时需要把对象的类和构造函数的类看作是同一个对虚函数的调用绑定正好符合这种把对象的类和构造函数的类看成同一个的要求对于析构函数也是同样的道理。上述的绑定不但对直接调用虚函数有效对间接调用也是有效的这里的间接调用是指通过构造函数或析构函数调用另一个函数。为了理解上述行为不妨考虑当基类构造函数调用虚函数的派生类版本时会发生什么情况。这个虚函数可能会访问派生类的成员毕竟如果它不需要访问派生类成员的话则派生类直接使用基类的虚函数版本就可以了。然而当执行基类构造函数时它要用到的派生类成员尚未初始化如果我们允许这样的访问则程序很可能会崩溃。 在此我们看一道选择题 假设A类中有虚函数B继承自AB重写A中的虚函数也没有定义任何虚函数则 A.A类对象的前4个字节存储虚表地址B类对象前4个字节不是虚表地址 B.A类对象和B类对象前4个字节存储的都是虚表的地址 C.A类对象和B类对象前4个字节存储的虚表地址相同 D.A类和B类中的内容完全一样但是A类和B类使用的不是同一张虚表 此题选 B。为什么呢 A.父类对象和子类对象的前4字节都是虚表地址。 B.A类对象和B类对象前4个字节存储的都是虚表的地址只是各自指向各自的虚表。 C.不相同各自有各自的虚表。 D.A类和B类不是同一类内容不同。 如果构造函数或析构函数调用了某个虚函数则我们应该执行与构造函数或析构函数所属类型相对于的虚函数版本。 ⚠️派生类对象销毁时先调用基类析构函数后调用子类析构函数 3、抽象基类和纯虚函数 在虚函数的后面写上 0 则这个函数为纯虚函数。含有纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 抽象类负责定义接口而后续的其他类可以覆盖该接口。 class LPL{ public:virtual void name() 0; }; class EDG :public LPL { public:virtual void name() { cout EDG endl; } }; class LNG :public LPL { public:virtual void name() { cout LNG endl; } }; int main(){LPL* pEDG new EDG;pEDG-name(); //EDGLPL* pLNG new LNG;pLNG-name(); //LNGreturn 0; }普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 我们可以为纯虚函数提供定义不过函数体必须定义在类的外部。也就是说我们不能在类的内部为一个 0 的函数提供函数体。若定义在类的内部会出现错误pure-specifier on function-definition。 4、多态的原理 通过观察测试我们发现b对象是8bytes除了_b成员还多一个_vfptr放在对象的前面注意有些平台可能会放到对象的最后面这个跟平台有关对象中的这个指针我们叫做虚函数表指针v代表virtualf 代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表。那么派生类中这个表放了些什么呢我们接着往下分析 class Base{ public:virtual void func1() { cout Base::func1() endl; } private:int _b 1;char _ch a; }; int main(){cout sizeof(Base) endl;//12//有了虚函数后对象中会多一个指针虚函数表指针 }添加了两个函数后类的大小仍不改变。 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;char _ch a; }; int main(){cout sizeof(Base) endl;//12//有了虚函数后对象中会多一个指针虚函数表指针Base bb; }⚠️虚函数表指针简称虚表指针。 我们增加一个派生类Derive去继承Base且Derive中重写Func1。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;char _ch a; }; class Derive :public Base{ public:virtual void func1() { cout Derive::func1() endl; } private:int _d 2; }; void t() {Base bb; Derive dd;cout sizeof(Base) endl;//12cout sizeof(Derive) endl;//16 }虚函数的重写也叫覆盖。
派生类对象dd中也有一个虚表指针dd对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在部分的另一部分是自己的成员。 基类对象和派生类对象虚表是不一样的这里我们发现Func1完成了重写所以dd的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法覆盖是原理层的叫法。 即派生类由父类和派生类构成父类中有虚表子类中包含的父类含有虚表子类自己的成员无虚表。另外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() { cout Derive::func1() endl; } private:int _d 2; }; void t() {int i 0;static int j 1;int* p1 new int;const char* p2 xxxxxxx;printf(栈%p\n, i);printf(静态区%p\n, j);printf(堆%p\n, p1);printf(常量区%p\n, p2);Base b; Derive d;Base* pb b;Derive* pd d;printf(Base虚表地址%p\n, (int)pb);printf(Derive虚表地址%p\n, (int)pd); }从打印结果可以看出 虚表位于常量区。vs和linux下都是 虚函数表是class specific的也就是针对一个类来说的这里就如同类里面的static成员遍历即它是属于一个类所有对象的不是属于某个对象特有的是一个类所有对象公有的。 虚表是什么阶段生成的 虚表是在编译时期生成的而虚表指针是在构造函数的初始化列表生成的。一个类的不同对象用的同一张虚表。 虚表是在编译时生成的。 在构造函数中走初始化列表之前初始化虚表指针。 我们可以通过如下代码打印类的虚表大家可以拿来实验 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() { cout Derive::func1() endl; }virtual void func3() { cout Derive::func3() endl; } private:int _d 2; }; typedef void(*VF_PTR)(); void PrintVFT(VF_PTR vtf[]) {cout 虚表地址 vtf endl;for (int i 0; vtf[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vtf[i]);VF_PTR f vtf[i];f();}cout endl; } void t() {Base b;Derive d;// 思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数///指针的指针数组这个数组最后面放了一个nullptr// 1.先取b的地址强转成一个int*的指针// 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针// 3.再强转成VFPTR因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案再编译就好了。VF_PTR vTableb (VF_PTR)((int)b);PrintVFT(vTableb);VF_PTR vTabled (VF_PTR)((int*)d);PrintVFT(vTabled); }⚠️多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。 下面我们看一道关于继承的选择题来帮我们理解 假设D类先继承B1然后继承B2B1和B2基类均包含虚函数D类对B1和B2基类的虚函数重写了并且D类增加了新的虚函数则
A.D类对象模型中包含了3个虚表指针 B.D类对象有两个虚表D类新增加的虚函数放在第一张虚表最后 C.D类对象有两个虚表D类新增加的虚函数放在第二张虚表最后 D.以上全部错误 此题选 B。为什么呢 A.D类有几个父类如果父类有虚函数则就会有几张虚表自身子类不会产生多余的虚表所以只有2张虚表。 C.子类自己的虚函数只会放到第一个父类的虚表后面其他父类的虚表不需要存储因为存储了也不能调用。 四、重载、覆盖(重写)、隐藏(重定义)的对比 在C中重载、覆盖重写和隐藏重定义都是面向对象编程中的概念用于处理函数的多态性。下面对它们进行比较 重载Overloading 定义重载是指在同一个作用域内使用相同的函数名但具有不同的参数列表的情况。函数重载可以根据参数的类型、顺序和个数进行区分。特点 函数名相同参数列表不同。返回值类型可以相同也可以不同。发生在同一个类或命名空间中。 覆盖重写Override 定义覆盖是指在派生类中重新实现基类中已经存在的虚函数。通过在派生类中使用相同的函数名、参数列表和返回类型来覆盖基类的函数。协变除外特点 函数名、参数列表和返回类型相同。发生在继承关系中基类函数必须声明为虚函数。 隐藏重定义Hide 定义隐藏是指在派生类中定义了与基类中相同名称的非虚函数从而隐藏了基类中的同名函数。隐藏并不涉及到动态绑定。特点 函数名相同参数列表可以相同也可以不同。发生在继承关系中两个基类和派生类的同名函数不构成重写就是重定义。
总结 重载发生在同一个类或命名空间中的函数之间根据参数的类型、顺序和个数进行区分。覆盖发生在继承关系中派生类重新实现了基类中的虚函数函数名、参数列表和返回类型相同。隐藏发生在继承关系中派生类定义了与基类中同名的非虚函数基类中的同名函数被隐藏。 需要注意的是覆盖只能发生在虚函数上而隐藏可以发生在虚函数和非虚函数上。使用 virtual 关键字声明函数为虚函数从而允许覆盖。使用作用域解析运算符 :: 可以指定访问被隐藏的基类函数。