栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【现代C++】第零部分 开始:初始输入输出、注释、控制流和类简介

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【现代C++】第零部分 开始:初始输入输出、注释、控制流和类简介

本文属于「现代C++学习实践」系列文章之一,这一系列正式开始于2021/09/04,着重于现代C++(即C++11、14、17、20、23等新标准)和Linux C++服务端开发的学习与实践。众所周知,「C++难就难在:在C++中你找不到任何一件简单的事」。因此,本系列将至少持续到作者本人「精通C++」为止(笑)。由于文章内容随时可能发生更新变动,欢迎关注和收藏现代C++系列文章汇总目录一文以作备忘。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:[https://github.com/memcpy0/]。在这一仓库中,你可以看到本人学习C++的全过程,包括C++书籍源码、练习实现、小型项目等。

需要特别说明的是,为了透彻理解和全面掌握现代C++,本系列文章中参考了诸多博客、教程、文档、书籍等资料,限于时间精力有限,这里无法一一列出。部分重要资料的不完全参考目录如下所示,在后续学习整理中还会逐渐补充:

  • C++ Primer 中文版(第5版),Stanley B. Lippman、Barbara E. Moo等著,王刚、杨巨峰译,叶劲峰、李云、刘未鹏等审校,电子工业出版社;
  • 侯捷老师的公开课;
    • C++面向对象高级开发上、下:正确理解面向对象的精神和实现手法,涵盖对象模型、关键机制、编程风格、动态分配;
    • STL标准库与范型编程:深入剖析STL标准库之六大部件、及其之间的体系结构,并分析其源码,引导高阶泛型编程。
    • C++新标准C++11/14:在短时间内深刻理解C++2.0的诸多新特性,涵盖语言和标准库两层
    • C++内存管理机制:学习由语言基本构件到高级分配器的设计与实作,并探及最低阶malloc 的内部实现。
    • C++ Startup揭密:C++程序的生前和死后。认识Windows平台下的Startup Code(启动码),完全通透C++程序的整个运行过程。

文章目录
  • 1.1 编写一个简单的C++程序
    • 1.1.1 编译、运行程序
      • 程序源文件命名约定
      • 从命令行运行编译器、编译运行程序、查看 `main` 返回值
    • 1.1节练习*
  • 1.2 初识输入输出
    • 1.2.1 标准输入输出对象
    • 1.2.2 使用IO库的程序
    • 1.2.3 向流写入数据
    • 1.2.4 从流读取数据
    • 1.2.5 使用标准库中的名字、命名空间的作用
    • 1.2.6 完成程序
    • 1.2节练习
  • 1.3 注释简介
    • 1.3.1 C++中注释的种类
    • 1.3节练习
  • 1.4 控制流
    • 1.4.1 while语句
    • 1.4.1节练习
    • 1.4.2 for语句
    • 1.4.2节练习
    • 1.4.3 读取数量不定的输入数据
    • 1.4.3节练习
    • 1.4.4 if语句
    • 1.4.4节练习
    • 关键概念:C++程序的缩进和格式
  • 1.5 类简介
    • 1.5.1 `Sales_item` 类
      • 读写 `Sales_item`
      • `Sales_item` 对象的加法
      • 使用文件重定向
    • 1.5.1节练习
    • 1.5.2 初识成员函数(`item.isbn()`)
    • 1.5.2节练习
  • 1.6 书店程序
    • 1.6节练习

本章中将编写一个小的C++程序,来解决简单的书店问题——书店保存所有销售记录的档案,每条记录都保存了某本书的一次销售信息:ISBN 售出册数 书的单价 ,此外老板还需要查询此档案,计算每本书的销售量、销售额、平均售价。


1.1 编写一个简单的C++程序

每个C++程序都包含许多函数,其中一个必须命名为 main,操作系统通过调用 main 来运行C++程序1。最简单的 main 函数如下,它只返回给操作系统一个值:

int main() 
{
	return 0;
}

每个函数2的定义都包括四部分,即使是特殊的 main 函数也不例外:返回类型 return type 、函数名 function name 、一个括号包围的形参列表 parameter list(允许为空)、函数体 function body ——一个以左花括号 curly brace 开始、以右花括号结束的语句块 block of statements 。

6.2.5节讨论 main 函数的形参列表和其他形参类型。

main 函数的返回类型必须为整数类型 int ,这是一种内置类型 built-in type ,即语言自身定义的类型。类型是程序设计的基本概念之一,不仅定义了数据元素的内容,还定义了在这类数据上可以进行的运算。程序处理的数据都保存在变量中,每个变量都有自己的类型。

此处 main 函数体语句块中只有一条 return 语句,它结束函数的执行,还会向调用者返回一个值——注意,return 语句返回值的类型必须与函数返回类型相容。大多数系统中,main 函数的返回值被用来指示状态,零表示成功执行,非零返回值的含义由系统定义,用来指出错误类型。

1.1.1 编译、运行程序

编写程序后,就要编译它,如何编译程序取决于使用的操作系统和编译器。在【VS Code】Windows10下VS Code配置C/C++语言环境这篇文章中,为了配置语言学习环境,我稍微介绍了一下GCC、Clang、MSCV三个编译器:

常见的C/C++编译工具有GCC(GNU Compiler Collection ,即GNU编译器套件,GCC过去代表 GNU C Compiler ,但是由于编译器支持除C之外的其他几种语言,它现在代表 GNU Compiler Collections ,GCC官网在此处)、Clang(C language family frontend for LLVM ,提供兼容GCC的编译器驱动程序 clang.exe 和兼容MSVC的编译器驱动程序 clang-cl.exe ,Clang官网在此)、MSVC(Microsoft C++ 编译器工具集)三巨头,个人觉得Clang的架构更优雅、优化更惊艳,然而姜还是老的辣……在对C++ 20的语核支持“比赛”中,GCC首先接近了终点——虽然MSVC一个月前宣布自己冲过了终点。

编译器包含许多选项,其中有些参数能对有问题的程序结构发出警告,总是打开这些选项是一个好习惯,如在GNU编译器中使用 -Wall 选项,在MSCV中使用 /W4 。更多参数可以查阅编译器的参考手册。

此外,还需要为编译器配备编辑器,比如我正在学习的VS Code和Vim/Emacs,或者使用集成开发环境,比如Jetbrains全家桶的CLion(跨平台的C/C++ IDE,支持现代C++、libc++以及Boost):

程序源文件命名约定

多数编译器要求程序源码存储在一个或多个文本文件中——程序文件常被称为源文件 source file ;源文件的名字以一个后缀名(一个句点接一个或多个字符组成)结尾,后缀名告知系统这个文件是一个C++源文件;不同的编译器使用不同的后缀命名约定,包括 .cc, .cxx, .cpp, .cp 和 .c 。

从命令行运行编译器、编译运行程序、查看 main 返回值

命令行如Unix/Linux的Shell、Windows的命令提示符或Powershell中,可以编译程序。假设 $ 是系统提示符,CC 是编译器程序的名字,prog1.cc 保存了我们的 main 程序:

$ CC prog1.cc

执行命令后,编译器生成一个可执行文件——Windows中将其命名为 prog1.exe ,Unix/Linux中的编译器如GCC通常将其命名为 a.out(个人尝试,我在Windows中使用GCC编译,默认生成的可执行文件是 a.exe)。

要运行可执行文件,需要提供可执行文件的文件名:

  • Windows中可以忽略其扩展名 .exe ,执行 prog1(我执行的是 a ),但是加上扩展名也无妨。
  • 某些系统中,即使文件就在当前工作目录中,也必须显式指出文件的位置,此时在 $ 提示符后键入 .prog1 即可(Windows CMD中,键入 .prog1 也可以执行程序,但是不可使用 ./prog1 ),. 后跟着反斜线 指出该文件在当前目录中。
  • Unix系统中运行一个可执行文件,需要使用全文件名,包括文件扩展名,如 $ a.out(书上说的,没有试过)。
  • Linux系统中运行一个可执行文件,要显式指出文件位置,用一个 . 后跟着斜线 / 来指出可执行文件位于当前目录中,如 ./a.out 。

访问 main 函数返回值的方法依赖于系统。Unix和Windows系统中,执行完一个程序后,都可以通过 echo 命令获取其返回值:

  • Unix中,通过 $ echo $? 获取状态;
  • Windows中,查看状态需要键入 $ echo %ERRORLEVEL% 。
1.1节练习*

练习1.1:查阅你使用的编译器的文档,确定它使用的文件命名约定。编译并运行第2页的 main 程序。
答:文中已经编译运行。我使用的是GCC 8.1.0:
点击查看GCC官方文档:

练习1.2:改写程序,让它返回-1。返回值-1通常被当作程序错误的标识。重新编译并运行你的程序,观察系统如何处理 main 返回的错误标识。
答:在CMD中编译运行,系统没有处理返回的错误标识:

在DEV C++中编译运行:


1.2 初识输入输出

C++语言没有定义任何输入输出语句,替代的是用一个全面的?标准库 standard library 来提供IO机制(和很多其他设施)。不过,我们只需了解IO库中一部分基本概念和操作。

其中之一是 iostream 库,它包含两个基础类型 istream, ostream ,分别表示输入流和输出流——所谓的流 stream ,实际就是一个从IO设备中读出或写入IO设备的字符序列,随着时间的推移,字符顺序生成或消耗。

1.2.1 标准输入输出对象

标准库定义了4个IO对象:

  • 处理输入,使用名为 cin 的 istream 类型对象,此对象也被称为标准输入 standard input ;
  • 处理一般输出,使用名为 cout 的 ostream 类型对象,此对象也被称为标准输出 standard output ;
  • 处理警告和错误消息的输出,使用名为 cerr 的ostream 类型对象,此对象也被称为标准错误 standard error ;
  • 处理程序运行时的一般性消息(即日志)的输出,使用名为 clog 的ostream 类型对象。

系统会将程序运行的窗口与这些对象关联起来——读取 cin 时,数据从程序正在运行的窗口读入;向 cout, cerr, clog 写入数据时,将写到同一个窗口。

1.2.2 使用IO库的程序

如下是一个使用IO库的简单程序,提示用户输入两个数,然后输出它们的和。我们将具体分析这一程序:

#include 
int main() 
{
	std::cout << "Enter two numbers:" << std::endl;
	int v1 = 0, v2 = 0;
	std::cin >> v1 >> v2;
	std::cout << "The sum of " << v1 << " and " << v2
			  << " is " << v1 + v2 << std::endl;
	return 0;
}

程序的第一行 #include 告诉编译器,程序要使用 iostream 库。#include 是一个预处理 preprocess 指令,<> 中则指出了一个头文件 header ,每个使用标准库设施的程序都必须包含相关的头文件。且有以下注意事项:

  • #include 指令和头文件名字必须写在同一行中;
  • #include 指令必须出现在所有函数之外;
  • 一个程序的所有 #include 指令,一般都放在源文件的开始位置。
1.2.3 向流写入数据

main 的函数体中,第一条语句执行了一个表达式 expression ——它由一个或多个运算对象和一个或多个运算符组成。C++中,一个表达式产生一个计算结果。这条语句中的表达式使用了输出运算符 << 在标准输出上打印消息 "Enter two numbers:" 。

std::cout << "Enter two numbers:" << std::endl;

<< 运算符接受两个运算对象:左侧必须是一个 ostream 对象,右侧运算对象是要打印的值对象。<< 将给定的值写到给定的 ostream 对象中,计算结果就是其左侧运算对象,即写入给定值的那个 ostream 对象。

这一条语句使用了两次 << 运算符。由于 << 返回左侧运算对象,因此第一个 << 的结果成为了第二个 << 的左侧运算对象,第二个 << 的结果还是那个左侧运算对象。这条链中每个 << 运算符的左侧运算对象和计算结果都是同一个对象,本例中是 std::cout(当然,对象的内部状态发生了改变),这样就可以将一系列输出请求连接起来,少写几条语句。

原表达式等价于:

(std::cout << "Enter two numbers:") << std::endl;

或者等价于以下形式。其中第一个 << 打印一条消息,消息是一个字符串字面值常量 string literal ,即用一对双引号包围的字符序列。双引号间的文本被打印到标准输出。第二个 << 打印 std::endl ,这是一个称为操纵符 manipulator 的特殊值,写入它的效果是结束当前行、并将与设备关联的缓冲区 buffer 中的内容刷到设备中。缓冲刷新操作保证,到目前为止程序产生的所有输出都真正写入输出流中、而非停留在内存中等待写入流。

std::cout << "Enter two numbers:");
std::cout << std::endl;

调试时经常要添加打印语句,这类语句应该保证一直刷新流;否则如果程序崩溃,输出可能还留在缓冲区中,导致错误推断程序崩溃的位置。

1.2.4 从流读取数据

提示输入数据后,要读入用户的输入。这里定义了两个名为 v1, v2 的变量 variable 来保存输入,将它们定义为内置的 int 类型以表示整数,还将它们初始化 initialize 为0。初始化一个变量,就是在变量创建的同时为它赋予一个值。

int v1 = 0, v2 = 0;

接着读入输入的数据:

std::cin >> v1 >> v2;

输入运算符 >> 和 << 类似,接受两个运算对象:左侧运算对象必须是一个 istream 对象,右侧运算对象用来存储读入的数据。>> 从给定的 istream 对象读入数据并存入给定对象中,计算结果就是其左侧运算对象。

这一条语句使用了两次 >> 运算符。由于 >> 返回左侧运算对象,因此第一个 >> 的结果成为了第二个 >> 的左侧运算对象,第二个 >> 的结果还是那个左侧运算对象。这条链中每个 >> 运算符的左侧运算对象和计算结果都是同一个对象,本例中是 std::cin(当然,对象的内部状态发生了改变),这样就可以将一系列输入请求连接起来,少写几条语句。

因此,这一表达式等价于:

(std::cin >> v1) >> v2;

或者等价于以下形式。它们的执行结果是一样的。

std::cin >> v1;
std::cin >> v2;
1.2.5 使用标准库中的名字、命名空间的作用

不可不提的是,程序使用了 std::cin, std::cout 而非 cin, cout 。前缀 std:: 指出名字 cin, cout 定义在名为 std 的命名空间 namespace 中。命名空间能避免不经意的名字定义冲突、使用库中相同名字导致的冲突。

标准库定义的所有名字都在命名空间 std 中,只是通过命名空间使用标准库有点不便——每次使用其中的一个名字时,必须用作用域运算符 :: 来显式指出,想使用来自命名空间 std 中的名字,即写出 std:: 。

3.1节有一个更简单的访问标准库中名字的方法。

1.2.6 完成程序

最后就是打印计算结果。有意思的是,<< 的右侧运算对象可以不是相同类型的值(>> 也类似),如字符串字面值常量,或者 int 值。原因在于,标准库定义了不同版本的输入和输出运算符,以处理不同类型的运算对象。

std::cout << "The sum of " << v1 << " and " << v2
		  << " is " << v1 + v2 << std::endl;
1.2节练习

练习1.3:编写程序,在标准输出上打印 Hello, World 。

练习1.4:我们的程序使用加法运算符+来将两个数相加。编写程序使用乘法运算符*,来打印两个数的积。

练习1.5:我们将所有输出操作放在一条很长的语句中。重写程序,将每个运算对象的打印操作放在一条独立的语句中。

练习1.6:解释下面程序片段是否合法:

std::cout << "The sum of " << v1;
			   << " and " << v2;
			   << " is " << v1 + v2 << std::endl;

答:不合法。原因在于,前两个分号使得第三个及以后的 << 运算符失去了左侧运算对象,无法将右侧对象的值输出到 ostream 对象中。应该去掉多余的分号,修改为:

std::cout << "The sum of " << v1
			   << " and " << v2
			   << " is " << v1 + v2 << std::endl;

1.3 注释简介

这里简单介绍C++如何处理注释 comments ——注释用于概述算法、确定变量用途、解释代码段,帮助读者理解程序。编译器会忽略注释,注释对程序的行为或性能不会有任何影响。

错误的注释比完全没有注释更糟糕,因为它会误导读者!因此当你修改代码时,不要忘记同时更新注释!

1.3.1 C++中注释的种类

C++中有两种注释,单行注释(用于半行和单行附注)和界定符对注释(用于多行解释):

  • 前者以 // 开始、以换行符结束,当前行双斜线右侧的所有内容都会被编译器忽略。这种注释可以包含除换行符外的任何文本,哪怕是额外的双斜线;
  • 后者使用继承自C的两个界定符 ,以 结束,中间的所有内容都被当作注释。界定符对注释可以放置于任何允许放置制表符、空格符和换行符的地方。这种注释可以包含除 */ 外的任意内容,包括换行符,因此可以跨越程序中的多行(并不是必须的)。
    当界定符对注释跨越多行时,最好显示指出其内部的程序行都属于多行注释的一部分。具体做法是,注释内的每行都以一个星号开头。

一个同时包含两种注释的程序:

#include 

int main() {
	// 提示用户输入两个数
	std::cout << "Enter two numbers:" << std::endl;
	int v1 = 0, v2 = 0;   // 保存我们读入的输入数据的变量
	std::cin >> v1 >> v2; // 读取输入数据
	std::cout << "The sum of " << v1 << " and " << v2
			  << " is " << v1 + v2 << std::endl;
	return 0;
}

注意,界定符对注释不能嵌套!因为前面说过,界定符对注释可以包含除 */ 外的任意内容,也包括一个或多个 ——这个 */ 会提前结束界定符对,导致后面的内容不被看做注释。

不能嵌套
 * “不能嵌套”几个字会被认为是源码,
 * 像剩余程序一样处理
 */
int main() {
    return -1;
}

在调试期间可能要注释掉一些代码,由于其中可能包含界定符对注释,就可能导致注释嵌套错误。最好的方法是使用单行注释方式,注释掉代码段的每一行:

// 
1.3节练习

练习1.7:编译一个包含不正确的嵌套注释的程序,观察编译器返回的错误信息。

练习1.8:指出下列哪些输出语句是合法的(如果有的话):

std::cout << "";
std::cout << " */;
std::cout << " ;

预测编译这些语句会产生什么样的结果,实际编译这些语句来验证你的答案(编写一个小程序,每次将上述一条语句作为其主体),改正每个编译错误。


1.4 控制流

前面的语句都是顺序执行的:语句块的第一句首先执行,然后是第二条语句……然而我们可以写出更加复杂的执行路径。

1.4.1 while语句

while语句反复执行一段代码,直到给定条件为假为止。其形式如下,执行过程是交替地检测 condition 条件和执行关联的语句 statement ,直到 condition 为假时停止。所谓条件 condition 就是一个或真或假的表达式,只要 condition 为真,statement 就会被执行。执行完 statement ,会再次检测 condition 。只要 condition 仍为真,statemetn 再次被执行。如此交替检测 condition 和执行 statement ,直到 condition 为假为止。

while (condition)
	statement

下面用while语句编写一段程序,求 1 到 10 这十个数之和。不难发现,while语句的条件中使用了小于等于运算符 <= 来比较 val 的当前值和 10 ,只要 val <= 10 ,条件就为真,就执行while循环体。如此循环,交替检测条件、执行循环体,直至 val > 10 为止。

本例中的循环体是由两条语句组成的语句块 block ——用花括号包围的零条或多条语句的序列。语句块也是语句的一种,在任何要求使用语句的地方都可以使用语句块。本例语句块的第一条语句使用了复合赋值运算符 += ,此运算符将右侧运算对象加到左侧运算对象上,将结果保存到左侧运算对象中,本质上与一个加法结合一个赋值 assignment 相同;下一条语句使用前缀递增运算符 ++ ,递增运算符将运算对象的值增加 1 ,++val 等价于 val = val + 1 。

//C++ version
#include 
int main() 
{
	int sum = 0, val = 1;
	//只要val的值小于等于10,while循环就会持续执行
	while (val <= 10) {
		sum += val; 	//将sum+val赋予给sum,用于保存和
		++val;		    //将val加1,表示从1到10的每个数
	}
	std::cout << "Sum of 1 to 10 inclusive is "
	   		  << sum << std::endl;
	return 0;
}	

跳出循环后,继续执行之后的语句,本例中继续执行打印输出语句,然后执行 return 语句完成 main 程序。编译并执行这个程序,它会打印出:

Sum of 1 to 10 inclusive is 55
1.4.1节练习

练习1.9:编写程序,使用 while 循环将 50 到 100 的整数相加。

练习1.10:除了 ++ 运算符将运算对象的值增加 1 之外,还有一个递减运算符 -- 实现将值减少 1 。编写程序,使用递减运算符在循环中按递减顺序打印出 10 到 0 之间的整数。

练习1.11:编写程序,提示用户输入两个整数,打印出这两个整数指定范围内的所有整数。

1.4.2 for语句

上述示例中,在循环条件中检测变量、在循环体中递增变量的模式使用非常频繁,因此C++专门定义了第二种循环语句——for语句,来简化符合这种模式的语句。每个for语句都包含两个部分:循环头和循环体。循环头控制循环体的执行次数,由三个部分组成:一个初始化语句 init-statement(只在for循环入口处执行一次)、一个循环条件 condition(循环体每次执行前都要先检查循环条件)以及一个表达式 expression(表达式在for循环体之后执行,执行后重新检测循环条件)。循环体同while循环一样,是一个语句 statement 。

重写上例程序,int val = 1 是初始化语句,定义了一个 int 型对象 val 、并赋初值为 1 ,变量 val 仅在for循环内部存在,在循环结束之后不能使用;val <= 10 是循环条件,每次先检测循环条件,为真时再执行循环体;表达式是 ++val ,在for循环体之后执行,其后for语句重新检测循环条件,仍为真就再次执行for循环体。如此循环持续这一过程,直至循环条件为假。

#include 
int main()
{
	int sum = 0;
	for (int val = 1; val <= 10; ++val) //从1加到10
		sum += val;	//等价于sum = sum + val
	std::cout << "Sum of 1 to 10 inclusive is "
			  << sum << std::endl;
	return 0;
}

上述for循环的总体执行流程如下:

  1. 创建变量 val ,初始化为 1 ;
  2. 检测 val 是否小于等于 10 。若检测成功则执行 for 循环体。若失败则退出循环,继续执行for循环体之后的第一条语句;
  3. 将 val 的值增加 1 ;
  4. 重复第二步的条件检测,只要条件为真就继续执行剩余步骤。
1.4.2节练习

练习1.12:下面的for循环完成了什么功能?sum 的终值是多少?

int sum = 0;
for (int i = -100; i <= 100; ++i)
	sum += i;

答:for循环将 -100 到 100 之间的整数相加,sum 的终值为 0 。

练习1.13:使用for循环重做1.4.1节的所有练习。

练习1.14:对比for循环和while循环,两种形式的优缺点各是什么?

练习1.15:编写程序,包含第14页“再谈编译”中讨论的常见错误,熟悉编译器生成的错误信息。

1.4.3 读取数量不定的输入数据

如果我们预先不知道要对多少个数求和,就需要不断读取数据直至没有新的输入为止。

#include 
int main()
{
	int sum = 0, value = 0;
	while (std::cin >> value) //读取数据直到遇到文件尾,计算所有读入的值的和
		sum += value; //等价于sum = sum + value
	std::cout << "Sum is: " << sum << std::endl;
	return 0;
}

显然,数据读取操作是在while的循环条件中完成的。对while循环条件进行求值,就是执行表达式 std:: cin >> value ,这一表达式从标准输入读取一个数,保存在 value 中——输入运算符 >> 的左侧运算对象必须是一个 istream 对象,右侧运算对象用来存储读入的数据。此处的 >> 从给定的 istream 对象 std::cin 读入数据并存入给定对象 value 中,计算结果就是其左侧运算对象 std::cin 。 因此,此循环条件实际上检测的是 std::cin 。

特别地,当我们使用一个 istream 对象作为条件时,效果是检测流的状态。如果流是有效的,即流未遇到错误,则检测成功;遇到文件结束符 end-of-file 或一个无效输入(如要求整数,读入的值却是一个字符串),istream 对象的状态会变为无效。处于无效状态的 istream 对象会使条件变为假。

因此,本例的while循环将一直执行,直到遇到文件结束符或无效输入。一旦条件失败,while循环将结束,并执行下一条语句,打印 sum 的值和一个 std::endl 。如果输入 3 4 5 6 +文件结束符,则程序会输出 Sum is: 18 。

我们可以从键盘键入文件结束符以指出文件结束,只是不同的操作系统有不同的约定。Windows系统中是输入 Ctrl+Z ,然后按下 Enter 键;Unix系统(包括Mac OS X系统)中,输入文件结束符是用 Ctrl+D 。

再探编译:

1.4.3节练习

练习1.16:编写程序,从 cin 读取一组数,输出其和。

1.4.4 if语句

类似其他语言,C++也提供了if语句来支持条件执行。比如说写一个程序,统计在不定长度的输入中每个值连续出现了多少次——程序以 val, currVal 两个变量的定义开始,currVal 记录正在统计出现次数的那个数,val 则保存从输入读取的每个数;最外层的if语句保证输入不为空,它读取一个数值存入 currVal 中,如果读取成功,则条件求值为真,继续执行条件之后的语句块;接着定义 cnt ,用于统计每个数值连续出现的次数;然后用一个while循环反复从标准输入读取整数;while循环中是第二条if语句,它使用相等运算符 == 检测 val 是否等于 currVal ,等于则执行紧跟条件之后的语句,将 cnt 增加 1 ,表示再次看到了 currVal ,否则执行 else 之后的语句块,输出语句打印我们刚刚统计完的值和值出现的次数,赋值语句将 cnt 重置为 1 、将 currVal 重置为刚刚读入的值 val :

#include 
int main() 
{
	// currVal是我们正在统计的数;我们将读入的新值存入val
	int currVal = 0, val = 0;
	// 读取第一个数,并确保确实有数据可以处理
	if (std::cin >> currVal) {		
		int cnt = 1;				// 保存我们正在处理的当前值的个数
		while (std::cin >> val) {	// 读取剩余的数
			if (val == currVal) 	// 如果值相同
				++cnt;				// 将cnt加1
			else {
				std::cout << currVal << " occurs "
						  << cnt << " times" << std::endl;
				currVal = val;	    // 记住新值
				cnt = 1;			// 重置计数器
			} 
		} // while循环在这里结束
		// 记住打印文件中最后一个值的个数
		std::cout << currVal << " occurs "
				  << cnt << " times" << std::endl;
	} // 最外层的if语句在这里结束
	return 0;
}

对应的输入和输出是:

Input: 42 42 42 42 42 55 55 62 100 100 100
Output:
42 occurs 5 times
55 occurs 2 times
62 occurs 1 times
100 occurs 3 times
1.4.4节练习

练习1.17:如果输入的所有值都是相等的,本节的程序会输出什么?如果没有重复值,输出又会是怎样的?

练习1.18:编译并运行本节的程序,给它输入全都相等的值。再次运行程序,输入没有重复的值。

练习1.19:修改你为1.4.1节练习1.10所编写的程序(打印一个范围内的数),使其能处理用户输入的第一个数比第二个数小的情况。

关键概念:C++程序的缩进和格式

很大程度上,C++程序是格式自由的,何处放置花括号、缩进、注释、换行符通常不会影响程序的语义。如表示 main 函数体开始的左花括号,可以放在 main 的同一行中,或者放在下一行的起始位置,或者放在我们喜欢的其他任何位置——唯一的要求是左花括号必须是 main 形参列表后的第一个非空、非注释的字符。

我们很大程度上可以按照自己的意愿自由地设定程序的格式,但是所做的选择会影响程序的可读性,所以必须谨慎行事——把整个 main 函数写在很长的单行内,虽然是合乎语法的,但会非常难读。

C/C++正确格式的辩论是无休无止的。以DevC++菜单栏的AStyle为例,可以设置格式化选项、并依此格式化当前文件,如下所示可以看到括号风格(很多种如 Allman, Java, K&R, Stroustrup, Whitesmith, Banner, GNU, ...)、缩进风格(Spaces, Tabs, Force Tab, Force Tab X)、Tab宽度、最大行数(搞错了吧,应该是列数)、缩进以下类型的代码、格式化命令(Artistic Style 3.1 是一个免费、快速、小型、自动化的格式化工具,用于C/C++, C++/CLI, Objective-C, C#, Java):

作者认为,不存在唯一正确的风格,但保持一致性是非常重要的……其他可能的程序格式总是存在的,当你要选择一种格式风格时,思考一下它会对程序的可读性和易理解性有什么影响,而一旦选择了一种风格,就要坚持使用。


1.5 类简介

为了解决书店程序,需要了解的唯一一个C++特性:如何定义一个数据结构 data structure ,以表示销售数据。事实上,C++中我们通过定义一个类 class 来定义自己的数据结构——一个类定义了一个新的类型和与其关联的一组操作,这一新类型的类型名就是类名。类机制是C++最重要的特性之一,也是C++最初的设计焦点——能定义在使用上像内置类型一样自然的类类型 class type 。

使用类需要了解三件事情:类名是什么?类是在哪里定义的?类支持什么操作?和使用标准库设施一样,使用自定义的类也要包含相关的头文件,来访问为自己的应用程序所定义的类。注意:头文件习惯上按照其中定义的类的名字来命名,且通常使用 .h 作为头文件的后缀(有些程序员习惯 .H, .hpp, .hxx 等)。标准库头文件通常不带后缀。不过,编译器一般不关心头文件名的形式,只是有的IDE对此有特定要求。

本节编写一个简单的类,用于书店程序,类名为 Sales_item ,在头文件 Sales_item.h 中定义。后续章节中学习了更多关于类型、表达式、语句和函数的知识后,才会真正实现这个类。

1.5.1 Sales_item 类

Sales_item 类的作用是表示一本书的总销售额、售出册数和平均售价。由于类名就是类型名,Sales_item 类定义了一个名为 Sales_item 的类型。与内在类型一样,我们可以定义类类型的变量,以下语句表示 item 是一个 Sales_item 类型的对象,或称为一个 Sales_item 对象。

Sales_item item;

为了使用一个类,我们不必关心它是如何实现的,只需知道类对象可以执行什么操作。当前,我们所知的可以在 Sales_item 对象上执行的全部操作,就是列出的这些操作:

  • 调用一个名为 isbn 的函数从一个 Sales_item 对象中提取ISBN书号;
  • 用输入运算符 >> 和输出运算符 << 读、写 Sales_item 类型的对象;
  • 用赋值运算符 = 将一个 Sales_item 对象的值赋给另一个 Sales_item 对象;
  • 用加法运算符 + 将两个 Sales_item 对象相加。注意,两个对象必须表示同一本书即相同的ISBN(对调用者的要求),加法结果是一个新的 Sales_item 对象,其ISBN与两个运算对象相同,其总销售额和售出册数则是两个运算对象的对应值之和。
  • 使用复合运算符 += 将一个 Sales_item 对象加到另一个 Sales_item 对象上。

要牢记的是,类 Sales_item 的作者定义了类类型对象上可以执行的所有动作,即 Sales_item 类定义了创建一个 Sales_item 对象时会发生什么事情,以及对 Sales_item 进行赋值、加法或输入输出运算时会发生什么事情。

读写 Sales_item

下面的程序从标准输入读入数据,存入一个 Sales_item 对象中,然后将 Sales_item 的内容写到标准输出。注意,该程序在包含来自标准库的头文件时,使用 <> 包围头文件名,对不属于标准库的头文件,则用双引号 "" 包围:

#include 
#include "Sales_item.h"
int main() 
{
	Sales_item book;
	std::cin >> book; 				// 输入ISBN号、售出的册数和销售价格
	std::cout << book << std::endl; // 写入ISBN号、售出的册数、总销售额和平均价格
	return 0;
}

该程序的输入和输出如下所示。输入告诉我们以每本24.99美元的价格售出了4册书,输出告诉我们总售出册数为4、总销售额为99.96美元、平均售价为24.99美元:

Input:
0-201-70353-X 4 24.99
Output:
0-201-70353-X 4 99.96 24.99
Sales_item 对象的加法

下面的程序从标准输入读入数据,存入两个 Sales_item 对象之中,输出表达式完成加法运算并打印结果。这一程序与1.2.2 使用IO库的程序非常相似,只是把运算对象从两个整数换成了两个 Sales_item 而已,读取和打印和的运算方式没有发生任何变化。当然,由于运算符重载,“和”的概念完全不同——我们对 int 对象用加法运算符 + ,计算的是传统意义上的算术加法和;对 Sales_item 用加法运算符 + ,得到的是两个 Sales_item 对象的成员对应相加的结果。

#include 
#include "Sales_item.h"
int main()
{
	Sales_item item1, item2;
	std::cin >> item1 >> item2;	// 读入一对交易记录
	std::cout << item1 + item2 << std::endl; // 打印它们的和
	return 0;
}

该程序的输入和输出如下所示。

Input:
0-201-78345-X 3 20.00
0-201-78345-X 2 25.00
Output:
0-201-78345-X 5 110 22
使用文件重定向

测试程序时,反复从键盘敲入这些销售记录作为程序的输入,非常乏味费时。幸好多数操作系统支持文件重定向机制,允许我们将标准输入和标准输出与命名文件关联起来:

$ addItems < infile > outfile

假设 $ 是操作系统提示符,加法程序已经编译为 addItems.exe 的可执行文件(在Unix中是 addItems ),则上述命令从一个名为 infile 的文件读取销售记录,并将输出结果写入到一个名为 outfile 的文件中,两个文件都位于当前目录中。

除此以外,使用文件重定向机制,可以进一步优化对拍技巧。

1.5.1节练习

练习1.20:在网站http://www.informit.com/title/0321714113上,第一章的代码目录中包含了头文件 Sales_item.h 。将它拷贝到你自己的工作目录中,用它编写一个程序,读取一组书籍销售记录,将每条记录打印到标准输出中。

练习1.21:编写程序,读取两个ISBN相同的 Sales_item 对象,输出它们的和。

练习1.22:编写程序,读取多个具有相同ISBN的销售记录,输出所有记录的和。

1.5.2 初识成员函数(item.isbn())

将两个 Sales_item 对象相加的程序,首先应检查两个对象是否具有相同的ISBN。如果相等则程序打印计算结果,并返回0表示成功;如果条件失败则打印错误消息,并返回-1表示错误标识。

#include 
#include "Sales_item.h"
int main() 
{
	Sales_item item1, item2;
	std::cin >> item1 >> item2;
	// 首先检查item1和item2是否表示相同的书
	if (item1.isbn() == item2.isbn()) {
		std::cout << item1 + item2 << std::endl;
		return 0;  // 表示成功
	} else {
		std::cerr << "Data must refer to same ISBN" << std::endl; 
		return -1; // 表示失败
	}
}

需要注意的是if语句的检测条件,调用了名为 isbn() 的成员函数 member function ,成员函数是定义为类的一部分的函数,有时也称为方法 method 。我们常以一个类对象的名义来调用成员函数,如 item1.isbn() 中使用点运算符 . 表达需要“名为 item1 的对象的 isbn 成员”。

要注意的是,点运算符只能用于类类型的对象,其左侧运算对象必须是一个类类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象指定的成员。

1.5.2节练习

练习1.23:编写程序,读取多条销售记录,并统计每个ISBN(每本书)有几条销售记录。

练习1.24:输入表示多个ISBN的多条销售记录来测试上一个程序,每个ISBN的记录应该聚在一起。


1.6 书店程序

现在万全具备,我们已经准备好完成书店程序了。先从一个文件中读取销售记录,生成每本书的销售报告,显示售出册数、总销售额和平均售价。此处假定每个ISBN书号的所有销售记录在文件中是聚在一起保存的。

程序将每个ISBN的所有数据合并起来,存入名为 total 的变量中,同时使用另一个名为 trans 的变量保存读取的每条销售记录。如果 total 和 trans 指向相同的ISBN,我们更新 total 的值;否则打印 total 的值,并重置为刚刚读取的数据 trans 。如果在第一条if语句就读取失败,则意味着没有任何销售记录,于是直接跳到最外层的else分支,打印一条警告消息,告诉用户没有输入:

#include 
#include "Sales_item.h"
int main()
{
	Sales_item total;			// 保存下一条交易记录的变量
	// 读入第一条交易记录,并确保有数据可以处理
	if (std::cin >> total) {
		Sales_item trans;		// 保存和的变量
		// 读入并处理剩余交易记录
		while (std::cin >> trans) {
			// 如果我们仍在处理相同的书
			if (total.isbn() == trans.isbn())
				total += trans; // 更新总销售额
			else {
				std::cout << total << std::endl;
				total = trans;  // total现在表示下一本书的销售额
			}
		}
		std::cout << total << std::endl; // 打印最后一本书的结果
	} else {
		// 没有输入!警告读者
		std::cerr << "No data?!" << std::endl;
		return -1; //表示失败
	}
	return 0;
}
1.6节练习

练习1.25 借助网站上的 Sales_item.h 头文件,编译并运行本节给出的书店程序。



  1. Main function ↩︎

  2. Functions ↩︎

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/317557.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号