24、C语言文件操作与随机访问:从基础到实践

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 );
  1. 编写一个程序,读取并打印具有以下格式的文件:
    • 文件的第一行包含一个整数 x
    • 后续所有行包含 x 个由制表符分隔的整数。
    • 持续读取并打印行,直到文件结束。
  2. 修改 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语言文件操作的能力,让你在编程的道路上更进一步。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值