家人们好呀!!!
前几篇文章里,我们让计算机记住了数据(变量),学会了算算术(运算符),还能根据条件做选择、反复干一件事(流程控制)。但不知道你有没有发现一个问题------我们写的程序都像在演独角戏:要么自己跟自己玩,要么输出一些写死的数字,从来不跟坐在键盘前的你互动。
一个真正有用的程序,得能和用户"交流":用户告诉它要算什么,它算完了再把结果告诉用户。比如登录系统需要你输入账号密码,计算器需要你输入算式,游戏需要你按下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的几个常见特性:
- 自动跳过空白字符:cin >>在读取之前会自动跳过前面的空格、换行符和制表符。
- 遇到空白就停:读取字符串时,遇到空格就停止,所以cin >> name只能读一个单词,不能读带空格的句子。
- 类型要匹配:如果你定义了一个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的注意事项:
- 必须加&(除了数组名和指针):忘记加&是新手最高频的错误,程序会崩溃或产生奇怪的结果。
- 遇到空白字符会停止:scanf("%s", name)只能读一个单词,和cin >>一样的毛病。
- 格式化字符串不要加多余字符: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;
}
这段代码展示了一个健壮的输入处理流程:
- cin.good():检查是否一切正常。
- cin.clear():如果输入失败,必须先用clear()清除错误状态,否则后续的输入操作会全部失效。
- 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风格统一。
九、最佳实践
- 优先用cin/cout,类型安全,不易出错。
- 读整行用getline(cin, str),不用操心缓冲区长度。
- cin >>和getline混用时,中间加cin.ignore()。
- 文件操作后检查是否成功,养成良好的错误处理习惯。
- 需要精细格式时,记得#include 。
- 算法竞赛中,加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()是黄金搭档
思考题(看看你学会了没):
- 为什么cin >> age之后直接用getline(cin, name)会跳过姓名输入?怎么解决?
- setprecision(2)加fixed和不加fixed有什么区别?
- printf的%d和%f分别对应什么类型?
- 如何用ofstream往文件末尾追加内容而不是覆盖原内容?
下一篇文章,我们将学习C++的数组与字符串------如何批量存储和处理数据。到时候你会发现,原来处理全班50个人的成绩,不需要定义50个变量!
谢谢大家!!!
------ 一个曾经被getline"跳过"bug折磨到凌晨两点的C++学习者