【问题标题】:Is it UB to cast a pointer to a void pointer and write to it?将指针转换为 void 指针并写入它是 UB 吗?
【发布时间】:2020-01-14 19:46:58
【问题描述】:

我正在研究一种在 C 中创建动态数组的方法,我想出了这个解决方案,作为我希望我的函数/宏如何工作的通用结构:

//dynarray.h
#define dynarray(TYPE)\
    struct{\
        TYPE *data;\
        size_t size;\
        size_t capacity;\
    }

int dynarray_init_internal(void **ptr, size_t *size, size_t *cap, size_t type_size, size_t count);

#define dynarray_init(ARR, SIZE) dynarray_init_internal(&ARR->data, &ARR->size, &ARR->capacity, sizeof(*ARR->data), SIZE)

//dynarray.c
int dynarray_init_internal(void **ptr, size_t *size, size_t *cap, size_t type_size, size_t count){
    *ptr = malloc(type_size*count);
    if(*ptr == NULL){
        return 1;
    }

    *size = 0;
    *cap = count;
    return 1;
}

这是一种可接受的方法来拥有一个通用的函数/宏组合,以一种与类型无关的方式动态分配内存?

我对此的唯一疑问是我不确定这是否是未定义的行为。我想这可以很容易地扩展到动态数组结构通常期望的其他功能。我能看到的唯一问题是,由于它是一个匿名结构,因此您不能将其作为参数传递到任何地方(至少很容易),但这可以通过创建一个定义动态数组的 dynarray_def(TYPE, NAME) 宏来轻松解决带有NAME 的结构并让它保存TYPE 的数据,同时仍然可以与上面列出的所有其他函数/宏样式一起使用。

【问题讨论】:

  • 你不能将TYPE** 传递给void**,这甚至不应该编译。
  • 已经有系统sizeof(void*) != sizeof(int*).

标签: c arrays macros undefined-behavior dynamic-arrays


【解决方案1】:

这是未定义的行为,因为您正在将(例如)int ** 转换为 void ** 并将其取消引用以生成 void *。与void * 的自动转换 扩展到void **。将一种类型读/写为另一种类型(在这种情况下,将int * 写为void *)是违规的。

处理这个问题的最好方法是将整个 init 例程变成一个宏:

#define dynarray_init(ARR, SIZE) \
do {\
    (ARR)->data = malloc(sizeof(*(ARR)->data*(SIZE));\
    if ((ARR)->data == NULL){\
        _exit(1);\
    }\
    (ARR)->size = 0;\
    (ARR)->capacity = (SIZE);\
} while (0)

编辑:

如果您希望避免使用类似函数的宏,则可以改用宏来创建函数及其使用的结构类型:

#include <stdio.h>
#include <stdlib.h>

#define dynarray(TYPE)\
struct dynarray_##TYPE {\
    TYPE *data;\
    size_t size;\
    size_t capacity;\
};\
\
int dynarray_##TYPE##_init(struct dynarray_##TYPE **ptr, size_t count){\
    *ptr = malloc(sizeof(*ptr)*count);\
    if(*ptr == NULL){\
        return 1;\
    }\
    \
    (*ptr)->size = 0;\
    (*ptr)->capacity = count;\
    return 1;\
}

// generate types and functions    
dynarray(int)
dynarray(double)

int main()
{
    struct dynarray_int *da1;
    dynarray_int_init(&da1, 5);
    // use da1
    struct dynarray_double *da2;
    dynarray_double_init(&da2, 5);
    // use da2

    return 0;
}

【讨论】:

  • 感谢您的回答,但我正在尝试专门这样做以避免使用类似函数的宏。我唯一的其他解决方案是让我的宏生成函数,但我也不太喜欢,但如果我真的想不出另一种方法来做到这一点,我可能最终会选择它。
  • @zee 查看我的编辑。您可以使用宏来生成函数。
【解决方案2】:

由于一些罕见的实现对不同类型的指针使用不同的表示,因此标准不要求实现允许它们可以互换操作。相反,它认为对这种操纵的支持是一种“流行的扩展”,支持是其管辖范围之外的“实施质量”问题。几乎所有远程通用平台的编译器都可以配置以支持该结构,虽然标准的作者希望给程序员一个“战斗机会”[他们的话]来编写可移植代码,但他们明确表示他们做到了不希望“贬低”不是 100% 可移植的程序。

但是请注意,某些优化器无法处理此类构造,除非完全禁用基于类型的别名分析,否则使用此类构造的任何程序都需要记录此类要求。另一方面,除非需要针对晦涩难懂的架构,否则最好使用构造并记录其使用情况,这通常比跳过障碍来适应劣质优化器要好。

注意,顺便说一句,即使是高质量的编译器也可能被一些涉及指针转换的非常棘手的使用模式绊倒。该标准的作者不想仅仅因为一些棘手和人为的使用模式可能会产生不正确的行为而禁止实现执行有用的优化,但他们希望实现能够识别用户实际使用的模式。例如,给定:

float f;
int *ip; float *fp;
int *ipp = (int**)(&fp);
...
void test(void)
{
  fp = &f;
  f = 1.0;
  **ip+=1;
  return f;
}

编译器无法识别对**ip 的写入实际上会影响float 类型的对象。然而,如果fp 的地址在写入f 和随后从中读取之间存储到ip,则在编写标准的时代优化编译器将认识到将T* 转换为U* 应被视为可能通过U* 访问的任何T* 类型对象上的潜在内存破坏者。我怀疑你的使用模式比前者更适合后者。

*ipp = someFloat;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-20
    • 2016-01-12
    • 2012-11-21
    相关资源
    最近更新 更多