C: 高级I/O与命令行
命令行参数就像餐厅点菜——你告诉程序要什么(argv),程序数一数你点了几个菜(argc),然后按单做菜。
1. 命令行参数
(1) argc 和 argv
main 函数可以接收两个参数:
C
int main(int argc, char *argv[])
argc:参数个数(argument count),至少为 1(程序名本身)argv:参数字符串数组(argument vector),argv[0]是程序名
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
💡 命令行参数都是字符串!如果需要数字,必须用
atoi、atol 或 strtol 转换。
(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: 错误信息 到 stderr。strerror 返回错误码对应的文字描述。
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 |
stdout 和 stderr 的区别:stdout 是有缓冲的,stderr 是无缓冲的。重定向时它们互不影响。
C
#include <stdio.h>
int main(void) {
fprintf(stdout, "这是普通输出\n");
fprintf(stderr, "这是错误输出\n");
return 0;
}
BASH
./program > output.txt
这样运行后,"普通输出"进入文件,"错误输出"仍然显示在屏幕上。
(1) 重定向原理
在命令行中:
>重定向stdout到文件2>重定向stderr到文件<用文件替代stdin|把一个程序的stdout连接到另一个程序的stdin
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;
}
TEXT
📖 仅展示
排序前:
香蕉
苹果
橙子
葡萄
排序后:
苹果
橙子
葡萄
香蕉
6. 格式化输出进阶
(1) snprintf
C
int snprintf(char *str, size_t size, const char *format, ...);
相比 sprintf,snprintf 多了一个 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,然后在函数返回错误值后再检查。
📖 小节
argc/argv接收命令行参数,argv[0]是程序名getenv读取环境变量,environ遍历所有环境变量errno+perror/strerror是标准库错误处理的标准方式stdout有缓冲,stderr无缓冲,重定向时互不影响tmpfile创建自动删除的临时文件,比tmpnam更安全
📝 作业
- 编写一个命令行程序,接受
-c统计文件字符数,-l统计行数,-w统计单词数 - 编写程序,用
tmpfile实现文件内容的反转(先全部写入临时文件,再倒序读出) - 编写程序,演示
stdout和stderr的重定向区别,用>和2>分别捕获两种输出