自动在释放后将指针设置为NULL。

huangapple go评论72阅读模式
英文:

Automatically set pointers to NULL after free

问题

这个帖子展示了一种使用 void** 来消除与悬挂指针相关的一类错误(在释放后使用,双重释放等)的方法:

void freep(void **p) {
    if (p) {
        free(*p);
        *p = NULL;
    }
}

我尝试了以下的驱动代码:

#include <stdlib.h>
#include <string.h>

#define free(x) freep(x)

int main(void) {
    char *s = malloc(5);
    strcpy(s, "hehe");

    char **ss = &s;

    free(s);
    free(ss);
    free(&s);
}

正如帖子及其评论中所指出的,从技术上讲,这违反了C标准 - 使用 -Wall -Wextra -pedantic -std=c17 编译时,会收到关于将 char*char** 类型参数传递给 void** 类型参数的警告。

我的问题是,是否可以在不违反C标准的情况下,仍然实现它的目标,即确保用户在使用 free() 后不会忘记将指针设置为 NULL

感谢您的时间。

英文:

This post shows a way to use void**s to eliminate a category of bugs related to dangling pointers (use after free, double free, etc):

void freep(void **p) {
    if (p) {
        free(*p);
        *p = NULL;
    }
}

I tried it out with the following driver code:

#include <stdlib.h>
#include <string.h>

#define free(x) freep(x)

int main(void) {
    char *s = malloc(5);
    strcpy(s, "hehe");

    char **ss = &s;

    free(s);
    free(ss);
    free(&s);
}

As noted in the post and its comments, it is technically in violation of the C standard - compiled with -Wall -Wextra -pedantic -std=c17, I get warnings regarding passing char* and char** type parameters to a void** type parameters.

My question is, can it be made to not violate the C standard, while still achieving its goal of avoiding dangling pointers by ensuring the user can't forget to set a pointer to NULL after free()-ing it?

Thank you for your time.

答案1

得分: 2

freep 只能将 void * 设置为 NULL。您可能想要将各种指针类型(包括 char *)设置为 NULL

freep 唯一能工作的方式是这样的:

void *p = s;
freep( &p );
s = p;

当然,这是荒谬的。

普通函数不行,因为不同的指针类型可以有不同的大小和布局。解决方法是:

#define free( p ) do { free( p ); p = NULL; } while ( 0 )

free( s );

警告:上述代码会对参数表达式进行两次评估。最好使用一个能清楚表明这是一个宏而不是覆盖 free 的名称。

#define SAFE_FREE( p ) do { free( p ); p = NULL; } while ( 0 )

SAFE_FREE( s );
英文:

freep can only set a void * to NULL. You presumably want to set a variety of pointer types (including char *) to NULL.

The only way freep would work is if you did

void *p = s;
freep( &p );
s = p;

Of course, that's ridiculous.

An ordinary function won't do because different pointer types can have different sizes and layouts. Solution:

#define free( p ) do { free( p ); p = NULL; } while ( 0 )

free( s );

Warning: The above evaluates the argument expression twice. It's better to use a name that makes it clear this is a macro rather than overridding free.

#define SAFE_FREE( p ) do { free( p ); p = NULL; } while ( 0 )

SAFE_FREE( s );

答案2

得分: 2

The only generic object pointer type that C supports is void*. This special ability of void* does not apply recursively to void**. Passing a char** to a function expecting a void** is invalid C like the compiler told you with a warning/error.

Therefore the only correct use of the freep function in your question is this:

char *s = malloc(5);
void* vptr = s;
freep(&vptr);

And then we can see why freep is problematic - the bad void** API means that s is still a dangling pointer because only vptr points to null now. Just forgot about this function, it was a bad idea and poorly implemented.

A call for sanity is to forget all about that function and instead write readable standard C:

free(s);
s = NULL;

There exists no reason why you can't write this instead of designing some bad API bloat function as an abstraction layer over 2 lines of readable C code.

A macro would work too, but it's good practice to avoid mysterious macros over well-known standard C.

英文:

The only generic object pointer type that C supports is void*. This special ability of void* does not apply recursively to void**. Passing a char** to a function expecting a void** is invalid C like the compiler told you with a warning/error.

Therefore the only correct use of the freep function in your question is this:

char *s = malloc(5);
void* vptr = s;
freep(&vptr);

And then we can see why freep is problematic - the bad void** API means that s is still a dangling pointer because only vptr points to null now. Just forgot about this function, it was a bad idea and poorly implemented.

A call for sanity is to forget all about that function and instead write readable standard C:

free(s);
s = NULL;

There exists no reason why you can't write this instead of designing some bad API bloat function as an abstraction layer over 2 lines of readable C code.

A macro would work too, but it's good practice to avoid mysterious macros over well-known standard C.

答案3

得分: 1

以下翻译好的部分:

#define freep(p) {free(p); p=NULL;}
英文:

Something like this might do the job:

#define freep(p) {free(p); p=NULL;}

答案4

得分: 1

作为特例,C语言规定void *类型与所有其他对象指针类型之间存在自动转换。这并不是一种模式,而是专门为void *类型提供的规定。也就是说,对于void **类型没有类似的特例。

你可以手动(通过类型转换)在不同的对象指针类型之间进行转换,比如在char **void **之间进行转换。一些编译器甚至可能隐式提供这样的转换,这构成了一种语言扩展。但是通过这样转换后的指针进行写操作违反了严格别名规则,会产生未定义行为。<sup>*</sup>

总的来说,不能将类型为void f(void **)的函数用作释放内存并将指针设置为NULL的通用函数。替代方法包括:

  • 使用宏代替:

    #define free(p) do { free(p); p = NULL; } while (0)
    
  • 传递一个额外的参数,传达指针的实际目标类型:

    enum target_type { VOID, CHAR, INT, DOUBLE };
    void freep(void *pp, enum target_type type) {
        switch (type) {
            case VOID: {
                void **p = (void **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case CHAR: {
                char **p = (char **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case INT: {
                int **p = (int **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case DOUBLE: {
                double **p = (double **) pp;
                free(*p);
                *p = NULL;
                break;
            }
        }
    }
    

    当然,这需要你预先选择支持哪些指针类型,所以它并不是完全通用的。但如果你需要类似的功能,可能可以考虑使用X宏来生成枚举定义和匹配函数实现,以减少样板代码。

  • 原则上,你也可以使用类型通用宏作为多个类型特定的释放和置空函数的前端,但我认为没有太多理由优先考虑这种方法,相对于前面的选项。如果你可以接受宏,那么前者要简单得多,如果不能接受宏,那就不要考虑了。

<sup>*</sup> 也许有人会争论,通过void **写入char *——*voidpp = NULL——是允许的情况,因为要求char *void *具有相同的表示和对齐要求,并且通常可以互换使用。但即使接受这一点,这仍然是一个特例。这并不能完全解答你的问题。

英文:

As a special case, C specifies that there are automatic conversions between void * and all other object-pointer types. This is not any kind of pattern, but rather a provision specifically for type void *. That is, there is no analogous special case for void **.

You can manually (via typecast) convert among different object pointer types, such as between char ** and void **. Some compilers may even provide such conversions implicitly, which constitutes a language extension. But writing through such a converted pointer violates the strict aliasing rule, producing undefined behavior.<sup>*</sup>

Overall, no, you cannot make a function of type void f(void **) serve as a general-purpose function for freeing memory and setting pointers to NULL. Alternatives include:

  • use a macro instead:

    #define free(p) do { free(p); p = NULL; } while (0)
    
  • pass an additional argument that conveys the pointer's actual target type:

    enum target_type { VOID, CHAR, INT, DOUBLE };
    void freep(void *pp, enum target_type type) {
        switch (type) {
            case VOID: {
                void **p = (void **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case CHAR: {
                char **p = (char **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case INT: {
                int **p = (int **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case DOUBLE: {
                double **p = (double **) pp;
                free(*p);
                *p = NULL;
                break;
            }
        }
    }
    

    Of course, this requires you to choose in advance which pointer types are supported, so it is not altogether general. But if you want something like this then it might be a good use case for X macros to generate the enum definition and matching function implementation without so much boilerplate.

  • In principle, you could also use a type-generic macro as a front end to multiple type-specific free-and-nullify functions, but I don't see much reason to prefer that over both the preceding options. If you're ok with a macro, then the former is much simpler, and if you're not then you're not.


<sup>*</sup> One might argue that writing a char * through a void ** -- *voidpp = NULL -- is an allowed case, given that char * and void * are required to have the same representation and alignment requirement, and are generally intended to be interchangeable. But even if one accepts that, it is (another) special case. That does not address your question in its full generality.

huangapple
  • 本文由 发表于 2023年6月29日 22:12:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76581871.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定