这里写目录标题
封装
简介+语法
类 :抽象的 共性的
对象:实例化的 具体的 个性的
封装 就是把属性和行为放在一起 加一些访问权限 就叫做封装
1、封装
2、实例化
案例
这里出现了成员变量 成员方法
set方法
访问权限
可以用访问权限来限制成员 包括成员变量 和 成员方法
保护权限和私有权限 在类外不可以访问 包括成员变量和成员方法
struct和class的区别
成员属性私有化
优点1:
对于某一个私有成员 可以设置public方法(俗称接口) 来进行读写权限的控制
通过设置get和set方法 如上图
优点2:
在set方法中进行if判断 可以进行合理性判断
注意 这里虽然是双支路线 但是没有用else 是因为if中用了return 结束了函数
案例1
参数传入类 是自定义的正方体类
并且利用引用类型 这样节省空间 函数体内不做修改操作 而是只读 所以暂时不用设置保护措施
如果不加引用 直接传入自定义类 也可以运行 只不过会再次复制一份实参
利用成员函数的时候 直接一个对象调用方法,将另一个对象传入即可
对象的初始化和清理
构造函数和析构函数
创建对象的时候会自动执行构造函数 最后函数运行完毕后 因为函数一般都在栈区 所以运行完毕就会被销毁 对象也就随之被销毁 所以最后当对象所在函数执行完毕 那么析构函数的操作随之被自动执行
构造函数的分类以及调用
拷贝函数 将一个对象的某些属性值拷贝给另一个对象 参数用常量引用
括号法 类+对象+(初始化值);
注意利用括号法调用无参构造函数时不能加括号 其他的都要加括号
其中,隐式转化法的真实操作就是显示法的操作,而显示法就是借助构造函数实现的类型转化,这种类名(变量)的形式,如Person(10),这其实是类型转化,因为Person类的由相应的构造函数,所以,该类型转换能够正常实现
补充:系统会默认提供构造函数(包括无参构造函数、拷贝构造函数)、析构函数
如果我们自己实现了一个有参构造函数,那么系统将不会再提供无参构造函数
拷贝函数使用时机
函数传参 以及函数返回值 都是以值传递的方式 而值传递进行都要复制一份 所以都会进行拷贝函数的调用
使用一个已经创建完毕的对象来初始化一个新对象:
这是Person类中的拷贝构造函数,其功能实现与系统提供的功能是一样的 只不过加了一个输出
ps:虽然是以括号法创建的拷贝构造,但是当使用隐式转化法中的“=”来初始化对象时(例如Person p2 = p1),仍然会调用此拷贝构造。所以他看似是针对括号法,实际上他就是拷贝构造函数的固定形式,与重载“=”不同的是,他适用于初始化。
对象当做函数参数(值传递):
在传值的过程中 会调用类的构造函数(具体是拷贝构造)
返回值传递对象:
第一个是一个以Person为返回值的函数
当doWork2函数返回一个对象时 执行类的拷贝构造函数
首先构造p1,所以打印“默认构造函数调用”
之后通过返回值的方式,初始化p,所以打印拷贝构造函数调用,
这时,已经有两个构造函数的调用打印出来了(即时刻牢记,拷贝构造也是构造函数)
构造函数调用规则
拷贝构造是默认会有的 所以进行参数传递以及返回值时 无需自己写拷贝函数仍然可以正常进行
无参构造(系统提供)、有参构造、拷贝构造(系统提供)会相互影响,而析构则不会受到影响
调用规则中 如果只提供有参构造 那么系统将不会提供默认的无参构造 但是有默认的拷贝构造 并且默认的拷贝构造有功能体:将属性值进行拷贝
1、类定义 只保留有参构造
2、tset方法 创建对象 并且调用有参以及拷贝构造
3、输出结果 证明拷贝构造仍然存在
深拷贝与浅拷贝
ps:深拷贝使用时机:类中属性有指针 且指针存的是类中某个部分new出来的数据的地址 这时就要使用深拷贝 重载拷贝构造函数
因为new的数据会在析构函数中被释放 如果使用浅拷贝(对于指针来说 也是简单的值赋值 也就是将地址直接赋值过去) 那么就会有两个指针指向同一块地址 最终释放时 会造成同一块内存重复释放 程序崩溃
当我们在类当中在堆区申请了空间之后 需要释放时 要在析构函数里统一释放
问题:如果使用编辑器默认的拷贝函数 那么只能实现浅拷贝 也就是在直接进行赋值复制的操作
那么 如果属性里面有指针 进行浅拷贝的话 只是将地址复制给了新的对象的属性 二者指向同一块内存
但是这个指针如果是在堆区申请的 那么就要在析构函数里释放 这样的话二者都会释放 但是由于是同一块内存 所以会造成重复释放 导致程序崩溃
解决方案
将参数对象拷贝到当前对象
拷贝时 不仅仅是地址值传递 而是开辟一块堆区空间 进行地址拷贝
直接new int(解出来数据) new的返回值是地址 直接把新地址赋值给新对象的属性值,这样两个对象的属性指向的是两块不同的空间 不会造成重复释放
初始化列表
对有参构造函数进行改造 在括号后面加冒号 然后是
属性值+(形参)
最后在后面加一个花括号
类+子对象成员+父类继承的构造以及构造顺序
当一个类 继承了父类 同时也有一个对象作为数据属性(下面称作子对象 但是并不是继承关系 只是一个类的对象而已)
那么构造该类时 会先构造父类 之后构造子对象 最后构造自己
比如 一个Student 类 成员有phone类对象 继承着Person类 那么构造顺序:Person Phone Student
构造时可以利用初始化成员列表 (前提是父类以及成员对象的类 都有有参构造,即有属性可以使用)
(如果父类或者成员属性类没有有参构造,那么就是默认构造,说明没有属性需要初始化,则不用管他们,会自动调用默认构造)
继承后 想要通过子类对象对继承而来的父类成员进行赋值 可以通过初始化列表 对父类初始化时 是用类名+括号
对于子对象成员 (即自己某个成员是一个对象)也可以放在初始化成员列表进行初始化 这里是用对象名(即属性名)+括号
同时对于初始化成员列表 还可以对一个类的常量进行初始化(const 修饰的变量)如下图
当然直接在类定义的时候给const赋值也没啥问题 但是这样的话const变量在类定义好之后就无法改变 在开发过程中会有些不方便
构造子类时 父类成员的构造方式(推荐第一种 第二种参考)
一种就是上面的利用初始化成员列表,适用于父类有 有参构造函数,直接用初始化成员列表即可
还有一种 因为子类继承父类之后 父类的成员都归子类旗下
所以 可以直接在子类的构造函数里 对继承来的成员进行赋值 **前提是父类的无参构造要存在(或者说默认构造)**因为是直接对成员赋值 没有调用有参构造 所以会调用无参构造
当补上无参构造或者将有参构造删去(系统默认无参构造生效) 那么就可以直接在子类的构造函数里对继承得来的成员进行赋值,
!!!也就是将其全部视为子类属性 进行赋值 进行使用
当有有参和无参之后 也可以直接在子类构造函数里调用父类有参构造函数
类对象作为成员
当一个类的成员中含有其他类时
1、手机类(人类中有手机类作为成员)
2、人类 其中有一个成员是Phone
注意点
人类中的有参构造 参数传入的是字符串 但是等待初始化的是一个手机类数据 而手机类中含有有参构造函数 所以实际代码为
Phone m_Phone=某个字符串
这就是第三种初始化的方式 成为隐式初始化,如下图
3、定义函数创建对象,主函数操作
4、构造函数的顺序
先是将大类的各个部位都构造完毕 才能构造大的类 所以先把手机类构造完毕 才去构造人类
析构时相反
静态成员
对于静态成员变量:
在类中声明 要在前面加static关键字
之后要在类外进行初始化 注意初始化格式 数据类型+类名+双冒号+变量名+初始化;
注意所有对象共享同一份数据 所以对象b修改了静态变量 对象a调用时是被修改后的值
并且加上私有属性后 仍然是类外无法访问
两种访问方式 一个是通过对象调用成员变量
二是直接 类名+双冒号+变量
静态变量可以直接通过类名进行访问
对于静态成员函数:
仍然是被所有对象共享数据
并且有访问权限
但是 静态成员函数体里面只能访问静态成员变量
C++对象模型和this指针
成员变量和成员函数分开存储
只有非静态成员变量 属于类的实例化对象上
而其他的单纯属于类的定义 都是独一份
至于非静态成员函数
编译器会使用一些办法将每个对象的非静态函数区分开
因此,我们得出结论,一个类的大小,仅由其成员属性决定
空对象占用1字节的空间
this指针
this指针指向 被调用的成员函数 所属的对象
也就是调用该函数的对象
1、
this可用来区分类内成员与形参 解决名称冲突的问题
2、
当想传入一个对象b 给对象a属性做操作 并且想返回对象a时 可以利用this 返回调用函数的对象 如上图 return *this
注意 函数返回值以及函数参数都是引用数据类型 不然会导致值传递 浪费空间而且会有断连 容易出风险
这样就可以实现链式调用
空指针调用成员函数
Person * p = NULL;
这样的代码,会使Person的对象p的地址是空地址
当用空指针调用类的方法 方法中含有类的成员属性的时候 会导致this指针指向空 这样就会导致程序崩溃
所以为了程序的健壮性 加一个if判断 如果为空 直接return退出
const修饰成员函数–常函数
常函数: 在类的函数后面加上const
由于this本质上是一个指针常量 也就是无法修改指针的指向
再加上const之后 就变成了常量指针常量 这样指针指向的值(也就是对象 或者说对象的属性值)也不能被修改了
在变量定义的整个语句之前 加一个mutable 这样该变量就可以在常函数中被修改 同时也可以被常对象调用并修改
但是没有加mutable的变量 不可以在常函数以及常对象使用
在对象的定义整个语句前面加一个const就可以
常对象只能调用常函数
因为如果允许常对象调用非常函数 那么就可以完成侧面对常函数里的变量进行修改的操作(例如可以通过非常函数修改常函数内含有的属性值)
补充:正常类对象 即“不是常对象” 仍然可以调用成员函数里的常函数
成员属性加了const之后 再加mutable 仍然不可以修改 因为加了const之后 再加mutable会报错
即,常函数只是关闭了通过该函数去修改成员属性的途径,所以,如果是非常对象来调用,合法,但是不能通过该函数修改非常对象属性值(mutable属性除外)。如果是常对象来调用,合法,不能通过该函数修改常对象的属性值,况且其本来就不能修改
友元
全局函数做友元
在类中声明谁是友元 就可以在接下来的操作中 允许该友元访问类中的private成员
在类的第一行 不在任何作用域规定的范围内 对全局函数声明友元 直接 friend 加函数声明
这样 在全局函数定义时 就可以在该函数中访问该类对象的私有成员
类做友元
注意 一定要在g类上方 声明一下b类 不然g类无法将b类的对象作为g类的成员
在b类中 声明 g类是b类的友元 那么g类可以访问到b类的私有内容
前提是 g类的成员变量中有b类的对象 这样才会涉及到g类去访问b类的私有属性
补充:类内的函数体的定义
类的构造函数
均可以拿到类外进行编写 但是要加上作用域(注意对于类内的函数 除构造函数之外 均有返回值 所以要在返回值后 在函数名之前加作用域)
visit函数是g类的成员函数
对于visit函数 函数体里 building(b类的对象)是g类的成员 所以有g类的成员去访问b类的私有属性
或者g没有b对象作为他的成员 但是visit函数去访问b类的私有成员 也是g类去访问b类的私有属性
补充:同时,类外实现还有个好处,因为在类内,成员变量的定义位置与成员函数的定义位置谁先谁后无所谓,哪怕定义到最后面,类内前面的函数也能访问到,所以,在类外实现成员函数,便于从视觉上统一访问顺序,即从上而下,且还可以实现“被声明为友元函数的函数的实现要在友元声明之后进行实现”的规则,即先在类的框架定义中声明友元函数,之后在所有类的框架定义完之后,实现友元函数
成员函数做友元
注意:此时被声明为友元的成员函数所属的类(即g类)的定义要在b类的前面
而实现顺序:被声明为友元函数的函数实现要在友元声明之后进行实现
且,如果g类的定义框架中有b类的类名,那么在g类定义的前面还要声明一下b类:class Building;(这条规则是面向对象通用规则,如果使用了,那么要在其前面声明,但是友元声明除外,友元声明语句会进行隐式声明)
在b类中声明友元时 声明g类中的成员函数(要声明成员函数的作用域 也就是属于哪个类) 也可以让g类的成员函数访问b类中的”私有“成员
总结
遵循三个规则:
1、“全部先声明后实现”
这个主要是因为有“友元实现要在友元声明的后面”这条规则的存在。
因为友元内部肯定要访问类的私有成员,而友元声明是在类的框架声明内,所以,如果友元的实现在友元声明之前,那么就是在类的框架定义之前,此时友元函数并不知道类中有哪些成员可以用,所以友元实现必须在友元声明之后,也就是类的框架定义完之后。
而为了避免其他函数也发生这种情况,以及符合“常规项目架构”的需要,我们对所有的都进行“先声明后实现”
2、统一的,如果在某个类的框架定义之前,用到了某个类,仅仅是用到了该类的对象,那么要在其前面声明用到的那个类(注意,如果是用到该类的成员,则只声明类是不够的,此时只能考虑调换位置了)。
这条规则是面向对象通用规则,如果使用了,那么要在其前面声明,但是友元声明除外,友元声明语句会进行隐式声明,所以友元声明语句可以不遵循这条规则,如全局函数做友元,友元语句先前就没有声明。
但是注意,如果是用到该类的成员,则只声明类是不够的,此时只能考虑调换位置了。
因为如果某个类声明框架时,用到了后面类的成员,但是此时后面类的框架还没声明,所以目前还不知道后面的类有哪些成员,所以,只能考虑调换位置。
3、实现时先实现友元
用处较少,重在理解友元声明的作用仅仅是影响可见权限,并不会真正声明函数:如果当前类其他成员函数想要使用当前类当中的声明的友元函数,那么必须等友元函数在类外再次声明(或者声明并同时实现)之后,再进行成员函数的实现,此时该成员函数才可以使用自己类中的友元声明,即被声明为友元的函数想要被其他地方调用,只有友元声明是不可以的,还需要真正的声明(用处较少,因为他的使用场景是当前类的成员函数调用当前类内的友元声明的函数,这很少见)
在以上三条规则的前提下,分为两类进行处理:
分为两类:
以a是b的友元为例(a可以是全局函数、类、类的成员函数,b是那个声明友元的类),且我们采用先声明,最后进行实现
1、全局函数做友元、类做友元
顺序:全局函数、类是一个形式,都是:最好先在b中声明友元,再在之后声明全局函数a或者类a的框架,最后对全局函数a或者类a进行实现。实现的顺序还是按照惯例从上而下可见(且可以随意一点,因为该声明的都声明了),唯一需要注意的是友元函数在友元声明之后又被声明或者实现,才可以被调用,所以,建议先实现友元函数以及友元类。
2、类的成员函数做友元
顺序:而成员函数要先声明“被声明为友元的类”的框架,再声明“声明友元”的类的框架,最后进行实现,这是因为由于声明的是成员函数,所以,需要让编译器提前知道该函数是那个类的友元函数·,所以要先声明那个“被声明为友元的类”的框架。最后也是最好先实现友元,之后再按照顺序依次进行实现
运算符重载
1、注意运算符重载 不可以改变内置的运算符 只是应用于自定义的数据类型的运算
2、不要滥用运算符重载 也就是对于+号的重载 在进行函数体的编写时 不要改成- 或者*或者/,这就叫滥用
3 运算符重载函数的返回值一般都是自定义类的数据类型 如果有链式编程的要求 那么返回值定义为引用 没有 那就定义为值传递
+号重载
成员函数进行加号重载 定义一个函数 函数体里实现想要实现的重载功能 这里的逻辑是 其中一个对象调用该函数 传入另一个对象 二者相加 并创建出一个新的对象 最终通过值传递的方式返回新的对象
在全局调用函数 并且参数是两个对象的地址
如果严格按照调用的规范 应该是上图中被注释的调用写法 但是编译器提供了简化写法 直接p1+p2 用一个新的类对象去接收
补充
+号重载也可以发生函数重载
只需要改一下参数列表以及函数体里的变量即可
调用的时候仍然可以使用简化写法
总结
1、对于数的加减乘除 一些编译器已经定义好了的运算符 是不可以更改的
运算符重载只能是针对一些自定义类型 进行重载
2、不要滥用运算符重载 也就是对于+号的重载 在进行函数体的编写时 不要改成- 或者*或者/,这就叫滥用
左移运算符重载
1、为什么要进行左移运算符重载
简化cout输出 重载后 直接输出p 那么就会输出p的属性(具体是哪些属性 要在重载的函数体里进行编写)
2、怎样进行左移运算符的重载
可能我们会想到,在ostream类内进行cout的<<重载即可将cout放在右侧,但是这样的话,每个cout,不管是不是Person对象,都会输出Person对象的属性,显然完全不合理。
所以:
与+号运算符不同的是 左移运算符的重载不可以用成员函数 只能用全局函数
传入的参数是两个变量的引用地址 (由于是引用 所以相当于起别名 所以cout可以改成out 不过建议用cout)
函数体是我们想要重载的内容 最后返回值是ostream类型的cout变量 接收也用ostream& 接收引用类型 因为参数传入的就是引用类型 而且 返回值是引用类型 可以起到链式编程的效果
注意 这里用引用 不仅仅可以节省空间 、链式编程,还是因为全局只能有一个ostream对象 所以只能利用同一块cout的空间
也就是可以在cout<<之后再接<<进行多输出。
注意,这里第一个左移运算符是使用了全局的那个左移重载,而之后就是使用的正常的cout,因为左移运算符的右边不是Person对象,继而不会执行左移重载,而是正常的cout
重载递增运算符
++递增运算符(跟谁挨着 就是谁调用)
对于前置递增
返回值是自定义类的对象的引用,方便进行链式编程 同时返回调用该运算符的对象
用值传递定义返回值 编译通过 但是无法实现链式编程
对于后置递增
用值传递定义返回值 之后参数需要参数站位符 告诉编译器是在重载后置运算符
函数体里先将原始值保存 用于之后的返回
但是仍然需要进行++的操作
(如果后置返回引用 但是return是返回局部对象 所以如果返回引用 那么后期地址上的变量会被销毁 所以对于后置的重载,要进行值传递)
1
注意值传递与引用传递的区别:
值传递会复制一份新的内容 所以无法进行链式编程
引用传递是在原元素上进行操作 可以进行链式编程
2
在进行递增运算符重载时 返回的都是自定义类的数据类型 如果有链式编程的要求 那么返回值定义为引用 没有 那就定义为值传递
补充:对cout<<先进行重载 输出数据
赋值运算符的重载
编译器提供了等号的重载
但是有个问题 编译器还是在把堆区数据进行了浅拷贝 也就会导致最终释放堆区空间的时候 会导致堆区内存重复释放(先释放p2 之后释放p1的时候 会重复释放 程序崩溃)
解决办法就是进行深拷贝
关系运算符重载
注意返回值是布尔类型
因为调用"==" 是
某个对象==某个对象
所以本质上 是一个对象调用==函数 传入另一个对象
所以重载函数写在类的成员函数部分,参数传入另一个对象
函数调用运算符重载
1 函数调用运算符重载 其实就是()的重载
他是类的成员函数 由类进行调用
2 写法非常灵活
返回值 operator()(参数列表)
匿名对象去调用
匿名对象调用 类名进行调用 而不是用对象名 也就是没有变量去调用 而是直接用数据类型—类 去调用
与之前直接类名+(参数)这种形式相比,上面多了一个(),注意区别。之前那种是因为本质上是使用构造函数实现了类型转换,而此处由于不是构造函数,所以,前面加了括号表示调用()重载的匿名对象
继承
基本语法
类有自己的大类 不同的类之间也有共性 这些共性可以抽象出父类
这是一个父类 也就是各个类的共性
基础语法 class 子类 : 继承方式 父类
子类中只需要写自己的个性的东西即可
子类调用父类的成员时 直接进行调用即可
继承方式
继承方式会向下兼容(但是private权限的成员不可被子类访问)
对于public的继承 子类继承到的父类成员访问权限都不变(private权限的成员不可被子类访问)
对于protected的继承 子类继承到的父类成员访问权限都改为保护权限(private权限的成员不可被子类访问)
对于privat的继承 子类继承到的父类成员访问权限改为私有权限(private权限的成员不可被子类访问)
这些不会影响类内成员函数的访问 (private权限的成员不可被子类访问)
但是会影响类外对象的访问,如下图
son3以private的方式继承Base3父类
类内虽然访问权限变为了private 但是对于类内成员函数 是可以访问类内private成员的
但是类外的类对象 不可以访问类内的私有成员
总结:
补充1:
当GrandSon继承Son的时候 将Son当做父类 且Son类中继承的是被修改权限的Base类的成员 现在Son类中的成员都是私有(因为Son类的成员都是以private的方式继承自其父类)
补充2:
成员函数与成员变量的继承规则一样:当以private/provate的方式继承时,外部无法访问到继承自父类的成员函数,如下:
但是在子类内部,是可以访问继承自父类的成员函数的:
继承中的对象模型(从父类继承过来的成员哪些属于子类对象)
从这张图可以看出 父类所有成员都归入到了子类对象 访问权限并不会影响成员变量的继承
补充:利用工具查看对象模型
补充:之所以成员函数并没有任何的出现,是因为在C++中,成员函数不属于任何一个对象模型,他与对象是合作关系,他会被系统放置到一个特定的区域,且不管有几个实例化对象,成员函数仅仅只被实例化一次,所以,真正意义上他并不属于对象模型。
继承中构造和析构的顺序
测试过程:
结论:
继承中同名成员的处理
父类定义
子类定义
测试函数与main函数运行
总结
对于成员变量
访问自己类: 子类对象可以直接访问自己类内的成员
访问父类: 子类对象加作用域可以访问到父类的同名成员
对于成员函数
访问自己类: 直接访问自己类中的成员函数
访问父类: 必须加作用域 不然哪怕父类有一个带参数的函数重载,而子类没有,调用带参的同名函数时 仍然会报错
也就是当子类与父类拥有同名函数时 子类会隐藏父类中的同名函数,只要同名 全部隐藏 不考虑是不是函数重载
当子类与父类没有同名函数时 但是父类中发生了函数重载 那么子类可以调用重载函数 并且不需要加作用域 这个时候并不会隐藏父类中的成员函数
所以 当子类与父类成员函数重名 调用父类成员函数也要加作用域
同名静态成员处理(子类和父类的重名成员都是静态)
最后一行 第一个双冒号代表通过子类访问 第二个双冒号代表声明父类的作用域
与非静态同名的处理一样 只不过多一种访问方式 通过类名访问
补充:
静态成员变量 类内声明 类外初始化 初始化时要加作用域
类的所有实例化对象共用一份数据
补充2:因为子类和父类的重名成员都是静态,那么可以直接使用父类来调用父类中的静态成员
总结:对于子类直接使用子类对象通过“点”去调用
对于父类,使用子类.父类作用域::成员,去调用
即下图这种方式(也就是上面介绍非静态成员时的方式):
这种方式是万能的,即无论子类成员是否是static、父类成员是否是static,或者怎样的混搭,他都可以进行。
此外,对于调用父类的成员,可以直接使用“补充2”介绍的方法
多继承语法
菱形继承
也就是有子类继承父类中的同一份数据
但是这俩子类有一个共同的孙子类 该孙子类要通过子类去访问父类中的那份数据 这样就有两种途径去访问 但是两种途径访问的结果一样 就造成了资源的浪费
所以 采用虚继承的方式来解决
两个子类虚继承父类 这样 父类就被称为虚基类 此时 父类中的数据就被两个子类共享 两个子类中继承到的父类数据是同一个地址的数据 如下图 两个子类各自出现一个虚指针 均指向父类的那份数据 所以 接下来不管孙子类通过哪种途径去访问 去修改基类的数据 那份数据都是同一份数据 不会出现二义性
当子类虚继承父类时,子类中会出现一个虚指针,叫做"vfptr"(虚函数指针) 该指针指向一个vftable(虚函数表) 表中记录着所虚继承的父类的地址
多态
基本语法
这里所说的多态 是指动态多态
进行地址晚绑定:实际上就是函数的地址 要在编译之后根据传入的实参 才能确定
三个类的定义
注意点1:子类继承父类
注意点2:要将父类的那个多态函数变成虚函数
注意点3:子类要重写父类的方法 包括返回值 函数名 以及参数列表
多态的实现与调用 在一个方法里 参数列表是父类的引用或者地址 实参是子类对象 这样就可以实现 父类指针或引用指向子类对象
之后使用父类的指针或者引用来进行操作
!!!为什么要有多态:
方便函数的编写
假设 我想要输出猫在叫 和 狗在叫
那么 在没有多态的前提下 我要写分别去调用成员函数 不具有统一性
但是有多态的话 写一个总函数 形参是动物类 也就是猫和狗的父类 函数体里面直接调用"动物叫"这个成员函数
然后猫和狗这两个对象直接统一调用这个总函数即可
总结
补充1:
父类指针或者引用指向子类对象可以成立的原因:
因为子类继承着父类 所以 子类中的数据>=父类中的数据 所以 子类有着父类的所有东西 那么就可以用子类对象来初始化父类对象 类似于拷贝构造
两种指向形式
父类指针指向子类对象:
首先是一个父类指针 之后指向子类对象 利用new子类() 创建子类对象 同时返回该对象的地址 因为虽然是指针指向子类对象 也要类型适配 即要将子类对象的地址跟父类指针划等号
new子类 可以在创建子类对象的同时 返回了子类对象的地址 所以可以直接划等号
当分步骤来创建子类对象时 就可以看到如果直接指向子类对象 会报错:(如下)
但是加上取地址符 就可以了 这就是上面所说的 既要指向子类对象 还要类型适配 创建一个子类对象 将地址给父类指针:
利用函数实现指向 是一个道理:
父类引用指向子类对象:
当使用父类引用来指向时 就要用子类对象实体 同时注意p要用点来调用函数
函数形式指向也是一个道理:
多态的原理
当对父类的成员函数加了virtual时 该类中就产生了一个指针 叫做"vfptr"(虚函数指针) 该指针指向一个vftable(虚函数表) 表中记录着虚函数的地址
当子类继承父类之后 父类中的虚函数指针以及虚函数表都会被继承到子类中 这时 子类继承到的指针 与父类中的指针 是同一个指针 并且子类继承到的虚函数表中的内容还是父类中的虚函数表的内容(相当于从父类中拷贝了一份)
当子类继承了父类 并且重写了父类的虚函数 那么子类从父类拷贝的虚函数表中的内容 就会被覆盖 会变成子类重写的这个函数的地址
所以 调用多态的时候
Cat cat;
Animal &animal= cat; //进行函数地址晚绑定 绑定到cat的虚函数表
animal.speak; // animal调用speak的时候 先调用虚函数指针vfptr 之后调用该指针指向的虚函数表
由于绑定了cat的虚函数表 并且指向cat虚函数表的指针与父类Animal的指针是同一个指针 所以 最终会调用cat重写的那个方法
总结:重写函数就是完成虚函数指针进行多个指向的构建,每重写一个,虚函数指针都会构建出自己的地址指向某个重写的虚函数表。
之后父类指针指向子类对象的时候,就是在选择使用指向的多个虚函数表中的哪个表。
多态的好处(为什么要用多态)
对于普通实现 要想拓展功能 还要去类的定义里面添加成员函数 也就是需要修改源码
不提倡修改 只提倡拓展
利用多态 直接添加一个类 类里面重写父类的虚函数即可 不需要进行修改 直接进行添加类即可 注意上图中为了实现父类指针指向子类对象 采用了在堆区开辟空间创建子类对象的办法 开辟类对象时 直接new类名即可 父类的指针来接收
用完要释放 释放等号左边的量
总结:
多态就是实现一个类似函数的东西,如果将多态写成函数形式,通过传入子类对象,函数做出不同的操作,即父类引用或指针是形参,子类对象是各种各样的实参(更具体的说,是实参的值),提高了代码的复用性和灵活性。复用性:写成函数的多态,直接调用函数名即可;灵活性:可以灵活的决定传入哪个子类
还有一个方面体现着:父类是形参,子类是实参值,比如一个多态数组,我们可以定义一个父类类型的数组,之后,将其每个元素赋值成不同的子类对象,例如,goods[i] = new 子类();其中,goods是父类引用或者指针类型的数组,最后操作good[i].调用父类函数。其实这个数组也可以理解为一下子创建了许多的父类引用,每一个goods[i]都是一个父类引用或者指针,等待着接收子类对象
总结就是,多态中父类引用或者指针和子类的关系就是int a = 2;或者int b = 2; int a = b;而这个关系放到函数里,就是形参(本质是一个变量)与实参(本质是值)的关系,放到数组里,就是变量和值的关系,最后操作那个形参或者变量,即父类指针或引用
纯虚函数(抽象类)
因为在多态中 父类的函数中的内容基本上用不到 因为一般想要使用子类重写的函数才会去使用多态 所以 可以将虚函数写成纯虚函数
当一个类中有纯虚函数的时候 这个类就被称为抽象类
1 抽象类的特点1:无法实例化对象
通过在栈区或者在堆区 都不可以创建实例化对象 但是可以创建父类型的指针 这也使得多态的调用不受影响
因为多态的调用是对子类进行实例化对象 父类只需要提供指针即可
父类 指针=new 子类;
2 实现继承关系后 子类必须重写父类中的纯虚函数 不然子类也属于抽象类 无法实例化对象
案例补充
由于实现多态的时候 是父类指针指向子类实例化对象之后 要通过父类指针去调用父类所被重写的纯虚函数 才可以实现多态
但是 当父类中有许多个虚函数的时候 就要多次对虚函数进行实现 之后想要调用每个步骤,就要每个函数都调用一下,就会很麻烦,所以就可以再写一个函数 将这些函数调用集合起来 到时候直接调用这个总函数即可
最后,如果想制作咖啡,就传入咖啡对象的地址,如果想制作茶叶,就传入茶叶对象的地址,流程的步骤顺序一致,只是具体的手续不一样。
同时要注意,哪里的指针接收了堆区数据的地址,就在哪里记得释放掉该指针所指的地址。
虚析构和纯虚析构
适用于子类的属性中有堆区的数据需要释放
注意是在父类中对父类进行虚析构
因为单纯释放父类指针 是无法释放子类中的堆区数据的 因为调用不到子类的析构函数
特点以及语法
注意二者都需要有具体的实现
虚析构因为有具体实现 所以不叫纯虚析构
而纯虚析构虽然声明的时候没有实现 但是仍然需要在类外进行实现 注意要加作用域
思路:
首先我们看到子类属性中有指针 用于在构造函数中维护堆区的数据 所以这时想到要用虚析构
所以 在父类中 将析构改为虚析构(或者纯虚析构)
这里也可以看出子类不会调用析构的原因:因为释放父类指针之后 子类的析构函数就不会进行 但是父类的指针又不得不被释放
总结
案例
需求思路
另一种思路:
1 将三个零件的虚函数写入电脑类当中 并集合到一个总函数
2 子类是具体的一家厂商 比如inter 在该类中重写所有的虚函数 并输出inter生产的对应零件
3 子类实现对父类的多态 调用父类的总函数 输出对应的厂商生产零件
弊端:每个零件的出品必须是同一家厂商 无法多个厂商组成一个电脑 缺乏灵活性
上图的方式虽然麻烦 但是灵活性高
在实现零件出品于同一个厂商的同时 还可以实现零件出品于不同的厂商
实现
零件类层
零件的类层 分别对三个零件写三个抽象类
零件子类的实现层
每个厂商实现每个零件都是单独一个子类 继承零件父类 并重写
因为是重写,所以,该函数不是继承而来的函数,而是属于子类的函数,所以要按子类的需求,为其设置访问权限。
这里注意与继承方式进行区别,因为如果一个变量是继承而来的,那么在子类中是不会再次出现这个变量的,不然会有二义性,所以继承方式的规则是在规定那些继承而来的变量,而子类中写出的成员,都要针对子类自己的需求进行权限设置
电脑层
构造函数里对成员变量进行赋值 成员变量都为父类指针 main函数里传入的是已经构造好多态的父类指针,多态是在main函数里构造完成的
形参接收到地址 赋值给类中成员的父类指针
多态调用起来之后
通过父类去调用抽象接口 (进而之后进行地址晚绑定 vfter虚函数指针指向对应的子类对象的虚函数表)
实现多态 输出子类重写的函数内容
最后在类中析构成员变量
由于电脑类中的父类指针拿到了子类对象的地址
所以实际上析构的是main函数中new出来的子类对象,但是是释放的父类指针,因为这些地址都存在父类指针内
这些成员数据类型为父类 都是父类指针
实现层
先对电脑的零件字类层在堆区进行空间的开辟 这些指针都是父类指针 指向子类的实例化对象 这一步,实现了多态的构建
之后创建com对象的指针,将构建好多态的父类指针传入构造函数
使用com对象指针,调用work函数 实现对父类中虚函数的调用 实现多态(由此可以看出,com类就是父类的集合类)
最后释放堆区指针 (这里释放的同时 会调用类中的析构函数 进行上面三个堆区数据的释放 因为多态构建时,是传入父类指针,而com类中有指针成员,且因为构造函数将构造好多态的父类地址传入,所以com类中的指针都是父类指针,所以直接通过普通析构析构即可)
虚析构/纯虚析构 是用于子类中含有指针成员,且保存的是堆区地址的情况。父类指针成员(指向堆区)直接在父类中进行普通析构即可
补充:这里是采用创建一个com类的对象指针,然后手动delete释放,我们也可以直接创建出一个实例化对象com,之后将父类指针传入,调用work,一样可以实现多态,最后也不用手动释放,因为函数结束后,栈区自动释放,析构函数自动调用
文件操作
简述
文本文件
写文件(将程序内存中的内容给到固定文件)
步骤
其中打开文件时 要规定打开方式
代码示例:
因为<<是输出描述符,所以,<<后面所有的数据都会被写入文件中,包括最后的endl,所以文件中的效果是三行,endl也被写入文件中进行换行操作
注意 文件路径:
当写文件并且先前没有创建文件时 会自动将文件创建到cpp文件的同级目录下
也就是说 文件路径的起点是当前ofs所在的cpp文件
如果是…/text.txt
那么他会在当前main函数所在的源文件的上一级创建text文本文件,(注意,在c++中的路径表示是正斜杠,这与在哪个平台是无关的,在c++源文件中都这样表示即可)
总结
读文件(将固定文件中的内容读到程序变量中,读完后文件中的内容还在)
步骤
对于第三步 判断是否打开
利用ifstream类中的对象调用该类中的is_open方法 可以判断文件是否已经打开
对于第四步 读数据
有四种方式
其中 着重采用第三种
定义一个字符串(记得包含string头文件)
用于准备接收文件的内容
之后在while循环里调用getline方法 参数有两个:输入流对象,准备好的字符串
在循环里输出即可
注意点:getline会将文件中的换行给读取到,然后并不会给buf,而是自己消化。
注意点2:每次getline,都会把原先读到buf的内容给覆盖掉,所以,每次拿到一个getline,都要及时作出处理,不然就被覆盖了
二进制文件
优点:读写不仅局限于字符类型的数据,任意类型都可以
下面以自定义类为例
写文件(将程序内存中的内容给到固定文件)
注意点1:创建输出流对象时 可以直接利用构造函数 进行打开文件
打开文件时 第二个参数 要写两个打开方式 一个说明是输入流还是输出流 一个说明是二进制 两者用 | 进行相连接
写文件时 采用ofstream类的write方法 该方法有两个参数:第一个是待写入的数据的地址(要强转为const char *类型),第二个是数据类型对应的数据(上图中是对象)的字节大小
读文件(将固定文件中的内容读到程序变量中,读完后文件中的内容还在)
因为写文件时写入的是Person类型的数据 所以读文件也需要定义出这个类型
之后按照步骤进行
同样打开文件需要判断是否打开
在之后读文件
利用read函数 两个参数:第一个是数据对象的地址(强转为char *类型),第二个是数据的字节数 传入p的字节 而不是Person
(根据对象模型可知,只要这里定义的person与写文件时person类中的成员变量一样,那么他们的大小就是一样的,(成员函数的数量可以不同),且与成员变量有没有赋值没关系)
这里就会读到p对象 所以想要得到具体数据 还要输出p.属性值
补充
键盘输入(接下来都是iostream头文件中的内容)
数据流
中心思想:键盘输入的东西 可以可以看做一个文件 称为“键盘输入文件”
首先我们要明确 从键盘上输入数据是带缓冲区的,只有输入完数据,并且在数据输入完之后按回车,就会形成输入流,也就是完成了文件到输入流的转化,就可以向程序中输入数据了(注意 如果数据没有输入完 也就是还有变量没对应数据 那么这时的回车是被当做空格符的)
键盘设备的输入流对象系统规定为cin,所以cin是一个系统默认键盘输入流对象,而ifstream ifs 是一个文件输入流对象,与cin的区别是cin是键盘文件的输入流对象 ,ifs是某个文件的输入流对象
cin>>
首先将输入输入到缓冲区 形成键盘文件 敲下回车之后 形成输入流 那么cin>>就可以从输入流中获取数据赋值给后面的变量
如果提取失败 那么cin的值就是0
cin>>x 表达式的值也是0 所以可以将其作为条件判断接下来是否还有键盘输入(常用于算法程序 判断还会不会给程序塞数据,因为算法程序的测试用例,就可以看成一个已经形成好的键盘文件,并且形成了输入流,拿着这个输入流塞给程序)
get()
注意,他是istream的成员,即头文件iostream的成员
因为cin是默认键盘输入对象 所以要用cin调用get函数 返回所获取的字符 文件结束时返回EOF
输入数据 按下回车键之后 形成输入流 之后交给get函数处理 get函数将字符一个一个读入程序,当读完时,意味着文件结束(这里的文件是键盘输入文件),那么get就会读到EOF(文件结束符,可以使用ctrl+z键入,或者-1)所以可以使用该条件来判断读入是否结束
geiline()
返回值是void 参数是存放的字符数组的数组名或地址,最大读取字符个数限制n 一般为字符数组的大小 ,因为每次都是往那个字符数组中存放
上图中 每次回车都是一个输入流 那么他的程序结束是用ctrl+z来表示接下来没有输入了,使得程序结束
(ps:对于算法题来说 不用ctrl+z 因为是系统强制输入数据 算法测试用例已经是一个键盘文件的输入流 所以直接将该输入流通过getline解析,之后判断目标字符数组str中的字符个数是否为0 就能告诉程序测试用例输入了几行)
getline()用法
getline是C++标准库函数;它有两种形式,一种是头文件< istream >中输入流成员函数;一种在头文件< string >中普通函数;
它遇到以下情况发生会导致生成的本字符串结束:
(1)到文件结束,(2)遇到函数的定界符,(3)输入达到最大限度 n。
输入流成员函数getline()
函数语法结构:
在< istream >中的getline()函数有两种重载形式:
istream& getline (char* s, streamsize n );(默认截止字符是‘\n’)
istream& getline (char* s, streamsize n, char delim);
作用是: 从istream中读取至多n个字符(包含结束标记符)保存在s对应的数组中。即使还没读够n个字符,
如果遇到delim 或 已经将输入的字符读完了(即文件结束),则读取终止,delim都不会被保存进s对应的数组中。
代码实例
#include
using namespace std;
int main()
{
char name[256];
cout << "Please input your name: ";
cin.getline(name, 256);
cout << "The result is: " << name << endl;
return 0;
}
#include
using namespace std;
int main( )
{
char line[100];
cout << " Type a line terminated by ‘t’" << endl;
cin.getline( line, 100, ‘t’ );
cout << line << endl;
return 0;
}
string中 getline的用法:
cin的gcount函数
返回值是int 无参数
read()
不会只读取一行 而是会读取全部 ctrl+z之前的都被读取,回车也会被读取并放入到buf中
此处的read函数与上面fstream文件中ifs的成员函数的原型是一样的。
read、write函数
他们在c++中,只有文件读写、IO读写,这两处有read、write函数,除此之外,就是网络编程中用到的文件描述符读写,如上图。除此之外,再无其他原型
职工管理系统的知识点
分区编写代码
格式
1、 将类的定义以及类的属性声明写在头文件里
可以视为类的框架
同时在源文件写头文件中类的声明的具体实现 直接包含对应的头文件 注意写实现时要写明作用域也就是类名 类实现的源文件服务于类声明的头文件 但是主要的操作内容要在该源文件实现
3、在程序的入口源文件 也就是包含main函数的文件里 (首先要明确 所有的源文件是独立的 不会直接共享 同时头文件与源文件独立)
想要访问头文件以及与头文件相辅相成的具体实现文件 就要包含头文件(注意 具体实现文件也要包含对应的头文件才能建立联系)
进一步理解
创建的管理类 实际上就是把之前在主文件中的所有函数以及用到的成员属性 都放在一个类里面进行管理 这样可以利用类 更加灵活 使用时直接在main函数创建管理类的对象使用即可
在管理类头文件的具体实现文件当中 如果用到了某些其他头文件里的定义 那么只需要在该文件对应的头文件里包含即可(如上上图)
while(1)以及Switch
1、while(1)的作用是 卡住页面不动 也就是用户没有输入之前 保持住页面的展示 换句话说是提供监听功能
2、Switch case语句中的break
break的作用是退出当前循环 在switch中 break 只会跳出switch框架 之后会继续向下执行switch框架之外东西
3、switch case语句中的continue
continue会跳出switch框架 并跳过Switch·之外的语句 直接进行下一次循环
释放堆区数据的实质
如上图 当创建完Employee对象之后 释放了worker
紧接着下一行 仍然可以使用worker指针
说明 释放的是指针变量中存放的数据 而不是指针变量自己 指针变量仍然可以使用
解析函数释放谁
解析函数用来释放程序运行过程中不可以释放的在堆区的地址 因为程序运行结束之后 就要把所有的堆区开辟的地址都释放掉 所以一些暂时用的到的地址 在程序运行结束后 在析构函数中进行释放
判断文本文件是否为空
首先读出一个字符 然后调用输入类的eof函数 该函数可以查看该字符是不是文件结束标志 也就是该函数返回值如果为真 那么就是读到了结尾
所以如果先读一个字符就到了结尾的话 那么该文件肯定为空
文本文件以空格为分隔符进行读取数据到指定的数据变量中
上图中 不采用任何API函数 而是直接利用ifs对象进行读数据 那么会以空格为分隔符 一个一个数据进行读取
图中的 Id name dId 都是待存放读取到的数据的数据变量
右移运算符 + 数据变量 意思就是将读取到的数据放入数据变量里
补充 图中利用&& 那么条件变成 读取完这些数据之后 意为完成一个周期
删除数组中的一个数据–数组前移
1、如上图for循环的内容 首先 i 定位在要删除的索引位置并且 i<num-1 (实际上i的范围就是要操作的索引范围 但是因为每次操作涉及到两个紧挨着的数据 且i是第一个数据的索引 所以 最终 i 的截止位置是倒数第二个元素的索引 所以小于num是最后一个元素 小于num-1就是倒数第二个元素)
2、想要删除一个数组元素 直接将该元素后面的元素一次前移 就把该元素覆盖住了 同时因为删除该元素 该元素就是空元素 所以应用数据结构中顺序表移动的思想 要向着空元素的位置依次移动 所以是从前面开始依次移动
3、注意更新数组元素个数 同时 !! 记得更新文件的内容 就是要写入文件的函数再调用一下 将堆区数据写入文件
delete释放地址之后数组的状态
如上图 可以直接delete数组中某个元素的地址 删除之后 该地址仍然可以使用 因为delete删除的是该地址的数据 地址仍然可以使用
释放时要释放干净
要依次释放在堆区申请的空间 利用for循环
注意 释放之后 因为释放的是数据 而不是地址 所以 要把地址指向空