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

福州网站建设策划动漫制作技术专业入门

福州网站建设策划,动漫制作技术专业入门,wordpress拖拽编辑插件,域名解析手机网站建设前置知识#xff1a;类和对象 参考书籍#xff1a;《C Primer 第五版》 目录 什么是面向过程#xff1f;什么是面向对象#xff1f; 一、封装 1、封装的含义以及如何实现封装 1.1 访问限定符#xff08;访问说明符#xff09; 1.2 什么是封装#xff1f; 2、封装的优点… 前置知识类和对象 参考书籍《C Primer 第五版》 目录 什么是面向过程什么是面向对象 一、封装 1、封装的含义以及如何实现封装 1.1 访问限定符访问说明符 1.2 什么是封装 2、封装的优点 3、class与struct的区别  4、友元——“突破封装” 4.1友元的声明 4.2友元在什么时候用有何利弊 4.3几点注意事项 5、类的static成员——“与类关联的成员” 5.1 声明静态成员 5.2 类外定义与初始化 5.3 类内定义与初始化 5.4 静态成员的使用场景 二、继承 1、继承的概念以及实现 1.1继承的概念 1.2 定义派生类——派生类列表  1.3 访问控制与继承 1.3.1 受保护的成员(protected)  1.3.2  继承基类成员访问方式的变化 1.3.3  改变个别成员的可访问性 ——using  2、派生类向基类的类型转换 2.1 派生类对象的内存分布 2.2 派生类向基类的隐式类型转换 2.3 切掉 —— 对象之间不存在类型转换 3、继承中的static与friend 3.1 继承于静态成员  3.2  友元与继承 4、继承中的作用域与隐藏 4.1 基类与派生类作用域关系 4.2 隐藏/重定义 5、派生类的默认成员函数 5.1 默认构造函数 5.2 拷贝构造函数。 5.3 赋值运算符重载 operator 5.4 析构函数 6、多继承及其造成的问题与解决方法 6.1 菱形继承及其问题 6.1.1  多继承 6.1.2  菱形继承模型 6.1.3  数据冗余与二义性问题 6.2 虚继承实现和底层原理 6.2.1  虚拟继承的实现 6.2.2  虚拟继承底层原理 7、继承与组合 7.1 Is A  与 Has A 7.2 继承和组合的应用场景 三、多态 1、虚函数 1.1 虚函数的定义 1.2 覆盖 1.2.1  虚函数的覆盖的条件与实现 1.2.2  协变 —— 返回值类型不同 1.3 动态绑定与静态绑定 1.3.1  动态类型 1.3.2  静态类型 1.3.3  动态绑定 1.3.4  静态绑定 1.4 虚析构函数 1.5 override 与 final 说明符 1.5.1 override 1.5.2 final  1.6 虚函数与作用域 1.7 重载、覆盖(重写)、隐藏(重定义) 的总结与对比 2、什么是多态 3、 抽象基类 3.1 纯虚函数 3.2 抽象基类 4、多态的底层原理 4.1 虚函数指针与虚函数表 4.2底层原理  4.3 单继承与复杂继承关系的虚函数表 4.2.1  单继承 4.2.2  多继承 4.2.3  菱形继承 4.2.4  菱形虚拟继承 什么是面向过程什么是面向对象 面向对象编程(object-oriented programming) :  利用数据抽象、继承以及动态绑定等技术编写程序的方法。 拿点外卖来说如果是C我会将点外卖分为四个过程用户拿手机点餐、外卖员取餐、外卖员送餐和用户取餐用户评价用餐等对于这四个过程我都要指定一个人去做将结构体指针传入函数这就是面相过程编程可以说C语言就是一个面向过程的语言。 而对于面向对象编程我会将上述四个过程分成两个类两个类可以实例化为多个对象对应于有多个不同的外卖员和用户每个对象调用自身的方法成员函数比如外卖员可以去做取餐和送餐的工作而用户不能去做这些事对于一个用户类实例化出的对象比如我自己则要做的是用户该做的事。 /****************代码(1) 以下代码实现了上述例子中的两个类****************/ class Delivery_boy { public://类的方法void Pick_up_meals() {} void Food_delivery() {} //类的属性std::vectorstd::string orders; std::string time; };class Client { public:void Pick_up_meals() {}void Meal() {}void Appraise() {}int money;std::string phone; };int main() {Delivery_boy Tom;Tom.Pick_up_meals(); //派送员取餐Client Dusong;return 0; } 一、封装 1、封装的含义以及如何实现封装 可以看到上述两个类并没有封装也就是说我们可以直达对象内部并控制它的具体实现细节 1.1 访问限定符访问说明符 ①public定义在public说明符之后的成员在整个程序内可被访问public成员定义类的接口 ②private定义在private说明符之后的成员可以被类的成员函数访问但是不能被使用改类的代码访问private部分封装了类的实现细节  (C Primer   P240) 1.2 什么是封装 含义将数据与操作数据的方法进行有机结合隐藏对象的属性和实现细节仅对外公开接口来和对象进行交互 实现通过类将数据以及操作数据的方法进行有机结合通过访问权限访问限定符来隐藏对象内部的实现细节控制哪些方法可以在类外部直接被使用 /****************代码(2) : 将上述例子中的外卖员类进行封装如下**************/ class Delivery_boy { public:void Pick_up_meals(std::string where, std::string which) {}void Food_delivery() {}private:std::vectorstd::string orders;std::string time; };int main() {Delivery_boy Tom;Tom.Pick_up_meals(China, McDonalds);//Tom.time 2023-7-28; 报错不可访问return 0; } 2、封装的优点 ①确保用户代码不会无意间破环封装对象的状态         ----如代码(2)中主函数的第三行代码用户代码访问并尝试更改私有成员编译器报错  ②被封装的类的具体实现细节可以随时改变而无需调整用户级别的代码         ----如代码(2)中主函数的第二行代码  (C Primer   P242) 3、class与struct的区别  ①封装中class定义的类默认访问权限private  struct定义的类默认访问权限public  (C Primer   P240) ②模板参数中typename与class是可以用来定义模板参数关键字两者有一定区别而struct不能替代class ③继承中class默认继承方式private  struct默认继承方式public    (C Primer   P546) 4、友元——“突破封装” 4.1友元的声明 友元函数是定义在类外部的函数,它可以直接访问类的私有成员且它不属于任何类但是需要在类的内部声明声明时加上friend关键字 /***********************代码(3) : 友元函数********************/ class Delivery_boy { public:void Pick_up_meals(std::string where, std::string which) {}void Food_delivery() {}friend std::string Get_time(const Delivery_boy person); //友元函数声明private:std::vectorstd::string orders;std::string time; };std::string Get_time(const Delivery_boy person) {return person.time; //person对象在类外访问私有成员 }int main() {Delivery_boy Tom;std::string time Get_time(Tom);return 0; } 4.2友元在什么时候用有何利弊 什么时候用根据上述可知当需要在类外访问对象的私有成员时可以使用友元函数 利实现类之间的数据共享 提高程序运行效率方便编程 弊增加耦合度破坏数据的隐蔽性和类的封装性降低了程序的可维护性 4.3几点注意事项 ①友元函数不能用const修饰肯定的因为这里的const修饰的是this指针而友元函数不是成员函数没有this指针 ②友元分为友元函数与友元类 ③许多编译器并未强制限定友元函数必须在使用之前在类的外部声明C Primer P242 5、类的static成员——“与类关联的成员” 有的时候类需要它的一些成员与类本身直接相关而不是与类的各个对象保持联系 5.1 声明静态成员 我们通过在成员的声明之前加上static关键字使得其与类关联在一起。和其他成员一样静态成员也可以是公有(public)或者私有(private)的。需要注意的是static成员包含static成员函数以及static成员变量他们不属于任何对象其static成员函数不包含this指针即不能声明成const的 5.2 类外定义与初始化 •  当在类的外部定义静态成员变量时不能重复static关键字该关键字只出现在类内部的声明语句前定义时需要指定对象的类型名、类名、作用域运算符以及成员自己的名字 •  类似于全局变量静态成员变量定义在任何函数之外因此一旦它被定义就将一直存在与程序的整个生命周期中 /*****************代码段(4) : 类外定义与初始化静态成员****************/ class Delivery_boy { public:void Pick_up_meals(std::string where, std::string which) {}void Food_delivery() {}private:std::vectorstd::string orders;std::string time;static int Number_of_workers; //static成员变量的声明}; int Delivery_boy::Number_of_workers 10; //定义以及初始化5.3 类内定义与初始化 通常情况下类的静态成员不应该在类的内部初始化我们可以为静态成员提供const整数类型的类内初始值不过要求静态成员必须是字面值常量的类型的constexpr。初始值必须是常量表达式因为constexpr修饰的成员本身就是常量表达式 /*****************代码段(5) : 类外定义与初始化静态成员****************/ class Delivery_boy { public:void Pick_up_meals(std::string where, std::string which) {}void Food_delivery() {}//void change() { // Number_of_workers 12; ❌//}private:std::vectorstd::string orders;std::string time;static constexpr int Number_of_workers 10; //static成员变量的定义与初始化//int arr[Number_of_workers]; ✔ }; //一个不带初始值的静态成员的定义 constexpr int Delivery_boy::Number_of_workers; //初始值在类的定义内提供 显然此时静态成员变量的值是无法更改的。 5.4 静态成员的使用场景 ①  静态数据成员可以是不完全类型 /*****************代码段(6) : static与不完全类型****************/ class Delivery_boy { public:void Pick_up_meals(std::string where, std::string which) {}void Food_delivery() {} private:static Delivery_boy p1; //静态成员可以是不完全类型Delivery_boy* p2;Delivery_boy p3; //指针和引用成员可以是不完全类型Delivery_boy p4; //错误不允许使用不完全类型}; ②  静态成员可作为默认实参  (C Primer   P271) 二、继承 1、继承的概念以及实现 1.1继承的概念 通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class) 其他类则直接或间接地从基类继承而来这些继承得到的类成为派生类(derived class)。基类负责定义在层次关系中所有类共同拥有的成员而每个派生类定义各自的有的成员 简单来说继承就是一种类的复用。 1.2 定义派生类——派生类列表 派生类需要使用派生类列表指明它是从哪个或哪些基类继承而来的(涉及多继承) 派生类列表的格式在派生类后加上冒号后面跟上以逗号分隔的基类列表  (C Primer   P529) 1.3 访问控制与继承 派生类继承了所有基类成员但每个类控制着其成员对于派生类来说是否可访问(accessible) 1.3.1 受保护的成员(protected) 一个类使用protected关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员 •  与private成员类似protected成员不能该类的对象访问 •  和public成员类似protected成员对于派生类的成员和友元来说是可访问的 (C Primer    P543) /*****************代码段(7) : protected成员****************/ class Person {void eat(std::string food) {};void buy(std::string goods) {}; protected:int money;int age; };class Delivery_boy : public Person{ public:friend void Pick_up_meals(Delivery_boy);friend void Food_delivery(Person);std::vectorstd::string orders;std::string time; }; void Pick_up_meals(Delivery_boy d) {d.time 2022;d.age 18; //基类的protected成员可以被派生类的成员或者友元访问 }//该函数是派生类的友元函数无法被基类对象访问protected成员根本上还是在类外访问protected成员此时与private成员类似 void Food_delivery(Person p) {p.age 10; //错误 }1.3.2  继承基类成员访问方式的变化 1.3.3  改变个别成员的可访问性 ——using  有时我们需要改变派生类继承基类的莫格成员的访问级别可以通过 using 声明到达这一目的 C Primer  P545     不是很常用了解即可 2、派生类向基类的类型转换 2.1 派生类对象的内存分布 /**************代码(7) : 基于以下代码建立对象的内存存储模型************/ class Person { public:int age;char gender; };class Programmer : public Person { public:int wages;char language; };int main() {Person Rose;Rose.age 10;Rose.gender g;Programmer dusong;dusong.age 20;dusong.gender b;dusong.language C;dusong.wages 250;return 0; } 调试上述代码(7)得下图 从内存和监视窗口我们能很清晰的看到从派生类对象指针所指向的内存的前八个字节为从基类继承下来的成员后八个字节为派生类对象自定义的成员 需要注意的是继承自基类的部分和派生类自定义的部分不一定是连续存储的。  (C Primer   P530) 2.2 派生类向基类的隐式类型转换 由2.1可知因为在派生类对象中含有与其基类对应的组成成分所以我们能把派生类的对象当成基类对象来使用而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。(C Primer   P530) /**************代码段(8) : 在代码(7)的main函数的最后添加以下代码************/Person* Rose_ptr Rose; //Rose_ptr为指向Rose的指针Rose_ptr dusong; //(1) Rose_ptr指向dusong的Person部分Person nobody dusong; //(2) nobody绑定到dusong的Person部分 如代码段(8)所示派生类可以转换到  (1)基类的指针 / (2)基类的引用但该转换只能是单向的即只能由派生类到基类。 2.3 切掉 —— 对象之间不存在类型转换 派生类向基类的自动类型转换只对指针或引用类型有效在派生类类型和基类类型之间不存在这样的转换。 当我们用一个派生类初始化或赋值一个基类时实际调用的时基类的拷贝构造函数或赋值运算符重载函数在这个过程中只有该派生类对象中的基类部分会被拷贝、移动或赋值它的派生类部分会被切掉(sliced down)      (C Primer   P535) 同样基类拷贝或赋值给派生类是错误的。 3、继承中的static与friend 3.1 继承于静态成员 如果基类定义了一个静态成员则在整个继承体系中只存在该成员的唯一定义    (C Primer   P532) 3.2  友元与继承 友元关系不能传递同样友元关系也不能继承。 •  基类的友元在访问派生类成员时不具有特殊性    (C Primer   P545) 怎么理解这句话呢在C Primer书中的第545页的第一段举例的代码中有行很有意思的代码我将其简化为以下代码便于理解⬇️ /********************代码(9) : 证明友元关系不能继承******************/ class Base {//Pal是Base的友元类friend class Pal; //Pal在访问Base的派生类Sneaky时不具有特殊性 private:int Base_mem; };class Sneaky : public Base { //Sneaky为Base的派生类 private:int Sneaky_mem; };class Pal { public:int f1(Sneaky s) {return s.Sneaky_mem; //❌Pal是Base的友元但Pal不是Sneaky的友元证明了友元关系不能继承} int f2(Sneaky s) {return s.Base_mem; //✔: Pal是Base的友元类即可访问Base的私有成员} }; 三者关系如图  类似的派生类的友元也不能随意访问基类的成员     将代码(9)中Pal类给成Sneaky的友元Pal中 f1 函数中的语句正确和  f2 中的语句报错 即证明派生类的友元不能访问基类的成员。 4、继承中的作用域与隐藏 4.1 基类与派生类作用域关系 在继承体系中基类和派生类都有独立的作用域并且派生类的作用域位于基类作用域之类如果一个名字在派生类的作用域中无法正确解析那么编译器会继续在外层的基类作用域中寻找该名字的定义 基于独立的作用域我们可以在继承体系的各自类域中定义同名成员在实际编程中尽量避免 /********************代码(10) : 继承作用域实验******************/ class Base { public:Base() : mem1(1), mem2(2) {} //初始化int func() { return mem1; } protected:int mem1;int mem2; };class Derive : public Base{ public:Derive() : mem1(11), mem3(33) {} //初始化int func() { return mem1; } //隐藏基类中的func protected:int mem1; //隐藏基类中的mem1int mem3; };int main() {Base a;Derive b;return 0; }4.2 隐藏/重定义 派生类能重用定义在其直接基类或间接基类中的名字此时定义在内层作用域派生类的名字将隐藏定义在外层作用域基类的名字 总的来说派生类的成员将隐藏同名的基类成员 (C Primer   P548) /**************代码段(11) : 将以下代码加入在代码(10)的主函数末尾***************/std::cout b.func() std::endl; //打印11std::cout b.Base::func() std::endl; //打印1 显示访问基类成员 5、派生类的默认成员函数 5.1 默认构造函数 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表显示调用 需要注意的是图片中的代码Base(x)先于mem1(11)执行这与初始化列表中的顺序无关。 5.2 拷贝构造函数。 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化 /**************代码段(12) : 派生类的拷贝构造函数***************/ class Base { public:Base(const Base b) : mem1(b.mem1) {} protected:int mem1; };class Derive : public Base{ public:Derive(const Derive d) : Base(d), mem2(d.mem2) {} //显示调用基类的拷贝构造 protected:int mem2; }; 5.3 赋值运算符重载 operator 同理派生类的operator必须调用基类的operator完成赋值 /**************代码(13) : 派生类的赋值运算符***************/ class Base { public:Base(int x) : mem1(x) {}Base operator(const Base b) {if (this ! b) mem1 b.mem1;return *this;} protected:int mem1; };class Derive : public Base{ public:Derive(int x, int y) : Base(x), mem2(y) {}Derive operator(const Derive d) {Base::operator(d); //显式调用基类的operatorif (this ! d) mem2 d.mem2;return *this;} protected:int mem2; };int main() {Derive d1(10, 20);Derive d2(30, 40);d2 d1; //如果没有显示调用基类的operator那么d2中mem1的值仍然是30return 0; } 5.4 析构函数 在析构函数题执行完成后对象成员会被隐式销毁。类似的对象的基类部分也是隐式销毁的。因此和构造函数及赋值运算符不同的是派生类析构函数只负责销毁由派生类自己分配的资源    (C Primer   P556) /**************代码段(14) : 派生类的析构函数***************/ class Base { public:Base(int x) : mem1(x) {}~Base() { /*该处由用户定义清理基类成员的操作*/ } protected:int mem1; }; class Derive : public Base{ public:Derive(int x, int y) : Base(x), mem2(y) {}~Derive() { /*该处由用户定义清理派生类成员的操作*/ }//Base::~Base() 在~Derive之后被自动调用 protected:int mem2; }; 总结构造和析构函数从创建到销毁一个派生类的顺序如下图⬇️ 6、多继承及其造成的问题与解决方法 6.1 菱形继承及其问题 6.1.1  多继承 在上述1.2定义派生类中我们说到基类列表由逗号分隔表示派生类由多个基类继承 6.1.2  菱形继承模型 尽管在派生列表中同一个基类只能出现一次但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类如下图也可以直接继承某个基类然后通过另一个基类再一次间接继承该类 (C Primer   P717) 直接基类不出现在派生类的派生类列表中的基类直接基类以直接或间接方式继承的类是派生类的间接基类如上图Animal是Panda的间接基类 间接基类 派生类直接继承的基类直接基类在派生类的派生列表中说明直接基类本身也可以是一个派生类如上图ZooAnimal是Panda的直接基类 6.1.3  数据冗余与二义性问题 我们通过下面代码(15)的简单例子来初步理解菱形继承带来的问题 /******************代码(15) : 菱形继承*********************/ class A { public:int a; }; class B : public A { public:int b; }; class C : public A { public:int c; }; class D : public B, public C { public:int d; }; int main() {D d;d.b 1;d.c 2;//通过像是访问指定访问哪个父类成员可以解决二义性问题但是数据冗余问题无法解决d.B::a 3; //通过对象d访问类B继承类A的成员ad.C::a 4; //通过对象d访问类C继承类A的成员ad.d 5;return 0; } 通过内存窗口与监视窗口我们可以看到一个对象里同时存在了两个类A的成员如果不指定类域那么编译器将无法判断访问哪一个成员造成二义性问题 通过指定类域可以解决上述问题数据冗余无法的到解决就如下图所示d对象中包含两个a成员 6.2 虚继承实现和底层原理 在C语言中我们通过虚继承(virtual inheritance)的机制解决上述问题。 虚继承的目的是令某个类做出声明承诺愿意共享它的基类。其中共享它自己基类子对象的基类称为虚基类(virtual base class)。   (C Primer   P717 6.2.1  虚拟继承的实现 实现虚拟继承实质上就是指定虚基类我们指定虚基类的方式是在继承方式前面或者后面加上virtual关键字 /**************代码段(16) : 将代码段(15)的B、C类定义为虚基类***************/ class B : public virtual A {}; class C : virtual public A {}; 虚基类会共享其基类的子对象实现虚拟继承进而解决数据冗余和二义性问题 6.2.2  虚拟继承底层原理 通过代码段(16)的更改之后对于更改成虚拟继承的代码(15)进行调试得下图⬇️ 对比6.1.3的内存分配图两个基类在派生类的内存分布的前四个字节均从一个整数变为一个指针将该指针称为虚基表指针。由图的可知该指针指向内存中的一块位置称为虚基表。 我们主要关注  d.B::a 3  这一段代码的汇编代码 • 第一行将对象d在内存中的起始位置的值转给eax寄存器此时我们在监视窗口看到eax寄存器的值为0x00487b48, 即eax为虚基表指针 • 第二行将eax(0x00487b48)4得到0x00487b4c随后将该地址的值move给ecx寄存器此时ecx的值为20(十进制) • 第三行学过C语言可知d[20] 等价与 *(d 20) 我们可以将这一行汇编代码理解为将与d对象起始位置相离偏移量为20的地址的值设为3 通过上述操作可知虚基类将自己的基类子对象存储在了派生类内存的一块共享位置并通过自己的虚基表指针找到虚基表中的偏移量来访问该地址以达到菱形继承造成的数据冗余和二义性问题 7、继承与组合 7.1 Is A  与 Has A 当我们令一个类公有的继承另一个类时派生类应当反映与基类“是一Is A”的关系 类型之间的另一种常见关系时“有一个Has A”的关系具有这种关系的类暗含成员的意思。     (C Primer   P564 7.2 继承和组合的应用场景 继承一定程度破坏了基类的封装基类的改变对派生类的影响较大派生类和基类间的以来关系很强耦合度很高而组合类之间没有很强的依赖关系耦合度低所以我们优先选择类之间的组合关系但有些关系更适合用继承或者需要实现多态时则使用继承 三、多态 1、虚函数 1.1 虚函数的定义 对于某些函数基类希望它的派生类各自定义适合自身的版本此时基类就将这些函数声明成虚函数(virtual function) 要定义虚函数只需在类的成员函数的声明前加上virtual关键字 /******************代码段(17) : 定义虚函数*********************/ class Common { public:virtual void Buy() { //定义虚函数std::cout Price std::endl; } };1.2 覆盖 1.2.1  虚函数的覆盖的条件与实现 派生类中定义的虚函数如果与基类中定义的虚函数 ①同名 且拥有相同的 ②参数列表 与 ③返回值则派生类版本将覆盖(override)基类的版本   (C Primer   P537/P576 /******************代码段(18) : 虚函数的覆盖*********************/ class Common { public:virtual void Buy() {std::cout Price std::endl;} }; class Member : public Common{virtual void Buy() { //覆盖重写std::cout discount std::endl; } }; 1.2.2  协变 —— 返回值类型不同 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回类对象的指针或引用派生类虚函数返回派生类对象的指针或引用时称为协变 1.3 动态绑定与静态绑定 1.3.1  动态类型 对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。 基类的指针或引用可以指向一个派生类的对象在这样的情况中静态类型是基类的引用或指针而动态类型是派生类的引用或指针如代码(18)。 /**************代码(19) : 在代码段(18)之后加上如下代码*****************/ void Buy_a_toy(Common p) { //静态类型Commonp.Buy(); }int main() {Common cp;Member mp;Buy_a_toy(cp);Buy_a_toy(mp); //动态类型Member 实质上是Common 绑定到mp这个对象 } 1.3.2  静态类型 对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。 总结当且仅当对通过指针或引用调用虚函数时才会在运行时解析该调用也只有在这种情况下对象的动态类型才有可能与静态类型不同    (C Primer   P537 1.3.3  动态绑定 在C中当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定(dynamic binding)    (C Primer   P527 动态绑定直到运行时才确定到底执行函数的哪个版本因此又称之为运行时绑定。在C中动态绑定的意思是在运行时根据引用或指针所绑定的对象的实际类型来选择执行虚函数的某一个版本 1.3.4  静态绑定 在程序编译期间确定程序的行为也称静态多态例如函数重载 1.4 虚析构函数 与普通类的析构函数一样但我们delete一个动态分配(new)的对象的指针时将执行析构函数 如果该指针指向继承体系中的某个类型则有可能出现指针的静态类型与被删除对象的动态类型不符的情况(参见本章1.3节)    (C Primer   P552 如果基类的析构函数不是虚函数的delete一个指向派生类对象的基类指针将产生未定义行为(此时对调用基类的析构函数导致派生类独自的空间未被释放)⬇️ 我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本⬇️派生类对象的析构顺序参见第二章5.4节 为什么这里派生类和基类的析构函数名字并不相同但是能构成虚函数覆盖呢这里可以理解编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor 1.5 override 与 final 说明符 1.5.1 override 通过第二章4.2节介绍的派生类如果定义了一个函数与积累中虚函数的名字相同但是形参列表不同此时构成隐藏时合法的行为但是如果我们原本希望该函数覆盖掉基类的版本此时调试并发现这样的错误是很难的。 在C11标准中我们可以使用override关键字来标记某个派生类的函数如果该函数没有覆盖已存在的虚函数此时编译器将报错     (C Primer   P538 /*****************代码(20) : override使用**********************/ struct B {virtual void f1(int) const;virtual void f2();void f3(); }; struct D : public B {void f1(int) const override; //✔: f1与基类中的f1匹配void f2(int) override; //❌B中没有形如f2(int)的函数 --- 避免了无意间的函数隐藏void f3() override; //❌f3不是虚函数void f4() override; //❌B中没有名为f4()的函数自然不构成覆盖的条件 }; 1.5.2 final 相反如果我们一个虚函数定义成final那么之后任何尝试覆盖该函数的操作都将引发错误⬇️  1.6 虚函数与作用域 本小节我们对于书上第550页的例子做深入了解巩固我们之前学习的动态绑定、隐藏、覆盖等知识 /*****************代码(21) : 虚函数与作用域**********************/ class Base { public:virtual int func(); }; class D1 : public Base { public:int func(int); //隐藏virtual void f2(); }; class D2 : public D1 { public:int func(int); //非虚函数隐藏了D1中的func(int)函数int func(); //覆盖Base中的虚函数funcvoid f2(); //覆盖D2中的虚函数f2 }; int main() {Base bobj; D1 d1obj; D2 d2obj;Base* bp1 bobj, *bp2 d1obj, *bp3 d2obj; //动态类型bp1-func(); //(1)虚调用运行时调用 Base::func()bp2-func(); //(2)虚调用D1中的func(int)没有对基类虚函数覆盖运行时调用Base::func()bp3-func(); //(3)虚调用D2中重写了func()函数运行时调用D2::func()D1* d1p d1obj; D2* d2p d2obj; bp2-f2(); //(4)错误Base没有名为 f2 的成员d1p-f2(); //(5)虚调用运行时调用D1::f2()d2p-f2(); //(6)虚调用运行时调用D2::f2()Base* p1 d2obj; D1* p2 d2obj;D2* p3 d2obj;p1-func(1); //(7)错误Base中没有接收一个int的func函数p2-func(1); //(8)静态绑定调用D1::func(int)p3-func(1); //(9)静态绑定调用D2::func(int) return 0; } 对于(1)(2)(3)因为func是虚函数所以编译器产生的代码将在运行时确定使用与函数的哪个版本判断的依据是该指针所绑定对象的类型 对于(7)(8)(9)的调用语句中指针都指向了D2类型的对象但是由于我们调用的是非虚函数所以不会发生动态绑定。实际调用的函数版本 由指针的静态类型决定。 为什么第(4)(7) 语句会错呢我们需要注意的是基类指针绑定到派生类指针并不意味着他能调用派生类独自定义的函数而是通过动态绑定调用派生类所覆盖的基类虚函数 1.7 重载、覆盖(重写)、隐藏(重定义) 的总结与对比 重载两函数处于同一作用域函数名相同参数不同 覆盖(重写)两函数分别位于基类和派生类作用域函数名、参数、返回值都相同(协变除外)两函数均为虚函数 隐藏(重定义)两函数分别位于基类和派生类作用域函数名相同 2、什么是多态 多态可分为动态多态和静态多态 动态多态程序能通过引用或指针的动态类型获取类型特定行为的能力 当我们使用基类的引用或指针调用基类中定义的一个函数是我们并不知道该函数真正作用的对象是什么类型因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数时虚函数则直接运行时才会决定到底执行哪个版本判断的依据是引用或指针所绑定的对象的真实类型      (C Primer   P537 静态多态例如函数重载、运算符重载、函数模板等  3、 抽象基类 3.1 纯虚函数 我们通过在虚函数函数体后加上   0 即可将一个虚函数说明为纯虚函数一个纯虚函数无需定义 /*****************代码段(22) : 纯虚函数**********************/ class Common { public:virtual void Buy() 0; }; 3.2 抽象基类 含有或者未经覆盖直接继承纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口因此又叫做接口类而后续的其他类可以覆盖该接口。   (C Primer   P540 抽象基类(或未覆盖纯虚函数直接继承的派生类)无法实例化出对象 /*****************代码(23) : 抽象基类与纯虚函数的覆盖**********************/ class Common { public:virtual void Buy() 0; }; class Member1 : public Common {void Buy() { //覆盖基类的纯虚函数std::cout discount std::endl;} }; class Member2 : public Common {}; int main() {Common obj1; //报错不允许使用抽象类类型Common的对象: Common::Buy()函数是纯虚拟函数Member1 obj2; //正确Member2 obj3; //派生类Member2未经覆盖直接继承任然是抽象类无法实例化出对象 } 4、多态的底层原理 4.1 虚函数指针与虚函数表 多态的底层是如何实现的如何实现通过指针或引用调用不同的虚函数我们先从以下代码入手⬇️ /*************代码(24) : 通过监视窗口查看虚函数表指针与虚函数表*************/ class Base { public:virtual void f1() {}virtual void f2() {}void f3() {} private:int a 1; };class Derive : public Base{ public:void f1() {} //覆盖基类虚函数virtual void f4() {} //Derive自己的虚函数private:int b 2; }; int main() {Base b;Derive d;return 0; } 调试代码(24)由监视窗口查看基类和派生类对象的构成⬇️ 图中_ vfptr 称为虚函数表指针指向虚函数表虚函数表实质上是存放虚函数指针的数组。 由图可知派生类与基类的虚函数表指针指向地址不同派生类对f1完成了覆盖因此Base::f1() 与 Derive::f1() 的函数地址不同而f2没有完成覆盖两者函数地址相同 这里的派生类通用定义了一个虚函数f4但在监视窗口中没有看见实际上该虚函数地址也存放在了派生类的虚函数表中我们通过后面的学习验证这个说法 4.2底层原理  /*************代码段(25) : 继承体系参考代码(24)*************/ Base bobj; Derive dobj; Base* p1 bobj, * p2 dobj; p1-f1(); p2-f1(); 由代码段(25)的汇编代码得 4.3 单继承与复杂继承关系的虚函数表 4.2.1  单继承 在本章节的4.1节说到在派生类的虚函数表中没有看到派生类自己的虚函数我们通过以下代码打印虚函数地址验证 /*************代码(26) : 验证派生类虚函数表中的虚函数*************/ class Base { public:virtual void f1() { cout Base::f1() endl; }virtual void f2() { cout Base::f2() endl; }void f3() { cout Base::f3() endl; } private:int a; };class Derive : public Base{ public:virtual void f1() { cout Derive::f1() endl; }virtual void f4() { cout Derive::f4() endl; }void f5() { cout Derive::f5() endl; } private:int b; }; typedef void(*vfptr) (); void Printvftable(vfptr vftable[]) {for (int i 0; vftable[i] ! nullptr; i) {printf(第%d个虚函数地址%p : , i, vftable[i]);vfptr f vftable[i];f(); //call} } int main() {Base bobj;Derive dobj;//取派生类对象的地址强转为整型指针再解引用得到前四个字节再强转为函数指针数组指针vfptr* d_vftable (vfptr*)(*(int*)dobj);Printvftable(d_vftable);return 0; } 可以看出内存窗口中的虚函数表与打印内容完全相符⬇️ 4.2.2  多继承 /*************代码(27) : 验证多继承派生类虚函数表中的虚函数*************/ class Base1 { public:virtual void f1() { cout Base::f1() endl; }virtual void f2() { cout Base::f2() endl; } private:int b1; }; class Base2 { public:virtual void f1() { cout Base::f1() endl; }virtual void f3() { cout Base::f3() endl; } private:int b2; }; class Derive : public Base1, public Base2{ public:virtual void f1() { cout Derive::f1() endl; } //覆盖virtual void f4() { cout Derive::f4() endl; } //派生类自己的虚函数 private:int d; }; typedef void(*vfptr) (); void Printvftable(vfptr* vftable) {cout 虚函数表地址 vftable endl;for (int i 0; vftable[i] ! nullptr; i) {printf(第%d个虚函数地址%p : , i, vftable[i]);vfptr f vftable[i];f(); //call} } int main() {Derive dobj;//取派生类对象的地址强转为整型指针再解引用得到前四个字节再强转为函数指针数组指针vfptr* vftable1 (vfptr*)(*(int*)dobj);Printvftable(vftable1);//强转为char*向后移动sizeof(Base1)个字节此时指向虚函数表指针强转为int*解引用取前四个字节即取到虚表地址vfptr* vftable2 (vfptr*)(*(int*)((char*)dobj sizeof(Base1)));Printvftable(vftable2);return 0; } 由下图内存和代码结果可知多继承的派生类中含有多个虚表指针派生类的虚函数存放再第一张虚表中。 4.2.3  菱形继承 /*************代码(28) : 验证菱形继承派生类虚函数表中的虚函数*************/ typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) {for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :%p, i, vTable[i]);VFPTR f vTable[i];f();}cout endl; } class A { public:virtual void f1() { cout A::f1() endl; }virtual void f2() { cout A::f2() endl; }int a; }; class B : public A { public:virtual void f1() { cout B::f1() endl; }virtual void f3() { cout B::f3() endl; }int b; }; class C : public A { public:virtual void f1() { cout C::f1() endl; }virtual void f4() { cout C::f4() endl; }int c; }; class D : public B, public C { public:virtual void f1() { cout D::f1() endl; } //覆盖virtual void f5() { cout D::f5() endl; }int d; };int main() {D d;d.b 1;d.c 2;d.B::a 3; d.C::a 4; d.d 5;VFPTR* vftable (VFPTR*)(*(int*)d);PrintVTable(vftable);return 0; } 由调式信息和打印结果可知派生类D继承了B和C因此拥有两张虚表第一张虚表存放覆盖的虚函数、间接基类A的虚函数、直接基类B的虚函数以及派生类D自己的虚函数与多继承一样派生类的虚函数存放在第一张虚表中 4.2.4  菱形虚拟继承 /*************代码段(29) : 验证菱形虚拟继承派生类虚函数表中的虚函数*************/ /*将代码(28)中的对应部分加上virtual关键字*/ class B : virtual public A { //虚拟继承 public:virtual void f1() { cout B::f1() endl; }virtual void f3() { cout B::f3() endl; }int b; }; class C : virtual public A { //虚拟继承 public:virtual void f1() { cout C::f1() endl; }virtual void f4() { cout C::f4() endl; }int c; }; 可以看到一个简单的菱形虚拟继承的派生类组成非常复杂 不同于菱形继承虚拟继承共享虚基类的成员地址唯一而对于继承虚基类的多个派生类若拥有自己的虚函数那么不能像普通的继承一样将自己的虚函数存放在基类的虚函数表中而是建立自己独立的虚表并且将虚基类的虚表独立出来如上图所示对于派生类D监视窗口中显示直接基类B直接包含了间接基类A的虚表指针且在内存中虚基类A的虚表指针独立出来。 下图打印l对象的前四个字节所指向的内容(内存中存放的第一个虚表)⬇️ 可以看出派生类的虚函数任然存放在内存中的第一张虚表中通过打印可验证直接基类B的虚表中不含B的基类A的虚函数
http://www.tj-hxxt.cn/news/227501.html

相关文章:

  • 水安建设集团网站wordpress 更改目录
  • 我有网站 怎么做淘宝推广的黄骅做网站的电话
  • 建设网站有什么原则网易企业邮箱改密码
  • 网站建设步骤域名备案中网站可以开通
  • 芜湖网站优化软文广告经典案例300
  • 网站建设费用固定资产怎么入设计好看的企业网站
  • 旅游网站开发设计文档购买网络商城系统
  • 网络上建个网站买东西多少钱wordpress个人博客主题2019
  • 软件公司做网站兰州网络推广的平台
  • 静态网站做等级保护网站建设必会的软件有哪些
  • 哪个在家做兼职网站比较好ui设计是什么含义
  • 优化手机访问网站速度哈尔滨网站搜索优化公司
  • 地方美食网站开发意义北碚网站建设公司
  • 合肥做网站便宜建站网址平台
  • 网站如何做延迟加载中国企业100强排名
  • 北海网站设计公司镇江外贸网站建设
  • 潍坊路通工程建设有限公司网站ps如何做网站轮播图
  • 网站开发书籍国内便宜机票网站建设
  • 怎么在服务器上部署网站微商城登录
  • 上海网站建设 迈网站怎么做301重定向
  • joomla网站如何加入会话功能深圳推广不动产可视化查询
  • 兰州有制作网站网站建设实习报告范文
  • 怎么把网站排名到百度前三名帮传销做网站会违法吗
  • 一条龙网站做网站需要什么配置的电脑
  • 编辑网站的软件手机软件wordpress cms 布局
  • 网站开发详细设计模板乐居房产官方网站
  • 使用腾讯云建设网站深圳遗像制作
  • 做网站需要公司有哪些江苏国龙翔建设有限公司网站
  • 网站文章推广如何制作网络平台
  • 建设银行网站源码英语网站推广策划书