侯杰-C++面向对象高级编程-笔记

1.C++编程简介

(1)目标:培养正规的编程习惯

class的两个经典分类:有指针成员的类(complex)、无指针成员的类(string)
在这里插入图片描述

(2)c vs cpp关于数据和函数:

c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的。
cpp中,将数据data和函数都包含在一起(class),创建出一个对象,即为面向对象。数据和函数(类的方法)都是局部的,不是全局的。

2.头文件与类的声明

(1)C++ programs代码基本形式

小tips:引用自己写的头文件,用双引号。
在这里插入图片描述

(2)头文件的标准写法:

首先是防卫式声明,如果没定义这个名词,那么就定义一下:ifndef+define+endif。(这样如果程序是第一次引用它,则定义,后续则不需要重复定义,不需要重复进入下面的过程)
1-写的类的声明
2-是要写类的具体定义
0-写12的时候发现有一些东西需要提前声明,写在该处。
在这里插入图片描述

(3)模板类型

因为实部和虚部的类型不确定,可能是double、float、int,定义起来比较费劲。我自己定义一个模板类型叫做T来满足这个要求。将T作为一个类型参数来传入,在调用时指定类型。通过在定义类的前面加入一行代码template来实现,如图红线圈出来的部分,
在这里插入图片描述

3.构造函数

(1)函数(inline)

定义类的时候,可以直接在body中定义函数(默认为inline函数)如图标号1,也可以只是在body中声明函数。
inline内联函数那么会比较好,运行比较快,尽可能定义为内联函数。
注意的是:上面所有的inline函数,都只是我们指定的,希望它为inline,具体是不是,要看编译器来决定。
在这里插入图片描述
一般情况下, 数据应该被定为private,这样外界看不到。函数应该定义为public,被外界使用。
在这里插入图片描述

(2)构造函数

通过构造函数来创建对象。会自动调用构造函数进行创建。
构造函数名称需要与类的名称一样。函数的参数可以有默认参数。构造函数没有返回类型。
注意:不要使用赋值的方法来写构造函数,使用构造函数的特殊的方法来写,更规范。使用初值列(如图中标红处)。
在这里插入图片描述
构造函数可以有很多个,可以重载。但用红框圈出来两个函数会产生歧义,当未设定初值时,两个函数都可以调用。
同名的函数可以有多个,编译器会编成不同的名称,实际调用哪个会根据哪个适用。
在这里插入图片描述
通常构造函数不要放在private中,这样外界没法调用,也就无法创建对象。
在设计模式Singleton单例中,将构造函数放在了private中。这个class只有一份,外界想要调用的时候,只能使用定义的getinstance函数来取得这一份;外界无法创建新的对象。
在这里插入图片描述

4.参数传递与返回值

(1)const

定义函数的时候,函数名后面➕const,对于不会改变数据内容的函数,一定要加上const。(如果上面real和img函数定义的时候,没有加const,那么这里函数默认的意思是可能会改变数据,与我们的常量复数就矛盾了。编译器会报错。)
在这里插入图片描述

(2)参数传递

1.传递数据:传递value是把整个参数全传过去,double4字节。尽量不要直接value传递。
2.传递引用:尽可能传递引用reference,传引用相当于传指针,很快,形式又很漂亮。
3.传递const引用:传引用过去,修改之后,都会改变;如果只是为了提升速度,不向改变数据,那么传const引用。这样传进去的东西,不能被修改
在这里插入图片描述

(3)返回值传递

返回值的传递,也尽量返回引用。
1中操作符重载的声明中,没有写变量名,也可以写上。c++中,声明函数的时候,可以不写变量名,实现的时候必须写。
注意:在该函数中创建的局部变量不能
在这里插入图片描述

(4)友元(friend)

修饰在函数定义之前,表示这个函数可以直接拿该类对象的private数据。
如下图所示,声明为friend之后,函数可以直接取到re和im,如果不被声明为friend,只能通过调用real和imag函数来得到,效率较低。
此外,相同class的不同对象objects互为友元,即可以直接拿到另一个object的data。
在这里插入图片描述

5. 操作符重载与临时对象

(1)操作符重载1-成员函数(包含this)

该类函数由类名+==函数名()==构成
成员函数。所有的成员函数都带有一个隐藏的参数this(是一个指针),this表示(指向)调用这个函数的调用者。
在这里插入图片描述
关于操作符重载的引用分析:
1.虽然返回值需要的是引用,但是代码中写的返回值可以是value
2.+=操作符中,定义的参数是引用,但是传进去的c1也可以是value。
综合1,2,引用可以接受各种形式传递
3.接收端使用什么形式接收与传递者无关
4.上面的操作符,进行c2+=c1操作之后,c2改变了,返回了c2的引用。因此感觉上,将操作符写为void函数也可以。
但实际上,考虑更周全的话,为了可以兼容c3+=c2+=c1的形式,即标黄部分不能为空,写成返回引用更好。
在这里插入图片描述

(2)操作符重载2-非成员函数(不包含this)

1.应对客户的三种方法,写出三种方式(复数+复数 / 复数+实数 / 实数+复数),使用时进行重载。
2.非成员函数是global函数
3.这些函数不能返回引用,因为该类中没有this,需创建一个临时对象接收,该临时对象生命周期在函数调用后便消失,即不能返回引用。
4.typename(),创建一个typename类型的临时对象。
在这里插入图片描述
1.cout不认识新定义的共轭复数,因此也需要对<<进行操作符重载。
2.操作符不能定义为void,定义为void之后不能连续使用<<,,即cout<< a << b;使用引用后cout<< a还是cout,可继续接受b。
在这里插入图片描述

6. 复习complex类的实现过程

(1)函数主体

在h文件中大致想好要做什么,实现什么功能
构造函数及其初始化(初值列) +=操作符 返回取值等
数据尽量设置为私有变量
friend函数,可以直接调取私有变量
像double real(); 这种函数直接类内写出来的,直接是inline function。
在这里插入图片描述

(2)函数外定义

1该函数设计为成员函数,.函数外定义可以通过inline定义为内联函数, +=操作符左边会变,右边不变,右边定义为const,+=左边对象已经创建,不是临时对象,可以直接通过引用传递
2.由于某些考量,返回值通过调用另一个函数进行赋值,也可以直接在该操作符内实现函数功能。
3.实现返回+=运算结果,思路和1相同。
在这里插入图片描述
1.该函数设计成非成员函数,不只是复数+复数情况,可能出现复数+实数等等情况。
2.+为1个对象和另一个对象操作之后赋给某个临时对象,故只能pass by value。
3.类的名称加小括号直接创建临时变量。
在这里插入图片描述
在这里插入图片描述

1.操作符重载不能写成成员函数,否则必须写成c1 << cout形式(即复数this为左值),故写成非成员函数。
2.考虑传引用还是传对象,const等情况,和上面类似。
在这里插入图片描述
最后实现的代码
在这里插入图片描述

7.三大函数:拷贝构造、拷贝赋值、析构函数

(1)string.h 主体框架

接下来学习另一种类,带指针的类,string.h的实现
1.h文件的主体框架和复数相同。针对带有指针的,编译器默认的只是拷贝了指针(相当于两个指针指向同一处),而不是指针指向的数据。因此,如果类中有指针,需要自己重写拷贝构造和拷贝赋值函数。而不带指针的类,系统默认拷贝数据,不需单独重写。
2.string s3(s1);是拷贝构造,s3 = s2;是拷贝赋值。
在这里插入图片描述

(1)拷贝构造、拷贝赋值、析构函数

1.针对字符串的思考:因为字符串的长度未知,不能直接设定一个xx长度的数组,这样会导致内存浪费。因此数据应该是一个指向字符的指针,给出字符串之后,可以动态的调整占用内存
2. 圈出来的第一行是普通构造函数,下面三个则是新加内容
圈出来的第二行是拷贝构造函数,它接受的是它自己这种东西,即参数就是string类。
圈出来的第三行是拷贝复制函数,该操作符接受的也是它自己这种东西,即参数就是string类。
圈出来的第四行是析构函数。这个类对象死亡的时候,会自动调用。

~String是析构函数。这个类对象死亡的时候,会自动调用。
在这里插入图片描述
string的构造函数和析构函数:
1.字符串是一个指针,最后有结束符号\0。如果传入的是0,说明是空字符串,则只有一个结束符号。
2.构造函数,通过new创建一个新对象,同时考虑未设定初值指定为0。
3.析构函数,释放指针指向的内存,不然会内存泄漏。
在这里插入图片描述
编译器默认为浅拷贝,只把指针拷贝过去。如下图令b = a会使得一处数据有两个指针指向(相当于有一个别名),而另一处无指针(内存泄漏),非常危险。
类中有指针,必须写拷贝构造和拷贝赋值,不然会内存泄漏。
在这里插入图片描述
拷贝构造函数:
1.我们需要的是创建足够的空间,并把蓝本的内容拷贝过去(深拷贝)。
2.下图中都是拷贝构造s2,即以s1为蓝本创建新的s2。s2在之前都未创建
在这里插入图片描述
拷贝赋值函数:
1.拷贝赋值(两边都已创建过)。第一步先把左边数据清空,第二步新建右边字符大小相同的数组,第三步把右边字符串内容复制到左边。
2.方框部分进行自我赋值检测,左右是否相同。如果左右相同执行123步骤则会出错。在这里插入图片描述
字符串输出函数:
1.定义为全局函数而不是成员函数,如果定义为成员函数,则操作符左值必须为string,与惯常使用不符。
2.获得字符串的指针来进行输出,get_c_str() 在class主体中已经定义过了,即获取string的指针。
在这里插入图片描述

8.堆,栈与内存管理

(1)创建对象背后的内存逻辑 - 栈?堆?

1.通过普通创建对象c1(stack object)由栈分配空间,存在于某一作用域内(如{ }; 内),创建的是区域型对象
2.通过new创建对象c2(heap object)由堆分配空间,堆是从操作系统提供的全局内存空间中动态分配一块空间。
在这里插入图片描述

(2)三种栈对象(stack objects)和一种堆对象(heap objects)的生命期

1.stack objects
其生命在作用域结束之际结束,会自动地调用析构函数进行释放。
在这里插入图片描述
2.static local objects 和 global objects
其生命在程序结束之际才会调用析构函数进行释放。

在这里插入图片描述
在这里插入图片描述
3.head object
其生命周期在它被delete之际结束。进行动态分配后必须要delete,否则会造成内存泄漏。
在这里插入图片描述

(3)关键字new和delete的解析

以复数为例子对new进行解析
1.分配内存,先调用C语言中的malloc函数进行内存分配,由于复数是设定为double类型,故分配两个double的内存大小。
2.将指针转型,这一块现在讲不清楚且次要的,暂时搁置一下。
3.pc通过指针调用构造函数来初始化,此处pc为隐藏的this。
在这里插入图片描述
以字符串为例子对delete进行解析
delete被编译器转化为两个动作:
1.先调用析构函数将字符串里面那一块杀掉,即释放分配的动态内存。字符串本身只是一个指针,此时还未释放。
2.再调用delete函数,内部使用free来释放,将字符串本身杀掉,即删除该指针。(针对复数那种不带指针的类,析构函数为空,不干事)
在这里插入图片描述

(4)动态分配所需内存大小分析

下面所有内容均在Vc编译器下进行,不同编译器大同小异。
1.在调试模式(Debug)下,①程序Debug所需内存:图中灰色的内存部分(上面32,下面4);②头尾的cookie,用来记录分配空间,③分配的内存都是16的倍数,因此用绿色pad进行填充,填充到64字节。
2.在发布模式(Release)下,没有灰色的,则占用16字节。
cookie:表示使用了多少字节,每一位是4位bit,因为内存必须是16的倍数,因此最后四位bit一定都是0,借用最后的一位1表示占用内存,0表示释放内存。
在这里插入图片描述
动态分配数组:
其他和动态分配一样,区别主要是增加4字节(int类型),用来保存数组的长度,如图中的[3]。
在这里插入图片描述
array new一定要搭配array delete
针对有指针的类:会内存泄漏。因为普通的delete只调用一次析构函数。内存泄漏会发生在剩下两个,因为剩下两个没有调用析构函数。如右图框出来部分。
而如果没有指针的类(比如之前的复数):没有动态分配内存new,因此也就不需要调用自己写的析构函数来杀掉。
在这里插入图片描述

9.复习String类的实现过程

思考:字符串一开始并不知道分配多大的内存空间,用数组的话必须是一个确定的值,故进行动态分配,创建一个私有指针变量。因此需要重写构造函数、析构函数、拷贝构造函数、拷贝赋值函数。注意const的使用。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.扩展补充:类模板,函数模板,及其他

(1)static

1.在数据或函数前加static关键字,则变为静态函数/数据。
2.通过类创建三个复数c1,c2,c3,在内存中即每个复数创建一次。一个成员函数要处理很多个数据,需要靠某个东西告诉他处理谁即(this pointer),如图步骤所示,再通过this找到处理的东西。
3.静态和普通的区别:
static数据不属于对象,在内存中单独储存,仅有一份,且没有指针。即加上static之后,这个数据/函数就不属于这个对象了,跟这个对象脱离。
有什么用?举个例子,银行开户有500人,属于一般数据,而利率这种东西和开户人无关,且每个开户人的利率相同,应该单独存储
static函数没有指针。不能像一般成员函数处理一般数据,只能处理静态数据。
在这里插入图片描述
1.利率mrate是静态数据,在类中的相当于声明,还需再外部进行定义,如用黄色标记的部分,在此处分配空间,是否初始化都可以。
2.set_rate是静态函数,静态函数只能处理静态数据。
3.静态函数可以通过class name来调用,也可以通过对象object来调用。通过对象调用时,无隐藏指针。
在这里插入图片描述
通过static来设计单例模式:
1.构造函数放在private中,不想让外界创建。
2.设计一个静态函数,来返回唯一的那一份,这个静态函数是外界取得这一份的唯一方法。
3.调用这个静态函数之后,才开始创建这唯一的一份,并且之后存在这一份。

在这里插入图片描述

(2)cout

1.cout是一种ostream。
2.ostream设计了很多种<<的操作符重载。
在这里插入图片描述

(3)模板

类模板:
1.使用T来代替某种类型,需写出关键字template< typename T>
2.使用的时候,<>中写明类型,编译器就会把T全部替换为这种类型。
在这里插入图片描述
函数模板:
思考当某个函数功能差不多时,例如最小值。
1.使用T来代替某种类型,需写出关键字template< class T>
2.用的时候不需要用<>绑定类型,编译器会通过参数推导根据传进去的对象类型自动绑定T类型
3.比较的时候,<符号就会使用T类型中重载的<符号来进行,必须提前定义,否则会报错。
在这里插入图片描述

(4)namespace

1.命名空间可全部释放,如释放标准库,using namespace std;
2.命名空间可部分释放,如using std::cout,使用cout时直接使用,其他函数使用时需再加命名空间使用,如std::cin;
3.不释放命名空间释放,直接使用命名空间::函数名
在这里插入图片描述

11.组合与继承

(1)复合 Composition

1.复合表示一种has-a的关系,例如,类A、类B两个类,类A中含有类B。
对于图中的特例,queue里面的所有功能都没有自己写,他都是通过 C 来调用 deque 的成员函数来完成的,所有的功能都在 deque 中已经完成了,而 queue 是借用 deque 已经完成的功能来实现自己的功能。这是 23种设计模式中的 Adapter(改造)

在这里插入图片描述
2.内存大小的计算:
queue中有deque,deque的源代码中,还有另一个复合,Itr,故queue占用40字节。
在这里插入图片描述
3.复合关系下的构造和析构:
对于 composition(复合) 而言,其构造函数是由内而外,析构函数是由外而内。比如说类 A 中拥有类 B ,则类A的构造函数首先调用内部的类 B 的 default(默认) 构造函数,之后才执行自己的构造函数,例如代码中红色的部分,是编译器来完成的,编译器会调用内部的默认的构造函数或析构函数。
当类 B 含有多个构造函数时,此时编译器不知道该调用哪一个构造函数,则此时需要程序设计者在写 composition(复合) 的构造函数时写上具体调用类B的哪一个构造函数。
在这里插入图片描述

(2)委托 Delegation (composition by reference)

1.对于委托而言,可以看做是一种复合 , 但是是使用指针相连,即引用方式的复合,左边has a右边类的指针。可以通过该指针,把任务委托给右边的类。
2.在复合中,内部和外部是一起出现的,即调用二者的构造函数;而委托的话,因为是指针,是不同步的,当需要用到右边的时候,才创建这个。
3.右面的类为具体的实现,左边只是调用的接口。左右边的程序对外不变,左边不用修改,左边只是对外的接口;真正的实现在右边。当左边需要动作时,都是通过调用右边的类来服务的。右边不管怎么变都不影响左边,也就不影响客户端。
在这里插入图片描述

(3)继承 Inheritance

1.继承,表示is-a,是一种什么。父类的数据会被完整继承下来,即子类拥有自己的以及父类的数据。public继承的语法为子类:父类,如class student:public human
2.其内存的形式为子类 (Derived class) 中包含父类 (Base class),需要注意的是Base class 的析构函数必须是虚函数(virture) ,否则会出现undefined behavior。
3.构造时,先调用父类的构造函数,然后再调用自己的。析构时,先析构自己,然后析构父类的。编译器自动完成。
在这里插入图片描述

12.虚函数与多态

(1)虚函数

继承需要搭配虚函数来完成。在任何成员函数之前加上virtual关键字,即为虚函数。子类可以调用父类的函数,即继承了函数(实际上是继承了函数的调用权)。

成员函数有3种: 非虚函数、虚函数和纯虚函数
1.非虚函數(non-virtual function): 不希望子类重新定义(override)的函数。
2.虚函數(virtual function): 子类可以重新定义(override)的函数,且有默认定义。
3.纯虚函數(pure virtual function): 希望子类重新定义它,且目前没有默认定义,一定要去定义。即函数定义后面直接=0。

下图中,定义了一个父类shape,其中定义了几种成员函数。objectID是非虚函数(可以直接决定Id,不需要根据子类情况进行考虑),不需要重新定义。error是虚函数,有默认定义,可以重新定义(有默认笼统报错信息,但子类可以重写更加详细的报错信息)。draw函数是纯虚函数,没有默认定义,必须要子类来重新定义(绘制形状是个抽象的概念,必须由子类定义,如三角形,矩形等)。
在这里插入图片描述

举一个例子:
1.使用虚函数实现框架: 框架的作者想要实现一般的文件处理类,由框架的使用者定义具体的文件处理过程,则可以用虚函数来实现.
2.将框架中父类CDocument的Serialize()函数设为虚函数,由框架使用者编写的子类CMyDoc定义具体的文件处理过程。
3.调用serialize时,通过隐藏的this pointer来调用,因为myDoc.OnFileOpen,因此this就是myDoc,因此调用的是我们override之后的serialize函数。这就是设计模式,template method。

设计思想
流程示意图

4.具体实现伪代码
在这里插入图片描述

13.委托相关设计

委托+继承

一个例子:
思想:通过observer来观察subject的数据。一个subject数据可以有多个observer来观察。observer是一个父类,可以定义子类来继承,因此可以有不同的观察方法。而当数据改变的时候,observer也需要更新,即notify函数,来将目前所有的observer更新。

示意图
伪代码图

设计一种文件系统的类,文件夹中即可以有其他文件,还可以有其他文件夹。
1.primitive是对象个体,composite是一种文件夹容器,特殊点在于放的可能是其他文件对象,也可能是文件夹。
2. 因此把primitive和composite都继承自component,然后composite容器存放的是指向component对象的指针,即委托给Composite。这样composite中存放的可能是文件夹,也可能是文件对象。在这里插入图片描述

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值