深入理解C语言(二)——比特序和字节序

比特序的概念

我们知道一个字节有8位,也就是8个比特位。从第0位到第7位共8位。比特序就是用来描述比特位在字节中的存放顺序的。字节中比特位的顺序有两种, LSB 0 位序和MSB 0 位序。

LSB是指 least significant bit, LSB 0 位序是指:字节的第0位存放数据的least significant bit,即我们的数据的最低位存放在字节的第0位。因此下图中的数值为0x95,对应十进制149。

LSB 0: A container for 8-bit binary number with the highlighted least significant bit assigned the bit number 0

MSB是指 most significant bit。MSB 0 位序是指:字节的第0位存放数据的most significant bit,即我们的数据的最高位存放在字节的第0位。下图中MSB在最左侧,因此下图中的数值同样为0x95。如果下图中最左边的1是LSB,则数值就变成0xa9

MSB 0:A container for 8-bit binary number with the highlighted most significant bit assigned the bit number 0

字节序的概念

如果计算机中所有的数据都可以在一个字节byte中描述,那么就不存在字节序的问题。正是因为存在多字节数据,所以字节之间也存在顺序问题,也就是字节序。由于历史的原因,业界存在两种字节序标准:Big-Endian(大端)和Little-Endian(小端)。Power PC是大端,X86是小端,MIPS ARM均可以通过寄存器设置字节序。

所谓大端就是高位在低字节,低位在高字节,小端则与此相反。例如0x12345678在大端和小端CPU中的字节排布如下图所示:

从上图可知,大端更符合我们的思维方式,所见即所得。但实际上字节序没有优劣之分。纯粹是历史原因导致硬件间的不一致。

比特序通常与字节序一致,也就是说,大字节序系统上,高比特位存放在低比特地址,在小字节序系统上,高比特位存储在高比特地址。

注意:字节序只是数据在内存中存放的顺序问题。我们对代码中数的理解(比如C代码中的常量),与CPU对通用寄存器中数据的理解方式是一样的。都是右边为低位(Least Significant Bit,缩写LSB)左边为高位(Most Significant Bit,缩写MSB)。CPU对数据位移等操作都是基于这种理解,与字节序无关。只有将寄存器数据放入内存或从内存取出时,才会面临字节序问题,也就是LSB是放在低内存地址还是高内存地址。

判断本机字节序的方式

代码获取本机字节序有多种方式

方式一:通过将int类型强制类型转换成char单字节,通过判断起始存储位置可以获取本机字节序。

示例:

BOOL IsBigEndian()  

{  

    int a = 0x1234;  

    char b =  *(char *)&a;   

    return (b == 0x12 ? TRUE : FALSE);

}

方式二:联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以获得本机字节序。

BOOL IsBigEndian()  

{  

    union NUM  

    {  

        int a;  

        char b;  

    }num;  

    num.a = 0x1234;  

    return (num.b == 0x12 ? TRUE : FALSE);

}

字节序对开发者的影响

一、考虑代码兼容不同字节序的CPU

二、考虑数据在不同字节序的系统中流转

例如:

a. 网络数据传输

b. CPU与外设或其他板内其他芯片的通信或共享内存

三、考虑本机自身对内存数据的存取不一致

a. 指针强制类型转换后的读写

b. 联合体不同位宽的重叠数据访问

c. 调试时查看内存

字节序——大小端转换

     比如32位的整形数字 0x12345678,在内存中存储为:

     低地址   | 0x78 | 0x56 | 0x34 | 0x12 | 高地址

     如果为大端模式,那么在内存中存储为:

     低地址   | 0x12 | 0x34 | 0x56 | 0x78 | 高地址

所以按照一种字节序读出为0x12345678的值,转换为另一种字节序后就是0x78563412

按照大小端转换的思想,可以轻松自定义类似的转换宏:

#define    NTOHL(val)          ((((val) & 0xff) << 24) |\

                               (((val) & 0xff00) <<  8) |\

                               (((val) & 0xff0000) >>  8) |\

                               (((val) & 0xff000000) >> 24))

字节序——网络字节序

在网络传输过程中,本端CPU无法了解到对端CPU的字节序,因此产生了“网络字节序”的概念——网络字节序采用 Big-Endian(大端)。网络字节序对应的概念是“主机字节序”,即本CPU访问内存时的字节序

读写用于网络通信的数据,都需要做“主机字节序”与“网络字节序”之间的转换。这包括下面两个场景

  • 当两台主机通信时,发送数据前需要将数据转换成为网络字节序后再进行传输。
  • 当两台主机通信时,接收数据后需要将数据从网络字节序转换到本机后再读内容

数据在网络字节序与主机字节序之间互相转换的函数或宏(在使用big endian类型的系统中这些函数会定义成空宏)如下:

一些栗子:

  • htonl()      把32位值从主机字节序转换成网络字节序
  • htons()      把16位值从主机字节序转换成网络字节序
  • ntohl()      把32位值从网络字节序转换成主机字节序
  • ntohs()      把16位值从网络字节序转换成主机字节序

使用举例:

typedef struct tag_MSG {

        U8 type;

        U8 opcode;

        U16 arg;

        U32 result;

} MSG_S

MSG_S msg;

//发送消息时按网络字节序填充消息

msg.type = 2;

msg.opcode = 6;

msg.arg = htons(129);

msg.result = htonl(255);

 //接收解析消息时将网络字节序转换成主机字节序

注意:单字节的对象不需要做大小端转换,可以回忆一下字节序的定义

字节序——外设总线字节序

外设总线是指在系统上连接CPU,外部设备和其他不同设备的中间媒体。总线的字节序是由总线协议定义的,其他设备必须遵守。以一个小字节序PCI总线为例。在32个地址/数据总线Line AD[31:0]中,总线需要连接32比特的设备,高比特数据线连接到AD31,低比特数据线连接到AD0。大字节序总线协议则相反。对于PCI总线,协议要求PCI设备实现一个配置空间。这是一组配置寄存器,它们和总线具有相同的字节序。像所有设备必须遵守总线字节/比特大小端规则一样,CPU也必须如此。如果CPU以不同于总线的大小端方式工作,总线控制器/桥通常需要进行大小端转换。

这里除了字节序以外,还涉及到比特序的问题,问题会更加复杂,我们在下一篇位域的章节中再展开讨论。

字节序——存取不一致

通过指针操作不同基本数据类型情况(涉及指针强转),需要考虑字节序问题。

Typedef struct _S {

       U32 i;

       U8 c1;

       U8 c2;

U8 c3;

U8 c4;

}

指针p原本指向结构体起始地址,为了使用4字节原子操作来把修改c3为0,就涉及到大小端的区别

word = *((U32*)p+1);

在大端情况下,修改c3为0:

word_new = word & 0xffff00ff;

在小端情况下,修改c3为0;

Word_new = word&0xff00ffff

最后再用4字节原子操作整个word,实现对c3成员的原子读写。

字节序——编译器的支持

以GCC为例,

GCC通过以下预定义宏可以区分大小端:

__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__时是小端模式

__BYTE_ORDER__==__ORDER_BIG_ENDIAN__时是小端模式

__BYTE_ORDER__,__ORDER_LITTLE_ENDIAN__,__ORDER_BIG_ENDIAN__都是gcc预定义的宏,在代码中可以直接使用。

通过GCC命令” gcc -posix -E -dM - </dev/null”可以查看预定义宏内容

gcc还提供了大小端转换内置函数:

Built-in Function: uint16_t __builtin_bswap16 (uint16_t x)

Built-in Function: uint32_t __builtin_bswap32 (uint32_t x)

Built-in Function: uint64_t __builtin_bswap64 (uint64_t x)

_bswap16,_bswap32,_bswap64三个函数分别提供了16位,32位,64位数字的字节反转功能,正好可以用来实现16,32,64位数字的大小端转换

相对于自定义转换宏,使用内置函数进行大小端转换可以获得性能上的优势。这是因为现代的CPU都在指令级别对于字节序翻转类的操作提供了更高效的指令,从而可以大大的提升执行性能。

GCC编译器已经利用了这一点,提供了内置的函数。直接使用内置的函数,就可以提升性能。

字节序——总结

为了实现代码跨运行平台做到大小端无关,需要在代码中识别以上提到的两类场景

对于本机对内存数据读写不一致的场景,需要使用大小端编译宏区别对待

对于数据在不通系统间流转的场景,需要在数据发送前和接收后进行字节序转换。当然如果传输中涉及到位域,那么我们在下一篇中再进行讨论。

下一篇:

上一篇:

深入理解C语言(一)——字节对齐

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,下面是对c语言实验——一元次方程Ⅰ的回复: 这个实验主要是让学生通过c语言来实现一元次方程的求解。一元次方程的一般形式为ax²+bx+c=,其中a、b、c为已知数,x为未知数。求解一元次方程的公式为x=(-b±√(b²-4ac))/(2a)。 在c语言中,我们可以通过输入a、b、c的值,然后利用公式计算出x的值。具体实现可以使用scanf函数来输入a、b、c的值,然后使用sqrt函数来计算平方根,最后使用printf函数输出x的值即可。 需要注意的是,当b²-4ac小于时,方程无实数解,需要进行特殊处理。此外,当a为时,方程不是一元次方程,也需要进行特殊处理。 希望以上回复能够对您有所帮助。 ### 回答2: 题目要求我们使用C语言编写求解一元次方程的程,计算出方程的解并输出。一元次方程的标准形式为:ax²+bx+c=0,其中a,b,c均为已知系数,我们需要求解x的值。 解一元次方程的一般公式一般为x = (-b ± √b²-4ac) / 2a。在实现这个公式的时候需要注意以下几点: 1. 公式中存在一些需要计算的中间数值,比如b²-4ac和2a,需要提前计算好并存储。 2. 当方程无解或有且仅有一个实根时需要特判处理,否则程会出现错误结果。 3. 输入系数值时需要判断是否输入的为数字,避免程因为输入错误而崩溃。 以下是一个实现求解一元次方程的C语言示例: ```c #include <stdio.h> #include <math.h> int main() { double a, b, c, delta, x1, x2; printf("请分别输入一元次方程的三个系数:"); if(scanf("%lf%lf%lf", &a, &b, &c) != 3) { printf("输入错误,请重新输入!"); return -1; } delta = b * b - 4 * a * c; if(delta < 0) printf("此方程无实数解"); else if(delta == 0) printf("此方程有唯一实根:%lf", - b / (2 * a)); else { x1 = (-b + sqrt(delta)) / (2 * a); x2 = (-b - sqrt(delta)) / (2 * a); printf("此方程有两个实根,分别为:%lf和%lf", x1, x2); } return 0; } ``` 以上程通过输入三个系数求解出方程的解,并根据不同的情况输出结果,实现了求解一元次方程的功能。 ### 回答3: 这个实验要求我们用C语言编写一个解一元次方程的程。在正式开始编程前,我们需要了解一元次方程的基本形式以及解法。 一元次方程的一般形式为:ax² + bx + c = 0 其中,a,b,c为常数,x为未知数。解一元次方程的方法有多种,常见的有配方法、公式法和图像法等。在本实验中,我们采用公式法。 公式法的原理是:当ax² + bx + c = 0(a ≠ 0)时,方程的解为x = (-b ± √(b² - 4ac)) / 2a。 在编写程时,我们需要考虑到以下几点: 1. 用户输入的系数a,b,c可能为浮点型,因此需要用float或double类型来存储。 2. 在计算中,涉及到开方和除法运算,需要用到math.h头文件中的函数。 3. 当判别式(b² - 4ac)小于0时,方程无实数解,需要进行特殊处理并给出提示。当判别式等于0时,方程有两个相等的实数根。当判别式大于0时,方程有两个不等的实数根。 4. 在输出时,需要注意格式化输出,尽可能准确地显示根的值。 根据上述要点,我们可以开始编写程,具体实现方法可以参考以下代码: #include <stdio.h> #include <math.h> int main() { float a, b, c, delta, x1, x2; // 提示用户输入系数 printf("请分别输入一元次方程的系数a、b、c:\n"); scanf("%f%f%f", &a, &b, &c); // 计算判别式 delta = b * b - 4 * a * c; if (delta < 0) { printf("方程无实数解!\n"); } else if (delta == 0) { x1 = x2 = -b / (2 * a); printf("方程有一个实数根:x = %.2f\n", x1); } else { x1 = (-b + sqrt(delta)) / (2 * a); x2 = (-b - sqrt(delta)) / (2 * a); printf("方程有两个实数根:x1 = %.2f,x2 = %.2f\n", x1, x2); } return 0; } 通过运行以上程,我们可以得到一个解一元次方程的可执行文件,并得到方程根的输出结果。这个实验不仅让我们掌握了解一元次方程的方法,更让我们深入理解C语言的基本语法和编程思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Bluetangos

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值