返回文章列表
·Johan

c++ uint8_t[4]到float类型转换技巧和细节

c++ uint8_t[4]到float类型转换技巧和细节

c++ uint8_t[4]到float类型转换技巧和细节

实现代码

static float uint8_to_float(const uint8_t data[4]){
       uint32_t combined = (static_cast<uint32_t>(data[3]) << 24) |
                           (static_cast<uint32_t>(data[2]) << 16) |
                           (static_cast<uint32_t>(data[1]) << 8)  |
                            static_cast<uint32_t>(data[0]);
       float result;
       memcpy(&result,&combined,sizeof(result));
       return result;
}

先将4个uint8_t类型数据重新拼接成一个32位整数,然后重新解释内存,直接将2进制数据拷贝到result(float类型)不进行数值转换,仅仅是内存布局的重新解释,类似于reinterpret_cast)

需要注意的细节

在进行字节拼接时需要注意字节序

这里代码是基于小段序的排列,如果是大端序,需要调整拼接顺序

为什么需要这样做

1.应用场景

在网络传输,文件存储或硬件设备中通常数据以uint8_t形式传输,接收到数据后需要转换成float类型然后在对数据进行下一步处理

2.避免未定义行为(UB)

直接对指针进行类型转换,如((float)data),可能会引发对齐问题或严格命名规则冲突,使用memcpy处理排布内存是一种安全的方式,不会触发未定义行为

3.为什么使用memcpy进行内存重新排布

对于uint32_t->float的类型转换有多种实现方法,现在对每种方法进行分析

<1>memcpy

memcpy(&result,&combined,sizeof(result))

这段代码的本质是一种内存拷贝,是一种安全的方式,无未定义行为

<2>reinterpret_cast

float result = *reinterpret_cast<float*>(&combined)

这种方法是一种可行的方法,但是有违反严格别名规则的风险

为什么有这种风险,可以看以下这段代码

uint32_t combined=0x4048F5C3;
float read_as_float(){
    return *reinterpret_cast<float*>(&combined);
}
void modify_as_uint32(){
    combined = 0x40000000; // 修改为2.0f
}
int main(){
    float f = read_as_float();
    modify_as_uint32();
    std::cout<<f;
}

combined的静态类型是uint32_t

通过float*访问他违反了"不能通过不同类型指针访问同意同一内存"的规则。

编译器在优化时可能假设combined不会被float*修改,导致错误优化,最后输出结果为3.14,而不是修改后的2.0

float f = *reinterpret_cast<float*>(data);

使用强制类型转换指针也可以直接实现类型转换,但是这样作有风险

风险1:float通常要求4字节对齐,但data可能未对齐(例如网络接收数据)

在ARM等架构上,未对齐会引发硬件异常。

风险2:c++标准规定,通过一种类型的指针访问另一种类型的数据是未定义行为(UB)

编译器可能优化出错(如-02下结果异常)

风险3:假设内存布局一致,但是不同平台的字节序会不一致

<3>std::bit_cast

#include<bit>
float result = std::bit_cast<float>(combined);

c++20标准引入了std::bit_cast,可以安全地重新解释内存,作用和memcpy类似,但更简洁,且编译器可查,但是很多微型嵌入式设备flash较小,为了避免额外引入库带来的内存花销,所以最佳方案是memcpy

← 更多文章

johan's blog