C: 高级I/O与命令行

命令行参数就像餐厅点菜——你告诉程序要什么(argv),程序数一数你点了几个菜(argc),然后按单做菜。

1. 命令行参数

(1) argc 和 argv

main 函数可以接收两个参数:

C
int main(int argc, char *argv[])
C
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("参数个数: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

运行:

BASH
./program hello world 123
TEXT 📖 仅展示
参数个数: 4
argv[0] = ./program
argv[1] = hello
argv[2] = world
argv[3] = 123
💡 命令行参数都是字符串!如果需要数字,必须用 atoiatolstrtol 转换。

(2) 参数解析实战

实际项目中,命令行参数通常以 - 开头表示选项:

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

int main(int argc, char *argv[]) {
    int verbose = 0;
    int count = 1;
    const char *filename = NULL;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
            verbose = 1;
        } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            count = atoi(argv[++i]);
        } else if (argv[i][0] != '-') {
            filename = argv[i];
        } else {
            fprintf(stderr, "未知选项: %s\n", argv[i]);
            return 1;
        }
    }

    if (filename == NULL) {
        fprintf(stderr, "用法: %s [-v] [-n 次数] 文件名\n", argv[0]);
        return 1;
    }

    printf("文件: %s, 次数: %d, 详细模式: %s\n",
           filename, count, verbose ? "开" : "关");
    return 0;
}
⚠️ 手动解析参数容易出错。复杂项目可以用 getopt 函数,它能处理短选项和长选项的组合。


2. 环境变量

(1) getenv

C
char *getenv(const char *name);

返回环境变量的值,不存在则返回 NULL

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

int main(void) {
    const char *path = getenv("PATH");
    if (path != NULL) {
        printf("PATH = %s\n", path);
    }

    const char *home = getenv("HOME");
    if (home != NULL) {
        printf("HOME = %s\n", home);
    }
    return 0;
}

(2) 外部变量 environ

C语言定义了一个全局变量 environ,指向所有环境变量字符串数组:

C
#include <stdio.h>

extern char **environ;

int main(void) {
    char **env = environ;
    while (*env) {
        printf("%s\n", *env);
        env++;
    }
    return 0;
}
💡 环境变量常用于配置程序行为,比如数据库连接字符串、日志级别等。比硬编码到代码里灵活得多。


3. errno 错误处理

(1) errno 机制

C语言标准库遇到错误时,通常不会直接崩溃,而是设置全局变量 errno 并返回一个错误值:

C
#include <errno.h>

errno 在每次库函数调用成功时不会被清零!所以必须在函数调用失败后才检查它。

(2) perror / strerror

C
void perror(const char *s);
char *strerror(int errnum);

perror 会输出 s: 错误信息stderrstrerror 返回错误码对应的文字描述。

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

int main(void) {
    FILE *fp = fopen("不存在的文件.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        printf("错误码: %d, 错误信息: %s\n", errno, strerror(errno));
        return 1;
    }
    fclose(fp);
    return 0;
}
TEXT 📖 仅展示
fopen: No such file or directory
错误码: 2, 错误信息: No such file or directory

▶ 示例

完整的错误处理模式:

C
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法: %s 文件名\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        fprintf(stderr, "无法打开 '%s': %s\n", argv[1], strerror(errno));
        return 1;
    }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    if (ferror(fp)) {
        fprintf(stderr, "读取时发生错误: %s\n", strerror(errno));
        fclose(fp);
        return 1;
    }

    fclose(fp);
    return 0;
}
▶ 试一试
⚠️ ferror 用来检测流是否发生了读写错误,和 feof(检测文件末尾)是不同的。


4. 标准流

C语言程序启动时自动打开三个流:

名称 默认设备 文件描述符
stdin 标准输入 键盘 0
stdout 标准输出 屏幕 1
stderr 标准错误 屏幕 2

stdoutstderr 的区别:stdout 是有缓冲的,stderr 是无缓冲的。重定向时它们互不影响。

C
#include <stdio.h>

int main(void) {
    fprintf(stdout, "这是普通输出\n");
    fprintf(stderr, "这是错误输出\n");
    return 0;
}
BASH
./program > output.txt

这样运行后,"普通输出"进入文件,"错误输出"仍然显示在屏幕上。

(1) 重定向原理

在命令行中:

BASH
./program 2>&1 all.log

这条命令把 stderr 也合并到 stdout,一起写入 all.log


5. 临时文件

(1) tmpfile

C
FILE *tmpfile(void);

创建一个临时文件,以 "wb+" 模式打开。文件在关闭或程序结束时自动删除。

C
#include <stdio.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    fprintf(tmp, "临时数据 %d\n", 42);
    rewind(tmp);

    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        printf("%s", buf);
    }

    fclose(tmp);
    return 0;
}
TEXT 📖 仅展示
临时数据 42

(2) tmpnam

C
char *tmpnam(char *s);

生成一个不与现有文件冲突的临时文件名。但存在竞态条件——生成名字和创建文件之间,别的程序可能抢先创建同名文件。

⚠️ 优先使用 tmpfile,它原子性地创建并打开文件,更安全。tmpnam 在多线程环境下不安全。

▶ 示例

用临时文件处理中间数据:

C 📖 仅展示
#include <stdio.h>
#include <string.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    char *lines[] = {"香蕉", "苹果", "橙子", "葡萄"};
    int n = 4;

    for (int i = 0; i < n; i++) {
        fprintf(tmp, "%s\n", lines[i]);
    }

    rewind(tmp);
    printf("排序前:\n");
    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    rewind(tmp);
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            char a[64], b[64];
            long pos = ftell(tmp);
            fgets(a, sizeof(a), tmp);
            fgets(b, sizeof(b), tmp);
            a[strcspn(a, "\n")] = '\0';
            b[strcspn(b, "\n")] = '\0';

            if (strcmp(a, b) > 0) {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", b, a);
            } else {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", a, b);
            }
            fseek(tmp, pos + strlen(a) + strlen(b) + 2, SEEK_SET);
        }
        rewind(tmp);
    }

    rewind(tmp);
    printf("排序后:\n");
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    fclose(tmp);
    return 0;
}
逻辑代码 47 行(超过 40 行限制,仅展示)
TEXT 📖 仅展示
排序前:
  香蕉
  苹果
  橙子
  葡萄
排序后:
  苹果
  橙子
  葡萄
  香蕉

6. 格式化输出进阶

(1) snprintf

C
int snprintf(char *str, size_t size, const char *format, ...);

相比 sprintfsnprintf 多了一个 size 参数限制写入长度,防止缓冲区溢出。

C
#include <stdio.h>

int main(void) {
    char buf[10];
    int n = snprintf(buf, sizeof(buf), "Hello, %s!", "World");
    printf("buf = \"%s\", 需要长度 = %d\n", buf, n);
    return 0;
}
TEXT 📖 仅展示
buf = "Hello, Wo", 需要长度 = 13
🔥 snprintf 的返回值是格式化后应该有的长度,不是实际写入的长度。如果返回值大于 size-1,说明输出被截断了。

❓ 常见问题

Q argv 里的参数顺序是什么?
A argv[0] 是程序名本身,argv[1] 开始才是用户传入的参数,顺序和命令行上一致。
Q 为什么 perror 输出到 stderr 而不是 stdout?
A 因为错误信息不应该被 stdout 重定向到文件后丢失。stderr 无缓冲且独立于 stdout,保证错误信息总能被看到。
Q errno 什么时候会被清零?
A 不会自动清零。必须在调用可能失败的函数之前手动设 errno = 0,然后在函数返回错误值后再检查。

📖 小节

📝 作业

  1. 编写一个命令行程序,接受 -c 统计文件字符数,-l 统计行数,-w 统计单词数
  2. 编写程序,用 tmpfile 实现文件内容的反转(先全部写入临时文件,再倒序读出)
  3. 编写程序,演示 stdoutstderr 的重定向区别,用 >2> 分别捕获两种输出
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏