24、输入输出
输入与输出(I/O)操作是程序与外界交互的基本方式。C语言提供了一系列函数来处理不同类型的输入与输出需求,使得程序能够读取用户输入的数据,以及将处理结果输出到屏幕或文件中。
标准输入输出函数
- printf()函数
printf()
函数是C语言中最常用的输出函数之一,用于向标准输出设备(通常是屏幕)输出格式化的字符串。
其原型为:
int printf(const char *format, ...);
format
:一个字符串,包含了普通字符和格式说明符。格式说明符用于指定如何格式化后续的参数。...
:可变参数列表,表示可以传递任意数量的参数,这些参数将根据format
中的格式说明符进行格式化输出。
示例代码:
#include <stdio.h>
int main() {
int num = 10;
float pi = 3.14159;
char ch = 'A';
char str[] = "Hello, World!";
printf("整数:%d\n", num);
printf("浮点数:%.2f\n", pi); // 输出两位小数
printf("字符:%c\n", ch);
printf("字符串:%s\n", str);
return 0;
}
输出:
整数:10
浮点数:3.14
字符:A
字符串:Hello, World!
- scanf()函数
scanf()
函数用于从标准输入设备(通常是键盘)读取格式化的输入。其原型为:
int scanf(const char *format, ...);
format
:一个字符串,包含了格式说明符,用于指定如何解析输入的数据。...
:可变参数列表,表示接收输入数据的变量。
示例代码:
#include <stdio.h>
int main() {
int num;
float pi;
char ch;
char str[100];
printf("请输入一个整数:");
scanf("%d", &num);
printf("请输入一个浮点数:");
scanf("%f", &pi);
printf("请输入一个字符:");
scanf(" %c", &ch); // 注意前面的空格,用于消耗可能存在的换行符
printf("请输入一个字符串:");
scanf("%99s", str); // %99s限制输入长度,防止缓冲区溢出
printf("你输入的整数是:%d\n", num);
printf("你输入的浮点数是:%.2f\n", pi);
printf("你输入的字符是:%c\n", ch);
printf("你输入的字符串是:%s\n", str);
return 0;
}
注意:在使用scanf()
读取字符时,如果之前输入了其他类型的数据并按下了回车键,回车符会被留在输入缓冲区中,导致scanf("%c", &ch);
直接读取到回车符。为了解决这个问题,可以在%c
前加一个空格,即scanf(" %c", &ch);
,这样空格会消耗掉缓冲区中的空白字符(包括空格、制表符和换行符)。
字符输入输出函数
- getchar()和putchar()函数
getchar()
函数用于从标准输入读取一个字符,putchar()
函数用于将一个字符输出到标准输出。它们的原型分别为:
int getchar(void);
int putchar(int c);
getchar()
不需要参数,返回读取到的字符(以整数形式表示)。putchar()
接受一个整数参数(通常是一个字符的ASCII码),并将其输出为字符。
示例代码:
#include <stdio.h>
int main() {
char ch;
printf("请输入一个字符:");
ch = getchar();
putchar(ch);
putchar('\n'); // 输出换行符
return 0;
}
- gets()和puts()函数(不推荐使用)
gets()
函数用于从标准输入读取一行字符串,直到遇到换行符或文件结束符(EOF)。puts()
函数用于将一个字符串输出到标准输出,并在末尾自动添加一个换行符。它们的原型分别为:
char *gets(char *s);
int puts(const char *s);
gets()
函数由于不检查缓冲区大小,容易导致缓冲区溢出,因此不推荐使用。建议使用fgets()
代替。puts()
函数接受一个字符串参数,并将其输出到标准输出,同时在末尾添加换行符。
注意:由于gets()
函数的安全性问题,现代C编程中通常使用fgets()
来替代它。fgets()
允许指定缓冲区的大小,从而避免了缓冲区溢出的风险。
文件输入输出函数
C语言提供了丰富的文件输入输出函数,使程序能够读取和写入文件。
主要有fopen()
、fclose()
、fread()
、fwrite()
、fprintf()
、fscanf()
等。
fopen()和fclose()函数
fopen()
函数用于打开文件,fclose()
函数用于关闭文件。它们的原型分别为:
FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
fopen()
函数接受文件名和打开模式作为参数,返回一个FILE
指针用于后续的文件操作。如果打开失败,返回NULL
。fclose()
函数接受一个FILE
指针作为参数,用于关闭打开的文件。如果关闭成功,返回0;否则返回EOF。
打开模式包括:
"r"
:只读模式,打开已存在的文件。"w"
:只写模式,打开文件用于写入。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。"a"
:追加模式,打开文件用于在文件末尾追加数据。如果文件不存在,则创建新文件。"r+"
:读写模式,打开已存在的文件用于读写。"w+"
:读写模式,打开文件用于读写。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。"a+"
:读写模式,打开文件用于在文件末尾追加数据,同时允许读取文件内容。
fread()和fwrite()函数
fread()
和fwrite()
函数分别用于从文件和向文件读写二进制数据。它们的原型分别为:
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);
fread()
函数从stream
指向的文件中读取nmemb
个元素,每个元素大小为size
字节,并将读取的数据存储到ptr
指向的内存中。返回成功读取的元素个数。fwrite()
函数将ptr
指向的内存中的nmemb
个元素写入到stream
指向的文件中,每个元素大小为size
字节。返回成功写入的元素个数。
下面是一段分段读取的示例代码:
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 10 // 每次读取的字节数
int main() {
FILE *file;
char buffer[BUFFER_SIZE];
size_t bytesRead;
// 打开文件用于读取
file = fopen("example.txt", "rb");
if (file == NULL) {
perror("打开文件失败");
return 1;
}
// 分段读取文件内容
while ((bytesRead = fread(buffer, sizeof(char), BUFFER_SIZE, file)) > 0) {
// 输出读取到的数据
fwrite(buffer, sizeof(char), bytesRead, stdout); // 将数据写到标准输出
}
// 检查是否发生读取错误
if (ferror(file)) {
perror("读取文件时发生错误");
}
// 关闭文件
fclose(file);
return 0;
}
代码说明:
- 定义缓冲区大小:
- 定义了一个宏
BUFFER_SIZE
,表示每次读取的字节数。在这个示例中,我们设置为 10 字节。
- 定义了一个宏
- 打开文件:
- 使用
fopen()
函数以二进制读取模式("rb")打开文件example.txt
。如果文件打开失败,输出错误信息并返回。
- 使用
- 分段读取文件内容:
- 使用
fread()
函数在循环中分段读取文件内容。每次读取BUFFER_SIZE
字节的数据到buffer
中。 fread()
返回实际读取的字节数,存储在bytesRead
中。
- 使用
- 输出读取到的数据:
- 使用
fwrite()
函数将读取到的数据写到标准输出(控制台)。这里我们直接将buffer
中的数据写到stdout
,这样可以看到文件的内容。
- 使用
- 检查读取错误:
- 在循环结束后,使用
ferror()
函数检查是否发生读取错误。如果有错误,输出错误信息。
- 在循环结束后,使用
- 关闭文件:
- 使用
fclose()
函数关闭文件。
- 使用
fprintf()和fscanf()函数
fprintf()
和fscanf()
函数分别用于向文件和从文件读写格式化数据。它们的原型与printf()
和scanf()
类似,只是多了一个FILE
指针参数用于指定文件。
示例代码:
#include <stdio.h>
int main() {
FILE *file;
int num = 123;
float pi = 3.14159;
char str[] = "Hello, File!";
// 打开文件用于写入
file = fopen("example.txt", "w");
if (file == NULL) {
perror("打开文件失败");
return 1;
}
// 向文件写入格式化数据
fprintf(file, "整数:%d\n", num);
fprintf(file, "浮点数:%.2f\n", pi);
fprintf(file, "字符串:%s\n", str);
// 关闭文件
fclose(file);
// 打开文件用于读取
file = fopen("example.txt", "r");
if (file == NULL) {
perror("打开文件失败");
return 1;
}
// 从文件读取格式化数据(这里只是演示,实际应用中需要更复杂的处理)
int read_num;
float read_pi;
char read_str[100];
fscanf(file, "整数:%d", &read_num);
fscanf(file, "浮点数:%f", &read_pi);
fscanf(file, "字符串:%s", read_str);
// 输出读取到的数据
printf("从文件读取的整数:%d\n", read_num);
printf("从文件读取的浮点数:%.2f\n", read_pi);
printf("从文件读取的字符串:%s\n", read_str);
// 关闭文件
fclose(file);
return 0;
}
首先使用fopen()
以写模式("w")打开一个名为example.txt
的文件,然后使用fprintf()
函数将格式化的数据写入文件。接着,我们关闭文件并重新以读模式("r")打开它,使用fscanf()
函数读取文件中的数据,并使用printf()
函数将读取到的数据输出到控制台。
文件定位函数
在文件操作中,有时需要定位到文件的特定位置进行读写。C语言提供了一系列文件定位函数,包括ftell()
、fseek()
和rewind()
。
ftell()函数
ftell()
函数用于获取当前文件位置指针的位置,即相对于文件开头的字节数。原型为:
long ftell(FILE *stream);
如果成功,返回当前文件位置指针的位置;如果失败,返回-1L。
fseek()函数
fseek()
函数用于设置文件位置指针的位置。原型为:
int fseek(FILE *stream, long offset, int whence);
stream
:文件指针。offset
:相对于whence
的偏移量,以字节为单位。whence
:起始位置,可以是以下值之一:SEEK_SET
:文件开头。SEEK_CUR
:当前文件位置。SEEK_END
:文件末尾。
如果成功,返回0;如果失败,返回非0值。
rewind()函数
rewind()
函数用于将文件位置指针重新定位到文件开头。原型为:
void rewind(FILE *stream);
它相当于调用fseek(stream, 0L, SEEK_SET);
。
示例代码:
#include <stdio.h>
int main() {
FILE *file;
char ch;
// 打开文件用于读写
file = fopen("example.txt", "r+");
if (file == NULL) {
perror("打开文件失败");
return 1;
}
// 读取并输出第一个字符
ch = fgetc(file);
printf("第一个字符:%c\n", ch);
// 定位到文件开头
rewind(file);
// 再次读取并输出第一个字符(应该与上次相同)
ch = fgetc(file);
printf("重新定位后的第一个字符:%c\n", ch);
// 定位到文件末尾前的一个字节(假设文件不为空)
fseek(file, -1L, SEEK_END);
// 读取并输出最后一个字符
ch = fgetc(file);
printf("最后一个字符:%c\n", ch);
// 关闭文件
fclose(file);
return 0;
}
首先使用fgetc()
函数读取并输出文件的第一个字符。然后,使用rewind()
函数将文件位置指针重新定位到文件开头,并再次读取并输出第一个字符。最后,使用fseek()
函数将文件位置指针定位到文件末尾前的一个字节,并读取并输出最后一个字符。
错误处理
在进行文件操作时,可能会遇到各种错误,如文件不存在、权限不足、磁盘空间不足等。为了处理这些错误,C语言提供了perror()
和ferror()
函数。
perror()函数
perror()
函数用于输出描述最近一次I/O操作错误的字符串。其原型为:
void perror(const char *s);
s
:用户自定义的错误信息字符串,可以为NULL
。如果为NULL
,则只输出系统提供的错误信息字符串。
示例代码:
#include <stdio.h>
int main() {
FILE *file;
// 尝试打开一个不存在的文件
file = fopen("nonexistent.txt", "r");
if (file == NULL) {
perror("打开文件失败");
}
return 0;
}
尝试打开一个不存在的文件,并使用perror()
函数输出错误信息。
ferror()函数
ferror()
函数用于检查文件流是否发生错误。其原型为:
int ferror(FILE *stream);
如果发生错误,返回非0值;否则返回0。
示例代码:
#include <stdio.h>
int main() {
FILE *file;
char ch;
// 打开文件用于读取
file = fopen("example.txt", "r");
if (file == NULL) {
perror("打开文件失败");
return 1;
}
// 尝试读取文件中的一个字符(假设文件不为空)
if (fgetc(file) == EOF) {
if (ferror(file)) {
perror("读取文件时发生错误");
} else {
printf("已到达文件末尾\n");
}
}
// 关闭文件
fclose(file);
return 0;
}
尝试从文件中读取一个字符,并检查是否发生错误。如果发生错误,我们使用perror()
函数输出错误信息。
扩展知识
缓冲区
C语言的标准I/O库使用了缓冲区来提高I/O操作的效率。缓冲区是一块内存区域,用于暂时存储输入或输出的数据。当程序执行输入或输出操作时,数据并不是立即从设备读取或写入设备,而是先存储到缓冲区中。当缓冲区满或程序显式地刷新缓冲区时,数据才会被实际地读取或写入设备。
C语言提供了三种类型的缓冲区:
- 全缓冲:只有在缓冲区满或调用
fflush()
函数时,才进行实际的I/O操作。通常用于文件流。 - 行缓冲:在遇到换行符或缓冲区满时,进行实际的I/O操作。通常用于终端(标准输入和标准输出)。
- 无缓冲:不进行缓冲,直接进行实际的I/O操作。通常用于标准错误输出。
可以使用setvbuf()
函数来设置缓冲区的类型和大小。
格式化字符串
在printf()
、scanf()
、fprintf()
和fscanf()
等函数中,格式化字符串用于指定如何格式化输出或解析输入的数据。格式化字符串可以包含普通字符和格式说明符。格式说明符以百分号(%
)开头,后面跟着一个或多个字符,用于指定数据类型、宽度、精度等。
常见的格式说明符包括:
%d
:输出十进制整数。%f
:输出浮点数。%c
:输出单个字符。%s
:输出字符串。%x
:输出十六进制整数。%%
:输出百分号本身。
此外,还可以在格式说明符之间添加修饰符,如宽度修饰符(如%5d
表示输出宽度为5的十进制整数)、精度修饰符(如%.2f
表示输出保留两位小数的浮点数)等。
文件结尾(EOF)
在文件操作中,EOF
(End Of File)是一个特殊的标记,表示文件的结尾。
当读取文件时,如果到达文件结尾,fgetc()
、fgets()
等函数会返回EOF
。可以使用feof()
函数来检查是否已到达文件结尾。
二进制文件与文本文件
C语言支持两种类型的文件:二进制文件和文本文件。二进制文件以二进制形式存储数据,不进行任何转换。文本文件则以文本形式存储数据,可能会根据系统的字符编码进行转换(如换行符的转换)。在打开文件时,可以通过指定打开模式("rb"、"wb"等)来选择以二进制模式或文本模式打开文件。
练习
编写一个程序,实现以下功能:
- 打开一个名为"example.txt"的文件,如果文件不存在则创建该文件。
- 向文件中写入字符串"Hello, World!\n"。
- 关闭文件。
- 重新打开该文件,以读取模式打开。
- 读取文件内容并打印到控制台。
- 再次关闭文件。
编写一个程序,完成以下任务:
- 定义一个结构体
Student
,包含姓名(字符数组)和年龄(整型)。 - 创建一个包含多个
Student
结构体的数组,并初始化。 - 将这些结构体数据写入到名为"students.dat"的二进制文件中。
- 从该文件中读取结构体数据,并打印到控制台。
- 定义一个结构体
编写一个程序,实现以下功能:
- 尝试打开一个名为"log.txt"的文件,以追加模式("a")打开。
- 向文件中追加当前日期和时间(可以使用标准库函数获取)。
- 如果文件打开失败,打印错误信息。
- 关闭文件。
进阶
- 封装一个方法函数,读取文件的所有内容到内存中,返回内存指针和文件内容的长度。
- 封装一个方法函数,输入参数为指针和长度和文件名,把内容写入到文件中,并
flush
。
