如何正确回收结构?

时间:2019-01-18 09:03:30

标签: c struct free const-pointer

我试图了解什么是提供结构的创建/声明功能的常见习惯用法(良好实践)。这是我尝试过的:

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

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

因此,通常的做法是将结构回收方法参数定义为指向const结构的指针,这样我们就可以避免在任何时候都强制转换为非const并在回收方法实现中进行一次这种脏转换?

1 个答案:

答案 0 :(得分:3)

  

因此,通常的做法是将结构回收方法参数定义为指向const结构的指针,这样我们就可以避免在任何时候都强制转换为非const并在回收方法实现中进行一次这种脏转换?

不。在动态分配的结构或包含指向动态分配的内存的指针的结构中不使用const更为常见。

您仅标记const个您不打算修改的内容;释放它或它的成员引用的数据是一种修改。只需看看如何声明free()void free(void *),而不是void free(const void *)

这是OP代码中的核心问题,使用不带struct test_struct_t *test_struct_ptr = create(10);限定符的const是正确的解决方案。


不过,在这里有一个有趣的潜在问题,我想稍作讨论,因为该问题的措词使得那些寻求答案的人将通过网络搜索遇到该问题。

  

如何正确回收结构?

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

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()或{{1 }}。这意味着在大多数情况下,指针仍可以在realloc()之后被取消引用而没有运行时错误,但是它指向的数据将完全不同。这就是使这些错误难以复制和调试的原因。

中毒只是将结构成员设置为无效值,因此,由于这些值很容易看到,因此在运行时可以很容易地检测到“后用后使用”。将用于访问动态分配的内存的指针设置为free()意味着,如果取消引用该指针,则程序应以segmentation fault崩溃。使用调试器进行调试更容易。至少您可以轻松地准确找到崩溃发生的位置和方式。

在独立代码中,这并不是很重要,但是对于库代码或其他程序员使用的代码,这可能会影响组合代码的总体质量。这取决于;尽管我确实倾向于使用指针成员和中毒版本作为示例,但我总是根据具体情况进行判断。

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