【问题标题】:Memory management in Linked Lists C链表 C 中的内存管理
【发布时间】:2020-08-07 07:41:34
【问题描述】:

我正在尝试实现一个表示文件夹树的链表数据结构。

以下结构:

typedef struct SRC_ERROR SRC_ERROR;
struct SRC_ERROR {
    int error_code;
    char *error;
};

typedef struct SRC_FILE SRC_FILE;
struct SRC_FILE {
    char *entry;
    char md5[MD5_DIGEST_LENGTH];
};

typedef struct SRC SRC; //Source file tree with md5 entry char for source verification.
struct SRC {
    SRC_ERROR error;
    char *name;
    char *full_path;
    SRC_FILE **entries;
    SRC *next_dir;
};

想法是将每个目录存储在SRC 中,SRC_FILE 将用作一个数组来存储每个文件的文件名和 MD5 哈希。

下面的scan_source() 填充结构。

SRC *scan_source(char *source_path) {
    SRC *source = malloc(sizeof(SRC));
    source->error.error_code = OK;
    int count = 0;
    DIR *dir;
    struct dirent *entry;

    if (!(dir = opendir(source_path))) {
        source->error.error_code = ERROR;
        source->error.error = "Unable to open source directory.\n";
        return source;
    }

    source->entries = (SRC_FILE **)malloc(sizeof(SRC_FILE *) * count);

    if (source->entries == NULL) {
        source->error.error_code = ERROR;
        source->error.error = "Unable to allocate memory to file entry tree\n";
    }

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type == DT_DIR) {
            char path[PATH_MAX];

            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
                continue;

            snprintf(path, sizeof(path), "%s/%s", source_path, entry->d_name);
            printf("[%s] - %s\n", entry->d_name, path);

            //add new node
            source = add_dir(source, insert_dir_node(entry->d_name, path));

            scan_source(path);
        } else
        if (entry->d_type == DT_REG) {
            printf("[FILE] - %s\n", entry->d_name);
            source->entries[count]->entry = entry->d_name; //SEGFAULT HERE
            count++;
            source->entries = realloc(source->entries, sizeof(SRC_FILE *) * (count));
        }
    }
    closedir(dir);
    return source;
}

我遇到了内存管理问题。当目录以某种方式构建时,我会遇到间歇性段错误。

我已经标记了调试器标记的行

source->entries[count]->entry = entry->d_name; //SEGFAULT HERE 

我以为我为每个结构都分配了内存,但也许我没有正确执行此操作,或者数据结构完全存在潜在问题?

例如:

test> tree
.
└── Text

0 directories, 1 file

这会导致段错误。然而,这不会:

/test> tree
.
├── another sample
│   └── Text
└── sample folder

2 directories, 1 file

使用的附加功能:

SRC *add_dir(SRC *file_tree, SRC *new_dir) {
    new_dir->next_dir = file_tree;
    return new_dir;
}

SRC *insert_dir_node(char *name, char *full_path) {
    SRC *next_dir;
    next_dir = (SRC *)emalloc(sizeof(SRC));
    next_dir->name = name;
    next_dir->full_path = full_path;
    next_dir->next_dir = NULL;
    return next_dir;
}

【问题讨论】:

  • 为什么这个标签是c++
  • 类似的语言,都具有某种手动内存管理功能。有问题吗?
  • @cigien 也许是因为这种代码如果用 C++ 编写会简单得多。
  • @hdcdigi 问题是你在 C 和 C++ 中处理这个问题的方式是完全不同的。您编写的代码可能是非常好的 C(尽管有错误),但它确实是糟糕的 C++。
  • 明白。也觉得懂c++的人可能也有一些c知识。

标签: c memory-management linked-list malloc


【解决方案1】:

主题是链表中的内存管理。事实上,这是 C 程序中的一个主要问题,因为没有自动内存管理。您必须从内存管理的角度决定并指定如何处理结构中指针指向的每个对象。指针是对象生命周期的 引用,还是生命周期在其他地方处理,而指针只是一个访问点。

让我们分析一下你的对象定义:

typedef struct SRC_ERROR SRC_ERROR;
struct SRC_ERROR {
    int error_code;
    char *error;
};

SRC_ERROR 只是一种打包错误描述的方式。如果error 成员始终存储指向字符串文字的指针,则应将其定义为const char *。相反,如果在某些情况下您分配的字符串包含特定于实际错误的信息,例如"error allocating 1023 objects\n",那么您需要一个指示符指定error 指向应在使用后释放的已分配内存,或者您应该始终分配错误消息的内存,并在丢弃 SRC_ERROR 对象时始终释放此内存。

typedef struct SRC_FILE SRC_FILE;
struct SRC_FILE {
    char *entry;
    char md5[MD5_DIGEST_LENGTH];
};

entry 应该指向分配的内存,并且在丢弃SRC_FILE 对象时应该释放该内存。

typedef struct SRC SRC; //Source file tree with md5 entry char for source verification.
struct SRC {
    SRC_ERROR error;
    char *name;
    char *full_path;
    SRC_FILE **entries;
    SRC *next_dir;
};
  • namefull_path 应该指向分配的内存,并且应该在丢弃 SRC 对象时释放。
  • next_dir 指向另一个 SRC 对象,应该一致地分配和释放。
  • entries 指向一个已分配的数组,其中的每个元素都指向一个已分配的对象。您需要一种方法来判断此数组中的元素数量。您可以在数组末尾维护一个NULL 指针,但在SRC 中添加一个count 成员以获取此信息更简单。将其设为指向已分配 SRC 对象数组的指针也会简单得多。

该函数不构造树,而是尝试构造目录列表。每当递归到目录时,您应该将新列表从 scan_source 返回的 SRC_ERROR 对象附加到已在调用者分配的 SRC_ERROR 对象中构造的列表中,并释放递归调用返回的对象。

这是一个测试程序中的修改版本:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>

#ifndef PATH_MAX
#define PATH_MAX 1024
#endif
#define MD5_DIGEST_LENGTH  16

#define TRACE(x)  //x

enum { OK = 0, ERROR, OUT_OF_MEMORY };

typedef struct ERROR_STATE ERROR_STATE;
struct ERROR_STATE {
    int code;
    const char *message;  // always a string literal
};

typedef struct SRC_FILE SRC_FILE;
struct SRC_FILE {
    char *name;        // points to allocated memory
    char md5[MD5_DIGEST_LENGTH];
};

typedef struct SRC SRC; //Source file tree with md5 entry char for source verification.
struct SRC {
    char *name;         // points to allocated memory
    char *full_path;    // points to allocated memory
    size_t count;       // number of elements in entries
    SRC_FILE *entries;  // allocated array of count elements
    SRC *next_dir;      // the next SRC
};

static char *basename_dup(const char *full_path) {
    char *p = strrchr(full_path, '/');
    return strdup(p ? p + 1 : full_path);
}

/* construct a SRC describing the directory contents.
 * if there is an error, either return a partially constructed SRC or return NULL
 */
SRC *scan_source(const char *source_path, ERROR_STATE *error) {
    char *full_path = strdup(source_path);
    char *name = basename_dup(source_path);
    SRC *source = calloc(1, sizeof(SRC));   // all members initialized to 0

    if (source == NULL) {
        error->code = ERROR;
        error->message = "Unable to allocate memory.\n";
        free(full_path);
        free(name);
        free(source);
        return NULL;
    }

    error->code = OK;
    source->full_path = full_path;
    source->name = name;

    DIR *dir;
    struct dirent *entry;

    if (!(dir = opendir(source_path))) {
        error->code = ERROR;
        error->message = "Unable to open source directory.\n";
        return source;
    }

    while ((entry = readdir(dir)) != NULL) {
        char path[PATH_MAX];
        int len;

        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
            continue;

        len = snprintf(path, sizeof(path), "%s/%s", source_path, entry->d_name);
        if (len >= (int)sizeof(path)) {
            // the path was truncated.
            // you can report this or ignore it...
            TRACE(printf("[%s] - %s - path too long, ignored\n", entry->d_name, path));
            continue;
        }
        if (entry->d_type == DT_DIR) {
            TRACE(printf("[%s] - %s\n", entry->d_name, path));

            SRC *source1 = scan_source(path, error);
            if (error->code != OK) {
                // either ignore the error or abort?
            }
            if (source1) {
                // append the new directory (and its list of sub-directories)
                SRC **tailp = &source->next_dir;
                while (*tailp) tailp = &(*tailp)->next_dir;
                *tailp = source1;
            }
        } else
        if (entry->d_type == DT_REG) {
            TRACE(printf("[FILE] - %s\n", entry->d_name));
            // add the file to the entries list
            SRC_FILE *entries = realloc(source->entries, sizeof(source->entries[0]) * (source->count + 1));
            if (entries == NULL) {
                // you should return to the caller with a proper error code
                error->code = OUT_OF_MEMORY;
                error->message = "cannot reallocate entries array";
                break;
            }
            source->entries = entries;
            // source->entries[count] must point to an allocated object
            name = strdup(entry->d_name);
            if (name == NULL) {
                error->code = OUT_OF_MEMORY;
                error->message = "cannot allocate entry name";
                break;
            }
            source->entries[source->count].name = name;
            memset(source->entries[source->count].md5, 0, sizeof(source->entries[source->count].md5));
            source->count++;
            //if (md5_sum(full_path, source->entries[source->count].md5)) {
            //    // error computing the MD5 sum...
            //}
        }
    }
    closedir(dir);
    return source;
}

void free_source(SRC *source) {
    if (source) {
        free(source->name);
        free(source->full_path);
        for (size_t i = 0; i < source->count; i++) {
            free(source->entries[i].name);
        }
        free(source);
    }
}

int main(int argc, char *argv[1]) {
    ERROR_STATE error = { 0, NULL };

    if (argc < 2) {
        printf("usage: scansource directory [...]\n");
        return 1;
    }

    for (int i = 1; i < argc; i++) {
        SRC *source = scan_source(argv[i], &error);
        if (error.code) {
            printf("Error %d: %s\n", error.code, error.message);
        }
        while (source) {
            SRC *cur = source;
            source = source->next_dir;

            printf("{\n"
                   "  name: '%s',\n"
                   "  full_path: '%s',\n"
                   "  count: %zu,\n"
                   "  entries: [\n",
                   cur->name, cur->full_path, cur->count);
            for (size_t j = 0; j < cur->count; j++) {
                printf("    { md5: '");
                for (size_t k = 0; k < MD5_DIGEST_LENGTH; k++)
                    printf("%02x", cur->entries[j].md5[k]);
                printf("', name: '%s' },\n", cur->entries[j].name);
            }
            printf("  ]\n},\n");
            free_source(cur);
        }
    }
    return 0;
}

【讨论】:

    【解决方案2】:

    我开始查看代码,我看到的第一个问题是您正在存储 readdir() 调用返回的指针 - 您应该复制其中包含的数据。

    改变

    source = add_dir(source, insert_dir_node(entry->d_name, path));
    

    source = add_dir(source, insert_dir_node(strdup(entry->d_name), path));
    

    您看到分段错误的原因是您总是在 source-&gt;entries 数组的末尾写入。

    您最初创建一个 0 大小的数组:

        int count = 0;
    /* ... */
        source->entries = (SRC_FILE **) malloc(sizeof(SRC_FILE*) * count);
    

    然后设置它的第一个(索引为 0)元素:

                source->entries[count]->entry = entry->d_name; //SEGFAULT HERE
                count++;
                source->entries = realloc(source->entries, sizeof(SRC_FILE*)*(count));
    

    然后将数组扩展为 1 个元素,然后写入第二个索引,依此类推。

    您可以修复逻辑(始终为 count+1 元素分配空间,因为您不仅希望为现有元素腾出空间,还希望为下一个元素留出空间),或者,在这种情况下可能更有效,在这里也切换到链表结构。

    下一个问题是您只分配指向 SRC_FILE 的指针,而不是 SRC_FILE 结构 - 您应该将定义更改为:

    struct SRC {
        SRC_ERROR error;
        char *name;
        char *full_path;
        SRC_FILE *entries;
        SRC *next_dir;
    };
    

    然后初始化到

    source->entries = (SRC_FILE *) malloc(sizeof(SRC_FILE) * (count + 1));
    

    然后是关键部分

    source->entries[count].entry = strdup(entry->d_name);
    count++;
    source->entries = realloc(source->entries, sizeof(SRC_FILE) * (count + 1));
    

    还有一件事需要注意:insert_dir_node 创建一个新的 SRC 结构,它需要有一个新初始化的条目成员:

    next_dir->count = 0;
    next_dir->entries = (SRC_FILE *)malloc(sizeof(SRC_FILE) * (1));
    
    • 而且,由于我们现在有单独的 entries,因此我们需要为每个 count 设置一个 count,因此也要将此变量移动到结构中。

    修复所有这些为我提供了一个无错误的程序。

    【讨论】:

    • 感谢帮助。它完全在空文件夹树上运行。但是,只要将文件放在树中。我得到另一个段错误。我也无法弄清楚路径变量的问题是什么。我认为这可能会导致一些问题。该列表似乎没有正确更新完整路径。查看readdir() 主循环的输出与列表中的输出。 [sample] - test/sample [sub_test] - test/sub_test [subsample] - test/sub_test/subsample NAME: sub_test - PATH: test/sub_test NAME: sample - PATH: test/sub_test NAME: (null) - PATH: (null)
    • 不确定回复您的答案的最佳做法?评论似乎有限,我是否更新我的主要问题?在我继续这个项目之前,也许我需要回去修改我的内存分配和数据结构。
    • 我找到了剩下的问题,编辑了答案。是的,如果您重新考虑要如何存储数据,那将是最好的 - 当前版本可以工作,但就性能和简单性而言,更简洁的方法可能会更好。
    猜你喜欢
    • 1970-01-01
    • 2011-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-17
    • 2010-09-06
    • 2018-12-17
    相关资源
    最近更新 更多