c/c++使用字节时,易出错点梳理总结

简介: 作为一名 C/C++ 程序员,字节是我们天天都要与之打交道的一个东西,我们甚至和它熟悉到几乎已经忘记了它存在的地步,不同的人接连掉入字节埋下的陷阱, 且问题都较为严重,有些问题甚至定位需要花费1、2天的时间,甚至不乏一些经验十分丰富的老员工,这深深引发了我的好奇心,不得不重新认识它、重新学习它。以.

作为一名 C/C++ 程序员,字节是我们天天都要与之打交道的一个东西,我们甚至和它熟悉到几乎已经忘记了它存在的地步,不同的人接连掉入字节埋下的陷阱, 且问题都较为严重,有些问题甚至定位需要花费1、2天的时间,甚至不乏一些经验十分丰富的老员工,这深深引发了我的好奇心,不得不重新认识它、重新学习它。以下总结了几年来测试过程中使用字节时的常见问题及收集了一些字节相关的资料总结了以下内容,文章中若有不妥之处,欢迎大牛指正,大家相互学习,让产品质量做到更好,大家一起安心喝咖啡。 

一、      什么是字节

字节(Byte)是计算机信息技术用于计量存储容量和传输容量的一种计量单位,一个字节等于8位二进制数。

二、      使用字节时的易出错点
1.数据溢出问题

造成数据溢出的原因是数据类型使用不当

举一个测试中发现的bug看一下数据溢出问题造成的后果

BUG描述:在数据库中查询以6513260320690535872为key的某个值,数据库中找不到这个key,反而多了个3228040640的key 

BUG影响:严重,直接影响了来源url,来源关键词的数据,影响几十万站长的数据。

BUG分析:第一感觉就是6513260320690535872这个数值太大了,是不是溢出了?于是去代码里看了下,果不其然,是溢出的问题

 

 从代码中可以看出pageTrack.page是uint32类型的数据,event->url是uint64位类型的数据,uint32数据类型范围是0到4294967295;而当event->url等于6513260320690535872并且赋值给pageTrack.page,超出了uint32的最大值, 4294967295。

这样势必会导致数据溢出。

上面的数字6513260320690535872太大,不便分析,为了分析方便,下面举个简单的例子看一下数据是如何溢出的,以下程序输出结果为i= 4294967298 、j=2;

把变量i转换为二进制后

i: 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000010

而由于j只有32位存储空间,只能接收蓝色部分数据

00000000 00000000 00000000 00000010,于是转化为十进制输出结果就变成2了。

同一进程内如果编程时注意一下很少出现数据溢出问题,造成这种问题的原因主要是两个进程间传递数据,而两个进程相关的内容是不同的人实现的,负责接收数据模块的开发人员由于对发送端业务不了解,为了减少使用内存空间,盲目的定义了数值表示范围更小的数据类型的变量去接收数据。

 2.主机字节序与网络字节序不一致,未进行字节序一致性转换带来的问题

 测试中发现的一个bug:

BUG描述:接收数据时,想得到某一个ip地址的值(10.131.6.202),发现结果是反的

BUG影响:严重,数据发给某一广告系统业务,并经过分析将一些信息推荐给用户,ip是广告中重要的数据,涉及到用户的地域,地域决定了气候。

比如冬天,推荐一件衣服,用户所在地在北方,本应该推荐一件冬季的衣服,然而计算反了的ip所在地在南方,推荐的是一款夏季衣服,广告精准度可想而知。

BUG原因:服务器的处理器是intel系列,采用小端字节排序方式,经过网络传输(网络字节序采用大端字节序),数据传输过程中发生了位序变化。

字节序问题主要出现在数据在不同平台之间进行交换时,交换的途径可能是网络传输,也可能是文件复制。

下面是一些字节排序的知识:

字节排序按分为大端和小端

大端(big endian):低地址存放高有效字节

小端(little endian):低地址存放低有效字节

现在主流的CPU,intel系列的是采用的little endian的格式存放数据,而motorola系列的CPU采用的是big endian,ARM则同时支持 big和little,

网络编程中,TCP/IP统一采用大端方式传送数据;

大端和小端的方式(下面的例子机器是32位windows的系统,处理器是intel的)

对于一个int型数0x12345678, 为方便说明,这里采用16进制表示,这个数在不同字节顺序存储的CPU中储存顺序如下:

高有效字节——>低有效字节: 12 34 56 78

低地址位 高低址位

大端: 12 34 56 78

小端: 78 56 34 12
3.字节对齐使用不当引发的问题

字节对齐(alignment)是CPU在性能方向所面临的一个非常重要的问题。有些处理器能自动的处理不对齐数据的访问,但是,有些处理器却无法处理。当处理器无法处理对齐问题时,其将引发一个异常(exception),当然从程序的角度来说就是出错(crash)。

      什么是对齐?

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

      为什么要对齐?

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

下面举个例子说明一下为什么进行字节对齐能提高运行效率

在一个32位处理器,指定4字节对齐的条件下,请看下面的例子

struct

{

char c;

int b;

} align_test;

align_test 内存布局示意图如下:

 
   

说明:每个表格代表一个字节的空间

下面看一下采用字节对齐时和不采用字节对齐时,cpu对于内存的访问次数有何不同,对于32位处理器,其数据总线是32位的,因此,cpu从内存中存取数据时是一次读入4个字节。

首先看一下采用字节对齐时的情况,可以看出,当cpu需要分别访问变量a和变量b时,都只需要分别进行一次内存存取(32位处理器, cpu从内存中存取数据时是一次读入4个字节)。

再来看一下不采用字节对齐时的情况,可以看出,cpu访问变量a时,只需进行一次内存操作,而变量b则可能需要两次内存操作,因为变量b跨越了4字节的边界。说有可能,是因为有这种情况,对b进行访问之前,可能刚好完成了对a的访问,对a访问时,b0,b1,b2也同时读入(或写出)了,这种情况下,只需要读入(或写入)b3即可。

更为糟糕的是如果b边界不对齐,还得将其合成一个4字节(一部分来自一个4字节中的b0,b1,b2 ,另一部分来自另一个4字节的b3 ),这又增加了程序的复杂性。

从以上来看,采用字节对齐能提高cpu存取数据的性能,但凡事有利有弊,在提高cpu存取数据的性能的同时,也需要耗费更多的内存空间,这也是空间和时间的博弈。

      字节对齐使用不当时,容易引发的问题

1) #pragma pack(n)和#pragma pack()注意成对使用,否则可能程序运行时会crash掉。

Bug描述: intel系列处理器上,32位机器正常运行,64位机器每次运行都会crash掉

Bug影响: 严重,定位bug耗时2天,及重写代码的惨痛代价

Bug原因:#pragma pack(n)和#pragma pack ()未成对使用,

以下代码可以看到只用了#pragma pack(n),而没在rta命名空间后使用#pragma pack ()。

#pragma pack(n)等价于#pragma pack(push,n),//指定按n字节对齐

#pragma pack ()等价于#pragma pack(pop),   //取消指定对齐,恢复缺省对齐

2)类体或结构体的合理定义

从结果可以看出为结构体A和B分配的内存大小分别是8、12


结构体A在变量b后面进行了1个字节的补齐

结构体B分别在变量a后面进行了3个字节的补齐及在变量c后面进行了2个进行的补齐

可以看出同样的类体A和类体B,只是变量a和变量b一个位置的交换,就多耗费4字节的内存空间,想象一下,如果定义一个B类体的数组,每个数组有上亿或更大的大小,那么将浪费很多内存资源。

字节对齐的规则比较多这里不多说了,推荐一篇文章(不是我写的,呵呵),如有疑问可以联系少特,大家一起交流学习。

http://www.cnblogs.com/repository/archive/2011/01/13/1933721.html

      针对字节对齐,在编程中如何考虑?

如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:

struct A{

char a;

char reserved[3];//使用空间换时间

int b;

}

reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.

编程时,字节使用不当带来的影响多数是严重的,上面的几个bug任何一个暴漏到现网,损失都是惨重的,灾难性的。最后再总结下编程中使用字节时,易出错点:

1.数据溢出问题的考虑
2.主机字节序与网络字节序是否一致性的考虑
3.字节对齐使用不当引发的问题

1)#pragma pack(n)和#pragma pack()注意成对使用

2)类体或结构体中不同类型变量顺序的合理安排

该文章转载自阿里巴巴技术协会(ata)精选集,作者:少特

目录
相关文章
|
C++
c++ 数据字节
#include using namespace std; void main() { cout
850 0
|
15天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
21天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
27天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4
|
28天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
66 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
29 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
23 1