【问题标题】:How to reclaim struct correctly?如何正确回收结构?
【发布时间】:2019-06-12 12:28:34
【问题描述】:

我试图了解提供结构的创建/回收功能的常见习语(良好做法)是什么。这是我尝试过的:

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,并在回收方法实现中执行一次这种脏强制转换?

【问题讨论】:

  • 通常,只有在结构具有需要首先释放的已分配指针成员,然后才能释放指向结构的指针时,才会执行这些清理功能。它应该始终与分配顺序相反。最后,悬空指针应设置为 NULL。
  • @Rafael 但是如何在清理函数中隐藏释放细节呢?因此,我们不必在整个代码库中用自定义 dealloc 函数替换 free,以防我们复杂化...
  • 起初对我来说似乎是个坏主意,只有 const 指针用于动态分配的对象。
  • @SomeName 是的。您的假设是正确的,但是由于您需要稍后对其进行修改(free(3)),因此您至少应该有一个指向它的非常量指针,以避免出现这样的警告。
  • 一种常见的做法是使用“不透明指针”并且根本不将结构暴露给调用者。给调用者一个指向不完整类型的指针。在这种情况下,您不需要 const 限定符,因为调用者无法对指针做任何有意义的事情。

标签: c struct free const-pointer


【解决方案1】:

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

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

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

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


不过,这里有一个有趣的潜在问题,我想稍微解释一下,因为这个问题的措辞使得那些寻找答案的人会通过网络搜索遇到这个问题。

如何正确回收struct?

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

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(&amp;v);!),他们通常期望函数是

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

相反;而这个不能毒化指针。除非用户执行sbuffer2_free2(sb); sb = NULL; 的等价操作,否则之后存在重复使用sb 内容的风险。

C 库通常不会立即将内存返回给操作系统,而只是将其添加到自己的内部空闲列表中,以供后续的malloc()、calloc() 或realloc() 调用使用。这意味着在大多数情况下,指针仍然可以在 free() 之后被取消引用而不会出现运行时错误,但它指向的数据将完全不同。这就是使这些错误难以重现和调试的原因。

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

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

指针成员与灵活数组成员 in this answer对于那些想知道如何回收/释放结构以及如何选择在各种情况下使用哪种类型(指针成员或灵活数组成员)的人来说可能会很有趣。

【讨论】:

  • @SomeName:我明白为什么使用临时变量会似乎更好。重要的是要意识到,如果这里的访问之间存在竞争条件,那么一个顺序总是会导致垃圾或运行时错误。因此,最小化窗口并不重要:您必须完全避免此类竞争,以确保正确操作。
  • @SomeName:当函数不会尝试修改其内容时,函数将参数定义为const struct foo * 是非常常见的,即使假设结构本身在调用者中是可变的。 nanosleep() POSIX.1 函数就是一个很好的例子。 (POSIX.1 spec 明确提到这两个指针也可以指向同一个对象,因此这也是明确允许别名的情况。)
  • @SomeName:虽然今天的编译器足够聪明,可以自己推断constness,但它对我们人类程序员也很有用。它可以帮助我们了解该功能应该如何运作。在nanosleep() 的情况下,第一个参数const struct timespec *req 指向请求的睡眠持续时间,而struct timespec *rem 要么为NULL,要么指向未睡眠的持续时间(如果被中断)。典型的用例是 struct timespec t; t.tv_sec = 5; t.tv_nsec = 500000000; nanosleep(&amp;t, NULL); 或 ...
  • struct timespec treq, trem; treq.tv_sec = 5; treq.tv_nsec = 500000000; if (nanosleep(&amp;treq, &amp;trem) == -1 &amp;&amp; errno == EINTR) { /* sleep interrupted, time unslept is ((double)trem.tv_sec + (double)trem.tv_nsec / 1000000000.0) seconds */ } else { /* Slept 5.5 seconds or tiny bit longer */ }。换句话说,const 标记根本不会影响调用代码,但它确实有助于我们更好地理解内部操作,例如,与只有一个 struct timespec *req 和一个 int save_unslept 标志作为参数相比。
  • @SomeName:这是安全的,是的。但是,如果它是不透明的,并且仅在您自己的(即库内部)C 代码中使用,您为什么要将它标记为 const?
猜你喜欢
  • 2020-02-18
  • 1970-01-01
  • 2021-11-16
  • 2021-02-01
  • 2015-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多