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

使用教材

《“笨办法” 学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 [email protected]
    (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 [email protected]
$ ./ex17 db.dat s 2 frank [email protected]
$ ./ex17 db.dat s 3 joe [email protected]
  • 3、使用命令l(小写的字母l),列出当前数据库内的全部记录
$ ./ex17 db.dat l
1 zed [email protected]
2 frank [email protected]
3 joe [email protected]
  • 4、使用命令行选项d,删除id=3的数据,并查看
$ ./ex17 db.dat d 3
$ ./ex17 db.dat l
1 zed [email protected]
2 frank [email protected]
  • 5、使用选项g,查看id=2的数据
$ ./ex17 db.dat g 2
2 frank [email protected]
  • 6、各种错误提示信息输出
$ ./ex17 db.dat s 2 hello [email protected]
ERROR: Already set, delete it first

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

$ ./ex17 db.dat
ERROR: USAGE: ex17   [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   [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 );

    [习题17]文件读写与结构体数组_第1张图片
    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 [email protected]

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]文件读写与结构体数组)