在C语言中正确地将void指针转换为uint8_t* / uint16_t*

3
我创建了一个函数,打印表中的元素。表总共有256个元素,元素类型可能是: uint8_t / uint16_t 或者 uint32_t。我在函数中进行了一个无参数的实验。
void printf_crc_table(void *pt, uint8_t crc_length) {
    for (uint8_t i = 0; i < 16; i++) {
        for (uint8_t k = 0; k < 16; k++) {
            switch (crc_length) {
              case 16:
                printf("0x%04X, ", *(uint16_t*)pt);
                break;
              case 32:
                printf("0x%08X, ", *(uint32_t*)pt);
                break;
              default:
                printf("0x%02X, ", *(uint8_t*)pt);
                break;
            }
            pt++; //works only for uint8_t. For uint16_t it has to be pt+=2;
        }
        printf("\n");
    }
}

我遇到的问题是:增加指针地址+1 仅对 uint8_t1 的表格有效。

如果我使用uint16_t元素类型调用函数,则每隔一个值都不正确,将指针地址增加2 可以修复此问题。但是为什么?如何调用/重写函数以始终打印正确的值?

main中,我这样调用函数:

    printf_crc_table(crc8_0x07_t, 8);     // works fine
    printf_crc_table(crc16_0x1021_t, 16); // every second value is printed incorrectly.

我可以一直使用uint32_t并将简单的uint8_t转换为uint32_t,但我希望尽可能地利用微控制器的资源。是否使用uint32_t而不是void更好?


3
你不能对空指针执行算术运算!由于没有类型,编译器无法知道要增加多少。 - Fredrik
由于调用者显然知道 crc_length 是什么,因此您可以为每个编写一个函数。如果您担心代码空间,则不要使用双重循环。 - Weather Vane
1
@Fredrik:关于“你不能在空指针上进行算术运算!”这句话是错误的。事实上,C标准并没有定义对void指针进行算术运算。然而,它也没有禁止这样做。C标准鼓励对语言进行扩展,GCC定义了对void指针进行算术运算。您可以使用GCC对void指针进行算术运算。 - Eric Postpischil
1
@Fredrik:使用实际的C标准,而不是Stack Overflow的答案。除了在脚注中提到硬件“非法指令”外,“合法”和“非法”这些词在C标准中并不出现。C标准要求发出诊断消息,但不禁止C实现成功地翻译对void指针进行算术运算的程序。而且,事实上,GCC支持这一点:当你尝试它时,它确实发生了,因此是可能的。 - Eric Postpischil
@Fredrik:引用标准并不意味着已经正确地陈述了标准。我有一份官方的标准副本,并且刚刚搜索了一下“legal”和“illegal”。它们的使用方式就像我所说的那样。以下是标准的正确引用:“它[一个C实现]也可以成功地翻译一个无效的程序。”你链接的答案甚至表明,有几个编译器允许这样做。 - Eric Postpischil
显示剩余4条评论
3个回答

3

这里有一个便携版本。如果平台硬件允许不对齐访问,编译器将优化掉memcpy操作,不用担心。

#include <inttypes.h>

void printf_crc_table(const void *pt, const uint8_t crc_length)
{
    union
    {
        uint8_t  data8;
        uint16_t data16;
        uint32_t data32;
    }u32;
    const unsigned char *u8ptr = pt;

    for(uint8_t i=0;i<16;i++){
        for(uint8_t k=0;k<16;k++){
            switch (crc_length) {
                case 16:
                    memcpy(&u32.data16, u8ptr, sizeof(u32.data16));
                    printf("0x%04"PRIx16", ", u32.data16);
                    u8ptr += sizeof(u32.data16);
                    break;
                case 32:
                    memcpy(&u32.data32, u8ptr, sizeof(u32.data32));
                    printf("0x%08"PRIx32", ", u32.data32);
                    u8ptr += sizeof(u32.data32);
                    break;
                default:
                    
                    printf("0x%02"PRIx8", ", *u8ptr);
                    u8ptr++;
                    break;
            }
        }
        printf("\n");
    }
}

生成的代码如下:

x86允许不对齐访问并且编译器已经移除了memcpy,Cortex M0不允许不对齐访问,并且memcpy没有被优化掉。这是一个安全且可移植的版本。

https://godbolt.org/z/MzEWdE

如果你不想使用memcpy,可以用另一种方式实现:

void printf_crc_table(void *pt,uint8_t crc_length)
{
    union
    {
        uint8_t  data8[sizeof(uint32_t)];
        uint16_t data16;
        uint32_t data32;
    }u32;
    unsigned char *u8ptr = pt;

    for(uint8_t i=0;i<16;i++){
        for(uint8_t k=0;k<16;k++){
            switch (crc_length) {
                case 16:
                    u32.data8[0] = *u8ptr++;
                    u32.data8[1] = *u8ptr++;
                    printf("0x%04"PRIx16", ", u32.data16);
                    break;
                case 32:
                    u32.data8[0] = *u8ptr++;
                    u32.data8[1] = *u8ptr++;
                    u32.data8[2] = *u8ptr++;
                    u32.data8[3] = *u8ptr++;
                    printf("0x%08"PRIx32", ", u32.data32);
                    break;
                default:
                    printf("0x%02"PRIx8", ", *u8ptr++);
                    break;
            }
        }
        printf("\n");
    }
}

按照定义,字节访问始终是对齐的。如果硬件支持非对齐访问,现代编译器将用单一访问指令替换它们。

https://godbolt.org/z/eqnovf


不用担心memcpy,如果平台硬件允许非对齐访问,编译器会将其优化掉。优化到什么程度?你如何期望基于不对齐地址的倍数进行正确的算术运算?黑魔法吗? - David Ranieri
@DavidRanieri memcpy() 在这里运行良好。memcpy() 不需要指针对齐。 - chux - Reinstate Monica
@DavidRanieri,“memcpy(&u32.dataN, u8ptr, sizeof(u32.dataN))” 的使用在我看来确实是可移植的。您对这里“memcpy()”的使用有什么具体的不可移植之处呢? - chux - Reinstate Monica
@chux-ReinstateMonica 是的,我应该添加 sizeof(....) 而不是魔术数字。 - 0___________
1
@DavidRanieri 我不理解你的评论,请解释一下你实际的意思。我没有看到这段代码有任何问题。memcpy是大多数现代编译器都熟知的,它们会决定是否安全和有效地替换对memcpy的调用,直接进行内存读取。 - 0___________
@P__J支持波兰妇女 哎呀,我在“optimized out”中漏掉了一个单词,“out”,现在我明白这意味着被编译器删除,而不是优化改进的意思,对此我感到抱歉。 - David Ranieri

3

使用一个 void * 参数声明函数可以,但是由于它不修改指针指向的数据,应该将其声明为 const void *。即时转换指针并递增指针会使代码变得更加繁琐,你可以初始化正确类型的指针,并将其用于读取值和递增指针。

如果 crc_length 为16或32,则此代码假定函数使用正确对齐的指针 pt 调用。

由于你关心代码生成效率,你可能想检查将循环索引变量定义为 int8_t 是否比定义为 int 生成更小的代码。

正如 chux 所评论的那样,仅将 void * 指针赋值给类型与对象的有效类型不匹配的指针具有未定义的行为(在极少见的体系结构上),因此这里有一个修改后的版本,仅对指定的 crc_length 执行赋值操作:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

void printf_crc_table(const void *pt, uint8_t crc_length) {
    for (uint8_t i = 0; i < 16; i++) {
        for (uint8_t k = 0; k < 16; k++) {
            switch (crc_length) {
              case 16: {
                  // assuming pt is properly aligned and points to an actual array of uint16_t
                  const uint16_t *p16 = pt;
                  printf("0x%04"PRIx16", ", *p16++);
                  pt = p16;
                  break;
                }
              case 32: {
                  // assuming pt is properly aligned and points to an actual array of uint32_t
                  const uint32_t *p32 = pt;
                  printf("0x%08"PRIx32", ", *p32++);
                  pt = p32;
                  break;
                }
              default: {
                  const uint8_t *p8 = pt;
                  printf("0x%02"PRIx8", ", *p8++);
                  pt = p8;
                  break;
                }
            }
        }
        printf("\n");
    }
}

好的答案,除了"像你这样即时转换指针违反了严格别名规则"。强制类型转换的位置对于严格别名冲突没有特定的影响。重要的是生成的指针是否实际上指向适当类型的对象。可以使用即时强制类型转换编写正确的问题代码,但它肯定会比此处呈现的代码更难看、更混乱,而OP的代码不是这样的代码。 - John Bollinger
1
@JohnBollinger:好观点!回答已修改。 - chqrlie
1
@chqrlie 这段代码在许多需要正确访问对齐的平台上会失败。 - 0___________
1
@RobertoCaboni:我没有对你的帖子进行DV,但它们可能源于你增加void *指针,这是一个值得怀疑的gcc扩展。即使在你编辑后仍然存在一个错误:你忘记将pt16pt32pt8的更新值重新赋给下一个条目的pt - chqrlie
1
@P__JsupportswomeninPoland:如果对齐是一个指定的限制条件,违反该限制可能会导致未定义的行为。如果没有指定对齐方式,您的方法更加合适,即使在允许不对齐访问的体系结构上也是如此。始终假设最坏的情况是我通常支持的防御性编程方法,我可能会使用您的方法。我的目标是向OP展示一个简单的解决方案,该方案对于问题中指定的用途是可以接受的:表格始终有256个元素,并且元素类型可能是:uint8_t / uint16_tuint32_t. - chqrlie
显示剩余6条评论

2
我遇到的问题是仅适用于 uint8_t1 表时增加指针地址+1才能正常工作。

如果我们接受 pt++ 会使地址增加1,那么只有元素大小为1个单位(即一个字符的大小)的数据才会产生正确的结果。对于元素大小不同的数据,增加地址1不会导致指向下一个元素的指针。

也许这就是你错过的关键点。 void 不是自动“编译器将找出实际类型”的占位符。它是一种特定的类型,尽管是不完整的。指针算术是根据所指类型的大小来定义的。

这对你的代码是个问题:因为 pt 是指向 void 的指针,一个不完整的类型,涉及 pt 的指针算术未被定义。在评论中声称编译器甚至没有警告这一点是令人惊讶的,如果是真的,那么最好从中提高你请求的警告级别,或选择更好的编译器。

然而,一些流行的编译器实现了一种扩展,在这种情况下,它们将 void * 的算术运算视为一种特殊情况,与 char * 的算术运算相当。

我建议采用@chqrlie的答案的方法,假设在实践中,对齐问题可能不会产生影响。如果数据可能未正确对齐于元素类型,则需要重新考虑这一点。

但是,为了记录,下面是一个不同的变化你原始代码,以便得到正确的指针算术,同时更接近你的原始代码:

void printf_crc_table(void *pt, uint8_t crc_length) {
    for (uint8_t i = 0; i < 16; i++) {
        for (uint8_t k = 0; k < 16; k++) {
            switch (crc_length) {
              case 16:
                uint16_t *p16 = pt;
                printf("0x%04X, ", *p16);
                pt = p16 + 1;
                break;
              case 32:
                uint32_t *p32 = pt;
                printf("0x%08X, ", *p32);
                pt = p32 + 1;
                break;
              default:
                uint8_t *p8 = pt
                printf("0x%02X, ", *p8);
                pt = p8 + 1
                break;
            }
        }
        printf("\n");
    }
}

如果chqrlie的版本存在对齐问题,那么这个版本也存在同样的问题。所要说明的是,你可以通过一点努力,在保持数据指针为void *的同时,执行与底层元素类型相符合的访问和更新操作。


1
printf("0x%08X, ", *p32); 是16位int/unsigned的问题--在嵌入式处理器中仍然非常流行。更具可移植性的方式是使用PRIxN - chux - Reinstate Monica
1
这段代码根本无法编译。你不能在case语句内定义变量,除非将它们包装到复合语句({})中。 - 0___________
1
实际上,在大多数现代平台上,假设对齐问题不太可能对实践产生影响,memcpy可以解决所有问题而不会受到惩罚。 - 0___________

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接