c++ uint8_t[4]到float类型转换技巧和细节
c++ uint8_t[4]到float类型转换技巧和细节
![c++ uint8_t[4]到float类型转换技巧和细节](/_next/image?url=%2Fimages%2Fposts%2Fuint8-to-float-cover.jpg&w=3840&q=75)
实现代码
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