如何正确回收结构?

How to reclaim struct correctly?

我试图了解提供结构的 creation/reclamation 功能的常用习惯用法(良好做法)是什么。这是我尝试过的:

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 结构的指针,这样我们就可以避免转换为非常量,并在回收方法实现中执行一次这种脏转换?

So is a common practice to define struct reclamation method parameter as a pointer to a const struct so we can avoid casting to non-const any time and do this dirty cast once in the reclamation method implementation?

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

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

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


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

How to reclaim struct correctly?

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

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;.

一个可以增加或减少为数据分配的内存量的函数,只需要一个指向第一个结构的指针;但是指向第二个结构的指针的指针,或者 return 可能修改的指针,以便调用者可以看到更改。

例如,如果我们想从某个来源设置缓冲区内容,也许

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!),所以它不需要指向数据的指针,只需要指向数据的指针。

真正有趣的部分是 reclaim/free 函数。

释放此类动态分配内存的函数在一个重要部分确实有所不同:第一个版本可以简单地毒化字段以帮助检测释放后使用错误:

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

第二个有点棘手。如果按照上面的逻辑,我们会写一个中毒的reclaim/free函数为

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 库通常不会return 立即将内存分配给OS,而是将其添加到自己的内部空闲列表中,以供后续malloc() 使用, calloc(),或realloc()来电。这意味着在大多数情况下,指针仍然可以在 free() 之后被取消引用而不会出现 运行 时间错误,但它指向的数据将完全不同。这就是使这些错误难以重现和调试的原因。

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

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

我对指针成员和灵活的数组成员有更多的了解 。对于那些想知道如何 reclaim/free 结构,以及如何选择在各种情况下使用哪种类型(指针成员或灵活数组成员)的人来说,这可能很有趣。