简体   繁体   中英

Structs containing a dynamic array

I am learning C at university, and also via "Learn C the Hard Way". This question specifically relates to Learn C the Hard Way - Exercise 17

In the code I have written, there is a struct Database used:

#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];
}

The 'extra credit' section of this exercise asks "Change the code to accept parameters for MAX_DATA and MAX_ROWS, store them in the Database struct, and write that to the file, thus creating a database that can be arbitrarily sized."

Ignoring MAX_DATA for now, as I want to focus on one at a time.

Note: Here is my full code

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#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;
};

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

    free(conn->file);
    free(conn->db);
    free(conn);

    exit(1);
}

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

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.", conn);
}

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

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

    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", conn);

    return conn;
}

void Database_close(struct Connection *conn)
{
    if(conn) {
        if(conn->file) fclose(conn->file);
        if(conn->db) free(conn->db);
        free(conn);
    }
}

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.", conn);

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

void Database_create(struct Connection *conn)
{
    int i = 0;

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

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", conn);

    addr->set = 1;

    char *res = strncpy(addr->name, name, MAX_DATA);
    addr->name[MAX_DATA - 1] = '\0';
    if(!res) die("Name copy failed", conn);

    res = strncpy(addr->email, email, MAX_DATA);
    addr->email[MAX_DATA - 1] = '\0';
    if(!res) die("Email copy failed", conn);
}

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", conn);
    }
}

void Database_delete(struct Connection *conn, int id)
{
    struct Address addr = {.id = id, .set = 0};
    conn->db->rows[id] = addr;
}

void Database_list(struct Connection *conn)
{
    int i = 0;
    struct Database *db = conn->db;

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

        if(cur->set) {
            Address_print(cur);
        }
    }
}

int main(int argc, char *argv[])
{
    char *filename = argv[1];
    char action = argv[2][0];
    int id = 0;
    struct Connection *conn = Database_open(filename, action);

    if(argc < 3) die("USAGE: ex17 <dbfile> <action> [action params]", conn);


    if(argc > 3) id = atoi(argv[3]);
    if(id >= sizeof(conn->db->rows) / sizeof(*conn->db->rows)) die("There's not that many records.", conn);

    switch(action) {
        case 'c':
            Database_create(conn);
            Database_write(conn);
            break;

        case 'g':
            if(argc != 4) die("Need an id to get", conn);

            Database_get(conn, id);
            break;

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

            Database_set(conn, id, argv[4], argv[5]);
            Database_write(conn);
            break;

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

            Database_delete(conn, id);
            Database_write(conn);
            break;

        case 'l':
            Database_list(conn);
            break;
        default:
            die("Invalid action, only: c=create, g=get, s=set, d=del, l=list", conn);
    }

    Database_close(conn);

    return 0;
}

My first thought was to not make "Database" a global struct, and instead put it in my main function so that it could flow through the rest of the program, but that seems so awkward, having to pass this variable throughout the entire program. However, this made me realise that I would be having to pass a pointer to it through every function call that needs it, which isn't what the extra credit implies.

I get the feeling that as a C newbie I am missing something. My question may be vague because I don't fully know the terms I am talking about.

In case my question isn't clear - how do I actually add this functionality??? I really am at a loss.

The exercise implies changing your Database as follows:

struct Database {
    int max_rows;
    struct Address *rows;
};

You can keep your database global, but now the rows inside it must be allocated dynamically with malloc . Of course in order to do that you need to know how many rows to allocate. That's what the "accept parameters for MAX_DATA and MAX_ROWS " is all about: add code to main to interpret the max_rows parameter, store it in max_rows of your struct Database , and allocate rows based on that number.

Your Database_load , Database_write , and Database_close would need to change to read and write the max_rows to the database file. The Database_load would be responsible for allocating rows with malloc after reading the size; the Database_close would be responsible for calling free() on the rows .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM