6. 复习Complex类的实现过程

发布时间 2023-06-09 18:27:43作者: cold_moon

好的欢迎回到现场,我们现在已经学习了,对于一个很经典的一个 class 叫复数的设计,我们已经知道该写些什么样的文件,在这些文件里面应该放什么东西。接下来如果我示范啊,在现场示范写,一边写这个程序,一边把我的思路过程讲出来,对大家会是一个很好的学习的一个历程,但是如果我现场再开始写代码的话呢,会花蛮多的时间啊,万一打错字又要返工,所以我打算用动画的方式来进行进行什么呢,仍然好像我在写代码一样啊,我想到什么,我把它说出来,等于说做一个总复习。再一次提醒大家,我将会付给大家的这一个完整的范例程序了,不是我写的是标准库里面的,我把它找出来拿掉一些,我们没有要讲的部分,所以这是一个标准库的东西,很有参考价值啊,你拿到的东西会比我讲的东西多很多,我现在要再写一次的这个代码,就是我们先前讲过的这些啊,你的翻译程序比这个会多很多,但是最具代表性的我都已经拿出来讲过了,比如说复数里面有加减乘除,我只拿加来讲,要加等于减等于乘,等于除等于我只想加等于这个动作,它还有很多复数的这个三角函数的计算,这个叫都没有在我们的讲解之列,因为我们要谈的代表性的函数的设计啦,参数的传递啦,返回值,返回类型的设计啦,我们都已经带到了。

现在我们开始来写程序,首先一定要养成一个好习惯,要有一个防卫式的一个常数定义,所以我们要写出这些if define的这些事情,这些东西啊,这个名字是由你自己取的,现在你所看到的这个是标准库里面的,所取的名称叫复数 __COMPLEX__ 。这下划线也含在名称里头,接下来我现在要设计一个复数。所以我该怎么办呢,我先写出 class head,然后我们在这个本体里面呢开始做动作,那我第一个考量的是,那复数需要具备什么样的数据,当然这个数据应该放在私有的 private 里头,应该准备什么呢,复数应该有实部虚部,所以,我们就写出了在 private 里头放 实部虚部,这时候我要考量实部虚部,它应该是什么样的类型,我现在考量是 double,当然你也可以想我要 float 也可以,你想我是 int 也可以。然后我开始想我要写哪些函数,这些函数当然是操作在复数身上好,所以函数我现在是对外发表的,所以这个函数应该放在 public 里头。首先任何一个 class 我们都要去想它的构造函数,构造函数 的语法是什么,它是要跟 class 名称相同的一个函数,这是构造函数的名称,是这样子,而且它没有返回类型啊,这样我们就写出来了。那一个构造函数应该接受哪些参数呢,现在这个例子里头它有实部虚部,所以我们就应该写出它有实部虚部,然后我们去考量要不要默认值哦,我现在的设计想要给它一个默认值,所以实部为零,虚部为零。我们还要去考虑这个参数的传递,是 pass by reference 还是 passed by value。现在这边写出来的是 pass by value,在效率上,由于这个是 double 是四个字节,所以它跟传引用 pass by reference,其实是效率是一样的。ok 如果你要把它改成传引用也可以也可以,这边传value也可以好,我们继续往下,那么当我们想到构造函数,我们就要想到构造函数有一个很特别的语法,叫做 initialization list,初值列一定要充分运用这个初值列,所以我现在把它写出来啊,就是这一行初直列的语法很特别,它有一个单冒号初始力,后面要做什么事情呢,我们目前学的这个最简单的这种情况,出直列,既然叫出值嘛,它就是要 设初值啊,多半我们就是要做这个事情,所以现在我就把这个我的实部,虚部设为传进来的,这个是r跟i这样就是一个设初值的动作,我充分运用了这个射出值的这一个时机点,跟这个写法之后,接下来我要考量,那我这个构造函数还要做什么事情,以这个例子来讲,已经不需要再任何做任何事情了,其它的例子呢说不定你要分配一些内存,说不定你要开一个一个一个一个窗口,或者你要开一个文文件都有可能,但是这个例子这样已经够了,这是构造函数。另外一种类型就带着指针的这种类,这种class里头呢,另外还有一些一定要写的特殊的函数,但是这个例子没有啊,那种带着指针的呢,是我们接下来的马上要进行的另外一个经典的例子跟主题。所以那一些很奇怪很特殊的函数不在,不考虑进来,剩下的就是你作为一个设计者,你想设计你你的复数,你想支持什么样的能力,我的想法是哦好,我要支持一个加等于,那其它还有减等于乘等于等等等,我就都可以像这样子把它列出来,我要设计一个加,这是加等于吗,我可能要设计一个加,但在这些函数里面,我们就有两种选择,一种是把它设计为成员函数,一种是非成员函数都可以。现在我的选择是把这一个函数设计为成员函数,所以才会写在这个 class 里头,其它的一些函数,等下我们会看到根本就不在这个class里头,它是一个全局函数,写在外头。等下我们要好好的再用另外一张投影片来表达,这个函数的参数传递以及返回类型,该怎么去定义它。好,我还想为这个和这个类class再准备些什么函数呢,我再准备一个 real 跟一个imaginary取要干什么,我想要取得实部跟取得虚部,所以这个函数的定义就非常的简单了,它的返回类型呢实部虚部都是 double,这个很很好很好的,很快的就定义下来了。然后我们要去想,当我们在设计任何一个函数的时候,它是不是后面要加 const,我们在之前提过,如果这个函数里头不会去改动这一个data的话,我们就应该给它加const,那我们现在去想,我这两个函数只需要取出实部跟取出曲部,它没有要改动这个数据,因此我现在这里我就要加上const,这一点一定要记住,很多人忽略了这一点,这样就差不多了,但是在这个例子里头呢比较特别的是啊,我们后面的发展哈,有一个函数我现在把它打出来,这样的一个函数叫 __doapl,do assignment plus 的意思啊,它里头呢这个是后头才想到的,我只是现在先把它打出来啊,在这个函数里头,由于要去取得这个实部,虚部直接去取得,而它们是私有的,那怎么办呢,所以我在这边在这个class里头,要先对大家宣称说我的朋友,我的 friend,我的友元是这个函数,这样在后头我们开始写这个函数的时候,就可以,这个函数就可以是像朋友一样的,直接去取它的data,啊我们很快的就把这一个class的整个想法跟写作方式全部完成了,接下来要谈的就是这里头的函数,像有一些函数在这里已经写出来了,那它就是 inline function 内联函数,那这个还没有写出来的,要在class的外头去写,那它就我们也可以让它是 inline function ,现在我们来看这个类class的本体之外,我们要怎么注意什么。我第一个要写的是加等于这个动作,这是一个操作符重载,而它是属于这个class里头的,所以整个函数的全名是这样,先把它写出来,这个这里有没有空格无所谓。然后我开始去思考它的参数是什么,加等于一定有一个左边,一定有个右边,由于它是一个成员函数,是作用在左边身上,所以左边这个我们在之前提到,左边就成为一个隐藏的参数,放进来不一定是第一个,也不一定是最后一个都不知道,但是反正它隐藏的无所谓。所以这个加等于左右呢,我们在参数这边只要写右边这个就好了,那右边我们考量啊,我把它命名叫 r,随便你命名,那这里呢嗯首先考量是传引用,所以我就这么写了,要不要加 const 的呢,那我们想想在这个函数里头,右边要加到左边身上,右边是不动的,不动了,所以我们应该加const,好接下来应该考量它的 return type,它它应该返回什么东西,你把右边加到左边去,得到的结果就是左手边这种东西,左手边是什么东西,就是复数,那复数要不要传引用呢,你所传出去的东西如果不是 local object,也就是说不是在这个函数本体里面去,创建出来的,不是,那你就可以传引用,我们现在是右边加到左边身上,左边本来就存在的啊,所以它不是 local object,因此我们这边应该可以传引用,这一个函数是在 class 的本体之外,所以我会想在这个地方我给它加上 inline,它能不能最后真的成为inline,我不知道,这要看编译器啊,但是我让它建议它去变成 inline 好,这是最重要的接口的部分,涉及任何一个函数,最重要的想法就是这样子去思考,接下来是定义的部分,好就写出这个,那么在这一个设计里头,是把加等于的动作不要不在这里做,而是丢给另外一个函数去做,另外一个函数就叫这,我就要开始去想啊,写出这个名称,那么所有的动作都交给那个函数去做,所以就把收到的两个参数呢,也原封不动的往上传,好那我们来看下面这个函数,其实你也可以不要这样设计啊,你可以直接就在这个地方开始去做,右边加到左边的,这一次这个所有的行为可以加坐在这里,但是由于某些考量,所以把它放到上面来做,既然这两个参数都已经确定下来了,所以它们是什么类型呢,上面这一个函数的类型,两个参数也就确定下来了,右边现在这个就是右边啊,所以这里为什么命名为r,右边的意思要放到左边身上,所以左边是会变动的,所以这里不能加 const。好接下来还要考虑什么,永远要去考虑它的 return type,那这个想法已经跟刚刚这个函数一样了啊,这个整个的一个推广过程,因为其实上面这个函数就代表下面这个函数,我们所有的事情都交给上面去做嘛,所以刚刚推想出来返回类型是这样,我们就照抄上来,啊所有的函数呢,尤其像这种小小的函数,我们都会想要让它成为inline,所以讲这一行,好里头开始要做什么,真正要做加等于的动作啊,这个想法非常的简单呐,我们把右边跟啊,把右边的实部加到左边身上,把右边的虚部夹到左边身上,这就完成了,所以最后的结果就是在左手边这边啊,就是这里的 ths 这个于是我们把这个传回去,这个 += 的动作就设计好了。

我们看下一页,还有哪一些函数呢,刚刚举的几个代表性的是它一个成员函数,另外在复数的其它的一些动作呢,我都把它设计为非成员函数,就是全局函数。现在我打算设计一个家,为什么我要把家这一个,当然我现在讲我其实是标准库在做的啊,我是个解说者。为什么我们要把 + 这个动作设计,不设计为成员函数,有一个考量是这样,你复数其实可以加实数,实数也可以加复数,而不只是复数加复数这样而已,它有这种多种变化,所以如果你把加这个动作,设计到复数的这个 class 里头,刚刚有一些动作是要受限,等一下会出现会你会更清楚啊,我们来看看。所以我这个设计为一个全局函数,它就它的全名就没有那个类的名称,class名称就没有了,这是一个全局函数,那家左家也有左边跟右边,在左边跟右边传进来,左边加完右边之后会不会改变内容,不会,它是要放到另外一个地方去,所以它的参数传递应该就最好是这样子传引用,这样才快,而且加const,因为这两个都不会动,未来的这个函数里面它都不会被改变。好我们往下看,加完之后,虽然我们现在还没有去设想它怎么加,但是左加右之后的新的东西,那个结果要放在一个新的位置上,你也不能放在左边身上,你也不能放在右边身上,所以你必须有个新的东西来放它,那么这个结果一定是放在这一个函数的里头,新创建的一个对象,所以它是一个local 的,是local,我们这里就一定不能传引用,这个之前谈过了,所以这边就确定下来要 return by value 啊,这边我们仍然跟刚刚的想法一样,都加inline啊。最重要的任设计任何一个函数,你都要去想这个接口,也就是返回类型,参数类型好,开始做动作,刚刚已经一再重复了,这个函数里头会自己创建一个复数,必须了来接受内容,你在函数里头要创建一个复数,你可以写complex c1,那这个 c1 就是被你新创建出来的,那这里呢我们引入一种新的,比较比较罕见的语法,要创建对象是吗,好,那我就直接在类的名称后面加小括号,类的名称后面加下挂,这样就是创建一个新的object,那等一下我就把这个创建出来的东西返回,这是我们在设计这个过程中的想法。现在要思考的就是这个新创建的复数,它里头可以是可以给初值的,我就利用这样的写法呢,把x跟y的实部相加,x跟y的虚部相加,当成新的复数的初值,一口气把这些所谓一口气是单独这一行啊,啊把这些事情全部完成,这是一个相当漂亮的写法。但是如果没有呃,没有经过正好好的训练的话,可能有的人看不懂这是什么东西,现在我们懂了,这就是一个临时对象。好这是第一个加的动作,是复数加复数,我们又考虑到复数加实数,我们再考虑到实数加复数,这个次序不一样,所以我们写出了三个版本,思考的方式完全一样,如果你只把你把这个加这个动作,设计到复数的 class 里头,那它只能应付这种复数加复数的,它没有办法应付复数加 double 或者是 double加复数

我们再看下一页,现在想把复数丢到 cout 的身上啊,我们去思考使用者可能这样用,那这仍然是一个操作符重载,操作符重载一定是作用在左边这个操作数身上,这样我们才有机会在这里去写这个复数的这个操作符重载,但是如果你这样思考的话,使用者就必须这么写了,把这个符号作用在复数身上。可是使用者怎么可能这样用这一行呢,完全违背她过去的习惯,过去的习惯是什么东西,都是放在右边,cout 的放在左手边,所以这一行应该避免是可以写出来,让使用者这么用,可是使用者会很惊讶,它不喜欢这样,应该避免。所以只好这个操作符重载,操作符重载有两种选择,一种是成为写成成员函数,一种是写成非成员函数,所以现在这种方法已经打叉了,不可以,只好把它写成非成员函数。好我们开始设计了,先把函数名称写出来,它是非成员函数,所以它的全名并没有前面那个class名称,第二个思考的是参数的类型,现在这样的设计是要应付下面这一种用法,所以左边是什么,右边是什么,就可以决定这里的一和二这两个参数,左边是什么,是 cout 的,那那我们得去查手册啊,没有人记得起来它是什么class,那它是什么class,它是属于哪种class呢,哦查到的结果是 ostream,这 ostream 其实很可能是很大的一包东西,你可以想象有很多的自己,所以传引用这是好事情。 第二个参数呢,右手边这个是复数,好我们也传引用,这是好事情,然后要不要加const,再思考再思考一次,现在要把右边丢到左边身上,那右边是不会被改变的,所以这里加const,右边是第二参数,那丢到左边身上,左边这个东西会不会改变的,乍看之下,或者是你基础不够的话,你可能不知道它会不会改变,好像不会改变啊,其实它会改变,它的状态会改变,那这必须是有经验的人,或现在告诉你,或者你查手册手册,你才会知道的好,它会改变。所以 cout 的在这个函数里头被使用的时候,状态一直在改变之中,因此传进来的时候,这里不可以是 const。好我们这样就把这个接口确定下来。那么哎现在已经增加了这一行,为什么增加这一行呢,因为现在要使用的东西叫 ostream ,这不是我们自己设计的,就去查手册哦,它说这个class你要使用这个class,你要 include 这个 file。我们往往哈我们的习惯是,把所有要include的一些头文件呢,放到我们整个文件的最上面,其实不必如此啊,那样的习惯也不错,但你也可以在任意的地方,只要不要再 func 里头,只要是在 func 的外头,你都可以去 include ,就像现在这样子。我一发现哦,这里需要include这个头文件,我在上面呢把它写出来。然后我们再去思考,我要思考的是它的return type,它该传回什么,请看来黄色这边如果你想的,你现在是设计者,你想你的使用者呢,它只是这么用右边丢到左边身上,就像上面这一行,第二行这样子丢完就算了,那你这个可以设计为 void,不在乎。可是由于你的使用者可能这么用连串的丢出,所以黄色这个区块执行完毕之后,应该传回一种东西,可以再接受更右边的这个的输出,所以它应该传回什么呢,它应该再传回像 cout 这种东西吗,所以那 cout 是什么东西,ostream,于是我们把这个确定下来了,我们要返回ostream,这个 ostream 并不是这一个函数的local的object,它是本来就有的,所以可以传回引用。而我们总是循着一样的思想在去检讨参数的,如何传返回值,如何传,好最重要的事情就是把这个接口定义下来。然后就怎么做呢,既然是要输出,你想怎么输出你的复数,你就怎么写,我们现在就像这样子写出(a,b),这样就输输出到你的屏幕上,输出完之后,那干脆我们就把输最后的结果再丢回去好了,再return出去,所以我这边再加一个return,这就是一个完整的故事。我们利用这种方式,动画的方式呢啊一边写代码,一边把我所想到的事情啊,跟各位所有的交代。最后你会获得的是这两个文件啊,你再提醒一次,你获得的会比我现在讲解的多很多,包括那些三角函数,sin cos在对复数的作用啊,极坐标等等很多。