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

四川建设设计公司网站关镇铨装修到底怎样

四川建设设计公司网站,关镇铨装修到底怎样,图片素材网站免费大推荐,网站办事服务建设情况纵有疾风起#xff0c;人生不言弃。本文篇幅较长#xff0c;如有错误请不吝赐教#xff0c;感谢支持。 #x1f4ac;文章目录 一.类和对象的概念①什么是对象#xff1f;②抽象和类1.类的基本概念2.类的声明与定义#xff1a;3.对象的创建与使用 二.类的封装①为什么有封… 纵有疾风起人生不言弃。本文篇幅较长如有错误请不吝赐教感谢支持。 文章目录 一.类和对象的概念①什么是对象②抽象和类1.类的基本概念2.类的声明与定义3.对象的创建与使用 二.类的封装①为什么有封装?②封装和访问权限③尽量把属性设置为私有权限④C中结构体(struct)和类(class)的区别 三.对象的构造函数和析构函数①对象的初始化和清理②构造函数和析构函数1.构造函数和析构函数作用和语法2.构造函数点和析构函数的注意事项 ③构造函数的分类1.构造拷贝函数2.拷贝构造函数中形参要用引用3.拷贝构造函数调用时机 ④构造函数调用规则 四.多个对象的构造函数和析构函数①构造函数的初始化列表1.初始化列表的定义2.作用3.注意事项 ②对象成员的构造函数和析构函数调用顺序 五.对象的深浅拷贝①浅拷贝②深拷贝 六.动态对象创建① C动态分配内存方法②new操作符③delete操作符④用于数组的new和delete1.new创建基础类型数组2.new创建对象数组3.delete void*可能出错,不会调用对象的析构函数4.C和C的申请和释放堆区空间不要混用 七.const修饰类的成员①const修饰成员变量—常成员变量②const修饰成员函数—常成员函数 八.static修饰类的成员①static修饰成员变量-静态成员变量②static修饰成员函数-静态成员函数③const静态成员属性 九.C的面向对象模型初探①成员变量和函数的存储②this指针1.this指针工作原理2.this指针的使用 十.有元①什么是有元1.普通函数作为友元函数2.其他类的成员函数作为友元函数3.友元类③友元的注意事项 十一.单例模式①单例模式简介②单例模式的作用③实现单例模式的思路④案例 ________________________一.类的继承①继承的概念②派生类定义③继承方式1.公有继承​ (public)2.保护继承​(protected)3.私有继承​ (privated) 二、派生类①派生类的构造函数与析构函数1.继承中的构造函数和析构函数的调用顺序 ②继承中同名成员的处理方法③继承中的静态成员特性④查看对象模型 三、多继承①多继承概念②菱形继承③虚继承④虚继承实现原理 四、派生类和基类的特殊关系①类型的兼容性②C中的类型兼容情况③拓展向上和向下强制类型转换 ____________________________________________________一、多态①什么是多态②一个问题 二、问题解决思路①静态联编与动态联编②虚函数 三、多态小结①多态作用②多态发生的三个条件 四、多态(虚函数)的实现原理①示例代码②原理详细分析 五、纯虚函数和抽象类六、虚析构函数纯虚析构函数①一个问题②虚析构函数③纯虚析构函数 一.类和对象的概念 ①什么是对象 生活中有各种各样的事物如人、动物、植物等在C中将这些称为对象。 对象多种多样各种对象的属性也不相同。 例如狗的品种毛色年龄等 各个对象都有自己的行为例如球的弹跳和缩小婴儿的唏哭睡眠走路和眨眼汽车的加速、刹车和转弯同时各个对象可能也有些共同之处至少它们都是现实世界的组成部分。 人们是通过研究对象的属性和观察它们的行为而认识对象的。可以把对象分成很多类每一大类中又可分成若干小类也就是说类是可以分层的。同一类的对象具有许多相同的属性和行为 不同类的对象可能具有相同的属性和类似的行为。例如婴儿和成人、小汽车和卡车等都有共同之处。 总之对象是指一个具有属性和行为的实体它在计算机程序中扮演着重要的角色使得程序更加灵活、可维护和可扩展。 ②抽象和类 类是对对象的抽象生活中的对象充满复杂性处理复杂性的方法之一就是简化和抽象。 1.类的基本概念 在C中用类来描述对象。 类是一种将抽象转换为用户定义类型的C工具它将对象的数据表示和操纵数据的方法组合成一个整洁的包。 1.数据相当于对象的属性)—存储数据的变量 2.操纵数据的方法相当于对象的行为)—函数1.类(class)是自定义数据类型是C语言的结构体进化而成的类中可以包含两类成员――数据成员和成员函数就是对象的两个部分。 2.对象是类实例化出的用类这个自定义数据类型定义一个变量这个变量就是对象。 2.类的声明与定义 C中类的定义包括类的声明和类的实现。 类的声明相当于函数声明其中对类中的数据成员和成员函数进行了说明。 类的实现相当于函数实现其中包括对数据成员进行的操作以及完成一些成员函数的实现。 类定义语法 class 类名 { 权限控制符:成员函数/成员变量 };⚠️类定义具体介绍 关于类定义格式的具体介绍如下。 class是定义类的关键字。类名是类的标识符其命名遵循标识符的命名规范。类名后面的一对大括号用于包含类的成员类的所有成员要在这一对大括号中声明。类中可以定义成员变量也称为属性和成员函数也称为方法成员变量用于描述对象的属性成员函数用于描述对象的行为。声明类的成员时通常需要使用权限控制符限定成员的访问规则权限控制符包括public、protected和private这三种权限控制符的权限依次递减。类的定义和声明本质上也属于一条语句因此在类定义结束的花括号后面带有一个分号( ; )不加分号会引起编译错误。 通常情况下类的成员函数在类中声明在类外实现。在类外实现成员函数必须在返回值之后、函数名之前加上所属的类作用域即“类名::”表示函数属于哪个类。在类外实现成员函数的格式如下所示 示例1 我们抽象出狗这个类。 class Dog//类的关键字是class //用类这个关键字创建一个叫做Dog的自定义数据类型 { public: //公有权限任何地方都可以访问int a;//成员属性成员变量狗的年龄//函数的声明void bark();//声明狗叫成员行为成员函数void eat();//声明狗吃饭的成员函数 }; void Dog::bark()//类外实现 {cout 狗汪汪叫 endl; } void Dog:: eat() {cout 狗吃狗粮 endl; }3.对象的创建与使用 定义了类就相当于定义了一个数据类型。类与int、char等数据类型的使用方法是一样的可以定义变量使用类定义的变量通常称为该类的对象。 对象的定义格式如下所示 类名 对象名;​​下面创建一个表示狗类Dog的对象示例代码如下所示 Dog jinmao;Dog erha;上述代码中创建了类的对象jinmao和erha之后系统就要为对象分配内存空间用于存储对象成员。每个对象都有成员变量和成员函数两部分内容。成员变量标识对象的属性比如创建两个Dog类对象jinmao和erha由于两个的品种、性别、年龄都不同因此在创建对象时应当为每个对象分配独立的内存空间存储成员变量的值。 成员函数描述的是对象的行为每个对象的行为都相同比如对象jinmao和erha都具有叫、吃饭行为。如果为每个对象的成员函数也分配不同的空间则必然造成浪费。因此C用同一块空间存放同类对象的成员函数代码每个对象调用同一段代码。 对象与成员之间的内存分配示意图 对象的成员变量和成员函数的访问和C语言的结构体一致可以通过“.”或指针–访问其格式如下所示 对象名.成员变量(成员函数) p指向要访问的对象-成员变量(成员函数)void test() {Dog jinmao;//m就是对象用Maker这一自定义数据类型创建jinmao.bark();//对象的成员就是类规定的成员jinmao.eat();Dog erha;Dog*p erha;p-bark();p-eat();//其访问方法和C语言的结构体一致。 }二.类的封装 ①为什么有封装? 我们编写程序的目的是为了解决现实中的问题而这些问题的构成都是由各种对象组成我们在计算机中要解决这种问题首先要做就是要将这个问题的参与者对象抽象到计算机程序中也就是用程序语言表示现实的对象。那么现在问题是如何用程序语言来表示对象 比如我们要抽象出人和动物这两个对象在C语言中我们可以这么表示: typedef struct Person {char name[64];//人的姓名int age;//人的年龄//以上都是人这一对象的属性 }Person; void PersonEat(Person* person) {printf(%s在吃人吃的饭!\n,person-name);//以上是人这一对象的行为-吃饭 } typedef struct Aninal {char name[64];//动物名字int age;//动物年龄int type; //动物种类//以上是动物这一对象的属性 }Ainmal; void AnimalEat(Ainmal* animal) {printf(%s在吃动物吃的饭!\n, animal-name);//以上是动物这一对象的行为-吃饭}int main(){Person person;strcpy(person.name, 小明);person.age 30;AnimalEat(person);//行为和属性匹配错误return 0;}「属性和行为不匹配解决方法就是封装。」 定义一个结构体用来表示一个对象所包含的属性函数用来表示一个对象所具有的行为这样我们就表示出来一个事物在C语言中行为和属性是分开的也就是说吃饭这个属性不属于某类对象而属于所有的共同的数据所以不单单是PeopleEat可以调用Person数据AnimalEat也可以调用Person数据那么万一调用错误将会导致问题发生。 从这个案例我们应该可以体会到对象的属性和行为应该放在一起一起表示一个具有属性和行为的对象。 假如某对象的某项属性不想被外界获知比如说漂亮女孩的年龄不想被其他人知道那么年龄这条属性应该作为女孩自己知道的属性或者女孩的某些行为不想让外界知道只需要自己知道就可以。那么这种情况下封装应该再提供一种机制能够给属性和行为的访问权限控制住。 ②封装和访问权限 封装 1.把变量属性和函数行为或操作合成一个整体封装在一个类中 。 2.访问控制现实事物本身有些属性和行为是不对外开放所以要对变量属性和函数行为或操作进行访问控制。 访问权限 1.在类的内部(作用域范围内)没有访问权限之分所有成员可以相互访问 2.在类的外部(作用域范围外)访问权限才有意义publicprivateprotected在类的外部只有public修饰的成员才能被访问在没有涉及继承与派生时private和protected是同等级的外部不允许访问。 在C用类的方法实现实现人这一事物 #include iostream using namespace std; class Person { public://公有权限任何地方都可以访问char name[20];//人的属性-名字void behavior(){cout 我勤奋刻苦长得高大帅气 endl;} private://私有权限外部不可以访问int age;//人的属性-年龄-不想外人知道int password;//密码-不想外人知道 protected://保护权限外部访问不了但在继承中子类可以访问int money;//个人财产 }; //继承 class Son :public Maker {void func(){a 20;//子类的类内可以方法父类的保护权限的成员} }; int main() {Person one;strcpy(one.name, 小明);//公有权限的成员可以在类外修改cout one.name one.name endl;//one.money 保护成员外部无法访问//one.age one.password私有外部成员外部无法访问one.behavior();return 0; }) ③尽量把属性设置为私有权限 1.可以控制属性的读写权限 如果我们让所有的成员变量为public类外任何区域都可以读写它如果把成员变量全部设置成private类外不能直接访问我们只能通过在类外访问类内公有权限的成员函数那么我们就可以实现“不准访问”、“只读访问”、“读写访问”“只写访问”的各类成员函数借助函数实现更精细的优化 2.可赋予客户端访问数据的一致性 如果成员变量不是public客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数客户在访问类成员时只会默认访问函数不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。 3.可以保护属性的合法性 ④C中结构体(struct)和类(class)的区别 类(class)不加权限修饰符默认访问权限为private(私有) struct默认访问权限为public(公有) ☮️ class A {int Age;void func(){coutAgeAgeendl;} }; struct B {int Age;void func(){coutAgeAgeendl;} }; void test() {A a1;a1.Age18;a1.func;B b1;b1.Age25;b1.func;}三.对象的构造函数和析构函数 ①对象的初始化和清理 引入 我们大家在购买一台电脑或者手机或者其他的产品这些产品都有一个初始设置也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久那么电脑和手机会慢慢被我们手动创建很多文件数据某一天我们不用手机或电脑了那么我们应该将电脑或手机中我们增加的数据删除掉保护自己的信息数据。 从这样的过程中我们体会一下所有的事物在起初的时候都应该有个初始状态当这个事物完成其使命时应该及时清除外界作用于上面的一些信息数据。 C也是来源于现实是对现实事物的抽象模拟具体来说当我们创建对象的时候,这个对象应该有一个初始状态当对象销毁之前应该销毁自己创建的一些数据。 对象的初始化和清理也是两个非常重要的安全问题一个对象或者变量没有初始时对其使用后果是未知同样的使用完一个变量没有及时清理也会造成一定的安全问题。 C为了给我们提供这种问题的解决方案构造函数和析构函数这两个函数将会被编译器自动调用完成对象初始化和对象清理工作。 无论你是否喜欢对象的初始化和清理工作是编译器强制我们要做的事情即使你不提供初始化操作和清理操作编译器也会给你增加默认的操作只是这个默认初始化操作不会做任何事所以编写类就应该顺便提供初始化函数。 ☯️为什么初始化操作是自动调用而不是手动调用?既然是必须操作那么自动调用会更好如果靠程序员自觉那么就会存在遗漏初始化的情况出现。 ②构造函数和析构函数 1.构造函数和析构函数作用和语法 ✅什么是构造函数 构造函数是类的特殊成员函数用于初始化对象。构造函数在创建对象时由编译器自动调用。C中的每个类至少要有一个构造函数如果类中没有定义构造函数系统会提供一个默认的无参构造函数默认的无参构造函数体也为空不具有实际的初始化意义。因此在C程序中要显示定义构造函数。 构造函数语法: 构造函数是类的特殊成员函数C编译器严格规定了构造函数的接口形式定义格式如下所示 ⚠️关于构造函数定义有以下几个注意事项 构造函数名必须与类名相同。构造函数名的前面不需要设置返回值类型。构造函数中无返回值不能使用return返回。构造函数的成员权限控制符一般设置为public。 ✅什么是析构函数? 创建对象时系统会为对象分配所需要的内存空间等资源当程序结束或对象被释放时系统为对象分配的资源也需要回收以便可以重新分配给其他对象使用。在C中对象资源的释放通过析构函数完成。析构函数的作用是在对象被释放之前完成一些清理工作。析构函数调用完成之后对象占用的资源也被释放。 析构函数语法: 与构造函数一样析构函数也是类的一个特殊成员函数其定义格式如下所示 ⚠️关于析构函数的定义有以下注意事项 析构函数的名称与类名相同在析构函数名称前添加“”符号。析构函数没有参数。因为没有参数所以析构函数不能重载一个类中只有一个析构函数。析构函数没有返回值不能在析构函数名称前添加任何返回值类型。在析构函数内部也不能通过return返回任何值。 构造函数和析构函数示例 class Maker { public://构造函数的作用是初始化成员变量是编译器去调用的Maker(){a 10;cout 构造函数被调用 endl;}//析构函数在对象销毁前编译器调用析构函数~Maker(){cout 析构函数被调用 endl;} public:int a; }; void test01() {//实例化对象内部做了两件事1.分配空间2.调用构造函数进行初始化Maker m;//如果构造函数被编译器调用a就被初始化为10//那么b的值为10int b m.a;cout b b endl;couttest01函数调用结束endl;//test01函数结束析构函数被调用 }2.构造函数点和析构函数的注意事项 1.构造函数和析构函数的权限必须是公有的私有实例不了对象。 class Maker { private: //私有权限//构造函数的作用是初始化成员变量是编译器去调用的Maker(){cout 构造函数被调用 endl;}//析构函数在对象销毁前编译器调用析构函数~Maker(){cout 析构函数被调用 endl;} }; void test02() {Maker m; }2.构造函数没有返回值不能用void,可以有参数析构函数没有返回值不能用void,没有参数。 3.如果构造函数存在参数当创建对象时需要在对象后面对照构造函数的参数列表添加参数。构造函数是无参数创建对象后面不需要加( )。 class Maker { public://构造函数的作用是初始化成员变量是编译器去调用的Maker(){a 99;cout 无参构造函数被调用 endl;}Maker(int x){a x;cout 有参构造函数被调用 endl;}//析构函数在对象销毁前编译器调用析构函数~Maker(){cout 析构函数被调用 endl;}void Makerprintf(){coutaaendl;} private:int a; }; void test03() {//调用无参构造函数Maker m1;m1.Makerprintf();//调用有参构造函数Maker m2(100);m2.Makerprintf(); }4.构造函数可以重载。 class Maker { public:Maker(){a 100;b 66;cout 无参构造函数1被调用 endl;}Maker(int x){a x;b 66;cout 有参构造函数2被调用 endl;}Maker(int x, int y){a x;b y;cout 有参构造函数3被调用 endl;}void Mprintf(){cout a a b b endl;}~Maker(){cout 析构函数被调用 endl;} private:int a;int b; }; void test() {Maker m1;m1.Mprintf();Maker m2(100);m2.Mprintf();Maker m3(100, 66);m3.Mprintf(); }5.有对象产生必然会调用构造函数有对象销毁必然会调用析构函数。有多少个对象产生就会调用多少次构造函数有多少个对象销毁就会调用多少次析构函数 ③构造函数的分类 参数类型分为无参构造函数和有参构造函数 按类型分类普通构造函数和拷贝构造函数(复制构造函数) 这些分类本质上就是函数重载函数名相同都是用类自定义的数据类型名区别就在参数部分。 1.构造拷贝函数 首先对于普通类型的对象来说它们之间的拷贝是很简单的例如 int a 66; int b a; //用变量a去初始化b//就是a赋值给b这就是普通类型对象的拷贝//拷贝构造函数也是这个功能而类对象与普通对象不同类对象内部结构一般较为复杂存在各种成员变量当类的对象需要拷贝赋值时通常会在类内定义一个函数这个函数对需要拷贝赋值的成员变量进行操作这个函数叫做拷贝构造函数。 拷贝构造函数的定义格式如下所示 举例 用一个已有的对象去初始化另一个对象。 同类型的对象 class Maker { public:Maker(){cout 无参构造函数 endl;a 20;}//拷贝构造函数Maker(const Maker m)//参数这等价于const Makermm1//这是赋值操作也就是类与类之间的拷贝{cout 拷贝构造函数 endl;a m.a; }//打印函数void printMaker(){cout a a endl;} private:int a; }; void test05() {Maker m1;//调用无参构造函数m1.printMaker();//用一个已有的对象去初始化另一个对象Maker m2(m1);//调用拷贝构造函数//也可以写成Maker m2m1;m2.printMaker(); }2.拷贝构造函数中形参要用引用 class Maker3 { public:Maker3(int Ma){cout 有参构造函数 endl;ma Ma;}Maker3(const Maker3 m)//形参我们不加引用看看会产生什么后果{cout 拷贝构造函数 endl;} private:int ma; }; void test03() {Maker3 m1(10);//调用有参构造Maker3 m2(m1);//调用拷贝构造函数 }解释 如果拷贝构造函数中的形参不是引用时 Maker3(const Maker3 m){cout 拷贝构造函数 endl;}Maker3 m1(10);//调用有参构造Maker3 m2(m1);//调用拷贝构造函数//函数接受实参时1.const Maker mm1;//这是赋值操作也就是类与类之间的拷贝//编译器会再次调用拷贝构造函数2.const Maker3 m(m1);//紧接着又进行传参又变为3.const Maker3 mm1;//编译器将这句话变为4.const Maker3 m(m1);5.进入死循环3.拷贝构造函数调用时机 ①对象以值方式给函数参数 举例: 当对象作为实参传给形参时形参在接受时就要使用拷贝构造函数去初始化对象 class Maker { public:Maker(){cout 无参构造函数 endl;}Maker(int a){cout 有参构造函数 endl;}Maker(const Maker maker){cout 拷贝构造函数 endl;}~Maker(){cout 析构函数 endl;} }; //1.对象以值方式给函数参数 void func(Maker m)//Maker mm1;等价于Maker m(m1)调用拷贝构造函数 {}void test04() {Maker m1;func(m1); }2.用已有的对象去初始化另一个对象 void test05() {Maker m1;Maker m2(m1);//用m1初始化m2 }3.函数的局部对象以值的方式从函数返回vs Debug(调试)模式下会调用拷贝构造vs Release发行模式下不会调用拷贝构造qt也不调用。 Maker func2() {//局部对象Maker m;//调用无参构造函数cout 局部对象的地址: m endl;return m; } void test() {Maker m1 func2();//先调用func2函数然后func2函数返了一个局部对象m//所以等价于Maker m1 m;然后调用拷贝构造函数。cout m1对象的地址 m1 endl; }debug版本:函数的局部对象以值的方式从函数返回vs Debug(调试)模式下会调用拷贝构造 release版本:vs Release发行模式下不会调用拷贝构造qt也不调用。 ④构造函数调用规则 ❤️默认情况下C编译器至少为我们写的类增加3个函数如果我们在定义类的时候没有写构造函数和析构函数编译器不会报错默认情况下C编译器至少为我们写的类增加3个函数分别是 1.默认构造函数(无参函数体为空) 2.默认析构函数(无参函数体为空) class Maker { public://编译器提供的默认构造函数Maker(){}//编译器提供的默认的析构函数~Maker(){} };3.默认拷贝构造函数对类中非静态成员属性简单值拷贝 class Maker2 { public:Maker2(){cout 无参构造函数 endl;a 20;}//编译器提供了默认的拷贝构造函数//Maker2(const Maker2 m)//{// //默认拷贝构造函数进行了成员变量的简单拷贝// a m.a;//}//打印函数void printMaker(){cout a a endl;} private:int a; }; void test() {Maker2 m1;//无参构造函数m1.printMaker();Maker2 m2(m1);//编译器调用默认拷贝构造函数m2.printMaker(); }❤️如果用户定义拷贝构造函数c不会再提供任何默认构造函数 ❤️如果用户定义了普通构造(非拷贝)c不在提供默认无参构造但是会提供默认拷贝构造函数。 四.多个对象的构造函数和析构函数 ①构造函数的初始化列表 1.初始化列表的定义 与其他函数不同构造函数除了有名字参数列表和函数体之外还可以有初始化列表初始化列表以冒号开头后跟一系列以逗号分隔的初始化字段。 2.作用 我们之前讲的构造函数是对创建的对象进行初始化操作而初始化列表是对创建的对象的成员进行初始化操作。 初始化对象的成员有两种方式一是使用初始化列表二是在构造函数体内进行赋值操作。 在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象叫做对象成员。 C中对对象的初始化是非常重要的操作当创建一个对象的时候C编译器必须确保调用了所有子对象的构造函数。如果所有的成员对象有默认无参构造函数编译器可以自动调用他们。但是如果成员对象定义了有参构造函数那么编译器就不会再提供默认构造函数或者想指定调用某个构造函数怎么办那么是否可以在类的构造函数直接调用成员对象类的成员变量完成初始化呢但是如果成员对象类的成员变量的访问权限是私有的我们是没有办法访问并完成初始化的。 解决办法非常简单对于子类调用构造函数C为此提供了专门的语法即构造函数初始化列表。 3.注意事项 1.初始化列表是干什么用的初始化对象的成员。 2.初始化列表只能写在构造函数后面。 3.如果使用了初始化列表那么所有的构造函数都要写初始化列表。 4.如果有多个对象的成员需要初始化用逗号隔开。 5.可以使用初始化列表传递数值给对象的成员。 举例 class Maker1 { public:Maker1(int a){cout Maker1有参构造函数 a endl;}~Maker1(){cout Maker1析构函数 endl;}int a; }; class Maker2 { public:Maker2(int b, int c){cout Maker2构造函数 b b c c endl;}~Maker2(){cout Maker2析构函数 endl;} private:int b;int c; }; class Maker3 { public://初始化列表//注意1初始化列表只能写在构造函数后面并加个://如果有多个成员需要初始化用逗号隔开//使用初始化列表给对象成员传参Maker3(int a, int b, int c, int d) :m1(a), m2(b, c), d(d)//m1m2d都是Maker3的成员abcd的值依次和后面abcd一一对应并分别赋值给m1m2和d//在初始化Maker3创建的对象要传4个参数{cout Maker3有参构造函数 endl;}//注意2:如果使用了初始化列表那么所有的构造函数都要写初始化列表Maker3(const Maker3 m2) :m1(40), m2(10, 20), d(100)//拷贝构造函数{//}~Maker3(){cout Maker3析构函数 endl;} private:Maker1 m1;//成员对象Maker2 m2;//成员对象int d;//成员变量 }; //初始化列表是调用成员对象的指定构造函数 void test() {Maker3 m3(30, 10, 20, 100);//调用Maker3的有参构造函数//100初始化给m3中的d30调用m1的有参构造函数并赋值给a//1020调用m2的有参构造函数并赋值给bc }②对象成员的构造函数和析构函数调用顺序 1.如果类有成员对象那么先调用成员对象的构造函数再调用本身的构造函数析构函数的调用顺序反之。 2.成员对象的构造函数调用和定义顺序一样3.注意如果有成员对象那么实例化对象时必须保证成员对象的构造和析构能被调用。 举例: class Maker1 { public:Maker1(){cout Maker1无参构造函数 endl;}~Maker1(){cout Maker1析构函数 endl;} }; class Maker2 { public:Maker2(){cout Maker2无参构造函数 endl;}~Maker2(){cout Maker2析构函数 endl;} }; class Maker3 { public:Maker3(){cout Maker3无参构造函数 endl;}~Maker3(){cout Maker3析构函数 endl;} private: Maker1 m1;//成员对象Maker2 m2;//成员对象 }; void test() {Maker3 m3; }五.对象的深浅拷贝 ①浅拷贝 同一类型的对象之间可以赋值使得两个对象的成员变量的值相同两个对象仍然是独立的两个对象这种情况被称为浅拷贝. 一般情况下浅拷贝没有任何副作用但是当类中有指针并且指针指向动态分配的内存空间析构函数做了动态内存释放的处理会导致内存问题。 注编译器提供的默认构造函数就是浅拷贝 例如: class Person { public: //提供了有参构造函数根据规则编译器会提供默认拷贝构造函数默认拷贝构造函数就是对类内成员的简单赋值Person(const char *name, int age){pName (char*)malloc(strlen(name) 1);strcpy(pName, name);Age age;}~Person(){cout 析构函数 endl;if (pName ! NULL){free(pName);pName NULL;}} public:char *pName;int Age; }; void test() {//调用有参构造函数Person p1(Edward,30);//调用编译器提供的默认构造函数Person p2(p1);cout p1 Name p1.pName p1 age p1.Age endl;cout p2 Name p2.pName p2 age p2.Age endl; }问题pName指针指向的是在堆区开辟的空间进行默认拷贝后s1和s2的pName相同那么当s1和s2的析构函数都被调用时pName被重复释放 ②深拷贝 当类中有指针并且此指针有动态分配空间析构函数做了释放处理往往需要自定义拷贝构造函数自行给指针动态分配空间深拷贝。 这样我们可以借助深拷贝解决浅拷贝的问题。 class Person { public:Person(char* name,int age){pName (char*)malloc(strlen(name) 1); strcpy(pName,name);Age age;}//增加拷贝构造函数Person(const Person person){pName (char*)malloc(strlen(person.pName) 1);strcpy(pName, person.pName);Age person.Age;}~Person(){if (pName ! NULL){ free(pName);} } public: char* pName; int Age; }; void test() {//调用有参构造函数Person p1(Edward,30);//调用拷贝构造函数Person p2(p1);cout p1 Name p1.pName p1 age p1.Age endl;cout p2 Name p2.pName p2 age p2.Age endl; }六.动态对象创建 ① C动态分配内存方法 当我们创建数组的时候总是需要提前预定数组的长度然后编译器分配预定长度的数组空间在使用数组时会有这样的问题数组也许空间太大了浪费空间也许空间不足所以对于数组来讲如果能根据需要来分配空间大小再好不过。所以动态的意思意味着不确定性。 c语言为了解决这个问题提出了一系列动态内存函数。 例如malloc以及它的变种calloc和realloc,释放内存的free。 然而这些函数在c中不能很好的运行因为它不能帮我们完成对象的初始化工作。 举例用c语言的思想创建动态类 class Maker { public:Maker(){cout Maker构造函数被调用 endl;}Maker(int a){cout Maker有参函数被调用 endl;}~Maker(){cout Maker析构函数被调用 endl;} }; void test() {//用C语言的方式申请堆区空间不会调用构造函数Maker *m (Maker*)malloc(sizeof(Maker));if(m NULL)//判断内存是否申请成功{return 0;}//对象释放时也不会调用析构函数。free(m); }还有一些其他问题 ⚛️程序员必须确定对象的长度sizeof(类名) ⚛️malloc返回一个void指针c不允许将void赋值给其他任何指针必须强转。 ⚛️malloc可能申请内存失败所以必须判断返回值来确保内存分配成功。 ⚛️用C语言的方式申请堆区空间不会调用构造函数对象释放时也不会调用析构函数我们需要手动进行初始化。 C的动态内存分配函数太复杂容易令人混淆是不可接受的C中我们推荐使用运算符new 和 delete. ②new操作符 C中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时它就在堆里为对象分配内存并调用构造函数完成初始化。 new返回的是指针是你申请内存类型的指针。 Maker *mnew Maker;//相当于Maker *m (Maker*)malloc(sizeof(Maker));if(m NULL){return 0;}free(m);new操作符能确定在调用构造函数初始化之前内存分配是成功的所有不用显式确定调用是否成功。 现在我们发现在堆里创建对象的过程变得简单了只需要一个简单的表达式它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。 ③delete操作符 new表达式的反面是delete表达式。delete表达式先调用析构函数然后释放内存。正如new表达式返回一个指向对象的指针一样delete需要一个对象的地址。 delete只适用于由new创建的对象。如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。 因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。 如果正在删除的对象的指针是NULL,将不发生任何事因此建议在删除指针后立即把指针赋值为NULL以免对它删除两次对一些对象删除两次可能会产生某些问题。 class Maker { public:Maker(){cout Maker构造函数被调用 endl;}Maker(int a){cout Maker有参函数被调用 endl;}~Maker(){cout Maker析构函数被调用 endl;} }; void test() {//用new方式申请堆区空间会调用类的构造函数Maker *m1new Maker;//调用有参构造函数Maker *m2new Maker(1000);//释放堆区空间会调用类的析构函数delete m1;delete m2;m1NULL;m2NULL; }④用于数组的new和delete 1.new创建基础类型数组 void test() {//申请基础数据类型的数组int *pInt new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};//不推荐这样初始化数组for (int i 0; i 10; i){pInt[i] i 1;}for (int i 0; i 10; i){cout pInt[i] ;}cout endl;//字符数组char *pChar new char[64];memset(pChar, 0, 64);strcpy(pChar, 小话);cout pChar endl;//其他数据类型都是这样申请//注意如果new时有中括号那么delete时也要有中括号delete[] pInt;delete[] pChar; }2.new创建对象数组 class Maker { public:Maker(){cout 构造函数 endl;}Maker(int a){cout 有参构造函数 endl;}~Maker(){cout 析构函数 endl;} }; void test() {Maker *ms new Maker[2];//调用无参构造delete[] ms;//大部分编译器不支持这种写法(聚合初始化//Maker *ms2 new Maker[2]{Maker(10), Maker(20)}; }3.delete void*可能出错,不会调用对象的析构函数 void test() {void *m new Maker;//如果用void*来接new的对象那么delete时不会调用析构函数delete m;//在编译阶段那么编译器就确定好了函数的调用地址//C编译器不认识void*,不知道void*指向那个函数所以不会调用析构函数//这种编译方式叫静态联编 }4.C和C的申请和释放堆区空间不要混用 malloc、free和new、delete可以混搭使用吗也就是说malloc分配的内存可以调用delete吗通过new创建的对象可以调用free来释放吗 void test() {Maker *mnew Maker;free(m); }不会调用析构函数。 七.const修饰类的成员 前面学习的类中成员变量都是我们比较熟悉的简单类型比如int、double等但很多时候为描述比较复杂的情况例如只允许类的成员函数读取成员变量的值但不允许在成员函数内部修改成员变量的值此时就需要使用const关键字修饰成员函数。同样在程序设计中有些数据也不希望被改变只允许读取。对于不希望被改变的数据也可以使用const关键字修饰。在类中const既可以修饰类的成员变量也可以修饰类的成员函数。下面对这两种情况分别进行讲解。 ①const修饰成员变量—常成员变量 使用const修饰的成员变量称为常成员变量。对于常成员变量仅仅可以读取第一次初始化的数据之后是不能修改的。常成员变量通常使用有参构造函数进行初始化。 例如 class Student { public:const string Name;//成员变量声明const int Age;Student(string name,int age);//构造函数声明~Student();//析构函数声明 }; Student::Student(string name,int age):Name(name),Age(age)//类外实现 {cout初始化const修饰的成员变量endl; ​ coutname:Nameendl; ​ coutage:Ageendl; } Student::~Student(){} void test() {Student s1(强风吹拂king,18);s1.age20;//err//在创建对象时完成了三个常成员变量的初始化。//这是创建对象后初始化常成员变量的唯一机会//常成员变量一旦初始化就不能再改变。 }②const修饰成员函数—常成员函数 使用const修饰的成员函数称为常成员函数。与修饰成员变量不同的是修饰成员函数时const位于成员函数的后面其格式如下 返回值类型 函数名()const;在常成员函数内部只能访问类的成员变量而不能修改类的成员变量。常成员函数只能调用类的常成员函数而不能调用类的非常成员函数。常对象const修饰的对象可以调用常函数但不能调用普通成员函数普通对象可以调用常函数也可以调用普通函数但类中定义的成员函数若与常成员函数名相同则构成重载则该常成员函数只能由常对象const修饰的对象进行访问。mutable修饰的成员变量在常函数中可以被修改 案例 class Student { public:string Add;//学生家庭住址Student(string add,string name,int age,string id);//构造函数声明void ShowInform();//普通成员函数展示学生信息void ShowInform()const;//const修饰成员函数与普通成员函数构成重载void ShowAdd()const;//const修饰的成员函数但不构成重载~Student();//析构函数声明 private:const string Name;//学生姓名const int Age;//学生年龄mutable string ID;//学生学号 }; Student::Student(string add,string name,int age,string id):Add(add),Name(name),Age(age),ID(id) {cout 成员变量已全部初始化完毕构造函数执行完成 endl; } void Student::ShowInform()//普通成员函数展示学生信息 {cout 调用被重载普通成员函数ShowInform展示学生信息 endl;cout 姓名 Name endl;cout 学号 ID endl;cout 年龄 Age endl;cout 家庭住址 Add endl; } void Student::ShowInform()const {cout 调用被重载const修饰成员函数ShowInform展示学生信息 endl;//Age20;错误//在常成员函数内部只能访问类的成员变量而不能修改类的成员变量。cout 姓名 Name endl;ID2203120220;//mutable修饰的成员变量在常函数中可以被修改cout 学号 ID endl;cout 年龄 Age endl;ShowAdd();//ShowInform()错误//常成员函数只能调用类的常成员函数而不能调用类的非常成员函数。 } void Student::ShowAdd()const//未被重载的常成员函数 {cout 调用const修饰成员函数ShowAdd展示学生家庭住址 endl;cout 家庭住址 Add endl; } Student::~Student(){}; void test() {Student s1(山东省,强风吹拂king,19,2203120201);//普通对象可以调用常函数也可以调用普通函数s1.ShowInform();//调用被重载的普通函数调用不了被重载的常函数s1.ShowAdd();//调用常函数//但类中定义的成员函数若与常成员函数名相同则构成重载//则该重载常成员函数只能由常对象const修饰的对象进行访问。const Student s2(山东省,我的好基友,19,2203120000);//常对象s2.ShowInform();//调用的是重载的常函数常对象调用不了普通函数 }八.static修饰类的成员 类中的成员变量在某些时候被多个类的对象共享实现对象行为的协调作用。共享数据通过static实现用static修饰成员后创建的对象都共享一个静态成员。 ①static修饰成员变量-静态成员变量 在一个类中若将一个成员变量声明为static这种成员称为静态成员变量。 静态变量是在编译阶段就分配空间对象还没有创建时就已经分配空间。 注意事项 ♋️静态成员变量必须在类中声明在类外定义。 ♋️静态成员变量不属于某个对象属于类在为对象分配空间中不包括静态成员所占空间也就是说我创建100对象也只创建一份静态成员变量归根结底是为了节省空间。 ♋️静态成员变量是所有对象共享都可以进行访问。 ♋️静态成员变量可以用对象访问也可以用类访问即在对象没有创建前也可通过类名调用 举例 class Maker { public:Maker()//构造函数{} public://1.静态成员变量的生命周期是整个程序作用域在类内static int a;static int b; }; //2.静态成员变量要在类内声明类外初始化初始化不加static int Maker::a 100; int Maker::b 101; void test01() {//3.静态成员变量属于类不属于对象是所有对象共享Maker m1;Maker m2;//m1和m2对象都可以进行访问cout m1.a m2.b endl;cout m2.a m2.bendl;//4.静态成员变量可以用类访问也可以用对象访问//即使没有创建对象m1m2也可以通过类名访问。cout Maker::a Maker::bendl; }②static修饰成员函数-静态成员函数 类定义中前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样同样在对象没有创建前即可通过类名调用。 静态成员函数主要为了访问静态变量但是不能访问普通成员变量。 静态成员函数的意义不在于信息共享数据沟通而在于管理静态成员变量完成对静态成员变量的封装。 注意事项 ⚛️1.静态成员函数只能访问静态成员变量不能访问普通成员变量。 ⚛️2.静态成员也有权限如果为私有类外也不可以访问。 ⚛️3.静态成员函数可以用对象访问也可以用类访问即在对象没有创建前也可通过类名调用 ⚛️4.普通成员函数可访问静态成员变量、也可以访问非静态成员变量 举例 class Maker { public: //普通成员函数可访问静态成员变量、也可以访问非静态成员变量void func1(int a) {xa;ya;cout普通成员变量xxendl;cout静态成员变量yyendl;}//静态成员函数只能访问静态成员变量不能访问普通成员变量。static void func2(int a){//xa;错误改不了普通成员变量。ya;cout静态成员变量yyendl;} private: //静态成员也有权限如果为私有类外也不可以访问。static void func3(int a){//xa;//错误ya;}int x;static int y; }; //类外初始化静态成员变量 int Maker::y100; void test03() {//静态成员函数可以用对象访问也可以用类访问即在对象没有创建前也可通过类名调用Maker::func2(200);Maker m1;m1.func1(300);//m1.fun3错误func3为私有类外不能访问。 }③const静态成员属性 如果一个类的成员既要实现共享又要实现不可改变那就用 static和const 修饰。 定义静态const数据成员时最好在类内部初始化。 //const修饰的静态成员变量最好在类内初始化 class Maker4 { public:const static int a 20;const static int b; }; //类外也可以初始化但尽量类内初始化。 const int Maker4::b 30;九.C的面向对象模型初探 ①成员变量和函数的存储 1.空类的大小是1不是0。 class Maker {}; void test() {coutsizeof(Maker)endl;//为什么是1不是0Maker *m1new Maker[100];//便于编译器处理如果是0怎么创建数组 }2.类的成员函数不占用类的大小静态成员变量不占用类的大小静态成员函数不占用类的大小 class Maker { public:void func1(){}static int a;static void func2(){} }; int Maker::a 10; void test() {cout sizeof(Maker) endl; }3.普通成员变量占用类的大小 class Maker { public:int b; }; void test() {coutsizeof(Maker)endl; }②this指针 1.this指针工作原理 通过对象模型的②和③我们可以知道C的数据和操作也是分开存储也就是成员变量和成员函数分开存储并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例也就是说多个同类型的对象会共用一块函数代码。 那么问题是这一块公用代码是如何区分是哪个对象调用自己的呢 C通过提供特殊的对象指针this指针解决上述问题。This指针指向被调用的成员函数所属的对象。 C规定this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后它的每一个成员函数都含有一个系统自动生成的隐含指针this用以保存这个对象的地址也就是说虽然我们没有写上this指针编译器在编译的时候也是会加上的。因此this也称为“指向本对象的指针”this指针并不是对象的一部分不会影响sizeof(对象)的结果。   this指针是C实现封装的一种机制它将对象和该对象调用的成员函数连接在一起在外部看来每一个对象都拥有自己的函数成员。一般情况下并不写this而是让系统进行默认设置。 成员函数通过this指针即可知道操作的是那个对象的数据。 This指针是一种隐含指针它隐含于每个类的非静态成员函数中。This指针无需定义直接使用即可。 总结: 1.每个对象都有一个隐藏的this指针但不属于对象是编译器添加的 2.编译器会把this指针传入成员函数内 3.this指针指向对象的存储空间 注意静态成员函数内部没有this指针所以静态成员函数不能操作非静态成员变量。 c编译器对普通成员函数的内部处理 2.this指针的使用 当形参和成员变量同名时可用this指针来区分 在类的非静态成员函数中返回对象本身可使用return *this. class Maker2 { public://1.当形参名和成员变量名相同时用this指针区分Maker2(int id){//idid;errthis-id id;}//2.返回对象的本身Maker2 getMaker2(){return *this;//运算符重载时有用} public:int id; };十.有元 ①什么是有元 类的主要特点之一是数据隐藏即类的私有成员无法在类的外部(作用域之外)访问。 但是有时候需要在类的外部访问类的私有成员怎么办解决方法是使用友元函数友元函数是一种特权函数C允许这个特权函数访问私有成员。 这一点从现实生活中也可以很好的理解比如你的家有客厅有你的卧室那么你的客厅是Public的所有来的客人都可以进去但是你的卧室是私有的也就是说只有你能进去但是呢你也可以允许你的闺蜜好基友进去。 程序员可以把一个普通函数、某个类中的成员函数、甚至整个类声明为友元。 1.普通函数作为友元函数 将普通函数作为类的友元函数在类中使用friend关键字声明该普通函数就可以实现友元函数可以在类中任意位置声明。普通函数作为类的友元函数的声明格式如下所示 2.其他类的成员函数作为友元函数 其他类中的成员函数作为本类的友元函数时需要在本类中表明该函数的作用域并添加友元函数所在类的前向声明其语法格式如下 class Building;//添加友元函数所在类的前向声明 class MyFriend { public://友元成员函数声明void LookAtBedRoom(Building building);void PlayInBedRoom(Building building); }; class Building {//声明普通函数做友元函数friend void CleanBedRoom(Building building);//声明另一个类的成员函数做友元函数friend void MyFriend::LookAtBedRoom(Building building);friend void MyFriend::PlayInBedRoom(Building building); public:Building(); public:string mSittingRoom; private:string mBedroom; }; //友元成员函数类外实现 void MyFriend::LookAtBedRoom(Building building) {cout 另一个类的成员函数我的朋友参观 building.mBedroom endl; } void MyFriend::PlayInBedRoom(Building building) {cout 另一个类的成员函数我的朋友玩耍在 building.mBedroom endl; } //友元普通函数实现 void CleanBedRoom(Building building) {cout 友元普通函数我的家人在 building.mBedroom endl; } Building::Building() {this-mSittingRoom 客厅;this-mBedroom 卧室; } void test() {Building building;MyFriend myfriend;CleanBedRoom(building);myfriend.LookAtBedRoom(building);myfriend.PlayInBedRoom(building); }3.友元类 除了可以声明函数为类的友元函数还可以将一个类声明为友元类友元类可以声明在类中任意位置。声明友元类之后友元类中的所有成员函数都是该类的友元函数能够访问该类的所有成员。 与声明友元函数类似友元类也是使用关键字friend声明其语法格式如下 ①通过给友元类传入参数来访问类的私有变量 class Building {//声明MyFriend类成为Building类的友元类friend class MyFriend; public://构造函数初始化Building(){livingroom 客厅;bedroom 卧室;} public:string livingroom; private:string bedroom; }; class MyFriend//友元类 { public:void GoodGay(Building bd){cout 好基友访问我的: bd.livingroom endl;cout 好基友访问我的: bd.bedroom endl;} }; void test() {Building myhouse;MyFriend myfriend;myfriend.GoodGay(myhouse); }②通过类内指针来访问类的私有变量 class Building {//声明MyFriend类成为Building类的友元类friend class MyFriend; public://构造函数初始化Building(){livingroom 客厅;bedroom 卧室;} public:string livingroom; private:string bedroom; }; class MyFriend { public:MyFriend(){cout 无参构造函数 endl;pbu new Building;}void GoodGay(){cout 访问: pbu-livingroom endl;cout 访问: pbu-bedroom endl;//通过成员指针访问私有成员 }//拷贝构造函数 防止对象拷贝时动态内存地址被拷贝//那样析构函数调用两次动态内存被重复释放。MyFriend(const MyFriend f2){cout 拷贝构造函数 endl;//1.申请空间 pbu new Building;}~MyFriend(){cout 析构函数 endl;if (pbu ! NULL){cout pbu销毁 endl;delete pbu;}} public:Building *pbu; }; void test() {MyFriend myfriend;myfriend.GoodGay();MyFriend friend2 myfriend; }③友元的注意事项 ♋️友元关系不能被继承你爹的朋友不是你的朋友。 ♋️友元关系是单向的类A是类B的朋友但类B不一定是类A的朋友。 ♋️友元关系不具有传递性。类B是类A的朋友类C是类B的朋友但类C不一定是类A的朋友。 十一.单例模式 ①单例模式简介 单例模式是设计模式中最简单的形式之一单例模式目的让一个类只能实例化出一个对象。 ②单例模式的作用 对于系统中的某些类来说只有一个实例很重要例如一个系统中可以存在多个打印任务但是只能有一个正在工作的任务一个系统只能有一个窗口管理器或文件系统一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化将弹出多个窗口如果这些窗口显示的内容完全一致则是重复对象浪费内存资源如果这些窗口显示的内容不一致则意味着在某一瞬间系统有多个状态与实际不符也会给用户带来误解不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。 ③实现单例模式的思路 要实现单例模式需要用一种只允许生成对象类的唯一实例的机制“阻止”所有能生成多个对象的方法。 ​1.把无参构造函数和拷贝构造函数私有化 ​2.定义一个类内的静态成员指针 ​3.在类外初始化时new一个对象 ​4.把指针的权限设置为私有然后提供一个静态成员函数让外面获取这个指针 //单例模式是一个类只能实例化一个对象 class Maker {//1.把构造函数私有化 private:Maker(){}Maker(const Maker m){} public://静态成员函数访问静态成员变量//4.把指针的权限设置为私有然后提供一个静态成员函数让外面获取这个指针static Maker* getMaker(){return pMaker;} private://2.定义一个类内的静态成员指针static Maker *pMaker; }; //3.在类外初始化时new一个对象 Maker* Maker::pMaker new Maker;//这里可以new是因为在Maker::作用域编译器把它当成在类内 void test() {//类外获取这个对象指针Maker* m Maker::getMaker();Maker* m2 Maker::getMaker();cout m m endl;cout m2 m2 endl;//Maker m3 *m;//调用拷贝构造这里会产生新对象所以要把拷贝构造也私有化//cout m3 m3 endl; }④案例 单例模式案例需求获取打印机打印次数 思路创建一个打印机类用这个类创建一个打印机并会记录使用次数只要使用打印机都是使用的这一台。 //需求获取打印机使用的次数 class Printer { private://1.把无参构造和拷贝构造私有化Printer(){mcount 0;}Printer(const Printer p){} public:static Printer *getPrinter(){return p;}void printPrinter(string name){cout name :打印 endl;mcount;}int getCount(){return mcount;} private:int mcount;//记录打印打印的次数//2.定义静态成员指针static Printer *p; }; //3.类外进行初始化new对象 Printer *Printer::p new Printer; void test() {//销售部Printer *p1 Printer::getPrinter();//p指针p1-printPrinter(销售部);//技术部Printer *p2 Printer::getPrinter();//p指针p2-printPrinter(技术部);//公关部Printer *p3 Printer::getPrinter();//p指针p3-printPrinter(公关部);Printer *p4 Printer::getPrinter();//p指针cout 打印使用的次数:p4-getCount() endl;}________________________ 一.类的继承 ①继承的概念 C最重要的特征是代码重用通过继承机制可以利用已有的数据类型来定义新的数据类型新的类不仅拥有旧类的成员还拥有新定义的成员。一个B类继承于A类或称从类A派生类B。 这样的话类A成为基类父类 类B成为派生类子类。 派生类中的成员包含两大部分 一类是从基类继承过来的一类是自己增加的成员。从基类继承过过来的表现其共性而新增的成员体现了其个性。 ②派生类定义 在类的继承中有以下几点需要注意 ①基类的构造函数与析构函数不能被继承。②派生类对基类成员的继承没有选择权不能选择继承或不继承某些成员。③派生类中可以增加新的成员用于实现新功能保证派生类的功能在基类基础上有所扩展。④一个基类可以派生出多个派生类一个派生类也可以继承自多个基类。 ③继承方式 在继承中派生类会继承基类除构造函数、析构函数之外的全部成员。从基类继承的成员其访问属性除了成员自身的访问属性还受继承方式的影响。类的继承方式主要有三种public公有继承、protected保护继承和private私有继承。不同的继承方式会影响基类成员在派生类中的访问权限。 1.公有继承​ (public) 采用公有继承方式时基类的公有成员和保护成员在派生类中仍然是公有成员和保护成员其访问属性不变可以使用派生类的对象访问基类公有成员。但是基类的私有成员在派生类中变成了不可访问成员。如果基类中有从上层基类继承过来的不可访问成员则基类的不可访问成员在它的派生类中同样是不可访问的。 公有继承对派生类继承成员的访问控制权限影响图 ✅注意 不可访问成员是指无论在类内还是在类外均不可访问的成员。它与私有成员的区别是私有成员在类外不可访问只能通过类的成员进行访问。不可访问成员完全是由类的派生形成的。对于顶层类不存在不可访问成员但是通过继承基类的私有成员在派生类内就成为不可访问成员。 2.保护继承​(protected) 采用保护继承方式时基类的公有成员和保护成员在派生类中全部变成保护成员派生类的其他成员可以直接访问它们在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。 保护继承对派生类继承成员的访问控制权限影响图 3.私有继承​ (privated) 采用私有继承方式时基类的公有成员和保护成员在派生类中全部变成私有成员派生类的其他成员可以直接访问它们在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。 私有继承对派生类继承成员的访问控制权限影响图 二、派生类 ①派生类的构造函数与析构函数 派生类的成员变量包括从基类继承的成员变量和新增的成员变量因此派生类的构造函数除了要初始化派生类中新增的成员变量还要初始化基类的成员变量即派生类的构造函数要负责调用基类的构造函数。派生类的构造函数定义格式如下所示 由上述格式可知在定义派生类构造函数时通过“”运算符在后面完成基类构造函数的调用。基类构造函数的参数从派生类构造函数的参数列表中获取。 1.继承中的构造函数和析构函数的调用顺序 创建派生类对象时程序首先创建基类对象。从概念上说这意味着基类对象应当在程序进入派生类构造函数之前被创建。 继承中的构造函数和析构函数的调用顺序 ✅派生类对象在创建时会首先调用基类的构造函数基类构造函数执行完毕后才会调用派生类类的构造函数。 ✅当基类构造函数有参数时需要在派生类初始化列表(参数列表)中显示调用基类构造函数。 ✅析构函数调用顺序和构造函数相反。 当派生类含有成员对象时派生类构造函数除了负责基类成员变量的初始化和本类新增成员变量的初始化还要负责成员对象的初始化其定义格式如下所示 此时的调用顺序为 先调用父类的构造然后调用成员对象的构造最后调用本身的构造析构函数调用顺序反之。 继承中的构造函数和析构函数的调用顺序案例 class Base//基类 { public:Base(string name,int age);//基类构造函数声明~Base();//基类析构函数声明 private:string Name;//成员变量声明int Age; }; Base::Base(string name,int age) {cout基类构造函数被调用endl;Namename;Ageage;cout基类Name:Name基类Age:Ageendl; } Base::~Base() {cout基类析构函数被调用endl; } //成员对象类 class Maker { public://构造函数和析构函数声明Maker(int x);~Maker(); private:int x; }; Maker::Maker(int m) {cout成员对象类构造函数被调用endl;xm; } Maker ::~Maker() {cout成员对象类析构函数被调用endl; } class Derived:public Base { public:Derived(string name,int age,int x,string add,string tele);//派生类构造函数声明~Derived(); private:Maker m;string Add;string Tele; }; //类外实现构造函数后面使用“:”运算符调用成员对象构造函数、基类构造函数 Derived::Derived(string name,int age,int x,string add,string tele):Base(name,age),m(x) {cout派生类构造函数被调用endl;Addadd;Teletele;cout派生类Add: Add 派生类Tele:Teleendl; } Derived::~Derived() {cout派生类析构函数被调用endl; } void test() {Derived d(强风吹拂king,19,666,山东省,15606333333); }②继承中同名成员的处理方法 在派生类中重新定义基类同名函数基类同名函数在派生类中被隐藏通过派生类对象调用同名函数时调用的是改写后的派生类成员函数基类同名函数不会被调用如果想通过派生类对象调用基类的同名函数需要使用作用域限定符“::”指定要调用的函数或者根据类型兼容规则通过基类指针调用同名成员函数。 ⚠️注意 需要注意的是只要是同名函数无论参数列表和返回值类型是否相同基类同名函数都会被隐藏。若基类中有多个重载函数派生类中有同名函数则基类中所有同名函数在派生类中都会被隐藏。 ③继承中的静态成员特性 1.静态成员可以被继承。 2.继承中的静态成员变量一样会被同名的子类成员变量隐藏。 3.继承中的静态成员函数中当子类有和父类同名静态函数时父类的所有同名重载静态函数都会被隐藏。 4.改变从基类继承过来的静态函数的某个特征返回值或者参数个数将会隐藏基类重载的函数。 5.静态成员函数不能是虚函数。 6.从父类继承过来的静态成员变量是父类的静态成员变量。 class Father { public:static int sNum; static void func(){cout Father static func endl;}static void func(int a){cout Father static func(int a) endl;}static void func(int a,int b){cout Father static func(int a,int b) endl;}}; int Father::sNum 10; class Son : public Father { public://定义一个与父类重名的静态成员变量//基类静态成员属性将被隐藏static int sNum; //定义一个与父类重名的函数//父类所以同名函数全部被隐藏static void func(){cout Son static func endl;}//改变从基类继承过来的静态函数的某个特征返回值或者参数个数将会隐藏基类重载的函数。 }; int Son::sNum 20; void test() {Son s;couts.sNums.sNumendl;//当子类和父类有同名静态成员时子类的静态同名成员会隐藏父类的同名成员couts.Father::sNums.Father::sNumendl;s.func(); //同名覆盖访问的是子类的func//想要访问父类需要加作用域s.Father::func();s.Father::func(20,30); }④查看对象模型 在C编译器的内部可以理解为结构体子类是由父类成员叠加子类新成员而成 class Aclass { public:int mA;int mB; }; class Bclass : public Aclass { public:int mC; }; class Cclass : public Bclass { public:int mad; }; void test() {cout A size: sizeof(Aclass) endl;cout B size: sizeof(Bclass) endl;cout C size: sizeof(Cclass) endl; }查看类继承的内部模型步骤VS2013为例 1.找到Visual Studio Tools文件夹打开找到VS2013开发人员命令提示程序(一般在C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts)打开 2.然后复制你工程路径窗口输入命令cd 路径回车进入你工程文件夹中(如果工程不在C盘在E盘的话要再E:下)。 3.输入命令cl /d1 reportSingleClassLayout类名 文件名全称如cl /d1 reportSingleClassLayoutSon test.cpp 三、多继承 ①多继承概念 我们可以从一个类继承我们也可以能同时从多个类继承这就是多继承。通俗说一个儿子好几个爹。但是由于多继承是非常受争议的从多个类继承可能会导致函数、变量等同名导致较多的歧义。 多继承是单继承的扩展在多继承中派生类的定义与单继承类似其语法格式如下所示 例如 class Base1 { public:void func1(){cout Base1::func1 endl; } }; class Base2 { public:void func1(){cout Base2::func1 endl;}void func2(){cout Base2::func2 endl;} };//派生类继承Base1、Base2class Derived : public Base1, public Base2{}; void test() {Derived derived;//func1是从Base1继承来的还是从Base2继承来的//derived.func1(); derived.func2();//解决歧义:显示指定调用那个基类func1derived.Base1::func1(); derived.Base2::func1(); }通过多继承派生类会从多个基类中继承成员。在定义派生类对象时派生类对象中成员变量的排列规则是按照基类的继承顺序将基类成员依次排列然后再存放派生类中的新增成员。 多继承会带来一些二义性的问题 如果两个基类中有同名的函数或者变量那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本解决方法就是显示指定调用那个基类的版本。 ②菱形继承 两个派生类继承同一个基类而又有某个类同时继承者两个派生类这种继承被称为菱形继承或者钻石型继承。 这种继承所带来的问题 羊继承了动物的数据和函数驼同样继承了动物的数据和函数而羊驼继承了羊和驼当羊驼调用函数或者数据时就会产生二义性问题。解决方法虚继承 羊驼继承自动物的函数和数据继承了两份其实我们应该清楚这份数据我们只需要一份就可以。 举例 class Anmial { public:Anmial(){ma10;}void func(){cout 动物类 endl; }int ma; }; class Sheep:public Anmial{}; class Camel:public Anmial{}; //羊驼类继承羊类、驼类 class SheepCamel : public Sheep, public Camel{}; void test() {SheepCamel sc;//sc.func1;错误二义性//因为羊类和驼类都继承了动物类的func//解决二义性方法指定调用基类的方式解决sc.Sheep::func();//重复继承问题cout SheepCamel size: sizeof(SheepCamel) endl; //重复继承是8 }上述问题如何解决对于调用二义性那么可通过指定调用那个基类的方式来解决那么重复继承怎么解决 对于这种菱形继承所带来的两个问题c为我们提供了一种方式采用虚继承的方法。 ③虚继承 在程序设计过程中通常希望间接基类的成员变量在底层派生类中只有一份拷贝从而避免成员访问的二义性。通过虚继承可以达到这样的目的虚继承就是在派生类继承基类时在权限控制符前加上virtual关键字其格式如下所示 在上述格式中在权限控制符前面添加了virtual关键字就表明派生类虚继承了基类。被虚继承的基类通常称为虚基类虚基类只是针对虚继承而不是针对基类本身。在普通继承中该基类并不称为虚基类。 class Sheep : virtual public Anmial {…}; class Camel : virtual public Anmial {…}; 然后可以将SheepCamel类定义为 class SheepCamel: public Sheep, public Camel{.….};现在SheepCamel对象将只包含Anmial对象的一个副本。从本质上说继承的Sheep和Camel对象共享一个Anmial对象而不是各自引入自己的Anmial对象副本。 class Anmial { public:Anmial(){ma10;}void func(){cout 动物类 endl; }int ma; }; class Sheep:virtual public Anmial{}; class Camel:virtual public Anmial{}; //羊驼类继承羊类、驼类 class SheepCamel : public Sheep, public Camel{}; void test() {SheepCamel sc;//二义性问题解决sc.func();//重复继承问题解决cout SheepCamel size: sizeof(SheepCamel) endl; }但是虚基类是如何解决二义性的呢并且SheepCamel大小为12字节这是怎么回事 ④虚继承实现原理 class BigBase { public:BigBase(){mParam 0; }void func(){cout BigBase::func endl; } public: int mParam;}; //虚继承 class Base1 : virtual public BigBase{}; class Base2 : virtual public BigBase{}; //普通继承 class Base1 : public BigBase{}; class Base2 : public BigBase{}; class Derived : public Base1, public Base2{};我们通过查看对象模型观察到底有什么区别 通过对象布局图我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。 ✅BigBase 菱形最顶层的类内存布局图没有发生改变。 ✅Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个虚指针vbptr (virtual base pointer),在虚继承中每个虚继承的派生类都会增加一个虚基类指针vbptr该指针位于派生类对象的顶部。vbptr指针指向一个虚基类表vbtable不占对象内存虚基类表中记录了基类成员变量相对于vbptr指针的偏移量根据偏移量就可以找到虚基类成员变量。 ✅当虚基类的派生类被当作基类继承时虚基类指针vbptr也会被继承因此底层派生类对象中成员变量的排列方式与普通继承有所不同Derived派生于Base1和Base2,继承了两个基类的vbptr指针并调整了vbptr与虚基类成员变量的偏移量。 由此可知编译器帮我们做了一些幕后工作使得这种菱形问题在继承时候能只继承一份数据并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 、Derived三个类对象共享了一份BigBase数据。 四、派生类和基类的特殊关系 ①类型的兼容性 不同类型的数据在一定条件下可以进行转换比如int n‘a’是将字符’a’赋值给整型变量n在赋值过程中发生了隐式类型转换字符类型的数据转换为整型数据。这种现象称为类型转换也称为类型兼容。 在C中基类与派生类之间也存在类型兼容。通过公有继承派生类获得了基类除构造函数、析构函数之外的所有成员。公有派生类实际上就继承了基类所有公有成员。因此在语法上公有派生类对象总是可以充当基类对象即可以将公有派生类对象赋值给基类对象在用到基类对象的地方可以用其公有派生类对象代替。 ②C中的类型兼容情况 1.使用公有派生类对象为基类对象赋值。本质上是用一个对象去初始化另一个对象调用基类的拷贝构造函数2.使用公有派生类对象为基类对象的引用或基类指针赋值。本质上发生隐式向上类型转换3.如果函数的参数是基类对象、基类对象的引用、基类指针则函数在调用时可以使用公有派生类对象、公有派生类对象的地址作为实参。等同于第二条因为函数在接受实参时会发生赋值。本质上发生隐式向上类型转换⚠️注意 派生类对象指针或引用赋值给基类对象的指针或引用后通过基类指针或引用只能访问基类的成员无法访问派生类的新增成员。 本质发生隐式向上类型转换。 class Derived; class Base { public:Base(){cout 基类Base的无参构造函数调用 endl;Basename 基类成员变量;}void Basefunc(){cout 基类Base的Basefunc调用 Basename endl;}//不定义析构函数用编译器默认的就行 protected:string Basename; }; class Derived :public Base { public:Derived(){cout 派生类Derived的无参构造函数调用 endl;Basename 派生类成员变量;}void Derivedfunc(){cout 派生类Derived的Derivedfunc调用 Basename endl;} }; //定义普通函数func()参数为基类指针 void func(Base* pbase) {pbase-Basefunc(); } void test() {//创建派生类Derive类对象deriveDerived derive;//使用对象derive为Base类对象base赋值实际上就是拿一个派生类去初始化基类Base base derive;//通过Base类对象调用Basefunc函数 base.Basefunc(); //使用公有派生类对象为基类对象的引用或基类指针赋值。本质上发生隐式向上类型转换Base qbase derive;Base *pbase derive;//通过Base类对象的引用或指针调用Basefunc函数//只能调用基类Base的Basefunc函数因为已经向上类型转换为基类了qbase.Basefunc();pbase-Basefunc();//qbase.Derivedfunc();pbase-Derivedfunc();//编译器报错func(derive); //取对象derive的地址作为func()函数的参数 }③拓展向上和向下强制类型转换 将派生类引用或指针转换为基类引用或指针被称为向上强制转换upcasting这种情况可以是隐式的所以才会出现类型兼容的情况。 相反的过程将基类指针或引用转换为派生类指针或引用——称为向下强制转downcasting。如果不使用显示类型转换则向下类型转换是不被允许的。派生类可以新增数据成员因此使用这些数据成员的类成员函数不能应用于基类。 ____________________________________________________ 一、多态 多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。 ①什么是多态 之前的继承中派生类对象使用基类的方法而未做任何修改。然而可能会遇到这样的情况即希望同一个方法在派生类和基类中的行为是不同的。换句话来说方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态–具有多种形态,即同一个方法的行为随上下文而异。 ②一个问题 代码 class Animal { public:void speak() {cout 动物在唱歌... endl; } }; class Dog : public Animal { public:void speak(){cout 小狗在唱歌... endl;}};void DoBussiness(Animal animal){animal.speak();//speak函数调用那它调用的是哪个对象的speak呢}void test(){Dog dog;DoBussiness(dog);}运行结果: 动物在唱歌 **❓问题抛出: ** 我们给DoBussiness传入的对象是dog而不是animal对象输出的结果应该是Dog::speak那么为什么会出现这种现象呢 如何解决 多态多态的目的就是实现方法的调用取决于该方法的对象我们传入了dog对象就应该调用dog的speak函数。 实现多态的重要机制 使用虚函数。 二、问题解决思路 ①静态联编与动态联编 程序调用函数时将使用哪个可执行代码块呢编译器负责回答这个问题。 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding。 ✅①静态联编 在C语言中这非常简单因为每个函数名都对应一个不同的函数。在C中由于函数重载的缘故这项任务更复杂。编译器必须查看函数参数以及函数名才能确定调用哪个函数。然而C/C编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(staticbinding又称为早期联编(earlybinding。 我们之前在类型兼容的时候讲过派生类对象的引用或指针赋值给基类对象的引用或指针后基类对象的引用和指针只能调用基类方法而调用不了派生类方法本质就是静态联编编译器早早的就根据指针类型确定了调用函数。 ✅②动态联编 使用哪一个函数是不能在编译时确定的因为编译器不知道用户将选择哪种类型的对象。所以编译器必须生成能够在程序运行时选择正确的虚方法的代码这被称为动态联编(dynamic binding) 。也叫晚期联编(late binding)。 上面的问题就是由于静态联编引起的因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal,传过来的实参是Dog类型anmial被隐式向上类型转换为Anmial类型编译器确定了应该调用的speak是Animal::speak的而不是真正传入的对象Dog::speak。 解决方法就是动态联编(晚期联编late binding),意味着绑定要根据对象的实际类型发生在运行。 C语言要实现这种动态联编必须有某种机制来确定运行时对象的类型并调用合适的成员函数——虚函数机制。 ②虚函数 C动态多态性是通过虚函数来实现的虚函数允许派生类重新定义基类成员函数而派生类重新定义基类虚函数的做法称为覆盖(override)或者称为重写。 虚函数的声明方式是在成员函数的返回值类型前添加virtual关键字格式如下所示 声明虚函数时有以下3点需要注意。 1构造函数不能声明为虚函数但析构函数可以声明为虚函数。2虚函数不能是静态成员函数。3友元函数不能声明为虚函数但虚函数可以作为另一个类的友元函数。 对于实现动态联编C要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用. 使用虚函数的主要目的是实现动态多态性。在程序运行时若类中声明了虚函数并且派生类重新定义了虚函数当使用基类指针或基类引用操作派生类对象调用虚函数时系统会自动调用派生类中的虚函数代替基类虚函数。 这样就可以实现在运行时根据对象的类型来调用对应的函数方法的行为应取决于调用该方法的对象从而实现多态性。 问题解决: class Animal { public:virtual void speak(){cout 动物在唱歌... endl;} }; class Dog : public Animal { public:virtual void speak(){cout 小狗在唱歌... endl;}};void DoBussiness(Animal animal){animal.speak();//speak函数调用那它调用的是哪个对象的speak呢}void test(){Dog dog;DoBussiness(dog);}三、多态小结 ①多态作用 1.可以解决项目中的紧耦合问题,提供程序的可扩展性 2.应用程序不必为每一个子类的功能调用编写代码只需要对抽象的父类进行处理。 ②多态发生的三个条件 1.有继承。2.重写父类的虚函数。 ✝️返回值函数名字函数参数必须和父类完全一致(析构函数除外) ✝️子类中virtual关键字可写可不写建议写3.基类指针指向派生类对象类型兼容 例如 有一个Hero类该类有attack(普通攻击)这个操作函数又有三个类继承Hero类分别是吕布干将莫邪百里守约它们这三个子类对父类Hero的attack函数有不同的解释有不同的效果子类不需要单独写出不同的解释只需对父类的attack函数进行处理。 如若出新英雄只需增加新代码不需要对就英雄代码进行更改提供程序的可扩展性 class Hero { public://虚函数virtual void attack(){} }; //1.有继承 class lvbu :public Hero { public://2.重写父类的虚函数virtual void attack(){cout 近战普通攻击 endl;} };class ganjiangmoye :public Hero { public: //重写父类的虚函数virtual void attack(){cout 近战法术攻击 endl;}}; class bailishouyue :public Hero { public://重写父类的虚函数virtual void attack(){cout 远程攻击且不会暴击 endl;} }; //同一个操作且参数部分父类指针指向子类对象 void doLogin(Hero *hero) {hero-attack();//产生不同的效果 } void test() {Hero *hero NULL;//3.父类指针指向子类对象hero new lvbu;doLogin(hero);//不同的对象delete hero;hero new ganjiangmoye;doLogin(hero);//不同的对象delete hero;hero new bailishouyue;doLogin(hero);//不同的对象delete hero; }四、多态(虚函数)的实现原理 ①示例代码 class Animal { public:virtual void speak(){cout Animal Speak endl;} }; class Dog :public Animal { public:void speak(){cout 狗汪汪叫 endl;} }; void test() {Anmial *anmial new Dog;anmial-speak; }②原理详细分析 步骤漫画演示对象模型图和代码图1️⃣编译器发现基类Anmial有虚函数会为Anmial添加一个vfptr指针指向虚函数表vtbl(virtual function table)虚函数表中存储了在类对象中进行声明的虚函数的地址。2️⃣派生类继承基类派生类Dog会继承基类Anmial的虚函数表指针这时的虚函数表指针指向基类然后派生类在初始化调用构造函数时使其指向自己的虚函数表如果派生类不改变基类的虚函数并且不新定义虚函数那么虚函数表中的内容还是基类的虚函数地址。3️⃣派生类Dog重写基类Anmial的虚函数SpeakDog的虚函数表原先Anmial的speak被重写的Dog的speak覆盖。4️⃣调用虚函数时如果使用指向基类对象Anmial的引用或指针来调用虚函数speak程序将通过指针或引用找到那份内存空间中(Dog)的虚函数表指针进而找到虚函数表然后再找到虚函数speak。 五、纯虚函数和抽象类 在设计时常常希望基类仅仅作为其派生类的一个接口。这就是说仅想对派生类进行向上类型转换为基类类型使用基类的接口而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数而不一定要提供一段可能对这个函数毫无意义的代码。 做到这点可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class). 纯虚函数使用关键字virtual并在其后面加上0。 如果试图去实例化一个抽象类编译器则会阻止这种操作。 抽象类只能作为基类派生新类不能创建抽象类的对象但可以定义抽象类的指针或引用通过指针或引用操作派生类对象 当派生类继承一个抽象类的时候必须实现所有的纯虚函数否则由抽象类派生的类也是一个抽象类。 Virtual void fun() 0;//纯虚函数声明告诉编译器在虚函数表vtable中为函数保留一个位置但在这个特定位置不放地址。 六、虚析构函数纯虚析构函数 ①一个问题 下面代码中delete animal;将调用谁的析构函数 class Animal { public:Animal(){cout Animal的构造 endl;}~Animal(){cout Animal的析构 endl;} }; class Son :public Animal { public:Son(){cout Son的构造 endl;pName new char[64];memset(pName, 0, 64);strcpy(pName, 如花);}~Son(){cout Son的析构 endl;if (pName ! NULL){delete[] pName;pName NULL;}} public:char *pName; };void test() {Animal *animal new Son;delete animal; }上述现象将会出现一个问题派生类Son中的指针没有被释放。 ②虚析构函数 虚析构函数的声明是在“”符号前添加virtual关键字格式如下所示 基类中声明虚析构函数之后基类的所有派生类的析构函数都自动成为虚析构函数。 在基类声明虚析构函数之后使用基类指针或引用操作派生类对象在析构派生类对象时编译器会先调用派生类的析构函数释放派生类对象资源然后再调用基类析构函数。如果基类没有声明虚析构函数在析构派生类对象时编译器只会调用基类析构函数不会调用派生类析构函数导致派生类对象申请的资源不能正确释放。 问题解决 只需要将需要将基类Anmial的析构函数声明为虚函数即可解决Son类中指针不能正确释放的问题。 ③纯虚析构函数 纯虚析构函数有纯虚析构函数的类是抽象类不能实例化对象。 注意有纯虚析构函数的类是抽象类那么我们想要派生类需要在派生类重写父类的纯虚析构函数但这是不可能的所以纯虚析构函数需要在类外实现。 class Animal { public:Animal(){cout Animal的构造 endl;}//虚析构函数会调用子类的析构函数/*virtual ~Animal(){cout Animal的析构 endl;}*///纯虚析构函数纯虚析构函数需要在类外实现virtual ~Animal() 0; }; //在类外实现纯虚析构函数 Animal::~Animal() {cout Animal的纯虚析构 endl; }
文章转载自:
http://www.morning.jgcyn.cn.gov.cn.jgcyn.cn
http://www.morning.ykrkb.cn.gov.cn.ykrkb.cn
http://www.morning.mmtbn.cn.gov.cn.mmtbn.cn
http://www.morning.ctfh.cn.gov.cn.ctfh.cn
http://www.morning.wgbmj.cn.gov.cn.wgbmj.cn
http://www.morning.rnmdp.cn.gov.cn.rnmdp.cn
http://www.morning.tlpsd.cn.gov.cn.tlpsd.cn
http://www.morning.bpp999.com.gov.cn.bpp999.com
http://www.morning.jbtwq.cn.gov.cn.jbtwq.cn
http://www.morning.qrsm.cn.gov.cn.qrsm.cn
http://www.morning.lswgs.cn.gov.cn.lswgs.cn
http://www.morning.cmhkt.cn.gov.cn.cmhkt.cn
http://www.morning.lctrz.cn.gov.cn.lctrz.cn
http://www.morning.khdw.cn.gov.cn.khdw.cn
http://www.morning.prprj.cn.gov.cn.prprj.cn
http://www.morning.ysjjr.cn.gov.cn.ysjjr.cn
http://www.morning.lyzwdt.com.gov.cn.lyzwdt.com
http://www.morning.sbpt.cn.gov.cn.sbpt.cn
http://www.morning.lmrjn.cn.gov.cn.lmrjn.cn
http://www.morning.nlywq.cn.gov.cn.nlywq.cn
http://www.morning.tqbqb.cn.gov.cn.tqbqb.cn
http://www.morning.ldqzz.cn.gov.cn.ldqzz.cn
http://www.morning.nckzt.cn.gov.cn.nckzt.cn
http://www.morning.hnk25076he.cn.gov.cn.hnk25076he.cn
http://www.morning.zkqwk.cn.gov.cn.zkqwk.cn
http://www.morning.addai.cn.gov.cn.addai.cn
http://www.morning.xrwtk.cn.gov.cn.xrwtk.cn
http://www.morning.bgygx.cn.gov.cn.bgygx.cn
http://www.morning.srmpc.cn.gov.cn.srmpc.cn
http://www.morning.hnhkz.cn.gov.cn.hnhkz.cn
http://www.morning.bprsd.cn.gov.cn.bprsd.cn
http://www.morning.sqtsl.cn.gov.cn.sqtsl.cn
http://www.morning.qnbsx.cn.gov.cn.qnbsx.cn
http://www.morning.czlzn.cn.gov.cn.czlzn.cn
http://www.morning.tqqfj.cn.gov.cn.tqqfj.cn
http://www.morning.wfcqr.cn.gov.cn.wfcqr.cn
http://www.morning.dhqg.cn.gov.cn.dhqg.cn
http://www.morning.rzmzm.cn.gov.cn.rzmzm.cn
http://www.morning.mkydt.cn.gov.cn.mkydt.cn
http://www.morning.dmwck.cn.gov.cn.dmwck.cn
http://www.morning.rhjsx.cn.gov.cn.rhjsx.cn
http://www.morning.zrkws.cn.gov.cn.zrkws.cn
http://www.morning.atoinfo.com.gov.cn.atoinfo.com
http://www.morning.wptdg.cn.gov.cn.wptdg.cn
http://www.morning.lsbjj.cn.gov.cn.lsbjj.cn
http://www.morning.dnhdp.cn.gov.cn.dnhdp.cn
http://www.morning.xflzm.cn.gov.cn.xflzm.cn
http://www.morning.qmrsf.cn.gov.cn.qmrsf.cn
http://www.morning.rqdx.cn.gov.cn.rqdx.cn
http://www.morning.mnkz.cn.gov.cn.mnkz.cn
http://www.morning.mdmc.cn.gov.cn.mdmc.cn
http://www.morning.wcqxj.cn.gov.cn.wcqxj.cn
http://www.morning.tnfyj.cn.gov.cn.tnfyj.cn
http://www.morning.qpqb.cn.gov.cn.qpqb.cn
http://www.morning.mplld.cn.gov.cn.mplld.cn
http://www.morning.kwqcy.cn.gov.cn.kwqcy.cn
http://www.morning.ryxbz.cn.gov.cn.ryxbz.cn
http://www.morning.tqpr.cn.gov.cn.tqpr.cn
http://www.morning.rgpy.cn.gov.cn.rgpy.cn
http://www.morning.lbpfl.cn.gov.cn.lbpfl.cn
http://www.morning.csznh.cn.gov.cn.csznh.cn
http://www.morning.lmpfk.cn.gov.cn.lmpfk.cn
http://www.morning.nbqwt.cn.gov.cn.nbqwt.cn
http://www.morning.lrprj.cn.gov.cn.lrprj.cn
http://www.morning.rpfpx.cn.gov.cn.rpfpx.cn
http://www.morning.gmplp.cn.gov.cn.gmplp.cn
http://www.morning.dqkcn.cn.gov.cn.dqkcn.cn
http://www.morning.gqtzb.cn.gov.cn.gqtzb.cn
http://www.morning.bbgn.cn.gov.cn.bbgn.cn
http://www.morning.crsqs.cn.gov.cn.crsqs.cn
http://www.morning.yfstt.cn.gov.cn.yfstt.cn
http://www.morning.bfjyp.cn.gov.cn.bfjyp.cn
http://www.morning.mkpkz.cn.gov.cn.mkpkz.cn
http://www.morning.fhntj.cn.gov.cn.fhntj.cn
http://www.morning.dwzwm.cn.gov.cn.dwzwm.cn
http://www.morning.mxhys.cn.gov.cn.mxhys.cn
http://www.morning.lwbhw.cn.gov.cn.lwbhw.cn
http://www.morning.zjqwr.cn.gov.cn.zjqwr.cn
http://www.morning.myhpj.cn.gov.cn.myhpj.cn
http://www.morning.lizpw.com.gov.cn.lizpw.com
http://www.tj-hxxt.cn/news/255363.html

相关文章:

  • 视频网站设计论文淄博云网信息技术有限公司
  • 网站怎么加ico国内最好的网站建设
  • 女装网站建设项目可行性分析表wordpress 头像不见了
  • 注册网站怎么开发wordpress主题图片路径
  • 建设通网站联系电话wordpress搜索提示
  • 哪些网站百度收录快网站如何备案icp备案
  • 贵州住房与城乡建设部网站阿里云网站建设程序
  • 做网站的背景怎么做做网站上哪买空间
  • 新手网站建设网站建设实施计划包括
  • 网站标题应该怎么做SEO优化企业品牌网站建设类型
  • 网站模块设计自己做网站项目
  • 驻马店河南网站建设服务器做网站教程
  • 上海备案证查询网站查询网站查询上海网站建设caiyiduo
  • 电商网站开发 文献综述成都多语种网站建设
  • 长沙臻钬建站活动方案苏州网络公司排行
  • 网站谁建设的产品包装设计网站
  • 设计类投稿网站突然爆了长沙致歉
  • 环保业网站建设的策划seo页面优化技术
  • 深圳网页设计网站制作微信公众号设计方案
  • 如何用模板做网站视频网站策划主要工作是什么
  • 南京网站设计制作建设大型的企业网站费用
  • 关键词优化网站排名医院网站开发方案
  • 完整网站开发步骤wordpress+商业主题插件
  • 每天干每天做网站平台型网站开发
  • 调颜色网站哪里有做假铁牌
  • 宝安网站开发商城网站开发实训报告
  • 怎么把网站做二维码wordpress 会员系统
  • 广东手机网站建设报价表网站建设机构培训
  • 宁波外贸网站建设和推广秦皇岛在线
  • 动力启航做网站网站建设 广州网站建设专业公司