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

在嵌入式编程数据通信场景中,因单片机数据传递限制,需对数据进行拆分与合并。博客先分析了C/C++中数据类型的长度和存储方法,包括整形的补码存储、unsigned修饰,浮点型的IEEE格式;接着介绍了利用位运算和指针进行数据拆分与合并的方法及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在嵌入式编程时,常常会遇到需要做数据通信的场景。单片机往往只支持一次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;
}

欢迎讨论~

<think>嗯,用户想了解C/C++中的modf函数用法和示例。首先,我需要回忆一下这个函数的基本信息。modf函数用于将一个浮点数分解成整数部分和小数部分,两部分的正负号都和原数相同。这个函数在C标准库math.h中定义,C++中则在cmath头文件里。 接下来,我应该确认函数的原型。C/C++中,modf的函数原型通常是这样的:double modf(double value, double *iptr);。其中,value是要分解的数,iptr是指向存储整数部分的指针。函数返回的是小数部分。 然后,我需要考虑用户可能的疑惑点。比如,参数iptr必须是一个有效的指针,不能是空指针,否则会导致未定义行为。另外,modf函数处理负数的情况是怎样的呢?比如,对于-3.14,整数部分应该是-3,小数部分是-0.14。这一点需要明确说明,避免用户误解。 接下来,举一个实际的例子会更有帮助。例如,分解3.14,整数部分是3,小数部分是0.14;分解-5.67,整数部分是-5,小数部分是-0.67。代码示例应该展示如何声明变量,调用函数,输出结果。需要注意的是,在C++中可能需要包含cmath头文件,且使用std命名空间,或者在调用时使用std::modf。 用户可能还会问到modf和类似函数(如frexp)的区别,或者如何在不同数据类型(如floatlong double)中使用对应的函数(modff和modfl)。这些扩展内容可以作为相关问题提出,帮助用户进一步学习。 另外,错误处理也是一个需要注意的地方。如果iptr是空指针,会导致什么问题?这时应该提醒用户确保指针有效。还有,modf函数是否会有精度损失的问题?一般来说,浮点数运算本身就可能存在精度问题,但modf的设计是为了正确分解整数和小数部分,保持两者的和为原数。 最后,我应该检查示例代码的正确性,确保语法正确,没有遗漏头文件或必要的命名空间。比如,C语言中需要包含math.h,而C++中则用cmath,且可能需要使用std命名空间。同时,输出时使用正确的格式说明符,如%f对应double类型。 总结起来,回答需要涵盖函数原型、参数说明、返回值、示例代码、注意事项以及相关问题,确保用户能够全面理解modf函数的用法,能够正确应用到实际编程中。</think>### C/C++中`modf`函数用法详解 `modf`是C/C++标准库中用于将浮点数分解为整数部分和小数部分的函数,两部分的正负号与原数一致。其函数原型为: $$double\ modf(double\ value,\ double\ *iptr);$$ #### 参数说明 1. `value`:待分解的浮点数 2. `iptr`:指向存储整数部分的指针 #### 返回值 返回小数部分,其绝对值小于1,符号与`value`相同 #### 示例代码(C语言) ```c #include <stdio.h> #include <math.h> int main() { double num = 3.14; double int_part, frac_part; frac_part = modf(num, &int_part); printf("原数: %.2f\n整数部分: %.0f\n小数部分: %.2f\n", num, int_part, frac_part); // 测试负数 double neg_num = -5.67; frac_part = modf(neg_num, &int_part); printf("\n负数分解:\n整数: %.0f\n小数: %.2f\n", int_part, frac_part); return 0; } ``` #### 输出结果 ``` 原数: 3.14 整数部分: 3 小数部分: 0.14 负数分解: 整数: -5 小数: -0.67 ``` #### 注意事项 1. 必须确保`iptr`是有效指针 2. 对于floatlong double类型,分别使用`modff`和`modfl` 3. 数学关系:$$value = integer\_part + fractional\_part$$ #### 应用场景 - 金融计算中的金额拆分 - 工程测量数据的整数/小数处理 - 信号处理中的波形分解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云端FFF

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

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

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

打赏作者

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

抵扣说明:

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

余额充值