1. C语言输入输出基础概念
在C语言编程中,输入输出(I/O)是与用户交互的基础手段。几乎所有实用程序都需要从外部获取数据(输入)或将处理结果展示给用户(输出)。C语言通过标准库<stdio.h>提供了一套完整的I/O函数体系,这些函数在不同操作系统上都能保持一致的接口,极大提高了代码的可移植性。
注意:虽然现代编程语言提供了更高级的I/O抽象,但理解C语言的I/O机制对于掌握计算机底层工作原理至关重要。
C语言的I/O系统基于"流"(stream)的概念设计。流可以看作是字节序列的抽象,程序通过流与各种I/O设备(如键盘、显示器、文件等)进行通信。标准库预定义了三个流:
- stdin:标准输入流(通常对应键盘)
- stdout:标准输出流(通常对应显示器)
- stderr:标准错误流(通常也输出到显示器)
2. 格式化输出函数详解
2.1 printf函数基础用法
printf是C语言中最常用的输出函数,其基本语法为:
c复制int printf(const char *format, ...);
一个简单示例:
c复制#include <stdio.h>
int main() {
int age = 25;
printf("我今年%d岁\n", age);
return 0;
}
printf的格式字符串中可以包含:
- 普通字符:原样输出
- 转义序列:如\n(换行)、\t(制表符)
- 格式说明符:以%开头,指定后续参数的输出格式
2.2 常用格式说明符
| 说明符 | 适用类型 | 说明 |
|---|---|---|
| %d | int | 十进制整数 |
| %f | float/double | 浮点数 |
| %c | char | 单个字符 |
| %s | char* | 字符串 |
| %p | void* | 指针地址 |
| %x | unsigned int | 十六进制整数(小写) |
| %X | unsigned int | 十六进制整数(大写) |
| %% | - | 输出%字符本身 |
2.3 格式修饰符进阶
格式说明符可以配合修饰符实现更精确的控制:
c复制printf("%10d", 123); // 输出宽度为10,右对齐
printf("%-10d", 123); // 输出宽度为10,左对齐
printf("%010d", 123); // 输出宽度为10,不足补0
printf("%.2f", 3.1415); // 保留2位小数
printf("%*d", 5, 10); // 动态指定宽度为5
提示:printf返回值是成功输出的字符数,可用于错误检查。
3. 格式化输入函数详解
3.1 scanf函数基础用法
scanf是C语言中最常用的输入函数,其基本语法为:
c复制int scanf(const char *format, ...);
简单示例:
c复制#include <stdio.h>
int main() {
int age;
printf("请输入您的年龄:");
scanf("%d", &age);
printf("您输入的年龄是:%d\n", age);
return 0;
}
3.2 scanf常见问题与解决方案
- 缓冲区问题:
c复制char ch;
scanf("%c", &ch); // 可能读取到之前输入留下的换行符
解决方案:
c复制scanf(" %c", &ch); // 注意%c前的空格可以跳过空白字符
- 输入类型不匹配:
c复制int num;
scanf("%d", &num); // 如果用户输入非数字,会导致问题
解决方案:
c复制if(scanf("%d", &num) != 1) {
printf("输入无效\n");
while(getchar() != '\n'); // 清空输入缓冲区
}
- 字符串输入安全:
c复制char name[20];
scanf("%s", name); // 可能导致缓冲区溢出
更安全的替代方案:
c复制fgets(name, sizeof(name), stdin);
name[strcspn(name, "\n")] = '\0'; // 移除末尾的换行符
3.3 scanf高级用法
- 指定输入长度:
c复制char str[10];
scanf("%9s", str); // 最多读取9个字符,留1位给'\0'
- 跳过特定输入:
c复制int day, month, year;
scanf("%d/%d/%d", &day, &month, &year); // 输入格式:31/12/2023
- 扫描集:
c复制char vowels[10];
scanf("%[aeiou]", vowels); // 只读取元音字母
4. 字符和字符串I/O函数
4.1 字符I/O函数
- getchar() / putchar()
c复制int getchar(void); // 从stdin读取一个字符
int putchar(int c); // 向stdout输出一个字符
示例:
c复制#include <stdio.h>
int main() {
int c;
printf("请输入一个字符:");
c = getchar();
printf("您输入的字符是:");
putchar(c);
putchar('\n');
return 0;
}
- getc() / putc()
c复制int getc(FILE *stream); // 从指定流读取一个字符
int putc(int c, FILE *stream); // 向指定流输出一个字符
4.2 字符串I/O函数
- gets() / puts()
c复制char *gets(char *s); // 从stdin读取一行(已废弃,不安全)
int puts(const char *s); // 向stdout输出字符串并追加换行
- fgets() / fputs()
c复制char *fgets(char *s, int size, FILE *stream); // 安全读取一行
int fputs(const char *s, FILE *stream); // 输出字符串不追加换行
安全字符串输入示例:
c复制#include <stdio.h>
#include <string.h>
#define MAX_LEN 100
int main() {
char buffer[MAX_LEN];
printf("请输入字符串:");
fgets(buffer, MAX_LEN, stdin);
// 移除可能的换行符
buffer[strcspn(buffer, "\n")] = '\0';
printf("您输入的字符串是:");
puts(buffer);
return 0;
}
5. 文件输入输出操作
5.1 文件打开与关闭
c复制FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
常用文件打开模式:
| 模式 | 描述 |
|---|---|
| "r" | 只读方式打开文本文件 |
| "w" | 只写方式创建/清空文本文件 |
| "a" | 追加方式打开/创建文本文件 |
| "rb" | 只读方式打开二进制文件 |
| "wb" | 只写方式创建/清空二进制文件 |
| "ab" | 追加方式打开/创建二进制文件 |
| "r+" | 读写方式打开文本文件 |
| "w+" | 读写方式创建/清空文本文件 |
| "a+" | 读写方式打开/追加文本文件 |
5.2 文件读写操作
- 格式化文件I/O:
c复制int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
- 字符文件I/O:
c复制int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
- 字符串文件I/O:
c复制char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
- 二进制文件I/O:
c复制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);
文件复制示例:
c复制#include <stdio.h>
int main() {
FILE *src, *dest;
char buffer[1024];
size_t bytes;
src = fopen("source.txt", "rb");
if(!src) {
perror("无法打开源文件");
return 1;
}
dest = fopen("destination.txt", "wb");
if(!dest) {
perror("无法创建目标文件");
fclose(src);
return 1;
}
while((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0) {
fwrite(buffer, 1, bytes, dest);
}
fclose(src);
fclose(dest);
printf("文件复制完成\n");
return 0;
}
5.3 文件定位函数
c复制int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
whence参数取值:
- SEEK_SET:从文件开头计算偏移
- SEEK_CUR:从当前位置计算偏移
- SEEK_END:从文件末尾计算偏移
6. 错误处理与调试技巧
6.1 错误检测函数
- feof() / ferror()
c复制int feof(FILE *stream); // 检测是否到达文件末尾
int ferror(FILE *stream); // 检测文件操作是否出错
- perror()
c复制void perror(const char *s); // 输出最近一次系统错误信息
6.2 常见错误及解决方法
- 文件打开失败
c复制FILE *fp = fopen("nonexistent.txt", "r");
if(fp == NULL) {
perror("文件打开失败");
// 处理错误
}
- 格式化输入不匹配
c复制int num;
if(scanf("%d", &num) != 1) {
printf("输入必须是整数\n");
// 清空输入缓冲区
while(getchar() != '\n');
}
- 缓冲区溢出
c复制char name[10];
// 不安全
scanf("%s", name);
// 安全替代
fgets(name, sizeof(name), stdin);
- 文件读写错误
c复制if(fwrite(buffer, 1, size, fp) != size) {
if(ferror(fp)) {
perror("写入文件时出错");
}
}
6.3 调试技巧
- 打印调试信息:
c复制#define DEBUG 1
#if DEBUG
printf("调试信息:变量x的值=%d\n", x);
#endif
- 使用assert:
c复制#include <assert.h>
FILE *fp = fopen("data.txt", "r");
assert(fp != NULL && "无法打开文件");
- 检查返回值:
c复制int items = scanf("%d %f", &num, &value);
if(items != 2) {
fprintf(stderr, "错误:需要输入两个值\n");
}
7. 性能优化与高级话题
7.1 缓冲机制
C标准库的I/O函数使用缓冲机制提高性能。缓冲类型包括:
- 全缓冲:缓冲区满时刷新(通常用于文件)
- 行缓冲:遇到换行符时刷新(通常用于终端)
- 无缓冲:立即输出(如stderr)
手动刷新缓冲区:
c复制fflush(FILE *stream); // 刷新指定流的缓冲区
setbuf(FILE *stream, char *buf); // 设置自定义缓冲区
setvbuf(FILE *stream, char *buf, int mode, size_t size); // 更灵活的设置
7.2 二进制与文本模式区别
在Windows系统中,文本模式会对换行符进行转换(\n <=> \r\n),而二进制模式不会。这可能导致:
- 文本文件在不同系统间移植性问题
- 二进制文件读写时数据损坏
解决方案:
- 跨平台文本文件统一使用\n作为换行符
- 二进制文件始终使用二进制模式打开
7.3 自定义流处理
通过freopen可以重定向标准流:
c复制FILE *freopen(const char *filename, const char *mode, FILE *stream);
示例:将stdout重定向到文件
c复制freopen("output.txt", "w", stdout);
printf("这行文字将写入文件\n");
freopen("/dev/tty", "w", stdout); // Linux下恢复控制台输出
7.4 临时文件处理
c复制FILE *tmpfile(void); // 创建临时文件(程序结束自动删除)
char *tmpnam(char *s); // 生成唯一临时文件名(不安全)
char *mkstemp(char *template); // 创建唯一临时文件(更安全)
安全临时文件示例:
c复制char template[] = "/tmp/mytempXXXXXX";
int fd = mkstemp(template);
if(fd != -1) {
FILE *fp = fdopen(fd, "w+");
// 使用临时文件...
fclose(fp);
} else {
perror("无法创建临时文件");
}
8. 实际应用案例
8.1 简单文本处理程序
c复制#include <stdio.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
if(argc != 3) {
fprintf(stderr, "用法:%s 输入文件 输出文件\n", argv[0]);
return 1;
}
FILE *in = fopen(argv[1], "r");
if(!in) {
perror("无法打开输入文件");
return 1;
}
FILE *out = fopen(argv[2], "w");
if(!out) {
perror("无法创建输出文件");
fclose(in);
return 1;
}
int ch;
while((ch = fgetc(in)) != EOF) {
fputc(toupper(ch), out);
}
fclose(in);
fclose(out);
printf("文件处理完成\n");
return 0;
}
8.2 学生成绩管理系统
c复制#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
#define NAME_LEN 50
typedef struct {
char name[NAME_LEN];
int id;
float score;
} Student;
void saveStudents(Student students[], int count, const char *filename) {
FILE *fp = fopen(filename, "wb");
if(!fp) {
perror("无法打开文件保存数据");
return;
}
fwrite(&count, sizeof(int), 1, fp);
fwrite(students, sizeof(Student), count, fp);
fclose(fp);
}
int loadStudents(Student students[], const char *filename) {
FILE *fp = fopen(filename, "rb");
if(!fp) {
perror("无法打开文件读取数据");
return 0;
}
int count;
fread(&count, sizeof(int), 1, fp);
fread(students, sizeof(Student), count, fp);
fclose(fp);
return count;
}
int main() {
Student students[MAX_STUDENTS];
int count = 0;
char filename[] = "students.dat";
// 示例:添加几个学生
strcpy(students[0].name, "张三");
students[0].id = 1001;
students[0].score = 85.5;
strcpy(students[1].name, "李四");
students[1].id = 1002;
students[1].score = 92.0;
count = 2;
// 保存到文件
saveStudents(students, count, filename);
// 从文件加载
Student loaded[MAX_STUDENTS];
int loadedCount = loadStudents(loaded, filename);
// 显示加载的数据
printf("加载的学生数据:\n");
for(int i = 0; i < loadedCount; i++) {
printf("%d: %s (ID:%d) 成绩: %.1f\n",
i+1, loaded[i].name, loaded[i].id, loaded[i].score);
}
return 0;
}
8.3 日志系统实现
c复制#include <stdio.h>
#include <time.h>
#include <stdarg.h>
void logMessage(const char *format, ...) {
FILE *logFile = fopen("app.log", "a");
if(!logFile) return;
time_t now;
time(&now);
struct tm *local = localtime(&now);
fprintf(logFile, "[%04d-%02d-%02d %02d:%02d:%02d] ",
local->tm_year + 1900, local->tm_mon + 1, local->tm_mday,
local->tm_hour, local->tm_min, local->tm_sec);
va_list args;
va_start(args, format);
vfprintf(logFile, format, args);
va_end(args);
fputc('\n', logFile);
fclose(logFile);
}
int main() {
logMessage("程序启动");
int x = 10;
logMessage("变量x的初始值: %d", x);
for(int i = 0; i < 3; i++) {
logMessage("循环迭代 %d", i);
}
logMessage("程序退出");
return 0;
}
9. 跨平台注意事项
9.1 文件路径差异
不同操作系统的文件路径表示:
- Windows:
C:\\path\\to\\file或C:/path/to/file - Unix/Linux:
/path/to/file
可移植的路径处理方法:
c复制#ifdef _WIN32
const char *path = "data\\file.txt";
#else
const char *path = "data/file.txt";
#endif
或者统一使用正斜杠(/)(在Windows上也有效):
c复制const char *path = "data/file.txt";
9.2 换行符差异
- Windows:
\r\n - Unix/Linux:
\n - Mac OS(旧版):
\r
处理建议:
- 文本文件统一使用
\n作为换行符 - 二进制文件避免使用文本模式函数
9.3 编码问题
常见文本编码:
- ASCII:基本字符集
- UTF-8:Unicode可变长度编码
- GBK:中文编码
处理多字节字符:
c复制#include <locale.h>
setlocale(LC_ALL, ""); // 使用系统默认locale
wprintf(L"中文测试\n"); // 宽字符输出
9.4 文件权限
Unix/Linux系统需要注意文件权限:
c复制// 创建只有用户可读写的文件
FILE *fp = fopen("private.txt", "w");
if(fp) {
fchmod(fileno(fp), 0600); // 设置权限
// 文件操作...
fclose(fp);
}
10. 现代替代方案
10.1 使用更安全的函数
标准C库提供了一些更安全的替代函数:
c复制int snprintf(char *str, size_t size, const char *format, ...);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
10.2 第三方库
- GLib:提供更丰富的I/O功能
- APR(Apache Portable Runtime):跨平台I/O抽象
- Boost.IO:C++库,但可与C交互
10.3 异步I/O
现代操作系统提供的异步I/O接口:
- Linux: aio_read/aio_write
- Windows: OVERLAPPED I/O
10.4 内存映射文件
c复制#include <sys/mman.h>
#include <fcntl.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
内存映射文件示例:
c复制int fd = open("data.bin", O_RDONLY);
if(fd == -1) {
perror("无法打开文件");
return 1;
}
struct stat sb;
if(fstat(fd, &sb) == -1) {
perror("无法获取文件大小");
close(fd);
return 1;
}
void *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(addr == MAP_FAILED) {
perror("内存映射失败");
close(fd);
return 1;
}
// 使用映射的内存区域...
munmap(addr, sb.st_size);
close(fd);
