C++与C语言的输入输出

基本概念与头文件

C++和C语言分别使用不同的头文件和方法来处理输入输出(I/O)操作。

C++: <iostream> 流式I/O

<iostream> 是C++标准库中的核心头文件,提供了基于“流”(stream)的输入输出功能。流是一种抽象,代表数据的来源或目的地。

// C++代码中包含头文件
#include <iostream>

// 通常需要配合命名空间使用
using namespace std; // 或者显式使用 std::cin, std::cout

它定义了标准输入流对象 cin (通常关联键盘)、标准输出流对象 cout (通常关联屏幕) 和标准错误流对象 cerr

C语言: <stdio.h> 或 C++中的 <cstdio> 函数式I/O

C语言使用 <stdio.h> (Standard Input/Output) 头文件提供输入输出功能。在C++程序中,为了兼容性或特定需求,也可以使用其C++版本 <cstdio>

// C语言代码中包含头文件
#include <stdio.h>

// C++代码中包含C兼容头文件
#include <cstdio>
using namespace std;

它提供了 scanf() (格式化输入) 和 printf() (格式化输出) 等核心函数。

核心区别初探

  • C++流式I/O: 面向对象类型安全(编译器自动推断类型),语法更自然(使用 <<>> 操作符),易于扩展自定义类型的I/O。
  • C函数式I/O: 面向过程,需要手动指定格式(通过格式字符串),类型匹配需开发者保证,通常认为在简单类型上执行效率稍高(但现代编译器优化后差距缩小)。

虽然可以在C++程序中混合使用两者,但为了代码风格的一致性和可维护性,通常建议在项目中选择一种主要方式

C++流式输入输出: cin / cout

cin / cout 基本用法

C++使用输入流提取运算符 >> 和输出流插入运算符 << 进行操作。

#include <iostream>
#include <string> // 处理字符串需要包含

using namespace std;

int main() {
    int age;
    double price;
    string name;

    cout << "请输入姓名: "; // 提示用户输入
    cin >> name; // 从cin读取字符串到name (遇空白停止)

    cout << "请输入年龄和价格: ";
    cin >> age >> price; // 链式输入,按顺序读取

    // 链式输出
    cout << "你好, " << name
         << "! 年龄: " << age
         << ", 价格: " << price << endl; // endl 插入换行并刷新缓冲区

    return 0;
}

数据流向示意图

键盘输入 cin >> 程序变量 (数据流向) 程序变量/值 cout << 屏幕输出 (数据流向)

运算符 >><< 的方向形象地表示了数据的流动方向。

格式控制 (<iomanip>)

通过包含 <iomanip> 头文件,可以使用流操纵符来控制输出格式。

#include <iostream>
#include <iomanip>  // 使用setw等需要包含此头文件

using namespace std;

int main() {
    double pi = 3.1415926535;
    int number = 123;

    cout << "默认输出 PI: " << pi << endl;
    // 控制精度和宽度
    cout << "设置精度(4): " << fixed << setprecision(4) << pi << endl; // fixed: 固定小数位数
    cout << "设置宽度(10): [" << setw(10) << pi << "]" << endl; // 默认右对齐
    cout << "左对齐:       [" << left << setw(10) << pi << "]" << endl;
    cout << "填充字符('*'): [" << setfill('*') << setw(10) << right << number << "]" << endl; // right恢复右对齐

    // 控制进制
    cout << "十进制: " << dec << number << endl;
    cout << "十六进制: " << hex << showbase << number << endl; // showbase 显示前缀 0x
    cout << "八进制: " << oct << number << endl;

    return 0;
}

常用操纵符:setw(), setprecision(), fixed, scientific, left, right, setfill(), dec, hex, oct, showbase等。

特性与注意事项

  • 类型安全:编译器检查类型,不易出错。
  • 自动处理空白cin >> var; 默认会跳过输入流中的前导空白符(空格、制表符、换行符)。
  • 读取整行:使用 getline(cin, str_variable); 读取包含空格的一整行到字符串。注意之前若有 cin >> 操作,可能需要先用 cin.ignore() 清除缓冲区中的换行符。
  • 缓冲区cout 输出通常是缓冲的。endl 会插入换行并刷新缓冲区,而只用 '\n' 仅插入换行符,效率可能更高,但不保证立即显示。
  • 错误处理:可以通过检查流的状态(如 if (cin.fail()) { ... })来判断输入是否成功。

C函数式输入输出: scanf / printf

scanf / printf 基本用法

C语言使用 scanf() 读取格式化输入,printf() 进行格式化输出。

#include <stdio.h>

int main() {
    int age;
    double price;
    char name[50]; // C风格字符串需要预定义大小

    printf("请输入姓名: ");
    scanf("%s", name); // 读取字符串,遇空白停止,【注意:有缓冲区溢出风险!】

    printf("请输入年龄和价格: ");
    // 【重要】scanf需要传入变量的地址(&)
    scanf("%d %lf", &age, &price); // %d: int, %lf: double

    // %s: 字符串, %d: 整数, %.2lf: double保留两位小数, \n: 换行
    printf("你好, %s! 年龄: %d, 价格: %.2lf\n", name, age, price);

    return 0;
}

关键点scanf 需要变量的地址(除了数组名本身代表地址外),而 printf 需要变量的

核心:格式字符串

printfscanf 的行为由格式字符串精确控制。格式说明符以 % 开始。

格式符 对应类型 (printf/scanf) 说明
%d%i int 有符号十进制整数
%u unsigned int 无符号十进制整数
%ld long 长整型
%lld long long 长长整型 (C99/C++11)
%f float (printf), float* (scanf) 单精度浮点数 (printf 中 float 会提升为 double)
%lf double (printf - 可选), double* (scanf - 必须) 双精度浮点数
%Lf long double* (scanf) 长双精度浮点数
%c int (printf - 接收 char), char* (scanf) 单个字符
%s char* (指向C风格字符串) 字符串 (遇空白停止)
%p void* 指针地址
%% 输出一个 '%' 字符

注意scanf中 %f, %lf, %Lf 的区别!

printf 的高级格式控制

可以在 % 和格式符之间加入修饰符来控制宽度、精度、对齐等。

#include <stdio.h>

int main() {
    int num = 42;
    double pi = 3.14159;

    // 宽度与对齐 (默认右对齐)
    printf("宽度5: [%5d]\n", num);     // 输出: [   42]
    printf("宽度5,左对齐: [%-5d]\n", num);    // 输出: [42   ]

    // 精度 (对浮点数是小数位数,对整数是最小数字位数,对字符串是最大字符数)
    printf("Pi保留2位小数: %.2f\n", pi);      // 输出: 3.14
    printf("整数补零宽度5: [%05d]\n", num);    // 输出: [00042]
    printf("字符串最多3字符: %.3s\n", "Hello"); // 输出: Hel

    // 组合使用: 宽度8, 保留3位小数, 左对齐
    printf("组合: [%-8.3f]\n", pi);   // 输出: [3.142   ]

    // 带符号显示
    printf("带符号: [%+d]\n", num);     // 输出: [+42]
    printf("带符号: [%+d]\n", -num);    // 输出: [-42]

    return 0;
}

scanf 的注意事项

  • 地址传递:务必使用 & 获取变量地址(数组名除外)。
  • 类型匹配:格式符必须与变量类型严格匹配,否则导致未定义行为。
  • 缓冲区溢出风险:使用 %s 读取字符串时,若输入超过缓冲区大小,会造成严重安全问题。建议使用 %<宽度>s (如 %49s 读取到大小为50的数组) 或更安全的函数(如 fgets)。
  • 空白符处理scanf 中,多数格式符(如 %d, %lf, %s)会自动跳过前面的空白符。但 %c 不会跳过空白符,它会读取遇到的第一个字符,包括空格或换行符。可以在 %c 前加一个空格 (" %c") 来跳过空白。
  • 返回值scanf 返回成功读取并赋值的项数。可以利用此返回值检查输入是否符合预期。
int items_read = scanf("%d %lf", &a, &b);
if (items_read != 2) {
    printf("输入错误!\n");
    // 处理错误,例如清空缓冲区
    int c;
    while ((c = getchar()) != '\n' && c != EOF); // 清空到行尾
}

缓冲区与刷新

C的标准I/O也是带缓冲的。

  • printf 的输出通常是行缓冲(遇到换行符 \n 时刷新)或全缓冲(缓冲区满时刷新)。可以使用 fflush(stdout); 强制刷新标准输出缓冲区,确保内容立即显示。
  • scanf 从标准输入缓冲区读取。如果缓冲区中有残留字符(特别是换行符),可能会影响后续的 scanfgetchar() 调用。上面示例中的 while ((c = getchar()) != '\n' && c != EOF); 是一种常见的清空输入缓冲区到行尾的方法。

对比分析与应用建议

C++流式 vs C函数式 I/O 对比

特性 C++ cin / cout C scanf / printf
语法简洁度 较高 (链式调用, 操作符重载) 较低 (函数调用, 格式字符串)
类型安全 (编译器自动处理类型) (需手动匹配格式符与类型)
易用性 对初学者友好,不易出错 需要记忆格式符,易因地址、类型错误导致问题
执行效率 通常稍慢 (涉及对象构造/析构、虚函数等开销),但可通过关闭同步(sync_with_stdio(false))和解绑(cin.tie(nullptr))大幅提速 通常更快 (直接函数调用,底层优化),尤其在简单类型上
格式控制 灵活 (通过操纵符),但有时略显繁琐 非常灵活 (格式字符串功能强大),但也更复杂
错误处理 面向对象方式 (检查流状态) 函数返回值 (检查读取项数)
扩展性 (易于为自定义类重载 <<>>) (不易扩展到自定义类型)
安全性 相对较高 (cin >> string 自动管理内存) 较低 (scanf %s 易缓冲区溢出,需谨慎使用)

应用场景建议

场景一:算法竞赛 (ACM/OI)

在时间效率要求极高的竞赛中:

  • 推荐 scanf/printf:通常具有微弱的速度优势,尤其在大量简单数据输入输出时。格式控制直接明了。
  • 或者优化后的 cin/cout:通过在 `main` 函数开头加入:
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    可以解除 C++ 流与 C 标准 I/O 的同步,并解绑 cincout,使其性能接近甚至超过 scanf/printf。但需注意,之后不能再混合使用 C 和 C++ 的 I/O 函数。且 endl 的刷新开销仍在,大量输出时用 '\n' 可能更快。

场景二:日常学习与项目开发

在教学、练习或一般 C++ 项目中:

  • 强烈推荐 cin/cout
    • 更安全:避免了 scanf 的许多陷阱(类型不匹配、忘记&、缓冲区溢出)。
    • 更符合C++风格:面向对象,与 C++ 的类和对象系统结合更自然。
    • 易于扩展:方便为自定义类实现输入输出。
    • 可读性好:代码通常更清晰易懂。
  • 避免混合使用:除非有明确理由,否则在一个项目中坚持使用一种风格以保持一致性。

总结

选择哪种输入输出方式取决于具体场景和需求。对于现代C++编程,cin/cout 因其类型安全、易用性和面向对象的特性,通常是首选。而在对性能要求极致或需要 C 语言兼容性的场合,scanf/printf 仍然是有效的工具,但使用时需格外小心

掌握两者的用法和优缺点,能够帮助你根据实际情况做出最佳选择。