美文网首页c语言
[习题17]文件读写与结构体数组

[习题17]文件读写与结构体数组

作者: AkuRinbu | 来源:发表于2018-10-07 01:31 被阅读25次

使用教材

《“笨办法” 学C语言(Learn C The Hard Way)》
https://www.jianshu.com/p/b0631208a794

说明

  • EX17,书上是没有注释的, 这个练习要求就是自己去读懂每一个函数.

目录

零、完整源码
一、代码功能
二、运行调试
三、代码说明
  1、代码主要结构体与常量
  2、主函数的逻辑结构
  3、数据库:加载/关闭,程序终止
        die  
        Database_open 
        Database_close
 4、数据库:创建/查询/插入/删除/列出
    (1)创建 :$ ./ex17 db.dat c
    (2)查询:$ ./ex17 db.dat g 2
    (3)插入:$ ./ex17 db.dat s 1 zed zed@zedshaw.com
    (4)删除:$ ./ex17 db.dat d 3
    (5)列出:$ ./ex17 db.dat l

零、完整源码(教材作者的github

https://github.com/zedshaw/learn-c-the-hard-way-lectures/tree/master/ex17

一、代码功能

  • EX17,主要利用的是文件的打开、关闭与写入操作
  • 代码会创建文件,可以命名为db.dat,在磁盘上保存数据,不妨看做一个“静态时期的超迷你数据库”
  • 运行期间,通过一个固定大小的结构体数组,来模拟数据库的创建、查看、插入、删除等操作;
  • 具体表现为,每次运行都读取db.dat文件的全部数据结构数组里,按命令修改数组数据,再全部回写到db.dat文件;

二、运行调试

  • 0、action: c=create, g=get, s=set, d=del, l=list
  • 1、编译程序 ex17
$ make ex17
clang -Wall -g    ex17.c   -o ex17
  • 2、创建数据库 db.dat,并使用命令行选项s,新建三条信息
$ ./ex17 db.dat c
$ ./ex17 db.dat s 1 zed zed@zedshaw.com
$ ./ex17 db.dat s 2 frank frank@zedshaw.com
$ ./ex17 db.dat s 3 joe joe@zedshaw.com
  • 3、使用命令l(小写的字母l),列出当前数据库内的全部记录
$ ./ex17 db.dat l
1 zed zed@zedshaw.com
2 frank frank@zedshaw.com
3 joe joe@zedshaw.com
  • 4、使用命令行选项d,删除id=3的数据,并查看
$ ./ex17 db.dat d 3
$ ./ex17 db.dat l
1 zed zed@zedshaw.com
2 frank frank@zedshaw.com
  • 5、使用选项g,查看id=2的数据
$ ./ex17 db.dat g 2
2 frank frank@zedshaw.com
  • 6、各种错误提示信息输出
$ ./ex17 db.dat s 2 hello hello@world.com
ERROR: Already set, delete it first

$ ./ex17 db.dat g 
ERROR: Need an id to get

$ ./ex17 db.dat
ERROR: USAGE: ex17 <dbfile> <action> [action paprams]

$ ./ex17 db.dat a  b c d e f g
ERROR: Invalid action: c=create, g=get, s=set, d=del, l=list

$ ./ex17 db.dat g 10000
ERROR: There's not that many records.

$ ./ex17 db.dat s 
ERROR: Need id, name, email to set

$ ./ex17 db.dat d
ERROR: Need id to delete

三、代码说明

1、代码主要结构体与常量

  • struct Address rows[MAX_ROWS];是一个结构体数组,也就是说,数组的每一个元素都是一个Address类型的结构体
  • struct Connection结构体,包括两个指针,一个是文件指针file,另一个是指向Database类型的指针db
#define MAX_DATA 512
#define MAX_ROWS 100

struct Address {
    int id;
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

struct Database {
    struct Address rows[MAX_ROWS];
};

struct Connection {
    FILE *file;
    struct Database *db;
};

2、主函数的逻辑结构

  • main():主要用于解析命令行参数,以执行对应的操作;
  • 命令行参数的基本格式程序名 数据库名称 操作选项 操作参数
USAGE: ex17 <dbfile> <action> [action paprams]
action: c=create, g=get, s=set, d=del, l=list
        创建 ,用id获取, 用id设置, 用id删除, 显示全部
  • argv[0]程序名,在本次练习中就是ex17
  • argv[1]数据库文件名,自己命名,示例中都是db.dat
  • argv[2][0],表示一个字符char,是操作选项,可选的有c g s d l
  • argv[3] 指示数据的 id值

int atoi( const char *str );
if(argc > 3) id = atoi(argv[3]);
https://en.cppreference.com/w/c/string/byte/atoi
字符串转为int值

3、数据库:加载/关闭,程序终止

die :终止程序并输出错误信息

void die(const char *message) 
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }

    exit(1);
}
  • exit():终止程序

https://en.cppreference.com/w/c/program/exit

  • errno 以及 perror

errno is a preprocessor macro that expands to a thread-local (since C11) modifiable lvalue of type int.
https://en.cppreference.com/w/c/error/errno

Prints a textual description of the error code currently stored in the system variable errno to stderr.
https://en.cppreference.com/w/c/io/perror

Database_open

In main: struct Connection *conn = Database_open(filename, action);

struct Connection *Database_open(const char *filename, char mode) 
{
    struct Connection *conn = malloc(sizeof(struct Connection));
    if(!conn)
        die("Memory error");

    conn->db = malloc(sizeof(struct Database));
    if(!conn->db) 
        die("Memory error");

    if(mode == 'c') {
        conn->file = fopen(filename, "w");
    } else {
        conn->file = fopen(filename, "r+");

        if(conn->file) {
            Database_load(conn);
        }
    }

    if(!conn->file) 
        die("Failed to open the file");

    return conn;
}
  • 使用malloc,给结构体Database以及Connection分配内存;

  • 按照指定模式mode,创建或者打开数据库文件
    FILE *fopen( const char *filename, const char *mode );

    fopen

https://en.cppreference.com/w/c/io/fopen

r+模式打开数据库文件时,继续调用Database_load

size_t fread ( void *buffer, size_t size, size_t count, FILE *stream );
参数
buffer - 指向要读取的数组中首个对象的指针
size - 每个对象的字节大小
count - 要读取的对象数
stream - 读取来源的输入文件流
从给定输入流 stream 读取count 个对象到数组 buffer 中

https://en.cppreference.com/w/c/io/fread

  • 函数功能:从打开的数据库文件conn->file中,读取1
    struct database对象到数组 conn->db
void Database_load(struct Connection *conn)
{
    int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc!=1)
        die("Failed to load Database");
}
  • buffer : conn->db来源于Database_openconn->db = malloc(sizeof(struct Database));,意思是刚刚有一块热乎的内存分了出来;
  • size:sizeof(struct Database)
  • count :1
struct Database {
    struct Address rows[MAX_ROWS];
};
  • stream :conn->file,来源于Database_openconn->file = fopen(filename, "r+");,刚刚打开的数据库文件;

Database_close

void Database_close(struct Connection *conn) 
{
    if(conn) {
        if(conn->file) 
            fclose(conn->file);
        if(conn->db)
            free(conn->db);
        free(conn);
    }
}
  • fclose :关闭指定的文件流

https://en.cppreference.com/w/c/io/fclose

  • free: Database_open两次malloc,这里就有两次free

Deallocates the space previously allocated by malloc()
https://en.cppreference.com/w/c/memory/free

free(conn->db); 对应 Database_open : conn->db = malloc(sizeof(struct Database));

free(conn);对应 Database_open: struct Connection *conn = malloc(sizeof(struct Connection));

4、数据库:创建/查询/插入/删除/列出

(1)创建 :$ ./ex17 db.dat c

case 'c':
            Database_create(conn);
            Database_write(conn);
            break;
struct Address {
    int id;
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

struct Database {
    struct Address rows[MAX_ROWS];
};
void Database_create(struct Connection *conn) 
{
    int i = 0;

    for(i = 0; i < MAX_ROWS; i++) {
        // make a prototype initialize it
        struct Address addr = {.id = i, .set = 0};
        // then just assign it
        conn->db->rows[i] = addr;
    }
}

void Database_write(struct Connection *conn) 
{
    rewind(conn->file);

    int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc != 1)
        die("Failed to write database.");

    rc = fflush(conn->file);
    if(rc == -1)
        die("Cannot flush database.");
}

Database_create

  • 这里创建了一个全新的数据库文件db.dat,还什么都没有;

  • Database结构体本质是一个Address结构体数组

  • 声明并初始化一个Address对象struct Address addr = {.id = i, .set = 0};,作为数据库每条记录的初始值;

  • id值,各个不同,是区别每条记录的唯一标识,在初始化时,就是使用的i,依次递增赋的值;

  • set字段,标识本条记录是否可以被覆盖,set=0时,可以在此处写入数据,set=1时,表示此处已经有记录了不能写;

Database_write

  • 将内存中的整个数据写入到空空的数据库文件中去;
  • rewind : 达到从头开始重新读文件的效果

void rewind( FILE *stream );
Moves the file position indicator to the beginning of the given file stream.
https://en.cppreference.com/w/c/io/rewind

  • int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);,将数组conn->db中的1struct database对象写到数据库文件conn->file

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
参数
buffer - 指向数组中要被写入的首个对象的指针
size - 每个对象的大小
count - 要被写入的对象数
stream - 指向输出流的指针
写 count 个来自给定数组 buffer 的对象到输出流stream
https://en.cppreference.com/w/c/io/fwrite

  • rc = fflush(conn->file);

int fflush( [FILE] *stream );
https://en.cppreference.com/w/c/io/fflush
对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。

(2)查询:$ ./ex17 db.dat g 2

case 'g':
            if(argc!=4)
                die("Need an id to get");
            Database_get(conn, id);
            break;`
void Database_get(struct Connection *conn, int id)
{
    struct Address *addr = &conn->db->rows[id];

    if(addr->set) {
        Address_print(addr);
    } else {
        die("ID is not set");
    }
}
struct Address {
    int id;
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

struct Database {
    struct Address rows[MAX_ROWS];
};

Database_get

  • & 取出地址:struct Address *addr = &conn->db->rows[id];

  • 打印记录

void Address_print(struct Address *addr)
{
    printf("%d %s %s\n", addr->id, addr->name, addr->email);
}

(3)插入:$ ./ex17 db.dat s 1 zed zed@zedshaw.com

case 's':
            if(argc != 6)
                die("Need id, name, email to set");

            Database_set(conn, id , argv[4], argv[5]);
            Database_write(conn);
            break;
void Database_set(struct Connection *conn, int id, const char *name, const char *email)
{
    struct Address *addr = &conn->db->rows[id];
    if(addr->set) 
        die("Already set, delete it first");

    addr->set = 1;
    // WARING: bug, read the "How To Break It" and fix this
    char *res = strncpy(addr->name, name, MAX_DATA);
    // demonstrate the strncpy bug
    if(!res)
        die("Name copy failed");

    res = strncpy(addr->email, email, MAX_DATA);
    if(!res)
        die("Email copy failed");   
}

Database_set

  • 使用id值,找到要插入的位置;
  • set字段为1,表示要插入要先删除旧的记录;
if(addr->set) 
        die("Already set, delete it first");
  • char *res = strncpy(addr->name, name, MAX_DATA);
    res = strncpy(addr->email, email, MAX_DATA);

char *strncpy( char *dest, const char *src, size_t count );
https://en.cppreference.com/w/c/string/byte/strncpy
复制 src 所指向的字符数组的至多 count 个字符(包含空终止字符,但不包含后随空字符的任何字符)到 dest 所指向的字符数组。

Database_write

  • 插入了新数据后,要把整个内存中的数据,回写到数据库文件中;

(4)删除:$ ./ex17 db.dat d 3

case 'd':
            if(argc!=4)
                die("Need id to delete");
            Database_delete(conn, id);
            Database_write(conn);
            break;

Database_delete

void Database_delete(struct Connection *conn, int id)
{
    struct Address addr = { .id = id, .set = 0};
    conn->db->rows[id] = addr;
}
  • 所谓删除,本质是覆盖id值不变,set设置为0(表示可以此处可以修改了)

Database_write

  • 删除了数据后,要把整个内存中的数据,回写到数据库文件中;

(5)列出:$ ./ex17 db.dat l

case 'l':
            Database_list(conn);
            break;
void Database_list(struct Connection *conn)
{
    int i = 0 ;
    struct Database *db = conn->db;

    for(i = 0 ;i < MAX_ROWS; i++) {
        struct Address *cur = &db->rows[i];

        if(cur->set) {
            Address_print(cur);
        }
    }
}
  • 访问数据库中每条记录,如果set值为1,表示存在有记录,就调用Address_print进行输出;

笔记

  • mallocfree 要成对出现;
  • 返回一个*ptr,接下来一定要写一个if(!ptr) ...

相关文章

  • [习题17]文件读写与结构体数组

    使用教材 《“笨办法” 学C语言(Learn C The Hard Way)》https://www.jiansh...

  • 【Python爬虫】-第二周 习题 13-17 (解包,参数,文

    习题 13-17 (解包,参数,文件读写等) 习题13 习题14 习题15 习题16 16-1 16-2 习题17...

  • 2018-05-30

    多个课上讲解的练习题就不发了! 文件复制 Fscanf测试 Fprintf测试 结构体录入 文件操作 字符串读写函...

  • C语言——第四次笔记

    指针指针的定义指针的类型指针的指向内容指针的运算数组与指针指针与函数动态分配内存结构体文件读写头文件与实现文件实例...

  • C语言续

    指针指针的定义指针的类型指针的指向内容指针的运算数组与指针指针与函数动态分配内存结构体文件读写头文件与实现文件实例...

  • 结构体与数组的关系

    结构体与数组的关系 结构体是数组的成员 一个数组的全部元素是结构体变量

  • 【Python爬虫】03作业

    一、作业内容:习题13-17解包、参数、文件读写习题13.使用import语句用于导入其他模块代码,被引用模块中的...

  • 结构体与结构体指针数组

    1.结构体定义与使用。 2.结构体指针 与 动态内存开辟。 3.结构体的数组。 4.结构体与结构体指针 取别名。 ...

  • C语言-5、结构体

    写法一 写法二 写法三 结构体指针 结构体指针 与 动态内存开辟 结构体的数组 结构体与结构体指针 取别名 取别名...

  • 结构体数组的定义

    结构体数组的定义 1、先定义结构体类型,再定义结构体数组 2、定义结构体类型的同时定义结构体数组 3、省略结构体类...

网友评论

    本文标题:[习题17]文件读写与结构体数组

    本文链接:https://www.haomeiwen.com/subject/csjgoftx.html