0

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

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

4

1 回答 1

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

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

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

在这个答案中,我对指针成员与灵活数组成员的区别更多。那些想知道如何回收/释放结构以及如何选择在各种情况下使用哪种类型(指针成员或灵活数组成员)的人可能会很有趣。

于 2019-01-18T10:55:03.650 回答