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
904 0
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
156 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
247 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
283 12
|
8月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
173 16
|
9月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
8月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。