<p>G:\Cpp\2023版C++教程</p>
<p style="margin-left:.0001pt;text-align:center;">C++语言程序设计</p>
<h2 style="text-align:justify;"><strong><strong><strong>第一部分</strong></strong><strong> </strong><strong><strong>基础篇</strong></strong></strong></h2>
<h3 style="text-align:justify;"><strong><strong><strong>一、什么是</strong></strong><strong><strong>C++</strong></strong></strong></h3>
<h4 style="background-color:transparent;text-align:justify;"><strong><strong><strong>1.1 C++ </strong></strong><strong><strong>简介</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">C++ 是一门非常经典的高级编程语言。顾名思义,C++可以看做是C语言的增强版,在C的基础上扩展了更多的功能;最主要的扩展,就是面向对象和泛型编程。</p>
<p style="margin-left:.0001pt;text-align:justify;">因此C++融合了多种不同的编程方式:以C语言为代表的面向过程编程;面向对象编程;以及模板化的泛型编程。</p>
<p style="margin-left:.0001pt;text-align:justify;">可以说,C++一门“大而全”的编程语言,你可以用它实现想要的任何功能;与此同时,学习C++需要掌握的内容也会比较多。</p>
<h5 style="background-color:transparent;text-align:justify;"><strong><strong><strong>1.1.1 C</strong></strong><strong><strong>和</strong></strong><strong><strong>C++</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">20世纪70年代,贝尔实验室的Dennis Ritchie为了开发UNIX操作系统,专门设计了一门结构化的高级语言,这就是大名鼎鼎的C语言。因为是为操作系统设计的语言,它本身是比较底层的,所以C具有低级语言的高运行效率、硬件访问能力,此外又融合了高级语言的通用性。</p>
<p style="margin-left:.0001pt;text-align:justify;">C语言语法清晰,具有非常好的结构化编程的特性。于是C语言快速地统治了底层的系统级编程,并成为了之后几十年内经典的教学语言。</p>
<p style="margin-left:.0001pt;text-align:justify;">C语言编程的整体思路是“过程式”的,也就是说,我们把想让计算机执行的操作按照步骤一步步定义好,然后用C语言写出来;所以我们写的代码,就是一个处理流程的描述。这种方式很容易理解,也可以非常方便地翻译成计算机能懂的机器语言;但是在面对大型项目、代码量非常大时,就会显得杂乱无章,代码的可读性就大大降低了。</p>
<p style="margin-left:.0001pt;text-align:justify;">于是另一种编程方式应运而生,这就是面向对象编程。这种方式的主要思路是先构建“对象”,然后通过定义好的对象行为,实现我们想要的操作。</p>
<p style="margin-left:.0001pt;text-align:justify;">贝尔实验室的 Bjarne Stroustrup(比雅尼·斯特劳斯特鲁普),在20世纪80年代创建了一个新的面向对象语言——C++。</p>
<p style="margin-left:.0001pt;text-align:justify;">名字一目了然,它是基于C的,扩展了C的功能;所以C++是C语言的超集,所有C语言程序都可以在C++的环境下运行。而扩展的部分,主要就是引入了面向对象的特性,并实现了对C的泛型编程支持。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++的出现极大地扩充了C的应用场景,为C语言的长盛不衰提供了很大的助力。所以我们平常看招聘要求的技术栈描述,往往是把C/C++放在一起说的。</p>
<h5 style="text-align:justify;"><strong><strong><strong>1.1.2 C++ </strong></strong><strong><strong>的应用场景</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">C++完全兼容C,具有C面向硬件的特性;此外还拥有面向对象和泛型编程的扩展。所以C++编写的程序运行效率高、功能强大,特别适合用在系统级应用场景上。所以我们经常可以看到,偏向底层、系统的开发,一般用的语言都是C++。</p>
<ol><li>底层硬件,系统编程:JVM的底层,Python解释器的底层,都离不开C/C++的身影;人工智能核心库的代码,也大多是C++写的</li><li>嵌入式开发</li><li>游戏开发</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">当然,除了这些实际应用场景外,由于C/C++是经典的教学语言,因此计算机专业考研、考级、竞赛等场合往往也是把C++作为第一语言的。无论学习还是工作,C++都是一门非常有用的编程语言。</p>
<h4 style="text-align:justify;"><strong><strong><strong>1.2 C++ </strong></strong><strong><strong>标准</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">C++作为一门高级编程语言,在不同的硬件平台上有着良好的可移植性。这意味着我们不需要改动代码,写出来的程序就可以在不同的平台“翻译”成机器能读懂的语言。要实现这个目标,就必须对C++编写的程序设定一些规范,这就是C++的标准。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++之父Stroustrup写过一本《C++编程语言》(The C++ Programming Language),里面有一个参考手册,专门介绍了这门语言的特性和用法。这其实就是最初的C++事实标准。</p>
<p style="margin-left:.0001pt;text-align:justify;">不过真正意义上的标准,还需要专门的组织认证。ANSI(American National Standards Institute,美国国家标准局)在制定了C语言标准之后,在90年代专门设了一个委员会来制定C++的标准,并和ISO(国际标准化组织)一起创建了联合组织ANSI/ISO。1998年,第一个C++国际标准终于出炉了;这个标准在2003年又做了一次技术修订。因此我们一般所说的C++标准,第一版往往被叫做C++ 98/03。</p>
<p style="margin-left:.0001pt;text-align:justify;">跟大多数语言一样,C++也在不停地发展更新。ISO在2011年批准了C++新标准,这可以认为是C++的2.0版本,一般被叫做C++ 11。C ++ 11新增了很多新特性,极大地扩展了C++的语言表达能力。此后在2014年和2017年,又出了两个新版本C++标准,一般叫做C++ 14和C++ 17,不过这两个版本增加的内容并不多;真正意义上的下一个大版本是2020年的C++ 20,它再一次给C++带来了大量的新特性。</p>
<h4 style="text-align:justify;"><strong><strong><strong>1.3 C++ </strong></strong><strong><strong>代码如何运行</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">我们用C++写好的代码,其实就是符合特定语法规则的一些文字和符号。计算机是怎样识别出我们想要做的操作、并正确执行呢?</p>
<p style="margin-left:.0001pt;text-align:justify;">这就需要一个专门的翻译程序,把我们写的源代码,翻译成计算机能理解的机器语言。这个翻译的过程就叫做“编译”,而这个“翻译官”就叫做编译器。所以C++是一门编译型的编程语言,这一点和C是一致的。</p>
<p style="margin-left:.0001pt;text-align:justify;">事实上,C++代码的运行过程跟C程序代码也是一样的,大致可以分为下面几步:</p>
<ol><li>首先编写C++程序,保存到文件中,这就是我们的源代码;</li><li>编译。用C++编译器将源代码编译成机器语言,得到的这个结果叫做目标代码;</li><li>链接。C/C++程序一般都会用到库(library),这些库是已经实现好的目标代码,可以实现特定的功能(比如在屏幕上把信息打印显示出来)。这时我们就需要把之前编译好的目标代码,和所用到的库里的目标代码,组合成一个真正能运行的机器代码。这个过程叫做“链接”,得到的结果叫做可执行代码;</li><li>运行。可执行代码就是可以直接运行的程序,运行它就可以执行我们想要的操作了。</li></ol>
<p style="margin-left:.0001pt;text-align:left;"></p>
<h3 style="text-align:justify;"><strong><strong><strong>二、简单上手——</strong></strong><strong><strong>Hello World</strong></strong></strong></h3>
<h4 style="text-align:justify;"><strong><strong><strong>2.1 </strong></strong><strong><strong>开发环境和工具(</strong></strong><strong><strong>Visual Studio</strong></strong><strong><strong>)</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">写C++程序其实很简单,直接用记事本写好代码,然后用一个编译器做编译运行就可以了;不过这意味这我们得自己保证语法正确,严重影响开发效率。所以实际应用中我们一般都会使用功能更强大的工具,除了提供编译器外,还可以给我们做语法检查和提醒,方便我们调试程序——这就是所谓的“集成开发环境”(IDE)。</p>
<p style="margin-left:.0001pt;text-align:justify;">Windows系统环境下,最普遍、最好用的IDE就是Visual Studio了,这是微软官方的开发工具,功能非常强大。</p>
<p style="margin-left:.0001pt;text-align:justify;">打开Visual Studio的中文版官方网站 Visual Studio: 面向软件开发人员和 Teams 的 IDE 和代码编辑器,点击“下载Visual Studio”按钮,选择最新的免费社区版Community 2022。然后双击运行安装程序VisualStudioSetup.exe。</p>
<p style="margin-left:.0001pt;text-align:justify;">在安装引导程序中,选择自己需要的组件。我们直接选择“使用C++的桌面开发”即可,这个选项会打包安装Windows下C++开发的所有组件。注意不需要选“通用Windows平台开发”,这个还包含了.net平台,是针对C#开发的。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/b6c788c4fca44f2290ce22ef54c139a3.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">点击“安装”,引导程序会自动帮我们下载和安装所有需要的组件,这个过程可能需要花费一些时间。</p>
<p style="margin-left:.0001pt;text-align:justify;">如果选择了“安装后启动”,那么安装完成就会自动运行。开始的界面是登录微软账号,我们可以直接跳过。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/90ee7c43d6be425290f756d4dc1fd40b.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">择开发设置为“Visual C++”,选择自己喜欢的界面主题色,然后点击启动。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/d2c624a882444ca5bfb920d221164ef5.png" />
</p>
<h4 style="text-align:justify;"><strong><strong><strong>2.2 </strong></strong><strong><strong>写一个</strong></strong><strong><strong>Hello World</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">Visual Studio启动之后,我们首先应该创建一个项目。所谓“项目”,就是一个工作任务,需要实现相应的需求。点击“创建新项目”。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/2409fdb840df4712a5b326f25dfe37ae.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">直接选择一个空项目。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/587f83ac8ca9490eaca48f4af6d41f62.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">指定项目名称和保存位置。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/62c1966409404bcf9e7749615a307966.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这里还有一个“解决方案”(Solution)的概念,其实就是一组有关联的项目,共同合作解决一个需求。</p>
<h5 style="text-align:justify;"><strong><strong><strong>2.2.1 </strong></strong><strong><strong>代码编写</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在打开的解决方案界面里,右键点击“源文件”文件夹图标,添加一个新建项。我们要添加的是一个C++文件,命名为HelloWorld,后缀名是.cpp。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/1e618114ac2d4cc2a139083188a3d39f.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">下面就是一段最简单的代码,我们在屏幕上输出Hello World。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">std::cout << "Hello World!" << std::endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以点击工具栏的按钮 </p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/3fdf7e8b28ca405c813291fd769e7de0.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">(快捷键F5),用一个本地的调试器来“调试”代码;所谓的调试,就是查看具体的运行过程,我们可以用它来解决出现的问题。当然也可以点它旁边的三角按钮,这是不调试直接运行(快捷键Ctrl+F5)。</p>
<p style="margin-left:.0001pt;text-align:justify;">结果如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/eaf5ef42dabc482293581cdb842d94dd.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">界面上弹出了一个窗口,显示出了我们想要的信息“Hello World!”。后面还跟着一串信息,这是调试控制台告诉我们,程序已经执行完毕正常退出了。随便一个键,就可以关闭这个窗口。</p>
<h5 style="text-align:justify;"><strong><strong><strong>2.2.2 </strong></strong><strong><strong>代码解读</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">这个简单的程序里,主要包括了这样几部分。</p>
<ol><li>第一行 #include<iostream></li></ol>
<p style="margin-left:.0001pt;text-align:justify;">这是一个预处理指令,告诉编译器我们需要使用一个叫做iostream的库。因为我们需要输出信息,而系统的标准库提供了这样的功能,所以要用#include做一个引入的预处理。</p>
<ol><li>主函数 main()</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">接下来的主体,是一个“主函数”。</p>
<p style="margin-left:.0001pt;text-align:justify;">所谓的函数,就是包装好的一系列要执行的操作,可以返回一个结果。一个C++程序可以包含很多函数,其中一个必须叫做main,它是执行程序的入口。也就是说,当我们运行这个程序的时候,操作系统就会找到这个“主函数”开始执行。</p>
<p style="margin-left:.0001pt;text-align:justify;">main() 的定义形式如下:</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;"><em>statements</em></p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">具体细分,第一行int main()叫做函数头,下面的花括号扩起来的部分叫函数体。函数头定义了函数的名字叫main,前面的int表示返回值是整数类型(integer);后面的括号里面本应该写传入的参数列表,这里是空的。花括号包围的部分就是函数体,里面就是我们要执行的操作。</p>
<ol><li>语句</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">函数体里,每一步操作都是一个“语句”(statement),用分号结尾。我们这里的语句,执行的就是输出Hello World的操作。</p>
<p style="margin-left:.0001pt;text-align:justify;">std::cout << "Hello World!" << std::endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这是一个“表达式”。所谓表达式,一般由多个运算的对象和运算符组成,执行运算之后会得到一个计算结果。在这里,两个连在一起的小于号“<<”就是一个用来输出的运算符。它的使用规则是:左边需要一个“输出流”的对象,也就是输出到哪里;右边是要输出的内容,最简单的就是一个“字符串”,需要用双引号引起来。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以 std::cout << "Hello World!" 的意思就是:将“Hello World!”这串信息,输出到cout这个对象。cout就是一个输出流对象,iostream库里定义了它的功能,接收到信息之后就可以输出显示了。而cout前面的std是所谓的“命名空间”(namespace),主要是为了避免还有别的cout对象重名起冲突。这里的双冒号“::”也是一个运算符,叫做作用域运算符,专门指明了我们用的cout是标准库std中的。如果不想总用双冒号,也可以直接加上一句:</p>
<p style="margin-left:.0001pt;text-align:justify;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:justify;">这样就可以直接用cout,不需要加std::了。</p>
<p style="margin-left:.0001pt;text-align:justify;">输出运算符 << 得到的计算结果,还是它左边的那个输出流对象cout。这样一来,我们就可以在后面继续写入信息信息了。所以后面的 << endl ,其实就是把 endl 这个内容,又写入到cout中输出了。这个endl是一个“操作符”,表示结束一行,并把缓冲区的内容都刷到输出设备。</p>
<ol><li>返回值</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">最后一行语句就是返回一个值。大多数系统中,main的返回值是用来指示状态的。返回0表示成功,非0表示出错,具体值可以用来表示错误类型,这是由系统定义的。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们这里写了return 0,其实不写也是可以的,默认正常运行结束就会返回0。</p>
<h5 style="text-align:justify;"><strong><strong><strong>2.2.3 </strong></strong><strong><strong>注释</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">可以看到,纯粹的代码还是比较抽象的;特别是当代码越来越多、越来越复杂之后,就会变得越来越难理解。所以我们一般会插入一些解释说明的文字,这叫做“注释”。注释不会被执行,对代码的功能没有任何影响。</p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,有两种注释的表示。一种是单行注释,用双斜线“//”,表示以它开始的当前行是注释内容;另一种是多行注释,使用一对“界定符”(/* 和 */),在它们之间的所有内容都是注释。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">/*</p>
<p style="margin-left:.0001pt;text-align:left;"> * 主函数</p>
<p style="margin-left:.0001pt;text-align:left;"> * Hello World</p>
<p style="margin-left:.0001pt;text-align:left;"> */</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 输出一行信息</p>
<p style="margin-left:.0001pt;text-align:left;">std::cout << "Hello World!" << std::endl;</p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>2.2.4 </strong></strong><strong><strong>代码的改进——简单的输入输出</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">我们之前写的代码非常简单,实现了输出Hello World的功能。不过输出显示用的是“调试控制台”,运行完成总会显示一行额外信息,能不能让它更纯粹地运行、不显示多余内容呢?</p>
<p style="margin-left:.0001pt;text-align:justify;">当然可以,调试台输出的信息本身就有提示,只要更改一下VS的设置。要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/bb5aedceec494cefa77792d051d8aba8.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">不过出现了新的问题:再次运行的时候,窗口一闪而过,根本看不清输出了什么。为了查看输出结果,我们还是希望把窗口保持住、不要直接退出,这可以通过在main()函数中增加一句输入语句来实现:</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 输出一行信息</p>
<p style="margin-left:.0001pt;text-align:left;">std::cout << "Hello World!" << std::endl;</p>
<p style="margin-left:.0001pt;text-align:left;">// 等待键盘输入</p>
<p style="margin-left:.0001pt;text-align:left;">std::cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里的cin跟cout刚好相反,它是一个输入流对象。调用它内部的函数get(),就可以读取键盘的输入;等待键盘输入的时候,窗口就会一直开着。这里的键盘输入是以回车作为结束标志的,所以运行看到结果之后,直接敲回车就可以退出了。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/b35d8cc1964341a0b3cd5832cfeca9b2.png" />
</p>
<h4 style="text-align:justify;"><strong><strong><strong>2.3 </strong></strong><strong><strong>编译、链接和运行</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">我们之前写好C++代码之后,是直接在Visual Studio里借助“本地windows调试器”运行的;而如果真正开发一个软件,显然不能总是依赖VS的调试器运行。真正应用中,我们最终要得到一个“可执行文件”,一般以.exe作为扩展名,双击就可以运行程序了。</p>
<p style="margin-left:.0001pt;text-align:justify;">怎样转换得到可执行文件呢?之前已经提到,C++是一种编译型语言,在运行之前需要进行编译和链接。我们现在就用上节写好的Hello World代码,把这个过程具体说明一下。</p>
<p style="margin-left:.0001pt;text-align:justify;">首先我们可以在Visual Studio左侧的“解决方案资源管理器”里,右键点击创建的项目HelloWorld,选择“在文件资源管理器中打开文件夹”,就会进入保存项目的文件夹。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/6cf7f1a549b14897a97125692f3cdf0e.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这里看到的helloworld.cpp,就是我们写好的C++源代码文件。其它的文件都是VS生成的项目文件。另外还有一个x64文件夹,是之前我们在本地进行调试运行时生成的,里面有一个Debug子文件夹,保存了调试运行的相关信息和日志。如果我们右键HelloWorld项目名,然后选择“清理”,Debug里面就只剩下一些日志和空文件了。</p>
<p style="margin-left:.0001pt;text-align:justify;">源代码首先需要编译(compile),得到目标代码。编译器当然是由Visual Studio提供的。我们首先点击一下源代码文件,然后在VS的菜单栏中选择“生成”-> “编译”(快捷键Ctrl+F7),就可以进行编译了。在下方的“输出”窗口内,可以看到编译的结果信息。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/b5c8e89b240d40e88c61fa3562074b59.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">编译完成之后,再回到之前打开的项目文件夹,找到x64下的Debug目录,点进去之后就会发现多了几个文件,除了一些调试工具外,最重要的就是一个helloworld.obj,这就是编译生成的目标代码文件。</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/5476e6d148e64721887be2ae58b5deec.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">目标文件就是计算机能够直接运行的机器码。但是仅有helloworld.cpp源代码转换的机器码还不够。因为我们用到了iostream中的cout和cin对象进行输入输出操作,这就需要把iostream中对应的目标代码也提出来,组合成一个完整的、能直接运行的机器代码。这就是所谓的“链接”(link)过程,结果就会生成一个可执行文件。</p>
<p style="margin-left:.0001pt;text-align:justify;">在VS中,我们可以点击工具栏“生成” –> “生成HelloWorld”(快捷键Ctrl+B);也可以直接右键HelloWorld项目名选择“生成”。在“输出”窗口可以清楚地看到,扩展名为.exe的可执行文件已经生成了。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/4f8f65837d3f42f3a1cb9d4e68f4beed.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">在对应的目录找到这个文件,双击运行,我们会发现跟之前在调试器中的运行结果是一样的,可以直接在窗口中显示“Hello World!”,回车就会退出。这个.exe文件可以复制到任何位置,直接双击运行程序。</p>
<h4 style="text-align:justify;"><strong><strong><strong>2.4 初步认识函数</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">通过一个最简单的Hello World程序,我们已经了解了C++基本的代码风格、简单的输入输出操作,以及程序编译运行的完整过程。利用这些知识我们可以为这个程序增加更多的功能,比如提示用户输入自己的名字XXX,然后显示“Hello, XXX”。</p>
<p style="margin-left:.0001pt;text-align:justify;">代码如下:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 输出一行信息</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 提示输入姓名</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入您的大名:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">// 用一个变量接收键盘输入</p>
<p style="margin-left:.0001pt;text-align:left;">string name;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> name;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 输出欢迎信息</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello, " << name << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 等待键盘输入</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get(); </p>
<p style="margin-left:.0001pt;text-align:left;">// 这里写两次是因为之前输入信息时敲回车确认,会由第一个get捕捉到</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">但是这样代码就比较多了,可读性会变差。解决办法是,我们可以把中间一部分代码“包装”成函数,就像主函数一样。只不过这种函数不是启动直接调用的,而是需要在程序中明确地写出来什么时候调用。</p>
<p style="margin-left:.0001pt;text-align:justify;">代码如下:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 定义一个函数</p>
<p style="margin-left:.0001pt;text-align:left;">void welcome()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入您的大名:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">string name;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> name;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello, " << name << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 调用函数</p>
<p style="margin-left:.0001pt;text-align:left;">welcome();</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 等待键盘输入</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这样每一部分处理逻辑都可以分块包装成函数,主函数的执行过程看起来就简单多了。当然,如果认为一个文件中有太多函数也会影响可读性,我们还可以把它们分开。比如新建一个叫做welcom.cpp的源文件,专门放刚才的welcome函数。而在主函数中,需要额外对它做一个“声明”,表示有这样一个函数,它的实现在另外的文件里。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 声明一个函数</p>
<p style="margin-left:.0001pt;text-align:left;">void welcome();</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 调用函数</p>
<p style="margin-left:.0001pt;text-align:left;">welcome();</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">函数是C++中基本的编程单元,也是“模块化编程”的核心思想,我们还会在后面的章节详细展开。</p>
<h3 style="text-align:justify;"><strong><strong><strong>三、变量和数据类型</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">一段程序的核心有两个方面:一个是要处理的信息,另一个就是处理的计算流程。计算机所处理的信息一般叫做“数据”(data)。</p>
<p style="margin-left:.0001pt;text-align:justify;">对计算机来说,需要明确地知道把数据存放在哪里、以及需要多大的存储空间。在机器语言和汇编语言中,我们可能需要充分了解计算机底层的存储空间,这非常麻烦;而在C++程序中,我们可以通过“声明变量”的方式来实现这些。</p>
<h4 style="text-align:justify;"><strong><strong><strong>3.1 </strong></strong><strong><strong>变量和常量</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">为了区分不同的数据,在程序中一般会给它们起个唯一的名字,这就是所谓的“变量”。在C++中,“变量”其实就是记录了计算机内存中的一个位置标签,可以表示存放的数据对象。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/574054f4c85f47eda9869aa522f724c0.png" />
</p>
<p style="margin-left:.0001pt;text-align:center;"> </p>
<h5 style="text-align:justify;"><strong><strong><strong>3.1.1 </strong></strong><strong><strong>变量的声明和赋值</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">想要使用变量,必须先做“声明”,也就是告诉计算机要用到的数据叫什么名字,同时还要指明保存数据所需要的空间大小。比如:</p>
<p style="margin-left:.0001pt;text-align:justify;">int a;</p>
<p style="margin-left:.0001pt;text-align:justify;">这里包含两个信息:一个是变量的名字,叫做“a”,它对应着计算机内存中的一个位置;另一个是变量占据的空间大小,这是通过前面的“int”来指明的,表示我们需要足够的空间来存放一个“整数类型”(integer)数据。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以变量声明的标准语法可以写成:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>数据类型 变量名;</em></p>
<p style="margin-left:.0001pt;text-align:justify;">变量名也可以有多个,用逗号分隔就可以。</p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,可以处理各种不同类型的数据,这里的int就是最基本的一种“数据类型”(data type),表示一般的整数。</p>
<p style="margin-left:.0001pt;text-align:justify;">当然,如果我们直接在代码中声明一个变量,然后打印输出的话就会报错,因为这个变量没有被“初始化”。也就是说,a这个变量现在可以表示内存中一个位置了,但是里面的数据是什么?这就需要让a有一个“初始值”:</p>
<p style="margin-left:.0001pt;text-align:justify;">int a = 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">这个操作叫做“赋值”。需要说明的是,这里等号“=”表示的是赋值操作,并不是数学上的“等于”。换句话说,我们还可以继续给a赋别的值:</p>
<p style="margin-left:.0001pt;text-align:justify;">int a = 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">a = 2;</p>
<p style="margin-left:.0001pt;text-align:justify;">现在a的值就是2了。a的值可以改变,所以它叫做“变量”。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">扩展知识:</p>
<p style="margin-left:.0001pt;text-align:justify;">C++是一种静态类型(statically typed)语言,需要在编译阶段做类型检查(type checking)。也就是说所有变量在创建的时候必须指明类型,而且之后不能更改。对于复杂的大型程序来说,这种方式更有助于提前发现问题、提高运行效率。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">代码如下:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">a = 2;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "现在 a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">要运行的话,可以右键项目名 -> 设为启动项目,或者右键解决方案 -> 设置启动项目。</p>
<p style="margin-left:.0001pt;text-align:justify;">注意,如果不给初始值,后面再赋值、再使用也是合法的;但一般不能不赋值、直接使用。因为在函数中定义的变量不被初始化,而在函数外部定义的变量会被默认初始化为0值。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.1.2 </strong></strong><strong><strong>标识符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">每个变量都有一个名字,就是所谓的“变量名”。在C++中,变量、函数、类都可以有自己专门的名字,这些名字被叫做“标识符”。</p>
<p style="margin-left:.0001pt;text-align:justify;">标识符由字母、数字和下划线组成;不能以数字开头;标识符是大小写敏感的,长度不限。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以下面的变量名都是合法而且不同的:</p>
<p style="margin-left:.0001pt;text-align:justify;">int b, B, B2, a1_B2;</p>
<p style="margin-left:.0001pt;text-align:justify;">此外,C++中还对变量命名有一些要求和约定俗成的规范:</p>
<ol><li>不能使用C++关键字;</li><li>不能用连续两个下划线开头,也不能以下划线加大写字母开头,这些被C++保留给标准库使用;</li><li>函数体外的标识符,不能以下划线开头;</li><li>要尽量有实际意义(不要定义a、b,而要定义name、age);</li><li>变量名一般使用小写字母;</li><li>自定义类名一般以大写字母开头;</li><li>如果包含多个单词,一般用下划线分隔,或者将后面的单词首字母大写;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">所谓的“关键字”,就是C++保留的一些单词,供语言本身的语法使用。包括:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/1454a6da34a441a49b7333e2422dc085.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">以及C++中使用的一些运算操作符的替代名:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/fd0c4882cca2406ea016170ae23b6ecb.png" />
</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.1.3 </strong></strong><strong><strong>作用域</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">变量有了名字,那只要用这个名字就可以指代对应的数据。但是如果出现“重名”怎么办呢?</p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,有“作用域”(scope)的概念,就是指程序中的某一段、某一部分。一般作用域都是以花括号{}作为分隔的,就像之前我们看到的函数体那样。</p>
<p style="margin-left:.0001pt;text-align:justify;">同一个名字在不同的作用域中,可以指代不同的实体(变量、函数、类等等)。</p>
<p style="margin-left:.0001pt;text-align:justify;">定义在所有花括号外的名字具有“全局作用域”(global scope),而在某个花括号内定义的名字具有“块作用域”。一般把具有全局作用域的变量叫做“全局变量”,具有块作用域的变量叫做“局部变量”。</p>
<p style="margin-left:.0001pt;text-align:justify;">测试代码如下:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 全局作用域,全局变量</p>
<p style="margin-left:.0001pt;text-align:left;">int number = 0;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 块作用域,局部变量</p>
<p style="margin-left:.0001pt;text-align:left;">int number = 1;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 访问局部变量</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "number = " << number << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">// 访问全局变量</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "number = " << ::number << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">如果在嵌套作用域里出现重名,一般范围更小的局部变量会覆盖全局变量。如果要特意访问全局变量,需要加上双冒号:: ,指明是默认命名空间。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.1.4 </strong></strong><strong><strong>常量</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">用变量可以灵活地保存数据、访问数据。不过有的时候,我们希望保存的数据不能更改,这种特殊的变量就被叫做“常量”。在C++中,有两种方式可以定义常量:</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)使用符号常量</p>
<p style="margin-left:.0001pt;text-align:justify;">这种方式是在文件头用 #define 来定义常量,也叫作“宏定义”。</p>
<p style="margin-left:.0001pt;text-align:justify;">#define ZERO 0</p>
<p style="margin-left:.0001pt;text-align:justify;">跟#include一样,井号“#”开头的语句都是“预处理语句”,在编译之前,预处理器会查找程序中所有的“ZERO”,并把它替换成0。这种宏定义的方式是保留的C语言特性,在C++中一般不推荐。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)使用const限定符</p>
<p style="margin-left:.0001pt;text-align:justify;">这种方式跟定义一个变量是一样的,只需要在变量的数据类型前再加上一个const关键字,这被称为“限定符”。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 定义常量</p>
<p style="margin-left:.0001pt;text-align:left;">const int Zero = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">// 不能修改常量值</p>
<p style="margin-left:.0001pt;text-align:justify;">//Zero = 10;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">const修饰的对象一旦创建就不能改变,所以必须初始化。</p>
<p style="margin-left:.0001pt;text-align:justify;">跟使用 #define定义宏常量相比,const定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。</p>
<h4 style="text-align:justify;"><strong><strong><strong>3.2 </strong></strong><strong><strong>基本数据类型</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">定义变量时,不可或缺的一个要素就是数据类型。本质上讲,这就是为了实现计算需求,我们必须先定义好数据的样式,告诉计算机这些数据占多大空间,这就是所谓“数据类型”的含义。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++支持丰富的数据类型,它内置了一套基本数据类型,也为我们提供了自定义类型的机制。</p>
<p style="margin-left:.0001pt;text-align:justify;">接下来我们先介绍基本数据类型,主要包括算术类型和空类型(void)。其中算术类型又包含了整型和浮点型;而空类型不对应具体的值,只用在一些特定的场合,比如一个函数如果不返回任何值,我们可以让void作为它的返回类型。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.1 </strong></strong><strong><strong>整型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">整型(integral type)本质上来讲就是表示整数的类型。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们知道在计算机中,所有数据都是以二进制“0”“1”来表示的,每个叫做一位(bit);计算机可寻址的内存最小单元是8位,也就是一个字节(Byte)。所以我们要访问的数据,都是保存在内存的一个个字节里的。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/a03993dc164f42b6a6d816bd6c36056c.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">一个字节能表示的最大数是28 = 256,这对于很多应用来讲显然是不够的。不同的需求可能要表示的数的范围也不一样,所以C++中定义了多个整数类型,它们的区别就在于每种类型占据的内存空间大小不同。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++定义的基本整型包括char、short、int、long,和C++ 11新增的long long类型,此外特殊的布尔类型bool本质上也是整型。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/1434033560b94f60868eece9468e2291.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中对它们占据的长度定义比较灵活,这样不同的计算机平台就可以有自己的实现了(这跟C是一样的)。由于char和bool相对特殊,我们先介绍其它四种。C++标准中对它们有最小长度的要求,比如:</p>
<ol><li>short类型至少为16位(2字节)</li><li>int至少2字节,而且不能比short短</li><li>long至少4字节,而且不能比int短</li><li>long long至少8字节,而且不能比long短</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">现在一般系统中,short和long都选择最小长度,也就是short为16位、long为32位、long long为64位;而int则有不同选择。我们一般使用的电脑操作系统,比如Windows 7、Windows 10、Mac OS等等的实现中,int都是32位的。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以short能表示的数有216 = 65536 个,考虑正负,能表示的范围就是-32768 ~ 32767;而int表示的数范围则为 - 231 ~ 231 - 1。(大概是正负20亿,足够用了)</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">short a = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的长度为:" << sizeof(a) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int b;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "b的长度为:" << sizeof(b) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">long c;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "c的长度为:" << sizeof(c) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">long long d;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "d的长度为:" << sizeof(d) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里我们用到了sizeof,这是一个运算符,可以返回某个变量占用的字节数。我们可以看到,变量占用的空间大小只跟类型有关,跟变量具体的值无关。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.2 </strong></strong><strong><strong>无符号整型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">整型默认是可正可负的,如果我们只想表示正数和0,那么所能表示的范围就又会增大一倍。以16位的short为例,本来表示的范围是-32768 ~ 32767,如果不考虑负数,那么就可以表示0 ~ 65535。C++中,short、int、long、long long都有各自的“无符号”版本的类型,只要定义时在类型前加上unsigned就可以。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">short a = 32768;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的长度为:" << sizeof a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">unsigned short a2 = 32768;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a2 = " << a2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "a2的长度为:" << sizeof a2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">上面的代码可以测试无符号数表示的范围。需要注意,当数值超出了整型能表示的范围,程序本身并不会报错,而是会让数值回到能表示的最小值;这种情况叫做“数据溢出”(或者“算术溢出”),写程序时一定要避免。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">由于类型太多,在实际应用中使用整型可以只考虑三个原则:</p>
<ol><li>一般的整数计算,全部用int;</li><li>如果数值超过了int的表示范围,用long long;</li><li>确定数值不可能为负,用无符号类型(比如统计人数、销售额等);</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.3 char</strong></strong><strong><strong>类型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">如果我们只需要处理很小的整数,也可以用另外一种特殊的整型类型——char,它通常只占一个字节(8位)。不过char类型一般并不用在整数计算,它更重要的用途是表示字符(character)。</p>
<p style="margin-left:.0001pt;text-align:justify;">计算机底层的数据都是二进制位表示的,这用来表示一个整数当然没有问题,可怎么表示字母呢?这就需要将常用的字母、以及一些特殊符号对应到一个个的数字上,然后保存下来,这就是“编码”的过程 。</p>
<p style="margin-left:.0001pt;text-align:justify;">最常用的字符编码集就是ASCII码,它用0~127表示了128个字符,这包括了所有的大小写字母、数字、标点符号、特殊符号以及一些计算机的控制符。比如字母“A”的编码是65,数字字符“0”的编码是48。</p>
<p style="margin-left:.0001pt;text-align:justify;">在程序中如果使用char类型的变量,我们会发现,打印出来就是一个字符;而它的底层是一个整数,也可以做整数计算。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">char ch = 65;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "65对应的字符为:" << ch << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">char ch2 = ch + 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "66对应的字符为:" << ch2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">char类型用来表示整数时,到底是有符号还是无符号呢?之前的所有整型,默认都是有符号的,而char并没有默认类型,而是需要C++编译器根据需要自己决定。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以把char当做小整数时,有两种显式的定义方式:signed char 和 unsigned char;至于char定义出来的到底带不带符号,就看编译器的具体实现了。</p>
<p style="margin-left:.0001pt;text-align:justify;">另外, C++还对字符类型进行了“扩容”,提供了一种“宽字符”类型wchar_t。wchar_t会在底层对应另一种整型(比如short或者int),具体占几个字节要看系统中的实现。</p>
<p style="margin-left:.0001pt;text-align:justify;">wchar_t会随着具体实现而变化,不够稳定;所以在C++11新标准中,还为Unicode字符集提供了专门的扩展字符类型:char16_t和char32_t,分别长16位和32位。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/36714611634c4e4baaeed6bca94f70b1.png" />
</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.</strong></strong><strong><strong>4</strong></strong><strong><strong> bool</strong></strong><strong><strong>类型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在程序中,往往需要针对某个条件做判断,结果只有两种:“成立”和“不成立”;如果用逻辑语言来描述,就是“真”和“假”。真值判断是二元的,所以在C语言中,可以很简单地用“1”表示“真”,“0”表示“假”。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++支持C语言中的这种定义,同时为了让代码更容易理解,引入了一种新的数据类型——布尔类型bool。bool类型只有两个取值:true和false,这样就可以非常明确地表示逻辑真假了。bool类型通常占用8位(1个字节)。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">bool bl = true;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "bl = " << bl << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "bool类型长度为:" << sizeof bl << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以看到,true和false可以直接赋值给bool类型的变量,打印输出的时候,true就是1,false就是0,这跟C语言里的表示其实是一样的。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.</strong></strong><strong><strong>5</strong></strong><strong> </strong><strong><strong>浮点类型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">跟整数对应,浮点数用来表示小数,主要有单精度float和双精度double两种类型,double的长度不会小于float。通常,float会占用4个字节(32位),而double会占用8个字节(64位)。此外,C++还提供了一种扩展的高精度类型long double,一般会占12或16个字节。</p>
<p style="margin-left:.0001pt;text-align:justify;">除了一般的小数,在C++中,还提供了另外一种浮点数的表示法,那就是科学计数法,也叫作“E表示法”。比如:5.98E24表示5.98×1024;9.11e-31表示9.11×10-31。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 浮点类型</p>
<p style="margin-left:.0001pt;text-align:left;">float f = 3.14;</p>
<p style="margin-left:.0001pt;text-align:left;">double pi = 5.2e-3;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "f = " << f << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "pi = " << pi << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这就极大地扩展了我们能表示的数的范围。一般来讲,float至少有6位有效数字,double至少有15位有效数字。所以浮点类型不仅能表示小数,还可以表示(绝对值)非常大的整数。</p>
<p style="margin-left:.0001pt;text-align:justify;">(float和double具体能表示的范围,可以查找float.h这个头文件)</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.6 字面值常量</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">我们在给一个变量赋值的时候,会直接写一个整数或者小数,这个数据就是显式定义的常量值,叫做“字面值常量”。每个字面值常量也需要计算机进行保存和处理,所以也都是有数据类型的。字面值的写法形式和具体值,就决定了它的类型。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)整型字面值</p>
<p style="margin-left:.0001pt;text-align:justify;">整型字面值就是我们直接写的一个整数,比如30。这是一个十进制数。而计算机底层是二进制的,所以还支持我们把一个数写成八进制和十六进制的形式。以0开头的整数表示八进制数;以0x或者0X开头的代表十六进制数。例如:</p>
<ol><li>30 十进制数</li><li>036 八进制数</li><li>0x1E 十六进制数</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">这几个数本质上都是十进制的30,在计算机底层都是一样的。</p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,一个整型字面值,默认就是int类型,前提是数值在int能表示的范围内。如果超出int范围,那么就需要选择能够表示这个数的、长度最小的那个类型。</p>
<p style="margin-left:.0001pt;text-align:justify;">具体来说,对于十进制整型字面值,如果int不够那么选择long;还不够,就选择long long(不考虑无符号类型);而八进制和十六进制字面值,则会优先用无符号类型unsigned int,不够的话再选择long,之后依次是unsigned long、long long和unsigned long long。</p>
<p style="margin-left:.0001pt;text-align:justify;">这看起来非常复杂,很容易出现莫名其妙的错误。所以一般我们在定义整型字面值时,会给它加上一个后缀,明确地告诉计算机这个字面值是什么类型。</p>
<ol><li>默认什么都不加,是int类型;</li><li>l或者L,表示long类型;</li><li>ll或者LL,表示long long类型;</li><li>u或者U,表示unsigned无符号类型;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">我们一般会用大写L,避免跟数字1混淆;而u可以和L或LL组合使用。例如9527uLL就表示这个数是unsigned long long类型。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)浮点型字面值</p>
<p style="margin-left:.0001pt;text-align:justify;">前面已经提到,可以用一般的小数或者科学计数法表示的数,来给浮点类型赋值,这样的数就都是“浮点型字面值”。浮点型字面值默认的类型是double。如果我们希望明确指定类型,也可以加上相应的后缀:</p>
<ol><li>f或者F,表示float类型</li><li>l或者L,表示long double类型</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">这里因为本身数值是小数或者科学计数法表示,所以L不会跟long类型混淆。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/9f6e5b4462ee45699c85fc8887a454aa.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)字符和字符串字面值</p>
<p style="margin-left:.0001pt;text-align:justify;">字符就是我们所说的字母、单个数字或者符号,字面值用单引号引起来表示。字符字面值默认的类型就是char,底层存储也是整型。</p>
<p style="margin-left:.0001pt;text-align:justify;">而多个字符组合在一起,就构成了“字符串”。字符串字面值是一串字符,用双引号引起来表示。</p>
<ol><li>‘ A ’ 字符字面值</li><li>“Hello World!” 字符串字面值</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">字符串是字符的组合,所以字符串字面值的类型,本质上是char类型构成的“数组”(array)。关于数组的介绍,我们会在后面章节详细展开。</p>
<ul><li>转义字符</li></ul>
<p style="margin-left:.0001pt;text-align:justify;">有一类比较特殊的字符字面值,我们是不能直接使用的。在ASCII码中我们看到,除去字母、数字外还有很多符号,其中有一些本身在C++语法中有特殊的用途,比如单引号和双引号;另外还有一些控制字符。如果我们想要使用它们,就需要进行“转义”,这就是“转义字符”。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++中规定的转义字符有:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/1edbc30784814acf9653dd6bc0eeb0ba.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">其中,经常用到的就是符号中的问号、双引号、单引号、反斜线,还有换行符和制表符。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 转义字符</p>
<p style="margin-left:.0001pt;text-align:left;">char tchar = '\n';</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "tchar = " << tchar << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World!\t\"Hello C++!\"" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(4)布尔字面值</p>
<p style="margin-left:.0001pt;text-align:justify;">布尔字面值非常简单,只有两个:true和false。</p>
<h5 style="text-align:justify;"><strong><strong><strong>3.2.7 类型转换</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">我们在使用字面值常量给变量赋值时会有一个问题,如果常量的值超出了变量类型能表示的范围,或者把一个浮点数赋值给整型变量,会发生什么?</p>
<p style="margin-left:.0001pt;text-align:justify;">这时程序会进行自动类型转换。也就是说,程序会自动将一个常量值,转换成变量的数据类型,然后赋值给变量。</p>
<p style="margin-left:.0001pt;text-align:left;">// 1. 整数值赋给bool类型</p>
<p style="margin-left:.0001pt;text-align:left;">bool b = 25; // b值为true,打印为1</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 2. bool类型赋值给算术整型</p>
<p style="margin-left:.0001pt;text-align:left;">short s = false; // s值为0</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 3. 浮点数赋给整数类型</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 3.14; // i值为3</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 4. 整数值赋给浮点类型</p>
<p style="margin-left:.0001pt;text-align:left;">float f = 10; // f值为10.0,打印为10</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 5. 赋值超出整型范围</p>
<p style="margin-left:.0001pt;text-align:left;">unsigned short us = 65536; // us值为0</p>
<p style="margin-left:.0001pt;text-align:left;">s = 32768; // s值为-32768</p>
<p style="margin-left:.0001pt;text-align:justify;">转换规则可以总结如下:</p>
<ol><li>非布尔类型的算术值赋给布尔类型,初始值为0则结果为 false , 否则结果为true ;</li><li>布尔值赋给非布尔类型,初始值为 false 则结果为0,初始值为 true 则结果为1;</li><li>浮点数赋给整数类型,只保留浮点数中的整数部分,会带来精度丢失;</li><li>整数值赋给浮点类型,小数部分记为0。如果保存整数需要的空间超过了浮点类型的容量,可能会有精度丢失。</li><li>给无符号类型赋值,如果超出它表示范围,结果是初始值对无符号类型能表示的数值总数取模后的余数。</li></ol>
<ol><li>给有符号类型赋值,如果超出它表示范围,结果是未定义的( undefined )。此时,程序可能继续工作,也可能崩溃。</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">C++中的数据类型转换,是一个比较复杂的话题。我们这里先了解一下变量赋值时的自动类型转换,关于更加复杂的转换,我们会在下一章继续介绍。</p>
<h3 style="text-align:justify;"><strong><strong><strong>四、运算符</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">有了数据之后,就可以对数据对象进行各种计算了。在编程语言中,可以通过“运算符”来表示想要进行的计算。</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.1 </strong></strong><strong><strong>表达式和运算符</strong></strong></strong></h4>
<h5 style="text-align:justify;"><strong><strong><strong>4.1.1 基本概念</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在程序中,一个或多个运算对象的组合叫做“表达式”(expression),我们可以把它看成用来做计算的“式子”。对一个表达式进行计算,可以得到一个结果,有时也把它叫做表达式的值。</p>
<p style="margin-left:.0001pt;text-align:justify;">前面讲到的字面值常量和变量,就是最简单的表达式;表达式的结果就是字面值和变量的值。而多个字面值和变量,可以通过一些符号连接组合在一起,表示进行相应的计算,这就可以得到更加复杂的表达式,比如 a + 1。像“+”这些符号就被叫做“运算符”(operator)。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++中定义的运算符,可以是像“+”这样连接两个对象,称为“二元运算符”;也可以只作用于一个对象,称为“一元运算符”。另外,还有一个比较特殊的运算符可以作用于三个对象,那就是三元运算符了。</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.1.2 运算</strong></strong><strong><strong>优先级和结合律</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">如果在一个表达式中,使用多个运算符组合了多个运算对象,就构成了更加复杂的“复合表达式”,比如 a + 1 - b。对于复合表达式,很显然我们应该分步来做计算;而计算顺序,是由所谓的“优先级”和“结合律”确定的。</p>
<p style="margin-left:.0001pt;text-align:justify;">简单来说,就是对不同的运算符赋予不同的“优先级”,我们会优先执行高优先级的运算、再执行低优先级的运算。如果优先级相同,就按照“结合律”来决定执行顺序。这其实跟数学的综合算式是一样的,我们会定义乘除的优先级要高于加减,平级运算从左往右,所以对于算式:</p>
<p style="margin-left:.0001pt;text-align:justify;">1 + 2 – 3 × 4</p>
<p style="margin-left:.0001pt;text-align:justify;">我们会先计算高优先级的 3×4,然后按照从左到右的结合顺序计算1+2,最后做减法。另外,如果有括号,那就要先把括起来的部分当成一个整体先做计算,然后再考虑括号外的结合顺序,这一点在C++表达式中同样适用。</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.2 </strong></strong><strong><strong>算术运算</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">最简单的运算符,就是表示算术计算的加减乘除,这一类被称为“算术运算符”。C++支持的算术运算符如下:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/b0ba786414d24178b162cfcf260c6c1a.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意的是,同一个运算符,在不同的场合可能表达不同的含义。比如“-”,可以是“减号”也可以是“负号”:如果直接放在一个表达式前面,就是对表达式的结果取负数,这是一元运算符;如果连接两个表达式,就是两者结果相减,是二元运算符。</p>
<p style="margin-left:.0001pt;text-align:justify;">算术运算符相关规则如下:</p>
<ol><li>一元运算符(正负号)优先级最高;接下来是乘、除和取余;最后是加减;</li><li>算术运算符满足左结合律,也就是说相同优先级的运算符,将从左到右按顺序进行组合;</li><li>算术运算符可以用来处理任意算术类型的数据对象;</li><li>不同类型的数据对象进行计算时,较小的整数类型会被“提升”为较大的类型,最终转换成同一类型进行计算;</li><li>对于除法运算“/”,执行计算的结果跟操作数的类型有关。如果它的两个操作数(也就是被除数和除数)都是整数,那么得到的结果也只能是整数,小数部分会直接舍弃,这叫“整数除法”;当至少有一个操作数是浮点数时,结果就会是浮点数,保留小数部分;</li><li>对于取余运算“%”(或者叫“取模”),两个操作数必须是整数类型;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 除法</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 20, b = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " a / b = " << a / b << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " -a / b = " << -a / b << endl; // 负数向0取整</p>
<p style="margin-left:.0001pt;text-align:left;">float a2 = 20;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " a2 / b = " << a2 / b << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 取模</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " a % b = " << a % b << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " -a % b = " << -a % b << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">在这里,同样是除法运算符“/”,针对不同类型的数据对象,其实会做不同的处理。使用相同的符号、根据上下文来执行不同操作,这是C++提供的一大特色功能,叫做“运算符重载”(operator overloading)。</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.3 赋值</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">将一个表达式的结果,传递给某个数据对象保存起来,这个过程叫做“赋值”。</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>3</strong></strong><strong><strong>.</strong></strong><strong><strong>1</strong></strong><strong> </strong><strong><strong>赋值运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,用等号“=”表示一个赋值操作,这里的“=”就是赋值运算符。需要注意的是,赋值运算符的左边,必须是一个可修改的数据对象,比如假设我们已经定义了一个int类型的变量a,那么</p>
<p style="margin-left:.0001pt;text-align:justify;">a = 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">这样赋值是对的,但</p>
<p style="margin-left:.0001pt;text-align:justify;">1 = a;</p>
<p style="margin-left:.0001pt;text-align:justify;">就是错误的。因为a是一个变量,可以赋值;而 1只是一个字面值常量,不能再对它赋值。</p>
<p style="margin-left:.0001pt;text-align:left;">int a, b;</p>
<p style="margin-left:.0001pt;text-align:left;">a = 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">//1 = a; // 错误:表达式必须是可修改的左值</p>
<p style="margin-left:.0001pt;text-align:left;">a = b + 5;</p>
<p style="margin-left:.0001pt;text-align:justify;">//b + 5 = a; // 错误:表达式必须是可修改的左值</p>
<p style="margin-left:.0001pt;text-align:left;">const int c = 10;</p>
<p style="margin-left:.0001pt;text-align:justify;">//c = a + b; // 错误:表达式必须是可修改的左值</p>
<p style="margin-left:.0001pt;text-align:justify;">所以像变量a这样的可以赋值的运算对象,在C++中被叫做“左值”(lvalue);对应的,放在赋值语句右面的表达式就是“右值”(rvalue)。</p>
<p style="margin-left:.0001pt;text-align:justify;">赋值运算有以下一些规则:</p>
<ol><li>赋值运算的结果,就是它左侧的运算对象;结果的类型就是左侧运算对象的类型;</li><li>如果赋值运算符两侧对象类型不同,就把右侧的对象转换成左侧对象的类型;</li><li>C++ 11新标准提供了一种新的语法:用花括号{}括起来的数值列表,可以作为赋值右侧对象。这样就可以非常方便地对一个数组赋值了;</li><li>赋值运算满足右结合律。也就是说可以在一条语句中连续赋值,结合顺序是从右到左;</li><li>赋值运算符优先级较低,一般都会先执行其它运算符,最后做赋值;</li></ol>
<p style="margin-left:.0001pt;text-align:left;">a = {2};</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[] = {1,2,3,4,5}; // 用花括号对数组赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">a = b = 20; // 连续赋值</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.3.2 复合赋值运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">实际应用中,我们经常需要把一次计算的结果,再赋值给参与运算的某一个变量。最简单的例子就是多个数求和,比如我们要计算a、b、c的和,那么可以专门定义一个变量sum,用来保存求和结果:</p>
<p style="margin-left:.0001pt;text-align:left;">int sum = a; // 初始值是a</p>
<p style="margin-left:.0001pt;text-align:left;">sum = sum + b; // 叠加b</p>
<p style="margin-left:.0001pt;text-align:justify;">sum = sum + c; // 叠加c</p>
<p style="margin-left:.0001pt;text-align:justify;">要注意赋值运算符“=”完全不是数学上“等于”的意思,所以上面的赋值语句sum = sum + b; 说的是“计算sum + b的结果,然后把它再赋值给sum”。</p>
<p style="margin-left:.0001pt;text-align:justify;">为了更加简洁,C++提供了一类特殊的赋值运算符,可以把要执行的算术运算“+”跟赋值“=”结合在一起,用一个运算符“+=”来表示;这就是“复合赋值运算符”。</p>
<p style="margin-left:.0001pt;text-align:justify;">复合赋值一般结合的是算术运算符或者位运算符。每种运算符都有对应的组合形式:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/fb82c2c0031e4c3b98ac393c3eac166a.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">关于位运算符,我们会在稍后介绍。</p>
<p style="margin-left:.0001pt;text-align:justify;">这样上面的代码可以改写为:</p>
<p style="margin-left:.0001pt;text-align:left;">int sum = a; // 初始值是a</p>
<p style="margin-left:.0001pt;text-align:left;">sum += b; // 完全等价于 sum = sum + b;</p>
<p style="margin-left:.0001pt;text-align:justify;">sum += c;</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>3</strong></strong><strong><strong>.3 </strong></strong><strong><strong>递增递减运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">C++为数据对象的“加一”“减一”操作,提供了更加简洁的表达方式,这就是递增和递减运算符(也叫“自增”“自减”运算符)。“递增”用两个加号“++”表示,表示“对象值加一,再赋值给原对象”;“递减”则用两个减号“--”表示。</p>
<p style="margin-left:.0001pt;text-align:left;">++a; // a递增,相当于 a += 1;</p>
<p style="margin-left:.0001pt;text-align:left;">--b; // b递减,相当于 b -= 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">递增递减运算符各自有两种形式:“前置”和“后置”,也就是说写成“++a”和“a++”都是可以的。它们都表示“a = a + 1”,区别在于表达式返回的结果不同:</p>
<p style="margin-left:.0001pt;text-align:justify;">前置时,对象先加1,再将更新之后的对象值作为结果返回;</p>
<p style="margin-left:.0001pt;text-align:justify;">后置时,对象先将原始值作为结果返回,再加1;</p>
<p style="margin-left:.0001pt;text-align:justify;">这要特别注意:如果我们单独使用递增递减运算符,那前置后置效果都一样;但如果运算结果还要进一步做计算,两者就有明显不同了。</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 0, j;</p>
<p style="margin-left:.0001pt;text-align:left;">j = ++i; // i = 1,j = 1</p>
<p style="margin-left:.0001pt;text-align:justify;">j = i--; // i = 0, j = 1</p>
<p style="margin-left:.0001pt;text-align:justify;">在实际应用中,一般都是希望用改变之后的对象值;所以为了避免混淆,我们通常会统一使用前置的写法。</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>4</strong></strong><strong> </strong><strong><strong>关系和逻辑运算</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在程序中,不可缺少的一类运算就是逻辑和关系运算,因为我们往往需要定义“在某种条件发生时,执行某种操作”。判断条件是否发生,这就是一个典型的逻辑判断;得到的结果或者为“真”(true),或者为“假”。很显然,这类运算的结果应该是布尔类型。</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>4</strong></strong><strong><strong>.1 </strong></strong><strong><strong>关系运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">最简单的一种条件,就是判断两个算术对象的大小关系,对应的运算符称为“关系运算符”。包括:大于“>”、小于“<”、等于“==”、不等于“!=”、大于等于“>=”、小于等于“<=”。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/553d55168de74a39896000951e3ee526.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这里要注意区分的是,在C++语法中一个等号“=”表示的是赋值,两个等号“==”才是真正的“等于”。</p>
<p style="margin-left:.0001pt;text-align:left;">1 < 2; // true</p>
<p style="margin-left:.0001pt;text-align:left;">3 >= 5; // false</p>
<p style="margin-left:.0001pt;text-align:justify;">10 == 4 + 6; // true</p>
<p style="margin-left:.0001pt;text-align:justify;">(10 != 4) + 6; // 7</p>
<p style="margin-left:.0001pt;text-align:justify;">关系运算符的相关规则:</p>
<ol><li>算术运算符的优先级高于关系运算符,而如果加上括号就可以调整计算顺序;</li><li>关系运算符的返回值为布尔类型,如果参与算术计算,true的值为1,false的值为0;</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>4</strong></strong><strong><strong>.2 </strong></strong><strong><strong>逻辑运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">一个关系运算符的结果是一个布尔类型(ture或者false),就可以表示一个条件的判断;如果需要多个条件的叠加,就可以用逻辑“与或非”将这些布尔类型组合起来。这样的运算符叫做“逻辑运算符”。</p>
<ol><li>逻辑非(!):一元运算符,将运算对象的值取反后返回,真值反转;</li><li>逻辑与(&&):二元运算符,两个运算对象都为true时结果为true,否则结果为false;</li><li>逻辑或(||):二元运算符,两个运算对象只要有一个为true结果就为true,都为false则结果为false;</li></ol>
<p style="margin-left:.0001pt;text-align:left;">1 < 2 && 3 >= 5; // false</p>
<p style="margin-left:.0001pt;text-align:left;">1 < 2 || 3 >= 5; // true</p>
<p style="margin-left:.0001pt;text-align:justify;">!(1 < 2 || 3 >= 5); // false</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以把逻辑运算符和关系运算符的用法、优先级和结合律总结如下(从上到下优先级递减):</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/e0e87a1bc11e4bd797431e30f21d3520.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意的规则有:</p>
<ol><li>如果将一个算术类型的对象作为逻辑运算符的操作数,那么值为0表示false,非0值表示true;</li><li>逻辑与和逻辑或有两个运算对象,在计算时都是先求左侧对象的值,再求右侧对象的值;如果左侧对象的值已经能决定最终结果,那么右侧就不会执行计算:这种策略叫做“<strong>短路求值</strong>”;</li></ol>
<p style="margin-left:.0001pt;text-align:left;">i = -1;</p>
<p style="margin-left:.0001pt;text-align:left;">1 < 2 && ++i; // false</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " i = " << i << endl; // i = 0</p>
<p style="margin-left:.0001pt;text-align:left;">1 < 2 || ++i; // true</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " i = " << i << endl; // i = 0</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>4</strong></strong><strong><strong>.3 </strong></strong><strong><strong>条件运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">C++还从C语言继承了一个特殊的运算符,叫做“条件运算符”。它由“?”和“:”两个符号组成,需要三个运算表达式,形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>条件判断表达式 ? 表达式1 : 表达式2</em></p>
<p style="margin-left:.0001pt;text-align:justify;">它的含义是:计算条件判断表达式的值,如果为true就执行表达式1,返回求值结果;如果为false则跳过表达式1,执行表达式2,返回求值结果。这也是C++中唯一的一个三元运算符。</p>
<p style="margin-left:.0001pt;text-align:left;">i = 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << ((1 < 2 && ++i) ? "true" : "false") << endl;</p>
<ol><li>条件运算符的优先级比较低,所以输出的时候需要加上括号</li><li>条件运算符满足右结合律</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">事实上,条件运算符等同于流程控制中的分支语句if...else...,只用一条语句就可以实现按条件分支处理,这就让代码更加简洁。关于分支语句,我们会在后面详细介绍。</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>5</strong></strong><strong> </strong><strong><strong>位运算符</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">之前介绍的所有运算符,主要都是针对算术类型的数据对象进行操作的;所有的算术类型,占用的空间都是以字节(byte,8位)作为单位来衡量的。在C++中,还有一类非常底层的运算符,可以直接操作到具体的每一位(bit)数据,这就是“位运算符”。</p>
<p style="margin-left:.0001pt;text-align:justify;">位运算符可以分为两大类:移位运算符,和位逻辑运算符。下面列出了所有位运算符的优先级和用法。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/ad4f93ced3c94aa889469ae62f201316.png" />
</p>
<h5 style="text-align:justify;"><strong><strong><strong>4</strong></strong><strong><strong>.5.1</strong></strong><strong><strong>移位运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">算术类型的数据对象,都可以看做是一组“位”的集合。那么利用“移位运算符”,就可以让运算对象的所有位,整体移动指定的位数。</p>
<p style="margin-left:.0001pt;text-align:justify;">移位运算符有两种:左移运算符“<<”和右移运算符“>>”。这个符号我们并不陌生,之前做输入输出操作的时候用的就是它,不过那是标准IO库里定义的运算符重载版本。</p>
<p style="margin-left:.0001pt;text-align:justify;">下面是移位运算符的一个具体案例:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/139eaba23e974de48118708a49a27200.png" />
</p>
<ol><li>较小的整数类型(char、short以及bool)会自动提升成int类型再做移位,得到的结果也是int类型</li><li>左移运算符“<<”将操作数左移之后,在右侧补0;</li><li>右移运算符“>>”将操作数右移之后,对于无符号数就在左侧补0;对于有符号数的操作则要看运行的机器环境,有可能补符号位,也有可能直接补0;</li><li>由于有符号数右移结果不确定,一般只对无符号数执行位移操作;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">unsigned char bits = 0xb5; // 181</p>
<p style="margin-left:.0001pt;text-align:left;">cout << hex; // 以十六进制显示</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "0xb5 左移2位:" << (bits << 2) << endl; // 0x 0000 02d4</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "0xb5 左移8位:" << (bits << 8) << endl; // 0x 0000 b500</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "0xb5 左移31位:" << (bits << 31) << endl; // 0x 8000 0000</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "0xb5 右移3位:" << (bits >> 3) << endl; // 0x 0000 0016</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << dec;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << (200 << 3) << endl; // 乘8操作</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << (-100 >> 2) << endl; // 除4操作,一般右移是补符号位</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>4</strong></strong><strong><strong>.5.2 </strong></strong><strong><strong>位逻辑运算符</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">计算机存储的每一个“位”(bit)都是二进制的,有0和1两种取值,这跟布尔类型的真值表达非常类似。于是自然可以想到,两个位上的“0”或“1”都可以执行类似逻辑运算的操作。</p>
<p style="margin-left:.0001pt;text-align:justify;">位逻辑运算符有:按位取反“~”,位与“&”,位或“|”和位异或“^”。</p>
<ol><li>按位取反“~”:一元运算符,类似逻辑非。对每个位取反值,也就是把1置为0、0置为1;</li><li>位与“&”:二元运算符,类似逻辑与。两个数对应位上都为1,结果对应位为1;否则结果对应位为0;</li><li>位或“|”:二元运算符,类似逻辑或。两个数对应位上只要有1,结果对应位就为1;如果全为0则结果对应位为0;</li><li>位异或“^”:两个数对应位相同,则结果对应位为0;不同则结果对应位为0;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">下面是位逻辑运算符的一个具体案例:</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/98d30cf372bf454d8b25bb60cd206fab.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 位逻辑运算</p>
<p style="margin-left:.0001pt;text-align:left;">cout << (~5) << endl; // ~ (0... 0000 0101) = 1... 1111 1010, -6</p>
<p style="margin-left:.0001pt;text-align:left;">cout << (5 & 12) << endl; // 0101 & 1100 = 0100, 4</p>
<p style="margin-left:.0001pt;text-align:left;">cout << (5 | 12) << endl; // 0101 | 1100 = 1101, 13</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << (5 ^ 12) << endl; // 0101 & 1100 = 1001, 9</p>
<h4 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>5</strong></strong><strong> </strong><strong><strong>类型转换</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,不同类型的数据对象,是可以放在一起做计算的。这就要求必须有一个机制,能让有关联的两种类型可以互相转换。在上一章已经介绍过变量赋值时的自动类型转换,接下来我们会对类型转换做更详细的展开。</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>5</strong></strong><strong><strong>.1 </strong></strong><strong><strong>隐式类型转换</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">大多数情况,C++编译器可以自动对类型进行转换,不需要我们干涉,这种方式叫做“隐式类型转换”。</p>
<p style="margin-left:.0001pt;text-align:justify;">隐式类型转换主要发生在算术类型之间,基本思路就是将长度较小的类型转换成较大的类型,这样可以避免丢失精度。隐式类型转换不仅可以在变量赋值时发生,也可以在运算表达式中出现。例如:</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">short s = 15.2 + 20;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " s = " << s << endl; // s = 35</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " 15.2 + 20 结果长度为:" << sizeof(15.2 + 20) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " s 长度为:" << sizeof(s) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">对于这条赋值语句,右侧是两个字面值常量相加,而且类型不同:15.2是double类型,20是int类型。当它们相加时,会将int类型的20转换为double类型,然后执行double的加法操作,得到35.2。</p>
<p style="margin-left:.0001pt;text-align:justify;">这个结果用来初始化变量s,由于s是short类型,所以还会把double类型的结果35.2再去掉小数部分,转换成short类型的35。所以s最终的值为35。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">隐式类型转换的一般规则可以总结如下:</p>
<ol><li>在大多数算术运算中,较小的整数类型(如bool、char、short)都会转换成int类型。这叫做“整数提升”;(而对于wchar_t等较大的扩展字符类型,则根据需要转换成int、unsigned int、long、unsigned long、long long、unsigned long long中能容纳它的最小类型)</li><li>当表达式中有整型也有浮点型时,整数值会转换成相应的浮点类型;</li><li>在条件判断语句中,其它整数类型会转换成布尔类型,即0为false、非0为true;</li><li>初始化变量时,初始值转换成变量的类型;</li><li>在赋值语句中,右侧对象的值会转换成左侧对象的类型;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">此外,要尽量避免将较大类型的值赋给较小类型的变量,这样很容易出现精度丢失或者数据溢出。</p>
<p style="margin-left:.0001pt;text-align:left;">s = 32767;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " s + 1 = " << s + 1 << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">short s2 = s + 1;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " s2 = " << s2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">另外还要注意,如果希望判断一个整型变量a是否在某个范围(0, 100)内,不能直接写:0 < a < 100;</p>
<p style="margin-left:.0001pt;text-align:justify;">由于小于运算符“<”满足左结合律,要先计算0 < a,得到一个布尔类型的结果,再跟后面的100进行比较。此时布尔类型做整数提升,不管值是真(1)还是假(0),都会满足 < 100 的判断,因此最终结果一定是true。</p>
<p style="margin-left:.0001pt;text-align:justify;">要想得到正确的结果,需要将两次关系判断拆开,写成逻辑与的关系。</p>
<p style="margin-left:.0001pt;text-align:left;">a = -1; </p>
<p style="margin-left:.0001pt;text-align:left;">0 < a < 100; // 不论a取什么值,总是true</p>
<p style="margin-left:.0001pt;text-align:justify;">0 < a && a < 100; // false</p>
<h5 style="text-align:justify;"><strong><strong><strong>4.</strong></strong><strong><strong>5</strong></strong><strong><strong>.2 </strong></strong><strong><strong>强制类型转换</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">除去自动进行的隐式类型转换,我们也可以显式地要求编译器对数据对象的类型进行更改。这种转换叫做“强制类型转换”(cast)。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如对于除法运算,我们知道整数除法和浮点数除法是不同的。如果希望对一组整数求一个平均数,直接相加后除以个数是无法得到想要的结果的:</p>
<p style="margin-left:.0001pt;text-align:left;">// 求平均数</p>
<p style="margin-left:.0001pt;text-align:left;">int total = 20, num = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">double avg = total / num;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " avg = " << avg << endl; // avg = 3</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">因为两个int类型的数相除,执行的是整数除法,得到3;再转换成double类型对avg做初始化,得到是3.0。如果想要更准确的结果,就必须将int类型强制转换成double,做浮点数除法。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">C++中可以使用不同的方式进行强制类型转换。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)C语言风格</p>
<p style="margin-left:.0001pt;text-align:justify;">最经典的强转方式来自C语言,格式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">(<em>类型名称</em>)<em> 值</em></p>
<p style="margin-left:.0001pt;text-align:justify;">把要强制转成的类型,用一个小括号括起来,放到要转换的对象值前面就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)C++函数调用风格</p>
<p style="margin-left:.0001pt;text-align:justify;">这种方式跟C语言的强转类似,只不过看起来更像是调用了一个函数:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>类型名称</em> (<em>值</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;">要转成的类型名就像是一个函数,调用的时候,后面小括号里是传递给它的参数。</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)C++强制类型转换运算符</p>
<p style="margin-left:.0001pt;text-align:justify;">C++还引入了4个强制类型转换运算符,这种新的转换方式比前两种传统方式要求更为严格。通常在类型转换中用到的运算符是static_cast,用法如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">static_cast<<em>类型名称</em>> (<em>值</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;">static_cast运算符后要跟一个尖括号,里面是要转换成的类型。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">有了这些强转的方式,就可以解决之前求平均数的问题了:</p>
<p style="margin-left:.0001pt;text-align:left;">// C语言风格</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " avg = " << (double) total / num << endl; </p>
<p style="margin-left:.0001pt;text-align:left;">// C++函数风格 </p>
<p style="margin-left:.0001pt;text-align:left;">cout << " avg = " << double (total) / num << endl; </p>
<p style="margin-left:.0001pt;text-align:left;">// C++强转运算符</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " avg = " << static_cast<double>(total) / num << endl; </p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">强制类型转换会干扰正常的类型检查,带来很多风险,所以通常要尽量避免使用强制类型转换。</p>
<h3 style="text-align:justify;"><strong><strong><strong>五、流程控制语句</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">C++程序执行的流程结构可以有三种:顺序、分支和循环。除了最简单的顺序结构是默认的,分支和循环都需要使用专门的“流程控制语句”来定义。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/5be2249539054a16a06ef41fea2ac8e5.png" />
</p>
<h4 style="text-align:justify;"><strong><strong><strong>5.1 </strong></strong><strong><strong>语句</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">C++中表示一步操作的一句代码,就叫做“语句”(statement),大多数语句都是以分号“;”结尾的。C++程序运行的过程,其实就是找到主函数,然后从上到下顺序执行每条语句的过程。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.1.1 </strong></strong><strong><strong>简单语句</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">使用各种运算符,作用到数据对象上,就得到了“表达式”;一个表达式末尾加上分号,就构成了“表达式语句”(expression statement)。</p>
<p style="margin-left:.0001pt;text-align:justify;">表达式语句表示,要执行表达式的计算过程,并且丢弃最终返回的结果。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int a = 0; // 变量定义并初始化语句</p>
<p style="margin-left:.0001pt;text-align:left;">a + 1; // 算术表达式语句,无意义</p>
<p style="margin-left:.0001pt;text-align:left;">++a; // 递增语句,a的值变为1</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << " a = " << a << endl; // 输出语句</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">其中第二行a + 1; 是没什么意义的,因为它只是执行了加法操作,却没有把结果保存下来(赋值给别的变量),a的值也没有改变,也没有任何附带效果(比如最后一句的输出)。</p>
<p style="margin-left:.0001pt;text-align:justify;">最简单的语句,其实是“空语句”,就是只有一个分号的语句:</p>
<p style="margin-left:.0001pt;text-align:left;">; // 空语句</p>
<p style="margin-left:.0001pt;text-align:justify;">这看起来好像没什么用。不过有时候,可能程序在语法上需要有一条语句,而逻辑上什么都不用做;这时就应该用一条空语句来填充。</p>
<p style="margin-left:.0001pt;text-align:justify;">初学C++,一定不要忘记语句末尾的分号;当然,对于不需要分号的场景,也尽量避免多写分号。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.1.2 复合语句</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">简单语句从上到下按顺序依次执行,这非常符合我们对计算机运行的预期。但是很多场景下,简单的顺序结构远远不能满足逻辑需要:比如我们可能需要按照条件判断,做程序的分支执行;也可能需要将一段代码循环执行多次。这就需要一些“流程控制语句”(比如if、while、for等)来表达更加复杂的操作了。</p>
<p style="margin-left:.0001pt;text-align:justify;">而对于流程控制语句,逻辑上来说只是一条语句;事实上却可能包含了多条语句、复杂的操作。这就需要用一个花括号“{}”,把这一组语句序列包成一个整体,叫做“复合语句”(compound statement),也叫做“块”(block)。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int i = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">while (i < 5) {</p>
<p style="margin-left:.0001pt;text-align:left;">int a = i;</p>
<p style="margin-left:.0001pt;text-align:left;">++i;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里的while表示一个循环,后面只能跟要循环执行的一条语句;如果我们想写两条语句,就要用花括号括起来,构成“块”。</p>
<p style="margin-left:.0001pt;text-align:justify;">对于复合语句(块)需要注意:</p>
<ol><li>花括号后面不需要再加分号,块本身就是一条语句;</li><li>块内可以声明变量,变量的作用域仅限于块内部;</li><li>只有一对花括号、内部没有任何语句的块叫做“空块”,等价于空语句;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h4 style="text-align:justify;"><strong><strong><strong>5.2 条件</strong></strong><strong><strong>分支</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">很多情况下,我们为程序的执行会提供“岔路”的选择机会。一般都是:满足某种条件就执行A操作,满足另一种条件就执行B操作……这样的程序结构叫做“条件分支”。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++提供了两种按条件分支执行的控制语句:if和switch。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.2.1 if</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">if语句主要就是判断一个条件是否为真(true),如果为真就执行下面的语句,如果为假则跳过。具体形式可以分为两种:一种是单独一个if,一般称为“单分支”;另一种是if … else …,称为“双分支”。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/6940ecae17264e849304fe44bc34db5e.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)单分支</p>
<p style="margin-left:.0001pt;text-align:justify;">单分支是最简单的if用法,判断的条件用小括号括起来跟在if后面,然后是如果条件为真要执行的语句。基本形式为:</p>
<p style="margin-left:.0001pt;text-align:justify;">if (<em>条件判断</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句</em></p>
<p style="margin-left:.0001pt;text-align:justify;">如果条件为假,那么这段代码就会被完全跳过。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以举一个简单示例,判断输入的年龄数值,然后输出一句欢迎词:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入您的芳龄:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">int age;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> age;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">if ( age >= 18 )</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "欢迎您,成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">通常会用一个花括号将if后面的语句括起来,成为一个“块”。现在块里只有一条语句,所以花括号是可以省略的:</p>
<p style="margin-left:.0001pt;text-align:left;">if ( age >= 18 )</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "欢迎您,成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">如果要执行的是多条语句,花括号就不能省略;否则if后面其实就只有第一条语句。为了避免漏掉括号出现错误,一般if后面都会使用花括号。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)双分支</p>
<p style="margin-left:.0001pt;text-align:justify;">双分支就是在if分支的基础上,加了else分支;条件为真就执行if后面的语句,条件为假就执行else后面的语句。基本形势如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">if (<em>条件判断</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句1</em></p>
<p style="margin-left:.0001pt;text-align:justify;">else</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句2</em></p>
<p style="margin-left:.0001pt;text-align:justify;">if分支和else分支,两者肯定会选择一个执行。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以在之前程序的基础上,增加一个else分支:</p>
<p style="margin-left:.0001pt;text-align:left;">if ( age >= 18 )</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "欢迎您,成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "本程序不欢迎未成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以回忆起来,之前介绍过的唯一一个三元运算符——条件运算符,其实就可以实现类似的功能。所以条件运算符可以认为是if … else 的一个语法糖。</p>
<p style="margin-left:.0001pt;text-align:justify;">以下两条语句跟上面的if…else是等价的:</p>
<p style="margin-left:.0001pt;text-align:left;">// 条件运算符的等价写法</p>
<p style="margin-left:.0001pt;text-align:left;">age >= 18 ? cout << "欢迎您,成年人!" << endl : cout << "本程序不欢迎未成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << (age >= 18 ? "欢迎您,成年人!" : "本程序不欢迎未成年人!") << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)嵌套分支(多分支)</p>
<p style="margin-left:.0001pt;text-align:justify;">程序中的分支有可能不只两个,这时就需要对if分支或者else分支再做条件判断和拆分了,这就是“嵌套分支”。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/2331435ea7f24c5a969c4ff21f58f286.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">简单来说,就是if或者else分支的语句块里,继续使用if或者if…else按条件进行分支。这是一种“分层”的条件判断。</p>
<p style="margin-left:.0001pt;text-align:left;">if ( age >= 18 )</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "欢迎您,成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">if (age < 35)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "加油,年轻人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "本程序不欢迎未成年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">if (age >= 12)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "少年,好好学习!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "小朋友,别玩电脑!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">嵌套分支如果比较多,代码的可读性会大幅降低。所以还有一种更加简单的嵌套分支写法,那就是if … else if …,具体形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">if (<em>条件判断1</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句1</em></p>
<p style="margin-left:.0001pt;text-align:justify;">else if (<em>条件判断2</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句2</em></p>
<p style="margin-left:.0001pt;text-align:justify;">else if (<em>条件判断3</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句3</em></p>
<p style="margin-left:.0001pt;text-align:justify;"><em>…</em></p>
<p style="margin-left:.0001pt;text-align:justify;">else</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句n</em></p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这种分支的嵌套,本质上只能对else分支进行,而且只能在最底层的分支中才能执行语句。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/4c3099cb05f44e4dab05864599d8f1d6.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">测试代码如下:</p>
<p style="margin-left:.0001pt;text-align:left;">if (age < 12) {</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "小朋友,别玩电脑!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else if (age < 18)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "少年,好好学习!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else if (age < 35)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "加油,年轻人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else if (age < 60)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "加油,中年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "好好休息,老年人!" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.2.2 switch</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在一些应用场景中,要判断的条件可能不是范围,而是固定的几个值。比如考试成绩只分“A”“B”“C”“D”四个档位,分别代表“优秀”“良好”“及格”“不及格”。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/e9e491c4aeb141bbb6095559c4dd7881.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">这个时候如果用if … else会显得非常繁琐,而swith语句就是专门为了这种分支场景设计的。</p>
<p style="margin-left:.0001pt;text-align:justify;">switch语法基本形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">swith (<em>表达式</em>){</p>
<p style="margin-left:.0001pt;text-align:justify;">case <em>值1</em>:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句1</em></p>
<p style="margin-left:.0001pt;text-align:justify;">break;</p>
<p style="margin-left:.0001pt;text-align:justify;">case <em>值2</em>:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句2</em></p>
<p style="margin-left:.0001pt;text-align:justify;">break;</p>
<p style="margin-left:.0001pt;text-align:justify;">…</p>
<p style="margin-left:.0001pt;text-align:justify;">default:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句n</em></p>
<p style="margin-left:.0001pt;text-align:justify;">break;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里switch后面的括号里是一个表达式,对它求值,然后转换成整数类型跟下面每个case后面的值做比较;如果相等,就进入这个case指定的分支,执行后面的语句,直到swith语句结束或者遇到break退出。需要注意的是:</p>
<ol><li>case关键字和后面对应的值,合起来叫做一个“case标签”;case标签必须是一个<strong>整型</strong>的常量表达式;</li><li>任何两个case标签不能相同;</li><li>break语句的作用是“中断”,会直接跳转到switch语句结构的外面;</li><li>如果没有break语句,那么匹配某个case标签之后,程序会从上到下一直执行下去;这会执行多个标签下面的语句,可能发生错误;</li><li>如果没有匹配上任何case标签的值,程序会执行default标签后面的语句;default是可选的,表示“默认要执行的操作”。</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以利用swith写一个判断考试成绩档位,输入一句相应的话:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入您的成绩:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">char score;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> score;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">switch (score)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">case 'A':</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "成绩优秀!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">case 'B':</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "成绩良好!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">case 'C':</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "恭喜!及格了!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">case 'D':</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "欢迎下次再来!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">default:</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "错误的成绩输入!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h4 style="text-align:justify;"><strong><strong><strong>5.3 </strong></strong><strong><strong>循环</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">可以重复执行一组操作的语句叫做“循环”,有时也叫作“迭代”。循环一般不能无限进行下去,所以会设置一个终止的判断条件。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++中的循环语句,有while、do while和for三种。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.3.1 while</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">while只需要给定一个判断条件,只要条件为真,就重复地执行语句。形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">while (<em>条件</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句</em></p>
<p style="margin-left:.0001pt;text-align:justify;">要执行的语句往往会有多条,这就需要用花括号将它们括起来。这个块一般被称为“循环体”。</p>
<p style="margin-left:.0001pt;text-align:justify;">一般来说,用来控制while循环的条件中一定会包含变量,通常叫做“循环变量”;而它或者在条件中变化,或者在循环体中变化,这样才能保证循环能够终止退出。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如我们可以用一个循环输出10次“Hello World”,并且打印出当前循环次数:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "循环开始...\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int i = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">while (i <= 10)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "现在是第" << i << "次循环\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">++i;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "循环结束!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意,循环体最后的 ++i一定不能漏掉。如果没有这条语句,i的值就不会更改,循环就永远不会退出。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.3.2 do while</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">do while和while非常类似,区别在于do while是先执行循环体中的语句,然后再检查条件是否满足。所以do while至少会执行一次循环体。</p>
<p style="margin-left:.0001pt;text-align:justify;">do while语法形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">do</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句</em></p>
<p style="margin-left:.0001pt;text-align:justify;">while (<em>条件</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以接着之前while循环的代码继续测试:</p>
<p style="margin-left:.0001pt;text-align:left;">do</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "现在是倒数第" << --i << "次循环" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "GoodBye World!\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">} while (i > 1);</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">由于之前的变量i已经做了10次递增,因此do wihle开始时i的值为11。进入循环体直接输出内容,每次i递减1,直到i = 1时退出循环。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.3.3 for</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">通过while和do while可以总结出来,一个循环主要有这样几个要素:</p>
<ol><li>一个条件,用来控制循环退出;</li><li>一个循环体,用来定义循环要执行的操作;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">而一般情况下,我们都是通过一个循环变量来控制条件的,这个变量需要随着循环迭代次数的增加而变化。while和do while的循环变量,都是在循环体外单独定义的。</p>
<p style="margin-left:.0001pt;text-align:justify;">for是用法更加明确的循环语句。它可以把循环变量的定义、循环条件以及循环变量的改变都放在一起,统一声明出来。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)经典for循环</p>
<p style="margin-left:.0001pt;text-align:justify;">for循环的经典语法形式是:</p>
<p style="margin-left:.0001pt;text-align:justify;">for (<em>初始化语句</em>; <em>条件</em>;<em> 表达式</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句</em></p>
<p style="margin-left:.0001pt;text-align:justify;">关键字for和它后面括号里的部分,叫做“for语句头”。</p>
<p style="margin-left:.0001pt;text-align:justify;">for语句头中有三部分,用分号分隔,主要作用是:</p>
<ol><li>初始化语句负责初始化一个变量,这个变量值会随着循环迭代而改变,一般就是“循环变量”;</li><li>中间的条件是控制循环执行的关键,为真则执行下面的循环体语句,为假则退出。条件一般会以循环变量作为判断标准;</li><li>最后的表达式会在本次循环完成之后再执行,一般会对循环变量进行更改;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">这三个部分并不是必要的,根据需要都可以进行省略。如果省略某个部分,需要保留分号表示这是一个空语句。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以用for循环语句,实现之前输出10次“Hello World”的需求:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 1; i <= 10; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "现在是第" << i << "次循环!\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)范围for循环</p>
<p style="margin-left:.0001pt;text-align:justify;">C++ 11新标准引入了一种更加简单的for循环,这种语句可以直接遍历一个序列的所有元素。这种for循环叫做“范围for循环”。语法形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">for (声明:<em> 序列表达式</em>)</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>语句</em></p>
<p style="margin-left:.0001pt;text-align:justify;">这里for语句头中的内容就很简单了,只需要声明一个变量,后面跟上一个冒号(注意不是分号),再跟上一个序列的表达式就可以了。所谓“序列”,其实就是一组相同类型的数据对象排成了一列来统一处理;所以这个声明的意思,其实就是从序列中依次取出所有元素,每次都赋值给这个变量。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以范围for循环的特点就是,不需要循环变量,直接就可以访问序列中的所有元素。</p>
<p style="margin-left:.0001pt;text-align:left;">// 范围for循环</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : {3, 6, 8, 10})</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "序列中现在的数据是:" << num << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里用花括号把一组数括起来,就构成了最简单的序列:{3, 6, 8, 10}。后面将要介绍的数组,以及vector、string等类型的对象,也都是序列。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.3.4 循环嵌套</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">循环语句和分支语句一样,也是可以进行嵌套的。具体可以while循环中嵌套while,可以for循环中嵌套for,也可以while、do while和for混合嵌套。因为for的循环变量定义更明确,所以一般用for的循环嵌套会多一些。</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < 3; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < 5; j++) </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "Hello World! i = " << i << ", j = " << j << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">循环嵌套之后,内层语句执行的次数,将是外层循环次数和内层循环次数的乘积。这会带来大量的时间消耗,使程序运行变慢,所以使用嵌套循环要非常谨慎。</p>
<p style="margin-left:.0001pt;text-align:justify;">下面是一个使用双重for循环,打印输出“九九乘法表”的例子。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main() </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// i表示行数,j表示列数</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 1; i < 10; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 1; j <= i; j++) {</p>
<p style="margin-left:.0001pt;text-align:left;">cout << j << " × " << i << " = " << i * j << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里使用内外两层for循环,实现了一个二维“表”的输出。后面我们会看到,循环嵌套对于处理多维数据非常有用。</p>
<h4 style="text-align:justify;"><strong><strong><strong>5.4 </strong></strong><strong><strong>跳转</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在流程控制语句中还有一类“跳转语句”,主要用来中断当前的执行过程。C++中有四种跳转语句:break,continue,goto以及return。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.4.1 break</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">break语句表示要“跳出”当前的流程控制语句,它只能出现在switch或者循环语句(while、do while、for)中。当代码中遇到break时,会直接中断距离最近的switch或者循环,跳转到外部继续执行。</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">while (true)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " Hello World! " << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " 这是第" << ++i << "次输出\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">if (i >= 5)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">如果循环条件永远为真,那么循环体中一定要有break,保证在某种情况下程序可以退出循环。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.4.2 continue</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">continue语句表示“继续”执行循环,也就是中断循环中的本次迭代、并开始执行下一次迭代。很明显,continue只能用在循环语句中,同样针对最近的一层循环有效。</p>
<p style="margin-left:.0001pt;text-align:justify;">continue非常适合处理需要“跳过”某些情况的场合。</p>
<p style="margin-left:.0001pt;text-align:left;">// 逢7过</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num = 1; num < 100; num++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">// 如果是7的倍数,或者数字中有7,则跳过</p>
<p style="margin-left:.0001pt;text-align:left;">if (num % 7 == 0 || num % 10 == 7 || num / 10 == 7)</p>
<p style="margin-left:.0001pt;text-align:left;">continue;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << num;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 如果是10的倍数,则换行</p>
<p style="margin-left:.0001pt;text-align:left;">if (num % 10 == 0)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">上面模拟了一个经典的小游戏“逢7过”,如果遇到7的倍数比如7、14、21,或者数字中有7比如17、27、71,都要跳过。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.4.3 goto</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">goto语句表示无条件地跳转到程序中的另一条语句。goto的语法形式为:</p>
<p style="margin-left:.0001pt;text-align:justify;">goto <em>标签</em>;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里的“标签”可以认为是一条语句的“名字”,跟变量类似,只不过它是指代一条语句的标识符。定义标签也非常简单,只要在一条语句前写出标识符,然后跟上冒号就可以了,比如:</p>
<p style="margin-left:.0001pt;text-align:justify;">begin: int a = 0;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">下面是一个具体的例子:</p>
<p style="margin-left:.0001pt;text-align:left;">int x = 0;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "程序开始..." << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">begin:</p>
<p style="margin-left:.0001pt;text-align:left;">do</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " x = " << ++x << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">} while (x < 10);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">if (x < 15) {</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "回到原点!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">goto begin;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "程序结束!" << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">由于goto可以任意跳转,所以它非常灵活,也非常危险。一般在代码中不要使用goto。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.4.4 return</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">return是用来终止函数运行并返回结果的。之前的Hello World程序中就曾经介绍,主函数最后的那句 return 0; 就是结束主函数并返回结果,一般这句可以省略。</p>
<p style="margin-left:.0001pt;text-align:justify;">而在自定义的函数中,同样可以用return来返回。</p>
<h4 style="text-align:justify;"><strong><strong><strong>5.5 应用案例</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">综合利用分支和循环语句,就可以实现很多有趣的功能。</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.5.1 判断质数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">质数也叫素数,是指一个大于1的自然数,因数只有1和它自身。质数是数论中一个经典的概念,很多著名定理和猜想都跟它有关;质数也是现代密码学的基础。</p>
<p style="margin-left:.0001pt;text-align:justify;">判断一个数是否为质数没有什么规律可言,我们可以通过验证小于它的每个数能否整除,来做暴力求解。下面是一段判断质数、并输出0~100内所有质数的程序:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 定义一个判断质数的函数,用return返回判断结果</p>
<p style="margin-left:.0001pt;text-align:left;">bool isPrime(int num)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 2;</p>
<p style="margin-left:.0001pt;text-align:left;">while (i < num)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (num % i == 0) return false;</p>
<p style="margin-left:.0001pt;text-align:left;">++i;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">return true;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入一个自然数(不超过20亿):" << endl; </p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int num;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> num;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">if (isPrime(num))</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "是质数!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "不是质数!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "\n=========================\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "0 ~ 100 内的质数有:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 2; i <= 100; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (isPrime(i))</p>
<p style="margin-left:.0001pt;text-align:left;">cout << i << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.5.2 猜数字</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">猜数字是一个经典的小游戏,程序随机生成一个0~100的数字,然后由用户输入来猜测。如果猜对,输出结果并退出;如果不对,则提示偏大还是偏小。我们可以对猜的次数做限制,比如一共5次机会。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "=========猜数字========" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "规则:输入0~100的整数,有5次机会\n" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 以当前时间为随机数种子,生成一个0~100的伪随机数</p>
<p style="margin-left:.0001pt;text-align:left;">srand(time(0)); </p>
<p style="margin-left:.0001pt;text-align:left;">int target = rand() % 100;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int n = 0; // 猜的次数</p>
<p style="margin-left:.0001pt;text-align:left;">while (n < 5)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "请输入0~100的整数:" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">int num;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> num;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">if (num == target)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "恭喜你,猜对了! 幸运数字是:" << target << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else if (num > target)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "数字太大了!再猜一遍!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "数字太小了!再猜一遍!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">++n;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">if (n == 5)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "已经猜过5遍,没有猜中!欢迎下次再来!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>5.5.3 爱心曲线</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">利用流程控制语句也可以绘制二维图形。只要知道函数表达式,就可以画出相应的曲线了。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以尝试绘制传说中的“爱心曲线”。一个典型的爱心曲线函数如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/3e416da8880c4462bef8569c6ccbe75c.png" />
</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/5ff76d394c874a0e9ebcf5a5bedf70d5.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">曲线是一个封闭图形,与坐标轴的四个交点坐标为</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/2a59168d7845418da523a55305b7ef1a.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">和</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/3ab26cc45e0546bd89438777fce07032.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">,我们知道坐标(x, y)满足 </p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/a527779b97e34291b62db1b8da93136a.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;"> 的点都在“爱心”内部,而满足</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/6c7aa86ae73f4571a5953e493ef8e429.png" />
的点都在“爱心”外部。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以我们可以取边长为</p>
<p style="margin-left:.0001pt;text-align:justify;">
<img alt="" src="https://i-blog.csdnimg.cn/direct/8a5e301553a4495689518bc7ebbe82cb.png" />
的正方形区域作为“画板”,扫描范围内所有点;在曲线内部的点用“*”填充,外部的点则用空格填充。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 爱心曲线方程 (x^2+y^2-a)^3 - x^2 y^3 = 0</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义绘图边界</p>
<p style="margin-left:.0001pt;text-align:left;">double bound = 1.3 * sqrt(a);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// x、y坐标变化步长</p>
<p style="margin-left:.0001pt;text-align:left;">double step = 0.05;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">for ( double y = bound; y >= -bound; y -= step)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (double x = -bound; x <= bound; x += step)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">double result = pow((pow(x, 2) + pow(y, 2) - a), 3) - pow(x, 2) * pow(y, 3);</p>
<p style="margin-left:.0001pt;text-align:left;">if (result <= 0)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "*";</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " ";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h3 style="text-align:justify;"><strong><strong><strong>六、复合数据类型</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">C++中不仅有基本数据类型,还提供了更加灵活和丰富的复合数据类型。</p>
<h4 style="text-align:justify;"><strong><strong><strong>6.1 </strong></strong><strong><strong>数组</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在程序中为了处理方便,常常需要把具有相同类型的数据对象按有序的形式排列起来,形成“一组”数据,这就是“数组”(array)。</p>
<p>
<img alt="" src="https://i-blog.csdnimg.cn/direct/aae63fcac18a43fdab391634e2b43f30.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">数组中的数据,在内存中是连续存放的,每个元素占据相同大小的空间,就像排好队一样。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.1.1 数组的定义</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">数组的定义形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>数据类型</em> <em>数组名</em>[<em>元素个数</em>];</p>
<ol><li>首先需要声明类型,数组中所有元素必须具有相同的数据类型;</li><li>数组名是一个标识符;后面跟着中括号,里面定义了数组中元素的个数,也就是数组的“长度”;</li><li>元素个数也是类型的一部分,所以必须是确定的;</li></ol>
<p style="margin-left:.0001pt;text-align:left;">int a1[10]; // 定义一个数组a1,元素类型为int,个数为10</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">const int n = 4;</p>
<p style="margin-left:.0001pt;text-align:left;">double a2[n]; // 元素个数可以是常量表达式</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int i = 5;</p>
<p style="margin-left:.0001pt;text-align:justify;">//int a3
; // 错误,元素个数不能为变量</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意,并没有通用的“数组”类型,所以上面的a1、a2的类型分别是“int数组”和“double数组”。这也是为什么我们把数组叫做“复合数据类型”。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.1.2 数组的初始化</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">之前在讲到for循环时,提到过使用范围for循环可以遍历一个“序列”,用花括号括起来的一组数就是一个序列。所以在给数组赋值时,也可以使用这样的序列。</p>
<p style="margin-left:.0001pt;text-align:left;">int a3[4] = {1,2,3,4};</p>
<p style="margin-left:.0001pt;text-align:left;">float a4[] = {2.5, 3.8, 10.1}; // 正确,初始值说明了元素个数是3</p>
<p style="margin-left:.0001pt;text-align:left;">short a5[10] = {3,6,9}; // 正确,指定了前三个元素,其余都为0</p>
<p style="margin-left:.0001pt;text-align:justify;">//long a6[2] = {3,6,9}; // 错误,初始值太多</p>
<p style="margin-left:.0001pt;text-align:justify;">//int a6[4] = a3; // 错误,不能用另一个数组对数组赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意的是:</p>
<ol><li>对数组做初始化,要使用花括号{}括起来的数值序列;</li><li>如果做了初始化,数组定义时的元素个数可以省略,编译器可以根据初始化列表自动推断出来;</li><li>初始值的个数,不能超过指定的元素个数;</li><li>初始值的个数,如果小于元素个数,那么会用列表中的值初始化靠前的元素;剩余元素用默认值填充,整型的默认值就是0;</li><li>如果没有做初始化,数组中元素的值都是未定义的;这一点和普通的局部变量一致;</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>6.1.3 数组的访问</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">(1)访问数组元素</p>
<p style="margin-left:.0001pt;text-align:justify;">数组元素在内存中是连续存放的,它们排好了队之后就会有一个队伍中的编号,称为“索引”,也叫“下标”;通过下标就可以快速访问每个元素了,具体形式为:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>数组名</em>[<em>元素下标</em>]</p>
<p style="margin-left:.0001pt;text-align:justify;">这里也是用了中括号来表示元素下标位置,被称为“下标运算符”。比如a[2]就表示数组a中下标为2的元素,可以取它的值输出,也可以对它赋值。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int a[] = {1,2,3,4,5,6,7,8};</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a[2] = " << a[2] << endl; // a[2] = 3</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">a[2] = 36;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "a[2] = " << a[2] << endl; // a[2] = 36</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意的是:</p>
<ol><li>数组的下标从0开始;</li><li>因此a[2]访问的并不是数组a的第2个元素,而是第三个元素;一个长度为10的数组,下标范围是0~9,而不是1~10;</li><li>合理的下标,不能小于0,也不能大于 (数组长度 - 1);否则就会出现数组下标越界;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)数组的大小</p>
<p style="margin-left:.0001pt;text-align:justify;">所有的变量,都会在内存中占据一定大小的空间;而数据类型就决定了它具体的大小。而对于数组这样的“复合类型”,由于每个元素类型相同,因此占据空间大小的计算遵循下面的简单公式:</p>
<p style="margin-left:.0001pt;text-align:justify;">数组所占空间 = 数据类型所占空间大小 * 元素个数</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这样一来,即使定义的时候没有指定数组元素个数,现在也可以计算得出了:</p>
<p style="margin-left:.0001pt;text-align:left;"> // a是已定义的数组</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a所占空间大小:" << sizeof(a) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "每个元素所占空间大小:" << sizeof(a[0]) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">// 获取数组长度</p>
<p style="margin-left:.0001pt;text-align:left;">int aSize = sizeof(a) / sizeof(a[0]);</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "数组a的元素个数:" << aSize << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里为了获取数组的长度,我们使用了sizeof运算符,它可以返回一个数据对象在内存中占用的大小(以字节为单位);数组总大小,除以每个数据元素的大小,就是元素个数。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)遍历数组</p>
<p style="margin-left:.0001pt;text-align:justify;">如果想要依次访问数组中所有的元素,就叫做“遍历数组”。我们当然可以用下标去挨个读取:</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a[0] = " << a[0] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a[1] = " << a[1] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">…</p>
<p style="margin-left:.0001pt;text-align:justify;">但这样显然太麻烦了。更好的方式是使用for循环:</p>
<p style="margin-left:.0001pt;text-align:left;">// 获取数组长度</p>
<p style="margin-left:.0001pt;text-align:left;">int aSize = sizeof(a) / sizeof(a[0]);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < aSize; i++ )</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a[" << i << "] = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">循环条件如果写一个具体的数,很容易出现下标越界的情况;而如果知道了数组长度,直接让循环变量i小于它就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;">当然,这种写法还是稍显麻烦。C++ 11标准给我们提供了更简单的写法,就是之前介绍过的范围for循环:</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num: a )</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">当然,这种情况下就无法获取元素对应的下标了。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>1</strong></strong><strong><strong>.</strong></strong><strong><strong>4</strong></strong><strong> </strong><strong><strong>多维数组</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">之前介绍的数组只是数据最简单的排列方式。如果数据对象排列成的不是“一队”,而是一个“方阵”,那显然就不能只用一个下标来表示了。我们可以对数组进行扩展,让它从“一维”变成“二维”甚至“多维”。</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[3][4]; // 二维数组,有三个元素,每个元素是一个长度为4的int数组</p>
<p style="margin-left:.0001pt;text-align:justify;">int arr2[2][5][10]; // 三维数组</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">C++中本质上没有“多维数组”这种东西,所谓的“多维数组”,其实就是“数组的数组”。</p>
<ol><li>二维数组int arr[3][4]表示:arr是一个有三个元素的数组,其中的每个元素都是一个int数组,包含4个元素;</li><li>三维数组int arr2[2][5][10]表示:arr2是一个长度为2的数组,其中每个元素都是一个二维数组;这个二维数组有5个元素,每个元素都是一个长度为10的int数组;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">一般最常见的就是二维数组。它有两个“维度”,第一个维度表示数组本身的长度,第二个表示每个元素的长度;一般分别把它们叫做“行”和“列”。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)多维数组的初始化</p>
<p style="margin-left:.0001pt;text-align:justify;">和普通的“一维”数组一样,多维数组初始化时,也可以用花括号括起来的一组数。使用嵌套的花括号可以让不同的维度更清晰:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>数据类型 数组名</em>[<em>行数</em>][列数] = {数据1, 数据2, 数据3, …};</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;"><em>数据类型 数组名</em>[<em>行数</em>][列数] = {</p>
<p style="margin-left:.0001pt;text-align:justify;">{数据11, 数据12, 数据13, …},</p>
<p style="margin-left:.0001pt;text-align:justify;">{数据21, 数据22, 数据23, …},</p>
<p style="margin-left:.0001pt;text-align:justify;">…</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意:</p>
<ol><li>内嵌的花括号不是必需的,因为数组中的元素在内存中连续存放,可以用一个花括号将所有数据括在一起;</li><li>初始值的个数,可以小于数组定义的长度,其它元素初始化为0值;这一点对整个二维数组和每一行的一维数组都适用;</li><li>如果省略嵌套的花括号,当初始值个数小于总元素个数时,会按照顺序依次填充(填满第一行,才填第二行);其它元素初始化为0值;</li><li>多维数组的维度,可以省略第一个,由编译器自动推断;即二维数组可以省略行数,但不能省略列数。</li></ol>
<p style="margin-left:.0001pt;text-align:left;">// 嵌套的花括号的初始化</p>
<p style="margin-left:.0001pt;text-align:left;">int ia[3][4] = {</p>
<p style="margin-left:.0001pt;text-align:left;">{1,2,3,4},</p>
<p style="margin-left:.0001pt;text-align:left;">{5,6,7,8},</p>
<p style="margin-left:.0001pt;text-align:left;">{9,10,11,12}</p>
<p style="margin-left:.0001pt;text-align:left;">};</p>
<p style="margin-left:.0001pt;text-align:left;">// 只有一层花括号的初始化</p>
<p style="margin-left:.0001pt;text-align:left;">int ia2[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 部分初始化,其余补0</p>
<p style="margin-left:.0001pt;text-align:left;">int ia3[3][4] = {</p>
<p style="margin-left:.0001pt;text-align:left;">{1,2,3},</p>
<p style="margin-left:.0001pt;text-align:left;">{5,6}</p>
<p style="margin-left:.0001pt;text-align:left;">};</p>
<p style="margin-left:.0001pt;text-align:left;">int ia4[3][4] = {1,2,3,4,5,6};</p>
<p style="margin-left:.0001pt;text-align:left;">// 省略行数,自动推断</p>
<p style="margin-left:.0001pt;text-align:justify;">int ia5[][4] = {1,2,3,4,5};</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)访问数据</p>
<p style="margin-left:.0001pt;text-align:justify;">也可以用下标运算符来访问多维数组中的数据,数组的每一个维度,都应该有一个对应的下标。对于二维数组来说,就是需要指明“行号”“列号”,这相当于数据元素在二维矩阵中的坐标。</p>
<p style="margin-left:.0001pt;text-align:left;">// 访问ia的第二行、第三个数据</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ia[1][2] = " << ia[1][2] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">// 修改ia的第一行、第二个数据</p>
<p style="margin-left:.0001pt;text-align:justify;">ia[0][1] = 19;</p>
<p style="margin-left:.0001pt;text-align:justify;">同样需要注意,行号和列号都是从0开始、到 (元素个数 - 1) 结束。</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)遍历数组</p>
<p style="margin-left:.0001pt;text-align:justify;">要想遍历数组,当然需要使用for循环,而且要扫描每一个维度。对于二维数组,我们需要对行和列分别进行扫描,这是一个双重for循环:</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "二维数组总大小:" << sizeof(ia) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "二维数组每行大小:" << sizeof(ia[0]) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "二维数组每个元素大小:" << sizeof(ia[0][0]) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 二维数组行数</p>
<p style="margin-left:.0001pt;text-align:left;">int rowCnt = sizeof(ia) / sizeof(ia[0]);</p>
<p style="margin-left:.0001pt;text-align:left;">// 二维数组列数</p>
<p style="margin-left:.0001pt;text-align:left;">int colCnt = sizeof(ia[0]) / sizeof(ia[0][0]);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < rowCnt; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < colCnt; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << ia[j] << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">同样,这里利用了sizeof运算符:</p>
<ol><li>行数 = 二维数组总大小 / 每行大小</li><li>列数 = 每行大小 / 每个元素大小</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">当然,也可以使用范围for循环:</p>
<p style="margin-left:.0001pt;text-align:left;">for (auto & row : ia)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (auto num : row)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里的外层循环使用了auto关键字,这也是C++ 11新引入的特性,它可以自动推断变量的类型;后面的&是定义了一个“引用”。关于这部分内容,会在后面继续介绍。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.1.5 数组的简单排序算法</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">数组排序指的是给定一个数组,要求把其中的元素按照从小到大(或从大到小)顺序排列。</p>
<p style="margin-left:.0001pt;text-align:justify;">这是一个非常经典的需求,有各种不同的算法可以实现。我们这里介绍两种最基本、最简单的排序算法。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)选择排序</p>
<p style="margin-left:.0001pt;text-align:justify;">选择排序是一种简单直观的排序算法。</p>
<p style="margin-left:.0001pt;text-align:justify;">它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后追加到已排序序列的末尾。以此类推,直到所有元素均排序完毕。</p>
<p><img alt="" src="https://i-blog.csdnimg.cn/direct/1a98c410dc464fc78db5710f78df476d.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">选择排序可以使用双重for循环很容易地实现:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[] = {5, 9, 2, 7, 4, 3, 12, 6, 1, 5, 7};</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int size = sizeof(arr) / sizeof(arr[0]);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 选择排序</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < size; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = i + 1; j < size; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (arr[j] < arr)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 如果arr[j]更小,就和arr交换位置</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = arr;</p>
<p style="margin-left:.0001pt;text-align:left;">arr = arr[j];</p>
<p style="margin-left:.0001pt;text-align:left;">arr[j] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 输出</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : arr) </p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)冒泡排序</p>
<p style="margin-left:.0001pt;text-align:justify;">冒泡排序也是一种简单的排序算法。</p>
<p style="margin-left:.0001pt;text-align:justify;">它的基本原理是:重复地扫描要排序的数列,一次比较两个元素,如果它们的大小顺序错误,就把它们交换过来。这样,一次扫描结束,我们可以确保最大(小)的值被移动到序列末尾。这个算法的名字由来,就是因为越小的元素会经由交换,慢慢“浮”到数列的顶端。</p>
<p><img alt="" src="https://i-blog.csdnimg.cn/direct/fb51f95685ba445c861e1ea013a7f7ec.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">冒泡排序的代码实现也非常简单,同样是使用双重for循环:</p>
<p style="margin-left:.0001pt;text-align:left;">// 冒泡排序</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < size; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < size - i - 1; j++) </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (arr[j] > arr[j+1])</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = arr[j+1];</p>
<p style="margin-left:.0001pt;text-align:left;">arr[j+1] = arr[j];</p>
<p style="margin-left:.0001pt;text-align:left;">arr[j] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h4 style="text-align:justify;"><strong><strong><strong>6.2 模板类vector</strong></strong><strong><strong>简介</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">数组尽管很灵活,但使用起来还是很多不方便。为此,C++语言定义了扩展的“抽象数据类型”(Abstract Data Type, ADT),放在“标准库”中。</p>
<p style="margin-left:.0001pt;text-align:justify;">对数组功能进行扩展的一个标准库类型,就是“容器”vector。顾名思义,vector“容纳”着一堆数据对象,其实就是一组类型相同的数据对象的集合。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.2.1 头文件和命名空间</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">vector是标准库的一部分。要想使用vector,必须在程序中包含<vector>头文件,并使用std命名空间。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<vector></p>
<p style="margin-left:.0001pt;text-align:justify;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:justify;">在vector头文件中,对vector这种类型做了定义;使用#include引入它之后,并指定命名空间std之后,我们就可以在代码中直接使用vector了。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.2.2 vector的基本用法</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">vector其实是C++中的一个“类模板”,是用来创建类的“模子”。所以在使用时还必须提供具体的类型信息,也就是说,这个容器中到底要容纳什么类型的数据对象;具体的形式是在vector后面跟一个尖括号<>,里面填入具体类型信息。</p>
<p style="margin-left:.0001pt;text-align:justify;">vector<int> v;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(1)初始化</p>
<p style="margin-left:.0001pt;text-align:justify;">跟数组相比,vector的初始化更加灵活方便,可以应对各种不同的需求。</p>
<p style="margin-left:.0001pt;text-align:left;">// 默认初始化,不含任何元素</p>
<p style="margin-left:.0001pt;text-align:left;">vector<int> v1;</p>
<p style="margin-left:.0001pt;text-align:left;">// 列表初始化</p>
<p style="margin-left:.0001pt;text-align:left;">vector<char> v2 = {'a', 'b', 'c'};</p>
<p style="margin-left:.0001pt;text-align:left;">// 省略等号的列表初始化</p>
<p style="margin-left:.0001pt;text-align:left;">vector<short> v3{1,2,3,4,5};</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 只定义长度,元素初值默认初始化,容器中有5个0</p>
<p style="margin-left:.0001pt;text-align:left;">vector<int> v4(5);</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义长度和初始值,容器中有5个100</p>
<p style="margin-left:.0001pt;text-align:justify;">vector<long> v5(5, 100);</p>
<p style="margin-left:.0001pt;text-align:justify;">这里有几种不同的初始化方式:</p>
<ol><li>默认初始化一个vector对象,就是一个空容器,里面不含任何元素;</li><li>C++ 11之后可以用花括号括起来的列表,对vector做初始化;等号可以省略;这种方式是把一个列表拷贝给了vector,称为“拷贝初始化”</li><li>可以用小括号表示初始化vector的长度,并且可以给所有元素指定相同的初始值;这种方式叫做“直接初始化”</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)访问元素</p>
<p style="margin-left:.0001pt;text-align:justify;">vector是包含了数据对象的“容器”,在这个容器集合中,每个数据对象都会有一个编号,用来做方便快速的访问;这个编号就是“索引”(index)。同样可以用下标操作符来获取对应索引的元素,这一点跟数组非常相似。</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "v5[2] = " << v5[2] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">v5[4] = 32;</p>
<p style="margin-left:.0001pt;text-align:justify;">//v5[5] = 16; // 严重错误!不能越界访问索引</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意:</p>
<ol><li>vector内元素的索引,也是从0开始;</li><li>vector索引最大值为 (vector长度 - 1),不能越界访问;如果直接越界访问并赋值,有可能导致非常严重的后果,出现安全问题</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)遍历所有元素</p>
<p style="margin-left:.0001pt;text-align:justify;">vector中有一个可以调用的函数size(),只要调用它就能直接得到vector的长度(即元素个数):</p>
<p style="margin-left:.0001pt;text-align:left;">// 获取vector的长度</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << v5.size() << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">调用的方式是一个vector对象后面跟上一个点,再跟上size()。这种基于对象来调用的函数叫做“成员函数”。</p>
<p style="margin-left:.0001pt;text-align:justify;">这样我们就可以非常方便地用for循环遍历元素了:</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < v5.size(); i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << v5 << "\t";</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">当然,用范围for循环同样非常简单:</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num: v5)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)添加元素</p>
<p style="margin-left:.0001pt;text-align:justify;">vector的长度并不是固定的,所以可以向一个定义好的vector添加元素。</p>
<p style="margin-left:.0001pt;text-align:left;">// 在定义好的vector中添加元素</p>
<p style="margin-left:.0001pt;text-align:left;">v5.push_back(69);</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : v5)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里的push_back同样是一个成员函数,调用它的时候在小括号里传入想要添加的数值,就可以让vector对象中增加一个元素了。</p>
<p style="margin-left:.0001pt;text-align:justify;">这就使得我们在创建vector对象时不需要知道元素个数,使用更加灵活,避免了数组中的缺陷。</p>
<p style="margin-left:.0001pt;text-align:justify;">下面的代码创建了一个空vector,并使用添加元素的方式给它赋值为倒序的10~1:</p>
<p style="margin-left:.0001pt;text-align:left;">vector<int> vec;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 10; i > 0; i--)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">vec.push_back(i);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.2.3 vector和数组的区别</strong></strong></strong></h5>
<ol><li>数组是更加底层的数据类型;长度固定,功能较少,安全性没有保证;但性能更好,运行更高效;</li><li>vector是模板类,是数组的上层抽象;长度不定,功能强大;缺点是运行效率较低;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">除了vector之外,C++ 11 还新增了一个array模板类,它跟数组更加类似,长度是固定的,但更加方便、更加安全。所以在实际应用中,一般推荐对于固定长度的数组使用array,不固定长度的数组使用vector。</p>
<h4 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>3</strong></strong><strong> </strong><strong><strong>字符串</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">字符串我们并不陌生。之前已经介绍过,一串字符连在一起就是一个“字符串”,比如用双引号引起来的“Hello World!”就是一个字符串字面值。</p>
<p style="margin-left:.0001pt;text-align:justify;">字符串其实就是所谓的“纯文本”,就是各种文字、数字、符号在一起表达的一串信息;所以字符串就是C++中用来表达和处理文本信息的数据类型。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>3</strong></strong><strong><strong>.1 </strong></strong><strong><strong>标准库类型string</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">C++的标准库中,提供了一种用来表示字符串的数据类型string,这种类型能够表示长度可变的字符序列。和vector类似,string类型也定义在命名空间std中,使用它必须包含string头文件。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<string></p>
<p style="margin-left:.0001pt;text-align:justify;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)定义和初始化string</p>
<p style="margin-left:.0001pt;text-align:justify;">我们已经接触过C++中几种不同的初始化方式,string也是一个标准库类型,它的初始化与vector非常相似。</p>
<p style="margin-left:.0001pt;text-align:left;">// 默认初始化,空字符串</p>
<p style="margin-left:.0001pt;text-align:left;">string s1;</p>
<p style="margin-left:.0001pt;text-align:left;">// 用另一个字符串变量,做拷贝初始化</p>
<p style="margin-left:.0001pt;text-align:left;">string s2 = s1;</p>
<p style="margin-left:.0001pt;text-align:left;">// 用一个字符串字面值,做拷贝初始化</p>
<p style="margin-left:.0001pt;text-align:left;">string s3 = "Hello World!";</p>
<p style="margin-left:.0001pt;text-align:left;">// 用一个字符串字面值,做直接初始化</p>
<p style="margin-left:.0001pt;text-align:left;">string s4("hello world");</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义字符和重复的次数,做直接初始化,得到 hhhhhhhh</p>
<p style="margin-left:.0001pt;text-align:justify;">string s5(8, 'h');</p>
<p style="margin-left:.0001pt;text-align:justify;">初始化方式主要有:</p>
<ol><li>默认初始化,得到的就是一个空字符串;</li><li>拷贝初始化,用赋值运算符(等号“=”)表示;可以使用另一个string对象,也可以使用字符串字面值常量;</li><li>直接初始化,用括号表示;可以在括号中传入一个字符串,也可以传入字符和重复的次数</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">可以发现,字符串也可以看做数据元素的集合;它里面的元素,就是字符。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)处理字符串中的字符</p>
<p style="margin-left:.0001pt;text-align:justify;">通过初始化已经可以看出,string的行为与vector非常类似。string同样也可以通过下标运算符访问内部的每个字符。字符的“索引”,就是在字符串中的位置。</p>
<p style="margin-left:.0001pt;text-align:left;">string str = "hello world";</p>
<p style="margin-left:.0001pt;text-align:left;">// 获取第3个字符</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "str[2] = " << str[2] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">// 将第1个字符改为'H'</p>
<p style="margin-left:.0001pt;text-align:left;">str[0] = 'H';</p>
<p style="margin-left:.0001pt;text-align:left;">// 将最后一个字符改为'D'</p>
<p style="margin-left:.0001pt;text-align:left;">str[str.size() - 1] = 'D';</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "str = " << str << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">字符串内字符的访问,跟vector内元素的访问类似,需要注意:</p>
<ol><li>string内字符的索引,也是从0开始;</li><li>string同样有一个成员函数size,可以获取字符串的长度;</li><li>索引最大值为 (字符串长度 - 1),不能越界访问;如果直接越界访问并赋值,有可能导致非常严重的后果,出现安全问题;</li><li>如果希望遍历字符串的元素,也可以使用普通for循环和范围for循环,依次获取每个字符</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">比如,我们可以考虑遍历所有字符,将小写字母换成大写:</p>
<p style="margin-left:.0001pt;text-align:left;">// 遍历字符串中字符,将小写字母变成大写</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < str.size(); i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">str = toupper(str);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里又调用了string的一个函数toupper,可以把传入的字符转换成大写并返回。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)字符串相加</p>
<p style="margin-left:.0001pt;text-align:justify;">string本身的长度是不定的,可以通过“相加”的方式扩展一个字符串。</p>
<p style="margin-left:.0001pt;text-align:left;">// 字符串相加</p>
<p style="margin-left:.0001pt;text-align:left;">string str1 = "hello", str2("world");</p>
<p style="margin-left:.0001pt;text-align:left;">string str3 = str1 + str2; // str3 = "helloworld"</p>
<p style="margin-left:.0001pt;text-align:left;">string str4 = str1 + ", " + str2 + "!"; // str4 = "hello, world!"</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:justify;">//string str5 = "hello, " + "world!"; // 错误,不能将两个字符串字面值相加</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意:</p>
<ol><li>字符串相加使用加号“+”来表示,这是算术运算符“+”的运算符重载,含义是“字符串拼接”;</li><li>两个string对象,可以直接进行字符串相加;结果是将两个字符串拼接在一起,得到一个新的string对象返回;</li><li>一个string对象和一个字符串字面值常量,可以进行字符串相加,同样是得到一个拼接后的string对象返回;</li><li>两个字符串字面值常量,不能相加;</li><li>多个string对象和多个字符串字面值常量,可以连续相加;前提是按照左结合律,每次相加必须保证至少有一个string对象;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(4)比较字符串</p>
<p style="margin-left:.0001pt;text-align:justify;">string类还提供几种用来做字符串比较的运算符,“==”和“!=”用来判断两个字符串是否完全一样;而“<”“>”“<=”“>=”则用来比较两个字符串的大小。这些都是关系型运算符的重载。</p>
<p style="margin-left:.0001pt;text-align:left;">str1 = "hello";</p>
<p style="margin-left:.0001pt;text-align:left;">str2 = "hello world!";</p>
<p style="margin-left:.0001pt;text-align:left;">str3 = "hehehe";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">str1 == str2; // false</p>
<p style="margin-left:.0001pt;text-align:left;">str1 < str2; // true</p>
<p style="margin-left:.0001pt;text-align:justify;">str1 >= str3; // true</p>
<p style="margin-left:.0001pt;text-align:justify;">字符串比较的规则为:</p>
<ol><li>如果两个字符串长度相同,每个位置包含的字符也都相同,那么两者“相等”;否则“不相等”;</li><li>如果两个字符串长度不同,而较短的字符串每个字符都跟较长字符串对应位置字符相同,那么较短字符串“小于”较长字符串;</li><li>如果两个字符串在某一位置上开始不同,那么就比较这两个字符的ASCII码,比较结果就代表两个字符串的大小关系</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>3</strong></strong><strong><strong>.2 </strong></strong><strong><strong>字符数组(C风格字符串)</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">通过对string的介绍可以发现,字符串就是一串字符的集合,本质上其实就是一个“字符的数组”。</p>
<p style="margin-left:.0001pt;text-align:justify;">在C语言中,确实是用char[]类型来表示字符串的;不过为了区分纯粹的“字符数组”和“字符串”,C语言规定:字符串必须以空字符结束。空字符的ASCII码为0,专门用来标记字符串的结尾,在程序中写作’\0’。</p>
<p style="margin-left:.0001pt;text-align:left;">// str1没有结尾空字符,并不是一个字符串</p>
<p style="margin-left:.0001pt;text-align:left;">char str1[5] = {'h','e','l','l','o'};</p>
<p style="margin-left:.0001pt;text-align:left;">// str2是一个字符串</p>
<p style="margin-left:.0001pt;text-align:left;">char str2[6] = { 'h','e','l','l','o','\0'};</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "str1 = " << str1 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "str2 = " << str2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">如果每次用到字符串都要这样定义,对程序员来说就非常不友好了。所以字符串可以用另一种更方便的形式定义出来,那就是使用双引号:</p>
<p style="margin-left:.0001pt;text-align:left;">char str3[] = "hello";</p>
<p style="margin-left:.0001pt;text-align:left;">//char str3[5] = "hello"; // 错误,"hello"的长度为6</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "str3 = " << str3 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这就是我们所熟悉的字符串“字面值常量”。这里需要注意的是,我们不需要再考虑末尾的空字符,编译器会自动帮我们补全;但真实的字符串的长度,依然要包含空字符,所以上面的字符串“hello”长度不是5、而是6。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">所以,C++中的字符串字面值常量,为了兼容C依然定义为字符数组(char[])类型,这和string是两种不同类型;两者的区别,跟数组和vector的区别类似,char[]是更底层的类型。一般情况下,使用string会带来更多方便,也会更加安全。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>6.3.3 读取输入的字符串</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">程序中往往需要一些交互操作,如果想获取从键盘输入的字符串,可以使用多种方法。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1) 使用输入操作符读取单词</p>
<p style="margin-left:.0001pt;text-align:justify;">标准库中提供了iostream,可以使用内置的cin对象,调用重载的输入操作符>>来读取键盘输入。</p>
<p style="margin-left:.0001pt;text-align:left;">string str;</p>
<p style="margin-left:.0001pt;text-align:left;">// 读取键盘输入,遇到空白符停止</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> str;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << str;</p>
<p style="margin-left:.0001pt;text-align:justify;">这种方式的特点是:忽略开始的空白符,遇到下一个空白符(空格、回车、制表等)就会停止。所以如果我们输入“hello world”,那么读取给str的只有“hello”:这相当于读取了一个“单词”。</p>
<p style="margin-left:.0001pt;text-align:justify;">剩下的内容“world”其实也没有丢,而是保存在了输入流的“输入队列”里。如果我们想读取更多的输入信息,就需要使用更多的string对象来获取:</p>
<p style="margin-left:.0001pt;text-align:left;">string str1, str2;</p>
<p style="margin-left:.0001pt;text-align:left;">cin >> str1 >> str2;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << str1 << str2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这样,如果输入“hello world”,就可以输出“helloworld”。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)使用getline读取一行</p>
<p style="margin-left:.0001pt;text-align:justify;">如果希望直接读取一整行输入信息,可以使用getline函数来替代输入操作符。</p>
<p style="margin-left:.0001pt;text-align:left;">string str3;</p>
<p style="margin-left:.0001pt;text-align:left;">getline(cin, str3);</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "str3 = " << str3 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">getline函数有两个参数:一个是输入流对象cin,另一个是保存字符串的string对象;它会一直读取输入流中的内容,直到遇到换行符为止,然后把所有内容保存到string对象中。所以现在可以完整读取一整行信息了。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)使用get读取字符</p>
<p style="margin-left:.0001pt;text-align:justify;">还有一种方法,是调用cin的get函数读取一个字符。</p>
<p style="margin-left:.0001pt;text-align:left;">char ch;</p>
<p style="margin-left:.0001pt;text-align:left;">ch = cin.get(); // 将捕获到的字符赋值给ch</p>
<p style="margin-left:.0001pt;text-align:justify;">cin.get(ch); // 直接将ch作为参数传给get</p>
<p style="margin-left:.0001pt;text-align:justify;">有两种方式:</p>
<ol><li>调用cin.get()函数,不传参数,得到一个字符赋给char类型变量;</li><li>将char类型变量作为参数传入,将捕获的字符赋值给它,返回的是istream对象</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">get函数还可以读取一行内容。这种方式跟getline很相似,也可以读取一整行内容,以回车结束。主要区别在于,它需要把信息保存在一个char[]类型的字符数组中,调用的是cin的成员函数:</p>
<p style="margin-left:.0001pt;text-align:left;">// get读取一整行</p>
<p style="margin-left:.0001pt;text-align:left;">char str4[20];</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get(str4, 20);</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "str4 = " << str4 << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// get读取一个字符</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get(); // 先读取之前留下的回车符</p>
<p style="margin-left:.0001pt;text-align:justify;">cin.get(); // 再等待下一次输入</p>
<p style="margin-left:.0001pt;text-align:justify;">get函数同样需要传入两个参数:一个是保存信息的字符数组,另一个是字符数组的长度。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里还要注意跟getline的另一个区别:键盘输入总是以回车作为结束的;getline会把最后的回车符丢弃,而get会将回车符保留在输入队列中。</p>
<p style="margin-left:.0001pt;text-align:justify;">这样的效果是,下次再调用get试图读取一行数据时,会因为直接读到了回车符而返回空行。这就需要再次调用get函数,捕获下一个字符:</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get(); // 先读取之前留下的回车符</p>
<p style="margin-left:.0001pt;text-align:justify;">cin.get(); // 再等待下一次输入</p>
<p style="margin-left:.0001pt;text-align:justify;">这样就可以将之前的回车符捕获,从而为读取下一行做好准备。这也就解释了之前为什么要写两个cin.get():第一个用来处理之前保留在输入队列的回车符;第二个用来等待下一次输入,让窗口保持开启状态。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.3.4 简单读写文件</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">实际应用中,我们往往会遇到读写文件的需求,这也是一种IO操作,整体用法跟命令行的输入输出非常类似。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++的IO库中提供了专门用于文件输入的ifstream类和用于文件输出的ofstream类,要使用它们需要引入头文件fstream。ifstream用于读取文件内容,跟istream的用法类似;也可以通过输入操作符>>来读“单词”(空格分隔),通过getline函数来读取一行,通过get函数来读取一个字符:</p>
<p style="margin-left:.0001pt;text-align:left;">ifstream input("input.txt");</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 逐词读取</p>
<p style="margin-left:.0001pt;text-align:left;">string word;</p>
<p style="margin-left:.0001pt;text-align:left;">while (input >> word) </p>
<p style="margin-left:.0001pt;text-align:left;">cout << word << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 逐行读取</p>
<p style="margin-left:.0001pt;text-align:left;">string line;</p>
<p style="margin-left:.0001pt;text-align:left;">while (getline(input, line))</p>
<p style="margin-left:.0001pt;text-align:left;">cout << line << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 逐字符读取</p>
<p style="margin-left:.0001pt;text-align:left;">char ch;</p>
<p style="margin-left:.0001pt;text-align:left;">while (input.get(ch))</p>
<p style="margin-left:.0001pt;text-align:left;">cout << ch << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">类似地,写入文件也可以通过使用输出运算符 << 来实现:</p>
<p style="margin-left:.0001pt;text-align:left;">ofstream output("output.txt");</p>
<p style="margin-left:.0001pt;text-align:left;">output << word << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h4 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>4</strong></strong><strong> </strong><strong><strong>结构体</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">实际应用中,我们往往希望把很多不同的信息组合起来,“打包”存储在一个单元中。比如一个学生的信息,可能包含了姓名、年龄、班级、成绩…这些信息的数据类型可能是不同的,所以数组和vector都无法完成这样的功能。</p>
<p style="margin-left:.0001pt;text-align:justify;">C/C++中提供了另一种更加灵活的数据结构——结构体。结构体是用户自定义的复合数据结构,里面可以包含多个不同类型的数据对象。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.4.1 结构体的声明</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">声明一个结构体需要使用struct关键字,具体形式如下:</p>
<p style="margin-left:.0001pt;text-align:justify;">struct <em>结构体名</em></p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;"> <em>类型1 数据对象1</em>;</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>类型2 数据对象2</em>;</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>类型3 数据对象3</em>;</p>
<p style="margin-left:.0001pt;text-align:justify;"> …</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;">结构体中数据对象的类型和个数都可以自定义,这为数据表达提供了极大的灵活性。结构体可以说是迈向面向对象世界中“类”概念的第一步。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以尝试定义这样一个“学生信息”结构体:</p>
<p style="margin-left:.0001pt;text-align:left;">struct studentInfo </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string name;</p>
<p style="margin-left:.0001pt;text-align:left;">int age;</p>
<p style="margin-left:.0001pt;text-align:left;">double score;</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;">这个结构体中包含了三个数据对象:string类型的名字name,int类型的年龄age,以及double类型的成绩score。一般会把结构体定义在主函数外面,称为“外部定义”,这样可以方便外部访问。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.4.2 结构体初始化</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">定义好结构之后,就产生了一个新的类型,叫做“studentInfo”。接下来就可以创建这种类型的对象,并做初始化了。</p>
<p style="margin-left:.0001pt;text-align:left;">// 创建对象并初始化</p>
<p style="margin-left:.0001pt;text-align:justify;">studentInfo stu = {"张三", 20, 60.0};</p>
<p style="margin-left:.0001pt;text-align:justify;">结构体对象的初始化非常简单,跟数组完全一样:只要按照对应顺序一次赋值,逗号分隔,最后用花括号括起来就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;">结构体还支持其它一些初始化方式:</p>
<p style="margin-left:.0001pt;text-align:left;">struct studentInfo </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string name;</p>
<p style="margin-left:.0001pt;text-align:left;">int age;</p>
<p style="margin-left:.0001pt;text-align:left;">double score;</p>
<p style="margin-left:.0001pt;text-align:justify;">}stu1, stu2 = {"小明", 18, 75.0}; // 定义结构体之后立即创建对象</p>
<p style="margin-left:.0001pt;text-align:left;">// 使用列表初始化</p>
<p style="margin-left:.0001pt;text-align:justify;">studentInfo stu3{"李四", 22, 87};</p>
<p style="margin-left:.0001pt;text-align:left;">// 使用另一结构体对象进行赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">studentInfo stu4 = stu2;</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意:</p>
<ol><li>创建结构体变量对象时,可以直接用定义好的结构体名作为类型;相比C语言中的定义,这里省略了关键字struct</li><li>不同的初始化方式效果相同,在不同位置定义的对象作用域不同;</li><li>如果没有赋初始值,那么所有数据将被初始化为默认值;算术类型的默认值就是0;</li><li>一般在代码中,会将结构体的定义和对象的创建分开,便于理解和管理</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>6.4.3 访问结构体中数据</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">访问结构体变量中的数据成员,可以使用成员运算符(点号.),后面跟上数据成员的名称。例如stu.name就可以访问stu对象的name成员。</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "学生姓名:" << stu.name << "\t年龄:" << stu.age << "\t成绩:" << stu.score << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这种访问内部成员的方式非常经典,后面要讲到的类的操作中,也会用这种方式访问自己的成员函数。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.4.4 结构体数组</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">可以把结构体和数组结合起来,创建结构体的数组。顾名思义,结构体数组就是元素为结构体的数组,它的定义和访问跟普通的数组完全一样。</p>
<p style="margin-left:.0001pt;text-align:left;">// 结构体数组</p>
<p style="margin-left:.0001pt;text-align:left;">studentInfo s[2] = {</p>
<p style="margin-left:.0001pt;text-align:left;">{"小红", 18, 92},</p>
<p style="margin-left:.0001pt;text-align:left;">{"小白", 20, 82}</p>
<p style="margin-left:.0001pt;text-align:left;">};</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "学生姓名:" << s[0].name << "\t年龄:" << s[0].age << "\t成绩:" << s[0].score << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "学生姓名:" << s[1].name << "\t年龄:" << s[1].age << "\t成绩:" << s[1].score << endl;</p>
<h4 style="text-align:justify;"><strong><strong><strong>6.5 枚举</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">实际应用中,经常会遇到某个数据对象只能取有限个常量值的情况,比如一周有7天,一副扑克牌有4种花色等等。对于这种情况,C++提供了另一种批量创建符号常量的方式,可以替代const。这就是“枚举”类型enum。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.5.1 枚举类型定义</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">枚举类型的定义和结构体非常像,需要使用enum关键字。</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义枚举类型</p>
<p style="margin-left:.0001pt;text-align:left;">enum week</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">Mon, Tue, Wed, Thu, Fri, Sat, Sun</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;">与结构体不同的是,枚举类型内只有有限个名字,它们都各自代表一个常量,被称为“枚举量”。</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意的是:</p>
<ol><li>默认情况下,会将整数值赋给枚举量;</li><li>枚举量默认从0开始,每个枚举量依次加1;所以上面week枚举类型中,一周七天枚举量分别对应着0~6的常量值;</li><li>可以通过对枚举量赋值,显式地设置每个枚举量的值</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>6.5.2 使用枚举类型</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">使用枚举类型也很简单,创建枚举类型的对象后,只能将对应类型的枚举量赋值给它;如果打印它的值,将会得到对应的整数。</p>
<p style="margin-left:.0001pt;text-align:left;">week w1 = Mon;</p>
<p style="margin-left:.0001pt;text-align:left;">week w2 = Tue;</p>
<p style="margin-left:.0001pt;text-align:left;">//week w3 = 2; // 错误,类型不匹配</p>
<p style="margin-left:.0001pt;text-align:left;">week w3 = week(3); // int类型强转为week类型后赋值</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "w1 = " << w1 << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "w2 = " << w2 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "w3 = " << w3 << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意:</p>
<ol><li>如果直接用一个整型值对枚举类型赋值,将会报错,因为类型不匹配;</li><li>可以通过强制类型转换,将一个整型值赋值给枚举对象;</li><li>最初的枚举类型只有列出的值是有效的;而现在C++通过强制类型转换,允许扩大枚举类型合法值的范围。不过一般使用枚举类型要避免直接强转赋值。</li></ol>
<h4 style="text-align:justify;"><strong><strong><strong>6.6 指针</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">计算机中的数据都存放在内存中,访问内存的最小单元是“字节”(byte)。所有的数据,就保存在内存中具有连续编号的一串字节里。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/26e728b3fb04bda2c08c0b727f5d0500.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">指针顾名思义,是“指向”另外一种数据类型的复合类型。指针是C/C++中一种特殊的数据类型,它所保存的信息,其实是另外一个数据对象在内存中的“地址”。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/2056408560dc85f43cf44eedc21dd74b.png" />
</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.6.1 指针的定义</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">指针的定义语法形式为:</p>
<p style="margin-left:.0001pt;text-align:justify;"><em>类型</em> * <em>指针变量</em>;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里的类型就是指针所指向的数据类型,后面加上星号“*”,然后跟指针变量的名称。指针在定义的时候可以不做初始化。相比一般的变量声明,看起来指针只是多了一个星号“*”而已。例如:</p>
<p style="margin-left:.0001pt;text-align:left;">int* p1; // p1是指向int类型数据的指针</p>
<p style="margin-left:.0001pt;text-align:left;">long* p2; // p2是指向long类型数据的指针</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "p1在内存中长度为:" << sizeof(p1) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "p2在内存中长度为:" << sizeof(p2) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">p1、p2就是两个指针,分别指向int类型和long类型的数据对象。</p>
<p style="margin-left:.0001pt;text-align:justify;">指针的本质,其实就是一个整数表示的内存地址,它本身在内存中所占大小跟系统环境有关,而跟指向的数据类型无关。64位编译环境中,指针统一占8个字节;若是32位系统则占4字节。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.6.2 指针的用法</strong></strong></strong></h5>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/60ada796cf7264ca2507280593eb2467.png" />
</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/376a6ea3c4cce379e79fd5ae9b14ff33.png" />
</p>
<p style="margin-left:.0001pt;text-align:center;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(1)获取对象地址给指针赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">指针保存的是数据对象的内存地址,所以可以用地址给指针赋值;获取对象地址的方式是使用“取地址操作符”(&)。</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 12;</p>
<p style="margin-left:.0001pt;text-align:left;">int b = 100;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的地址为:" << &a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "b的地址为:" << &b << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int* p = &b; // p是指向b的指针</p>
<p style="margin-left:.0001pt;text-align:justify;">p = &a; // p指向了a</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "p = " << p << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">把指针当做一个变量,可以先指向一个对象,再指向另一个不同的对象。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)通过指针访问对象</p>
<p style="margin-left:.0001pt;text-align:justify;">指针指向数据对象后,可以通过指针来访问对象。访问方式是使用“解引用操作符”(*):</p>
<p style="margin-left:.0001pt;text-align:left;">p = &a; // p是指向a的指针</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "p指向的内存中,存放的值为:" << *p << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">*p = 25; // 将p所指向的对象(a),修改为25</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">在这里由于p指向了a,所以*p可以等同于a。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.6.3</strong></strong><strong> </strong><strong><strong>无效指针、空指针和void*指针</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">(1)无效指针</p>
<p style="margin-left:.0001pt;text-align:justify;">定义一个指针之后,如果不进行初始化,那么它的内容是不确定的(比如0xcccc)。如果这时把它的内容当成一个地址去访问,就可能访问的是不存在的对象;更可怕的是,如果访问到的是系统核心内存区域,修改其中内容会导致系统崩溃。这样的指针就是“无效指针”,也被叫做“野指针”。</p>
<p style="margin-left:.0001pt;text-align:left;">int* p1;</p>
<p style="margin-left:.0001pt;text-align:justify;">//*p1 = 100; // 危险!指针没有初始化,是无效指针</p>
<p style="margin-left:.0001pt;text-align:justify;">指针非常灵活非常强大,但野指针非常危险。所以建议使用指针的时候,一定要先初始化,让它指向真实的对象。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)空指针</p>
<p style="margin-left:.0001pt;text-align:justify;">如果先定义了一个指针,但确实还不知道它要指向哪个对象,这时可以把它初始化为“空指针”。空指针不指向任何对象。</p>
<p style="margin-left:.0001pt;text-align:left;">int* np = nullptr; // 空指针字面值</p>
<p style="margin-left:.0001pt;text-align:left;">np = NULL; // 预处理变量</p>
<p style="margin-left:.0001pt;text-align:left;">np = 0; // 0值</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int zero = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">//np = zero; // 错误,int变量不能赋值给指针</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "np = " << np << endl; // 输出0地址</p>
<p style="margin-left:.0001pt;text-align:justify;">//cout << "*np = " << *np << endl; // 错误,不能访问0地址的内容</p>
<p style="margin-left:.0001pt;text-align:justify;">空指针有几种定义方式:</p>
<ol><li>使用字面值nullptr,这是C++ 11 引入的方式,推荐使用;</li><li>使用预处理变量NULL,这是老版本的方式;</li><li>直接使用0值;</li><li>另外注意,不能直接用整型变量给指针赋值,即使值为0也不行</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">所以可以看出,空指针所保存的其实就是0值,一般把它叫做“0地址”;这个地址也是内存中真实存在的,所以也不允许访问。</p>
<p style="margin-left:.0001pt;text-align:justify;">空指针一般在程序中用来做判断,看一个指针是否指向了数据对象。</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)void * 指针</p>
<p style="margin-left:.0001pt;text-align:justify;">一般来说,指针的类型必须和指向的对象类型匹配,否则就会报错。不过有一种指针比较特殊,可以用来存放任意对象的地址,这种指针的类型是void*。</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 10;</p>
<p style="margin-left:.0001pt;text-align:left;">string s = "hello";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">void* vp = &i;</p>
<p style="margin-left:.0001pt;text-align:left;">vp = &s;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "vp = " << vp << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "vp的长度为: " << sizeof(vp) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">//cout << "*vp = " << *vp << endl; // 错误,不能通过void *指针访问对象</p>
<p style="margin-left:.0001pt;text-align:justify;">void* 指针表示只知道“保存了一个地址”,至于这个地址对应的数据对象是什么类型并不清楚。所以不能通过 void* 指针访问对象;一般 void* 指针只用来比较地址、或者作为函数的输入输出。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.6.4 指向指针的指针</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">指针本身也是一个数据对象,也有自己的内存地址。所以可以让一个指针保存另一个指针的地址,这就是“指向指针的指针”,有时也叫“二级指针”;形式上可以用连续两个的星号**来表示。类似地,如果是三级指针就是***,表示“指向二级指针的指针”。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/1df488a47d68621514150a43541cbc27.png" />
</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 1024;</p>
<p style="margin-left:.0001pt;text-align:left;">int* pi = &i; // pi是一个指针,指向int类型的数据</p>
<p style="margin-left:.0001pt;text-align:left;">int** ppi = π // ppi是一个二级指针,指向一个int* 类型的指针</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "pi = " << pi << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "* pi = " << * pi << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ppi = " << ppi << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "* ppi = " << * ppi << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "** ppi = " << ** ppi << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">如果需要访问二级指针所指向的最原始的那个数据,应该做两次解引用操作。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.6.5 指针和const</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">指针可以和const修饰符结合,这可以有两种形式:一种是指针指向的是一个常量;另一种是指针本身是一个常量。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)指向常量的指针</p>
<p style="margin-left:.0001pt;text-align:justify;">指针指向的是一个常量,所以只能访问数据,不能通过指针对数据进行修改。不过指针本身是变量,可以指向另外的数据对象。这时应该把const加在类型前。</p>
<p style="margin-left:.0001pt;text-align:left;">const int c = 10, c2 = 56;</p>
<p style="margin-left:.0001pt;text-align:left;">//int* pc = &c; // 错误,类型不匹配</p>
<p style="margin-left:.0001pt;text-align:left;">const int* pc = &c; // 正确,pc是指向常量的指针,类型为const int *</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">pc = &c2; // pc可以指向另一个常量</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 1024;</p>
<p style="margin-left:.0001pt;text-align:left;">pc = &i; // pc也可以指向变量</p>
<p style="margin-left:.0001pt;text-align:justify;">*pc = 1000; // 错误,不能通过pc更改数据对象</p>
<p style="margin-left:.0001pt;text-align:justify;">这里发现,pc是一个指向常量的指针,但其实把一个变量i的地址赋给它也是可以的;编译器只是不允许通过指针pc去间接更改数据对象。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)指针常量(const指针)</p>
<p style="margin-left:.0001pt;text-align:justify;">指针本身是一个数据对象,所以也可以区分变量和常量。如果指针本身是一个常量,就意味它保存的地址不能更改,也就是它永远指向同一个对象;而数据对象的内容是可以通过指针改变的。这种指针一般叫做“指针常量”。</p>
<p style="margin-left:.0001pt;text-align:justify;">指针常量在定义的时候,需要在星号*后、标识符前加上const。</p>
<p style="margin-left:.0001pt;text-align:left;">int* const cp = &i;</p>
<p style="margin-left:.0001pt;text-align:left;">*cp = 2048; // 通过指针修改对象的值</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "i = " << i << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">//cp = &c; // 错误,不可以更改cp的指向</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:justify;">const int* const ccp = &c; // ccp是一个指向常量的常量指针</p>
<p style="margin-left:.0001pt;text-align:justify;">这里也可以使用两个const,定义的是“指向常量的常量指针”。也就是说,ccp指向的是常量,值不能改变;而且它本身也是一个常量,指向的对象也不能改变。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>6</strong></strong><strong><strong>.</strong></strong><strong><strong>6</strong></strong><strong> </strong><strong><strong>指针和数组</strong></strong></strong></h5>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/0e871d6bb2232f044108c9c0443fd67f.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)数组名</p>
<p style="margin-left:.0001pt;text-align:justify;">用到数组名时,编译器一般都会把它转换成指针,这个指针就指向数组的第一个元素。所以我们也可以用数组名来给指针赋值。</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[] = {1,2,3,4,5};</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "arr = " << arr << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "&arr[0] = " << &arr[0] << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int* pia = arr; // 可以直接用数组名给指针赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "* pia = " << *pia << endl; // 指针指向的数据,就是arr[0]</p>
<p style="margin-left:.0001pt;text-align:justify;">也正是因为数组名被认为是指针,所以不能直接使用数组名对另一个数组赋值,数组也不允许这样的直接拷贝:</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[] = {1,2,3,4,5};</p>
<p style="margin-left:.0001pt;text-align:justify;">//int arr2[5] = arr; // 错误,数组不能直接拷贝</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)指针运算</p>
<p style="margin-left:.0001pt;text-align:justify;">如果对指针pia做加1操作,我们会发现它保存的地址直接加了4,这其实是指向了下一个int类型数据对象:</p>
<p style="margin-left:.0001pt;text-align:left;">pia + 1; // pia + 1 指向的是arr[1]</p>
<p style="margin-left:.0001pt;text-align:justify;">*(pia + 1); // 访问 arr[1]</p>
<p style="margin-left:.0001pt;text-align:justify;">所谓的“指针运算”,就是直接对一个指针加/减一个整数值,得到的结果仍然是指针。新指针指向的数据元素,跟原指针指向的相比移动了对应个数据单位。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)指针和数组下标</p>
<p style="margin-left:.0001pt;text-align:justify;">我们知道,数组名arr其实就是指针。这就带来了非常有趣的访问方式:</p>
<p style="margin-left:.0001pt;text-align:left;">* arr; // arr[0]</p>
<p style="margin-left:.0001pt;text-align:justify;">*(arr + 1); // arr[1]</p>
<p style="margin-left:.0001pt;text-align:justify;">这是通过指针来访问数组元素,效果跟使用下标运算符arr[0]、arr[1]是一样的。进而我们也可以发现,遍历元素所谓的“范围for循环”,其实就是让指针不停地向后移动依次访问元素。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(4)指针数组和数组指针</p>
<p style="margin-left:.0001pt;text-align:justify;">指针和数组这两种类型可以结合在一起,这就是“指针数组”和“数组指针”。</p>
<ol><li>指针数组:一个数组,它的所有元素都是相同类型的指针;</li><li>数组指针:一个指针,指向一个数组的指针;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int arr[] = {1,2,3,4,5};</p>
<p style="margin-left:.0001pt;text-align:left;">int* pa[5]; // 指针数组,里面有5个元素,每个元素都是一个int指针</p>
<p style="margin-left:.0001pt;text-align:left;">int(* ap)[5]; // 数组指针,指向一个int数组,数组包含5个元素</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "指针数组pr的大小为:" << sizeof(pa) << endl; // 40</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "数组指针ap的大小为:" << sizeof(ap) << endl; // 8</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">pa[0] = arr; // pa中第一个元素,指向arr的第一个元素</p>
<p style="margin-left:.0001pt;text-align:left;">pa[1] = arr + 1; // pa中第二个元素,指向arr的第二个元素</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">ap = &arr; // ap指向了arr整个数组</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "arr =" << arr << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "* arr =" << *arr << endl; // arr解引用,得到arr[0]</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "arr + 1 =" << arr + 1 << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ap =" << ap << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "* ap =" << *ap << endl; // ap解引用,得到的是arr数组</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ap + 1 =" << ap + 1 << endl; </p>
<p style="margin-left:.0001pt;text-align:justify;">这里可以看到,指向数组arr的指针ap,其实保存的也是arr第一个元素的地址。arr类型是int *,指向的就是arr[0];而ap类型是int (*) [5],指向的是整个arr数组。所以arr + 1,得到的是arr[1]的地址;而ap + 1,就会跨过整个arr数组。</p>
<h4 style="text-align:justify;"><strong><strong><strong>6.</strong></strong><strong><strong>7</strong></strong><strong> </strong><strong><strong>引用</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">我们可以在C++中为数据对象另外起一个名字,这叫做“引用”(reference)。</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.7.1 引用的用法</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在做声明时,我们可以在变量名前加上“&”符号,表示它是另一个变量的引用。引用必须被初始化。</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 10;</p>
<p style="margin-left:.0001pt;text-align:left;">int& ref = a; // ref是a的引用</p>
<p style="margin-left:.0001pt;text-align:left;">//int& ref2; // 错误,引用必须初始化</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ref = " << ref << endl; // ref等于a的值</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的地址为:" << &a << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "ref的地址为:" << &ref << endl; // ref和a的地址完全一样</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">引用本质上就是一个“别名”,它本身不是数据对象,所以本身不会存储数据,而是和初始值“绑定”(bind)在一起,绑定之后就不能再绑定别的对象了。</p>
<p style="margin-left:.0001pt;text-align:justify;">定义了应用之后,对引用做的所有操作,就像直接操作绑定的原始变量一样。所以,引用也是一种间接访问数据对象的方式。</p>
<p style="margin-left:.0001pt;text-align:left;">ref = 20; // 更改ref相当于更改a</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:left;">int b = 26;</p>
<p style="margin-left:.0001pt;text-align:left;">ref = b; // ref没有绑定b,而是把b的值赋给了ref绑定的a</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的地址为:" << &a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "b的地址为:" << &b << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ref的地址为:" << &ref << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">当然,既然是别名,那么根据这个别名再另起一个别名也是可以的:</p>
<p style="margin-left:.0001pt;text-align:left;">// 引用的引用</p>
<p style="margin-left:.0001pt;text-align:left;">int& rref = ref;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "rref = " << rref << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的地址为:" << &a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "ref的地址为:" << &ref << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "rref的地址为:" << &rref << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">“引用的引用”,是把引用作为另一个引用的初始值,其实就是给原来绑定的对象又绑定了一个别名,这两个引用绑定的是同一个对象。</p>
<p style="margin-left:.0001pt;text-align:justify;">要注意,引用只能绑定到对象上,而不能跟字面值常量绑定;也就是说,不能把一个字面值直接作为初始值赋给一个引用。而且,引用本身的类型必须跟绑定的对象类型一致。</p>
<p style="margin-left:.0001pt;text-align:left;">//int& ref2 = 10; // 错误,不能创建字面值的引用</p>
<p style="margin-left:.0001pt;text-align:left;">double d = 3.14;</p>
<p style="margin-left:.0001pt;text-align:justify;">//int& ref3 = d; // 错误,引用类型和原数据对象类型必须一致</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.7.2 对常量的引用</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">可以把引用绑定到一个常量上,这就是“对常量的引用”。很显然,对常量的引用是常量的别名,绑定的对象不能修改,所以也不能做赋值操作:</p>
<p style="margin-left:.0001pt;text-align:left;">const int zero = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">//int& cref = zero; // 错误,不能用普通引用去绑定常量</p>
<p style="margin-left:.0001pt;text-align:left;">const int& cref = zero; // 常量的引用</p>
<p style="margin-left:.0001pt;text-align:justify;">//cref = 10; // 错误,不能对常量赋值</p>
<p style="margin-left:.0001pt;text-align:justify;">对常量的引用有时也会直接简称“常量引用”。因为引用只是别名,本身不是数据对象;所以这只能代表“对一个常量的引用”,而不会像“常量指针”那样引起混淆。</p>
<p style="margin-left:.0001pt;text-align:justify;">常量引用和普通变量的引用不同,它的初始化要求宽松很多,只要是可以转换成它指定类型的所有表达式,都可以用来做初始化。</p>
<p style="margin-left:.0001pt;text-align:left;">const int& cref2 = 10; // 正确,可以用字面值常量做初始化</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 35;</p>
<p style="margin-left:.0001pt;text-align:left;">const int& cref3 = i; // 正确,可以用一个变量做初始化</p>
<p style="margin-left:.0001pt;text-align:left;">double d = 3.14;</p>
<p style="margin-left:.0001pt;text-align:justify;">const int& cref4 = d; // 正确,d会先转成int类型,引用绑定的是一个“临时量”</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这样一来,常量引用和对变量的引用,都可以作为一个变量的“别名”,区别在于不能用常量引用去修改对象的值。</p>
<p style="margin-left:.0001pt;text-align:left;">int var = 10;</p>
<p style="margin-left:.0001pt;text-align:left;">int& r1 = var;</p>
<p style="margin-left:.0001pt;text-align:left;">const int& r2 = var;</p>
<p style="margin-left:.0001pt;text-align:left;">r1 = 25;</p>
<p style="margin-left:.0001pt;text-align:justify;">//r2 = 35; // 错误,不能通过const引用修改对象值</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.7.3 指针和引用</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">从上一节中可以看到,常量引用和指向常量的指针,有很类似的地方:它们都可以绑定/指向一个常量,也可以绑定/指向一个变量;但不可以去修改对应的变量对象。所以很明显,指针和引用有很多联系。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)引用和指针常量</p>
<p style="margin-left:.0001pt;text-align:justify;">事实上,引用的行为,非常类似于“指针常量”,也就是只能指向唯一的对象、不能更改的指针。</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 10;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 引用的行为,和指针常量非常类似</p>
<p style="margin-left:.0001pt;text-align:left;">int& r = a;</p>
<p style="margin-left:.0001pt;text-align:left;">int* const p = &a;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">r = 20;</p>
<p style="margin-left:.0001pt;text-align:left;">*p = 30;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a = " << a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "a的地址为:" << &a << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "r = " << r << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "r的地址为:" << &r << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << "*p = " << *p << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << "p = " << p << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">可以看到,所有用到引用r的地方,都可以用*p替换;所有需要获取地址&r的地方,也都可以用p替换。这也就是为什么把操作符*,叫做“解引用”操作符。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)指针的引用</p>
<p style="margin-left:.0001pt;text-align:justify;">指针本身也是一个数据对象,所以当然也可以给它起别名,用一个引用来绑定它。</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 56, j = 28;;</p>
<p style="margin-left:.0001pt;text-align:left;">int* ptr = &i; // ptr是一个指针,指向int类型对象</p>
<p style="margin-left:.0001pt;text-align:left;">int*& pref = ptr; // pref是一个引用,绑定指针ptr</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">pref = &j; // 将指针ptr指向j</p>
<p style="margin-left:.0001pt;text-align:justify;">*pref = 20; // 将j的值变为20</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">pref是指针ptr的引用,所以下面所有的操作,pref就等同于ptr。</p>
<p style="margin-left:.0001pt;text-align:justify;">可以有指针的引用、引用的引用,也可以有指向指针的指针;但由于引用只是一个“别名”,不是实体对象,所以不存在指向引用的指针。</p>
<p style="margin-left:.0001pt;text-align:left;">int& ref = i;</p>
<p style="margin-left:.0001pt;text-align:left;">//int&* rptr = &ref; // 错误,不允许使用指向引用的指针</p>
<p style="margin-left:.0001pt;text-align:justify;">int* rptr = &ref; // 事实上就是指向了i</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)引用的本质</p>
<p style="margin-left:.0001pt;text-align:justify;">引用类似于指针常量,但不等同于指针常量。</p>
<p style="margin-left:.0001pt;text-align:justify;">指针常量本身还是一个数据对象,它保存着另一个对象的地址,而且不能更改;而引用就是“别名”,它会被编译器直接翻译成所绑定的原始变量;所以我们会看到,引用和原始对象的地址是一样,引用并没有额外占用内存空间。这也是为什么不会有“指向引用的指针”。</p>
<p style="margin-left:.0001pt;text-align:justify;">引用的本质,只是C++引入的一种语法糖,它是对指针的一种伪装。</p>
<p style="margin-left:.0001pt;text-align:justify;">指针是C语言中最灵活、最强大的特性;引用所能做的,其实指针全都可以做。但是指针同时又令人费解、充满危险性,所以C++中通过引用来代替一些指针的用法。后面在函数部分,我们会对此有更深刻的理解。</p>
<h4 style="text-align:justify;"><strong><strong><strong>6.8 应用案例</strong></strong></strong></h4>
<h5 style="text-align:justify;"><strong><strong><strong>6.8.1 翻转数组</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">翻转数组,就是要把数组中元素的顺序全部反过来。比如一个数组{1,2,3,4,5,6,7,8},翻转之后就是{8,7,6,5,4,3,2,1}。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)另外创建数组,反向填入元素</p>
<p style="margin-left:.0001pt;text-align:justify;">数组是将元素按照顺序依次存放的,长度固定。所以如果想要让数组“翻转”,一种简单的思路是:直接创建一个相同长度的新数组,然后遍历所有元素,从末尾开始依次反向填入就可以了。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">const int n = 8;</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[n] = { 1,2,3,4,5,6,7,8 };</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 1. 直接创建一个新数组,遍历元素反向填入</p>
<p style="margin-left:.0001pt;text-align:left;">int newArr[n];</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">newArr[n-i-1] = arr;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 打印数组</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << newArr << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">需要注意原数组下标为i的元素,对应翻转后的新数组下标为n-i-1(n为数组长度)。</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)基于原数组翻转</p>
<p style="margin-left:.0001pt;text-align:justify;">另建数组的方式很容易实现,但有明显的缺点:需要额外创建一个数组,占用更多的内存。最好的方式是,不要另开空间,就在原数组上调整位置。</p>
<p style="margin-left:.0001pt;text-align:justify;">这种思路的核心在于:我们应该有两个类似“指针”的东西,每次找到头尾两个元素,将它们调换位置;而后指针分别向中间逼近,再找两个元素对调。由于数组中下标是确定的,因此可以直接用下标代替“指针”。</p>
<p style="margin-left:.0001pt;text-align:left;">// 2. 双指针分别指向数组头尾,元素对调</p>
<p style="margin-left:.0001pt;text-align:left;">int head = 0, tail = n - 1;</p>
<p style="margin-left:.0001pt;text-align:left;">while(head < tail)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = arr[head]; </p>
<p style="margin-left:.0001pt;text-align:left;">arr[head] = arr[tail];</p>
<p style="margin-left:.0001pt;text-align:left;">arr[tail] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 指针向中间移动</p>
<p style="margin-left:.0001pt;text-align:left;">++head;</p>
<p style="margin-left:.0001pt;text-align:left;">--tail;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 打印数组</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << arr << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << endl;</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.8.2 检验幻方</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">“幻方”是数学上一个有趣的问题,它让一组不同的数字构成一个方阵,并且每行、每列、每个对角线的所有数之和相等。比如最简单的三阶幻方,就是把1~9的数字填到九宫格里,要求横看、竖看、斜着看和都是15。</p>
<p style="margin-left:.0001pt;text-align:justify;">口诀:二四为肩,六八为足,左三右七,戴九履一,五居中央。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/cfc7776cfb5b044407387f25ae582293.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以给定一个n×n的矩阵,也就是二维数组,然后判断它是否是一个幻方:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">const int n = 3;</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[n][n] = {</p>
<p style="margin-left:.0001pt;text-align:left;">{4, 9, 2},</p>
<p style="margin-left:.0001pt;text-align:left;">{3, 5, 7},</p>
<p style="margin-left:.0001pt;text-align:left;">{8, 1, 6}</p>
<p style="margin-left:.0001pt;text-align:left;">};</p>
<p style="margin-left:.0001pt;text-align:left;">// 目标和</p>
<p style="margin-left:.0001pt;text-align:left;">int target = (1 + n * n) * n / 2;</p>
<p style="margin-left:.0001pt;text-align:left;">bool isMagic = true;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 检验每一行</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int sum = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < n; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">sum += arr[j];</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 如果和不是target,说明不是幻方</p>
<p style="margin-left:.0001pt;text-align:left;">if (sum != target)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">isMagic = false;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 检验每一列</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < n; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int sum = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">sum += arr[j];</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">if (sum != target)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">isMagic = false;</p>
<p style="margin-left:.0001pt;text-align:left;">break;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 检验两个对角线</p>
<p style="margin-left:.0001pt;text-align:left;">int sumDiag1 = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">int sumDiag2 = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">sumDiag1 += arr;</p>
<p style="margin-left:.0001pt;text-align:left;">sumDiag2 += arr[n-i-1];</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">if (sumDiag1 != target || sumDiag2 != target)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">isMagic = false;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 判断结果</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "给定的矩阵arr" << (isMagic ? "是" : "不是") << n << "阶幻方!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.8.3 大整数相加</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">实际应用中,有时会遇到非常大的整数,可能会超过long、甚至long long的范围。这时就需要用不限长度的字符串保存数据,然后进行计算。</p>
<p style="margin-left:.0001pt;text-align:justify;">最简单的需求就是“大整数相加”,即给定两个字符串形式的非负大整数 num1 和num2 ,计算它们的和。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以把字符串按每个字符一一拆开,相当于遍历整数上的每一个数位,然后通过“乘10叠加”的方式,就可以整合起来了。这相当于算术中的“竖式加法”。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string num1 = "32535943020935527435432875";</p>
<p style="margin-left:.0001pt;text-align:left;">string num2 = "9323298429842985843509";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 用一个空字符串保存结果</p>
<p style="margin-left:.0001pt;text-align:left;">string result;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 获取两数个位的索引</p>
<p style="margin-left:.0001pt;text-align:left;">int p1 = num1.size() - 1;</p>
<p style="margin-left:.0001pt;text-align:left;">int p2 = num2.size() - 1;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 设置一个进位标志</p>
<p style="margin-left:.0001pt;text-align:left;">int carry = 0;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">while (p1 >= 0 || p2 >= 0 || carry > 0)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int x = (p1 >= 0) ? (num1[p1] - '0') : 0;</p>
<p style="margin-left:.0001pt;text-align:left;">int y = (p2 >= 0) ? (num2[p2] - '0') : 0;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int sum = x + y + carry; </p>
<p style="margin-left:.0001pt;text-align:left;">result += (sum % 10 + '0'); // 和的个位写入结果</p>
<p style="margin-left:.0001pt;text-align:left;">carry = sum / 10; // 和的十位保存在进位上</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 继续遍历下一位</p>
<p style="margin-left:.0001pt;text-align:left;">--p1;</p>
<p style="margin-left:.0001pt;text-align:left;">--p2;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 结果需要做翻转</p>
<p style="margin-left:.0001pt;text-align:left;">int i = 0, j = result.size() - 1;</p>
<p style="margin-left:.0001pt;text-align:left;">while (i < j)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">char temp = result[j];</p>
<p style="margin-left:.0001pt;text-align:left;">result[j] = result;</p>
<p style="margin-left:.0001pt;text-align:left;">result = temp;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">++i;</p>
<p style="margin-left:.0001pt;text-align:left;">--j;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << num1 << " + " << num2 << endl << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << " = " << result;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.8.4 旋转图像</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">旋转图像的需求,在图片处理的过程中非常常见。我们知道对于计算机而言,图像其实就是一组像素点的集合,所以图像旋转的问题,本质上就是一个二维数组的旋转问题。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以给定一个二维数组,用来表示一个图像,然后将它顺时针旋转90°。例如,对于4×4的矩阵:</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 5, 1, 9, 11},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 2, 4, 8, 10},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 13, 3, 6, 7},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 15, 14, 12, 16}</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">旋转之后变为:</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 15, 13, 2, 5},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 14, 3, 4, 1},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 12, 6, 8, 9},</p>
<p style="margin-left:.0001pt;text-align:justify;"> { 16, 7, 10, 11}</p>
<p style="margin-left:.0001pt;text-align:justify;">]</p>
<p style="margin-left:.0001pt;text-align:justify;">根据数学上矩阵的特性,可以把矩阵A先做转置得到AT,然后再翻转每一行就可以了。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">const int n = 4;</p>
<p style="margin-left:.0001pt;text-align:left;">int image[n][n] = {</p>
<p style="margin-left:.0001pt;text-align:left;"> { 5, 1, 9, 11},</p>
<p style="margin-left:.0001pt;text-align:left;"> { 2, 4, 8, 10},</p>
<p style="margin-left:.0001pt;text-align:left;"> { 13, 3, 6, 7},</p>
<p style="margin-left:.0001pt;text-align:left;"> { 15, 14, 12, 16}</p>
<p style="margin-left:.0001pt;text-align:left;">};</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 矩阵转置</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j <= i; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 以对角线为对称轴,两边互换</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = image[j];</p>
<p style="margin-left:.0001pt;text-align:left;">image[j] = image[j];</p>
<p style="margin-left:.0001pt;text-align:left;">image[j] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 每一行翻转</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < n / 2; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = image[j];</p>
<p style="margin-left:.0001pt;text-align:left;">image[j] = image[n-j-1];</p>
<p style="margin-left:.0001pt;text-align:left;">image[n - j - 1] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 打印输出</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int j = 0; j < n; j++)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << image[j] << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>6.8.5 翻转链表</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">链表(Linked List)是一种常见的基础数据结构,它是一种线性表,但是并不会像数组那样按顺序存储数据,而是在每一个节点里存指向下一个节点的指针。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/a5337bc5b32a418b7282b9596bb2a012.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">对于数组,可以通过下标访问每个元素;如果想要翻转一个数组,只要不停地把头尾元素互换就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;">而链表并没有“下标”,所以想要访问元素只能依次遍历;如果要翻转一个链表,关键就在于“next”指针需要反向。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以先定义一个结构体类型ListNode,用来表示链表中的每个节点:</p>
<p style="margin-left:.0001pt;text-align:left;">#pragma once</p>
<p style="margin-left:.0001pt;text-align:left;">struct ListNode</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int value;</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* next;</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;">自定义一个头文件list_node.h,将结构体TreeNode的定义放在里面,这样之后如果需要使用它,就可以直接引入:</p>
<p style="margin-left:.0001pt;text-align:justify;">#include "list_node.h"</p>
<p style="margin-left:.0001pt;text-align:justify;">这里的#prama once是一条预处理指令,表示头文件的内容只被解析一次,不会重复处理。</p>
<p style="margin-left:.0001pt;text-align:justify;">接下来就可以实现翻转链表的过程了。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">#include "list_node.h"</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义一个链表 1->2->3->4->5->NULL</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode node5 = { 5, nullptr };</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode node4 = { 4, &node5 };</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode node3 = { 3, &node4 };</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode node2 = { 2, &node3 };</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode node1 = { 1, &node2 };</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* list = &node1;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* curr = list;</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* prev = nullptr; </p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 翻转链表</p>
<p style="margin-left:.0001pt;text-align:left;">while (curr)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* temp = curr->next;</p>
<p style="margin-left:.0001pt;text-align:left;">curr->next = prev;</p>
<p style="margin-left:.0001pt;text-align:left;">prev = curr;</p>
<p style="margin-left:.0001pt;text-align:left;">curr = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* newList = prev;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 打印链表</p>
<p style="margin-left:.0001pt;text-align:left;">ListNode* np = newList;</p>
<p style="margin-left:.0001pt;text-align:left;">while (np)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << np->value << "\t->\t";</p>
<p style="margin-left:.0001pt;text-align:left;">np = np->next;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "null" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里对指向结构体对象的curr指针,需要先解引用,然后取它所指向ListNode对象里的next指针。这个过程本应该写作:</p>
<p style="margin-left:.0001pt;text-align:left;">(*curr).next;</p>
<p style="margin-left:.0001pt;text-align:justify;">这个写法比较麻烦,所以一般会用另一种简化写法:</p>
<p style="margin-left:.0001pt;text-align:justify;">curr->next;</p>
<p style="margin-left:.0001pt;text-align:justify;">这两个写法完全等价。这里的“->”叫做“箭头运算符”,它是解引用和访问成员两个操作的结合;这样就可以很方便地表示“取指针所指向内容的成员”。</p>
<h3 style="text-align:justify;"><strong><strong><strong>七、函数</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">函数其实就是封装好的代码块,并且指定一个名字,调用这个名字就可以执行代码并返回一个结果。</p>
<h4 style="text-align:justify;"><strong><strong><strong>7.1 </strong></strong><strong><strong>函数基本知识</strong></strong></strong></h4>
<h5 style="text-align:justify;"><strong><strong><strong>7.1.1 </strong></strong><strong><strong>函数定义</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">一个完整的函数定义主要包括以下部分:</p>
<ol><li>返回类型:调用函数之后,返回结果的数据类型;</li><li>函数名:用来命名代码块的标识符,在当前作用域内唯一;</li><li>参数列表:参数表示函数调用时需要传入的数据,一般叫做“形参”;放在函数名后的小括号里,可以有0个或多个,用逗号隔开;</li><li>函数体:函数要执行的语句块,用花括号括起来。</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">函数一般都是一个实现了固定功能的模块,把参数看成“输入”,返回结果看成“输出”,函数就是一个输入到输出的映射关系。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以定义一个非常简单的平方函数:</p>
<p style="margin-left:.0001pt;text-align:left;">// 平方函数 y = f(x) = x ^ 2</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int y = x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">return y;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">使用流程控制语句return,就可以返回结果。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.1.2 </strong></strong><strong><strong>函数调用</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">调用函数时,使用的是“调用运算符”,就是跟在函数名后面的一对小括号;括号内是用逗号隔开的参数列表。</p>
<p style="margin-left:.0001pt;text-align:justify;">这里的参数不是定义时的形参,而是为了初始化形参传入的具体值;为了跟函数定义时的形参列表区分,把它叫作“实参”。</p>
<p style="margin-left:.0001pt;text-align:justify;">调用表达式的类型就是函数的返回类型,值就是函数执行返回的结果。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 平方函数 y = f(x^2)</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << n << "的平方是:" << square(n) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意:</p>
<ol><li>实参是形参的初始值,所以函数调用时传入实参,相当于执行了int x = 6的初始化操作;实参的类型必须跟形参类型匹配;</li><li>实参的个数必须跟形参一致;如果有多个形参,要按照位置顺序一一对应;</li><li>如果函数本身没有参数,参数列表可以为空,但空括号不能省;</li><li>形参列表中多个参数用逗号分隔,每个都要带上类型,类型相同也不能省略;</li><li>如果函数不需要返回值,可以定义返回类型为void;</li><li>函数返回类型不能是数组或者函数</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>7.1.3 案例练习</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">下面几个案例可以作为函数的基本练习。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)求两个数的立方和</p>
<p style="margin-left:.0001pt;text-align:justify;">定义一个函数,输入两个整型参数x、y,返回x3 + y3。</p>
<p style="margin-left:.0001pt;text-align:left;">int cubeSum(int x, int y)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return pow(x, 3) + pow(y, 3);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">(2)求阶乘</p>
<p style="margin-left:.0001pt;text-align:justify;">阶乘的计算公式n! = 1 × 2 × 3 ×…× n,可以用一个循环来实现。定义一个求阶乘的函数,传入一个整数n,返回n!。</p>
<p style="margin-left:.0001pt;text-align:left;">// 求阶乘</p>
<p style="margin-left:.0001pt;text-align:left;">int factorial(int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int result = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 1; i <= n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">result *= i;</p>
<p style="margin-left:.0001pt;text-align:left;">return result;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">(3)复制字符串</p>
<p style="margin-left:.0001pt;text-align:justify;">定义一个函数,传入一个字符串str和一个整数n,将字符串str复制n次后返回。</p>
<p style="margin-left:.0001pt;text-align:left;">// 复制字符串</p>
<p style="margin-left:.0001pt;text-align:left;">string copyStr(string str, int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string result;</p>
<p style="margin-left:.0001pt;text-align:left;">while (n > 0)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">result += str;</p>
<p style="margin-left:.0001pt;text-align:left;">--n;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">return result;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong>7.1.4 局部变量的生命周期</strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">之前介绍过变量的作用域,对于花括号内定义的变量,具有“块作用域”,在花括号外就不可见了。函数体都是语句块,而主函数main本身也是一个函数;所以在main中定义的所有变量、所有函数形参和在函数体内部定义的变量,都具有块作用域,统称为“局部变量”。局部变量仅在函数作用域内部可见。</p>
<p style="margin-left:.0001pt;text-align:left;">// 函数形参x是局部变量,作用域为函数内部</p>
<p style="margin-left:.0001pt;text-align:left;">void f(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 函数内部定义的变量a是局部变量,作用域为函数内部</p>
<p style="margin-left:.0001pt;text-align:left;">int a = 10;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 主函数中定义的变量b也是局部变量,作用域为主函数内</p>
<p style="margin-left:.0001pt;text-align:left;">int b = 0;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,作用域指的是变量名字的可见范围;变量不可见,并不代表变量所指代的数据对象就销毁了。这是两个不同的概念:</p>
<ol><li>作用域:针对名字而言,是程序文本中的一部分,名字在这部分可见;</li><li>生命周期:针对数据对象而言,是程序在执行过程中,对象从创建到销毁的时间段</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">基于作用域,变量可以分为“局部变量”和“全局变量”。对于全局变量而言,名字全局可见,对象也只有在程序结束时才销毁。</p>
<p style="margin-left:.0001pt;text-align:justify;">而对于局部变量代表的数据对象,基于生命周期,又可以分为“自动对象”和“静态对象”。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)自动对象</p>
<p style="margin-left:.0001pt;text-align:justify;">平常代码中定义的普通局部变量,生命周期为:在程序执行到变量定义语句时创建,在程序运行到当前块末尾时销毁。这样的对象称为“自动对象”。</p>
<p style="margin-left:.0001pt;text-align:justify;">形参也是一种自动对象。形参定义在函数体作用域内,一旦函数终止,形参也就被销毁了。</p>
<p style="margin-left:.0001pt;text-align:justify;">对于自动对象来说,它的生命周期和作用域是一致的。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)静态对象</p>
<p style="margin-left:.0001pt;text-align:justify;">如果希望延长一个局部变量的生命周期,让它在作用域外依然保留,可以在定义局部变量时加上static关键字;这样的对象叫做“局部静态对象”。</p>
<p style="margin-left:.0001pt;text-align:justify;">局部静态对象只有局部的作用域,在块外依然是不可见的;但是它的生命周期贯穿整个程序运行过程,只有在程序结束时才被销毁,这一点与全局变量类似。</p>
<p style="margin-left:.0001pt;text-align:left;">// 显示自身被调用多少次的函数</p>
<p style="margin-left:.0001pt;text-align:left;">int callCount()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">static int cnt = 0; // 静态对象只会创建一次</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "我被调用了" << ++cnt << "次!" << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">return cnt;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;">//cout << cnt << endl; // 错误,局部变量在作用域外不可见</p>
<p style="margin-left:.0001pt;text-align:justify;">callCount();</p>
<p style="margin-left:.0001pt;text-align:left;">callCount();</p>
<p style="margin-left:.0001pt;text-align:left;">callCount();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">可以发现,静态对象只在第一次执行到定义语句时创建出来,之后即使函数执行结束,它的值依然保持;下一次函数调用时,不会再次创建、也不会重新赋值,而是直接在之前的值基础上继续叠加。</p>
<p style="margin-left:.0001pt;text-align:justify;">静态对象和自动对象应用的场景不同,所以它们存放的内存区域也是不一样的。静态对象如果不在代码中做初始化,基本类型会被默认初始化为0值。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.1.5 函数声明</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">如果我们将一个函数放在主函数后面,就会出现运行错误:找不到标识符。这是因为函数和变量一样,使用之前必须要做声明。函数只有一个定义,可以定义在任何地方;如果需要调用函数,只需要在调用前做一个声明,告诉编译器“存在这个函数”就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;">函数声明的方式,和函数的定义非常相似;区别在于声明时不需要把函数体写出来,用一个分号替代就可以了。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 声明函数</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x);</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << n << "的平方是:" << square(n) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义函数</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int y = x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">return y;</p>
<p style="margin-left:.0001pt;text-align:left;">return x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">事实上,由于没有函数体的执行过程,所以形参的名字也完全不需要,可以省略。可以直接这样声明一个函数:</p>
<p style="margin-left:.0001pt;text-align:justify;">int square(int);</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">函数声明中包含了返回类型、函数名和形参类型,这就说明了调用这个函数所需要的所有信息。函数声明也被叫做“函数原型”。</p>
<p style="margin-left:.0001pt;text-align:justify;">一般情况下,把函数声明放在头文件中会更加方便。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.1.6 分离式编译和头文件</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">(1)分离式编译</p>
<p style="margin-left:.0001pt;text-align:justify;">当程序越来越复杂,我们就会希望代码分散到不同的文件中来做管理。C++支持分离式编译,这就可以把函数单独放在一个文件,独立编译之后链接运行。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如可以把复制字符串的函数单独保存成一个文件copy_string.cpp:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<string></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;">// 复制字符串</p>
<p style="margin-left:.0001pt;text-align:left;">string copyStr(string str, int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string result;</p>
<p style="margin-left:.0001pt;text-align:left;">while (n > 0)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">result += str;</p>
<p style="margin-left:.0001pt;text-align:left;">--n;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">return result;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">然后只要在主函数调用之前做声明就可以了:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 声明函数</p>
<p style="margin-left:.0001pt;text-align:left;">string copyStr(string, int);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << copyStr("hello ", n) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)编写头文件</p>
<p style="margin-left:.0001pt;text-align:justify;">对于一个项目而言,有些定义可能是所有文件共用的,比如一些常量、结构体/类,以及功能性的函数。于是每次需要引入时,都得做一堆声明——这显然太麻烦了。</p>
<p style="margin-left:.0001pt;text-align:justify;">一个好方法是,把它们定义在同一个文件中,需要时用一句#include统一引入就可以了,就像使用库一样。这样的文件以.h作为后缀,被称为“头文件”。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如我们可以把之前的一些功能性的函数(比如求平方、阶乘、复制字符串等),放在一个叫做utils.h的头文件中:</p>
<p style="margin-left:.0001pt;text-align:left;">#pragma once</p>
<p style="margin-left:.0001pt;text-align:left;">#include<string></p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 平方函数 y = f(x^2)</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int y = x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">return y;</p>
<p style="margin-left:.0001pt;text-align:left;">return x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 求立方和</p>
<p style="margin-left:.0001pt;text-align:left;">int cubeSum(int x, int y)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return pow(x, 3) + pow(y, 3);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 求阶乘</p>
<p style="margin-left:.0001pt;text-align:left;">int factorial(int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int result = 1;</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 1; i <= n; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">result *= i;</p>
<p style="margin-left:.0001pt;text-align:left;">return result;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 复制字符串</p>
<p style="margin-left:.0001pt;text-align:justify;">std::string copyStr(std::string str, int n);</p>
<p style="margin-left:.0001pt;text-align:justify;">这里有两点需要说明:</p>
<ol><li>#pragma once是一条预处理指令,表示这个头文件的内容只会被编译一次,这就避免了多次引入头文件时的重复定义;</li><li>复制字符串函数copyStr已经在别的文件单独做了定义,这里只要声明就可以;</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">如果想要使用这些函数,只要在文件中引入头文件即可:</p>
<p style="margin-left:.0001pt;text-align:justify;">#include "utils.h"</p>
<p style="margin-left:.0001pt;text-align:justify;">这里文件名没有使用尖括号<>,而是使用了引号;这表示要在当前项目的根目录下寻找文件,而不是到编译器默认的库目录下去找。</p>
<h4 style="text-align:justify;"><strong><strong><strong>7.2 </strong></strong><strong><strong>参数传递</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">函数在每次调用时,都会重新创建形参,并且用传入的实参对它进行初始化。形参的类型,决定了形参和实参交互的方式;也决定了函数的不同功能。</p>
<p style="margin-left:.0001pt;text-align:justify;">可以先回忆一下对变量的初始化:对一个变量做初始化,如果用另一个变量给它赋初值,意味着值的拷贝;也就是说,此后这两个变量各自一份数据,各自管理,互不影响。而如果是定义一个引用,绑定另一个变量做初始化,并不会引发值的拷贝;引用和原变量管理的是同一个数据对象。</p>
<p style="margin-left:.0001pt;text-align:left;">int i1 = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">int i2 = i1;</p>
<p style="margin-left:.0001pt;text-align:left;">i2 = 1; // i1的值仍然是0</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int& i3 = i1;</p>
<p style="margin-left:.0001pt;text-align:left;">i3 = 10; // i1的值也变为10</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">参数传递和变量的初始化类似,根据形参的类型可以分为两种方式:传值(value)和传引用(reference)。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.2.1 </strong></strong><strong><strong>传值参数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">直接将一个实参的值,拷贝给形参做初始化的传参方式,就被称为“值传递”,这样的参数被称为“传值参数”。之前我们练习过的所有函数,都是采用这种传值调用的方式。</p>
<p style="margin-left:.0001pt;text-align:left;">int square(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return x * x;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">cout << n << "的平方是:" << square(n) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">在上面平方函数的调用中,实参n的值(6)被拷贝给了形参x。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)传值的困扰</p>
<p style="margin-left:.0001pt;text-align:justify;">值传递这种方式非常简单,但是面对这样的需求会有些麻烦:传入一个数据对象,让它经过函数处理之后发生改变。例如,传入一个整数x,调用之后它自己的值要加1。这看起来很简单,但如果直接:</p>
<p style="margin-left:.0001pt;text-align:left;">void increase(int x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">++x;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 6;</p>
<p style="margin-left:.0001pt;text-align:left;">increase(n); // n的值不会增加</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这样做并不能实现需求。因为实参n的值是拷贝给形参x的,之后x的任何操作,都不会改变n。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)指针形参</p>
<p style="margin-left:.0001pt;text-align:justify;">使用指针形参可以解决这个问题。如果我们把指向数据对象的指针作为形参,那么初始化时拷贝的就是指针的值;复制之后的指针,依然指向原始数据对象,这样就可以保留它的更改了。</p>
<p style="margin-left:.0001pt;text-align:left;">// 指针形参</p>
<p style="margin-left:.0001pt;text-align:left;">void increase(int* p)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">++(*p);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">increase( &n ); // 传入n的地址,调用函数后n的值会加1</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>7.2.2 </strong></strong><strong><strong>传引用参数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">使用指针形参可以解决值传递的问题,不过这种方式函数定义显得有些繁琐,每次调用还需要记住传入变量的地址,使用起来不够方便。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)传引用方便函数调用</p>
<p style="margin-left:.0001pt;text-align:justify;">C++新增了引用的概念,可以替换必须使用指针的场景。采用引用作为函数形参,可以使函数调用更加方便。这种传参方式叫做“传引用参数”。之前的例子就可以改写成:</p>
<p style="margin-left:.0001pt;text-align:left;">// 传引用</p>
<p style="margin-left:.0001pt;text-align:left;">void increase(int& x)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">++x;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int n = 0;</p>
<p style="margin-left:.0001pt;text-align:left;">increase( n ); // 调用函数后n的值会加1</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">由于使用了引用作为形参,函数调用时就可以直接传入n的值,而不用传地址了;x只是n的一个别名,修改x就修改了n。对比可以发现,这段代码相比最初尝试写出的传值实现,只是多了一个引用声明&而已。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)传引用避免拷贝</p>
<p style="margin-left:.0001pt;text-align:justify;">使用引用还有一个非常重要的场景,就是不希望进行值拷贝的时候。实际应用中,很多时候函数要操作的对象可能非常庞大,如果做值拷贝会使得效率大大降低;这时使用引用就是一个好方法。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如,想要定义一个函数比较两个字符串的长度,需要将两个字符串作为参数传入。因为字符串有可能非常长,直接做值拷贝并不是一个好选择,最好的方式就是传递引用:</p>
<p style="margin-left:.0001pt;text-align:left;">// 比较两个字符串的长度</p>
<p style="margin-left:.0001pt;text-align:left;">bool isLonger(const string & str1, const string & str2)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return str1.size() > str2.size();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)使用常量引用做形参</p>
<p style="margin-left:.0001pt;text-align:justify;">在上面的例子中,比较两个字符串长度,并不会更改字符串本身的内容,所以可以把形参定义为常量引用。</p>
<p style="margin-left:.0001pt;text-align:justify;">这样的好处是,既避免了对数据对象可能的更改,也扩大了调用时能传的实参的范围。因为之前讨论过常量引用的特点,可以用字面值常量对它做初始化,也可以用变量做初始化。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以在代码中,一般要尽量使用常量引用作为形参。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>7.2.</strong></strong><strong><strong>3</strong></strong><strong> </strong><strong><strong>数组形参</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">之前已经介绍过,数组是不允许做直接拷贝的,所以如果想要把数组作为函数的形参,使用值传递的方式是不可行的。与此同时,数组名可以解析成一个指针,所以可以用传递指针的方式来处理数组。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如一个简单的函数,需要遍历int类型数组所有元素并输出,就可以这样声明:</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int*); // 指向int类型常量的指针</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int[]); </p>
<p style="margin-left:.0001pt;text-align:justify;">void printArray(const int[5]);</p>
<p style="margin-left:.0001pt;text-align:justify;">由于只是遍历输出,不需要修改数组内容,所以这里使用了const。</p>
<p style="margin-left:.0001pt;text-align:justify;">以上三种声明方式,本质上是一样的,形参的类型都是const int *;虽然第三种方式指定了数组长度,但由于编译器会把传入的数组名解析成指针,事实上的数组长度还是无法确定的。</p>
<p style="margin-left:.0001pt;text-align:justify;">这就带来另一个问题:在函数中,遍历元素时怎样确定数组的结束?</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)规定结束标记</p>
<p style="margin-left:.0001pt;text-align:justify;">一种简单思路是,规定一个特殊的“结束标记”,遇到这个标记就代表当前数组已经遍历完了。典型代表就是C语言风格的字符串,是以空字符’\0’为结束标志的char数组。</p>
<p style="margin-left:.0001pt;text-align:justify;">这种方式比较麻烦,而且太多特殊规定也不适合像int这样的数据类型。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)把数组长度作为形参</p>
<p style="margin-left:.0001pt;text-align:justify;">除指向数组的指针外,可以再增加一个形参,专门表示数组的长度,这样就可以方便地遍历数组了。</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int* arr, int size)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < size; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << arr << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;">int arr[6] = { 1,2,3,4,5,6 };</p>
<p style="margin-left:.0001pt;text-align:left;">printArray(arr, 6);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">在C语言和老式的C++程序中,经常使用这种方法来处理数组。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)使用数组引用作为形参</p>
<p style="margin-left:.0001pt;text-align:justify;">之前的方法依赖指针,所以都显得比较麻烦。更加方便的做法,还是用引用来替代指针的功能。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++允许使用数组的引用作为函数形参,这样一来,引用作为别名绑定在数组上,使用引用就可以直接遍历数组了。</p>
<p style="margin-left:.0001pt;text-align:left;">// 使用数组引用作为形参</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int(&arr)[6])</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : arr)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;">int arr[6] = { 1,2,3,4,5,6 };</p>
<p style="margin-left:.0001pt;text-align:left;">printArray(arr);</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意的是,定义一个数组引用时需要用括号将&和引用名括起来:</p>
<p style="margin-left:.0001pt;text-align:justify;">int(&arr)[6] // 正确,arr是一个引用,绑定的是长度为6的int数组</p>
<p style="margin-left:.0001pt;text-align:justify;">// int & arr[6] // 错误,这是引用的数组,不允许使用</p>
<p style="margin-left:.0001pt;text-align:justify;">使用数组引用之后,调用函数直接传入数组名就可以了。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>7.2.4 可变形参</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">有时候我们并不确定函数中应该有几个形参,这时就需要使用“可变形参”来表达。</p>
<p style="margin-left:.0001pt;text-align:justify;">C++中表示可变形参的方式主要有三种:</p>
<ol><li>省略符(…):兼容C语言的用法,只能出现在形参列表的最后一个位置;</li><li>初始化列表initializer_list:跟vector类似,也是一种标准库模板类型;initializer_list对象中的元素只能是常量值,不能更改;</li><li>可变参数模板:这是一种特殊的函数,后面会详细介绍。</li></ol>
<h4 style="text-align:justify;"><strong><strong><strong>7.3 </strong></strong><strong><strong>返回类型</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">函数可以通过return语句,终止函数的执行并“返回”函数调用的地方;并且可以给定返回值。返回值的类型由函数声明时的“返回类型”决定。</p>
<p style="margin-left:.0001pt;text-align:justify;">return语句可以有两种形式:</p>
<p style="margin-left:.0001pt;text-align:left;">return;<em> </em> // 直接返回,无返回值</p>
<p style="margin-left:.0001pt;text-align:justify;">return <em>表达式</em>; // 返回表达式的值</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>7.3.1 </strong></strong><strong><strong>无返回值</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">当函数返回类型为void时,表示函数没有返回值。可以在函数中需要返回时直接执行 return语句,也可以不写。因为返回类型为void的函数执行完最后一句,会自动加上return返回。</p>
<p style="margin-left:.0001pt;text-align:justify;">例如,可以将之前“两元素值互换”的代码,包装成一个函数。可以先做一个判断,如果两者相等就直接返回,这样可以提高运行效率。</p>
<p style="margin-left:.0001pt;text-align:left;">// 元素互换</p>
<p style="margin-left:.0001pt;text-align:left;">void swap(int& x, int& y)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (x == y)</p>
<p style="margin-left:.0001pt;text-align:left;">return; // 不需要交换,直接返回</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int temp = x;</p>
<p style="margin-left:.0001pt;text-align:left;">x = y;</p>
<p style="margin-left:.0001pt;text-align:left;">y = temp;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这里判断如果元素相等就直接返回,有些类似于流程控制中的break。最后一句代码后面省略了return。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.3.2 </strong></strong><strong><strong>有返回值</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">如果函数返回类型不为void,那么函数必须执行return,并且每条return必须返回一个值。返回值的类型应该跟函数返回类型一致,或者可以隐式转换为一致。</p>
<p style="margin-left:.0001pt;text-align:justify;">(1)函数返回值的原理</p>
<p style="margin-left:.0001pt;text-align:justify;">函数在调用点会创建一个“临时量”,用来保存函数调用的结果。当使用return语句返回时,就会用返回值去初始化这个临时量。所以返回值的相关规则,跟变量或者形参的初始化是一致的。</p>
<p style="margin-left:.0001pt;text-align:justify;">之前写过一个“比较字符串长度”的isLonger函数,我们可以稍作修改,让它可以返回较长的那个字符串:</p>
<p style="margin-left:.0001pt;text-align:left;">// 字符串比较长度,返回较长的</p>
<p style="margin-left:.0001pt;text-align:left;">string longerStr(const string& str1, const string& str2)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return str1.size() > str2.size() ? str1 : str2;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string str1 = "hello world!", str2 = "c++ is interesting!";</p>
<p style="margin-left:.0001pt;text-align:left;">cout << longerStr(str1, str2) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">调用这个函数,经过判断发现str2较长,这时执行return将返回str2。由于返回类型是string,所以将用str2对一个string临时量做初始化,执行的是值拷贝。最终返回的值,是str2的一个副本。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)返回引用类型</p>
<p style="margin-left:.0001pt;text-align:justify;">对于string对象,显然做值拷贝并不高效。所以我们依然可以借鉴之前的经验,使用引用类型来做返回值的传递,这样就可以避免值拷贝。</p>
<p style="margin-left:.0001pt;text-align:left;">// 返回一个string常量对象的引用,不做值拷贝</p>
<p style="margin-left:.0001pt;text-align:left;">const string & longerStr(const string& str1, const string& str2)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return str1.size() > str2.size() ? str1 : str2;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里我们同样把返回值定义成了常量引用,方式和作用跟形参完全一样。</p>
<p style="margin-left:.0001pt;text-align:justify;">上面函数返回的是形参str1或者str2的引用;而函数中的形参本身又是引用类型,所以最终是实参对象的引用。</p>
<p style="margin-left:.0001pt;text-align:justify;">而如果返回的是一个函数内局部变量的引用,比如:</p>
<p style="margin-left:.0001pt;text-align:left;">const string & f()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string str = "test";</p>
<p style="margin-left:.0001pt;text-align:left;">return str;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这样做是不安全的:因为str是函数内部的局部对象,函数执行完成后就销毁了;而返回值是它的引用,相当于引用了一个不存在的对象,这可能会导致无法预料的问题。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以,函数返回引用类型时,不能返回局部对象的引用;同样道理,也不应该返回指向局部对象的指针。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)返回类对象后连续调用</p>
<p style="margin-left:.0001pt;text-align:justify;">如果函数返回一个类的对象,那么我们可以继续调用这个对象的成员函数,这样就形成了“链式调用”。例如:</p>
<p style="margin-left:.0001pt;text-align:justify;">longerStr(str1, str2).size();</p>
<p style="margin-left:.0001pt;text-align:justify;">调用运算符,和访问对象成员的点运算符优先级相同,并且满足左结合律。所以链式调用就是从左向右依次调用,代码可读性会更高。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.3.3 主函数的返回值</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">主函数main是一个特殊函数,它是我们执行程序的入口。所以C++中对主函数的返回值也有特殊的规定:即使返回类型不是void,主函数也可以省略return语句。如果主函数执行到结尾都没有return语句,编译器就会自动插入一条:</p>
<p style="margin-left:.0001pt;text-align:justify;">return 0;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">主函数的返回值可以看做程序运行的状态指示器:返回0表示运行成功;返回非0值则表示失败。非0值具体的含义依赖机器决定。</p>
<p style="margin-left:.0001pt;text-align:justify;">这也是为什么之前我们在主函数中都可以不写return。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.3.3 </strong></strong><strong><strong>返回数组指针</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">与形参的讨论类似,由于数组“不能拷贝”的特点,函数也无法直接返回一个数组。同样的,我们可以使用指针或者引用来实现返回数组的目标;通常会返回一个数组指针。</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[5] = { 1,2,3,4,5 };</p>
<p style="margin-left:.0001pt;text-align:left;">int* pa[5]; // 指针数组,pa是包含5个int指针的数组</p>
<p style="margin-left:.0001pt;text-align:left;">int(*ap)[5] = &arr; // 数组指针,ap是一个指针,指向长度为5的int数组</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:justify;">int(*fun(int x))[5]; // 函数声明,fun返回值类型为数组指针</p>
<p style="margin-left:.0001pt;text-align:justify;">这里对于函数fun的声明,我们可以进行层层解析:</p>
<ol><li>fun(int x) :函数名为fun,形参为int类型的x;</li><li>( * fun(int x) ):函数返回的结果,可以执行解引用操作,说明是一个指针;</li><li>( * fun(int x) )[5]:函数返回结果解引用之后是一个长度为5的数组,说明返回类型是数组指针;</li><li>int ( * fun(int x) )[5]:数组中元素类型为int</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">数组指针的定义比较繁琐,为了简化这个定义,我们可以使用关键字typedef来定义一个类型的别名:</p>
<p style="margin-left:.0001pt;text-align:left;">typedef int arrayT[5]; // 类型别名,arrayT代表长度为5的int数组</p>
<p style="margin-left:.0001pt;text-align:justify;">arrayT* fun2(int x); // fun2的返回类型是指向arrayT的指针</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">C++ 11新标准还提供了另一种简化方式,用一个->符号跟在形参列表后面,再把类型单独提出来放到最后。这种方式叫做“尾置返回类型”。</p>
<p style="margin-left:.0001pt;text-align:justify;">auto fun3(int x) -> int(*)[5]; // 尾置返回类型</p>
<p style="margin-left:.0001pt;text-align:justify;">因为返回类型放到了末尾,所以前面的类型用了自动推断的auto。</p>
<h4 style="text-align:justify;"><strong><strong><strong>7</strong></strong><strong><strong>.</strong></strong><strong><strong>4</strong></strong><strong> </strong><strong><strong>递归</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">如果一个函数调用了自身,这样的函数就叫做“递归函数”(recursive function)。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.4.1 递归的实现</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">递归是调用自身,如果不加限制,这个过程是不会结束的;函数永远调用自己下去,最终会导致程序栈空间耗尽。所以在递归函数中,一定会有某种“基准情况”,这个时候不会调用自身,而是直接返回结果。基准情况的处理保证了递归能够结束。</p>
<p style="margin-left:.0001pt;text-align:justify;">递归是不断地自我重复,这一点和循环有相似之处。事实上,递归和循环往往可以实现同样的功能。</p>
<p style="margin-left:.0001pt;text-align:justify;">比如之前求阶乘的函数,我们可以用递归的方式重新实现:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 递归方式求阶乘</p>
<p style="margin-left:.0001pt;text-align:left;">int factorial(int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (n == 1)</p>
<p style="margin-left:.0001pt;text-align:left;">return 1;</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">return factorial(n - 1) * n;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "5! = " << factorial(5) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里我们的基准情况是n == 1,也就是当n不断减小,直到1时就结束递归直接返回。5的阶乘具体计算流程如下:</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/cf2a3d6598ab154e6a9b1b371bacda6d.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">因为递归至少需要额外的栈空间开销,所以递归的效率往往会比循环低一些。不过在很多数学问题上,递归可以让代码非常简洁。</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.4.2 经典递归——斐波那契数列</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:</p>
<p style="margin-left:.0001pt;text-align:justify;">1, 1, 2, 3, 5, 8, 13, 21, 34, …</p>
<p style="margin-left:.0001pt;text-align:justify;">它的规律是:当前数字,是之前两个数字之和。在数学上,斐波那契数列被以递推的方法定义:</p>
<p style="margin-left:.0001pt;text-align:justify;">F(0)=1,F(1)=1, F(n) = F(n - 1) + F(n - 2)(n ≥ 2,n ∈ N*)</p>
<p style="margin-left:.0001pt;text-align:justify;">这天然适合使用递归实现:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int fib(int n)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">if (n == 1 || n == 2)</p>
<p style="margin-left:.0001pt;text-align:left;">return 1;</p>
<p style="margin-left:.0001pt;text-align:left;">return fib(n - 2) + fib(n - 1);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "fib(9) = " << fib(9) << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h4 style="text-align:justify;"><strong><strong><strong>7.5 应用案例</strong></strong></strong></h4>
<h5 style="text-align:justify;"><strong><strong><strong>7.5.1 二分查找</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据对象必须先排好序。二分查找事实上采用的是一种“分治”策略,它充分利用了元素间的次序关系。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 可以递归调用的二分查找</p>
<p style="margin-left:.0001pt;text-align:left;">int search(const int(&a)[10], int start, int end, int target)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 基准情况:目标值超出范围,或者start > end,说明没有找到</p>
<p style="margin-left:.0001pt;text-align:left;">if ( target < a[start] || target > a[end] || start > end)</p>
<p style="margin-left:.0001pt;text-align:left;">return -1;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 取二分的中间坐标</p>
<p style="margin-left:.0001pt;text-align:left;">int mid = (start + end) / 2;</p>
<p style="margin-left:.0001pt;text-align:left;">// 比较中间值和目标值的大小</p>
<p style="margin-left:.0001pt;text-align:left;">if (a[mid] == target)</p>
<p style="margin-left:.0001pt;text-align:left;">return mid; // 找到了</p>
<p style="margin-left:.0001pt;text-align:left;">else if (a[mid] > target)</p>
<p style="margin-left:.0001pt;text-align:left;">return search(a, start, mid - 1, target); // 比目标值大,在更小的部分找</p>
<p style="margin-left:.0001pt;text-align:left;">else</p>
<p style="margin-left:.0001pt;text-align:left;">return search(a, mid + 1, end, target); // 比目标值小,在更大的部分找</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[10] = { 1,2,3,4,5,6,9,12,25,38 };</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int key = 25;</p>
<p style="margin-left:.0001pt;text-align:left;">int size = sizeof(arr) / sizeof(arr[0]);</p>
<p style="margin-left:.0001pt;text-align:left;">int result = search(arr, 0, size - 1, key);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">result == -1 </p>
<p style="margin-left:.0001pt;text-align:left;">? cout << "在数组中没有找到" << key << "!" << endl</p>
<p style="margin-left:.0001pt;text-align:left;">: cout << "在数组中找到" << key << ",索引下标为:" << result << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.5.2 快速排序</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">之前介绍过两种对一组数据进行排序的算法:选择排序和冒泡排序,它们都需要使用两层for循环遍历数组,效率较低。一种巧妙的改进思路是:通过一次扫描,将待排记录分隔成独立的两部分,其中一部分的值全比另一部分的小;接下来分别对这两部分继续进行排序,最终全部排完。这种算法更加高效,被称为“快速排序”。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/22c9c5428f7b7a4ee9bc85e43262a1a1.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">可以看出,快排也应用了分治思想,一般会用递归来实现。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">void quickSort(int(&)[10], int, int);</p>
<p style="margin-left:.0001pt;text-align:left;">int partition(int(&)[10], int, int);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">void printArr(const int(&)[10]);</p>
<p style="margin-left:.0001pt;text-align:left;">void swap(int(&)[10], int, int);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int arr[10] = { 23, 45, 18, 6, 11, 19, 22, 18, 12, 9 };</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printArr(arr);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int size = sizeof(arr) / sizeof(arr[0]);</p>
<p style="margin-left:.0001pt;text-align:left;">quickSort(arr, 0, size - 1);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printArr(arr);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 快速排序</p>
<p style="margin-left:.0001pt;text-align:left;">void quickSort(int(&a)[10], int start, int end)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 基准情况</p>
<p style="margin-left:.0001pt;text-align:left;">if (start >= end)</p>
<p style="margin-left:.0001pt;text-align:left;">return;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 分区,返回分区点下标</p>
<p style="margin-left:.0001pt;text-align:left;">int mid = partition(a, start, end);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 递归调用,分别对两部分继续排序</p>
<p style="margin-left:.0001pt;text-align:left;">quickSort(a, start, mid - 1);</p>
<p style="margin-left:.0001pt;text-align:left;">quickSort(a, mid + 1, end);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 按照pivot分区的函数</p>
<p style="margin-left:.0001pt;text-align:left;">int partition(int(&a)[10], int start, int end)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 选取一个分区的“支点”</p>
<p style="margin-left:.0001pt;text-align:left;">int pivot = a[start];</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int left = start, right = end;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">while (left < right)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 分别从左右两边遍历数组</p>
<p style="margin-left:.0001pt;text-align:left;">while (a[left] <= pivot && left < right)</p>
<p style="margin-left:.0001pt;text-align:left;">++left;</p>
<p style="margin-left:.0001pt;text-align:left;">while (a[right] >= pivot && left < right)</p>
<p style="margin-left:.0001pt;text-align:left;">--right;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 左右互换</p>
<p style="margin-left:.0001pt;text-align:left;">swap(a, left, right);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">if (a[left] < pivot) {</p>
<p style="margin-left:.0001pt;text-align:left;">swap(a, start, left);</p>
<p style="margin-left:.0001pt;text-align:left;">return left; </p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">else if (a[left] > pivot)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">swap(a, start, left - 1);</p>
<p style="margin-left:.0001pt;text-align:left;">return left - 1;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 数组中两元素互换的函数</p>
<p style="margin-left:.0001pt;text-align:left;">void swap(int(&a)[10], int i, int j)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">int temp = a;</p>
<p style="margin-left:.0001pt;text-align:left;">a = a[j];</p>
<p style="margin-left:.0001pt;text-align:left;">a[j] = temp;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 打印输出一个数组</p>
<p style="margin-left:.0001pt;text-align:left;">void printArr(const int(&a)[10]) </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : a)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h5 style="text-align:justify;"><strong><strong><strong>7.5.3 遍历二叉树</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">跟数组不同,树是一种非线性的数据结构,是由n(n >=0)个节点组成的有限集合。如果n==0,树为空树。如果n>0,树有一个特定的节点,叫做根节点(root)。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/a61535cb7f10d76fb17a76bf3798974c.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">对于树这种数据结构,使用最频繁的是二叉树。每个节点最多只有2个子节点的树,叫做二叉树。二叉树中,每个节点的子节点作为根的两个子树,一般叫做节点的左子树和右子树。</p>
<p class="img-center"><img alt="" src="https://i-blog.csdnimg.cn/blog_migrate/bea588aa786bc8f18c69e8e1ba7e4c84.png" />
</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以为树的节点定义一种结构体类型,而且为了方便以后在不同的文件中使用,还可以自定义一个头文件tree_node.h,将结构体TreeNode的定义放在里面:</p>
<p style="margin-left:.0001pt;text-align:left;">#pragma once</p>
<p style="margin-left:.0001pt;text-align:left;">#include<string></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">struct TreeNode</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string name;</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode* left;</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode* right;</p>
<p style="margin-left:.0001pt;text-align:justify;">};</p>
<p style="margin-left:.0001pt;text-align:justify;">在别的文件中,如果想要使用TreeNode这个结构体,我们只要引入就可以:</p>
<p style="margin-left:.0001pt;text-align:justify;">#include "TreeNode.h"</p>
<p style="margin-left:.0001pt;text-align:justify;">对于树的遍历,主要有这样三种方式:</p>
<ol><li>先序遍历:先访问根节点,再访问左子树,最后访问右子树;</li><li>中序遍历:先访问左子树,再访问根节点,最后访问右子树;</li><li>后序遍历:先访问左子树,再访问右子树,最后访问根节点。</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">这种遍历方式就隐含了“递归”的思路:左右子树本身又是一棵树,同样需要按照对应的规则来遍历。</p>
<p style="margin-left:.0001pt;text-align:justify;">我们可以先单独创建一个文件print_tree.cpp,实现二叉树的遍历方法:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">#include "tree_node.h"</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 先序遍历打印二叉树</p>
<p style="margin-left:.0001pt;text-align:left;">void printTreePreOrder(TreeNode* root)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 基准情况,如果是空树,直接返回</p>
<p style="margin-left:.0001pt;text-align:left;">if (root == nullptr) return;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">//cout << (*root).name << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">cout << root->name << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 递归打印左右子树</p>
<p style="margin-left:.0001pt;text-align:left;">printTreePreOrder(root->left);</p>
<p style="margin-left:.0001pt;text-align:left;">printTreePreOrder(root->right);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 中序遍历打印二叉树</p>
<p style="margin-left:.0001pt;text-align:left;">void printTreeInOrder(TreeNode* root)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 基准情况,如果是空树,直接返回</p>
<p style="margin-left:.0001pt;text-align:left;">if (root == nullptr) return;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printTreeInOrder(root->left);</p>
<p style="margin-left:.0001pt;text-align:left;">cout << root->name << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;">printTreeInOrder(root->right);</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 后序遍历打印二叉树</p>
<p style="margin-left:.0001pt;text-align:left;">void printTreePostOrder(TreeNode* root)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 基准情况,如果是空树,直接返回</p>
<p style="margin-left:.0001pt;text-align:left;">if (root == nullptr) return;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printTreePostOrder(root->left);</p>
<p style="margin-left:.0001pt;text-align:left;">printTreePostOrder(root->right);</p>
<p style="margin-left:.0001pt;text-align:left;">cout << root->name << "\t";</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">然后将这些函数的声明也放到头文件tree_node.h中:</p>
<p style="margin-left:.0001pt;text-align:left;">void printTreePreOrder(TreeNode* root);</p>
<p style="margin-left:.0001pt;text-align:left;">void printTreeInOrder(TreeNode* root);</p>
<p style="margin-left:.0001pt;text-align:justify;">void printTreePostOrder(TreeNode* root);</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">接下来就可以在代码中实现具体的功能了:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">#include "tree_node.h"</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 定义一棵二叉树</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeG = {"G", nullptr, nullptr};</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeF = { "F", nullptr, nullptr };</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeE = { "E", &nodeG, nullptr };</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeD = { "D", nullptr, nullptr };</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeC = { "C", nullptr, &nodeF};</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeB = { "B", &nodeD, &nodeE };</p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode nodeA = { "A", &nodeB, &nodeC };</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">TreeNode* tree = &nodeA;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printTreePreOrder(tree);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printTreeInOrder(tree);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl << endl;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">printTreePostOrder(tree);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<h3 style="text-align:justify;"><strong><strong><strong>八、函数高阶</strong></strong></strong></h3>
<p style="margin-left:.0001pt;text-align:justify;">函数是模块化编程思想的重要体现,相对于传统的C语言,C++还提供了很多新的函数特性。这一章我们就来深入探讨一下函数的高级用法以及在C++中的新特性。</p>
<h4 style="text-align:justify;"><strong><strong><strong>8.1 内联函数</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">内联函数是C++为了提高运行速度做的一项优化。</p>
<p style="margin-left:.0001pt;text-align:justify;">函数让代码更加模块化,可重用性、可读性大大提高;不过函数也有一个缺点:函数调用需要执行一系列额外操作,会降低程序运行效率。</p>
<p style="margin-left:.0001pt;text-align:justify;">为了解决这个问题,C++引入了“内联函数”的概念。使用内联函数时,编译器不再去做常规的函数调用,而是把它在调用点上“内联”展开,也就是直接用函数代码替换了函数调用。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.1.1 内联函数的定义</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">定义内联函数,只需要在函数声明或者函数定义前加上inline关键字。</p>
<p style="margin-left:.0001pt;text-align:justify;">例如之前写过的函数:比较两个字符串、并返回较长的那个,就可以重写为内联函数:</p>
<p style="margin-left:.0001pt;text-align:left;">inline const string& longerStr(const string& str1, const string& str2)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return str1.size() > str2.size() ? str1 : str2;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">当我们试图打印输出调用结果时:</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << longerStr(str1, str2) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">编译器会自动把它展开为:</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << (str1.size() > str2.size() ? str1 : str2) << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">这样就大大提高了运行效率。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.1.2 内联函数和宏</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">内联函数是C++新增的特性。在C语言中,类似功能是通过预处理语句#define定义“宏”来实现的。</p>
<p style="margin-left:.0001pt;text-align:justify;">然而C中的宏本身并不是函数,无法进行值传递;它的本质是文本替换,我们一般只用宏来定义常量。用宏实现函数的功能会比较麻烦,而且可读性较差。所以在C++中,一般都会用内联函数来取代C中的宏。</p>
<h4 style="text-align:justify;"><strong><strong><strong>8.2 默认实参</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在有些场景中,当调用一个函数时它的某些形参一般都会被赋一个固定的值。为了简单起见,我们可以给它设置一个“默认值”,这样就不用每次都传同样的值了。</p>
<p style="margin-left:.0001pt;text-align:justify;">这种会反复出现的默认值,称为函数的“默认实参”。当调用一个有默认实参的函数时,这个实参可以省略。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.2.1 定义带默认实参的函数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">我们用一个string对象表示学生基本信息,调用函数时应传入学生的姓名、年龄和平均成绩。对于这些参数,我们可以指定默认实参:</p>
<p style="margin-left:.0001pt;text-align:left;">// 默认实参</p>
<p style="margin-left:.0001pt;text-align:left;">string stuInfo(string name = "", int age = 18, double score = 60) </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string info = "学生姓名:" + name + "\t年龄:" + </p>
<p style="margin-left:.0001pt;text-align:left;">to_string(age) + "\t平均成绩:" + to_string(score);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">return info;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">定义默认实参,形式上就是给形参做初始化。这里在整合学生信息时,使用了运算符+进行字符串拼接,并且调用to_string函数将age和score转换成了string。</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意,一旦某个形参被定义了默认实参,那它后面的所有形参都必须有默认实参。也就是说,所有默认实参的指定,应该在形参列表的末尾。</p>
<p style="margin-left:.0001pt;text-align:left;">// 错误,默认实参不在形参列表末尾</p>
<p style="margin-left:.0001pt;text-align:left;">//string stuInfo(string name = "", int age = 18, double score); </p>
<p style="margin-left:.0001pt;text-align:justify;">// 正确,可以前面的形参没有默认实参</p>
<p style="margin-left:.0001pt;text-align:justify;">string stuInfo(string name, int age = 18, double score = 60); </p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>8.2.2 使用默认实参调用函数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">函数调用时,如果对某个形参不传实参,那么它初始化时用的就是默认实参的值。由于之前所有形参都定义了默认实参,因此可以用不同的传参方式调用函数:</p>
<p style="margin-left:.0001pt;text-align:left;">cout << stuInfo() << endl; // "",18, 60.0</p>
<p style="margin-left:.0001pt;text-align:left;">cout << stuInfo("张三") << endl; // "张三",18, 60.0</p>
<p style="margin-left:.0001pt;text-align:left;">cout << stuInfo("李四", 20) << endl; // "李四",20, 60.0</p>
<p style="margin-left:.0001pt;text-align:left;">cout << stuInfo("王五", 22, 85.5) << endl; // "王五",22, 85.5</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">//cout << stuInfo(19, 92.5) << endl; // 错误,不能跳过前面的形参给后面传值</p>
<p style="margin-left:.0001pt;text-align:justify;">//cout << stuInfo(, , 59.5) << endl; // 错误,只能省略末尾的形参</p>
<p style="margin-left:.0001pt;text-align:justify;">可以看到,默认实参定义时要优先放到形参列表的尾部;而调用时,只能省略尾部的参数,不能跳过前面的形参给后面传值。</p>
<h4 style="text-align:justify;"><strong><strong><strong>8.3 函数重载</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">在C++中,同一作用域下,同一个函数名是可以定义多次的,前提是形参列表不同。这种名字相同但形参列表不同的函数,叫做“重载函数”。这是C++相对C语言的重大改进,也是面向对象的基础。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.3.1 定义重载函数</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">在上一章数组形参部分,我们曾经实现过几个不同的打印数组的函数,它们是可以同时存在的:</p>
<p style="margin-left:.0001pt;text-align:left;">// 使用指针和长度作为形参</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int* arr, int size)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int i = 0; i < size; i++)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << arr << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">// 使用数组引用作为形参</p>
<p style="margin-left:.0001pt;text-align:left;">void printArray(const int(&arr)[6])</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">for (int num : arr)</p>
<p style="margin-left:.0001pt;text-align:left;">cout << num << "\t";</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cout << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:justify;">{</p>
<p style="margin-left:.0001pt;text-align:justify;">int arr[6] = { 1,2,3,4,5,6 };</p>
<p style="margin-left:.0001pt;text-align:left;">printArray(arr, 6); // 传入两个参数,调用第一种实现</p>
<p style="margin-left:.0001pt;text-align:left;">printArray(arr); // 传入一个参数,调用第二种实现</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意:</p>
<ol><li>重载的函数,应该在形参的数量或者类型上有所不同;</li><li>形参的名称在类型中可以省略,所以只有形参名不同的函数是一样的;</li><li>调用函数时,编译器会根据传递的实参个数和类型,自动推断使用哪个函数;</li><li>主函数不能重载</li></ol>
<h5 style="text-align:justify;"><strong><strong><strong>8.3.2 有const形参时的重载</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">当形参有const修饰时,要区分它对于实参的要求到底是什么,是否要进行值的拷贝。如果是传值参数,传入实参时会发生值的拷贝,那么实参是变量还是常量其实是没有区别的:</p>
<p style="margin-left:.0001pt;text-align:justify;">void fun(int x);</p>
<p style="margin-left:.0001pt;text-align:justify;">void fun(const int x); // int常量做形参,跟不加const等价</p>
<p style="margin-left:.0001pt;text-align:left;">void fun2(int* p);</p>
<p style="margin-left:.0001pt;text-align:left;">void fun2(int* const p); // 指针常量做形参,也跟不加const等价</p>
<p style="margin-left:.0001pt;text-align:justify;">这种情况下,const不会影响传入函数的实参类型,所以跟不加const的定义是一样的;这叫做“顶层const”。这时两个函数相同,无法进行函数重载。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">另一种情况则不同,那就是传引用参数。这时如果有const修饰,就成了“常量的引用”;对于一个常量,只能用常量引用来绑定,而不能使用普通引用。</p>
<p style="margin-left:.0001pt;text-align:justify;">类似地,对于一个常量的地址,只能由“指向常量的指针”来指向它,而不能用普通指针。</p>
<p style="margin-left:.0001pt;text-align:justify;">void fun3(int &x);</p>
<p style="margin-left:.0001pt;text-align:justify;">void fun3(const int & x); // 形参类型是常量引用,这是一个新函数 </p>
<p style="margin-left:.0001pt;text-align:left;">void fun4(int* p);</p>
<p style="margin-left:.0001pt;text-align:left;">void fun4(const int* p); // 形参类型是指向常量的指针,这是一个新函数</p>
<p style="margin-left:.0001pt;text-align:justify;">这种情况下,const限制了间接访问的数据对象是常量,这叫做“底层const”。当实参是常量时,不能对不带const的引用进行初始化,所以只能调用常量引用做形参的函数;而如果实参是变量,就会优先匹配不带const的普通引用:这就实现了函数重载。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.3.3 函数匹配</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">如果传入的实参跟形参类型不同,只要能通过隐式类型转换变成需要类型,函数也可以正确调用。那假如有几个不同的重载函数,它们的形参类型可以进行自动转换,这时传入实参应该调用哪个函数呢?例如:</p>
<p style="margin-left:.0001pt;text-align:justify;">void f();</p>
<p style="margin-left:.0001pt;text-align:justify;">void f(int x); </p>
<p style="margin-left:.0001pt;text-align:justify;">void f(int x, int y); </p>
<p style="margin-left:.0001pt;text-align:justify;">void f(double x, double y = 1.5);</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">f(3.14); // 应该调用哪个函数?</p>
<p style="margin-left:.0001pt;text-align:justify;">确定到底调用哪个函数的过程,叫做“函数匹配”。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(1)候选函数</p>
<p style="margin-left:.0001pt;text-align:justify;">函数匹配的第一步,就是确定“候选函数”,也就是先找到对应的重载函数集。候选函数有两个要求:</p>
<ol><li>与调用的函数同名</li><li>函数的声明,在函数的调用点是可见的</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">所以上面的例子中,一共有4个叫做f的函数,它们都是候选函数。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(2)可行函数</p>
<p style="margin-left:.0001pt;text-align:justify;">接下来需要从候选函数中,选出跟传入的实参匹配的函数。这些函数叫做“可行函数”。可行函数也有两个要求:</p>
<ol><li>形参个数与调用传入的实参数量相等</li><li>每个实参的类型与对应形参的类型相同,或者可以转换成形参的类型</li></ol>
<p style="margin-left:.0001pt;text-align:justify;">上面的例子中,传入的实参只有一个,是一个double类型的字面值常量,所以可以排除 f() 和 f(int, int) 。而剩下的 f(int) 和 f(double, double = 1.5) 都是匹配的,所以有2个可行函数。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(3)寻找最佳匹配</p>
<p style="margin-left:.0001pt;text-align:justify;">最后就是在可行函数中,选择最佳匹配。简单来说,实参类型与形参类型越接近,它们就匹配得越好。所以,能不进行转换就实际匹配的,要优于需要转换的。</p>
<p style="margin-left:.0001pt;text-align:justify;">上面的例子中,f(int) 必须要将double类型的实参转换成int,而f(double, double = 1.5) 不需要,所以后者是最佳匹配,最终调用的就是它。第二个参数会由默认实参1.5来填补。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(4)多参数的函数匹配</p>
<p style="margin-left:.0001pt;text-align:justify;">如果实参的数量不止一个,那么就需要逐个比较每个参数;同样,类型能够精确匹配的要优于需要转换的。这时寻找最佳匹配的原则如下:</p>
<ol><li>如果可行函数的所有形参都能精确匹配实参,那么它就是最佳匹配</li><li>如果没有全部精确匹配,那么当一个可行函数所有参数的匹配,都不比别的可行函数差、并且至少有一个参数要更优,那它就是最佳匹配</li></ol>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">(5)二义性调用</p>
<p style="margin-left:.0001pt;text-align:justify;">如果检查所有实参之后,有多个可行函数不分优劣、无法找到一个最佳匹配,那么编译器会报错,这被称为“二义性调用”。例如:</p>
<p style="margin-left:.0001pt;text-align:justify;">f(10, 3.14); // 二义性调用</p>
<p style="margin-left:.0001pt;text-align:justify;">这时的可行函数为 f(int, int) 和 f(double, double = 1.5)。第一个实参为int类型,f(int, int)占优;而第二个实参为double类型,f(double, double = 1.5)占优。这时两个可行函数分不出胜负,于是就会报二义性调用错误。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.3.4 重载与作用域</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">重载是否生效,跟作用域是有关系的。如果在内层、外层作用域分别声明了同名的函数,那么内层作用域中的函数会覆盖外层的同名实体,让它隐藏起来。</p>
<p style="margin-left:.0001pt;text-align:justify;">不同的作用域中,是无法重载函数名的。</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">void print(double d)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "d: " << d << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">void print(string s)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "s: " << s << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">// 调用之前做函数声明</p>
<p style="margin-left:.0001pt;text-align:left;">void print(int i);</p>
<p style="margin-left:.0001pt;text-align:left;">print(10);</p>
<p style="margin-left:.0001pt;text-align:left;">print(3.14); // 将3.14转换为3,然后调用</p>
<p style="margin-left:.0001pt;text-align:left;">//print("hello"); // 错误,找不到对应参数的函数定义</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">void print(int i)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "i: " << i << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">如果想让函数正确地重载,应该把函数声明放到同一作用域下:</p>
<p style="margin-left:.0001pt;text-align:left;">#include<iostream></p>
<p style="margin-left:.0001pt;text-align:left;">using namespace std;</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 作用域和重载测试</p>
<p style="margin-left:.0001pt;text-align:left;">void print(int i)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "i: " << i << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">void print(double d)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "d: " << d << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;">void print(string s)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">cout << "s: " << s << endl;</p>
<p style="margin-left:.0001pt;text-align:left;">}</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">int main()</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">print(10);</p>
<p style="margin-left:.0001pt;text-align:left;">print(3.14); </p>
<p style="margin-left:.0001pt;text-align:left;">print("hello"); </p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">cin.get();</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<h4 style="text-align:justify;"><strong><strong><strong>8.5 函数指针</strong></strong></strong></h4>
<p style="margin-left:.0001pt;text-align:justify;">一类特殊的指针,指向的不是数据对象而是函数,这就是“函数指针”。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.5.1 声明函数指针</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">函数指针本质还是指针,它的类型和所指向的对象类型有关。现在指向的是函数,函数的类型是由它的返回类型和形参类型共同决定的,跟函数名、形参名都没有关系。</p>
<p style="margin-left:.0001pt;text-align:justify;">例如之前写过的函数:</p>
<p style="margin-left:.0001pt;text-align:left;">string stuInfo(string name = "", int age = 18, double score = 60) </p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">string info = "学生姓名:" + name + "\t年龄:" + </p>
<p style="margin-left:.0001pt;text-align:left;">to_string(age) + "\t平均成绩:" + to_string(score);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">return info;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">它的类型就是:string(string, int, double)。</p>
<p style="margin-left:.0001pt;text-align:justify;">如果要声明一个指向它的指针,只要把原先函数名的位置填上指针就可以了:</p>
<p style="margin-left:.0001pt;text-align:justify;">string (* fp) (string, int, double); // 一个函数指针</p>
<p style="margin-left:.0001pt;text-align:justify;">这里要注意,指针两侧的括号必不可少。如果去掉括号,</p>
<p style="margin-left:.0001pt;text-align:justify;">string *fp(string, int, double); // 这是一个函数,返回值为指向string的指针</p>
<p style="margin-left:.0001pt;text-align:justify;">这就变成了一个返回string *类型的函数。</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">更加复杂的例子也是一样,例如之前写过的比较字符串长度的函数:</p>
<p style="margin-left:.0001pt;text-align:left;">const string& longerStr(const string& str1, const string& str2)</p>
<p style="margin-left:.0001pt;text-align:left;">{</p>
<p style="margin-left:.0001pt;text-align:left;">return str1.size() > str2.size() ? str1 : str2;</p>
<p style="margin-left:.0001pt;text-align:justify;">}</p>
<p style="margin-left:.0001pt;text-align:justify;">对应类型的函数指针就是:</p>
<p style="margin-left:.0001pt;text-align:justify;">const string &(*fp) (const string &, const string &);</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.5.2 使用函数指针</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">当一个函数名后面跟调用操作符(小括号),表示函数调用;而单独使用函数名作为一个值时,函数会自动转换成指针。这一点跟数组名类似。</p>
<p style="margin-left:.0001pt;text-align:justify;">所以我们可以直接使用函数名给函数指针赋值:</p>
<p style="margin-left:.0001pt;text-align:left;">fp = longerStr; // 直接将函数名作为指针赋给fp</p>
<p style="margin-left:.0001pt;text-align:left;">fp = &longerStr; // 取地址符是可选的,和上面没有区别</p>
<p style="margin-left:.0001pt;text-align:justify;">也可以加上取地址符&,这和不加&是等价的。</p>
<p style="margin-left:.0001pt;text-align:justify;">赋值之后,就可以通过fp调用函数了。fp做解引用可以得到函数,而这里解引用符*也是可选的,不做解引用同样可以直接表示函数。</p>
<p style="margin-left:.0001pt;text-align:left;">cout << fp("hello", "world") << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">cout << (*fp)("C++", "is good") << endl;</p>
<p style="margin-left:.0001pt;text-align:justify;">所以这里能够看出,函数指针完全可以当做函数来使用。</p>
<p style="margin-left:.0001pt;text-align:justify;">在对函数指针赋值时,函数的类型必须精确匹配。当然,函数指针也可以赋nullptr,表示空指针,没有指向任何一个函数。</p>
<h5 style="text-align:justify;"><strong><strong><strong>8.5.3 函数指针作为形参</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">有了指向函数的指针,就给函数带来了更加丰富灵活的用法。比如,可以将函数指针作为形参,定义在另一个函数中。也就是说,可以定义一个函数,它以另一个函数类型作为形参。当然,函数本身不能作为形参,不过函数指针完美地填补了这个空缺。这一点上,函数跟数组非常类似。</p>
<p style="margin-left:.0001pt;text-align:left;">void selectStr(const string& s1, const string& s2, const string & fp(const string&, const string&));</p>
<p style="margin-left:.0001pt;text-align:justify;">void selectStr(const string& s1, const string& s2, const string & (*fp) (const string&, const string&));</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">同样,上面两种形式是等价的,*是可选的。</p>
<p style="margin-left:.0001pt;text-align:justify;">很明显,对于函数类型和函数指针类型来说,这样的定义太过复杂,所以有必要使用typedef做一个类型别名的声明。</p>
<p style="margin-left:.0001pt;text-align:left;">// 类型别名</p>
<p style="margin-left:.0001pt;text-align:left;">typedef const string& Func(const string&, const string&); // 函数类型</p>
<p style="margin-left:.0001pt;text-align:left;">typedef const string& (*FuncP)(const string&, const string&); // 函数指针类型</p>
<p style="margin-left:.0001pt;text-align:justify;">当然,还可以用C++ 11提供的decltype函数直接获取类型,更加简洁:</p>
<p style="margin-left:.0001pt;text-align:left;">typedef decltype(longerStr) Func2;</p>
<p style="margin-left:.0001pt;text-align:justify;">typedef decltype(longerStr) *FuncP2;</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<p style="margin-left:.0001pt;text-align:justify;">这样一来,声明函数指针做形参的新函数,就非常方便了:</p>
<p style="margin-left:.0001pt;text-align:justify;">void selectStr(const string&, const string&, Func);</p>
<p style="margin-left:.0001pt;text-align:justify;"></p>
<h5 style="text-align:justify;"><strong><strong><strong>8.5.4 函数指针作为返回值</strong></strong></strong></h5>
<p style="margin-left:.0001pt;text-align:justify;">类似地,函数不能直接返回另一个函数,但是可以返回函数指针。所以可以将函数指针作为另一个函数的返回值。</p>
<p style="margin-left:.0001pt;text-align:justify;">这里需要注意的是,这种场景下,函数的返回类型必须是函数指针,而不能是函数类型。</p>
<p style="margin-left:.0001pt;text-align:left;">// 函数指针作为返回值</p>
<p style="margin-left:.0001pt;text-align:left;">FuncP fun(int);</p>
<p style="margin-left:.0001pt;text-align:left;">//Func fun2(int); // 错误,不能直接返回函数</p>
<p style="margin-left:.0001pt;text-align:left;">Func* fun2(int);</p>
<p style="margin-left:.0001pt;text-align:left;"></p>
<p style="margin-left:.0001pt;text-align:left;">// 尾置返回类型</p>
<p style="margin-left:.0001pt;text-align:justify;">auto fun3(int) -> FuncP; </p>
<p style="margin-left:.0001pt;text-align:justify;">另外也可以使用尾置返回类型的方式,指定返回函数指针类型。</p>