代码之家  ›  专栏  ›  技术社区  ›  Some Name

如何正确回收结构?

  •  0
  • Some Name  · 技术社区  · 6 年前

    我试图理解提供结构的创建/回收功能的常见习惯用法(良好实践)。以下是我的尝试:

    struct test_struct_t{
        int a;
    };
    
    struct test_struct_t *create(int a){
        struct test_struct_t *test_struct_ptr = malloc(sizeof(*test_struct_ptr));
        test_struct_ptr -> a = a;
        return test_struct_ptr;
    }
    
    void release(struct test_struct_t *test_struct_ptr){
        free((void *) test_struct_ptr);
    }
    
    int main(int argc, char const *argv[])
    {
        const struct test_struct_t *test_struct_ptr = create(10);
        release(test_struct_ptr); // <--- Warning here
    }
    

    我收到警告了

    passing argument 1 of ‘release’ discards ‘const’ qualifier from pointer 
       target type [-Wdiscarded-qualifiers]
    

    很明显。所以我倾向于定义填海方法如下:

    void release(const struct test_struct_t *test_struct_ptr){
        free((void *) test_struct_ptr);
    }
    

    警告消失了,但我不确定它是否不容易出错。

    那么,将struct reclamation method参数定义为const结构的指针,这样我们就可以避免在任何时候强制转换为非const,并且在reclamation method实现中执行一次这种脏的强制转换吗?

    1 回复  |  直到 6 年前
        1
  •  3
  •   Nominal Animal    6 年前

    那么,将struct reclamation method参数定义为const结构的指针,这样我们就可以避免在任何时候强制转换为非const,并且在reclamation method实现中执行一次这种脏的强制转换吗?

    不,不使用更常见 const 具有动态分配的结构,或者具有包含指向动态分配内存的指针的结构。

    你唯一的标记 康斯特 您不打算修改的内容;释放它或它的成员引用的数据是一种修改。看看怎么做 free() 声明: void free(void *) 不是 void free(const void *) .

    这是OP代码中的核心问题,并且使用 struct test_struct_t *test_struct_ptr = create(10); 没有 康斯特 限定符是正确的解决方案。


    不过,这里有一个有趣的潜在问题,我想仔细考虑一下,因为这个问题的措辞是这样的,那些寻找答案的人将通过网络搜索遇到这个问题。

    如何正确回收结构?

    让我们来看一个实际情况:动态分配的字符串缓冲区。有两种基本方法:

    typedef struct {
        size_t          size;  /* Number of chars allocated for data */
        size_t          used;  /* Number of chars in data */
        unsigned char  *data;
    } sbuffer1;
    #define  SBUFFER1_INITIALIZER  { 0, 0, NULL }
    
    typedef struct {
        size_t          size;  /* Number of chars allocated for data */
        size_t          used;  /* Number of chars in data */
        unsigned char   data[];
    } sbuffer2;
    

    可以使用预处理器初始值设定项宏声明和初始化第一个版本:

        sbuffer1  my1 = SBUFFER1_INITIALIZER;
    

    这在例如posix.1中使用 pthread_mutex_t 互斥和 pthread_cond_t 条件变量。

    但是,由于第二个数组成员具有灵活的数组成员,因此不能静态声明它;只能声明指向它的指针。因此,您需要一个构造函数函数:

    sbuffer2 *sbuffer2_init(const size_t  initial_size)
    {
        sbuffer2  *sb;
    
        sb = malloc(sizeof (sbuffer2) + initial_size);
        if (!sb)
            return NULL; /* Out of memory */
    
        sb->size = initial_size;
        sb->used = 0;
        return sb;
    }
    

    你这样使用:

        sbuffer2 *my2 = sbuffer2_init(0);
    

    尽管我亲自实现了相关的功能,所以你可以

        sbuffer2 *my2 = NULL;
    

    相当于 sbuffer1 my1 = SBUFFER1_INITIALIZER; .

    一个函数,它可以增大或缩小为数据分配的内存量,只需要一个指向第一个结构的指针;但要么是指向指向第二个结构的指针的指针,要么返回可能修改过的指针,以便调用方能够看到更改。

    例如,如果我们想从某个源设置缓冲区内容,可能

    int  sbuffer1_set(sbuffer1 *sb, const char *const source, const size_t length);
    
    int  sbuffer2_set(sbuffer2 **sb, const char *const source, const size_t length);
    

    只访问数据但不修改数据的函数也不同:

    int  sbuffer1_copy(sbuffer1 *dst, const sbuffer1 *src);
    
    int  sbuffer2_copy(sbuffer2 **dst, const sbuffer2 *src);
    

    请注意 const sbuffer2 *src 不是打字。因为函数不会修改 src 指针(我们可以做到 const sbuffer2 *const src !)它不需要指向数据指针的指针,只需要指向数据的指针。

    真正有趣的部分是回收/免费功能。

    释放这些动态分配的内存的功能在一个重要部分上确实有所不同:第一个版本可以轻微地毒害字段,以帮助检测释放错误后的使用情况:

    void sbuffer1_free(sbuffer1 *sb)
    {
        free(sb->data);
        sb->size = 0;
        sb->used = 0;
        sb->data = NULL;
    }
    

    第二个问题很棘手。如果我们遵循上述逻辑,我们将编写一个中毒回收/释放函数

    void sbuffer2_free1(sbuffer2 **sb)
    {
        free(*sb);
        *sb = NULL;
    }
    

    但是因为程序员习惯了 void *v = malloc(10); free(v); 模式(与 free(&v); !),它们通常期望函数为

    void sbuffer2_free2(sbuffer2 *sb)
    {
        free(sb);
    }
    

    相反,这个不能毒害指针。除非用户做了相当于 sbuffer2_free2(sb); sb = NULL; ,存在重复使用 sb 之后。

    C库通常不会立即将内存返回到操作系统,而是将其添加到自己的内部空闲列表中,供后续的操作系统使用。 malloc() , calloc() realloc() 打电话。这意味着在大多数情况下,指针仍然可以在 自由() 没有运行时错误,但它指向的数据将完全不同。这就是为什么这些bug在复制和调试时如此令人讨厌的原因。

    中毒只是将结构成员设置为无效的值,因此在运行时很容易检测到空闲后的使用,因为很容易看到这些值。将用于访问动态分配内存的指针设置为 NULL 意味着如果指针被取消引用,程序将崩溃 segmentation fault .使用调试器进行调试要容易得多;至少您可以轻松地找到崩溃发生的确切位置和方式。

    这在自包含代码中并不那么重要,但对于库代码或其他程序员使用的代码,它可以对组合代码的一般质量产生影响。这要看情况而定;我总是根据具体情况来判断它,尽管我确实倾向于使用指针成员和中毒版本作为例子。

    我对指针成员和灵活的数组成员进行了更多的上进和下退。 in this answer .对于那些想知道如何回收/释放结构,以及如何选择在各种情况下要使用的类型(指针成员或灵活的数组成员)的人来说,这可能很有趣。

    推荐文章