将指针转换为 void 指针并写入它是 UB 吗?

Is it UB to cast a pointer to a void pointer and write to it?

我正在研究一种在 C 中创建动态数组的方法,我想出了这个解决方案作为我希望我的 functions/macros 工作方式的一般结构:

//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;
}

使用通用 function/macro 组合以类型不可知的方式动态分配内存是否是一种可接受的方法?

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

这是未定义的行为,因为您正在将(例如)int ** 转换为 void ** 并取消引用它以生成 void *。自动转换 to/from a void * 而不是 扩展到 void **。 Reading/writing 一种类型作为另一种类型(在这种情况下,将 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;
}

因为一些罕见的实现对不同类型的指针使用不同的表示,所以标准不要求实现允许它们可以互换操作。相反,它将对此类操纵的支持视为 "popular extension",而支持是其管辖范围之外的 "Quality of Implementation" 问题。几乎所有用于远程通用平台的编译器都可以配置为支持该构造,而标准的作者希望为程序员提供 "fighting chance" [他们的话] 来编写可移植代码,他们明确表示他们不希望 "demean" 不是 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;