C语言文件操作与随机访问:从基础到实践
在C语言编程中,文件操作是一项非常重要的技能。它允许我们将数据存储在文件中,以便后续使用或共享。本文将深入探讨C语言中的文件操作,包括基本的文件打开模式、随机文件访问以及一个实际的示例程序
dinoEdit
。
1. 基本文件打开模式
在C语言中,我们可以使用不同的模式打开文件。这些模式决定了我们可以对文件进行的操作,以及文件打开时的行为。以下是基本的文件打开模式及其规则:
| 模式 | 命名文件必须已存在 | 现有文件内容丢失 | 可读 | 可写 | 写入从文件末尾开始 |
| — | — | — | — | — | — |
| “r” | 是 | 否 | 是 | 否 | 否 |
| “w” | 否 | 是 | 否 | 是 | 否 |
| “a” | 否 | 否 | 否 | 是 | 是 |
| “r+” | 是 | 否 | 是 | 是 | 否 |
| “w+” | 否 | 是 | 是 | 是 | 否 |
| “a+” | 否 | 否 | 是 | 是 | 是 |
此外,C语言还允许在模式字符串末尾添加 “t” 或 “b” 来指定文件是以文本模式还是二进制模式打开。如果模式字符串中不包含 “t” 或 “b”,则需要查看开发环境文档以确定默认模式。
2. 随机文件访问
到目前为止,我们看到的文件操作示例都是将文件视为字节的顺序流。但在某些情况下,我们可能需要随机访问文件中的特定位置。随机文件访问允许我们将文件位置指示器移动到文件中的任意位置,从而在需要的地方进行读写操作。
例如,假设一个文件包含100个
long
类型的数据,每个
long
类型占4字节,文件总长度为400字节。如果我们想获取第10个
long
类型的数据,使用顺序访问模式,我们需要进行10次读取操作才能将第10个
long
类型的数据加载到内存中。而使用随机访问模式,我们可以先计算出第10个
long
类型数据在文件中的起始位置,然后直接跳转到该位置进行读取。
3. 随机访问函数
为了实现随机文件访问,我们需要了解一些有用的函数:
-
fseek()
:将文件位置指示器移动到指定的偏移位置。
int fseek( FILE *fp, long offset, int whence );
- `fp`:文件指针。
- `offset`:偏移量。
- `whence`:参考位置,可以是`SEEK_SET`(文件开头)、`SEEK_CUR`(当前位置)或`SEEK_END`(文件末尾)。
- ftell() :返回当前文件位置指示器的值。
long ftell( FILE *fp );
- rewind() :将文件位置指示器重置到文件开头。
void rewind( FILE *fp );
4.
dinoEdit
示例程序
dinoEdit
是一个简单的随机文件访问示例程序,它允许我们编辑存储在文件
MyDinos
中的恐龙名称。每个恐龙名称长度为20个字符,如果实际名称不足20个字符,则会在后面添加空格以达到20个字符的长度。
以下是
dinoEdit
程序的主要步骤:
1.
打开项目
:打开
Learn C Projects
文件夹,进入
10.03 - dinoEdit
文件夹,打开并运行
dinoEdit.xcodeproj
。
2.
选择恐龙编号
:程序会提示我们输入一个恐龙编号(范围从1到文件中恐龙名称的数量,输入0退出程序)。
3.
编辑恐龙名称
:输入编号后,程序会显示该编号对应的恐龙名称,并提示我们输入新的名称。输入新名称后,程序会将其写入文件,覆盖原来的名称。
4.
验证修改
:可以再次输入相同的编号,验证修改是否成功。
5.
dinoEdit
源代码解析
以下是
dinoEdit
程序的主要源代码文件及其功能:
dinoEdit.h
文件
/***********/
/* Defines */
/***********/
#define kDinoRecordSize 20
#define kMaxLineLength 100
#define kDinoFileName "../../My Dinos"
/********************************/
/* Function Prototypes - main.c */
/********************************/
int GetNumber( void );
int GetNumberOfDinos( void );
void ReadDinoName( int number, char *dinoName );
bool GetNewDinoName( char *dinoName );
void WriteDinoName( int number, char *dinoName );
void Flush( void );
void DoError( char *message );
该文件定义了一些常量和函数原型,用于后续的文件操作。
main.c
文件
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "dinoEdit.h"
/********************************> main <*/
int main( void ) {
int number;
char dinoName[ kDinoRecordSize+1 ];
while ( (number = GetNumber() ) != 0 ) {
ReadDinoName( number, dinoName );
printf( "Dino #%d: %s\n", number, dinoName );
if ( GetNewDinoName( dinoName ) )
WriteDinoName( number, dinoName );
}
printf( "Goodbye..." );
return 0;
}
main
函数是程序的入口,它通过一个循环不断提示用户输入恐龙编号,并根据用户的选择进行相应的操作。
其他函数
- GetNumber() :提示用户输入一个有效的恐龙编号。
/***************************> GetNumber <*/
int GetNumber( void ) {
int number, numDinos;
numDinos = GetNumberOfDinos();
do {
printf( "Enter number from 1 to %d (0 to exit): ", numDinos );
scanf( "%d", &number );
Flush();
}
while ( (number < 0) || (number > numDinos));
return( number );
}
- GetNumberOfDinos() :计算文件中恐龙记录的数量。
/*********************> GetNumberOfDinos <*/
int GetNumberOfDinos( void ) {
FILE *fp;
long fileLength;
if ( (fp = fopen( kDinoFileName, "r" )) == NULL )
DoError( "Couldn't open file...Goodbye!" );
if ( fseek( fp, 0L, SEEK_END ) != 0 )
DoError( "Couldn't seek to end of file...Goodbye!" );
if ( (fileLength = ftell( fp )) == -1L )
DoError( "ftell() failed...Goodbye!" );
fclose( fp );
return( (int)(fileLength / kDinoRecordSize) );
}
- ReadDinoName() :读取指定编号的恐龙名称。
/************************> ReadDinoName <*/
void ReadDinoName( int number, char *dinoName ) {
FILE *fp;
long bytesToSkip;
if ( (fp = fopen( kDinoFileName, "r" )) == NULL )
DoError( "Couldn't open file...Goodbye!" );
bytesToSkip = (long)((number- 1) * kDinoRecordSize);
if ( fseek( fp, bytesToSkip, SEEK_SET ) != 0 )
DoError( "Couldn't seek in file...Goodbye!" );
if ( fread( dinoName, (size_t)kDinoRecordSize,
(size_t)1, fp ) != 1 )
DoError( "Bad fread()...Goodbye!" );
fclose( fp );
}
- GetNewDinoName() :获取用户输入的新恐龙名称。
/**********************> GetNewDinoName <*/
bool GetNewDinoName( char *dinoName ) {
char line[ kMaxLineLength ];
int i, nameLen;
printf( "Enter new name: " );
if ( fgets( line, kMaxLineLength, stdin ) == NULL )
DoError( "Bad fgets()...Goodbye!" );
line[ strlen( line ) - 1 ] = '\0';
for ( i=0; i<kDinoRecordSize; i++ )
dinoName[i] = ' ';
nameLen = strlen( line );
if ( nameLen > kDinoRecordSize )
nameLen = kDinoRecordSize;
for ( i=0; i<nameLen; i++ )
dinoName[i] = line[i];
return true;
}
- WriteDinoName() :将新的恐龙名称写入文件。
/************************> WriteDinoName <*/
void WriteDinoName( int number, char *dinoName ) {
FILE *fp;
long bytesToSkip;
if ( (fp = fopen( kDinoFileName, "r+" )) == NULL )
DoError( "Couldn't open file...Goodbye!" );
bytesToSkip = (long)((number- 1) * kDinoRecordSize);
if ( fseek( fp, bytesToSkip, SEEK_SET ) != 0 )
DoError( "Couldn't seek in file...Goodbye!" );
if ( fwrite( dinoName, (size_t)kDinoRecordSize,
(size_t)1, fp ) != 1 )
DoError( "Bad fwrite()...Goodbye!" );
fclose( fp );
}
- Flush() :清除输入缓冲区。
/*******************************> Flush <*/
void Flush( void ) {
while ( getchar() != '\n' )
;
}
- DoError() :处理错误并退出程序。
/*****************************> DoError <*/
void DoError( char *message ) {
printf( "%s\n", message );
exit( 0 );
}
通过以上代码,我们可以看到
dinoEdit
程序是如何实现随机文件访问的。它通过
fseek()
函数将文件位置指示器移动到指定位置,然后使用
fread()
和
fwrite()
函数进行读写操作。
6. 总结
本文介绍了C语言中的基本文件打开模式、随机文件访问以及一个实际的示例程序
dinoEdit
。通过学习这些内容,我们可以更好地掌握C语言中的文件操作技能,提高程序的效率和灵活性。
7. 练习
为了巩固所学知识,我们可以尝试完成以下练习:
1. 找出以下代码片段中的错误:
// a.
FILE *fp;
fp = fopen( "w", "My Data File" );
if ( fp != NULL )
printf( "The file is open." );
// b.
char myData = 7;
FILE *fp;
fp = fopen( "r", "My Data File" );
fscanf( "Here's a number: %d", &myData );
// c.
FILE *fp;
char *line;
fp = fopen( "My Data File", "r" );
fscanf( fp, "%s", &line );
// d.
FILE *fp;
char line[100];
fp = fopen( "My Data File", "w" );
fscanf( fp, "%s", line );
-
编写一个程序,读取并打印具有以下格式的文件:
-
文件的第一行包含一个整数
x。 -
后续所有行包含
x个由制表符分隔的整数。 - 持续读取并打印行,直到文件结束。
-
文件的第一行包含一个整数
-
修改
dvdFiler程序,使其在读取标题和注释行时动态分配内存。首先,需要将DVDInfo结构体声明修改如下:
struct DVDInfo {
char rating;
char *title;
char *comment;
struct DVDInfo *next;
};
不仅要使用
malloc()
分配
DVDInfo
结构体,还要使用
malloc()
分配标题和注释字符串的空间,并确保为每个字符串的末尾留出足够的空间用于终止符。
通过完成这些练习,我们可以进一步加深对C语言文件操作的理解和掌握。
C语言文件操作与随机访问:从基础到实践
8. 代码错误分析
代码片段a
FILE *fp;
fp = fopen( "w", "My Data File" );
if ( fp != NULL )
printf( "The file is open." );
错误在于
fopen
函数的参数顺序错误。
fopen
函数的原型是
FILE *fopen(const char *filename, const char *mode);
,第一个参数应该是文件名,第二个参数才是打开模式。正确的代码应该是:
FILE *fp;
fp = fopen( "My Data File", "w" );
if ( fp != NULL )
printf( "The file is open." );
代码片段b
char myData = 7;
FILE *fp;
fp = fopen( "r", "My Data File" );
fscanf( "Here's a number: %d", &myData );
有两个错误。首先,
fopen
函数参数顺序错误,应将文件名和打开模式位置互换。其次,
fscanf
函数使用错误,
fscanf
的第一个参数应该是文件指针,而不是格式字符串。正确的代码如下:
char myData = 7;
FILE *fp;
fp = fopen( "My Data File", "r" );
if (fp != NULL) {
fscanf( fp, "%d", &myData );
fclose(fp);
}
代码片段c
FILE *fp;
char *line;
fp = fopen( "My Data File", "r" );
fscanf( fp, "%s", &line );
错误在于
&line
。
fscanf
期望的是一个指向字符数组的指针,而
&line
是指向指针的指针。并且
line
没有分配内存。正确的做法是先分配内存,然后传递
line
本身。代码如下:
FILE *fp;
char *line = (char *)malloc(100 * sizeof(char)); // 假设分配100字节
if (line == NULL) {
// 处理内存分配失败
return;
}
fp = fopen( "My Data File", "r" );
if (fp != NULL) {
fscanf( fp, "%s", line );
fclose(fp);
}
free(line);
代码片段d
FILE *fp;
char line[100];
fp = fopen( "My Data File", "w" );
fscanf( fp, "%s", line );
错误在于使用
w
模式打开文件,
w
模式会清空文件内容并用于写入,不能用于读取。应该使用
r
模式。正确代码为:
FILE *fp;
char line[100];
fp = fopen( "My Data File", "r" );
if (fp != NULL) {
fscanf( fp, "%s", line );
fclose(fp);
}
9. 读取特定格式文件的程序实现
以下是一个读取并打印具有特定格式文件的程序:
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
int x;
int num;
fp = fopen("input.txt", "r");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
// 读取第一行的整数x
if (fscanf(fp, "%d", &x) != 1) {
printf("读取第一行整数失败\n");
fclose(fp);
return 1;
}
// 持续读取后续行
while (fscanf(fp, "%d", &num) == 1) {
printf("%d", num);
for (int i = 1; i < x; i++) {
if (fscanf(fp, "%d", &num) == 1) {
printf("\t%d", num);
} else {
printf("\n读取行数据不完整\n");
break;
}
}
printf("\n");
}
fclose(fp);
return 0;
}
该程序的流程如下:
1. 打开文件。
2. 读取第一行的整数
x
。
3. 持续读取后续行,每行读取
x
个整数并打印。
4. 关闭文件。
10. 修改
dvdFiler
程序以动态分配内存
以下是修改
dvdFiler
程序的步骤和代码示例:
首先,修改
DVDInfo
结构体声明:
struct DVDInfo {
char rating;
char *title;
char *comment;
struct DVDInfo *next;
};
然后,在读取数据时动态分配内存:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct DVDInfo {
char rating;
char *title;
char *comment;
struct DVDInfo *next;
};
struct DVDInfo* createDVDInfo() {
struct DVDInfo *dvd = (struct DVDInfo *)malloc(sizeof(struct DVDInfo));
if (dvd == NULL) {
printf("内存分配失败\n");
return NULL;
}
dvd->rating = ' ';
dvd->title = NULL;
dvd->comment = NULL;
dvd->next = NULL;
return dvd;
}
void freeDVDInfo(struct DVDInfo *dvd) {
if (dvd != NULL) {
if (dvd->title != NULL) {
free(dvd->title);
}
if (dvd->comment != NULL) {
free(dvd->comment);
}
free(dvd);
}
}
int main() {
struct DVDInfo *dvd = createDVDInfo();
if (dvd == NULL) {
return 1;
}
// 假设从标准输入读取数据
printf("请输入评级: ");
scanf(" %c", &(dvd->rating));
char buffer[100];
printf("请输入标题: ");
scanf(" %99[^\n]", buffer);
dvd->title = (char *)malloc((strlen(buffer) + 1) * sizeof(char));
if (dvd->title == NULL) {
printf("标题内存分配失败\n");
freeDVDInfo(dvd);
return 1;
}
strcpy(dvd->title, buffer);
printf("请输入注释: ");
scanf(" %99[^\n]", buffer);
dvd->comment = (char *)malloc((strlen(buffer) + 1) * sizeof(char));
if (dvd->comment == NULL) {
printf("注释内存分配失败\n");
freeDVDInfo(dvd);
return 1;
}
strcpy(dvd->comment, buffer);
// 打印信息
printf("评级: %c\n", dvd->rating);
printf("标题: %s\n", dvd->title);
printf("注释: %s\n", dvd->comment);
// 释放内存
freeDVDInfo(dvd);
return 0;
}
该程序的流程如下:
1. 定义
DVDInfo
结构体并创建动态分配内存的函数。
2. 在
main
函数中创建
DVDInfo
结构体实例。
3. 从标准输入读取评级、标题和注释。
4. 为标题和注释动态分配内存并复制数据。
5. 打印信息。
6. 释放分配的内存。
11. 总结与展望
通过本文的学习,我们深入了解了C语言中的文件操作,包括基本的文件打开模式、随机文件访问以及相关函数的使用。同时,通过
dinoEdit
示例程序和练习,我们进一步巩固了所学知识。
在实际编程中,文件操作是非常常见的需求,掌握好这些技能可以让我们更好地处理数据的存储和读取。未来,我们可以进一步探索更复杂的文件操作,如多线程文件读写、文件加密等,以满足不同的应用场景。
希望本文能帮助你提升C语言文件操作的能力,让你在编程的道路上更进一步。
超级会员免费看
1354

被折叠的 条评论
为什么被折叠?



