本章将讲述基本的输入、输出方法。介绍
printf、scanf、std::cin、std::cout的基础用法,并让读者领略 C++23 的std::print的优雅实现。
4.1 爽文女主不是哑巴 #
“……她,曾是上古世家的神女,却因遭人嫉妒而被夺去仙骨,废去修为,沦为宗门最下等的杂役。十年屈辱,受尽白眼,她咬紧牙关,默默积蓄力量……”
“大师兄,她……她好像要说话了!”
“哼,一个哑巴废人,还能掀起什么风浪?”
只见女主缓缓走上试炼台,朱唇轻启。众人本以为会是她十年来的第一句控诉,却只听她清晰地说道:std::cout << "三十年河东,三十年河西,莫欺少女穷!" << std::endl;全场愕然。
一个程序,不能只会自己闷头算(int a = 1 + 2;),还得能开口说话(输出),能用户指挥(输入),才能完成与世界的交互,成为真正的“爽文主角”!
本章,我们就来给我们的程序装上“嘴巴”和“耳朵”。
4.2 C 语言的“嘴巴”与“耳朵”:printf 和 scanf
#
在 C++的“老祖宗”C 语言那里,有一套非常经典的输入输出函数,它们至今依然十分强大。
4.2.1 printf - 格式化输出(开口说话)
#
printf ("print formatted") 就像是让程序说出一句 格式工整 的话。
#include <stdio.h>
int main() {
int year = 10;
printf("莫欺少年穷!莫欺中年穷!莫欺老年穷!死者为大!\n");
printf("再给我%d年,我定能重回巅峰!\n", year);
double power_level = 114514.1919810;
// .2的意思是保留小数点后2位
printf("我的战斗力是%.2f!\n", power_level);
return 0;
}
输出:
莫欺少年穷!莫欺中年穷!莫欺老年穷!死者为大!
再给我10年,我定能重回巅峰!
我的战斗力是114514.19!
核心:格式说明符 (Format Specifiers)
就像说话要注意语法,printf 用特殊的占位符来指定后面变量的输出格式。格式说明符以 % 开始。下面是一些常见的说明符:
-
%d:输出一个整数 ( decimal integer)。 -
%f:输出一个浮点数 ( floating-point)。%.2f表示保留两位小数。 -
%c:输出一个字符 ( character)。 -
%s:输出一个字符串 ( string)。提示:要输出
%本身的时候,需要打 2 个%,也就是printf("%%");。更复杂的格式控制将在我们使用到的时候再阐述。
碎碎念:
\n“换行符”,属于所谓的“转义字符”,相当于你打完字后按了一下回车。在后面的std::cout中,它比std::endl更常用,因为std::endl不仅换行还会立刻刷新输出缓冲区,有时会影响性能。(看不懂没关系,未来会详细展开)转义字符是 70 年代设计的,当时的计算机没有屏幕,是使用电传打字机输出的,所以转义字符的设计里面你可以看见很多这样的残余。
以下是其他转义字符(按需查询,无需记忆):
字符 名称 含义 '\0'空字符 标记 C 字符串的结尾 '\a'警告符(Alert) 触发系统的蜂鸣声(或者是其他提示音) '\b'退格符(Backspace) 将光标移回前一位置,但是一般不会删除字符 '\t'水平制表符(Tab) 跳转到下一个水平制表点位。具体效果读者可以通过在记事本中输入一些文本然后按 Tab按键来查看'\n'换行符(Newline) 开始新的一行
不过只有 Linux 系统是正宗的\n(LF, )换行。Windows 系统文本文件的换行符实际上是\r\n(CRLF),MacOS 则是\r(CR, Carriage Return)。不过在 C 标准库里面都统一体现为\n——这充分体现了标准化的方便性'\v'垂直制表符(Vertical Tab) 跳转到下一个垂直制表点位。这个在现代的终端(就是弹出的那个黑漆漆的窗口)可能无效 '\f'换页符(Form Feed) 开启新的一页。Form Feed 是进纸的意思。这个在现代的终端可能无效,不过有些实现会做成清空终端内容 '\r'回车符(Return) 将光标移回当前行的开头。这同样是打字机时代的残余 '\''单引号 由于 '是字符的标识,所以它本身需要转义
比如printf("这是单引号:\' ");'\"'双引号 同理, "是字符串的标识'\ddd'八进制表示法 其中 ddd 代表 1 到 3 位的八进制数,用于表示 ASCII 字符。
比如A的的 ASCII 码是 65。65 的二进制是100 0001。分成三组:001000001(为了凑成 3 位一组,前面补零)。第一组 001 是八进制的 1,第二组000是八进制的 0,第三组001是八进制的 1。
所以A的 ASCII 码八进制表示就是'\101'。用这种方式可以输出一些打不出来的字符。具体查阅ASCII表'\xhh'十六进制表示法 其中 hh 代表 1 到 2 位的十六进制数。 A的 ASCII 码十六进制表示是'\x41'。
4.2.2 scanf - 格式化输入(聆听指挥)
#
scanf ("scan formatted") 就像是程序竖起耳朵,等待用户输入 特定格式 的内容。
请看下面的例子:
#include <stdio.h>
int main() {
int cultivation_base; // 修为
char sect_name[20]; // 宗门名
printf("请输入你的当前修为(年):");
scanf("%d", &cultivation_base); // 注意这里的 & 符号!
printf("请输入你的宗门:");
scanf("%s", sect_name); // 对于字符串数组,不需要 &
printf("原来你是%s宗的的道友,道行已有%d年之久!\n", sect_name, cultivation_base);
return 0;
}
交互内容:
请输入你的当前修为(年):114
请输入你的宗门:下北沢空手部
原来你是下北沢空手部宗的的道友,道行已有114年之久!
4.2.2.1 美观、简洁……那代价是什么? #
上面的交互进行的是相对“正常”(至少是从程序的角度)的输入,考虑这样的交互(在 Ubuntu 24.04.2 LTS (GNU/Linux 6.6.87.2-microsoft-standard-WSL2 x86_64)上运行):
请输入你的当前修为(年):1145141919810
请输入你的宗门:下北沢空手部池沼流派
原来你是下北沢空手部池沼流派宗的的道友,道行已有-1614348222年之久!
*** stack smashing detected ***: terminated
已中止 (核心已转储)
何 だよ?堆栈溢出?为什么呢?还有-1614348222 是什么鬼!我们把目光移向这两行:
int cultivation_base; // 修为
char sect_name[20]; // 宗门名
发现什么了吗?int 的范围是 -2147483648~+2147483647,而 1145141919810 远超这个范围。被截断后,1145141919810 % 2147483648 - 2147483648 正好就是 -1614348222!
而堆栈错误的问题更加严重。sect_name 的长度是 20,这意味着它最多存储 19 个 ASCII 字符(还记得吗,必须有一位 \0 作为字符串的结尾)。而中文 在这里 每个字占据 2 个 ASCII 位,所以只能输入 9 个汉字。而 下北沢空手部池沼流派 已经是 10 个字了。scanf 不知道这个 sect_name 的地盘有多大,所以 scanf 不语,只是一味地往后写,直到超过 sect_name 而写到其他变量、其他程序甚至系统核心的位置。“我以为减速带呢”、“和我的%s 说去吧”,虽然示例里面的溢出被系统轻易地发现并阻止,但是不保证更复杂的程序,比如系统驱动等会被系统兜底。所以我们说 scanf 是不安全的。很多黑客也会采取类似的缓冲区溢出来达到入侵系统的目的。而我们写代码就一定要注意这方面的问题。
事实上 printf 也有类似的问题。因为 printf 没法检查传入的参数正不正确(因为在传入参数的时候会发生自动类型转换,具体将在下一章讲解),比如 printf("%s", 26710);,就强行将一个整数值当成了字符串(本质是指针)传递,而访问一个非法区的指针也是很危险的。
而且,printf 和 scanf 不能扩展,对于自己定义的变量类型,无法直接进行输入输出。所以虽然 printf 和 scanf 非常强大方便,在 C++中使用却大大受限。
碎碎念:
&符号:对于int,double,char等基本类型变量,必须在变量前加上&(取地址)。这相当于告诉scanf:“请把读取到的数据,送到 xx 的位置去!”。(指针章节会详解,现在先牢记规则)。- 数组例外:对于 char 数组(用来存字符串),名字本身就代表了地址,所以不需要加
&。
4.3 C++的流式 I/O:更安全 、更丑陋(?) 的方案
#
看到了吗?printf 和 scanf 虽然很美观、简洁且强大,但是它确实称得上是“不会拿捏距离的 printf(scanf)同学”,稍有不慎就会造成严重事故。C++作为 C 的“现代化”后代,引入了一套更安全、更直观的输入输出机制——流(Stream)。
4.3.1 std::cout - 输出流
#
使用 <<(流插入运算符)将数据“插入”到输出流 std::cout 中。这样可以连续插入不同的数据对象,比如:
#include <iostream>
#include <string> // 必须包含这个头文件才能使用string类型
// using namespace std; // 我们这次显式使用std::
int main() {
std::string name = "韩立"; // C++的字符串类型,更安全好用
int spirit_stones = 500;
double power = 114514.1919810;
std::cout << "我" << name << "就是饿死,从这儿跳下去!" << std::endl;
std::cout << "……真香。" << std::endl;
// 输出: 韩立道友,你的灵石还剩500块,战斗力为114514。
// cout会自动判断类型,不需要%d %f
std::cout << name << "道友,你的灵石还剩" << spirit_stones
<< "块,战斗力为" << power << "。" << std::endl;
return 0;
}
因为数据是向外输出的,所以箭头指向 std::cout。
碎碎念:
std::endlvs\n
两者都能换行。但std::endl多做了一个动作:刷新输出缓冲区。
你可以把它理解为:\n是“把这句先记在本子上,等待合适的时候念出来”,而std::endl是“把这句话记下来并且立刻念出来”——显然反复看稿子的std::endl一般更慢些。但在需要确保信息立刻显示时(如调试日志),用std::endl会合适些;而在需要大量输出时,用\n效率一般会更高。
4.3.2 std::cin - 输入流 (聆听指挥 2.0)
#
使用 >>(流提取运算符)可以从输入流 std::cin 中“提取”数据到变量。它大大提升了安全性。因为数据是来自 std::cin,所以箭头从 std::cin 出发向外指。
#include <iostream>
#include <string> // 必须包含这个头文件才能使用string类型
int main() {
std::string sect_name; // 使用std::string,没有字符上限
int cultivation_base;
std::cout << "请问道友来自何门何派? ";
std::cin >> sect_name; // 从键盘读取一个字符串到sect_name
std::cout << "请问道友修行多少年了? ";
std::cin >> cultivation_base; // 从键盘读取一个整数到age
std::cout << "原来是" << cultivation_base << "岁的" << sect_name
<< "道友,失敬失敬!" << std::endl;
return 0;
}
交互:
请问道友来自何门何派? 下北沢空手部池沼流派
请问道友修行多少年了? 114514
原来是114514岁的下北沢空手部池沼流派道友,失敬失敬!
总结下 std::cin 和 std::cout。最关键的一点就是 安全:比如对于 std::string,std::cin 会 自动管理内存,用户输入再长也不会导致“堆栈粉碎”(只要你的内存没被吃完)——这就从根本上杜绝了 scanf 的最大安全隐患;而 std::cout 也会自动判断类型,不再需要 %d、%s 这样一一对应了。另外,你也不需要关心是不是需要 &,直接无脑填进去就可以了。可以说唯一的缺点就是 丑 且麻烦。对比下面的等效代码:
std::string stuff = "生瓜蛋子";
int price = 2;
int weight = 15;
// stuff.c_str()将stuff转换成C的字符串便于printf读取
printf("这%s %d 块一斤,%d 斤,收您 %d 块", stuff.c_str() , price, weight, price*weight);
std::cout << "这" << stuff << " " << price << "块一斤," << weight << "斤,收您" << price*weight <<"块";
由于 << 和 >> 不位于常用打字位置,所以写出流符号需要反复移动手的位置(特别是中文输入时需要反复切换语言)。同时,流符号的反复出现容易打断阅读、写入的思维。然而由于 printf 和 scanf 实在太不安全了,所以大家还是 忍着恶心 选择了 std::cin 和 std::cout。
4.4 文件流也是流:fopen 和 fstream
#
很多时候我们不一定要从标准输入 stdin 获取输入,而需要从特定的文件读入数据(信息竞赛往往如此)。这时候 stdio.h 和 iostream 就不管用了。我们往往需要使用 文件流 进行输入输出。
4.4.1 C 的文件流 #
4.4.1.1 fopen() 与 fclose():打开/关闭文件
#
fopen 用于通过某种方式打开一个文件,函数原型是:
FILE *fopen(const char *filename, const char *mode)
也就是打开一个文件 filename,使用的方式由 mode 字符串描述。返回一个 FILE 指针
-
如果出错,会返回
NULLmode字符串的具体效果如下:
| 模式 | 描述 |
|---|---|
| “r” | Read,打开一个仅供读取而不能写入的文件。该文件 必须存在 |
| “w” | Write,创建一个可以写入的 空文件,如果文件已存在,会 清空文件 |
| “a” | Append,追加到一个文件。写入操作将向文件末尾 追加 数据。如果文件不存在,则创建文件 |
| “r+” | 打开一个文件,可读取也可写入。该文件 必须存在 |
| “w+” | 创建一个文件,可读取也可写入。已有的文件会被 清空 |
| “a+” | 打开一个文件,可读取也可追加 |
打开文件后即获得一个 FILE 指针。务必检查 FILE 是否为 NULL。得到 FILE 指针以后,我们就能进行文件操作了(见后)。文件操作结束后,记得使用 fclose() 关闭,如果 fclose 返回非 0 值则表明关闭文件出错。
fpConfigFfile = fopen("./config.json", "r+");
if (fpConfigFfile == NULL) {
// 错误处理
}
// ...
// 处理文件
if (fclose(fpConfigFfile) != 0) {
// 错误处理
}
4.4.1.2 f 系列函数:看似戴上面具,实则摘下面具
#
f 系列函数有很多,我们简单介绍一些
-
fprintf、fscanf和fget:这些就是非f函数的文件版本,使用方式几乎与后者完全一致。我们继续前面的例子:fprintf(fpConfigFfile, "%s: %d", key, value); // printf("%s: %d", key, value);对比一下其实基本一致,只是多了个文件指针罢了。
碎碎念:万物皆文件 の 哲学
C 语言继承了 Linux“万物皆文件”的哲学,以至于
stdin和stdout本质上也是一个FILE*,所以下面的代码是等效的:fprintf(stdout, "Hello World!"); printf("Hello World!");换言之其实
fprintf才是更底层的东西,虽然多了个f表明它是操作文件的,这也是为什么这一节叫做“看似戴上面具,实则摘下面具”。此外,还有
stderr,用于错误输出,一般情况下它输出的位置与stdout是一致的,但是由于stderr没有缓冲区,所以错误信息可以立即输出 -
fread、fseek、fwrite:这是整块读入、写入文件的函数。它们的函数原型如下size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);意思是“将
ptr指向的每个元素大小为size、含有nmemb个元素的内存区域写入FILE指向的文件/从FILE当中读取”。该函数返回一个
size_t,表示成功写入元素的总数。如果成功,它应该与nmemb一样大。如果该数字与nmemb参数不同,则会显示一个错误。(位于全局变量errno中)还有若干函数难以一一介绍,下面是一个较为全面的例子,展示了大部分常见的 C 风格的文件操作。读者可以模仿这个例子进行变成:
#include<stdio.h> #include<string.h> int main () { FILE *fp; char str[] = "watashino onanii wo mite kudasai!"; char buffer[100]; long file_size = 0; // 写入文件(使用fwrite) fp = fopen("0721.txt", "w"); if (!fp) { // stderr 错误流 fprintf(stderr, "failed to open file for writing!\n"); return -1; } // 写入数据 size_t written = fwrite(str, sizeof(char), strlen(str), fp); if (written != strlen(str)) { fprintf(stderr, "failed to write all data!\n"); } fclose(fp); printf("Data written to file: %s\n", str); // 读取文件并修改部分内容(使用fread-fseek-fwrite) fp = fopen("0721.txt", "r+"); // 以读写模式打开 if (!fp) { fprintf(stderr, "failed to open file for reading and writing!\n"); return -1; } // 使用fseek获取文件大小 // int fseek(FILE *stream, long int offset, int whence) // stream: 文件指针 // offset: 相对whence的偏移量,可以是正值(向后)、负值(向前)、0(不偏移) // whence: 相对文件开头的偏移量 // whence 可以是常量:SEEK_SET(文件开头)、SEEK_END(文件结尾)、SEEK_CUR(当前位置) fseek(fp, 0, SEEK_END); // 移动到文件末尾 file_size = ftell(fp); // 获取当前位置(即文件大小) fseek(fp, 0, SEEK_SET); // 回到文件开头 printf("File size: %ld bytes\n", file_size); // 使用fread读取文件内容 // sizeof(buffer)-1 防止缓冲区溢出 size_t read_count = fread(buffer, 1, sizeof(buffer)-1, fp); buffer[read_count] = '\0'; printf("Original content: %s\n", buffer); // 使用fseek定位到特定位置进行修改 fseek(fp, 10, SEEK_SET); // 定位到从文件开头的第10个字节("onanii"的位置) // 使用fwrite修改部分内容 char new_text[] = "coding"; fwrite(new_text, sizeof(char), strlen(new_text), fp); // 验证修改结果 fseek(fp, 0, SEEK_SET); // 回到文件开头 read_count = fread(buffer, 1, sizeof(buffer)-1, fp); buffer[read_count] = '\0'; printf("Modified content: %s\n", buffer); // 3. 在文件末尾追加内容 fseek(fp, 0, SEEK_END); // 移动到文件末尾 char append_text[] = " - arigatou!"; fwrite(append_text, sizeof(char), strlen(append_text), fp); // 读取最终结果 fseek(fp, 0, SEEK_SET); fseek(fp, 0, SEEK_END); long final_size = ftell(fp); fseek(fp, 0, SEEK_SET); char final_buffer[150]; read_count = fread(final_buffer, 1, sizeof(final_buffer)-1, fp); final_buffer[read_count] = '\0'; printf("Final content: %s\n", final_buffer); fclose(fp); return 0; }
碎碎念:文件结尾EOF
某些时候程序会使用管道,将文件输入视为
stdin/stdout,这时候如何判断文件是否到达结尾呢?那就是EOF常量了:#include <stdio.h> int main() { int c; while ((c = getchar()) != EOF) { putchar(c); } return 0; }
getchar()是从stdin读取一个字符,putchar()则是输出一个字符到stdout。
EOF是一个特殊的值(一般为-1,即使如此不要硬编码),以表示“文件结束”或“输入结束”。当我们使用输入函数(如scanf()、getchar()等)读取数据时,如果遇到文件结束或输入结束的情况,这些函数会返回EOF。在终端输入输出时,你可以通过按下 Ctrl+D(Unix/部分Linux)或在行首输入 Ctrl+Z(Windows/部分Linux)来手动触发 EOF。
4.4.2 C++ 的 fstream:面向对象的文件操作
#
4.4.2.1 三种文件流类 #
C++ 提供了三个主要的文件流类,它们都定义在 <fstream> 头文件中:
ifstream:输入文件流,用于读取文件(Input File Stream)ofstream:输出文件流,用于写入文件(Output File Stream)fstream:文件流,既可读又可写(File Stream)
4.4.2.2 打开和关闭文件 #
与 C 语言类似,C++ 也需要打开和关闭文件,但方式更加“面向对象”:
#include <fstream>
#include <iostream>
using namespace std;
int main() {
// 方法1:先创建对象,再打开文件
ifstream inputFile;
inputFile.open("data.txt");
// 方法2:创建对象时直接打开文件
ofstream outputFile("output.txt");
// 检查文件是否成功打开
if (!inputFile.is_open()) {
cerr << "无法打开输入文件!" << endl;
return -1;
}
if (!outputFile) { // 重载了!运算符,也可以这样检查
cerr << "无法打开输出文件!" << endl;
return -1;
}
// 使用文件...
// 关闭文件
// 其实析构函数(第11章你会学到)会自动调用
// 当然你也可以像这样显式关闭
// 文件对象生命周期较长为避免泄露最好显示关闭
inputFile.close();
outputFile.close();
return 0;
}
4.4.2.3 打开模式 #
与 C 语言的 fopen 模式对应,C++ 也提供了类似的打开模式:
| 模式标志 | 描述 |
|---|---|
ios::in |
读取模式 |
ios::out |
写入模式 |
ios::app |
Append。追加模式 |
ios::trunc |
Truncate。如果文件存在,清空内容。这只是个操作,不是打开模式。必须和 ios::out 一起使用才有意义 |
ios::binary |
二进制模式(而不是按字符读取) |
也可以组合使用:
fstream file("data.txt", ios::in | ios::out | ios::app);
4.4.2.4 文件读写操作 #
C++ 的文件流重载了 << 和 >> 运算符,使得文件操作与 cin/cout 别无二致:
下面是完整的案例,可以与前面的 C 版本相对比:
#include <iostream>
#include <fstream>
#include <string>
int main() {
// 要写入的字符串
std::string str = "watashino onanii wo mite kudasai!";
// 1. 写入文件
// std::ofstream 用于文件输出(写入)
std::ofstream ofs("0721.txt");
if (!ofs) {
// std::cerr 是标准错误流,等同于 C 的 stderr
std::cerr << "Failed to open file for writing!" << std::endl;
return -1;
}
// 使用 << 运算符直接写入字符串,非常直观
ofs << str;
// ofstream 析构时会自动关闭文件,但显式关闭是好习惯
ofs.close();
std::cout << "Data written to file: " << str << std::endl;
// 2. 读取文件并修改部分内容
// std::fstream 用于文件输入和输出(读写)
std::fstream fs("0721.txt", std::ios::in | std::ios::out);
if (!fs) {
std::cerr << "Failed to open file for reading and writing!" << std::endl;
return -1;
}
// 读取整个文件内容到 std::string
// 先将文件指针移到末尾以获取大小
fs.seekg(0, std::ios::end);
long file_size = fs.tellg();
fs.seekg(0, std::ios::beg);
std::string buffer(file_size, '\0'); // 预分配空间
// 最好强制转换
// 这样可能不会出错
// fs.read(&buffer[0], file_size);
fs.read(&buffer[0], static_cast<std::streamsize>(file_size));
std::cout << "Original content: " << buffer << std::endl;
// 使用 seekp (seek put) 定位到写入位置
// 注意:fstream 有两个指针,g (get) 用于读,p (put) 用于写
fs.seekp(10, std::ios::beg); // 定位到从文件开头的第10个字节
// 直接写入新内容
std::string new_text = "coding";
fs.write(new_text.c_str(), new_text.length());
// 验证修改结果
fs.seekg(0, std::ios::beg); // 将读指针移回开头
std::string modified_content;
// 使用 >> 或 getline 读取,但为了读取整行(包括空格),我们用 getline
std::getline(fs, modified_content, '\0'); // 读取直到文件末尾(或空字符)
std::cout << "Modified content: " << modified_content << std::endl;
// 3. 在文件末尾追加内容
// 使用 std::ofstream 并指定 app (append) 模式,会自动定位到文件末尾
std::ofstream append_ofs("0721.txt", std::ios::app);
if (!append_ofs) {
std::cerr << "Failed to open file for appending!" << std::endl;
return -1;
}
std::string append_text = " - arigatou!";
append_ofs << append_text;
append_ofs.close();
// 读取最终结果
std::ifstream ifs("0721.txt");
if (!ifs) {
std::cerr << "Failed to open file for final reading!" << std::endl;
return -1;
}
std::string final_content;
std::getline(ifs, final_content, '\0'); // 读取整个文件内容
std::cout << "Final content: " << final_content << std::endl;
return 0;
}
4.5 小结与选择:C 风格 vs C++风格 #
控制台IO:
| 特性 | printf/scanf (C 风格) |
cout/cin (C++流) |
|---|---|---|
| 安全性 | 低 易缓冲区溢出、类型不匹配、忘记 & |
高 自动内存管理、类型安全、无需 & |
| 易用性 | 直观,但需记忆格式符 | 像搭积木,但是代码编写稍繁琐 |
| 底层控制 | 强 可以对方便地输出进行精确的控制 | 同样强 但是相对麻烦得多 |
文件IO:
| 特性 | C 风格 | C++ 风格 |
|---|---|---|
| 头文件 | #include <stdio.h> |
#include <fstream> |
| 文件指针 | FILE* fp |
fstream file |
| 打开文件 | fopen("file", "mode") |
file.open("file", mode) |
| 关闭文件 | fclose(fp) |
file.close() |
| 错误检查 | fp == NULL |
!file 或 file.is_open() |
| 写入数据 | fprintf, fwrite |
file <<, file.write() |
| 读取数据 | fscanf, fread |
file >>, file.read() |
| 文件定位 | fseek(fp, offset, whence) |
file.seekg()(读), file.seekp()(写) |
| 获取位置 | ftell(fp) |
file.tellg()(读), file.tellp()(写) |
小建议:
-
作为初学者,请毫不犹豫地优先使用
iostream和fstream标准库。 它们能保护你免受大多数低级错误的困扰。 -
但至少要 了解
stdio.h的函数,因为你一定会遇到它们,尤其是在阅读 C 语言代码或某些教程时。
4.6 展望未来:C++23 的 std::print
#
天下苦 iostream 久矣,于是在 C++23 草案中,终于引入了强大的 FMT 库(Format)。可以使用 {} 表示空位。参见下面的代码:
#include <print> // 需要包含新的头文件
int main() {
std::string technique = "百分百弱点击破";
int success_rate = 100;
// 也可以是println,这个会自动换行
std::print("绝技「{}」的成功率是{}%。\n", technique, success_rate);
}
输出:
绝技「百分百弱点击破」的成功率是 100%。
fmt 结合了 printf/scanf 的占位符格式,以及 iostream 的安全性,可谓是集百家之长。未来会有越来愈得多的编译器支持这一特性,我们拭目以待!
现在,你的程序不再是哑巴和聋子了!C++为你提供了从经典但危险的 C 风格,到现代安全的流式 I/O,乃至最新的的 std::print 等一系列工具。
记住:能力越大,责任越大。 scanf 给了你强大的格式化能力,但也带来了风险。而从 iostream 开始,无疑是养成良好编程习惯的、更安全的第一步。
学到这里,让我听见你程序的声音吧!
Let me hear your voice
shout it out shout it out——《Time to Shine》
-
使用
printf和std::cout分别输出第一行和第二行的内容:If I'm an eggplant Then I will give you my NUTRIENTS If I'm a tomato Then I will give you ANTIOXIDANTS给定这些变量:
const char stuff1[] = "eggplant"; const char stuff2[] = "tomato"; const char donation1[] = "my NUTRIENTS"; const char donation2[] = "ANTIOXIDANTS"; -
使用合理的方式实现以下交互(第一行是输入),最好 C 和 C++:都尝试下
15 12 15 + 12 = 27 -
使用
2.的程序(C++版本),但是输入改为:十五 十二看看会发生什么
-
使用
2.的程序(C 版本)在
scanf语句前面加上int count =。也就是int count = scanf(......
然后在return 0前面再加上下面几行:printf("成功读入了%d个参数\n", count); char remain[100] = ""; if(count < 2) { fgets(remain, 100, stdin); printf("缓冲区还剩下:%s", remain); }先正常输入阿拉伯数字,然后尝试输入:
15 十二和
十五 十二再试一试。
你得到了什么启示?如何解决 3.和 4.的问题?因为还没学习控制语句和更多 IO,说出大致思路即可
提示:对于
std::cin,有std::cin.fail()返回输入是否失败;有std::cin.clear();清除错误
而对于scanf,想想count可以怎么利用、缓冲区还剩下的东西怎么处理。 -
你发现了一个神秘的日记本文件
diary.txt,但是里面的内容被加密(凯撒加密)了!每个字符的 ASCII 码都比实际值大了 1。请用文件流操作解密并输出内容。要求:
- 使用 C 或 C++ 文件流读取
diary.txt - 将每个字符的 ASCII 码减 1 后输出到
decrypted_diary.txt - 如果文件不存在,要给出合适的错误提示
PS: 请手动创建
diary.txt,内容是cbojttzvnnfooup/ezjtv/xp.svep,并将它和你的exe文件在同一个目录下。 - 使用 C 或 C++ 文件流读取