在浏览了C++“三分天下”的世界版图之后,我们对这门强大的语言有了初步的认识,可以说已经迈出了踏入C++世界的第一步。但如何更深入地探索这个世界,完全融入C++的编程领域呢?是直接开始编写C++程序,还是应该采取其他学习策略?
正当我们犹豫不决时,注意到前方有一群人聚集在一起,似乎在讨论着什么。我们好奇地挤进人群,发现原来是一段C++程序正在“自我介绍”,吸引了一群初学者的注意和好奇。
这个场景提醒我们,学习C++的过程不仅是编写代码,更是理解代码背后的逻辑和原理。与他人交流,观察和分析示例程序,都是深化我们对C++理解的有效途径。
“大家好,欢迎来到奇妙的C++世界。我是你们的向导—一个简单而经典的C++程序,我的名字叫‘你好,C++.exe’。虽然我结构简单,但几乎每个C++初学者的第一次编程体验都是通过我这样的程序开始的。
在深入了解之前,你们可能会觉得C++程序很神秘:C++程序是如何创建的?它们由哪些部分组成?源文件又是什么?C++程序是如何执行的?但我要告诉你们,C++程序并不神秘。就像人类一样,每个C++程序都有自己的‘父母’—开发者,有自己的‘五官四肢’—组成部分,以及自己的‘生命过程’—从编写到执行的旅程。
什么?你们觉得这不可思议?别急,让我慢慢为你们揭开C++程序的神秘面纱,带你们一步步了解C++的世界。”
大家是否还记得在前面的章节中,我们一步步地编写出了一个C++程序?尽管只是一个简短的程序,由几行代码组成,功能简单,但却具备C++程序的基本要素:预处理指令、程序代码和注释,如图2-1所示。
通常,这些基本组成部分被整合在一个以.cpp为扩展名的文本文件中,我们称之为C++源文件。这个源文件详细记录了程序的结构和功能,定义了程序的存在和行为。
源文件的编写者是赋予程序形态和功能的设计师。通过修改这个源文件,设计师可以调整程序的特性,改变程序的功能,使程序能够适应不同的需求,完成多样化的任务。
图2-1 C++程序=预处理指令+程序代码+注释
下面一起来看程序的源文件,深入了解构成程序的“五官和四肢”。
在C++的源文件中,以“#”开始的行被称为预处理指令。这些指令的作用是指示编译器在正式编译之前对源文件进行特定的预处理操作,如插入文件、替换宏定义等,从而生成最终参与编译的源文件。
例如,在源文件“你好,C++.cpp”中,就包含一条预处理指令:
#include <iostream>
其中,#include指令用于将指定的文件插入该指令所在的位置,作为整个源文件的一部分。因为这样的文件通常插入到源文件的头部,所以被称为头文件(header file)。这里插入了iostream这个头文件,是因为我们在程序中需要用到其中定义的cout来完成字符串的屏幕输出(关于C++的输入输出,可以参考2.2节的介绍)。
#include指令后的文件名有两种表示方式:使用双引号(" ")和使用尖括号(<>)。这两种方式的区别在于,如果使用双引号来包含文件名,预处理器在处理该指令时将首先在当前目录(即源文件所在的目录)下搜索该文件,如果不存在,则继续在项目的包含目录下搜索(包括项目的默认头文件目录,如Visual Studio安装目录下的\VC\include文件夹,以及在项目属性中设置的项目附加头文件目录);而如果使用尖括号包含文件名,预处理器会直接在项目的包含目录下搜索该文件。因此,通常我们使用双引号来插入当前项目目录下的头文件(例如我们自己创建的头),而使用尖括号来插入各种项目包含目录下的库头文件(例如这里的iostream)。
程序代码主体由若干C++语句构成(通常以分号结束的一行代码就是一条语句),可以说语句是构成程序的基本单位。在源文件“你好,C++.cpp”中,第一条C++语句是:
int main()
这条语句连同它后面花括号内的内容,共同构成了main()函数,也称为主函数。函数是C++程序中最基本的组织单元,它把若干语句组织到一起共同实现某个特定的功能。如果说一条语句相当于人体的一个细胞,那么函数就相当于由若干细胞构成、拥有一定功能的器官。一个C++程序必须有一个主函数,而且只能有一个主函数。当C++程序开始执行时,首先进入主函数,然后逐条执行其中的语句,直到其中的语句执行完毕后退出主函数,程序执行也就宣告结束了。可以说,主函数定义了一个C++程序的一生。
当我们双击执行一个程序时,程序的进程首先会创建一个主线程。随后,这个主线程会按照约定启动运行时库,该运行时库接着调用预先定义好的main()函数,从而开始执行用户编写的代码。main()函数作为主线程的入口点,在一个C++程序中是必需的,且只能有一个。
同时,一个线程不能拥有多个执行入口点。在C++中,全局符号(包括变量和函数)必须唯一定义。作为全局函数的main(),同样必须唯一。
接下来的是主函数中的一条语句:
std::cout<<"你好,C++!";
cout是定义在头文件iostream中的一个输出流对象,它是C++标准库预定义的对象,通常用于将文字或数字输出到屏幕上。前面使用#include预处理指令包含iostream头文件就是为了在代码中使用这个对象。关于输入/输出流会在后面的章节中进行更详细的介绍,这里只要知道这条语句可以将“你好,C++!”这串文字输出到屏幕上即可。
这里需要注意的是,代码中使用的所有标点符号(例如,前面的尖括号,这条语句中的双引号、分号等)必须是英文格式的。有些中英文符号虽然看起来非常相似,但它们在代码中的含义和作用是不同的,很容易被初学者混淆,从而引起编译错误。这一点尤其值得初学者注意,以避免不必要的编程障碍。
程序的最后一条语句是:
return 0;
它表示程序成功执行完毕并返回(return)。通常,我们返回一个0值表示程序正常结束。如果在程序的执行过程中出现错误,可以返回一个非零值表示错误信息。程序的调用者可以通过这个返回值来判断程序是否成功执行。到这里,主函数中的语句执行完毕,程序的执行流程也就随之结束。
注释是源代码中的重要部分,编写者通过它们帮助代码的阅读者(包括后期维护人员和编写者自己)更好地理解代码。虽然注释不会参与编译,但它们可以显著提高代码的可读性和可维护性。例如:
// 你好,C++.cpp : 此文件包含main函数。程序执行将在此处开始并结束
这是一条单行注释,它解释了文件的主要功能和执行流程。
在C++中,注释分为以下两种形式。
● 单行注释:使用//表示,//之后直到行尾的所有内容都属于注释。单行注释通常用于对代码作简短的解释。
● 块注释:使用/*和*/作为注释块的起止,这对符号之间的所有内容都属于注释。块注释适用于多行的详细解释。例如:
/* 这是一段注释 */
在功能上,注释可以分为以下两种类型。
● 序言性注释:通常位于程序源文件的开始,提供程序的文件名、用途、编写时间、维护历史等信息。
● 解释性注释:用于解释代码段的功能或实现方式。
例如,我们可以在源文件的第一行加上一个序言性注释来解释这个源文件的功能:
// 你好,C++.cpp : 在屏幕上输出“你好,C++!”字符串
序言性注释被广泛用于大型项目中。通常,每个项目都有自己定义的序言性注释格式,用来向代码的阅读者说明一些必要的信息。下面是从一个实际项目中摘录的一段序言性注释,它说明了源文件的名字、作用、文件的修改历史等信息,帮助阅读者更好地理解代码。读者可以以此为模板,编写适合自己的序言性注释。
与序言性注释多位于源文件开始部分不同,解释性注释多分散于源代码的各个部分,用来向代码阅读者解释代码的含义,说明一些必要的问题等。例如,我们可以在前面例子中的程序代码之前加上一行注释:
//在屏幕上输出“你好,C++!”字符串 std::cout << "你好,C++!";
这句解释性注释用来向代码阅读者说明其下代码的功能是将字符串“你好,C++!”输出到屏幕上。
虽然程序的注释并不影响程序功能的实现,编译器也不会阅读注释,但好的注释可以显著增加程序代码的可读性,使程序更易于维护。谁都不愿意维护一份没有注释的代码,那无异于阅读天书。那么,什么样的注释才算是好的注释呢?
首先,该注释的地方一定要注释。
注释是对代码的“提示和说明”,旨在帮助代码的阅读者更好地理解代码。当我们认为代码不能被“一目了然”,或者需要特别说明时,就应该添加注释。例如:
这里的注释恰当地对较难理解的代码进行了解释(如果没有注释,不易理解double s = d -(int)d;这行代码的含义),提高了代码的可读性。
其次,不该注释的地方最好不要加注释。
注释只是对代码的“提示和说明”,如果代码本身已经能够很好地做到“望文生义”,就没有必要“画蛇添足”地加以注释。另外,需要注意的是,注释应为简短的说明性文字,而不是详尽的文档。程序的注释不可喧宾夺主,注释过多会让人眼花缭乱,反而降低了代码的可读性。例如,下面代码中的注释就不太合适:
这段代码对一些浅显易懂的语句也进行了详尽的解释,注释的内容远超过了代码的内容,这样不但不会增加代码的可读性,反而会让代码淹没在复杂的注释中,降低了代码的可读性。这样的注释实属画蛇添足。
另外,应该养成良好的代码注释习惯。编写代码时添加必要的注释,修改代码时一并修改相应的注释,删除无用的注释,以保证注释与代码的一致性。注释应当准确、易懂,避免二义性。错误的注释不但无益,反而有害。注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。例如:
// 在屏幕上输出“你好,C++!”字符串 // 对下方的代码进行注释 std::cout<<"你好,C++!"; int n = 1024; // 循环次数,对左侧的代码进行注释
如果代码比较长,特别是有多重嵌套时,应当在某些段落的结束处加以注释,以便查看嵌套结构的起始和结束位置。例如,一个多重循环的代码及其注释如下:
通过这些实践,我们可以确保注释的有效性和代码的可维护性。
程序代码不仅是写给编译器看的,更是写给程序员自己或他人看的。对于编译器而言,代码中有无注释并不重要,但对于阅读代码的程序员来说,合适的注释可以显著提高代码的可读性,使代码更易于理解,从而更易于维护。因此,注释是C++程序代码中不可或缺的一部分。程序代码中是否包含恰当的注释,也成为衡量一个程序员水平的重要标准之一。
预处理指令、程序代码与注释共同构成了程序的“五官与四肢”。然而,此时的程序还只是一个后缀为.cpp的文本文件。要将其转化为最终的可执行文件,例如.exe文件,我们还需要依靠编译器和链接器—它们就像是程序的“父母”,负责将原始文本转化为可执行的程序。
虽然程序是在Visual Studio这个开发环境中创建的,但真正将程序从源代码文件“你好,C++.cpp”转换为可执行程序“你好,C++.exe”的,是Visual Studio内置的C++编译器cl.exe和链接器link.exe。它们才是程序真正的创造者,是程序的“父母”。
为了便于人们编写、阅读和维护,我们的源文件是使用C++这种高级程序设计语言编写的。然而,计算机并不理解这种高级语言,也就无法直接执行高级语言编写的源文件。因此,需要一个翻译过程,将源文件中的C++高级语言翻译成计算机可以理解和执行的机器语言。程序的“父亲”编译器实际上是一个翻译官。他的工作就是将用C++这种高级语言编写的源文件(.cpp)翻译成计算机可以理解的目标文件(.obj),这个过程称为编译。
在Visual Studio中,程序的“父亲”名为cl.exe。用户可以在Windows的“开始”菜单中找到Developer Command Prompt for VS 2022并单击,然后在打开的命令提示符窗口中通过cl命令调用编译器,将C++源文件编译成目标文件。例如,使用以下命令将程序的源文件“你好,C++.cpp”编译成目标文件“你好,C++.obj”:
cl /c /EHsc你好,C++.cpp
其中,cl是调用编译器的指令,/c表示只编译不链接,/EHsc指定异常处理模型,而“你好,C++.cpp”是要编译的源文件名。经过编译器的处理,得到的是一个中间的目标文件“你好,C++.obj”,它还不能直接执行。
接下来,需要链接器将这个目标文件和Visual Studio提供的标准库目标文件(例如msvcrt.lib等)整合成最终的可执行文件。这个过程称为链接。在命令提示符窗口中,可以使用以下命令让链接器完成链接过程:
link你好,C++.obj
当然,编译和链接的工作也可以由编译器cl.exe一次性完成,命令如下:
cl /EHsc你好,C++.cpp
经过编译器和链接器的共同努力,程序从一个源文件“你好,C++.cpp”变成了一个可执行文件“你好,C++.exe”,就像一个新生儿一样来到了这个世界。整个过程如图2-2所示。
图2-2 编译和链接的过程
一旦生成可执行文件,就可以给操作系统下达指令让文件开始执行。一个程序的执行是从其主函数(main())开始的。但在进入主函数执行之前,操作系统会帮我们做很多准备工作。例如,当操作系统接到执行某个程序的指令后,它首先要创建相应的进程并分配私有的进程空间;然后,加载器会把可执行文件的数据段和代码段映射到进程的虚拟内存空间中;接着,操作系统会初始化程序中定义的全局变量等。做好这些准备工作后,程序就可以进入主函数开始执行了。
进入主函数后,程序会按照源代码制定的人生规划逐条执行语句。例如,以下源代码:
int main() { std::cout<<"你好,C++!"; return 0; }
可以看到,进入主函数后,程序执行的第一条语句是:
std::cout<<"你好,C++!";
这条语句的作用是在命令提示符窗口中输出“你好,C++!”这串文字。程序通过控制命令提示符窗口,显示这串文字,完成了程序员通过这行代码交给它的任务。
紧接着,下一条语句是:
return 0;
这条简洁的语句标志着主函数的结束,也意味着整个程序执行完毕。return 0;通常表示程序正常结束,没有错误发生。图2-3所示的是“你好,C++.exe”程序短暂而光辉的一生。
图2-3 “你好,C++.exe”程序短暂而光辉的一生
在上面的例子中,我们看到了一个C++程序的执行过程,它是从main()函数开始逐条语句往下执行的。这个过程看起来非常简单,但在每条语句的背后,还有着更多的故事。
在Visual Studio的调试模式下,我们可以通过反汇编视图(使用快捷键Alt+8打开)来查看C++程序中各条语句对应的汇编代码。这种视角让我们能够清晰地看到每条语句是如何被转换成机器指令的,以及程序的各个功能是如何在底层实现的。例如,“你好,C++.exe”程序虽然功能上只是简单地输出一个字符串,但当我们深入探究其内部时,会发现背后进行了许多操作。
在反汇编视图中,“你好,C++.exe”的关键操作可能包括但不限于以下内容:
当我们启动一个程序后,操作系统会创建一个新的进程来执行它。进程是应用程序的一个实例,拥有自己独立的内存空间(默认堆),作为其私有的虚拟地址空间。通常,一个应用程序的执行对应一个进程,而进程负责管理程序运行时的一切资源,包括资源的分配和调度等。
尽管进程是程序执行的调度单位,但它本身并不直接执行程序代码。具体的执行工作是由进程中的线程完成的。每个进程至少有一个主线程,多线程应用程序还可以包含多个辅助线程。线程共享所属进程的资源,但拥有自己的执行栈和执行路径。
在程序的主线程被创建后,它会进入main()函数开始执行。在执行具体的程序代码之前,主线程会进行一些初始化工作,例如保存现场环境、初始化堆、传递程序参数等。尽管C++程序的源代码可能只有简单的一行,但在汇编层面,这一行代码会被分解成多个步骤来完成。
从汇编视图来看,main()函数的执行涉及对寄存器的操作和对库函数的调用。例如,进入main()函数后,常见的操作是使用push rbp指令来保存当前的栈基址(在x86-64架构中,rbp通常用作栈基址寄存器)。这个操作确保了main()函数在执行完毕后能够返回到正确的位置继续执行。
除寄存器操作(如push、mov和pop等汇编指令)外,汇编代码还包括通过call指令完成的对其他函数的调用。例如,一个输出操作可能通过以下汇编指令实现:
call std::operator<<(std::ostream&, const char*)
这个call指令调用了C++标准库中的输出流操作符<<,用于将字符串输出到控制台。
在汇编视图下,我们可以看到每一条C++语句背后都有其实现细节。理解这些细节有助于我们深入理解每一条语句的工作原理。同样,如果我们发现某条语句的行为与预期不符,而代码层面的分析无法找到原因,那么从汇编层面探究语句背后的实现机制可能会揭示问题的根本所在。
人们编写程序的核心目的在于利用程序解决现实世界的问题。这些问题通常涉及对数据的输入、处理和输出,最终通过结果数据来实现问题的解决。因此,作为辅助人们解决问题的工具,程序的核心任务自然包括对数据的描述和处理。
人们已经用一个简洁的公式定义了程序的本质(见图2-4):
数据+算法=程序
在这个公式中,数据是对现实世界中各种事物的抽象和描述。例如,在C++程序中,我们使用不同的数据类型来抽象现实世界的数据:整数被抽象为int类型,小数被抽象为float类型等。我们通过定义这些数据类型的变量来表示生活中的具体数据。例如,使用int类型定义的变量nWidth来描述一个矩形的宽度,使用string类型定义的变量strName来描述一个人的姓名。此外,我们还可以创建自定义的数据类型来描述更复杂的实体,如创建一个Human类型来抽象表示“人”,并用它来定义一个变量,描述一个具体的人。
总之,程序的首要任务之一就是使用数据对现实世界中的事物进行有效的抽象和描述,为进一步的处理和分析奠定基础。
图2-4 程序的本质
用数据描述现实世界只是程序的初步任务,程序的最终目的是对这些数据进行处理,以获得所需的结果数据。例如,当我们用nWidth和nHeight描述一个矩形的宽度和高度时,这并非最终目标。我们的目标是计算矩形的面积。为此,我们必须对nWidth和nHeight进行处理,通过乘法运算(使用“*”运算符)计算它们的乘积,从而得到矩形的面积。这种对数据处理过程的抽象就是所谓的算法。因此,程序的第二个关键任务是描述和实现算法,对数据进行必要的处理以获得期望的结果。
数据和算法是程序不可或缺的两个组成部分,它们贯穿于程序的整个生命周期。即使是简单的程序,如“你好,C++.exe”,也包含了数据和算法的元素。例如,以下语句:
std::cout << "你好,C++!";
其中,“你好,C++!”是一个字符串数据,表示要输出到屏幕上的内容。整个语句展示了对该数据的处理过程:将其显示在屏幕上。数据和算法总是密不可分,它们共同构成了程序的两大核心任务。
通过“你好,C++.exe”程序的演示,我们已经知道了一个C++程序的任务就是描述数据和处理数据。这两大任务的对象都是数据。然而,数据并不能从虚空中产生,C++程序也不可能凭空创造出数据。那么,C++程序中的数据又从何而来呢?
在现实世界中,国与国之间的交流通过外交官来完成。在C++世界中,也有负责应用程序与外界进行数据交流的“外交官”,它们就是基本输入/输出流对象(iostream)。当一个C++程序在运行时,负责输入的外交官(istream)会将现实世界中的数据(例如来自键盘的用户输入数据)输入程序中,然后C++程序才能对这些数据进行处理。当C++程序生成结果数据后,负责输出的外交官(ostream)则会将结果数据输出(例如输出到屏幕或者文件)。在C++程序中,我们将这种数据在程序和外部对象(键盘、屏幕等)之间的流动称为流(stream),由istream和ostream这两位“外交官”负责。正是这两位外交官的协作,完成了C++程序与外界的数据交流。
为了便于使用,C++标准库中已经预先定义了4个基本的输入/输出流对象,其中最常用的是负责键盘输入的cin对象和负责屏幕输出的cout对象。另外,标准库还定义了两个辅助的输出对象,分别是用于输出程序错误信息的cerr和用于输出日志信息的clog。这些对象在标准库中已经预先定义,只需包含头文件<iostream>,我们就可以在程序中直接使用它们来实现程序的基本输入/输出操作,就像前面的示例程序中直接使用cout输出“你好,C++!”字符串一样。
cin和cout的使用非常简单,可以使用提取符“>>”从cin中提取用户通过键盘输入的数据,实现从键盘到程序的数据输入;也可以使用插入符“<<”向cout中插入程序内的数据,实现从程序到屏幕的数据输出。箭头的方向形象地表示了数据流动的方向:输入数据时,数据从cin对象流出到程序,所以箭头指向远离cin对象的方向;在输出数据时,数据从程序流入cout对象,所以箭头指向靠近cout对象的方向。例如,可以使用“<<”插入符向cout对象中插入数字或者字符串,然后将显示在屏幕上:
第一句中的插入符将数字1插入cout对象中,从而在屏幕上显示数字1。同理,第二句会在屏幕上显示一个字符串“你好,C++!”。在最后一条语句中,程序首先计算并得到1+2的结果数据3,然后第一个插入符将“1+2 =”这个字符串数据插入cout对象,接着第二个插入符将计算结果数据3插入cout对象。这样,我们在屏幕上看到的最终输出就是“1 + 2 = 3”这样字符串。
对于输入流对象cin,可以使用提取符“>>”从cin输入流中获取用户通过键盘输入的数据,并将其保存到程序内的变量中。例如:
在这里,我们首先定义了两个变量strName和nAge,分别用于保存用户输入的字符串数据和整数数据。然后,利用提取符“>>”从cin对象中提取用户通过键盘输入的数据。当程序执行到这里时,会暂停下来等待用户输入,一旦用户完成输入并按Enter键,“>>”就会从cin对象中提取用户输入的数据,并分别保存到相应的变量中,这样就完成了数据从键盘到应用程序的输入。
下面来看一个输入和输出配合使用的实例。
利用cin和cout,这段代码实现了一个整数加法计算程序。它可以接受用户输入的数据(两个整数),然后对数据进行处理(加法运算),并将结果(nRes)输出,从而圆满解决了计算两个整数之和的问题。
在输出数据时,除简单地输出数据外,在不同的应用场景下,我们往往对数据输出的格式也有着不同的要求。例如,输出一个double类型的小数,如果是学生成绩,通常保留一位小数就足够了;但如果是人民币汇率,至少需要保留三位小数。为了控制输出格式,C++提供了很多控制符,例如前面代码中使用的endl就是一个控制换行的控制符。
这些控制符可以使用“<<”直接插入输出流对象cout中,以实现对输出格式的控制。这些控制符大多定义在头文件<iomanip>中,因此,如果想在代码中使用它们来控制输出格式,需要先使用预处理指令#include引入这个头文件。
表2-1列出了C++中常用的格式控制符。
表2-1 常用的输出流格式控制符
综合运用这些控制符,可以满足一些特殊的输出格式要求。例如,如果要求以“保留小数点后两位有效数字”的格式输出小数1.23456,并在输出后换行,可以使用如下代码实现:
cout<<fixed<<setprecision(2)<<1.23456<<endl;
在这里,首先向cout对象插入一个fixed控制符,表示以固定的小数位数输出小数数值。然后,通过setprecision()设置需要保留的小数点后的有效数字位数,这样就可以达到“保留小数点后两位有效数字”的输出格式要求了。
除对数值数据输出格式的控制之外,很多时候我们还需要对字符串的输出格式进行控制,从而让程序的输出更加美观。与在输出流中插入控制符控制数值数据的输出格式类似,我们也可以通过在字符串中加入一些用于格式控制的转义字符来实现对字符串输出格式的控制。这里所谓的转义字符,就是在某些字符前加上“\”符号构成的特殊字符,这些字符不再表达它们原本的含义,转而表达的是对格式的控制或其他特殊意义。常用的格式控制转义字符包括:“\n”表示换行,将输出位置移动到下一行的开头;“\t”表示制表符,通常用于输出等宽的空白间隔,相当于一个Tab键的空格数。例如,下面的代码实现了换行显示:
cout<<"分多行\n显示一个字符串"<<endl;
程序执行后,将在屏幕上看到“\n”把一个字符串分成两行显示:
分多行 显示一个字符串
综合使用C++语言提供的这些输出流控制符和格式控制转义字符,可以实现灵活多样的自定义格式化输出,从而满足对输出格式的个性化要求。
除使用硬件设备(键盘、屏幕等)与程序进行输入/输出外,程序通常还需要对文件进行读写,以实现数据的输入/输出。具体来说,这包括从文件读取数据到应用程序进行处理(数据输入),以及将处理得到的结果数据写入文件进行保存(数据输出)。C++标准库提供了ifstream(输入文件流)和ofstream(输出文件流),分别用于从文件中读取数据和将数据写入文件。它们定义在<fstream>头文件中,如果想在程序中使用它们来读写文件,需要先包含(或引入)这个头文件。
需要注意的是,本节涉及C++中较高级的内容,如类、对象以及成员函数等。如果在阅读时遇到困难,可以先跳过这部分内容,等掌握了后面的必要知识再回头学习,会更容易理解。
使用文件流时,首先需要创建它们的实例对象。如果要将数据输出到文件,创建ofstream对象;如果要从文件读取数据,创建ifstream对象。通过在创建对象时提供文件名,这些对象就可以打开或者创建相应的文件,并与之关联。接下来就可以操作这些对象,从中读取或者写入数据。
文件流(读写文件)的操作与使用cin和cout非常类似。使用插入符“<<”将数据写入ofstream对象,将程序中的数据输出到关联的文件;使用提取符“>>”从ifstream对象中提取数据,把关联文件中的数据读入程序。例如,可以读取文件中的内容,然后将新的内容写入该文件:
在这段程序中,首先创建了一个输入文件流ifstream的对象fin,并利用它的构造函数将其关联到一个文本文件Data.txt。所谓构造函数,就是在对象创建时所执行的函数。这里,使用Data.txt文件名作为构造函数的参数,实际上就是打开这个文件并使用它创建fin对象。除此之外,还可以使用fin提供的open()成员函数来打开一个文件。当创建fin对象之后,为了提高程序的健壮性,在进行读取操作之前,我们通常会使用该对象的is_open()成员函数判断是否成功打开文件。只有在文件成功打开后,才能进行下一步的读取操作。当使用fin成功打开一个文件之后,就可以利用提取符“>>”从fin中提取各种数据,即从Data.txt文件中读取数据。例如,如果文件中的内容如下:
1981 9 22
对应地,程序中用于读取的代码如下:
fin>>nYear>>nMonth>>nDate;
默认情况下,提取符“>>”会以空格为分隔符,逐个从文件中读取数据并将其保存到相应的数据变量中。代码执行完毕后,文件中的1981、9和22这三个数值就分别被读取并保存到程序中的nYear、nMonth和nDate这三个变量中,实现从文件到程序的数据输入。在文件读取完毕之后,需要用close()成员函数关闭文件。
同样,为了将数据写入文件,需要创建一个输出文件流ofstream的对象fout,同时通过它的构造函数或open()函数打开一个文件,将这个文件和fout对象关联起来。然后,通过插入符“<<”将需要输出的数据插入fout对象,也就是将数据写入与之关联的文件。输出完成后,用close()函数关闭文件,这样就实现了从程序到文件的数据输出。整个过程如图2-5所示。
图2-5 文件读/写
这里介绍的只是文件输入/输出的最基本操作,包括打开文件、进行文本读写、关闭文件等,这些已经基本上能够满足日常功能开发的需要。然而,C++标准库提供了更为丰富的文件读写功能,例如读/写二进制文件、调整移动读写位置等,以支持更复杂的文件处理需求。
1.编写一个BMI指数计算程序,接收用户输入的身高(米)和体重(千克),计算并输出BMI指数(BMI指数的计算公式为:BMI=体重(千克)/身高(米)的平方)。
2.编写一个成绩录入程序,接收用户输入的学生姓名和成绩,并将其保存到data.txt文件中。