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

目录

 


让我们回到刚刚的话题,我们刚刚谈到操作符重载,操作符重载是一个 C++ 的一个很大的特性,要讲的东西也很多。

Header (頭文件) 中的防衛式聲明

我们先回到前面的,有一张投影片啊,我们来看一个东西。这是最早的时候,我再说,一开始,其实我们一直都在进行的,就是在这个头文件,终在防卫式的这个声明之下啊,写这边的东西,那在这一张投影片里面呢,我这边列出来了,就是使用者可能这么去使用,我们现在所设计出来的复数这个 class,我们在这边看,可以看到对于复数的操作,我们都使用这种操作符,因为我们在学校里头学过的复数,也还可以加减乘除嘛,这里面的加减乘除是一样的,一组类似的动作,所以我这边只列出 +、+= 是 C++ 很独特的。我要提示我要引导大家去注意到的,就是对于这种数据,现在这个复数的数据的计算的、运算的操作,都是使用操作符。这个是在其它语言啊,或者在早先的 C 语言呢,你要对一个东西做操作,一定是设计一个函数。事实上在 C++ 里面,操作符就是一种函数,是可以让你重新定义的,这是它很大的一个特点。那因为我们认为对于复数这种东西,比如说你要相加,你如果写一个函数叫 plus,好像还不如你直接用 + 这个符号来做运算,所以 C++ 就说那好吧,自然一点,我允许大家来写操作符。去改变它原来的定义,本来 + 可能只能加整数,现在我们可以拿来加复数,可以拿来加分数,如果你写了一个分数分子分母的分数的话,你也可以这么去做,你甚至可以加两颗石头啊,你写一个 class 叫做石头,那石头怎么相加,你是设计石头的人,由你来决定。现在我就是设计复数的人,我来决定复数要怎么 + ,那我们当然要把数学课本拿出来,说复数是怎么加,我们就把它实现出来好。现在我所以我们现在要来谈操作符重载。

operator overloading (操作符重載-1, 成員函數) this

再回到我们现在要讲的这个画面来,操作符重载。这个标题提醒我们一点,操作符重载有 -1 ,所以等一下会有一个 -2 跑出来。-1 跟 -2 差别在于,成员函数或者是非成员函数。也就是说你有两个方式,可以来写出这个复数的加法,现在呢我要把它写成一个成员函数,也就是在复数的 class 里头,我要去定义这样的一个函数。我们现在好好的分析一下,操作符是怎么被编译器看待的。像这样就是一个操作符,这种叫做二元操作符,两个操作数的有左跟右,所以我们会常常讲左数或右数指的是这个东西。编译器看待这么一行呢,它会把这一个符号作用在左边身上,如果左边这种东西有去对这个符号做定义的话,编译器就找到了,把这一行就定义成要去调用那个函数,那我们现在是焦点放在成员函数身上了。现在我就为复数写这么一个操作符,来应付这一件事情啊,因为我在设计的时候,我想象使用者可能会这么使用,我就写出这个函数出来,这个函数就是这个函数,这个就是这个,但是差别在哪里,我为什么要重新再写一遍呢。我把它隐藏在里面的一个东西挖出来给你看,就是这个灰色的部分,这个故事是这样的,所有的成员函数一定带着一个隐藏的参数,所谓隐藏就是其实你没有写,但是它在,这一个函数叫做 this。为什么取这个名字呢,谁调用我,谁调用这个函数,那个谁就是 this, 调用者。所以你看编译器把这个符号作用在左边,所以左边这个 c2 它就是 this。而 this 是一个什么,是一个指针,所以编译器会自动的为我们把 c2 的地址传进来,当成这个 this pointer,我们写代码不必写也不能写,你要写出来就错了,你不能写。 那这个this point 是我现在把它显示出来,放在第一个参数,一定是这样吗,不一定,不同的编译器行为都不一样啊,有的会把它放到最后一个参数去,反正我们写代码看不到,不影响我们写代码,但是放在哪里都无所谓,反正有这么一个东西。然后在这个 += 这个动作呀, += 是什么意思呢,是要把右边 + 到 左边身上去,所以我们在这里面,这个函数里面一定要看到左边,也要看到右边,不然怎么做操作呢。那现在你所看到的这个例子,又把这个右边 + 到左边身上的这一种动作,再包成另外一个函数放到上面来,其实不包,不把它包成另外一个函数放到上面也可以,你直接写,当然没有问题。现在是多了一层,冲上去,为什么要这么做,在这里你看不出道理,但是你可以感受它可以啊,本来该在这里做了,这样只能拿出来到另外一个地方去做而已。想必是这个函数这种右边 + 到左边的这种动作,大概在其它地方也会用到,所以我们独立出来,让其它地方也可以来调用。所以 右边要 + 到左边,那右边有了就是这个 r,左边也有了,就是这个 this, 隐藏的。 但是 你不能够在参数列写出来,但是你在函数里面可以用它。我们就把左跟右呢传到另外一个函数去,是上面这里,那上面这里要怎么去做,把 右边 + 到左边 去,复数的加法是怎么加的呀,你看右边的实部 + 到左边身上,就这个第一行,右边的虚部也 + 到左边的虚部身上,这就是第二行,复数的加法,就是这样加的,数学课本告诉我们的。加完了以后就是已经改变了左边,因为我们正在最左边这个东西做动作,加完以后把左边这个东西 return 回去,这样就完成了,上面这个 return 回去,return 给谁呢,返返回给谁呢,是下面这个函数调用的呀,所以就返回到下面来,下面又把这个结果再返回来,就执行完了这个动作。这个就是对 += 这个操作符的设计。所有的操作符只要是两个操作数的,有左右两种,这种它的规则都这样,都作用到左边身上然后有一个 this pointer,就是刚刚的故事都是一样的,这种叫操作符重载。这个名字(__doapl)取得很奇怪,我怎么会取这个名字呢,其实我从一开始没有告诉各位,这一段代码是标准库里面的复数的代码,我是把它抓出来,拿掉一些我们用不到的东西,所以我们看的其实是标准库啊,一个很伟大的一个团队, 里面都是很棒的人,它们所写的很正规的一个复数的设计啊。那个团队把这一个函数命名为这有 __doapl 我们会很奇怪它为什么这样命名呢,应该可以想象这个就是 do assignment plus,因为这种 左边 + 到右边,这种 += 的动作叫做 赋值的加法,也就是 assignment plus。所以我们可以推想,它命了这个很奇怪的名称的原因是什么。所以在这个地方好传达了一件事情,任何成员函数都有一个隐藏的,this pointer。 pointer 指向东西,它指向谁呢,就指向调用者,谁调用我这个函数,那个 this 就指向它。这是我们的这一页,讲到操作符重载,我们继续往下看。

return by reference 語法分析:传递者無需知道接收者是以 reference 形式接收

inline complex&
__doapl(complex* ths, const complex& r)
{
...
return *ths;
}

我们再一次回到一个主题,就是 reference。reference 用在传递参数身上,以及返回值的返回的方式身上。有 pass by reference 或者是 return by reference。好我又回到这个话题,我想谈什么事情,因为你可能会对这个写法感到疑惑,这是什么,这是你刚刚看到的那一个 + 的那个动作。这边传进来的左操作数是一根指针,返回的时候呢,这个黄色这个地方的写法是指针所指的东西,所以这个其实是返回一个东西,就一个 object 一个东西。可是返回的时候,它的声明却说返回的是一个 reference,这样你下面用星号上面用这个特殊符号,这样子对吗,对的。你返回的是东西是 value,但是接收端是怎么去接收它,你不必在乎。我们看这一句话,传递者无需知道接收者,接收端是用什么形式来接收,这就是好处。我们为什么要用 reference 的好处。各位想一想,如果你用 pointer 来传, C++ 也允许 pointer 来传东西,你用 pointer 来传的话,传的人必须知道现在传的是 pointer,要有一个特殊符号。但是现在回到画面上来,用 reference 来传的话,传递者无需知道,这个太好了。 看看这个例子,这边。现在我们这这里有一个连串的加法,要再引出另外一个故事,我们先看这个好了。把 c1 传到这个函数去, c1 其实是个 value,它接收的时候是用 reference 来接收,引用来接收,对传递者是无需知道的。说不定设计的人设计的不够大气,不够厉害,设计的人是用 value 来接收,这里,那也没关系,反正都不影响我使用者啊,这是好事情,其实是好事情。所以我们再一次感受,这边这个传递者呢,不需要知道接收者到底是怎么接收的,它也许是 by value 的方式来接收,也许是 by reference 方式来接收,使用者都无所谓,不在乎写法都一样。所以回到这个话题来,当初是我认为大家可能会对这个语法有疑惑,我们再看一次。我们反正返回就是 object,至于返回去接收端,这里就是接收端,它要用什么形式接收,我不管。如果它设计得够好,它就可能像这样用 reference 来接收,那速度就快,但如果它用 value 来接收,速度就慢,但反正我送出去的人都不管。我们好好地体会这一个地方跟这个地方,这个奇怪的符号其实是可以搭配的。好,然后我在这段画面,再引导大家来看另外一个故事,你好好的想这一个动作,c1 加到 c2 身上,加完就完了,这个一加完这一个函数就执行完毕,没有人去在乎返回什么。所以你可能会想,那我这个函数执行完,我在设计这个函数的时候呢,我的反复返回类型就写 void 这里,也可以,没错。如果使用者确实只有这么用的话,你当初这个设计 void 是 ok 的。但是使用者也可能这样子用,如果你能想到这一点,你就会把你的 class 设计的更好一些,它怎么用呢? 因为是从 C语言 上来的,这些程序员习惯或都知道可以这样连串的赋值,所以它也很可能对你的复数,它也这样子连串的做动作。那我们来好好分析一下,这一串是先做什么事情。那这个得查书,我的印象是 c1 先 加 到 c2 身上, c2 再 加 到 c3 身上。所以 c1 加到 c2 身上,这个执行完了,这个就是这个函数,它执行完了之后的东西就很重要了。不能是 void 的,因为这个东西马上还要当成右值, 加到 c3 身上,所以这里就不能设计为 void,要不然的话这一行就通不过。我们在一些操作符重载上面呢,等一下还会出现另外一个例子,也是这样的考量。当使用者没有连串使用的时候,但是当使用者连串使用的时候呢,你要多做一些思考啊。我现在谈的是 += 这个操作符的返回类型,不可以是 void 的,为的是这个原因。

Header (頭文件)的布局

好我们继续往下,这边标出来就是在我要设计我的这个复数的 class 里面有两大段 2-2。

我们很快地回去看看,我们不要迷失了,当初在这边说一个头文件里面,我要设计这一块跟这一块,那 1 这一块呢已经讲完了,这就是类的本体,class 的本体讲完了。我们现在是在讲本体以外的这一段,所以这一段这里面存在的一定都是函数,要不就是这一个类 class 的成员函数,那就是这种全名,要不就是全局函数,global 函数,那它的函数名称一定不会带着 class name 在前面,一定不会。所以我们一看function name,一看 function name 就会知道这是成员函数呢,还是这是全域的函数,现在一直都关注在现在 2 这个区域。

class body 之外的各種定義 (definitions)

回到刚刚那一页,刚才讲到这里,这是 2-1,我们往继续往下 2-2,那这个函数你看它没有带 class的名称,所以这是一个全域函数,global。这函数非常简单,做什么呢,下面是用法。我想要取得某一个复数的实部,或某一个复数的虚部,实现在上面这里。把要注意的第一件事情是,class尽量用 reference 来传速度快,那在这个函数里头呢,就通过这个传进来是 x 去取得实部啊,就得到了这两个函数非常的简单,在技巧上没有特别要注意的地方。

operator overloading (操作符重載-2, 非成員函數) (無 this)

我们再往下看,这里是 2-3 ,就继续看,我们有一大堆的函数要看。这里又出现了操作符重载,刚刚讲的那个形式是 成员函数,现在讲的形式是 -2 非成员函数。我们看看右手边这里,如果使用者这么写的话,它使用你的复数,它这么写的话,c1+c2 编译器就要去找,看看有哪一个函数可以符合这个 + 的动作,它也许先去找成员函数,也许先来找非成员函数,我也不知道它先找谁,但是先找谁都无所谓,因为你不能写两个。你不能写出这两个,然后看它先找谁啊,这样是不行的,你只能写两个里面的。其中一个刚刚已经示范,在 += 那边示范的成员函数的写法,现在看看非成员函数的写法,我们现在应付的是 + 这个动作。你看到了,这里有三个版本,为什么呢,因为我们在设计的时候,认为使用者可能会这样加,复数加复数或者是复数加一个实部,或者是一个实部也就是在这里就 double 值加一个复数。因为数学上告诉我们可以这么加。一个复数可以跟一个实数加在一起。我认为使用者可能会这三种书写法,于是对应的我就必须写出。你看这边讲,为了应付 client客户 的三种可能用法,我们这里要写出三个函数。好这三个函数差别在哪里,因为一一对应嘛,复数加复数,那这里的两个参数就都必须是复数。如果是 复数加 double,这个 5 在这里被视为double, 5会被变成五点,那就传进来复数加 double, 这个函数就是 复数加 double。那如果是 double加复数,所以下面这个函数是 double加复数。我们用三个函数来对应,右边三种可能使用,这是我这一页要传达的。事实上在数学里,数学里头还可以复数加虚数,虚数加复数等等,如果你作为设计者,你要应付使用者的所有这种可能的话,那你还要继续写下去,加你还要继续写下去。好所以对于 非成员函数的这种操作符重载,我们的写法是这样子。跟前面的不同在哪里,前面有 this pointer,现在这个是这个是全局函数,global,它没有 this pointer。

temp object (臨時對象) typename ();

下面這些函數絕不可 return by reference,因為,它們返回的必定是個 local object.

我们继续往下看,画面不同了,但是我还是要讲同样的这三个 + ,我要谈的是它们返回的为什么不是 reference,不是说最优先考量要是 reference 吗,但是这三个蓝色的部分是 return by value, by value 没有 by reference 好,它比较差,比较慢。但为什么这三个不能 by reference 呢,这边写了,绝不可以。为什么,因为它们返回的必定是一个 local object,这句话什么意思,你现在把 左边和右边相加,加的那个东西要放在哪里,目前没有那个东西,先前那个例子是把 右边 + 到左边 身上,而左边已经存在了,现在不是这种情况啊。所以 加好以后要放到哪里去呢,势必要在这个函数里面先创建一个出来,准备放结果。所以是在这个函数里面创建出来的,这三个函数都是这样,那在这里面才创建的,离开这个函数之后,它就死亡了,所以这种东西你不能把 reference 传出去。一传出去之后,这个自己死掉了,那外界要用就用到了错误的东西。所以我们就是这样子在分析一个 函数 ,能不能 return by reference,就是这样子的思考。所以我们得到的结果,我们就可以确定蓝色的这三块一定要 return by value。现在我们来看看,那刚刚不,是说应该在函数里面创建了一个东西吗,这里怎么好像好像没看到,这里有个特殊语法。这是类的名称,complex, 就是一个 typename。typename 后面直接加小括号,对很多人来讲,这个语法是陌生的,其实很,但是这个在标准库里面很常见到,我们必须熟悉它, 一个 typename 直接加小括号,就好像你写 int 直接加小括号。 过去你习惯的是 int i,然后你去用这个 i。现在是不一样。我们现在回到这里来,现在这个你看这是一个 typename 直接加小括号,只不过里头有参数。typename 加小括号就是要创建临时对象什么叫临时对象呢,它临时才要的,我临时才要的,所以我不想给它名称,要临时的嘛,它的生命到下一行就结束了。那下一行就结束了,而且没有名称,这适合我们现在这样子的用法吗,适合。因为我现在只是要创建一个空间,一个东西来放 + 的结果,然后马上 return 返回。所以虽然生命很短,虽然没有名字都无所谓。至于这个临时对象要放什么东西,那就是这三个函数所要做的事情。复数加复数的话,我们就把实部加实部,虚部加虚部,以此类推,比如说你有一个实部, double x 算是实部好了,虚部就是零嘛,这个实部要加复数的实部,虚部是零,零不用加,这里没有加的动作。所以这一页主要传达的是什么是临时对象,然后语法要怎么写。而这种东西临时对象呢,在这个例子是适用于复数的加法,这三个函数身上。你看右边这样也是临时对象,黄色的这一部分,其实这个语法就是左边。它只是这这也是 typename 加小括号,只不过它没有参数,没有参数,那就是默认值了,我们前面已经讲过构造函数有默认值,那这个就是零零。那下面这个呢也是一个临时对象,它的内容是 4 和 5,这两个是没有名称的,上面这两个是有名称的。那下面这两个没名称的,它们的生命呢进行到这里已经没有了,进行到下一行,这两个都不见了。 这个是临时对象,很特殊的一种语法。所谓特殊是其实一般人可能少用啊,但是标准库确实用的很多,所以要注意

class body 之外的各種定義 (definitions)

我们继续往下看,仍然在操作符重载身上。那操作符重载的语法要注意的,其实在前面都已经有代表性的,都讲完了。那么这边又提出两个,这也是 + ,跟刚刚是重复吗,那这个是 - ,刚刚都没出现过。其实这个不是加这个叫正号,这个叫负号,它的用法是这样,像下面这样子。我们的创建了一个复数之后,可以把它取反或者叫反向 negate,数学上有这样的运作。那也可以取正,正号,所以上面的+ 是正号,正跟负。那编译器怎么知道你,你都是这个 + 这个符号,它怎么知道你是这是这时候是正号,那边是加法,这是怎么分呢,看参数就知道了,这个只有一个参数的,在我们的数学符号里头只有一个参数的,那一定是表示正号,不表示它要加,它要加谁呢,所以靠参数的个数来表现出来它们的不同。好这里有个有趣的事情,像负号来讲,你传了一个复数进去,负号表示要做出一个新东西,因此一定在这个函数里面要创建一个空间,来放新的结果,这是刚刚一再强调的。所以这里又是一个临时对象,临时对象, local, 函数内的这个绝对不能 return by reference,所以这个是return by value。这是刚刚的那个逻辑,完全延伸,完全没变。我们是要做正号,做正号就表示都不变都不变,所以并没有一个说有要产生一个新的结果,没有啊,没有这件事情啊,所以并没有所谓的 local object 在这里诞生没有,因此应该可以传回 reference,应该可以。那为什么这里不写呢,这不是你写的吗,不,这不是我写的,这一个我刚刚提过,是标准库里面有确实有这么一个复数,我把它抓出来为大家讲解,这个不是我写的。那你可能会问我,那它为什么写成这样,标准库这么厉害的,这些人会写错吗,那个很难说,它并没有错,它只是写的不是最好,它并没有错,这个结果是不会错的。所以这个等于说带给大家一个习题,我前面提过,你这个课程学完之后,你可以下载,我们有相关的网址,你可以下载这个例子,你可以把这个地方改成 reference,看看它可不可以正确运作,我相信是可以的,这里。标准库那一些很棒的人,它们所写的标准库并不是圣经,不是说它绝无可挑剔之处,不是。好这一页带给我们的是这些东西。

operator overloading (操作符重載), 非成員函數

我们再往下看,继续下去的全部都是操作符重载。主要是因为我们现在设计的这个复数,它是一个数学上的东西,大家都习惯用熟悉的符号来操作它。所以这边有 ==,就 a跟b 等不等,两个复数等不等。那等不等,这件事情呢。用法是像这样,我举了三个例子,复数等不等于另外一个复数,或者复数等不等于某一个double值,或者某一个 double值 等不等于复数,有三种可能。所以我们要写出三个函数来应付它。就这样其它的考量都跟刚刚相同,我们要考量什么,参数的传递快不快,有没有传reference,返回的时候快不快,有没有返回 reference。 这个例子没有返回 reference,那为什么,想想刚刚所讲的话。所以在语法上已经没有新的要讲了,这个是比较两个复数相不相等。

这个也是比较相不相等,这个是不等于,刚刚那个是等于,其它的考量都一样,所以这里也没有新东西要说。

这也是我们整个例子的最后一页,整个程序快要结束了。一个复数的计算其实很多加减乘除,那些三角函数一大堆,我只抽出几个可以代表性,我要讲的语法的部分拿出来讲。我们看看啊,大家印象很深刻的复数会有所谓的共轭复数。共轭复数是什么意思,就是实部相等,虚部正负号相反,这个叫共轭复数。那我们就来设计这么一个函数,conj 共轭复数。那它应该接收一个复数进来了,那我们在这里应该是在里头产生一个新的空间,准备容纳执行结果。这执行结果会是什么呢,实部就是原来的实部,虚部再加一个负号,这是共轭复数,这个在语法上也没有什么独特的地方。用法呢,下面是一个用法啊,我本来有一个复数,我要调用这个函数,那这是成员函数吗,这不是。其实你也可以考虑把它写为成员函数,任何一个操作你都有两种想法,设计为成员函数,或者设计为全局函数,都可以解决问题,也不一定谁就一定比另外一种好,都要看状况。在这个例子里面把共轭复数设计为 global,全局函数。所以用法是这样了,我调用这个函数,把我的参数,我的一个复数传进去,会得到一个结果,那我在这里是把这个结果输出,输出到 cout 的,也就是你的屏幕去。剩下最后一件事情,我们整个例子的最后一件事情要讲,这个操作符的重载有比前面有一些特别的地方,<<,我们再一次分析这个这种既然 <<,就是又要把 右边输出到左边身上,我们再一次分析它的语法。假设你就是编译器,你看到使用者这么用的话,编译器会把这个操作符号作用在左边身上,是不可能作用在右边身上的。 C++ 语法就没有这种作用在右边身上这种语法,所以它会把它作用到左边身上。左边是什么,是 cout ,cout 是什么,是标准库定义的一种东西,我们现在不去深究它,当它你把它想象,它代表屏幕。就要输出到屏幕去,这个是标准库早就写好了,所以你这个符号作用在它身上,也许是 10 年前写好的cout,它不可能去认识你现在才发展出来的复数这种东西,它顶多只能认识,当时既有的那是什么呢,就是 building 的内置的那些类型,它不可能认识新的这种类型。所以对于 << 这个叫做 output operator,我们绝对不要想把这个这一种操作符写成一个成员函数操作符有两种写法,一种是成员函数的写法,一种是非成员函数的写法。现在这两种,本来我说你都可以考虑,但现在我告诉你,对于这种特殊的操作符,你只要选,你只能选一种,就是全局的那一种写法。 global 的那种写法,原因刚刚已经告诉你了。那我们就开始来写了。所以就写出这么一个函数,operator << ,这地方都可以,有没有空格都是一样的(operator<<),前面我提过 C++ 跟 c 的写法,它都是free-form 自由形式的啊,你要加几个空格都无所谓。现在我们来看看这是一个 global 的函数,那它有两个参数,这两个参数就是使用者这么用的时候的 左边和右边。右边必须是一个复数,这我们都知道了,因为我们现在就是要输出一个复数,所以右边也就是第二参数这里是一个复数,很好,而且是传的是引用,速度快,传进来以后呢,只是要输出而已,没有要改内容,所以这边加 const,一切都很好。那第一个参数该怎么确定它,第一个参数就是这个 cout,cout 是个object,是个对象。它是什么 type,也就是它的 classname 是什么呢,那要去查手册,这不是你写的,也不是我写的,标准库的,这要去查手册,查到的结果是 cout 是什么,就是一种 ostream 这种东西,这个叫 output stream,我们现在也不必管这是什么,反正 cout 就是这种东西。所以我们第一参数确定下来了,它必须是这种 type。然后开始考虑了,那东西传进去是要传引用呢还是传值呢,首先考虑传引用,看有没有副作用,是不是可以传引用,可以。要不要加 const,这里第二个考虑在这里不可以加const,为什么,加了 const 表示这个 os, os 代表 ostream,os 传进来之后不可以改。我再讲一次,当你在设计这一块的时候,你如果这里加 const,意思是传进来的这个 os 在这个函数里面不可以被改变。本来你想象中对呀,我根本就没有要改这个传进来的,cout,没有,我没有要改它,也这里也看不出来,我有改它,但实际上是你在这个函数里面,把各种东西往 cout,也就是这里的 os 丢的时候 ,其实每丢任何东西都在改变这个 os 的状态,这可能让你感觉有点抽象。因为毕竟 os 这种 output stream 不是你写的,不过事实是如此。每一次输出都在改变它的状态,它里面的资料,由于这个因素,所以我们设计这个操作符的时候,这里不能加 const,这里要注意。我们是在前面讲了那么多的操作符重载,现在碰到这个操作符很特别。这个想完毕了在这个函数里面要做的事情,你想怎么输出,你就输出。你就怎么输出吧,你现在想把你的复数怎么输出呢。我可能会想要输出复数,有实部虚部,我可能会想要输出 (a,bi),数学上都是这样表示。或者另外一个输出方式:[a b] 。它想这么输出都可以,你想怎么输出,你就怎么输出。现在这个例子我们看看啊,这是标准库提供的一个程序,它是怎么输出的,(a,b) 。这是这一个函数的设计,它想设计成这个样子。这些刚刚我提过这些小括号的输出,实部,逗点的输出都在改变,现在叫 os 的这个东西,也就是 output stream 的状态,只是你可能看不出来。全部输出去之后,现在我们来检讨返回的类型。你想一想这一行,你把一个复数,在这里已经取得它的共轭复数,那还是一个复数。你把它输出到 cout 的去,输出完了之后呢,谁在乎结果,已经输出到屏幕,这就是结果。没有人再去在乎这个函数传回什么东西,don't care,所以按照道理说,这样思考的话呢,这个函数它的返回类型我们可以设计为 void,ok 没问题。但如果你想远一点,你的使用者可能这种连串的输出,有人很习惯这么用,这是延续过去以来的习惯,为了让它能够获得满足,我们就返回的,不能是void。这个输出是什么样的,次序是先输出 c1, 输出完了之后看得到什么,还要能够接收右边这一个复数。所以啊再讲一次,你看把希望丢给 cout,得到的结果,还要再能够继续接收这个 <<。可见这个函数执行完的结果又要是 cout 这种东西,它不能是 void,也不能是其它,它就是还要再传回 cout。这样我们就确定下来了,所以这里呢返回的仍然是 cout 这种东西,就是 output stream。至于返回的是 by value 还是by reference,再思考一次,最优先就是 by reference,然后回来看看,这东西是这个函数里面的 local 的东西吗,不是啊,所以没问题。要不要加 const 返回类型,这个要不要加 const,在这里,不可以加。在这个例子里头,你看 c1 丢给 cout 之后得到的结果,得到的结果还要继续接收右边的的这个参数,又要去改变这个这个 cout 本身,所以 cout 不可以是 const。这个图在这个地方问你可以吗,问的事情就是如果返回类型改为 void 的呢,我们刚刚其实都讨论过了,在这里现在把这个返回类型改成 void,可以吗?当然如果你改成 void 的,你就不要 return 了,这样一并改,可以吗,不行。不行是太武断了啊,如果使用者只是这样用就可以,但是如果使用者是连串输出就不行。所以作为一个好的设计,你当然要去思考使用者可能怎么去用你,你统统都要去满足它。这是我从标准库抽取出来的这一个例子的最后一部分。

你將獲得的代碼

你上完这个课之后,你将会获得的代码是这两个文件。前面我们提过类 class 有两种经典的分类,一种是带指针的,一种是不带指针的,我们现在把不带指针这一部分讲完了。当然这个过程里面还有很多的更细节的部分,或者是联想拉出去的更多的部分,那个我们在后头继续再把它加上去。现在的第一要件就是让大家能够在很短的时间里面,写出一个复数这样的类出来,并且一起手,就是一个很大气的一个程序。

我们回到最前面来啊,最重要的一个观点再做一次整理,然后这一个主题就结束,我们就会进入下一个经典的例子。我来找出最适当能够代表的这个投影片,这是很前面出现过的。我们很快的想一想哦,设计一个 class 要注意什么事情,构造函数的这一个 Initialization list,一定要会用,不会用的话,我会认为你可能训练不够精良,虽然最后结果可能一样,你用或没用结果一样。第二件事情,函数该不该加 const 一定要考虑,该加就要加万,因该加,而你没加,可能会有副作用,跑出来。第三件事情,参数的传递尽量考虑 pass by reference,像这样就是 pass by value,尽量考虑 by reference,而且要不要加 const 要考虑。第四下一件事情 return 的时候 return 这就是return by value,这个就是 return by reference,这都要去考虑。再下一个你的数据,尽可能的几乎没有例外,要放在 private 里头,而你的函数绝大部分要放在 public 里头,因为你的函数主要是被外界调用。这就是我们谈到的复数这个例子所交代的几个,请大家要特别注意。

总结

  • C 语言呢,你要对一个东西做操作,一定是设计一个函数。在 C++ 里面,操作符就是一种函数,是可以让你重新定义的,比如自定义不同对象的 +,更加自然。

operator overloading 的写法有两种:写成 成员函数或者非成员函数。

操作符重载:成员函数 this

  • 所有的成员函数一定带着一个隐藏的参数,所谓隐藏就是其实你没有写,但是它在,这一个函数叫做 this。
  • this 是一个指针,编译器会自动的为我们把 对象 的地址传进来,当成这个 this pointer,我们写代码不必写也不能写(不能写到写到函数参数中),你要写出来就错了。你不能够在参数列写出来 this,但是你在函数里面可以用 this。
  • 所有的操作符只要是两个操作数的,有左右两种,都作用到左边身上**,然后有一个 this pointer。
  • 任何成员函数都有一个隐藏的 this pointer,指向调用者。

传递者无需知道接收者,是用什么形式来接收。

用引用来接收的好处:

如果你用 pointer 来传, C++ 也允许 pointer 来传东西,你用 pointer 来传的话,传的人必须知道现在传的是 pointer,要有一个特殊符号。但是现在回到画面上来,用 reference 来传的话,传递者无需知道。引用来接收,对传递者是无需知道的(传递者可以传值,传指针,传引用,而接收者用引用都可以接收)。所以对使用者来说,传什么都可以调用想调用的函数,返回值也是一样,返回一个 值、指针和引用,返回类型是 引用即可。

  • 操作符重载的连串操作,返回值的类型不能是 void。

操作符重载:非成员函数 無 this

  • 非成员函数的这种操作符重载是 global 函数,没有 this pointer。
  • 返回函数内产生的局部对象,返回值类型不能是引用,只能是 by value。
  • typename(类名) 加小括号 是创建 临时对象。临时对象是临时才要的,不想给它名称,它的生命到下一行就结束了。一般人可能少用,但是标准库确实用的很多。
  • 对于 << 这个叫做 output operator,绝对不要想把这一种操作符写成一个成员函数。因为成员函数的操作符是作用到左边。 x << cout 人们不会这么写。
  • cout 是 ostream 类的对象,输出会改变 cout 这个对象,因此 函数的该参数不能加 const。
  • 因为可能 cout 会连串的输出,因此返回类型不能是 void(如果不需要连串的输出返回值类型可以是 void)。因为连串的输出会改变返回值的状态,因此返回类型不能加 const。

写程序需要注意的几点:

  • 构造函数要用 Initialization list
  • 成员函数要不要加 const,不改变类内的数据就要加上
  • 参数的传递尽量考虑 pass by reference,要考虑加不加 const(不会对传入的数据进行更改就要加上)
  • return 时, 是 return by value,还是 return by reference,都要去考虑。函数内创建的局部对象要 return by value,否则 return by reference。
  • 数据都要放到 private 内,函数大部分放到 public 内。
posted @   Zenith_Hugh  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示

喜欢请打赏

扫描二维码打赏

微信打赏