C++基础(五)——屏幕和文件输入输出

家人们好呀!!!

前几篇文章里,我们让计算机记住了数据(变量),学会了算算术(运算符),还能根据条件做选择、反复干一件事(流程控制)。但不知道你有没有发现一个问题------我们写的程序都像在演独角戏:要么自己跟自己玩,要么输出一些写死的数字,从来不跟坐在键盘前的你互动。

一个真正有用的程序,得能和用户"交流":用户告诉它要算什么,它算完了再把结果告诉用户。比如登录系统需要你输入账号密码,计算器需要你输入算式,游戏需要你按下WASD来控制角色移动。

这种"程序与外界的对话",在编程术语里叫输入输出(I/O, Input/Output) 。输入就是程序从外界接收数据(比如你敲键盘),输出就是程序把结果展示给外界(比如显示在屏幕上)。

C++为这种"对话"提供了两套工具:一套是C++原生的流(Stream) 体系,一套是继承自C语言的printf/scanf函数族。本篇文章,我们就来系统地学习这两套工具,让你写的程序真正"活"起来------能听、能说、能跟你聊上天。

一、流的概念

在深入具体代码之前,我们先来理解一个核心概念------流(Stream)。

你可以把流想象成一条数据传送带。想象一个工厂的生产线:原材料从传送带的一端送进来(输入),经过加工后,成品从传送带的另一端送出去(输出)。

C++的"流"就是这样的抽象模型:

· 输入流:数据从外部设备(键盘、文件、网络)通过传送带流进你的程序。

· 输出流:数据从你的程序通过传送带流到外部设备(屏幕、文件、网络)。

这种抽象的好处是:无论数据来自键盘还是文件,操作方式几乎一模一样。你不需要关心底层的硬件差异,只需要学会"往传送带上放东西"和"从传送带上取东西"这两个动作就行了。

C++中负责I/O操作的所有类,都定义在三个核心头文件里:iostream提供控制台输入输出,iomanip提供格式化控制,fstream提供文件操作。这三个头文件组成了C++流操作的基础。

二、四大天王:cin、cout、cerr、clog

头文件里预定义了四个全局流对象,它们是C++程序与外界交流的"四大天王":

对象名 含义 对应设备 典型用途
cin 标准输入 键盘 接收用户输入
cout 标准输出 屏幕(带缓冲) 普通输出信息
cerr 标准错误输出 屏幕(无缓冲) 输出错误信息,立即显示
clog 标准日志输出 屏幕(带缓冲) 输出日志信息

这四个对象你都可以直接拿来用,不需要自己创建。

2.1 cout:屏幕上的"广播员"

cout(发音类似"c-out")是我们打交道最多的对象。它的作用是把数据输出到屏幕上。使用的运算符是<<------叫插入运算符,意思是把右边的数据"塞"进左边的输出流里。

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, 我又回来了!" << endl;
    cout << "我的年龄是:" << 18 << endl;
    cout << "π约等于:" << 3.14159 << endl;
    return 0;
}

注意那个endl------它的作用是输出一个换行符,并且刷新缓冲区(立刻把内容显示到屏幕上)。

2.2 cin:键盘前的"收音员"

cin(发音类似"c-in")负责从键盘接收输入。它使用的运算符是>>------叫提取运算符,意思是把数据从输入流中"抽"出来,存进变量里。

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int age;
    cout << "请输入你的年龄:";
    cin >> age;
    cout << "哇,你已经" << age << "岁了!" << endl;
    return 0;
}

cin >> age这行代码会暂停程序,等你从键盘输入一个数字并按下回车,然后把那个数字存进age变量里。

连续输入:cin可以连续读取多个数据,用空格或回车隔开:

cpp 复制代码
int a, b;
cout << "请输入两个整数:";
cin >> a >> b;
cout << "它们的和是:" << a + b << endl;

cin的几个常见特性:

  1. 自动跳过空白字符:cin >>在读取之前会自动跳过前面的空格、换行符和制表符。
  2. 遇到空白就停:读取字符串时,遇到空格就停止,所以cin >> name只能读一个单词,不能读带空格的句子。
  3. 类型要匹配:如果你定义了一个int变量,却输入了字母,cin会进入失败状态,后续的输入操作都会失效。

2.3 cerr和clog:报错专用通道

cerr和clog都是用来输出错误和日志信息的,用法和cout完全一样:

cpp 复制代码
cerr << "错误:文件打开失败!" << endl;
clog << "日志:程序已启动" << endl;

它们的区别在于缓冲策略:cerr无缓冲(错误信息立刻显示,不会因为程序崩溃而丢失),clog有缓冲(性能更好,适合大量日志)。对于新手来说,知道它们存在就行了,平时用cout完全够用。

三、cout格式化输出

基础版cout虽然能干活,但输出的样子往往让人不太满意------比如输出3.14159,你可能只想保留2位小数(变成3.14);或者想把数字对齐成一列整齐的表格。

这时,你需要请出第二个头文件------(Input/Output Manipulation,输入输出操纵器)。它提供了一堆"格式操纵符",可以直接"插"进输出流里,临时改变输出格式。

重要提醒:所有格式操纵符(如setw、setprecision、fixed等)都在std命名空间中,需要包含iomanip才能使用。这是新手最容易遗漏的头文件!

3.1 控制小数精度:fixed + setprecision

这是最常用的格式化功能。想保留2位小数输出金额?用fixed配合setprecision(n):

cpp 复制代码
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double pi = 3.1415926;
    cout << "默认输出:" << pi << endl;                      // 3.14159
    cout << fixed << setprecision(2) << pi << endl;          // 3.14
    cout << fixed << setprecision(4) << pi << endl;          // 3.1416(四舍五入)
    return 0;
}

关键区别:

· 不加fixed:setprecision(n)表示总共保留n位有效数字。

· 加了fixed:setprecision(n)表示保留小数点后n位。

注意:fixed和setprecision一旦设置,会对后续所有浮点数输出持续生效,直到你再次修改。

3.2 控制输出宽度和对齐:setw + left/right + setfill

setw(n)可以给输出的内容预留n个字符的宽度,如果内容不够宽,就用空格补齐。它只对紧跟在后面的那一个输出项生效(一次性),这是很多新手容易搞混的点。

配合left(左对齐)、right(右对齐,默认)和setfill(自定义填充字符),可以轻松制作整齐的表格:

cpp 复制代码
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    cout << left << setw(10) << "姓名" 
         << setw(6) << "年龄" 
         << setw(10) << "成绩" << endl;
    cout << setw(10) << "张三" 
         << setw(6) << 18 
         << setw(10) << fixed << setprecision(1) << 98.5 << endl;
    cout << setw(10) << "李四" 
         << setw(6) << 19 
         << setw(10) << 87.0 << endl;
    cout << setw(10) << "王五" 
         << setw(6) << 17 
         << setw(10) << 92.3 << endl;
    return 0;
}

输出效果:

复制代码
姓名        年龄  成绩      
张三        18    98.5      
李四        19    87.0      
王五        17    92.3      

如果觉得空格填充不够"醒目",可以用setfill把填充字符换成别的:

cpp 复制代码
cout << setfill('-') << setw(20) << "" << endl;  // 输出20个横线
cout << setw(10) << "Hello" << endl;              // 填充字符已经变成'-'了

3.3 其他实用操纵符一览

操纵符 作用 示例
hex 输出十六进制 cout << hex << 255; → ff
oct 输出八进制 cout << oct << 8; → 10
dec 恢复十进制(默认) cout << dec << 10; → 10
scientific 科学计数法 3.14159e+00
boolalpha 布尔值显示为true/false cout << boolalpha << true; → true
showpos 正数显示+号 cout << showpos << 42; → +42
setfill© 设置填充字符 cout << setfill('*') << setw(5) << 3; → ****3

3.4 流成员函数方式(另一种选择)

除了用操纵符,你还可以用cout的成员函数来控制格式,效果是一样的:

cpp 复制代码
cout.precision(2);      // 相当于 setprecision(2)
cout.width(10);         // 相当于 setw(10)
cout.fill('*');         // 相当于 setfill('*')
cout.setf(ios::fixed);  // 相当于 fixed

操纵符写法更直观,推荐新手用操纵符;成员函数写法在一些老代码中比较常见。

四、字符串输入的正确姿势:为什么我的getline"罢工"了?

用cin >>读字符串有一个致命缺陷------遇到空格就停。如果你想读入一句完整的话(比如用户输入的名字"李 小 龙"),cin >> name只会读进去"李"。

这时候,你需要getline上场。

4.1 std::getline:整行读取的专家

getline定义在头文件中,它可以从输入流中读取一整行(包括空格),直到遇到换行符为止。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string sentence;
    cout << "请输入一句话:";
    getline(cin, sentence);
    cout << "你说的是:" << sentence << endl;
    return 0;
}

4.2 经典Bug:getline"跳过去了"

无数C++新手都曾被这个问题折磨过------先用了cin >>读数字,紧接着用getline读字符串,结果getline好像直接"跳过"了,什么都没读进去。

场景复现:

cpp 复制代码
int age;
string name;

cout << "请输入年龄:";
cin >> age;

cout << "请输入姓名:";
getline(cin, name);   // 这行会被跳过!

cout << "姓名:" << name << ",年龄:" << age << endl;

运行这个程序,你会发现------输完年龄按下回车后,程序直接跳过了姓名输入,name变成了空字符串。

原因:cin >> age读取数字后,你在键盘上按下的那个回车键产生的换行符(\n)还残留在输入缓冲区里。而getline遇到换行符就会立刻返回(读到空行),所以它根本不等你输入,直接把那个残留的\n当成了一整行。

解决方案:在cin >>和getline之间加一行cin.ignore(),把残留的换行符吃掉。

cpp 复制代码
int age;
string name;

cout << "请输入年龄:";
cin >> age;
cin.ignore(numeric_limits<streamsize>::max(), '\n');  // 清空缓冲区!

cout << "请输入姓名:";
getline(cin, name);

cin.ignore(n, c)的意思是:忽略输入流中的n个字符,或者直到遇到字符c(包括c本身一起丢弃)。

推荐写法:cin.ignore(numeric_limits::max(), '\n');------把缓冲区里直到换行符的所有内容全部清掉,不留后患。这个写法在面试和实际开发中都是标准操作。

4.3 string版 vs char数组版

C++提供了两种getline:

版本 头文件 特点 推荐度
std::getline(cin, str) string 自动扩容,安全方便 ⭐⭐⭐⭐⭐ 首选
cin.getline(buf, size) iostream 固定长度,需要C接口时用 ⭐⭐ 备选

新手直接用std::getline(cin, string变量)就行,不用操心数组越界的问题。

五、C风格输入输出:printf与scanf的怀旧之旅

在C++诞生之前,C语言用printf和scanf来搞定输入输出。C++为了兼容C的代码,也保留了这两个函数(定义在头文件中)。虽然C++流更现代、更安全,但printf/scanf在某些场景下依然有独特的优势------比如格式化输出非常简洁,而且速度通常比cin/cout更快。

5.1 printf:格式化打印

printf(print formatted)的核心思想是格式化字符串+占位符。你写一个包含%占位符的字符串,然后把对应的变量按顺序塞进去:

cpp 复制代码
#include <cstdio>

int main() {
    int age = 18;
    double pi = 3.14159;
    char grade = 'A';
    printf("我今年%d岁,π约等于%.2f,成绩是%c\n", age, pi, grade);
    return 0;
}
// 输出:我今年18岁,π约等于3.14,成绩是A

常用占位符速查表:

占位符 对应类型 示例
%d int(整数) printf("%d", 42);
%f float/double(浮点数) printf("%f", 3.14);
%lf double(明确指定) printf("%lf", 3.14);
%c char(字符) printf("%c", 'A');
%s char*(C风格字符串) printf("%s", "Hello");
%p 指针地址 printf("%p", &age);
%x 十六进制整数 printf("%x", 255); → ff
%% 百分号本身 printf("100%%"); → 100%

printf的格式修饰符:

你可以在%和字母之间插入修饰符来控制宽度、精度和对齐:

修饰符 含义 示例
%5d 最小宽度5,右对齐 printf("%5d", 42); → 42
%-5d 最小宽度5,左对齐 printf("%-5d", 42); → 42
%.2f 保留2位小数 printf("%.2f", 3.14159); → 3.14
%+d 正数也显示+号 printf("%+d", 42); → +42
%05d 宽度5,用0填充 printf("%05d", 42); → 00042

注意:printf的占位符必须和参数类型严格匹配,否则会出现未定义行为。比如用%d输出double类型的数据,结果会是一堆乱码。

5.2 scanf:格式化输入

scanf的用法和printf类似,也是占位符+变量列表。但有一个关键区别------变量名前必须加&取地址符!

cpp 复制代码
#include <cstdio>

int main() {
    int age;
    double height;
    char name[50];
    
    printf("请输入年龄和身高(用空格隔开):");
    scanf("%d %lf", &age, &height);
    
    printf("请输入姓名:");
    scanf("%s", name);  // name是数组名,本身就是地址,所以不加&
    
    printf("姓名:%s,年龄:%d,身高:%.2f\n", name, age, height);
    return 0;
}

scanf的注意事项:

  1. 必须加&(除了数组名和指针):忘记加&是新手最高频的错误,程序会崩溃或产生奇怪的结果。
  2. 遇到空白字符会停止:scanf("%s", name)只能读一个单词,和cin >>一样的毛病。
  3. 格式化字符串不要加多余字符:scanf("年龄:%d", &age)要求用户精确输入"年龄:18",差一个空格都会导致读取失败。

5.3 printf/scanf vs cin/cout:如何选择?

对比维度 printf/scanf cin/cout
类型安全 ❌ 靠占位符,不匹配会出错 ✅ 编译期检查,自动处理类型
格式化便捷性 ✅ 一个字符串搞定,非常紧凑 ❌ 需要多个操纵符
运行速度 ✅ 通常更快 ❌ 默认较慢(可优化)
扩展性 ❌ 只能处理基本类型 ✅ 可以重载<<和>>支持自定义类型
学习曲线 中等(需要记住占位符) 平缓(符合直觉)

建议:

· 新手入门:优先用cin/cout,类型安全,不容易出错。

· 需要复杂格式化:printf写起来更简洁,比如printf("%-10s %6.2f\n", name, score);。

· 性能敏感场景(如算法竞赛):关闭流同步后,cin/cout速度可以和printf/scanf相当甚至更快。

六、文件输入输出:把数据存起来,下次接着用

到目前为止,我们程序的输入都来自键盘,输出都跑到屏幕上。但真正的程序需要能读写文件------比如游戏存档、文本编辑器、日志系统。

C++中操作文件,需要用到头文件。它提供了三个核心类:

类名 用途 对应模式
ofstream 输出文件流(写文件) ios::out
ifstream 输入文件流(读文件) ios::in
fstream 输入输出文件流(读写) ios::in | ios::out

6.1 写文件:把"心里话"记到本子上

写文件的基本步骤:创建ofstream对象 → 打开文件 → 写入内容 → 关闭文件。

cpp 复制代码
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // 创建输出文件流对象,同时打开文件
    ofstream outFile("my_diary.txt");
    
    // 检查文件是否成功打开
    if (!outFile) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    // 像用cout一样往文件里写东西
    outFile << "2026年4月23日 晴" << endl;
    outFile << "今天学会了C++文件操作,开心!" << endl;
    outFile << "期待明天学习更多知识。" << endl;
    
    // 关闭文件(可选,析构函数会自动关闭)
    outFile.close();
    
    cout << "日记已保存到 my_diary.txt" << endl;
    return 0;
}

操作文件和使用cout几乎一模一样------都是用<<把数据塞进去。文件打开模式可以选择ios::app(追加模式,在文件末尾接着写而不是覆盖)。

6.2 读文件:把"历史记录"翻出来看

读文件的基本步骤:创建ifstream对象 → 打开文件 → 读取内容 → 关闭文件。

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    ifstream inFile("my_diary.txt");
    
    if (!inFile) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    string line;
    cout << "=== 我的日记 ===" << endl;
    while (getline(inFile, line)) {  // 一行一行读
        cout << line << endl;
    }
    
    inFile.close();
    return 0;
}

while (getline(inFile, line))是读文件的标准写法,getline在读到文件末尾时会返回false,循环自动结束。这种写法比while (!inFile.eof())更可靠。

6.3 文件打开模式一览

ofstream和ifstream默认分别以写模式(覆盖原内容)和读模式打开文件。你可以通过第二个参数指定更精细的模式:

模式标志 含义
ios::in 打开文件用于读取
ios::out 打开文件用于写入(默认覆盖)
ios::app 追加模式,写入内容追加到文件末尾
ios::ate 打开后定位到文件末尾
ios::trunc 如果文件存在,清空内容(默认行为)
ios::binary 二进制模式(默认是文本模式)

多个模式可以用|组合,例如:

cpp 复制代码
ofstream outFile("log.txt", ios::out | ios::app);  // 以追加方式打开
fstream file("data.bin", ios::in | ios::out | ios::binary);  // 以二进制读写方式打开

七、流状态与错误处理:当"对话"出错了怎么办?

人跟人聊天有时候也会听岔了、说错了,程序跟外界交流也一样------比如用户该输入数字却敲了字母,或者文件突然被删除了。C++的流对象有一套状态标志机制来记录这些意外情况。

7.1 四个状态标志

每个流对象内部都有四个状态标志位:

标志 含义 触发条件
goodbit 一切正常 无错误
eofbit 到达文件末尾 试图读取但文件已经读完了
failbit 操作失败 格式错误(比如要读数字却遇到字母),流仍然可用
badbit 严重错误 流缓冲区损坏等不可恢复的错误

这些标志可以通过成员函数来检查:

函数 返回true的条件
good() 没有任何错误标志(即只有goodbit)
eof() eofbit被设置
fail() failbit或badbit被设置
bad() badbit被设置

7.2 实战:处理用户输入错误

cpp 复制代码
#include <iostream>
#include <limits>
using namespace std;

int main() {
    int number;
    
    while (true) {
        cout << "请输入一个整数:";
        cin >> number;
        
        if (cin.good()) {
            // 输入成功
            cout << "你输入的是:" << number << endl;
            break;
        } else if (cin.fail()) {
            // 输入失败(比如输入了字母)
            cout << "输入无效,请输入一个整数!" << endl;
            cin.clear();   // 清除错误状态
            cin.ignore(numeric_limits<streamsize>::max(), '\n');  // 清空缓冲区
        } else if (cin.bad()) {
            // 严重错误,直接退出
            cerr << "发生了不可恢复的错误!" << endl;
            return 1;
        }
    }
    
    return 0;
}

这段代码展示了一个健壮的输入处理流程:

  1. cin.good():检查是否一切正常。
  2. cin.clear():如果输入失败,必须先用clear()清除错误状态,否则后续的输入操作会全部失效。
  3. cin.ignore():把缓冲区里的"垃圾数据"清掉,免得下次输入又读到同样的错误内容。

八、现代C++输入输出新特性(C++17到C++23)

C++的I/O系统也在不断进化,以下是几个值得了解的新特性:

C++17:文件系统库。提供了跨平台的文件和目录操作,比如检查文件是否存在、遍历文件夹等,比传统的函数更现代、更安全。

C++20:std::format。这是一个堪比Python f-string的格式化神器,兼具printf的简洁和cout的类型安全:

cpp 复制代码
#include <format>
#include <iostream>

int main() {
    std::string name = "张三";
    int age = 18;
    std::cout << std::format("姓名:{},年龄:{}", name, age);
    // 输出:姓名:张三,年龄:18
}

C++23:std::print。C++23引入了std::print和std::println,用法和printf类似但更安全,而且不需要占位符,直接和std::format风格统一。

九、最佳实践

  1. 优先用cin/cout,类型安全,不易出错。
  2. 读整行用getline(cin, str),不用操心缓冲区长度。
  3. cin >>和getline混用时,中间加cin.ignore()。
  4. 文件操作后检查是否成功,养成良好的错误处理习惯。
  5. 需要精细格式时,记得#include 。
  6. 算法竞赛中,加ios::sync_with_stdio(false)可以让cin/cout飞起来。

十、动手实践

打开你的Visual Studio,把下面的代码复制进去,运行看看会发生什么:

cpp 复制代码
#include <iostream>
#include <iomanip>
#include <string>
#include <fstream>
using namespace std;

int main() {
    // 第一部分:基础输入输出
    cout << "=== 基础输入输出 ===" << endl;
    string name;
    int age;
    cout << "请输入你的名字:";
    getline(cin, name);
    cout << "请输入你的年龄:";
    cin >> age;
    cout << name << ",你好!你今年" << age << "岁。" << endl;
    
    // 第二部分:格式化输出
    cout << "\n=== 格式化输出 ===" << endl;
    double pi = 3.1415926;
    cout << "默认:" << pi << endl;
    cout << "保留2位:" << fixed << setprecision(2) << pi << endl;
    cout << "保留4位:" << setprecision(4) << pi << endl;
    
    // 第三部分:表格对齐
    cout << "\n=== 表格对齐 ===" << endl;
    cout << left << setw(10) << "姓名" 
         << setw(6) << "年龄" 
         << setw(10) << "成绩" << endl;
    cout << setw(10) << name << setw(6) << age 
         << setw(10) << "A+" << endl;
    
    // 第四部分:文件写入
    cout << "\n=== 文件操作 ===" << endl;
    ofstream outFile("test.txt");
    if (outFile) {
        outFile << "这是" << name << "的测试文件" << endl;
        outFile << "年龄:" << age << endl;
        outFile.close();
        cout << "文件已写入 test.txt" << endl;
    }
    
    system("pause");
    return 0;
}

十一、总结

恭喜你!现在你已经掌握了让C++与外界"对话"的核心技能。

快速回顾:

· 流的概念:数据传送带,统一的I/O抽象

· 标准流对象:cin(输入)、cout(输出)、cerr/clog(错误/日志)

· 格式化输出:fixed + setprecision控制小数,setw + left/right控制对齐

· 字符串输入:getline读整行,注意cin.ignore()清除缓冲区

· C风格I/O:printf/scanf,占位符要记牢,&不能忘

· 文件操作:ofstream写,ifstream读,记得检查是否成功打开

· 错误处理:clear() + ignore()是黄金搭档

思考题(看看你学会了没):

  1. 为什么cin >> age之后直接用getline(cin, name)会跳过姓名输入?怎么解决?
  2. setprecision(2)加fixed和不加fixed有什么区别?
  3. printf的%d和%f分别对应什么类型?
  4. 如何用ofstream往文件末尾追加内容而不是覆盖原内容?

下一篇文章,我们将学习C++的数组与字符串------如何批量存储和处理数据。到时候你会发现,原来处理全班50个人的成绩,不需要定义50个变量!

谢谢大家!!!

------ 一个曾经被getline"跳过"bug折磨到凌晨两点的C++学习者

相关推荐
ytttr8731 小时前
C++ LZW 文件压缩算法实现
开发语言·c++
1candobetter1 小时前
JAVA后端开发——多模块项目重命名体系解析
java·开发语言·intellij-idea
citi1 小时前
OpenViking 本地搭建指南
开发语言·python·ai
AI玫瑰助手2 小时前
Python基础:列表的切片与嵌套列表使用技巧
android·开发语言·python
Rnan-prince2 小时前
Count-Min Sketch:海量数据频率统计的“轻量级计数器“
python·算法
Bat U2 小时前
JavaEE|多线程(四)
java·开发语言
白日梦想家6812 小时前
实战避坑+性能对比,for与each循环选型指南
开发语言·前端·javascript
sycmancia2 小时前
Qt——文本编辑器中的数据存取
开发语言·qt
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【排序贪心】:加工生产调度
c++·算法·贪心·csp·信奥赛·排序贪心·加工生产调度