4. 参数传递与返回值
刚刚谈到构造函数,我们设计一个class,第一个遭遇的比较大的问题就是,一个构造函数由于它的语法比较特别,所以然后又有一种特殊的初始化叫 initialization list,所以我们要要怎么好好的去善用它呢,刚刚提过了,我们继续下去。
constructor (ctor, 构造函数) 被放在 private 区
这张标题说把构造函数放在 private 的区域里头,在传达什么意思呢,感觉起来怪怪的。如果你把它放到 private 的这个区域,私有区域表示这个函数是不可以被调用,被外界调用。那这样的话,像这样说,外界说我要创建一个这种对象,就构造函数就会被调用起来,可是构造函数又放在 private 不能被构造啊,所以这两个动作都是不可以的。那可见不应该有人把构造函数放在private的区域喽,也不是,既然提出来了,表示会有人这么做。什么情况要这么做呢,就说我不允许被外界创建对象,那这样这个类有什么用。
ctors 放在 private 区
下面是一个经典的写法,这是最简单的一个设计模式 design pattern,如果我们不懂什么是设计模式没有关系,在这设计模式里头有一个叫做 Singleton,它就用到了刚刚我们所说的那一种那种写法,把构造函数放在 private 的里头,我们先看看这段代码,我一个class A, A的构造函数,像这一个例子就写了两个版本,我们刚刚不是说有 function overloading 重载吗,可以有好几个吗,这它就写了两个。它都放在私有区private,所以外界不能够去创建它。那外界需要它怎么办呢,这种 class 既然叫做 singleton,它的意思 singleton 你可能查字典查不到这个字了,你查到single, 查不到 singleton 是个演化的字眼,意思就是单例、单体、单键都可以,意思就是说我设计这个 A 呢,我只要一份, 外界只能用一份,所以它才不允许外界创建,那那一份在哪里呢,那一份在这个黄色的部分,是 static。我们还没有去谈到 static 的特性,所以我现在也不打算跳过去谈它。但我告诉你,它里头自己准备了一份自己,各位看这一行,这个 class A,它这里面有一个自己,然后外界要的时候,不能够用传统的那种创建的方式,要通过 A 这一个class的这个函数叫 getInstance,这名字随便取啊,但是通过这个函数去取得里面它自己的那一份。这里面出现一些到目前为止还没有提过的语法,我们重点不是在这里,重点是告诉各位说,确实有一种需求是把构造函数放到 private 里头,而且这个还很有名,目前的这个的写法叫 singleton ,是有这种可能性的这种需求的。
const member functions (常量成员函数)
好我们再往下看,所构造函数讲完了。我们继续分析,在这一个类的 body 本体里头,还有些什么要注意的呢,我们前面已经讲过了,在本体里头就是有 data 以及函数,就是这两大区域。 现在看到函数的部分,这一件事情是很多很多人会忽略的,包括你可能写了很多年的程序,你都会忽略这个事情,但是它非常重要。就是在函数的后头加 const,一定要现在这个位置,就是小括号的后面,大括号的前面,花括号的前面。我们先认识一下我给的这两个例子,实部虚部,这两个函数是要取得对复数的实部虚部,所以这两个函数会改变这个对象里面的数据吗,不会,它只是把它拿出来而已。如果是要写进去,就改变了实部虚部,但是它只是拿出来。我是要引导你去想一件事情。就是对于类 class 里面的函数,有分为会改变数据的和不会改变数据两种。不会改变数据内容的,马上加上 const,所以 const 函数的意思就是我不改变数据内容,这里面的数据的内容。让大家想一想啊,当我们在写一个类,现在就叫复数嘛啊我写一个类叫做 person 人啊,或者叫做石头 stone,我开始要去想外界可能么怎么样去用这个人,或用这个石头好,所以我要写一些函数,好像写十个函数,这十个函数的第一个,第二个,第三个,这些函数会不会改变数据呢,当你在想这件事情的时候,你的心里头已经有答案了。比如说你只需要把数据拿出来打印出来,这个函数是不会改变数据的,它只是拿数据,当你在想它的逻辑意义的时候,你已经知道它要不要加 const。你后面的定义可能是你这个这个叫做接口界面,接口 interface,你可能想好了以后才交给其它人去定义,定义是后面也许是一个礼拜之后的事情,三天之后的事情,但是你在设计接口的时候,你已经知道要不要加 const,那要加的话就一定要加。不加会有什么后果,我们来看看。左手边这种情况我现在加了 const ,左手边这种情况呢复数,我得我现在创建一个复数是实部为二,复数虚部为一,我通过上面这两个函数,各自去取得实部和取得数虚部,把它打印出来,这在逻辑想当然是 ok ,ok。但是右边这个例子,我如果我的复数是一个常量,注意这里注意这里差,const 可能出现在这个位置上,也可能出现在对象或者叫变量的前面,那意思是说我这一个对象或我这个变量的内容,一定是不动的,不可以改的。然后我再通过这种对象去调用它的函数,万一你没有写 const,那是什么意思。 我们现在正在检讨说这里该加一定要加啊,万一你没有加,而使用者却这么用,会产生什么后果。使用者的第一行的意思是说,我这个值是不可以改变的哦,我这个复数里面二和一这个都不能变。然后我通去调用,我要取得实部,逻辑上应该可以吧,我二一我应该取得二,可是我去调用它的时候,它忘了写 const,那意思就是说我这个函数里面可能会改data。使用者说我绝对不可以改,我调用你这个函数,你这个函数却告诉我说我可能会改变它,这一定是这是矛盾的,所以编译器编到这样,它就会说不行,谁不行呢,你这个函数,你这个东西要调用,它说不行啊,万一是如果你没有写 const 的话,它会说不行,这样使用者是不是很愕然,大吃一惊,我我我有一个复数是二和一,为什么我不能够打印出它的实部呢,那就表示你这个复数设计不好。复数是谁设计的,不就是我们吗,我们现在不是这样设计的复数吗,我们之所以设计不好,就是因为我们这里该加 const,而没有加。很多很多人忽略这个很重要的这件事情,关于 const 用来修饰函数,这样就叫做用它用来修饰这个函数,像这样就是它用来修饰这个变量。它用来修饰函数,这个主题呢还有很多事情要讨论,不过目前这样就可以了,我们就可以进行下去了。我前面不是说希望带大家能够有一个,正规化的写法吗,一出手,就是就是这个完全正规的,正规军作战啊,所以这些小地方,也许如果使用者不像右下角这样用的话,你也不会出错啊,但是我们要要要想得更周全一些。
参数传递:pass by value vs. pass by reference (to const)
我们再往下看,这个主题也是我们在判断你写的代码,你也许可以,你的这个程序也许可以运作,但是你有没有受过正规训练,好的系统训练,我会看123某几个点,这又是其中的一个点。就是你参数的传递是 pass by value,还是有没有用到 pass by reference。那我要先解释 by value 和 by reference 的意思,它的形式是这样,现在这些都是参数,被我黄色圈起来的,这些全部都是参数,所以任何人要调用这个函数,就要把东西传进去,这个叫 pass 传参数。这样就是 pass by value,没有任何特殊符号,那这样就是 pass by reference,有这样有这个这个取地址的这个符号,但是在这里它不是取地址值的意思。标题上说这个叫 pass by reference (to const),就是现在你看的这种情况,那标题也告诉你,也可能不是 const,那就是这种情况,这种情况它前面没有 const,所以标题出现三种情况,在这里都出现了。pass 传递 value 是什么意思,就是整包都传过去,这个 value 多大,它就整包传过去,传的动作其实是压到函数的 stack,栈里头去,但是那个事情我们假设你不知道,我们也不用管它,反正就是整包传过去。所以像现在是double double,是几个 byte 的呢,啊如果是四个 byte,就算是四个字节,就传四个字节。但是你可能将来传的,在其它的写其它的程序的时候,你传的是100个字节或300个字节,可能有这个可能性嘛,所以我们要养成习惯了,尽量不要 pass by value。那怎么办,你还是要把这个东西传过去,怎么办呢,在过去 C 可以传指针哦,我现在要传这个东西,这包东西太大了,我就把这个这包东西的地址传出去,地址就是它的一个指针传出去,指针四个字节啊,传很快。那现在说我有一个东西像指针,但是更漂亮,C++ 的,就是 reference 引用。那引用这个东西,很多人都因为因为你指针你看得到指针,你可以打印出来。引用,你好像好像有点摸不着边,看不到。我现在先给你建立概念,就是引用就是在底部,它就是一个指针,所以纯引用就相当于传指针那么的快。但是它的形式又很漂亮,比如说这样子吧,我们来看看啊。像现在这边有一个例子,这第一行要创建一个复数,这个二和一就会被传进来,这里在设计的时候说我接受的是 by value,它没有特殊符号,所以是 by value。那下面这个例子呢,调用加等于,加等于这个函数后面才会出现,但是它的声明在这里,所以加等于等于是把这个复数传进来,传到这里,那是传引用。因为这里有这个特殊符号,速度很快,就是一个指针的速度,传递指针的速度。那这样子来讲的话,我们都尽量传引用啦,没错,甚至于你传 double,你也给它传引用也可以。所以这边建立一个良好习惯啊,你最好所有的参数传递都传引用,尽量不要传value,传值尽量不要。虽然这句话一讲下去,有的人开始检讨了,那如果说我传一个字符呢,字符只是一个字节而已,那你传引用相当于传指针速度上,相当于传指针是存四个字节,那我还不如就单单传一个字符,我是 pass by value 来传还快一些呢,这个是很细部的讨论的,没有错,这个刚刚在这个说法是对的,可是这样的话也可以哈,你愿意想得这么细也可以。它如果给你一个大范围,给你一个大的一个遵循守则的话,我愿意说大家都不管什么,你都传引用。那么传引用啊,如果你在 C 语言有这方面的经验啊,什么经验呢,你如果传的是指针,一开始会想我指针传过去,那一边那个函数它一改就会影响我了,因为我们是传指针。那传引用也会有这个效果喽,对,我传给你,你这个函数一写完,一改完就影响了我,因为我们其实都同一个东西,效果是这样,是不是你要的,有时候是你要的,有时候不是你要的,这要看情况来说,但如果你知道我传过去只是为了速度,我并不希望你一改就改了我,我可以传引用,而且引用to const。像这样,意思就是说我传一个复数给你哈,我速度很快哦,因为我是传引用,但是我传进去之后,你在这个函数里面,你不可以去改这个内容,因为我传给你呢,我不打算让你改,你一改就会影响我。那如果它里头传过去,它这个函数里面真的改了呢,编译就会出错,在我们双方,我付给你这个条件,你不能改啊,你去偏偏去改,在编译的时候就出错了。好最后做一个结论是什么,在这一章里头啊,参数传递尽量都传引用,如果你想的非常细,某一些变量呢是只有两个字节或一个字节,那时候你想传 value传值也可以。那么你在传引用的时候,你去想,如果传过去不希望对方改,因为对方一改就会影响我,所以我不希望对方改,这种情况之下,加 const, 语法是这样。所以现在看到这个例子,这边有一个例子,这一个函数,虽然我们还没看到,这是一嘛,这是 2-7,这后面才会出现的,后面那个二的部分有一个参数,567有好几段,但是既然这边设计为传进来,它不是 to const,那就表示这个函数理念会对传进来的这个 os,这个东西做改变了,看起来是这样了,的确是这样,不然这边就应该加 const ,它没有加。我们后面会才会去分析这个函数所做的事情,你就会更清楚,为什么第一个传进来是没有const。
返回值传递:return by value vs. return by reference (to const)
reference 这个东西,延续这个话题,主要就是用来做参数传递,还有一个地方就是用来做返回值的传递。返回也要传递啊,那这个术语叫 return,可以是 by value 或者是 by reference,这个地方虽然很小,但是会影响到 C++ 程序的效率。为什么你会来选择学C++,多半是因为你考虑到它的效率,它的效率比其它的面向对象语言的更快,所以我们在每一个影响效率的小节上,我们都要注意把它内化成我们的习惯。这边又要注意一个事了,刚刚说参数的传递尽量 by reference,现在说返回值的传递也尽量 by reference,那尽量当然前提是如果可以的话,有也有不少情况是不可以,我们来谈谈。那这个谈的这个主题会在后面才会出现,现在先告诉你语法。这些都是函数,下面这个这个函数名字好特别啊,你一下子不知道它要表达什么,这个叫 do assign assignment plus,好,后面再说好。这些函数的前头,这黄色的部分就是 return 类型,返回的类型。像这样就是 return by value,前面如果加这个符号 & 就是return by reference,那我们刚刚提过,尽量 return by reference。至于细节后头再来谈。尽量的意思并不是大家没有,既然叫尽量,而不是一定不是大家一想,就是传 reference,是在可以的情况下,一定要注意什么是可以,什么是不可以呢,这个后面来讲。我们一直花了不少时间,一直在这个 class 类的本体上面的一些特征啊,语法上的特征在这边打转,希望把这全部把它们弄清楚。
friend (友元)
这里要介绍的是 friend,friend,被翻译成 友元 。也就是我们日常生活里面的朋友啊。我们身上的钱任何人可以来拿吗,当然不行,朋友可以来拿吗,看你在现实生活中你怎么定义啊,但是在这个语言里头,朋友可以来拿,所以这里面的朋友可以来拿,拿什么呢,我刚刚说钱嘛啊 在这里是指数据。什么意思,我们来看看好,我现在设计的一个类叫做复数,它有实部和虚部,我把实部虚部设计为 private,意思就是我不想让外界,任何人是可以任意的去取我的实部虚部,这是好事情,这叫封装,我们就是要这个效果。外界如果要取实部,虚部,可以通过刚刚的这些函数来取。但是对于特别的一种人,我网开一面,我是设计这个 class 的人。比如说现在对于这一个函数,我们现在不要管它这个函数做什么事情啊,这一个 class 说这个函数是我的朋友,后面这一整行是函数的声明,它是我的朋友。于是这个函数呢,这是下面是它的定义,它做什么事,我们后面再来讲它。你看它的写法就直接去拿了,这边传进来有一个参数叫 ths 是一个复数,就是复数这种东西,传进来,我就说我要拿你这个复数里面的实部啊,你这个复数里面的虚部,直接拿,并没有通过某个函数来拿。为什么可以这样呢,因为前面已经讲了,它们是朋友,这个是 friend 的意思。这你也可以像这个情况的,你不要设计为 friend,因为好像朋友太多,是不是会有困扰,不知道啊,这个你的现实人生是怎么样啊。但是在 C++ 里面是强调封装,朋友那就打开了封装的一个大门,打破了封装,所以你也可以说我不要太多的 friend,那这样的话呢以这个例子来讲,下面这个函数就不再是上面的 friend 咯,那它要拿它的数据就得通过某一个函数来拿它,这样也是解决了问题,它也拿到了,那这样稍微慢一点嘛,因为它是通过函数来拿数据稍微慢一点点,朋友是直接拿,这是你的选择,你要用朋友这种设计或者是非朋友的这种设计。
相同 class 的各个 objects 互为 friends (友元)
有一个问题常常我遇到,被被很多人提起,甚至于很多年的工作经验的,程序员也会提这个问题。它可能在日常生活中已经习惯怎么写的,它知道这样写是对的,可是它解释不出来,我们看看什么问题。我现在黄色的部分我为这个复数呢,一直以来我们好几张投影片都是完全一样的啊,因为这就是后面你会拿到的代码,但现在我增加两行上去啊,你课程结束后拿到的这个代码,你下载的这个代码没有这两行啊,这是我现在要额外说明的。假设我现在设计了一个函数叫 func ,它接收一个复数,现在这个在讲的就是复数,那这个函数也接收一个复数,进来之后,它的做法是它的它在做什么事呢,它却取得传进来这个复数的实部和虚部。就好像说我现在设计了一个东西,我的这个函数是要处理另外一个复数,所以这函数复数传进来了,我要去处理它,但是竟然直接拿,竟然是直接拿实部跟虚无,没有通过函数来拿,这不是破坏了封装性吗,也没有出现 friend 的这个字眼,各位整理一下现在在现在是什么情况。那感觉就好像我这个复数啊做了三个出来,其中的一个直接去拿第三个的实部虚部,这样是可以的,那写了 3年 5年 经验的程序员,它太熟悉了,这样可以,可是它解释不出来,为什么呢,怪怪的。有很多个角度可以去解释,这句话不错,同一个class里面做出来的各个 object 彼此互为 friend,如果这样就就已经很好地解释了,为什么这个行为看起来好像打破了封装啊,但其实它是成立的,这句话,就可以解释。有的人解释这个事情不是用这句话,不是用这个角度解释,也解释的通啊,不过我想这句最好。所以我们看下面这里上面写成了黄色这两行,这一块下面就是用法。你看我在前两行准备两个复数,然后黄色这一行呢,我通过第二个复数的这个函数去处理,第一个复数,而处理复数的时候是直接拿它的实部,虚部,这个事情刚刚讲了嘛,下面只是示范了一个用法。很多人没有弄清楚的一点,在这个事情上面。
class body 外的各种定义 (definitions)
刚刚提到,所以我现在先做一个整理哈,当我自己要设计一个类,或者我看学生来,你交给我一个你写好的类,我会特别注意什么地方呢,复习前面都出现过了。第一,数据一定放在private里头,第二参数尽可能的是以reference来传,要不要加 const 看状况,第三返回值也尽量以 reference 来传,能不能万一不行,那当然就不行啊,那等下现在马上要来讲什么情况不行,但是首先你得先告诉自己,我先考虑用reference引用来传,可不可以。第四,函数在类的本体 body 里头的这些函数应该加 const 就要加,如果不加,可能使用者去用的时候会报错,编译器会报错,它会埋怨你,你就是 class 的设计者啊,你设计的不好。 好现在我也是临时的在现场帮大家整理了,我想到了这几点都是我关注的点,还有一个构造函数,有一个特殊的语法叫 initialization list,因为大冒号的那一行那个要尽量去用它。好在刚刚讲的这些里面呢,提到了 传参数或者是返回值的 by reference。 现在我们来看看什么情况之下,是不能够 return by reference,首先考虑要 return by reference,但什么时候不行。后面会出现,后面这里不就出现了吗,这个函数,但是我没有要讲这个函数里面的动作,所以后面还会再继续讲,我现在只讲,我现在关注它返回东西应该怎么设计,它怎么传。这边告诉各位,它的传进来的两个参数,由于是个加等于的动作,就是跟c是一样的,加等于,所以可以把意思就是比如说 c1 加等于 c2,那就是把 c2 加到 c1 的身上,所以 c1 就是第一参数,加到 c1 身上, c1 是第一参数会改被改变,第二是不会改变的,所以你想一想,在这个函数里头,你加完之后会有一个东西,因为结果跑出来,这个结果要被放在哪里。如果是放在一个本来已经有的空间上面,那就是现在这种情况,现在有一和二两个参数,它加完以后,我要放在一这个参数里面,这一这个参数是空间本来就在的,这是一种情况。但还有一种情况,我举一个例子,比如说, c1 加 c2 现在是加等于哦,我现在说 c1 加 c2,在这个函数加这个函数里头,加完的东西要放在哪里,你不能放在 c1 ,你也不能放在 c2 ,所以你一定是在这个加这个函数里面,是再创建出另外一个东西出来放结果,这是两种不同。整理一下,一个函数的操作结果,运算结果,这个结果如果有地方,它要放在什么位置上呢,如果必须一情况一啊,这函数必须创建一个地方来让它放,这是一种。第二种,像这个它就把它放到某个位置,这里的位置是第一参数放上去,这是第二种。那种必须要在函数里面创建出来的那种情况,那返回的就是新创建出来那个东西了,那个东西在函数一结束,它生命就消失了,就叫 local 变量,local 的对象,这时候你就不能传 reference,不能返回 reference,return by reference。为什么呢,你想想我现在有一个函数,我在里面创建了一个东西,我要把这个东西用引用的方式传出去啊,对你可以传出去,语法都对,但是这个函数一结束,那个东西其实已经死亡了,你却把它的一个引用传出去,它这个本体已经死掉了,外界它再去看的时候,它当然看到了坏东西,看到了不好的东西,所以那种情况之下不可以传引用。除了那种情况,都可以传引用返回引用。那现在这是什么情况,一和二相加之后是要放到一里头去,所以在一本来就在了,这是参数嘛,所以它可以返回引用。参数的传递或返回值的传递,首先考虑引用,然后再考虑用引用有没有问题呢,可不可行呢,才去加这个更进一步的设想。好这张投影片让大家好好的去思考,reference 主要是用在传递东西的身上。好我们再往下,操作符重载是一个很大很大的话题。
总结
constructor (ctor, 构造函数) 被放在 private 区
- 可以把构造函数放在 private 的区域里头
- singleton 单例设计模式
- 设计这个 class,只要一份, 外界只能用一份,所以它才不允许外界创建, 通过类的某个函数去取得里面它自己的那一份
const member functions (常量成员函数)
- 类 class 里面的两种函数
- 会改变数据的
- 不会改变数据(在函数的后头加 const), 原因:定义一个 const 的对象(说明不能改变变量),但是 类 内的函数没有加 const(说明可以改变数据),访问对象的该函数,编译器会迷惑 到底可不可以改变 数据。
参数传递:pass by value vs. pass by reference (to const)
参数的传递分为:
- pass by value: 尽量不要
- pass by reference(& 符号):是传递指针的速度。
- 带 const 的:不改变传过去的 引用
- 不带 const 的:改变传递的引用
最好所有的参数传递都传引用。
返回值传递:return by value vs. return by reference (to const)
返回值的传递也尽量 by reference。
friend (友元)
友元 会破坏封装,可以直接来拿 数据。
相同 class 的各个 objects 互为 friends (友元)
即,A 对象可以直接 拿 B 对象的数据,它们默认就是 友元。
class body 外的各种定义 (definitions)
回顾设计类的原则:
- 数据一定放在 private 里头
- 参数尽可能的是以 reference 来传,要不要加 const 看状况
- 返回值也尽量以 reference 来传,也要知道什么情况不能传 reference
- 函数在类的本体 body 里头的这些函数应该加 const(即 函数名后,大括号前) 就要加,如果不加,可能使用者去用的时候会报错,编译器会报错,它会埋怨你,你就是 class 的设计者啊,你设计的不好。
- 构造函数要使用 initialization list 进行初始化
什么情况之下,返回值 是不能够 return by reference?
- 有一个函数,在里面创建了一个东西,要把这个东西用引用的方式传出去,可以传出去,语法都对,但是这个函数一结束,那个东西其实已经死亡了(局部对象),你却把它的一个引用传出去,它这个本体已经死掉了,外界它再去看的时候,它当然看到了坏东西,看到了不好的东西,所以那种情况之下不可以传引用。比如 对象1 = 对象1 + 对象2,因为对象1 本来就存在,所以可以传引用;而 对象3 = 对象1 + 对象2,对象3 是在 函数内创建的对象,函数结束就会死亡,所以不能传引用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现