HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • C 语言入门教程

    • C语言系列教程
    • 第01章 基础概念与编译
    • 第02章 数据类型与变量
    • 第03章 运算符
    • 第04章 输入输出
    • 第05章 流程控制
    • 第06章 数组
    • 第07章 指针
    • 第08章 函数
    • 第09章 构造类型

第04章 输入输出

4.1 格式化输出 printf

4.1.1 printf基本用法

#include <stdio.h>

int printf(const char *format, ...);

返回值: 成功时返回输出的字符数,失败返回负数。

基本示例:

#include <stdio.h>

int main(void)
{
    int a = 10;
    float f = 3.14;
    char c = 'A';

    printf("整数: %d\n", a);
    printf("浮点数: %f\n", f);
    printf("字符: %c\n", c);

    return 0;
}

输出:

整数: 10
浮点数: 3.140000
字符: A

4.1.2 格式控制符

格式符说明示例
%d / %i有符号十进制整数printf("%d", 123);
%u无符号十进制整数printf("%u", 123);
%o八进制整数printf("%o", 123); // 173
%x / %X十六进制整数(小写/大写)printf("%x", 255); // ff
%f浮点数printf("%f", 3.14);
%e / %E科学计数法printf("%e", 1234.5);
%g / %G自动选择%f或%eprintf("%g", 0.0001);
%c字符printf("%c", 'A');
%s字符串printf("%s", "hello");
%p指针地址printf("%p", ptr);
%%输出百分号printf("%%");

4.1.3 输出宽度控制

右对齐(默认):

#include <stdio.h>

int main(void)
{
    int a = 123;

    printf("[%d]\n", a);       // [123]
    printf("[%5d]\n", a);      // [  123]  右对齐,宽度5
    printf("[%10d]\n", a);     // [       123]  右对齐,宽度10

    return 0;
}

左对齐:

int main(void)
{
    int a = 123;

    printf("[%-5d]\n", a);     // [123  ]  左对齐,宽度5
    printf("[%-10d]\n", a);    // [123       ]  左对齐,宽度10

    return 0;
}

补零:

int main(void)
{
    int a = 123;

    printf("[%05d]\n", a);     // [00123]  宽度5,前面补0
    printf("[%010d]\n", a);    // [0000000123]  宽度10,前面补0

    return 0;
}

4.1.4 浮点数精度控制

#include <stdio.h>

int main(void)
{
    double pi = 3.141592653589793;

    printf("%f\n", pi);         // 3.141593  (默认6位小数)
    printf("%.2f\n", pi);       // 3.14  (2位小数)
    printf("%.10f\n", pi);      // 3.1415926536  (10位小数)

    printf("%10.2f\n", pi);     // [      3.14]  总宽度10,2位小数
    printf("%-10.2f\n", pi);    // [3.14      ]  左对齐

    return 0;
}

4.1.5 特殊格式

输出正号:

int main(void)
{
    int a = 10;
    int b = -10;

    printf("%+d\n", a);         // +10
    printf("%+d\n", b);         // -10

    return 0;
}

显示进制前缀:

int main(void)
{
    int a = 255;

    printf("%#o\n", a);         // 0377  (八进制前缀0)
    printf("%#x\n", a);         // 0xff  (十六进制前缀0x)
    printf("%#X\n", a);         // 0XFF

    return 0;
}

4.2 格式化输入 scanf

4.2.1 scanf基本用法

#include <stdio.h>

int scanf(const char *format, ...);

返回值: 成功赋值的输入项个数,遇到文件结束符返回EOF(-1)。

基本示例:

#include <stdio.h>

int main(void)
{
    int a;
    float f;
    char c;

    printf("请输入整数: ");
    scanf("%d", &a);

    printf("请输入浮点数: ");
    scanf("%f", &f);

    printf("请输入字符: ");
    scanf(" %c", &c);  // 注意空格

    printf("a=%d, f=%.2f, c=%c\n", a, f, c);

    return 0;
}

⚠️ 重要: scanf的参数必须是变量的地址(使用&运算符)!

4.2.2 scanf常见格式符

格式符说明示例
%d读取十进制整数scanf("%d", &a);
%u读取无符号整数scanf("%u", &a);
%f读取floatscanf("%f", &f);
%lf读取doublescanf("%lf", &d);
%c读取单个字符scanf("%c", &c);
%s读取字符串(遇空格结束)scanf("%s", str);
%[^\n]读取一行(包含空格)scanf("%[^\n]", str);

4.2.3 scanf的陷阱

陷阱1: 缓冲区残留换行符

#include <stdio.h>

int main(void)
{
    int a;
    char c;

    printf("输入整数: ");
    scanf("%d", &a);        // 输入:10回车 → 缓冲区残留'\n'

    printf("输入字符: ");
    scanf("%c", &c);        // 读取到残留的'\n'!!!

    printf("a=%d, c=%d\n", a, c);  // c的值是'\n'的ASCII码10

    return 0;
}

解决方法:

方法1: 格式符前加空格

scanf(" %c", &c);  // 空格会跳过所有空白符(空格、制表符、换行符)

方法2: 清空缓冲区

while (getchar() != '\n');  // 读取直到换行符
scanf("%c", &c);

陷阱2: 字符串读取遇空格停止

#include <stdio.h>

int main(void)
{
    char name[50];

    printf("输入姓名: ");
    scanf("%s", name);  // 输入"Zhang San",只读到"Zhang"

    printf("name = %s\n", name);  // 输出: Zhang

    return 0;
}

解决方法: 使用%[^\n]读取整行

scanf("%[^\n]", name);  // 读取直到遇到换行符

陷阱3: 未检查返回值

#include <stdio.h>

int main(void)
{
    int a;
    int ret;

    printf("输入整数: ");
    ret = scanf("%d", &a);

    if (ret != 1)
    {
        fprintf(stderr, "输入错误!\n");
        return 1;
    }

    printf("a = %d\n", a);

    return 0;
}

4.2.4 scanf的输入宽度限制

#include <stdio.h>

int main(void)
{
    char str[10];

    // 最多读取9个字符(留一个给'\0')
    scanf("%9s", str);

    printf("str = %s\n", str);

    return 0;
}

⚠️ 重要: 使用scanf读取字符串时,务必限制输入长度,防止缓冲区溢出!

4.3 字符输入输出

4.3.1 getchar与putchar

getchar: 从标准输入读取一个字符

#include <stdio.h>

int getchar(void);

putchar: 向标准输出写入一个字符

#include <stdio.h>

int putchar(int c);

示例: 字符转换

#include <stdio.h>

int main(void)
{
    int c;

    printf("输入小写字母: ");
    c = getchar();

    if (c >= 'a' && c <= 'z')
    {
        c = c - 32;  // 转换为大写
        printf("大写字母: ");
        putchar(c);
        putchar('\n');
    }

    return 0;
}

4.3.2 getchar读取一行

#include <stdio.h>

int main(void)
{
    int c;

    printf("输入一行文本:\n");

    while ((c = getchar()) != '\n')
    {
        putchar(c);
    }

    putchar('\n');

    return 0;
}

4.3.3 getchar与EOF

EOF(End Of File)是一个宏,值为-1,表示文件结束。

#include <stdio.h>

int main(void)
{
    int c;

    printf("输入内容(Ctrl+D结束):\n");

    while ((c = getchar()) != EOF)
    {
        putchar(c);
    }

    return 0;
}

⚠️ 注意:

  • Linux/Mac: Ctrl+D表示EOF
  • Windows: Ctrl+Z表示EOF

4.4 字符串输入函数

4.4.1 gets函数(已废弃)

char *gets(char *s);  // 危险!不检查缓冲区大小

⚠️ 危险: gets不检查缓冲区大小,容易造成缓冲区溢出,已被C11标准废弃!

// 危险示例
char buf[10];
gets(buf);  // 如果输入超过9个字符,会造成缓冲区溢出!

4.4.2 fgets函数(推荐)

#include <stdio.h>

char *fgets(char *s, int size, FILE *stream);

特点:

  • 安全,会限制读取长度
  • 读取整行,包括换行符'\n'
  • 遇到换行符或读满size-1个字符时停止

示例:

#include <stdio.h>

int main(void)
{
    char name[50];

    printf("输入姓名: ");

    if (fgets(name, sizeof(name), stdin) != NULL)
    {
        printf("name = %s", name);  // 包含换行符

        // 去除换行符
        size_t len = strlen(name);
        if (len > 0 && name[len-1] == '\n')
        {
            name[len-1] = '\0';
        }

        printf("处理后: %s\n", name);
    }

    return 0;
}

4.4.3 getline函数(GNU扩展)

#include <stdio.h>

ssize_t getline(char **lineptr, size_t *n, FILE *stream);

特点:

  • 自动分配内存
  • 可以读取任意长度的行
  • 使用后需要free释放内存

示例:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *line = NULL;
    size_t len = 0;
    ssize_t nread;

    printf("输入一行文本: ");

    nread = getline(&line, &len, stdin);

    if (nread != -1)
    {
        printf("读取了 %zd 字符\n", nread);
        printf("内容: %s", line);

        free(line);  // 释放内存
    }

    return 0;
}

4.4.4 三种函数对比

函数安全性内存管理是否包含换行符推荐度
gets不安全手动否已废弃
fgets安全手动是推荐
getline安全自动是推荐

4.5 输入输出缓冲区

4.5.1 缓冲区类型

C语言的I/O系统使用缓冲区来提高效率:

  • 全缓冲: 缓冲区满时才刷新(文件I/O)
  • 行缓冲: 遇到换行符时刷新(标准输出stdout)
  • 无缓冲: 立即输出(标准错误stderr)

4.5.2 缓冲区刷新

示例: 观察缓冲区

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("Hello");  // 没有换行符,数据在缓冲区
    sleep(3);         // 等待3秒
    printf(" World\n");  // 遇到换行符,刷新缓冲区

    return 0;
}

输出: 等待3秒后一次性输出"Hello World"

手动刷新缓冲区:

#include <stdio.h>

int main(void)
{
    printf("Hello");
    fflush(stdout);  // 强制刷新标准输出缓冲区

    sleep(3);
    printf(" World\n");

    return 0;
}

输出: 立即输出"Hello",3秒后输出" World"

4.5.3 清空输入缓冲区

错误的方法:

fflush(stdin);  // 未定义行为!

正确的方法:

// 方法1: 读取到换行符
while (getchar() != '\n');

// 方法2: 读取到EOF或换行符
int c;
while ((c = getchar()) != '\n' && c != EOF);

示例: 清空缓冲区

#include <stdio.h>

void clear_input_buffer(void)
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main(void)
{
    int a;
    char c;

    printf("输入整数: ");
    scanf("%d", &a);

    clear_input_buffer();  // 清空缓冲区

    printf("输入字符: ");
    scanf("%c", &c);

    printf("a=%d, c=%c\n", a, c);

    return 0;
}

4.6 文件定位符

4.6.1 stdin、stdout、stderr

#include <stdio.h>

extern FILE *stdin;   // 标准输入
extern FILE *stdout;  // 标准输出
extern FILE *stderr;  // 标准错误

示例: 使用stderr输出错误信息

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int a, b;

    printf("输入两个整数: ");

    if (scanf("%d%d", &a, &b) != 2)
    {
        fprintf(stderr, "错误: 输入格式不正确!\n");
        exit(1);
    }

    printf("%d + %d = %d\n", a, b, a + b);

    return 0;
}

stdout与stderr的区别:

# stdout和stderr都输出到屏幕
$ ./test

# 只重定向stdout到文件,stderr仍输出到屏幕
$ ./test > output.txt

# 同时重定向stdout和stderr
$ ./test > output.txt 2>&1

4.7 实践练习

练习1: 格式化输出表格

编写程序输出格式化的成绩表:

#include <stdio.h>

int main(void)
{
    printf("%-10s %8s %8s %8s\n", "姓名", "语文", "数学", "英语");
    printf("%-10s %8.1f %8.1f %8.1f\n", "张三", 89.5, 92.0, 88.5);
    printf("%-10s %8.1f %8.1f %8.1f\n", "李四", 85.0, 90.5, 91.0);
    printf("%-10s %8.1f %8.1f %8.1f\n", "王五", 92.5, 88.0, 89.5);

    return 0;
}

练习2: 安全的字符串输入

编写程序安全地读取用户姓名:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char name[50];

    printf("请输入姓名: ");

    if (fgets(name, sizeof(name), stdin) != NULL)
    {
        // 去除换行符
        size_t len = strlen(name);
        if (len > 0 && name[len-1] == '\n')
        {
            name[len-1] = '\0';
        }

        printf("您好, %s!\n", name);
    }

    return 0;
}

练习3: 输入验证

编写程序,要求用户输入1-10之间的整数,直到输入正确为止:

#include <stdio.h>

int main(void)
{
    int num;
    int ret;

    do
    {
        printf("请输入1-10之间的整数: ");
        ret = scanf("%d", &num);

        // 清空缓冲区
        int c;
        while ((c = getchar()) != '\n' && c != EOF);

        if (ret != 1)
        {
            printf("错误: 输入格式不正确!\n");
            continue;
        }

        if (num < 1 || num > 10)
        {
            printf("错误: 数字必须在1-10之间!\n");
        }

    } while (num < 1 || num > 10);

    printf("输入正确: %d\n", num);

    return 0;
}

4.8 常见错误

错误1: scanf忘记取地址符&

int a;
scanf("%d", a);  // 错误!应该是 &a

错误2: 字符串使用&

char str[50];
scanf("%s", &str);  // 错误!数组名本身就是地址
scanf("%s", str);   // 正确

错误3: 混用不同类型

int a;
scanf("%f", &a);  // 错误!int应该用%d

错误4: 缓冲区溢出

char buf[10];
scanf("%s", buf);  // 危险!未限制输入长度
scanf("%9s", buf);  // 正确!限制最多读取9个字符

错误5: 不检查scanf返回值

int a;
scanf("%d", &a);  // 如果输入非数字,a的值未定义
// 正确做法
if (scanf("%d", &a) != 1)
{
    fprintf(stderr, "输入错误!\n");
    return 1;
}

4.9 小结

本章学习了:

printf格式化输出(格式符、宽度、精度控制) scanf格式化输入及常见陷阱 getchar/putchar字符I/O gets/fgets/getline字符串输入函数对比 输入输出缓冲区机制 标准I/O流(stdin、stdout、stderr)

重点提示:

  1. 禁用gets,使用fgets替代
  2. scanf读取字符串时务必限制长度
  3. 总是检查scanf的返回值
  4. 注意清空输入缓冲区,避免读取到残留字符
  5. 使用fprintf(stderr, ...)输出错误信息

上一章: 第03章 运算符下一章: 第05章 流程控制

Prev
第03章 运算符
Next
第05章 流程控制