C/C++中拆分long/float/double等数据并重新组合的方法

在嵌入式编程时,常常会遇到需要做数据通信的场景。单片机往往只支持一次8位的数据传递,为了传输较长的数据类型,只能先在主机将数据拆分,再在从机重新组合,这里介绍一些实用的数据拆分组合方法


一、数据类型分析

1、类型长度

C/C++中有多种数据类型,但不管什么类型的数据都是以二进制形式存储的,在不同的系统和编译器中,各种类型转换为二进制后的长度有时会不一样,可以利用sizeof函数来查看你的环境中数据类型的长度,如下:

#include "iostream"
#include "iomanip"
using namespace std;

int main()
{
	cout<<left;
	cout<<setw(18)<<"char:"<<sizeof(char)<<endl;
	cout<<setw(18)<<"unsigned char:"<<sizeof(unsigned char)<<endl;

	cout<<setw(18)<<"short:"<<sizeof(short)<<endl;
	cout<<setw(18)<<"unsigned short:"<<sizeof(unsigned short)<<endl;

	cout<<setw(18)<<"int:"<<sizeof(int)<<endl;
	cout<<setw(18)<<"unsigned int:"<<sizeof(unsigned int)<<endl;

	cout<<setw(18)<<"long:"<<sizeof(long)<<endl;
	cout<<setw(18)<<"unsigned long:"<<sizeof(unsigned long)<<endl;

	cout<<setw(18)<<"float:"<<sizeof(float)<<endl;
	cout<<setw(18)<<"double:"<<sizeof(double)<<endl;

	return 0;
}

运行程序,可以看到我的环境中各数据类型的长度如下
在这里插入图片描述

2、类型存储方法

从上面的示例中我们注意到,unsigned关键字不会改变类型长度,而且unsigned只能修饰整形数据,这些都是C/C++中类型存储方法决定的。

(1)整形

1、补码存储

整形数据在内存中用补码形式存储,补码定义最高位为符号位,0代表非负数,1代表负数。具体转换方法如下

非负数负数
直接转换为二进制,高位用0填充先得到此负数绝对值的补码(直接转二进制),然后最高位置1,再把除了最高位以外的所有数取反,最后对结果再加1

举例来说,定义一个int型的变量a

a=4a=-4
补码0000 0000 0000 0000 0000 0000 0000 0100补码1111 1111 1111 1111 1111 1111 1111 1100

a=4的情形一目了然,这里分析一下a=-4的情况是怎么得到的

  1. 首先得到a绝对值4的补码,注意int是4个字节:0000 0000 0000 0000 0000 0000 0000 0100
  2. 符号位置1:1000 0000 0000 0000 0000 0000 0000 0100
  3. 除符号位以外全部取反:1111 1111 1111 1111 1111 1111 1111 1011
  4. 整体加一:1111 1111 1111 1111 1111 1111 1111 1100

在C/C++中,用十六进制或八进制输出数据,即可看到补码的效果

#include "iostream"
#include "iomanip"
using namespace std;

int main()
{
	int a=-4;
	int b=4;
	cout<<left<<hex;
	cout<<a<<endl<<b<<endl;
	
	return 0;
}

结果如下图所示,与我们的分析相符
在这里插入图片描述

2、unsigned修饰

unsigned关键字强制程序不考虑符号位,但不会改变整形数据补码存储的存储方式。也就是说,程序会按上面的方法将变量值转换为补码,然后直接转为十进制数。
在这种情况下,-4会先被存为0xfffffffc,再转十进制为4294967292,这一点也可以编程验证

#include "iostream"
#include "iomanip"
using namespace std;

int main()
{
	unsigned int a=-4;
	cout<<left;
	cout<<a<<endl;
	cout<<hex<<a<<endl;
	return 0;
}

在这里插入图片描述
因此unsigned修饰过的数据类型与未经修饰过类型长度一样就显而易见了

(2)浮点形

浮点型数据采用IEEE格式,与整形的存储格式完全不同,也不能用unsigned进行修饰,具体可以参考这篇文章:
单双精度浮点数的IEEE标准格式

二、数据类型的拆分与合并

(1)利用位运算

说道数据的拆分与合并,本质上就是把数据按8位长度拆开与拼装,首先想到的就是利用位运算处理。
按位与&运算可以用来拆分,按位或|运算可以用来合并,关于位运算可参考我的这篇文章:C语言位运算应用实例

下面是一个利用位运算拆分与合并4字节长long型数据的例子

#include "iostream"
#include "iomanip"
using namespace std;

typedef unsigned char u8;
typedef long s32;

//拆分数据
void dataSplit(s32 data,u8 *buf)
{
	s32 temp=0xFF;
	for(int i=0;i<4;i++)
		buf[i]=(data & temp<<8*i)>>8*i;

/*	
	buf[0]=data & 0xFF;
	buf[1]=(data & 0xFF00)>>8;
	buf[2]=(data & 0xFF0000)>>16;
	buf[3]=(data & 0xFF000000)>>24;
*/

}

//拼接数据
void dataAssmeble(s32 *data,u8 *buf)
{
	s32 temp=buf[3];
	for(int i=2;i>=0;i--)
		temp=(temp<<8)|buf[i];
	
	*data=temp;
}

int main()
{
	s32 a=-1024;
	s32 res;
	
	u8 buf[4];
	dataSplit(a,buf);			//拆分,主机可以发送
	dataAssmeble(&res,buf);		//合并,从机接收后可以拼装
	
	cout<<"原始数据:"<<a<<endl;
	cout<<"拆分合并后:"<<res<<endl;

	return 0;
}

在这里插入图片描述
可见处理正确

需要注意的一点是,这种方法只适用于处理整形数据因为浮点型数据的存储比较特殊,强行规定了各个位域的含义,若直接进行位运算取出一部分,取出的数据无法被正确解释,所以浮点型不能直接位运算,也就不能直接用这种方法处理
看到这里有人可能会想:若想先把浮点型转成整形,处理后再转回来不就好了吗。注意,别忘了浮点型转整形时会丢失精度,这个方法也不太好

(2)利用指针

大家应该注意到了,这个问题在本质上涉及到如何把内存中的数据解释成变量值,在这一点上或许指针可以帮到我们。我们知道,当你用一个指针指向内存中的一段数据,这段数据就会被解释为这个指针的类型的变量值。这启发我们用以下方法处理:

  1. 不要进行任何类型转换,以免破坏原始数据
  2. 找到一个方法,可以在不破坏数据的情况下将其拆开为8位一组,这个需要利用指针
  3. 现在可以进行数据传输
  4. 接收到数据后,用位运算把它们按序拼接为一个足够长的整形
  5. 定义一个原变量类型的指针,指向拼接成的整形的地址
  6. 取出指针指向的变量值,这就是被发送的原始变量的值了

示例程序如下:

#include "iostream"
#include "iomanip"
using namespace std;


typedef unsigned char u8;
typedef float f32;
typedef unsigned long u32;

//拆分数据
void dataSplit(f32 data,u8 *buf)
{	
	for(int i=0;i<4;i++)
		buf[i]=(*((u8 *)(&data)+i));
}

//拼接数据
f32 dataAssmeble(u8 *buf)
{
	u32 temp=buf[3];
	for(int i=2;i>=0;i--)
		temp=(temp<<8)|buf[i];
	
	f32 *data=(f32*)(&temp); 
	return *data;
}

int main()
{
	f32 a=-3.456;
	f32 res;
	
	u8 buf[4];
	dataSplit(a,buf);			//拆分 
	res=dataAssmeble(buf);		//合并 
	
	
	cout<<"原始数据:"<<a<<endl;
	cout<<"拆分合并后:"<<res<<endl;
	
	return 0;
}

注意拆分与拼接的程序出现了变化,下面详细分析一下

1、数据的拆分

//拆分数据
void dataSplit(f32 data,u8 *buf)
{	
	for(int i=0;i<4;i++)
		buf[i]=(*((u8 *)(&data)+i));
}
  1. (&data)取出原始数据data的地址
  2. (u8 *)(&data),用一个u8(即unsigned char)型指针指向这个地址
  3. ((u8 *)(&data)+i),指针加减法会移动指向位置,这里按u8长度为一个单位进行移动,从而依次指向原始数据中的每一段u8数据
  4. (*((u8 *)(&data)+i)),将这个指针的值取出,也就是取出了原始数据中的每一段u8数据的值

2、数据的拼接

(1)分析
//数据拼接
f32 dataAssmeble(u8 *buf)
{
	u32 temp=buf[3];
	for(int i=2;i>=0;i--)
		temp=(temp<<8)|buf[i];
	
	return *(f32*)(&temp); 
}
  1. 将若干u8数据用位运算拼成一个长度相同的整形数据temp
  2. (&temp) 把temp的地址取出
  3. (f32*)(&temp),用一个(f32*)类型指针指向这个地址,确认了解释方法
  4. (f32)(&temp)取出这个指针的值
(2)一种常见错误

需要注意的是,以下写法是错的

//错误的拼接
void dataAssmeble(f32 *data , u8 *buf)
{
	u32 temp=buf[3];
	for(int i=2;i>=0;i--)
		temp=(temp<<8)|buf[i];
	
	data=(f32*)(&temp);	
}
//-----------------------------------------
//和这个犯得是一样的错误
void func(int a)
{
	a=10;
}

还记得初学C/C++时函数传参那部分的内容吗?看下面的fun函数,它并不能对a的实参产生影响,因为这里只是对局部变量形参a做了赋值,函数func退出后就没了
上面的程序也是一样的错误,我们只是对局部的形参指针*data进行了赋值,并没能影响实参指针

上面func函数的错误可以用指针来解决,dataAssmeble函数也可以类似地用双指针解决,参考如下:

//类似这种方法改正
void func(int *a)
{
	*a=10;
}

//--------------------------------------
#include "iostream"
#include "iomanip"
using namespace std;


typedef unsigned char u8;
typedef float f32;
typedef unsigned long u32;

void dataSplit(f32 data,u8 *buf)
{	
	for(int i=0;i<4;i++)
		buf[i]=(*((u8 *)(&data)+i));
}

void dataAssmeble(f32 **data , u8 *buf)
{
	u32 temp=buf[3];
	for(int i=2;i>=0;i--)
		temp=(temp<<8)|buf[i];
	
	f32 *p=(f32*)(&temp);//暂存
	**data=*p; 
}

int main()
{
	f32 a=-3.456;
	f32 res;
	f32 *p=&res;//暂存
	
	u8 buf[4];
	dataSplit(a,buf);			//拆分 
	dataAssmeble(&p,buf);		//合并 
	
	
	cout<<"原始数据:"<<a<<endl;
	cout<<"拆分合并后:"<<res<<endl;
	
	return 0;
}

欢迎讨论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云端FFF

所有博文免费阅读,求打赏鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值