十四、C++中如何处理多返回值
本部分也是碎碎念,因为这些点都是很小的点,构不成一篇文章,所以本篇就是想到哪个点就写哪个点。
1、C++中如何处理多个返回值
写过python的同学都知道,当你写一个函数的返回时,那是你想返回谁就能返回谁,想返回几个就可以返回几个,几乎是非常的随心所欲了,因为python背后是做了很多很多事情才让你如此肆意的。C++就非常不行了,因为C++本身就很底层,没有更底层的东西去为它做这些事情了,所以所有都得C++自己花式变出来。
我们都知道,C++默认情况下,一个函数是不能返回两个对象的,就是只能返回一个特定的对象,所以更别提返回多个类型的对象了。
那你的意思是python都能完成的事情,C++却完成不了?非也,这两者根本都没有可对比性。C++是底层,就是所有顶层的东西都是底层一生二、二生三、三生万物而呈现出来的百花齐放。底层不存何来万物。所以C++当然也可以实现返回多个对象,只是这个实现过程需要你用你的智慧去实现。
所以在C++中,你要使一个函数返回多个对象或者多种类型的对象,其实是有很多不同的方法可以实现的。下面我就尽量多罗列几种方法吧。
(1)利用结构体实现
我个人也比较推荐这种方法,比较清晰也好理解。
这里想强调的是,只要涉及到多返回值,或多或少都要涉及到复制,只要涉及到复制,性能就是一个绕不开的话题。上述例子中,两个字符串是程序进入main函数后,先开始运行func函数,而运行func函数就先组织参数,组织参数就是在常量区先写入"lyy""liyuanyuan"这两个字符串,然后才开始执行func,func又是在栈内存创建的,而执行func我们是通过引用传参的,这样就少了一次复制。
func函数执行完毕,就是赋值给e。此时就得先找到连续的空间给e,然后把e的3个对象的值初始化成func的返回值。此时必定的复制呀。所以这里想说的是不管你使用哪种方法,你脑子里一定要不断确认是否有性能问题。
(2)通过参数实现
这种方法不需要使用C++提供的特定类(方法),但是稍微有点难懂
通过参数实现一个函数多返回值的方法,稍稍难理解,不过如果你对传值、传址、指针、引用这些基础概念非常清晰的话,其实也不难。
所以这里要强调的是,如果你是通过参数实现的,那你千万可不能传值,传值将不会得到你想要的结果的。因为func函数的返回值变量,我们是先定义到main函数里,然后再把变量的引用(上左图)或者变量的地址(上右图)传给func,然后在func函数体内初始化或者修改或者赋值这些变量的。
如果你传给func是变量的值,那func会在自己体内重新复制一份数据,然后再计算,结果就和main函数中定义的变量完全无关了,所以你就得不到你想要的结果了。
(3)通过数组实现返回相同类型的多个对象
通过数组实现是最难懂的,是考察一个人对C++基本数据类型的理解程度和灵活使用程度。因为C++还兼容了底层的C,所以数组本身就非常复杂,有最基本的C风格数组、array数组、动态数组vector、还有字符串也有自己单独的数组string[],更有很多变异的交叉组合的东西,比如指针数组、数组指针、高维数组、指向数组首元素的指针、指向数组的指针、指向“指向数组的指针”的指针、函数指针、指针函数、函数返回一个指针、指针指向一个函数、用指针当参数传参、还可以和结构体、类等结合,等等,非常晦涩,非常考验你的底层基础理解得有多深入。
针对本小知识点,你要清楚下面三方面:
首先你要明确:C++中的函数是不能返回普通数组的!会报编译错误!那我们又想让函数返回一个普通数组怎么办?我们可以通过返回一个指针来曲线救国,就是要通过直接返回一个指针的方式,来间接返回一个数组。我们知道一个数组的数组名就是这个数组的第一个元素的指针,&数组名就是一个指向数组的指针。所以一是你可以返回这个数组的&数组名(下面的A案例)。二是你可以返回一个指向指针数组名的的指针,就是返回一个指向指针的指针(下面的B案例)。
其次,前面知识点牢固的同学都知道,在C++中,数组可以用C风格的、也可以用array数组。所以你用哪种类型的数组,难度是不一样的。C风格的数组几乎就是内置类型了,非常底层,所以非常难,但效率高。array数组是标准库的东西,已经包装得非常人性化了,虽然简单好用但肯定是要多一点点开销的。我们先展示如何用最原始的C风格的数组(A案例、B案例、D案例),再展示array数组(C案例)。
最后,不管我们是用C风格数组还是array数组,也不管我们是要返回指针数组的数组名还是返回一个指向指针的指针,背后都还是数组,那既然是数组,数组是要求所有元素的数据类型是相同的。即使你的数组是指针数组,也是要求所有元素(指针)指向的对象都是相同类型的。所以下面案例中就不能比如创建两个字符串数组,再创建也给整型,然后再创建一个指针数组,把三者的地址分别放进去!这样是没法赋值指针数组中的元素的!虽然每个元素都是地址,但是也是没法赋值的。所以下面的A、B、C案例我们都用的是字符串类型。就是只能返回某一个类型的多个对象,不能返回多个类型的对象。不像上面的结构体方法、参数方法是可以返回多个类型的多个对象的。
A案例:返回指向指针数组的指针
上面展示的案例中,func函数返回的是一个指向“指针数组”的指针。就是func返回了一个指针,这个指针指向的是一个数组,这个数组中的元素又都是指针,这些元素又指向堆内存中的字符串数组。但是上述案例main函数中调用func函数的做法有些不恰当。因为这种调用方式根本没有任何用处,因为一点func函数调用完毕,“指针数组”就随着func函数的结束而湮灭了,那要一个指向"指针数组"的指针干嘛用啊,一个指向不存在的指针没有任何用处的。所以main函数应该如下写法:
B案例:返回指向指针的指针
这次我在堆上创建指针数组,这样就不会出现调用func完毕后,指针数组消失的情况了(上图方法1)。或者我还可以先在main函数创建一个指针数组,然后当参数传给func,让func给我初始化一下再返回(上图方法2)。当然也可以不用返回(就是下面图D案例)。
C案例+ D案例:
C案例中,虽然s1、s2、array都是在func函数内部创建的,main函数调用了func后,这些对象都还在,那是因为main函数又自己复制了一份数据。由于array库给我们包装了很多东西,所以这里的代码看起来就非常容易理解。
D案例也是非常巧妙的用法,和前面的小标(2)的思路是一样的。
E案例:返回vector数组
C案例中的array是在栈上创建的,vector会把它的底层存储存储在堆上,所以从技术上讲array会更快。
上面给大家展示这么多小案例,也只是一部分,聪明的你应该能想出更多的办法。这里仅仅是为了展示各种用法。用法和用法是没有优劣之分的,每种用法都有其特定的应用场景。只有非常了解每种用法才能灵活应用。此处也是深刻的领略了一下C++的超级灵活性,真是像水的品行一样,可以各种变形、各种绕开、各种组合,利万物而不争、静水深流、强大而温润。如果你对指针、数组、指针数组、数组指针等这些基础概念不清晰的同学,你就会非常懵,花式报错,建议仔细看这篇博文:【C++】深度理解C++数据类型:常量、变量、数组、字符串、指针、函数_c++ 字符串常量-CSDN博客 ,这篇博文是基础中的基础,是C++内置类型的详细说明,你把这些吃透了,这些案例你也就懂了。
(4)通过元组tuple实现多个返回对象
(5)通过Pair实现多个返回对象
十五、C++中的模板templates
本部分讨论C++中的模板templates
在别的语言中,比如java、c、c#等托管语言中,模板类似泛型的概念,但模板比泛型要强大得多。模板有点像宏,而泛型却非常受限于类型系统以及其他很多因素。同时模板也是一个巨大的、复杂的话题,本部分仅仅是浅浅的入门。
1、什么是模板?
模板就是基于你给定的一套规则让编译器为你写代码。或者通俗的说就是,你写个模板,里面抠出一些空,这些空填上不同的东西,就是一个可用的对象。或者我举个例子,比如开发票,其实发票的格式都是一样的,只有抬头、金额、数量等几个要素不一样。你把空发票就可以看出模板,里面的抬头、金额、数量等几个地方是空的,你只要根据不同的客户填上不同的信息,每个客户的发票就开好了。通俗的理解,模板就是那个空发票。你把要填的信息填到对应的空里面,就你生成一张特定客户的发票了。
所以模板就是把代码的某些部分挖掉,然后传给编译器挖掉部分要填的内容,编译器就帮你完成这段代码了。所以说模板就是你给编译器一个套路,然后再给要填的空的答案,编译器就自动帮你完成了。
2、为什么要用模板?
如上左图所示,我只是想写一个func函数,但是我得要允许func函数可以接受各种类型的参数,比如整型、字符串、浮点型等等,那此时我就得写好几个函数重载,如上图A处。而且这些重载函数除了参数类型不一样外,其他地方都一模一样。手动重复写A处这么多差别不大的函数太费劲。如果我写一个模板,其他都写全,就把参数类型空出来,然后我给编译器传入那个空的答案,编译器像填空一样帮我填上就行了。这样我就省力很多了。这就是template诞生的初衷。
说明:cout是可以接受任何基本类型的或者说C++内置类型,就是我们现在正在使用的这些类型。
3、template的语法
上右图B处是template的语法:template单词就表示你定义了一个模板;尖括号里面的typename是模板的参数;T是名字,你可以随便取。
T用在C处。也就是T可以在整个模板代码中使用,来代替任何出现类型的地方。比如如果代码中出现int value,我们就可以写成T value;再比如如果出现string value,我们也可以写成T value。
当你定义了一个模板后(B处),编译器就会在编译期评估这个模板。所以上右图B、C处的代码不是真正的代码,func函数也不是一个真正的函数,它只是模板的一部分。只有当我们实际调用它的时候(D处),func函数才被真正的创建,创建时也是根据我们传入的参数类型,T才被替换,func才被创建出来,并作为源代码的一部分进行编译的。所以,比如MSVC编译器就不会对你不使用的模板错误进行报错,但是比如Clang等一些编译器会报错。
4、模板的工作原理
当我们调用func("hello")时,模板的另一个版本的函数就会被编译器创造(把尖括号中的类型替换T),并复制到模板下面,然后才编译。
所以模板的工作原理就是,当你调用模板中的函数时,编译器就根据你给它的信息,把该填的空都对应填上,并将生成的代码复制到模板后面即可。
模板不仅可以让编译器帮我们写函数,还可以写类。事实上,大量的C++标准模板库同样完全使用了模板,下面我们再展示如何写类的模板:
上面右图我们创造数组的方式,和C++标准模板库创造array数组的方式一样。记不记得array数组也是让我们在尖括号里面提供两个参数,一个是数组的类型,一个是数组的长度。所以模板有点像C++的meta programming(元编程),就是编译器在编译时实际还进行了编程。
了解模板模板的工作原理后,我们自然可以判断,不是什么情况都适合使用模板的,有的个性化非常强的工作,你就还是别使用模板了。如果是一些重复性很强的工作,比如日志系统,比如要写很多类型的重载函数,这样的工作使用模板就是非常合适的。