跳过正文

丙加·第4章·I want to tell you something——(输入与输出)

·1815 字·9 分钟·
目录

本章将讲述基本的输入、输出方法。介绍 printfscanfstd::cinstd::cout 的基础用法,并让读者领略 C++23 的 std::print 的优雅实现。

4.1 爽文女主不是哑巴
#

“……她,曾是上古世家的神女,却因遭人嫉妒而被夺去仙骨,废去修为,沦为宗门最下等的杂役。十年屈辱,受尽白眼,她咬紧牙关,默默积蓄力量……”
“大师兄,她……她好像要说话了!”
“哼,一个哑巴废人,还能掀起什么风浪?”
只见女主缓缓走上试炼台,朱唇轻启。众人本以为会是她十年来的第一句控诉,却只听她清晰地说道:

std::cout << "三十年河东,三十年河西,莫欺少女穷!" << std::endl;

全场愕然。

一个程序,不能只会自己闷头算(int a = 1 + 2;),还得能开口说话(输出),能用户指挥(输入),才能完成与世界的交互,成为真正的“爽文主角”!

本章,我们就来给我们的程序装上“嘴巴”和“耳朵”。

4.2 C 语言的“嘴巴”与“耳朵”:printfscanf
#

在 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。分成三组:001 000 001(为了凑成 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);,就强行将一个整数值当成了字符串(本质是指针)传递,而访问一个非法区的指针也是很危险的。
而且,printfscanf 不能扩展,对于自己定义的变量类型,无法直接进行输入输出。所以虽然 printfscanf 非常强大方便,在 C++中使用却大大受限。

碎碎念

  1. & 符号:对于 int, double, char 等基本类型变量,必须在变量前加上&(取地址)。这相当于告诉 scanf:“请把读取到的数据,送到 xx 的位置去!”。(指针章节会详解,现在先牢记规则)。
  2. 数组例外:对于 char 数组(用来存字符串),名字本身就代表了地址,所以不需要加 &

4.3 C++的流式 I/O:更安全 、更丑陋(?) 的方案
#

看到了吗?printfscanf 虽然很美观、简洁且强大,但是它确实称得上是“不会拿捏距离的 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::endl vs \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::cinstd::cout。最关键的一点就是 安全:比如对于 std::stringstd::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 <<"块";

由于 <<>> 不位于常用打字位置,所以写出流符号需要反复移动手的位置(特别是中文输入时需要反复切换语言)。同时,流符号的反复出现容易打断阅读、写入的思维。然而由于 printfscanf 实在太不安全了,所以大家还是 忍着恶心 选择了 std::cinstd::cout

4.4 文件流也是流:fopenfstream
#

很多时候我们不一定要从标准输入 stdin 获取输入,而需要从特定的文件读入数据(信息竞赛往往如此)。这时候 stdio.hiostream 就不管用了。我们往往需要使用 文件流 进行输入输出。

4.4.1 C 的文件流
#

4.4.1.1 fopen()fclose():打开/关闭文件
#

fopen 用于通过某种方式打开一个文件,函数原型是:

FILE *fopen(const char *filename, const char *mode)

也就是打开一个文件 filename,使用的方式由 mode 字符串描述。返回一个 FILE 指针

  • 如果出错,会返回 NULL

    mode 字符串的具体效果如下:

模式 描述
“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 系列函数有很多,我们简单介绍一些

  • fprintffscanffget:这些就是非 f 函数的文件版本,使用方式几乎与后者完全一致。我们继续前面的例子:

    fprintf(fpConfigFfile, "%s: %d", key, value);
    // printf("%s: %d", key, value);
    

    对比一下其实基本一致,只是多了个文件指针罢了。

    碎碎念:万物皆文件 の 哲学

    C 语言继承了 Linux“万物皆文件”的哲学,以至于 stdinstdout 本质上也是一个 FILE*,所以下面的代码是等效的:

    fprintf(stdout, "Hello World!");
    printf("Hello World!");
    

    换言之其实 fprintf 才是更底层的东西,虽然多了个 f 表明它是操作文件的,这也是为什么这一节叫做“看似戴上面具,实则摘下面具”。

    此外,还有 stderr,用于错误输出,一般情况下它输出的位置与 stdout 是一致的,但是由于 stderr 没有缓冲区,所以错误信息可以立即输出

  • freadfseekfwrite:这是整块读入、写入文件的函数。它们的函数原型如下

    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 !filefile.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()(写)

小建议:

  • 作为初学者,请毫不犹豫地优先使用 iostreamfstream 标准库。 它们能保护你免受大多数低级错误的困扰。

  • 但至少要 了解 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》

  1. 使用 printfstd::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";
    
  2. 使用合理的方式实现以下交互(第一行是输入),最好 C 和 C++:都尝试下

    15 12
    15 + 12 = 27
    
  3. 使用 2. 的程序(C++版本),但是输入改为:

    十五 十二
    

    看看会发生什么

  4. 使用 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 可以怎么利用、缓冲区还剩下的东西怎么处理。

  5. 你发现了一个神秘的日记本文件 diary.txt,但是里面的内容被加密(凯撒加密)了!每个字符的 ASCII 码都比实际值大了 1。请用文件流操作解密并输出内容。

    要求:

    • 使用 C 或 C++ 文件流读取 diary.txt
    • 将每个字符的 ASCII 码减 1 后输出到 decrypted_diary.txt
    • 如果文件不存在,要给出合适的错误提示

    PS: 请手动创建 diary.txt,内容是 cbojttzvnnfooup/ezjtv/xp.svep,并将它和你的exe文件在同一个目录下。

命令提示符@CommandPrompt-Wang
作者
命令提示符@CommandPrompt-Wang