1 位、字节与字
1.1 位
计算机内存的基本单元是位(bit)。可以将位看作电子开关,可以开,也可以关。关表示值0,开表示值1。8位的内存块可以设置出256种不同的组合,因为每一位都可以有两种设置,所以8位的总组合数为2×2×2×2×2×2×2×2,即256。因此,8位单元可以表示0-255或者-128到127。每增加一位,组合数便加倍。这意味着可以把16位单元设置成65536个不同的值,把32位单元设置成4294672296个不同的值,把64位单元设置为18446744073709551616个不同的值。
1.2 字节
字节(byte)通常指的是8位的内存单元。从这个意义上说,字节指的就是描述计算机内存量的度量单位,1KB等于1024字节,1MB等于1024KB。
1.3 字
计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word)。一个字通常由一个或多个(一般是字节的整数位)字节构成。例如286 微机的字由2个字节组成,它的字长为16,486微机的字由4个字节组成,它的字长为32位。
计算机的字长决定了其 CPU 一次操作处理实际位数的多少,由此可见计算机的字长越大,其性能越优越。
在存储器中,通常每个单元存储一个字,因此每个字都是可以寻址的。字的长度用位数来表示。
在计算机的运算器、控制器中,通常都是以字为单位进行传送的。宇出现在不问的地址其含义是不相同。例如,送往控制器去的字是指令,而送往运算器去的字就是一个数。 在计算机中作为一个整体被存取、传送、处理的二进制数字符串叫做一个字或单元,每个字中二进制位数的长度,称为字长。一个字由若干个字节组成,不同的计算机系统的字长是不同的,常见的有8位、16位、32位、64位等,字长越长,计算机一次处理的信息位就越多,精度就越高,字长是计算机性能的一个重要指标。目前主流微机都是32位机。 注意字与字长的区别,字是单位,而字长是指标,指标需要用单位去衡量。正象生活中重量与公斤的关系,公斤是单位,重量是指标,重量需要用公斤加以衡量。
8个位 (bit) 称为一个字节 (byte) ,两个字节称为一个字 (word),两个字称为一个双字 (dword),两个双字称为一个四字 (qword)。
2 整型
C++定义的基本整型包括char、short、int、long,和C++ 11新增的long long类型,此外特殊的布尔类型bool本质上也是整型。其中short是short int的简称,而long是long int的简称。
2.1 整型占用空间
各个整型所占的字节数是由编译器决定:
- 16位编译器
char : 1个字节
short : 2个字节
int : 2个字节
long : 4个字节
long long : 8个字节
- 32位编译器
char : 1个字节
short : 2个字节
int : 4个字节
long : 4个字节
long long : 8个字节
上述字节数可以使用sizeof运算符获取(运算符是内置的语言元素,对一个或多个数据进行运算,并生成一个值。例如,加号运算符+将两个值相加)。 为了避免在将 C++ 程序从一种环境移到另一种环境(包括在同一个系统中使用不同编译器)时引发问题,强烈建议在代码中使用如下重定义的基本整型(也就是不同平台下,使用以下名称可以保证固定长度): 1字节 int8_t ------ char 2字节 int16_t ------ short 4字节 int32_t ------ int 8字节 int64_t ------ long long
2.2 整型类型修饰符
C++提供了四个整型类型修饰符作为前缀,用来改变整型数据类型的含义。这些类型修饰符为:
- signed : 表示有符号型
- unsigned : 表示有符号型
- long : 表示长型
- short : 表示短型
2.3 数据范围
2.3.1 数据范围的宏定义
头文件climits( 在C语言中为limits.h) 中包含了关于整型范围的宏定义。具体如下表所示:
符号常量 | 含义 |
---|---|
CHAR_BIT | char 的位数 |
CHAR_MAX | char 的最大值 |
CHAR_MIN | char 的最小值 |
SCHAR_MAX | signed char 的最大值 |
SCHAR_MIN | signed char 的最小值 |
UCHAR_MAX | unsigned char 的最大值 |
SHRT_MAX | short 的最大值 |
SHRT_MIN | short 的最小值 |
USHRT_MAX | unsigned short 的最大值 |
INT_MAX | int 的最大值 |
INT_MIN | int 的最小值 |
UINT_MAX | unsigned int 的最大值 |
LONG_MAX | long 的最大值 |
LONG_MIN | long 的最小值 |
ULONG_MAX | unsigned long 的最大值 |
LLONG_MAX | long long 的最大值 |
LLONG_MIN | long long 的最小值 |
ULLONG_MAX | unsigned long long 的最大值 |
样例代码:
cpp
#include <iostream>
#include <climits>
int main(int argc, char *argv[])
{
printf("INT_MAX = %d\n", INT_MAX);
printf("INT_MIN = %d\n", INT_MIN);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("LLONG_MAX = %lld\n", LLONG_MAX);
printf("LLONG_MIN = %lld\n", LLONG_MIN);
printf("ULLONG_MAX = %llu\n", ULLONG_MAX);
return 0;
}
输出结果如下:
ini
INT_MAX = 2147483647
INT_MIN = -2147483648
UINT_MAX = 4294967295
LLONG_MAX = 9223372036854775807
LLONG_MIN = -9223372036854775808
ULLONG_MAX = 18446744073709551615
2.3.2 数据范围表
以32位机为例:
类型 | 位 | 范围 |
---|---|---|
char | 1 个字节 | -128 到 127 或者 0 到 255 |
unsigned char | 1 个字节 | 0 到 255 |
short | 2 个字节 | -32768 到 32767 |
unsigned short | 2 个字节 | 0 到 65535 |
int | 4 个字节 | -2147483648 到 2147483647 |
unsigned int | 4 个字节 | 0 到 4294967295 |
long | 4 个字节 | -2147483648 到 2147483647 |
unsigned long | 4 个字节 | 0 到 4294967295 |
long long | 8 个字节 | -9223372036854775808 到 9223372036854775807 |
unsigned long long | 8 个字节 | 0 到 18446744073709551615 |
2.3.3 超出数据范围的计算结果
超出数据范围的计算实际上是做循环计算,即 最大值+1 = 最小值,最小值+1 = 最大值。
2.3.3.1 有符号整数
以 int 为例,其他类型的行为类似。 代码:
cpp
#include <iostream>
#include <climits>
int main(int argc, char *argv[])
{
printf("INT_MAX = %d\n", INT_MAX);
printf("INT_MIN = %d\n", INT_MIN);
int val1 = INT_MAX;
val1 += 1;
printf("INT_MAX + 1 = %d\n",val1);
int val2 = INT_MIN;
val2 -= 1;
printf("INT_MIN - 1 = %d\n", val2);
return 0;
}
输出结果如下:
ini
INT_MAX = 2147483647
INT_MIN = -2147483648
INT_MAX + 1 = -2147483648
INT_MIN - 1 = 2147483647
2.3.3.2 无符号整数
以 unsigned int 为例,其他类型的行为类似。 代码:
cpp
#include <iostream>
#include <climits>
int main(int argc, char *argv[])
{
printf("UINT_MAX = %u\n", UINT_MAX);
unsigned int val = UINT_MAX;
val += 1;
printf("UINT_MAX + 1 = %u\n", val);
val = UINT_MAX;
val += 2;
printf("UINT_MAX + 2 = %u\n", val);
return 0;
}
输出结果如下:
ini
UINT_MAX = 4294967295
UINT_MAX + 1 = 0
UINT_MAX + 2 = 1
2.4 整型与字符数组相互转换
各个整型均占用1~8个字节的存储空间,其每一个字节都可以用一个16进制数表示,在序列化开发以及网络开发中,经常会使用到整型与字符数组的相互转换,这个过程需要了解大端小端的概念以及C++数据转换的方法。
2.4.1 大端小端的概念
大端(Big-Endian)和小端(Little-Endian),表示数据在存储器中的存放顺序。不同的 CPU、操作系统对待数据的存储方式各有不同,一般常见的操作系统都是小端,而通讯协议则是大端。
以16进制数 0x12345678(对应10进制数为 305419896 ) 为例:
大端法在内存中按字节依次存放为:12 34 56 78
小端法在内存中按字节依次存放为:78 56 34 12
2.4.2 整型转化为字符数组
代码(以 int 与 long long 为例,其他类型的转化过程类似):
cpp
#include <iostream>
#include <string>
void printBinaryCharToHex(const char* data, size_t len)
{
std::string ret;
static const char *hex = "0123456789ABCDEF";
for (uint64_t i = 0; i < len; i++)
{
ret.append("0x");
ret.push_back(hex[(data[i] >> 4) & 0xf]);
ret.push_back(hex[data[i] & 0xf]);
ret.append(" ");
}
printf("%s\n", ret.c_str());
}
int main(int argc, char *argv[])
{
int val1 = 0x12345678;
char* bytesVal1 = new char[sizeof(int)];
// int 型转化为字符数组
memcpy(bytesVal1, (char*)&val1, sizeof(int));
printf("short val1 = 0x12345678, char* bytesVal1 = ");
printBinaryCharToHex(bytesVal1, sizeof(int));
delete[] bytesVal1;
long long val2 = 0x1234567812345678;
char* bytesVal2 = new char[sizeof(long long)];
// long long 型转化为字符数组
memcpy(bytesVal2, (char*)&val2, sizeof(long long));
printf("long long val2 = 0x1234567812345678, char* bytesVal2 = ");
printBinaryCharToHex(bytesVal2, sizeof(long long));
delete[] bytesVal2;
return 0;
}
输出:
arduino
int val1 = 0x12345678, char* bytesVal1 = 0x78 0x56 0x34 0x12
long long val2 = 0x1234567812345678, char* bytesVal2 = 0x78 0x56 0x34 0x12 0x78 0x56 0x34 0x12
2.4.3 字符数组转化为整型
代码(以 int 与 long long 为例,其他类型的转化过程类似):
cpp
#include <iostream>
int main(int argc, char *argv[])
{
char bytesVal1[sizeof(int)] = { 0x78, 0x56, 0x34, 0x12 };
//字符数组转化为 int 型
int val1 = *(int *)bytesVal1;
printf("char* bytesVal1 = 0x78 0x56 0x34 0x12, int val1 = 0x%x\n", val1);
char bytesVal2[sizeof(long long)] = { 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12 };
//字符数组转化为 long long 型
long long val2 = *(long long *)bytesVal2;
printf("char* bytesVal2 = 0x78 0x56 0x34 0x12 0x78 0x56 0x34 0x12, long long val2 = 0x%llx\n", val2);
return 0;
}
输出:
arduino
char* bytesVal1 = 0x78 0x56 0x34 0x12, int val1 = 0x12345678
char* bytesVal2 = 0x78 0x56 0x34 0x12 0x78 0x56 0x34 0x12, long long val2 = 0x1234567812345678
3 浮点型
和 ANSI C ---样,C++也有3种浮点类型:float、double 和 long double。这些类型是按它们可以表示的有效数位和允许的指数最小范围来描述的。
3.1 十进制小数转二进制的方法
十进制小数转二进制的方法是将小数部分乘以2,将结果的整数部分作为二进制的下一位,然后将小数部分保留小数部分,重复以上步骤直到小数部分为0或达到所需的精度。例如,将0.625转换为二进制,首先将其乘以2得到1.25,整数部分为1,小数部分为0.25,再将0.25乘以2得到0.5,整数部分为0,小数部分为0.5,再将0.5乘以2得到1,整数部分为1,小数部分为0,因此0.625用二进制表示为0.101。
3.2 浮点型存储模型
3.2.1 浮点型数据的存储和读取的公式
根据IEEE754标准规定,浮点型数据的存储和读取的公式为:
ini
value = (-1)^S*M*2^E
其中 value 为浮点型数据的二进制值。
S 表示浮点型数据的正负,0表示正,1表示负。
M 为位于位于区间 [1,2) 的小数。这个数的第一位总是1,因此可以被舍去,只保存后面的 xxxxx 部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
E 表示为指数部分(小数点移动的位数)。指数 E 的情况比较复杂,其详细含义如下:
首先,E 为一个无符号整型(unsingde int)
这意味着,如果 E 为 8 位,它的取值范围为 0~255 ;如果 E 是 11 位,它的取值范围为 0~204 。但是科学计数法中的 E 是可以出现负数的,所以 IEEE754 规定,存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数是 127 ;对于 11 位的 E ,这个中间数是 1023 。
比如:2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即化为二进制为 10001001。
对于指数 E 从内存中取出还可以再分成三种情况:
(1) E 不全为 0 或不全为 1
这时,浮点数就采用下面的规则表示,即指数 E 的计算值减去 127(或1023),得到真实值,再将有效数字 M 前加上第一位的 1。
比如:
0.5(1/2) 的二进制形式为 0.1 ,由于规定正数部分必须为 1 ,即小数点右移 1 位,则为 1.0*2^(-1),其阶码为 -1+127=126,表示为 01111110 而尾数 1.0 去掉整数部分为 0 ,补齐 0 到 23 位 00000000000000000000000,则其二进制表示形式为: 0 01111110 00000000000000000000000
(2) E 全为0
这时,浮点数的指数 E 等于 1-127(或者1023) 即为真实值,有效数字 M 不再加上第一位的 1 ,而是还原为 0.xxxxx 的小数。这样做是为了表示 ±0 ,以及接近于 0 的很小的数字。
(3)E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位S);
C++ 使用 float 与 double 两种类型来实现上述的浮点型数据的存储和读取的公式。
对于 float 型数据,其长度是 4 个字节,右边 23 位用来表示小数点后面的数字,中间 8 位用来表示 e ,左边一位用来表示正负。
对于 double 型数据,其长度是 8 个字节,右边 52 位用来表示小数点后面的数字.中间 11 位表示 e ,左边一位用来表示正负。
类型 | 符号位 | 指数位 | 小数部分 | 指数偏移量 |
---|---|---|---|---|
float | 1 位(31) | 8 位(30~23) | 23 位(22~00) | 127 |
double | 1 位(63) | 11 位(62~52) | 52 位(51~00) | 1023 |
3.2.2 浮点型数据的存储和读取的样例
27.5的二进制为11011.1,使用科学计数法位:1.10111*2^4。 尾数(小数点后的数)10111,补够23位 1011 1000 0000 0000 0000 000 指数:4,加上127,就是131,二进制1000 0011 用二进制表示就是 (符号数位1位)0 (指数位8位)1000 0011 (尾数位23位)1011 1000 0000 0000 0000 000 写成二进制标准形式:0100 0001 1101 1100 0000 0000 0000 0000 写成16进制就是41 DC 00 00
3.2.3 数据范围的宏定义
头文件 cfloat 中包含了关于浮点型范围的宏定义。具体如下表所示:
符号常量 | 含义 |
---|---|
FLT_MAX | float 的最大值 |
FLT_MIN | float 的正最小值 |
DBL_MAX | double 的最大值 |
DBL_MIN | double 的正最小值 |
LDBL_MAX | long double 的最大值 |
LDBL_MIN | long double 的正最小值 |
样例代码:
cpp
#include <iostream>
#include <cfloat>
int main(int argc, char *argv[])
{
printf("FLOAT_MAX = %e\n", FLT_MAX);
printf("FLOAT_MIN = %e\n", FLT_MIN);
printf("DOUBLE_MAX = %e\n", DBL_MAX);
printf("DOUBLE_MIN = %e\n", DBL_MIN);
return 0;
}
输出结果如下:
ini
FLOAT_MAX = 3.402823e+38
FLOAT_MIN = 1.175494e-38
DOUBLE_MAX = 1.797693e+308
DOUBLE_MIN = 2.225074e-308
3.2.4 数据精度
float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是⼀个隐含着的"1",由于它是不变的,故不能对精度造成影响。
float:2^23 = 8388608,一共 7 位,这意味着最多能有 7 位有效数字,但绝对能保证的为 6 位,也即float的精度为 6~7 位有效数字;
double:2^52 = 4503599627370496,一共 16 位,因此 double 的精度为 15~16 位。
重点注意:这里的有效数字不是仅指小数部分,而是全部的数字。
举一个具体的例子:
cpp
float val1 = 1234567.12345f;
val1 += 0.00001;
printf("val1 = %f\n", val1);
上述代码的输出为:
ini
val1 = 1234567.125000
3.2.5 浮点数使用的两个注意事项
1、判断相等的处理
由于浮点数存入时很可能发生有效数值 M 二进制序列的截断以及被截去的序列的最高位的四舍五入而造成精度损失,所以两个浮点数直接用操作符进行比较很可能会得到不符合预期的结果。
浮点数的比较应该使用如下方式:
对于浮点数而言比较合适的精度为:0.000001
对于双进度浮点数而言比较合适的精度为:0.0000000000000001
因此可以定义两个宏:
cpp
#define ACCURACY_F 1e-6
#define ACCURACY_D 1e-16
判断浮点数是否等于 0 :
float 类型:if(fabs(f) <= ACCURACY_F );
double 类型:if(fabs(d) <= ACCURACY_D);
判断两个浮点数是否相等:
float 类型:if(fabs(f1 - f2) <= ACCURACY_F);
double 类型:if(fabs(d1 - d2) <= ACCURACY_D);
2、超过有效位数的运算
由于浮点数存入时可能发生数据截断,因此超过有效位数的浮点数进行加减运算是没有意义的。
cpp
float val1 = 1234567.12345f;
float val2 = 1234567.12344f;
if (val1 > val2)
{
printf("val1 > val2\n");
}
else
{
printf("val1 <= val2\n");
}
上述代码的输出为:
val1 <= val2
这是由于 val1 与 val2 在读入时都会被截断为 1234567.125000。从而导致最后的计算结果与预期不同。
3.3 浮点型与字符数组相互转换
浮点型与字符数组相互转换同样也要注意大小端的问题,float 类型 2.25 为例:
大端法在内存中按字节依次存放为:40 10 00 00
小端法在内存中按字节依次存放为:00 00 10 40
3.3.1 浮点型转化为字符数组
代码:
cpp
#include <iostream>
#include <string>
void printBinaryCharToHex(const char* data, size_t len)
{
std::string ret;
static const char *hex = "0123456789ABCDEF";
for (uint64_t i = 0; i < len; i++)
{
ret.append("0x");
ret.push_back(hex[(data[i] >> 4) & 0xf]);
ret.push_back(hex[data[i] & 0xf]);
ret.append(" ");
}
printf("%s\n", ret.c_str());
}
int main(int argc, char *argv[])
{
float val1 = 2.25f;
char* bytesVal1 = new char[sizeof(float)];
// float 型转化为字符数组
memcpy(bytesVal1, (char*)&val1, sizeof(float));
printf("float val1 = 2.25, char* bytesVal1 = ");
printBinaryCharToHex(bytesVal1, sizeof(int));
delete[] bytesVal1;
double val2 = 2.25;
char* bytesVal2 = new char[sizeof(double)];
// double 型转化为字符数组
memcpy(bytesVal2, (char*)&val2, sizeof(double));
printf("double val2 = 2.25, char* bytesVal2 = ");
printBinaryCharToHex(bytesVal2, sizeof(double));
delete[] bytesVal2;
return 0;
}
输出:
arduino
float val1 = 2.25, char* bytesVal1 = 0x00 0x00 0x10 0x40
double val2 = 2.25, char* bytesVal2 = 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x40
3.3.2 字符数组转化为浮点型
代码:
cpp
#include <iostream>
int main(int argc, char *argv[])
{
char bytesVal1[sizeof(float)] = { 0x00, 0x00, 0x10, 0x40 };
//字符数组转化为 float 型
float val1 = *(float *)bytesVal1;
printf("char* bytesVal1 = 0x00 0x00 0x10 0x40, int float = %f\n", val1);
char bytesVal2[sizeof(double)] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x40 };
//字符数组转化为 double 型
double val2 = *(double *)bytesVal2;
printf("char* bytesVal2 = 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x40, double val2 = %lf\n", val2);
return 0;
}
输出:
arduino
char* bytesVal1 = 0x00 0x00 0x10 0x40, int float = 2.250000
char* bytesVal2 = 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x40, double val2 = 2.250000