这一章在干嘛?

循环解决的是”重复多少次”,这一章解决的是”该走哪条路”:用户选了哪个菜单项?输入的是不是 0?这个字符是字母还是数字?C++ 提供 if、if else、switch 三种分支语句做选择,用 &&、||、! 把多个条件拼成复杂判断,还有条件运算符 ? : 这种单行版选择器。最后把控制台 I/O 的本领平移到文本文件的读写上。加上前一章的循环,程序流程控制的工具箱就齐了。

6.1 if 语句与 if else 语句

6.2 逻辑运算符与短路求值

6.3 cctype 字符函数库

6.4 条件运算符

6.5 switch 语句

6.6 break、continue 与 goto

6.7 读取数字的循环

6.8 简单文件输入输出

6.1 if 语句与 if else 语句

简单 if

if 语句的语法与 while 相似:

if (test-condition)
    statement

条件为真执行 statement(单条语句或块),为假跳过。测试条件同样会被转换成 bool(0 假、非 0 真),整个 if 结构算一条语句。

▲ 图 6.1 if 语句的结构

程序清单 6.1 在逐字符读入的循环里用 if 统计空格:

// if.cpp -- 使用 if 语句
#include <iostream>
int main()
{
    using std::cin;    // using 声明
    using std::cout;
    char ch;
    int spaces = 0;
    int total = 0;
    cin.get(ch);
    while (ch != '.')    // 句号处结束
    {
        if (ch == ' ')   // 检查 ch 是不是空格
            ++spaces;
        ++total;         // 每次都执行
        cin.get(ch);
    }
    cout << spaces << " spaces, " << total;
    cout << " characters total in sentence\n";
    return 0;
}
The balloonist was an airhead with lofty goals. 6 spaces, 46 characters total in sentence

++spaces; 只在 ch 是空格时执行;++total; 在 if 之外,每轮都执行。总数还包括按回车产生的换行符。

if else

if 决定”做不做”,if else 决定”做这个还是做那个”:

if (test-condition)
    statement1
else
    statement2

条件为真执行 statement1 跳过 statement2,为假反之。两个分支各只能放一条语句,多条必须用花括号打包成块。

▲ 图 6.2 if else 语句的结构

程序清单 6.2 对文本做简单加密(每个字符码 +1),换行符保持原样——正好用 if else 分两条路:

// ifelse.cpp -- 使用 if else 语句
#include <iostream>
int main()
{
    char ch;
 
    std::cout << "Type, and I shall repeat.\n";
    std::cin.get(ch);
    while (ch != '.')
    {
        if (ch == '\n')
            std::cout << ch;   // 换行符原样输出
        else
            std::cout << ++ch; // 其他字符码加 1
        std::cin.get(ch);
    }
 
    std::cout << "\nPlease excuse the slight confusion.\n";
    return 0;
}
Type, and I shall repeat.
An ineffable joy suffused me as I beheld
Bo!jofggbcmf!kpz!tvggvtfe!nf!bt!J!cfifme
the wonders of modern computing.
uif!xpoefst!pg!npefso!dpnqvujoh
Please excuse the slight confusion.

(书里留了个思考题:把 ++ch 换成 ch+1 会发生什么?提示:cout 对不同类型的处理方式不同。)

格式提醒与 if else if else

分支里放多条语句必须加花括号,否则 else 会变成”无主之 else”,编译报错:

if (ch == 'Z')
{
    zorro++;
    cout << "Another Zorro candidate\n";
}
else
{
    dull++;
    cout << "Not a Zorro candidate\n";
}

超过两种选择时,把 if else 套进 else 里并排书写,就是清晰的 if else if else 结构——它其实是嵌套,只是排版让人看不出来:

if (ch == 'A')
    a_grade++;
else if (ch == 'B')
    b_grade++;
else
    soso++;

程序清单 6.3 用它做猜数字游戏:

// ifelseif.cpp -- 使用 if else if else
#include <iostream>
const int Fave = 27;
int main()
{
    using namespace std;
    int n;
 
    cout << "Enter a number in the range 1-100 to find ";
    cout << "my favorite number: ";
    do
    {
        cin >> n;
        if (n < Fave)
            cout << "Too low -- guess again: ";
        else if (n > Fave)
            cout << "Too high -- guess again: ";
        else
            cout << Fave << " is right!\n";
    } while (n != Fave);
    return 0;
}
Enter a number in the range 1-100 to find my favorite number: 50
Too high -- guess again: 25
Too low -- guess again: 37
Too high -- guess again: 31
Too high -- guess again: 28
Too high -- guess again: 27
27 is right!

防错小技巧:常量写左边

variable == value 反写成 value == variable,手滑漏成 value = variable 时编译器会立刻报错(不能给字面量 3 赋值);而正着写错成 myNumber = 3 是合法赋值,块照样执行,成了极难查找的暗病。

常见坑:if/else 与花括号

C++ 不会自动把 if 和 else 之间的内容当块,缩进只是给人看的。漏掉花括号轻则 else 找不到 if 报语法错误,重则某条语句悄悄落到分支之外,逻辑全乱。

6.2 逻辑运算符与短路求值

C++ 用三个逻辑运算符组合或反转已有表达式。

逻辑 OR:||

任一(或两个)表达式为真,结果就为真:

5 == 5 || 5 == 9   // 真,第一个为真
5 > 8 || 5 < 2     // 假,两个都为假

| expr1 \|\| expr2 的值 | expr1 为真 | expr1 为假 | |---|---|---| | expr2 为真 | 真 | 真 | | expr2 为假 | 真 | 假 |

|| 优先级低于关系运算符,不用加括号。程序清单 6.4 用一条 || 同时接受大小写:

// or.cpp -- 使用逻辑 OR 运算符
#include <iostream>
int main()
{
    using namespace std;
    cout << "This program may reformat your hard disk\n"
            "and destroy all your data.\n"
            "Do you wish to continue? <y/n>";
    char ch;
    cin >> ch;
    if (ch == 'y' || ch == 'Y')        // y 或 Y
        cout << "You were warned!\a\a\n";
    else if (ch == 'n' || ch == 'N')   // n 或 N
        cout << "A wise choice ... bye\n";
    else
        cout << "That wasn't a y or n! Apparently you "
                "can't follow\ninstructions, so "
                "I'll trash your disk anyway.\a\a\a\n";
    return 0;
}
This program may reformat your hard disk and destroy all your data.
Do you wish to continue? <y/n> N
A wise choice ... bye

程序只读一个字符,用户输入 NO! 时程序只看到 N(但剩下的 O! 还赖在输入队列里)。

逻辑 AND:&&

两个表达式为真结果才为真:

5 == 5 && 4 == 4   // 真,两个都为真
5 > 3 && 5 > 10    // 假,第二个为假
expr1 \&\& expr2 的值expr1 为真expr1 为假
expr2 为真
expr2 为假

程序清单 6.5 用 && 同时检查”数组还有空位”和”输入非负”两个退出条件:

// and.cpp -- 使用逻辑 AND 运算符
#include <iostream>
const int ArSize = 6;
int main()
{
    using namespace std;
    float naaq[ArSize];
    cout << "Enter the NAAQs (New Age Awareness Quotients) "
         << "of\nyour neighbors. Program terminates "
         << "when you make\n" << ArSize << " entries "
         << "or enter a negative value.\n";
 
    int i = 0;
    float temp;
    cout << "First value: ";
    cin >> temp;
    while (i < ArSize && temp >= 0)  // 两个退出条件
    {
        naaq[i] = temp;
        ++i;
        if (i < ArSize)              // 数组还有空位,
        {
            cout << "Next value: ";
            cin >> temp;             // 就读下一个值
        }
    }
    if (i == 0)
        cout << "No data--bye\n";
    else
    {
        cout << "Enter your NAAQ: ";
        float you;
        cin >> you;
        int count = 0;
        for (int j = 0; j < i; j++)
            if (naaq[j] > you)
                ++count;
        cout << count;
        cout << " of your neighbors have greater awareness of\n"
             << "the New Age than you do.\n";
    }
    return 0;
}
Enter the NAAQs (New Age Awareness Quotients) of your neighbors.
Program terminates when you make 6 entries or enter a negative value.
First value: 28
Next value: 72
Next value: 15
Next value: 6
Next value: 130
Next value: 145
Enter your NAAQ: 50
3 of your neighbors have greater awareness of the New Age than you do.

注意程序先把输入读进临时变量 temp,验证合格后才存入数组——这样非法输入(负数)不会混进数组。

&& 的另一个妙用是划分区间,程序清单 6.6 按年龄段发”参赛资格”,用指针数组存一组提示字符串:

// more_and.cpp -- 使用逻辑 AND 运算符
#include <iostream>
const char * qualify[4] =    // 指针数组
{                            // 指向字符串
    "10,000-meter race.\n",
    "mud tug-of-war.\n",
    "masters canoe jousting.\n",
    "pie-throwing festival.\n"
};
int main()
{
    using namespace std;
    int age;
    cout << "Enter your age in years: ";
    cin >> age;
    int index;
 
    if (age > 17 && age < 35)
        index = 0;
    else if (age >= 35 && age < 50)
        index = 1;
    else if (age >= 50 && age < 65)
        index = 2;
    else
        index = 3;
 
    cout << "You qualify for the " << qualify[index];
    return 0;
}
Enter your age in years: 87
You qualify for the pie-throwing festival.

设计区间测试要保证区间之间无缝且不重叠(35 用 >= 接住,否则 35 会被所有区间漏掉)。

常见坑:数学式区间写法

if (17 < age < 35) 是合法 C++ 但语义全错:按左结合它等于 (17 < age) < 35,而 17 < age 的结果是 0 或 1,永远小于 35——整个条件恒为真!区间测试必须写成两个完整关系表达式用 && 相连。

逻辑 NOT:!

! 把后面的真值反转:真变假、假变真。多数时候不用它反而更清楚(!(x > 5) 不如 x <= 5 明白),但它配合返回真/假值的函数很好用:!strcmp(s1, s2) 为真即两串相同。程序清单 6.7 用 while (!is_int(num)) 拒绝超出 int 范围的输入——先把数读进范围更大的 double,用 climits 的 INT_MAX/INT_MIN 检查:

// not.cpp -- 使用 not 运算符
#include <iostream>
#include <climits>
 
bool is_int(double);
int main()
{
    using namespace std;
    double num;
 
    cout << "Yo, dude! Enter an integer value: ";
    cin >> num;
    while (!is_int(num))    // 不在 int 范围内就继续
    {
        cout << "Out of range -- please try again: ";
        cin >> num;
    }
    int val = int (num);    // 类型转换
    cout << "You've entered the integer " << val << "\nBye\n";
    return 0;
}
 
bool is_int(double x)
{
    if (x <= INT_MAX && x >= INT_MIN)    // 使用 climits 的值
        return true;
    else
        return false;
}
Yo, dude! Enter an integer value: 6234128679
Out of range -- please try again: -8000222333
Out of range -- please try again: 99999
You've entered the integer 99999
Bye

直接把大数读进 int 的话,很多实现会默默截断而不吭声;用 double 中转能提前拦住。

优先级与求值顺序

  • 关系运算符 > && > ||(&& 比 || 优先级高):age > 30 && age < 45 || weight > 300 意思是”(31~44 岁)或(体重超 300)“。拿不准就加括号——代码易读,还不怕记错规则。
  • ! 的优先级高于关系运算符:!x > 5(!x) > 5,恒为假;想否定整个表达式必须写 !(x > 5)
  • 短路求值:逻辑表达式从左到右算,一旦能定结果就停。x != 0 && 100.0 / x > 100.0 中若 x 为 0,右边根本不会求值——恰好避免了除零!反之 && 左边为真、|| 左边为假时右边一定会求值。&& 和 || 都是序列点,左边的副作用先完成。

替代表示

某些键盘打不出这些符号,C++ 提供了保留字替身:&& 可写 and、|| 可写 or、! 可写 not(C 里需包含 iso646.h,C++ 不用)。

6.3 cctype 字符函数库

判断”是不是字母”这类问题,手写 (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') 又长又依赖编码方案(它假设字母码值连续,ASCII 成立但并非处处成立)。C 从 C 继承了一包字符函数(头文件 cctype),isalpha(ch) 一句搞定且更通用。程序清单 6.8 逐字符分类统计:

// cctypes.cpp -- 使用 ctype.h 库
#include <iostream>
#include <cctype>    // 字符函数的原型
int main()
{
    using namespace std;
    cout << "Enter text for analysis, and type @"
            " to terminate input.\n";
    char ch;
    int whitespace = 0;
    int digits = 0;
    int chars = 0;
    int punct = 0;
    int others = 0;
 
    cin.get(ch);              // 读第一个字符
    while (ch != '@')         // 测试哨兵
    {
        if (isalpha(ch))      // 是字母吗?
            chars++;
        else if (isspace(ch)) // 是空白字符吗?
            whitespace++;
        else if (isdigit(ch)) // 是数字吗?
            digits++;
        else if (ispunct(ch)) // 是标点吗?
            punct++;
        else
            others++;
        cin.get(ch);          // 读下一个字符
    }
    cout << chars << " letters, "
         << whitespace << " whitespace, "
         << digits << " digits, "
         << punct << " punctuation, "
         << others << " others.\n";
    return 0;
}
Enter text for analysis, and type @ to terminate input.
AdrenalVision International producer Adrienne Vismonger
announced production of their new 3-D film, a remake of
"My Dinner with Andre," scheduled for 2013. "Wait until
you see the the new scene with an enraged Collossipede!"
177 letters, 33 whitespace, 5 digits, 9 punctuation, 0 others.

常用函数一览(这些函数返回 int,但可以当 bool 用):

函数名返回真的条件
isalnum()字母或数字
isalpha()字母
isblank()空格或水平制表符
iscntrl()控制字符
isdigit()十进制数字 0–9
isgraph()除空格外的打印字符
islower()小写字母
isprint()打印字符(含空格)
ispunct()标点符号
isspace()标准空白(空格、换页、换行、回车、水平/垂直制表)
isupper()大写字母
isxdigit()十六进制数字(0–9、a–f、A–F)
tolower()大写则返回对应小写,否则原样返回
toupper()小写则返回对应大写,否则原样返回

6.4 条件运算符

?: 是 C++ 唯一的三元运算符,是 if else 选值的单行版:

expression1 ? expression2 : expression3

expression1 为真整个表达式取 expression2 的值,否则取 expression3 的值。例如 5 > 3 ? 10 : 12 的值是 10。程序清单 6.9 求两数较大值:

// condit.cpp -- 使用条件运算符
#include <iostream>
int main()
{
    using namespace std;
    int a, b;
    cout << "Enter two integers: ";
    cin >> a >> b;
    cout << "The larger of " << a << " and " << b;
    int c = a > b ? a : b; // a>b 则 c=a,否则 c=b
    cout << " is " << c << endl;
    return 0;
}
Enter two integers: 25 28
The larger of 25 and 28 is 28

int c = a > b ? a : b; 等价于一段 if else,但它是表达式,可以直接嵌进更大的表达式里赋值。简洁但可读性差是它的标签,最适合简单场景:x = (x > y) ? x : y;;想炫技把它嵌套三四层?那是制造乱码的捷径,复杂逻辑老老实实用 if else。

6.5 switch 语句

从一长串整数常量中选一个,if else if else 能做但啰嗦,switch 是专门为此设计的路由器:

switch (integer-expression)
{
    case label1 : statement(s)
    case label2 : statement(s)
    ...
    default : statement(s)
}

integer-expression 必须是结果为整数的表达式,每个标签是整数常量表达式(通常是 int 或 char 常量、枚举量)。程序跳到值匹配的 case 标签处开始执行;都不匹配就去 default(可省略,省略时直接执行 switch 后面的语句)。

▲ 图 6.3 switch 语句的结构

关键区别:case 标签只是行标签,不是边界!跳到某行后程序会一路顺序执行下去,直到遇到 break 才跳出 switch。程序清单 6.10 的每个 case 都以 break 收尾:

// switch.cpp -- 使用 switch 语句
#include <iostream>
using namespace std;
void showmenu();    // 函数原型
void report();
void comfort();
int main()
{
    showmenu();
    int choice;
    cin >> choice;
    while (choice != 5)
    {
        switch(choice)
        {
            case 1 :    cout << "\a\n";
                        break;
            case 2 :    report();
                        break;
            case 3 :    cout << "The boss was in all day.\n";
                        break;
            case 4 :    comfort();
                        break;
            default :   cout << "That's not a choice.\n";
        }
        showmenu();
        cin >> choice;
    }
    cout << "Bye!\n";
    return 0;
}
 
void showmenu()
{
    cout << "Please enter 1, 2, 3, 4, or 5:\n"
            "1) alarm    2) report\n"
            "3) alibi    4) comfort\n"
            "5) quit\n";
}
void report()
{
    cout << "It's been an excellent week for business.\n"
            "Sales are up 120%. Expenses are down 35%.\n";
}
void comfort()
{
    cout << "Your employees think you are the finest CEO\n"
            "in the industry. The board of directors think\n"
            "you are the finest CEO in the industry.\n";
}
Please enter 1, 2, 3, 4, or 5:
1) alarm    2) report
3) alibi    4) comfort
5) quit
4
Your employees think you are the finest CEO in the industry.
The board of directors think you are the finest CEO in the industry.
Please enter 1, 2, 3, 4, or 5:
1) alarm    2) report
3) alibi    4) comfort
5) quit
2
It's been an excellent week for business.
Sales are up 120%. Expenses are down 35%.
Please enter 1, 2, 3, 4, or 5:
1) alarm    2) report
3) alibi    4) comfort
5) quit
6
That's not a choice.
Please enter 1, 2, 3, 4, or 5:
1) alarm    2) report
3) alibi    4) comfort
5) quit
5
Bye!

试试删掉所有 break:输入 2 会连着执行 case 2、3、4 和 default 的所有语句。不过”穿透”也有正经用法——多个标签共用一组语句(大小写字母选同一项):

char choice;
cin >> choice;
while (choice != 'Q' && choice != 'q')
{
    switch(choice)
    {
        case 'a':
        case 'A': cout << "\a\n";
                  break;
        case 'r':
        case 'R': report();
                  break;
        case 'l':
        case 'L': cout << "The boss was in all day.\n";
                  break;
        case 'c':
        case 'C': comfort();
                  break;
        default : cout << "That's not a choice.\n";
    }
    showmenu();
    cin >> choice;
}

case ‘a’ 后面没有 break,程序顺势落到 case ‘A’ 的语句——这正是想要的。

用枚举量当标签

程序清单 6.11 用 enum 定义一组常量做 switch 标签(cin 不认识枚举类型,所以仍按 int 读入,比较时枚举量提升为 int):

// enum.cpp -- 使用 enum
#include <iostream>
// 为 0 - 6 创建命名常量
enum {red, orange, yellow, green, blue, violet, indigo};
 
int main()
{
    using namespace std;
    cout << "Enter color code (0-6): ";
    int code;
    cin >> code;
    while (code >= red && code <= indigo)
    {
        switch (code)
        {
            case red    : cout << "Her lips were red.\n"; break;
            case orange : cout << "Her hair was orange.\n"; break;
            case yellow : cout << "Her shoes were yellow.\n"; break;
            case green  : cout << "Her nails were green.\n"; break;
            case blue   : cout << "Her sweatsuit was blue.\n"; break;
            case violet : cout << "Her eyes were violet.\n"; break;
            case indigo : cout << "Her mood was indigo.\n"; break;
        }
        cout << "Enter color code (0-6): ";
        cin >> code;
    }
    cout << "Bye\n";
    return 0;
}
Enter color code (0-6): 3
Her nails were green.
Enter color code (0-6): 5
Her eyes were violet.
Enter color code (0-6): 2
Her shoes were yellow.
Enter color code (0-6): 8
Bye

switch 与 if else 的选择

if else 更万能:能处理范围、浮点测试、两个变量之间的比较。switch 的每个标签只能是单个整数常量,处理不了范围和浮点。但凡是”整数常量选一个”的场景,switch 通常代码更紧凑、执行更快;一般经验:三个以上备选项用 switch

6.6 break、continue 与 goto

  • break:用于 switch 和所有循环,直接跳出整个 switch 或循环,执行其后的语句。
  • continue:只用于循环,跳过本轮循环体剩余部分,开始下一轮。注意:for 循环里 continue 会先跳到更新表达式再测试;while 循环里直接跳到测试表达式——while 体内写在 continue 之后的更新代码会被跳过,可能出问题。

▲ 图 6.4 continue 与 break 语句的结构

程序清单 6.12 一并演示两者:

// jump.cpp -- 使用 continue 和 break
#include <iostream>
const int ArSize = 80;
int main()
{
    using namespace std;
    char line[ArSize];
    int spaces = 0;
 
    cout << "Enter a line of text:\n";
    cin.get(line, ArSize);
    cout << "Complete line:\n" << line << endl;
    cout << "Line through first period:\n";
    for (int i = 0; line[i] != '\0'; i++)
    {
        cout << line[i];          // 显示字符
        if (line[i] == '.')       // 遇句号退出循环
            break;
        if (line[i] != ' ')       // 非空格跳过余下部分
            continue;
        spaces++;
    }
    cout << "\n" << spaces << " spaces\n";
    cout << "Done.\n";
    return 0;
}
Enter a line of text:
Let's do lunch today. You can pay!
Complete line:
Let's do lunch today. You can pay!
Line through first period:
Let's do lunch today.
3 spaces

break 在句号处终止整个循环;continue 让非空格字符跳过 spaces++。这个 continue 其实可以用 if (line[i] == ' ') spaces++; 替代,但当 continue 之后还有多条语句时,用它可避免把一大段都塞进 if,可读性更好。

C++ 也有 gotogoto paris; 跳到 paris: 标签处),但几乎所有场合它都是坏品味,请用 if else、switch、continue 这些结构化工具管住程序流。

通关标准:

能说清 break 与 continue 的区别(跳出整个循环 vs 跳过本轮剩余);能解释 switch 为什么必须配 break 以及”穿透”的正确用法;能列出 switch 做不了而 if else 能做的三种情况(范围、浮点、变量比较)。

6.7 读取数字的循环

cin >> n 遇到用户输入单词而非数字(类型不匹配)时会发生四件事:n 的值不变;错误输入留在输入队列;cin 内部设置错误标记;cin 表达式转换成 bool 后为 false。最后一条正好可以拿来终止”读数字”循环,但错误标记必须先清除才能继续读。

程序清单 6.13 读体重算平均,输够 5 个或遇到非数字输入就收手:

// cinfish.cpp -- 非数字输入终止循环
#include <iostream>
const int Max = 5;
int main()
{
    using namespace std;
    // 获取数据
    double fish[Max];
    cout << "Please enter the weights of your fish.\n";
    cout << "You may enter up to " << Max
         << " fish <q to terminate>.\n";
    cout << "fish #1: ";
    int i = 0;
    while (i < Max && cin >> fish[i])
    {
        if (++i < Max)
            cout << "fish #" << i+1 << ": ";
    }
    // 计算平均
    double total = 0.0;
    for (int j = 0; j < i; j++)
        total += fish[j];
    // 报告结果
    if (i == 0)
        cout << "No fish\n";
    else
        cout << total / i << " = average weight of "
             << i << " fish\n";
    cout << "Done.\n";
    return 0;
}
Please enter the weights of your fish.
You may enter up to 5 fish <q to terminate>.
fish #1: 30
fish #2: 35
fish #3: 25
fish #4: 40
fish #5: q
32.5 = average weight of 4 fish
Done.

测试条件 i < Max && cin >> fish[i] 里有短路求值的功劳:i 已满时左边为假,右边不会执行,从而避免往数组末尾之外读数据。

如果想拒收错误输入并要求重输(程序清单 6.14,必须交齐 5 个高尔夫成绩),要做三步:重置 cin → 清掉坏输入 → 提示重试

// cingolf.cpp -- 跳过非数字输入
#include <iostream>
const int Max = 5;
int main()
{
    using namespace std;
    // 获取数据
    int golf[Max];
    cout << "Please enter your golf scores.\n";
    cout << "You must enter " << Max << " rounds.\n";
    int i;
    for (i = 0; i < Max; i++)
    {
        cout << "round #" << i+1 << ": ";
        while (!(cin >> golf[i])) {
            cin.clear();             // 重置输入
            while (cin.get() != '\n')
                continue;            // 丢掉坏输入
            cout << "Please enter a number: ";
        }
    }
    // 计算平均
    double total = 0.0;
    for (i = 0; i < Max; i++)
        total += golf[i];
    // 报告结果
    cout << total / Max << " = average score "
         << Max << " rounds\n";
    return 0;
}
Please enter your golf scores.
You must enter 5 rounds.
round #1: 88
round #2: 87
round #3: must i?
Please enter a number: 103
round #4: 94
round #5: 86
91.6 = average score 5 rounds

核心在 while (!(cin >> golf[i])) 内部:cin 输入失败时先 cin.clear() 重置标记(不清就再也读不进任何东西),再用 while (cin.get() != '\n') 逐字符读掉本行剩余内容(连坏带好一锅端),最后提示重输。顺序不能乱:必须先 clear 再丢弃

常见坑:忘了 cin.clear() 或顺序颠倒

类型不匹配后 cin 处于错误状态,直接重读是徒劳的;若先去清输入队列,get() 也会因错误标记而立刻失败。口诀:先 clear(解锁),再 get 循环(倒垃圾)。

6.8 简单文件输入输出

控制台 I/O 的本领可以几乎原样平移到文本文件上——类是同构的:ofstream 之于 cout,ifstream 之于 cin。

文本 I/O 的本质

所有输入最初都是文本(字符编码序列),cin 负责把它翻译成目标类型。同一行输入 38.5 19.2,不同的读取方式看到不同的东西:

  • cin >> ch(char):读到字符 ‘3’,存的是字符码不是数值 3;
  • cin >> n(int):读到 “38”,在句号前停下,翻译成整数 38;
  • cin >> x(double):读到 “38.5”,翻译成浮点数 38.5;
  • cin >> word(char 数组):读到 “38.5” 的字符码并存进数组、加 \0,不翻译;
  • cin.getline(word, 50):读整行直到换行符(丢弃换行符),加 \0,不翻译。

输出时做反向翻译。所以与控制台输入对应的文件形态就是文本文件——每个字节存一个字符码。数据库、电子表格存的是二进制格式的数字,不在本章讨论范围。

写文本文件

四步:包含 fstream → 创建 ofstream 对象 → 用 open() 关联文件 → 像 cout 一样用它。程序清单 6.15:

// outfile.cpp -- 写入文件
#include <iostream>
#include <fstream>    // 文件 I/O
 
int main()
{
    using namespace std;
 
    char automobile[50];
    int year;
    double a_price;
    double d_price;
 
    ofstream outFile;             // 创建输出对象
    outFile.open("carinfo.txt");  // 与文件关联
 
    cout << "Enter the make and model of automobile: ";
    cin.getline(automobile, 50);
    cout << "Enter the model year: ";
    cin >> year;
    cout << "Enter the original asking price: ";
    cin >> a_price;
    d_price = 0.913 * a_price;
 
    // 用 cout 在屏幕上显示信息
 
    cout << fixed;
    cout.precision(2);
    cout.setf(ios_base::showpoint);
    cout << "Make and model: " << automobile << endl;
    cout << "Year: " << year << endl;
    cout << "Was asking $" << a_price << endl;
    cout << "Now asking $" << d_price << endl;
 
    // 用 outFile 做一模一样的事
 
    outFile << fixed;
    outFile.precision(2);
    outFile.setf(ios_base::showpoint);
    outFile << "Make and model: " << automobile << endl;
    outFile << "Year: " << year << endl;
    outFile << "Was asking $" << a_price << endl;
    outFile << "Now asking $" << d_price << endl;
 
    outFile.close(); // 文件用完了
    return 0;
}
Enter the make and model of automobile: Flitz Perky
Enter the model year: 2009
Enter the original asking price: 13500
Make and model: Flitz Perky
Year: 2009
Was asking $13500.00
Now asking $12325.50

程序结束后,可执行文件所在目录会多出一个 carinfo.txt,内容与屏幕输出完全相同。要点:open() 的参数必须是 C 风格字符串;close() 不需要文件名(对象已关联);格式化方法(setf()、precision())各自独立作用于调用的对象——cout 设 2 位精度不影响 outFile 设 4 位。

常见坑:open() 会清空已有文件

对已存在的文件调用 open() 写入,默认把它截断为零长度——原内容全部丢失!想追加或另作处理得用第 17 章的技巧,眼下至少记住:别对重要文件随手 open。

读文本文件

对称四步:包含 fstream → 创建 ifstream 对象 → open() 关联 → 像 cin 一样用。打开失败(文件不存在、路径不对、权限不足、名字打错)会让后续读取全部失败,所以必须检查。首选 is_open():

inFile.open("bowling.txt");
if (!inFile.is_open())
{
    exit(EXIT_FAILURE); // 原型在 cstdlib,EXIT_FAILURE 也定义于彼
}

程序清单 6.16 读用户指定文件里的数字,报告个数、总和与平均:

// sumafile.cpp -- 带数组参数的函数
#include <iostream>
#include <fstream>    // 文件 I/O 支持
#include <cstdlib>    // 支持 exit()
const int SIZE = 60;
int main()
{
    using namespace std;
    char filename[SIZE];
    ifstream inFile;    // 处理文件输入的对象
 
    cout << "Enter name of data file: ";
    cin.getline(filename, SIZE);
    inFile.open(filename);      // inFile 与文件关联
    if (!inFile.is_open())      // 打开文件失败
    {
        cout << "Could not open the file " << filename << endl;
        cout << "Program terminating.\n";
        exit(EXIT_FAILURE);
    }
    double value;
    double sum = 0.0;
    int count = 0;              // 已读取的条目数
 
    inFile >> value;            // 取第一个值
    while (inFile.good())       // 输入良好且未到文件尾
    {
        ++count;                // 又读到一个
        sum += value;           // 累计总和
        inFile >> value;        // 取下一个值
    }
    if (inFile.eof())
        cout << "End of file reached.\n";
    else if (inFile.fail())
        cout << "Input terminated by data mismatch.\n";
    else
        cout << "Input terminated for unknown reason.\n";
    if (count == 0)
        cout << "No data processed.\n";
    else
    {
        cout << "Items read: " << count << endl;
        cout << "Sum: " << sum << endl;
        cout << "Average: " << sum / count << endl;
    }
    inFile.close();             // 文件用完了
    return 0;
}

对文件 scores.txt(内容 18 19 18.5 13.5 14 / 16 19.5 20 18 12 18.5 / 17.5)的运行结果:

Enter name of data file: scores.txt
End of file reached.
Items read: 12
Sum: 204.5
Average: 17.0417

读文件循环的设计要点:good() 报告的是最近一次读取尝试,所以循环前先读一次、循环体末尾再读一次;循环结束后可用 eof()(是否到文件尾)和 fail()(文件尾类型不匹配)区分结束原因——先测 eof 再测 fail,剩下才是未知错误。利用”inFile >> value 本身就是 inFile 且可转 bool”的特性,两处读取可浓缩为一个条件:

while (inFile >> value)  // 边读边测
{
    // 循环体
}

通关标准:

能默写 ofstream/ifstream 的四步用法;写读文件循环时记得”循环前读一次、循环尾再读一次”或直接用 while (inFile >> value);打开文件后先查 is_open() 再动手读。