跨越数据类型的重重陷阱

2024-01-14 08:58

本文主要是介绍跨越数据类型的重重陷阱,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

数据类型是编程语言中最基本的构成元素,但却是最易被忽略的一环,程序员愿意把几乎100%的精力都花在算法研究、程序流控制等大环节上,却很少在数据类型问题上反复斟酌。 
   细节决定成败,一个螺丝钉的失误可能导致一个飞行器的毁灭,一个数据类型的错误同样可以让庞大的软件系统崩溃。 
    MISRA—c中关于数据类型的规则主要分为两个方面。一是数据类型相关的编程风格;二是不同数据类型之间的转换,后者是重点。这里介绍MISRA_C关于数据类型的部分规则,更多的规则请参考《MISRA-C:2OO4)》一书。 
    下文中凡是未加特殊说明的都是强制(required)规则.个别推荐(advisory)规则加了“推荐”标识。 
    在展开论述之前,先看两个问题,读者可以带着疑问阅读完本章内容。
问题1:执行以下程序,result_8的值是多少?
ulnt8_t porI=0x5a;
uint8一t resuh_8;
result_8=(~port)>>4;
/*注:uint8_t表示8位无符号整型*/

问题2:执行以下程序,d的值是多少?
uintl6_t a=10;
uin|16_t b=6553l;
uint32_t c=0;
uint32_t d;
d=a+b+c;
/*注:uintl6_t表示16位无符号整型,uint32_t表示32位无符号整型*/ 

数据类型相关的编程风格
规则6.3(推荐):必须用typedef显式标识出各数据
类型的长度和符号特性,避免直接使用标准数据类型。
例如,一个32位的整数系统,可定义如下:
typedef unsigned char  BOOLEAN;         
typedef unsigned char  uint8;           
typedef signed   char  int8;            
typedef unsigned short uint16;          
typedef signed   short int16;           
typedef unsigned long  uint32;          
typedef signed   long  int32;           
typedef float          FP32;            
typedef double         FP64;            


    之所以用intl6_t和uint32_t等代替signed short和unsigned int等标准数据类型标识符,是由于不同的编译器对标准数据类型的长度定义是不一样的。比如说一个16位系统,很可能就把short和int都定义成16位,long定义成32位,这与上文32位系统中标准数据类型的长度就不一致。用intl6_t和uint_32等标识符来定义变量,一方面增加了程序的可读性,使得程序员本人或其他读者都能对程序中数据的具体信息胸有成竹;另一方面也有助于程序在不同系统之间的移植,节省开发时间,减少隐患。规则7 1:不得使用八进制常数(O除外)或八进制转义符。
思考如下数组:
code[1]=109;
code[2]=100;
code[3]=O52
code[4]=O71;
/*注:八进制常数须在最高位加O*/

    code[3]的实际值是42(十进制),code[4]的实际值是57(十进制);但估计很多读者会把code[3]认成是52(十进制),code[4]认成是7l(十进制)。

    八进制数在C程序中使用的频率远小于十进制数和十六进制数,为了保证程序的可读性和安全性,程序员不允许使用八进制数以及八进制转义符。 


 数据类型转换
    如果程序员对数据类型的转换有很清晰的认识,并且在必要的地方做了正确的显式强制转换,那程序是安全的。但有时由于程序员的疏忽,或者是过于相信编译器的“智慧”程度,导致表达式中有很多隐式转换(即没有显式地强制转换),而这些隐式数据类型转换很可能就构成致命的漏洞。MISRA—C中数据类型转换规则的着眼点,即是避免有漏洞的隐式数据转换。

    在介绍MISRA—C关于数据类型转换的部分规则之前,先介绍整型操作数的“平衡(balance)”原则。所谓整型操作数“平衡”原则,即对于隐式表达式,编译器会按照既定规则对操作数进行位数扩充,其中int和unsined

int在整型表达式“平衡”过程中占重要地位。

    下面分析一个简单的隐式整型表达式c=a+b(假设a的存储位数不大于b的存储位数),编译器是这样来处理这个表达式的:

    如果b是短整型(即位数少于int,比如char、short等)或者整型(int或unsigned int),那a也是短整型或者整型,执行“+”运算之前,a和b都将被扩充为整型(int或者unsigned int),然后相加的结果赋给c(如果c不是int或者unsigned int类型,则这个赋值操作也会包含隐式的扩充或截断操作)。

    如果b是长整型(存储位数多于int),则a会被扩充为与b相当的长整型,再执行“+”运算,所得结果赋给c(可能包含隐式的扩充或截断操作)。

    绝大部分的操作符用于整型运算的时候,都遵循上述“平衡”原则,比如:算术操作符、位操作符和关系运算符。

    但逻辑操作符不遵循上述“平衡”原则。此外左移(<<)和右移(>>)运算符也不遵循“平衡”原则,只和移位操作符左边的整型操作数相关。假设一个8位的短整型变量值为Oxf5(十六进制),则右移4位所得结果是O xof(十六进制)。

    明确了上述背景后,下面来关注本文一开始提出的“问题1”(代码参见前文)。绝大部分拥有嵌人式C程序开发经验的人都明白这段代码的原意是将port的值取反后右移4位赋值给result_8(在用I/O口控制共阳的LED时经常这么做),程序员期望的结果显然是resuIt_8=0xof。然而,由于整型的“平衡”原则,在16位编译器中,~port的值是Oxffa5;在32位编译器中,~pott的值是Oxffffffa5。无论哪种情况,最后结果(右移4位后赋值给result_8的时候有一个截断操作)都是resuIt_8=Oxfa,而非程序员预期的result_8=OxOf。

    倘若将最后一行代码改成result一8=((uin8_t)(~port))>>4,则result_8可取得预期的值。

    针对以上情况,MISRA-c提出了相应规则。

    规则10.5:如果位操作符~和移位操作符<<(或>>)联合作用于unsigned char或者unsigned short类型的操作数时,中间运算步骤的结果必须立刻显式强制转换为预期的短整型数据类型。

    为了加深对“平衡”原则的理解,再来分析一下“问题2”。

    如果用一个32位的编译器来编译这段程序,最终结果是d=6554l,程序员“幸运地”得到了预期的结果。如果是16位的编译器,得到的结果却是d=5。

    由于“+”运算是左结合的,所以d=a+b+c等效于d=(a+b)+c,即先执行a+b,所得的和再与c相加.最后结果赋值给d。问题就出在a+b这个中间步骤中。由于a和b都是16位整型(注意编译器也是16位的),故而a+b的结果也是16位整型,则a+b的值是Ox0005(有溢出);再扩充为32位整型Ox00000005和c相加赋值给d,d=5,这并非程序员预期的结果。

    所以,在16位编译器中,问题2的那段代码很可能导致严重错误。当然,如果程序员用()指定了运算优先级的话,即最后一行代码写成d=a+(b+c),也可以避免上述溢出错误,然而,这终究不是治本的办法。只有明确每一个操作数的实际数据类型,才能保障代码的安全性。

    MISRA-C中对于表达式中存在隐式数据类型转换的情况作了严格的限制。

    规则10.1:以下情况下,整型表达式中不允许出现隐式数据类型转换。
    ①整型操作数不是被扩充为更多位数的同符号整数;
    ②表达式是复杂表达式;
    ③表达式不是常数表达式,且是函数的参数;
    ④表达式不是常数表达式,且是函数的返回表达式。。

    规则10.2:以下情况下,浮点数表达式中不允许出现隐式数据类型转换。
    ①浮点型操作数不是被扩充为更多位数的同符号浮点数;
    ②表达式是复杂表达式;
    ③表达式是函数的参数;
    ④表达式是函数的返回表达式。

    整型表达式规则和浮点数表达式规则基本类似,只是浮点数表达式规则更为苛刻一些,对浮点型的常数也作了严格的限定。

    这两条规则中,出现了“复杂表达式”的概念。请注意,MISRA—C中“复杂表达式”的概念和其他介绍C编程规范书籍中“复杂表达式”的概念是不一样的。在MISRA-C中,非“复杂表达式”基本只限制在常数表达式或者函数的返回值。为了明确上述规则中关于“复杂表达式”和“返回表达式”的概念,此处举一例子。定义一个函数uintl6_t foo(void),函数体如下:
uintl6_t foo(void){
return(a+b+c);

    函数体中最后一句return(a+b+c)中的a+b+c是返回表达式。倘若在C程序的其他地方有a=foo()这样的语句,则用的是foo()函数的返回值。在MISRA-c中,的资源,完成了采用USB接口技术的热敏打印机的开发,并对打印头作了充分的保护。通过采用相应的算法实现这个赋值表达式不是“复杂表达式”。

    至于表达式作为函数参数等情况,碍于篇幅的原因,此处就不再详细展开了。

    权衡一下利弊,在涉及到数据类型转换的时候,与其花很大力气去区分一个隐式表达式是否在MISRA—C规则的“黑名单”中,还不如用强制转换符显式地标识出每个操作数的实际数据类型,这是最为稳妥的方法。总而言之,MISRA—C关于数据类型转换规则的中心意思,是要求程序员明确任意一个操作数的实际数据类型。 


 小 
    作为一名优秀程序员,第一步就是以严谨的态度对待程序中的每一个数据,明白任何一个数据操作的关键,从而能写出最清晰易懂而又安全的代码。MISRA—C关于数据类型的规则可保障程序员在迈出这一步的时候不会摔倒。 

这篇关于跨越数据类型的重重陷阱的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/604614

相关文章

C 语言的基本数据类型

C 语言的基本数据类型 注:本文面向 C 语言初学者,如果你是熟手,那就不用看了。 有人问我,char、short、int、long、float、double 等这些关键字到底是什么意思,如果说他们是数据类型的话,那么为啥有这么多数据类型呢? 如果写了一句: int a; 那么执行的时候在内存中会有什么变化呢? 橡皮泥大家都玩过吧,一般你买橡皮泥的时候,店家会赠送一些模板。 上

C语言程序设计(数据类型、运算符与表达式)

一、C的数据类型 C语言提供的数据类型: 二、常量和变量 2.1常量和符号常量 在程序运行过程中,其值不能被改变的量称为常量。 常量区分为不同的类型: 程序中用#define(预处理器指令)命令行定义变量将代表常量,用一个标识符代表一个常量,称为符合常量。 2.2变量 变量代表内存中具有特定属性的一个存储单元,用来存放数据,在程序运行期间,这些值是可以 改变的。 变

C++学习笔记----6、内存管理(四)---- 通常的内存陷阱(2)

3、Windows环境下使用Visual C++发现并修复内存渗露         内存渗露很难跟踪是因为你无法很容易地看着内存并且看到什么对象处于使用中,一开始在哪儿分配的内存。然而,是有程序可以为你做到这一点的。内存渗露检测工具有昂贵的专业软件包,也有免费下载的工具。如果你是在Microsoft Visual C++环境下工作,它的排错工具库有内建的对于内存渗露检测的支持。该内存检测默认没有

Redis地理数据类型GEO

通常要计算两个地理位置的距离不是很方便,这里可以直接通过Redis提供的GEO操作来完成地理位置相关的计算 1)添加地理位置 语法:geoadd key longitude latitude member [longitude latitude member] ...字段说明:key:存放地理位置的集合名称longitude:地理坐标的经度latitude:地理坐标的纬度member:表示这

【JavaScript】基本数据类型与引用数据类型区别(及为什么String、Boolean、Number基本数据类型会有属性和方法?)

基本数据类型   JavaScript基本数据类型包括:undefined、null、number、boolean、string。基本数据类型是按值访问的,就是说我们可以操作保存在变量中的实际的值。 1)基本数据类型的值是不可变的 任何方法都无法改变一个基本类型的值,比如一个字符串: var name = "change";name.substr();//hangconsole.log

用异或交换两个整数的陷阱

前面我们谈到了,可用通过异或运算交换两个数,而不需要任何的中间变量。 如下面: void exchange(int &a, int &b) {     a ^= b;     b ^= a;     a ^= b; } 然而,这里面却存在着一个非常隐蔽的陷阱。 通常我们在对数组进行操作的时候,会交换数组中的两个元素,如exchang(&a[i], &b[j]),

【MySQL】MySQL常用的数据类型——表的操作

前言: 🌟🌟本期讲解关于MySQL常用数据类型,表的简单使用,希望能帮到屏幕前的你。 🌈上期博客在这里:http://t.csdnimg.cn/wwaqe 🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客   目录 前言: 📚️1.MySQL常用的数据类型 1.1数值类型 1.2字符串类型 1.3日期类型  📚️2.表的简单操作 2.1创建

c语言(三种语言对比、数据类型及输出、变量存储、内存分配)

数据类型: 变量的输出: c语言不能直接打印数字,字符,只能将他们转化成字符串打印, printf("%d\n", num); %d 是整数占位符,将来由逗号右边的变量的值占据这个位置。(%i和%d是一样的) %f 是浮点数占位符,默认输出小数点后6位,若只想输出小数点后两位:%.2f %c 是字符型数据占位符。

SpinalHDL之数据类型(一)

本文作为SpinalHDL学习笔记第五十四篇,介绍SpinalHDL的Bool数据类型。 SpinalHDL技术交流QQ群: Note: 1.本群是个人技术交流群,不是什么官方答疑群; 2.提问是你的权利,但回答不是别人的义务; 3.可以潜水,不能灌水; 4.请文明交流,做这行的都算高层次人才,希望你有对应的高素质; 5.不强制改名,但希望统一格式:姓名(昵称也行)-公司/学校-

C++学习笔记----6、内存管理(四)---- 通常的内存陷阱(1)

使用new/delete/new[]/delete[]处理动态内存以及底层内存操作是非常容易出错的。对于引起内存有关的问题还特别难以定位。每一个内存泄露与错误指针都有其细微差别。没有能够解决内存问题的银弹。我们就来谈一谈一些通常问题以及能够检测和解决的一些工具。 1、少分配了数据空间与越界内存访问         对于C风格的字符串来讲少分配了数据空间是一个常见的问题,当程序员