C语言中数据进行类型转换时需要注意的那些问题
匆忙会让你少去私心杂念,却容易让你忽视安放自己的空间。
文 章 导 读
今天给大家整理了下类型转换相关的内容包括类型转换所涉及的内容、显式类型转换、隐式类型转换、类型转换可能出现的问题、指针类型转换等等,内容比较多,建议大家转pdf看,希望对大家有所帮助哈,!
1)基本数据类型
表达式的类型是指其运算结果的类型。当两个long类型的值相加时,表达式具有long类型。大多数数值运算符产生其类型依赖于操作数类型的结果。
另一方面,某些操作符会给出具有int类型的布尔结果而不管其操作数类型如何。所以,举例来说,当两个long类型的项用关系运算符做比较时,该表达式的类型为int。
术语“基本类型”的定义是,在不考虑整数提升的作用下描述由计算表达式而得到的类型。
当两个int类型的操作数相加时,结果是int类型,那么表达式可以说具有int类型。
当两个unsigned char类型的数相加时,结果也是int类型(通常如此,因为整数提升的原因),但是该表达式基本的类型按照定义则是unsigned char。
术语“基本类型”描述了一个假想的对C语言的违背,其中不存在整数提升而且常用的数值转换一致性地应用于所有的整数类型。引进这样的概念是因为整数提升很敏感且有时是危险的。
整数提升是C语言中不可避免的特性,但是这些规则的意图是要使整数提升的作用能够通过不利用发生在small integer操作数上的宽度扩展来中和。
当int类型的数相加时,程序员必须要确保运算结果不会超出int类型所能体现的值。如果他没有这样做,就可能会发生溢出而结果值是未定义的。
程序员要确保两个unsigned char类型的数相加的结果是能够被unsigned char体现的,即使整数提升会引起更大类型的计算。换句话说,对表达式基本类型的遵守要多于其真实类型。
2)整数常量表达式的基本类型
C语言的一个不利方面是,它不能定义一个char或short类型的整型常量。比如,值“5”可以通过附加的一个合适的后缀来表示为int、unsigned int、long或 unsigned long类型的常量;
但没有合适的后缀用来创建不同的char或short类型的常量形式。这为维护表达式中的类型一致性提出了困难。
如果需要为一个unsigned char类型的对象赋值,那么或者要承受对一个整数类型的隐式转换,或者要实行强制转换,这种情况下使用强制转换只能导致可读性下降。
在初始化、函数参数或数值表达式中需要常量时也会遇到同样的问题。然而只要遵守强类型(strong typing)原则,这个问题就会比较乐观。
解决该问题的一个方法是,想象整型常量、枚举常量、字符常量或者整型常量表达式具有适合其量级的类型。
这个目标可以通过以下方法达到,即延伸基本类型的概念到整型常量上,并想象在可能的情况下数值常量已经通过提升想象中的具有较小基本类型的常量而获得。
这样,整型常量表达式的基本类型就可以如下定义:
① 如果表达式的真实类型是(signed)int,其基本类型就是能够体现其值的最小的有符号整型。
② 如果表达式的真实类型是unsigned int,其基本类型就是能够体现其值的最小的无符号整型。
③ 在所有其他情况下,表达式的基本类型与其真实类型相同。
注意,基本类型是人造的概念,它不会以任何方式影响实际运算的类型。这个概念的提出只是为了定义一个在其中可以构建数值表达式的安全框架。
3)复杂表达式
通常描述的类型转换规则在某些地方是针对“复杂表达式”的概念。
术语“复杂表达式”意味着任何不是如下类型的表达式:
① 非常量表达式。
② lvalue(即一个对象)。
③ 函数的返回值。
应用在复杂表达式的转换也要加以限制以避免上面总结出的危险。特别地,表达式中的数值运算序列需要以相同类型进行。
以下是复杂表达式:
s8a + s8b
~u16a
u16a >> 2
foo (2) + u8a
*ppc + 1
++u8a
以下不是复杂表达式,尽管某些包含了复杂的子表达式:
pc[u8a]
foo (u8a + u8b)
++ppuc
** (ppc + 1)
pcbuf[s16a * 2]
4)整数后缀
规则(强制):后缀“U”应该用在所有unsigned类型的常量上。
整型常量的类型是混淆的潜在来源,因为它依赖于许多因素的复杂组合,包括:
① 常数的量级。
② 整数类型实现的大小。
③ 任何后缀的存在。
④ 数值表达的进制(即十进制、八进制或十六进制)。
例如,整型常量“40000”在32位环境中是int类型,而在16位环境中则是long类型。
值0x8000在16位环境中是unsigned int类型,而在32位环境中则是(signed)int类型。
注意:
① 任何带有“U”后缀的值是unsigned类型。
② 一个不带后缀的小于231的十进制值是signed类型。
但是:
① 不带后缀的大于或等于215的十六进制数可能是signed或unsigned类型。
② 不带后缀的大于或等于231的十进制数可能是signed或unsigned类型。
常量的符号应该明确。
符号的一致性是构建良好形式的表达式的重要原则。如果一个常数是unsigned 类型,为其加上“U”后缀将有助于避免混淆。
当用在较大数值上时,后缀也许是多余的(在某种意义上它不会影响常量的类型);然而后缀的存在对代码的清晰性是种有价值的帮助。
C语言给程序员提供了相当大的自由度并允许不同数值类型可以自动转换。
由于某些功能性的原因可以引入显式的强制转换,例如:
① 用以改变类型使得后续的数值操作可以进行。
② 用以截取数值。
③ 出于清晰的角度,用以执行显式的类型转换。
为了代码清晰的目的而插入的强制转换通常是有用的,但如果过多使用就会导致程序的可读性下降。
规则(强制):整型复杂表达式的值只能强制转换到更窄的类型且与表达式的基本类型具有相同的符号。
规则(强制):浮点类型复杂表达式的值只能强制转换到更窄的浮点类型。
如果强制转换要用在任何复杂表达式上,可以应用的转换的类型应该严格限制。
节所阐释的,复杂表达式的转换经常是混淆的来源,保持谨慎是明智的做法。为了符合这些规则,有必要使用临时变量并引进附加的语句。
规则(强制):如果位运算符~和<<应用在基本类型为unsigned char或 unsignedshort的操作数,结果应该立即强制转换为操作数的基本类型。
当这些操作符(~和<<)用在small integer类型(unsigned char或 unsigned short)时,运算之前要先进行整数提升,结果可能包含并非预期的高端数据位。
存在三种隐式转换的类别需要加以区分。
1)整数提升转换
整数提升描述了一个过程,借此过程数值操作总是在int或long(signed或 unsigned)整型操作数上进行。其他整型操作数(char、short、bit-field和 enum)在数值操作前总是先转化为int或unsigned int类型。这些类型称为small integer类型。
整数提升的规则命令,在大多数数值操作中,如果int类型能够代表原来类型的所有值,那么small integer类型的操作数要被转化为int类型;否则就被转化为 unsigned int。
注意,整数提升:
① 仅仅应用在small integer类型上。
② 应用在一元、二元和三元操作数上。
③ 不能用在逻辑操作符(&&、||、!)的操作数上。
④ 用在switch语句的控制表达式上。
整数提升经常和操作数的“平衡”发生混淆。事实上,整数提升发生在一元操作的过程中,如果二元操作的两个操作数是同样类型的,那么也可以发生在二元操作之上。
由于整数提升,两个类型为unsigned short的对象相加的结果总是signed int 或unsigned int类型的;事实上,加法是在后面两种类型上执行的。
因此对于这样的操作,就有可能获得一个其值超出了原始操作数类型大小的结果。
例如,如果int类型的大小是32位,那么就能够把两个short(16位)类型的对象相乘并获得一个32位的结果,而没有溢出的危险。
另一方面,如果int类型仅是16位,那么两个16 位对象的乘积将只能产生一个16位的结果,同时必须对操作数的大小给出适当的限制。
整数提升还可以应用在一元操作符上。
例如,对一个unsigned char操作数执行位非(~)运算,其结果通常是signed int类型的负值。
整数提升是C语言中本质上的不一致性,在这当中small integer类型的行为与 long和int类型不同。鼓励使用typedef。
然而,由于众多整型的行为是不一致的,忽略基本类型可能是不安全的,除非对表达式的构造方式给出一些限制。
2)赋值转换
赋值转换发生在:
① 赋值表达式的类型被转化成赋值对象的类型时。
② 初始化表达式的类型被转化成初始化对象的类型时。
③ 函数调用参数的类型被转化成函数原型中声明的形式参数的类型时。
④ 返回语句中用到的表达式的类型被转化成函数原型中声明的函数类型时。
⑤ switch-case标签中的常量表达式的类型被转化成控制表达式的提升类型时。这个转换仅用于比较的目的。
每种情况中,必要时数值表达式的值是无条件转换到其他类型的。
3)平衡转换
平衡转换的描述是在ISO C标准中的“Usual Arithmetic Conversions”条目下。
这套规则提供一个机制,当二元操作符的两个操作数要平衡为一个通用类型时或三元操作符(? :)的第二、第三个操作数要平衡为一个通用类型时,产生一个通用类型。
平衡转换总是涉及到两个不同类型的操作数;
其中一个、有时是两个需要进行隐式转换。
整数提升(上面描述的)的过程使得平衡转换规则变得复杂起来,在整数提升时,small integer类型的操作数首先要提升到int或unsigned int类型。
整数提升是常见的数值转换,即使两个操作数的类型一致。
与平衡转换明显相关的操作符是:
① 乘除*、/、%。
② 加减+、-。
③ 位操作&、^、|。
④ 条件操作符(… ? … : …)。
⑤ 关系操作符>、>=、<、<=。
⑥ 等值操作符==、!=。
其中大部分操作符产生的结果类型是由平衡过程产生的,除了关系和等值操作符,它们产生具有int类型的布尔值。
要注意的是,位移操作符(<<和>>)的操作数不进行平衡,运算结果被提升为第一个操作数的类型;第二个操作数可以是任何有符号或无符号的整型。
正如下面所描述的,一些隐式转换是可以安全地忽略的,而另一些则不能。
规则(强制):下列条件成立时,整型表达式的值不应隐式转换为不同的基本类型:
a) 转换不是带符号的向更宽整数类型的转换,或者
b) 表达式是复杂表达式,或者
c) 表达式不是常量而是函数参数,或者
d) 表达式不是常量而是返回的表达式。
规则(强制):下列条件成立时,浮点类型表达式的值不应隐式转换为不同的类型:
a) 转换不是向更宽浮点类型的转换,或者
b) 表达式是复杂表达式,或者
c) 表达式是函数参数,或者
d) 表达式是返回表达式。
还要注意,在描述整型转换时,始终关注的是基本类型而非真实类型。
这两个规则广泛地封装了下列原则:
① 有符号和无符号之间没有隐式转换。
② 整型和浮点类型之间没有隐式转换。
③ 没有从宽类型向窄类型的隐式转换。
④ 函数参数没有隐式转换。
⑤ 函数的返回表达式没有隐式转换。
⑥ 复杂表达式没有隐式转换。
限制复杂表达式的隐式转换的目的,是为了要求在一个表达式里的数值运算序列中,所有的运算应该准确地以相同的数值类型进行。注意这并不是说表达式中的所有操作数必须具备相同的类型。
表达式u32a + u16b + u16c是合适的——两个加法在概念上都以U32类型进行表达式u16a + u16b + u32c是不合适的——第一个加法在概念上以U16类型进行,第二个加法是U32类型的。
使用名词“在概念上”是因为,在实际中数值运算的类型将依赖于int实现的大小。通过遵循这样的原则,所有运算都以一致的(基本)类型来进行,能够避免程序员产生的混淆和与整数提升有关的某些危险。
指针类型可以归为如下几类:
① 对象指针。
② 函数指针。
③ void指针。
④ 空(null)指针常量(即由数值0强制转换为void*类型)。
涉及指针类型的转换需要明确的强制,除非在以下时刻:
① 转换发生在对象指针和void指针之间,而且目标类型承载了源类型的所有类型标识符
② 当空指针常量(void*)被赋值给任何类型的指针或与其做等值比较时,空指针常量被自动转化为特定的指针类型C当中只定义了一些特定的指针类型转换,而一些转换的行为是实现定义的。
规则(强制):转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。
函数指针到不同类型指针的转换会导致未定义的行为。举个例子,这意味着一个函数指针不能转换成指向不同类型函数的指针。
规则(强制):对象指针和其他除整型之外的任何类型指针之间、对象指针和其他类型对象的指针之间、对象指针和void指针之间不能进行转换。这些转换未经定义。
规则(建议):不应在指针类型和整型之间进行强制转换
当指针转换到整型时所需要的整型的大小是实现定义的。尽可能的情况下要避免指针和整型之间的转换,但是在访问内存映射寄存器或其他硬件特性时这是不可避免的。
规则(建议):不应在某类型对象指针和其他不同类型对象指针之间进行强制转换。
如果新的指针类型需要更严格的分配时这样的转换可能是无效的。
uint8_t * p1;
uint32_t * p2;
p2 = (uint32_t *) p1; /* Imcompatible alignment ? */
规则(强制):如果指针所指向的类型带有const或volatile限定符,那么移除限定符的强制转换是不允许的。
任何通过强制转换移除类型限定符的企图都是对类型限定符规则的违背。注意,这里所指的限定符与任何可以应用在指针本身的限定符不同。
类型转换过程中存在大量潜在的危险需要加以避免:
① 数值的丢失:转化后的类型其数值量级不能被体现。
② 符号的丢失:从有符号类型转换为无符号类型会导致符号的丢失。
③ 精度的丢失:从浮点类型转换为整型会导致精度的丢失。
对于所有数据和所有可能的兼容性实现来说,唯一可以确保为安全的类型转换是:
① 整数值进行带符号的转换到更宽类型。
② 浮点类型转换到更宽的浮点类型。
当然,在实践中,如果假定了典型类型的大小,也能够把其他类型转换归类为安全的。
普遍来说,采取的原则是,利用显式的转换来辨识潜藏的危险类型转换。
类型转换中还有其他的一些危险需要认清。这些问题产生于C语言的难度和误解,而不是由于数据值不能保留。
整数提升中的类型放宽:整数表达式运算的类型依赖于经过整数提升后的操作数的类型。
总是能够把两个8位数据相乘并在有量级需要时访问16位的结果。有时而不总是能够把两个16位数相乘并得到一个32位结果。这是C语言中比较危险的不一致性,为了避免混淆,安全的做法是不要依赖由整数提升所提供的类型放宽。
考虑如下例子:
uint16_t u16a = 40000; /* unsigned short / unsigned int ? */
uint16_t u16b = 30000; /* unsigned short / unsigned int ? */
uint32_t u32x; /* unsigned int / unsigned long ? */
u32x = u16a + u16b; /* u32x = 70000 or 4464 ? */
期望的结果是70000,但是赋给u的值在实际中依赖于int实现的大小。如果int 实现的大小是32位,那么加法就会在有符号的32位数值上运算并且保存下正确的值。
如果int实现的大小仅是16位,那么加法会在无符号的16位数值上进行,于是会发生折叠(wraparound)现象并产生值4464(70000%65536)。
无符号数值的折叠是经过良好定义的甚至是有意的;但也会存在潜藏的混淆。
类型计算的混淆:
程序员中常见的概念混乱也会产生类似的问题,人们经常会以为参与运算的类型在某种方式上受到被赋值或转换的结果类型的影响。例如,在下面的代码中,两个16位对象进行16位的加法运算(除非被提升为32位 int),其结果在赋值时被转换为uint32_t类型。
u32x = u16a + u16b;
并非少见的是,程序员会认为此表达式执行的是32位加法——因为u32x的类型。对这种特性的混淆不只局限于整数运算或隐式转换,下面的例子描述了在某些语句中,结果是良好定义的但运算并不会按照程序员设想的那样进行。
u32a = (uint32_t) (u16a * u16b);
f64a = u16a / u16b ;
f32a = (float32_t) (u16a / u16b) ;
f64a = f32a + f32b ;
f64a = (float64_t) (f32a + f32b) ;
数学运算中符号的改变:
整数提升经常会导致两个无符号的操作数产生一个(signed)int类型的结果。比如,如果int是32位的,那么两个16位无符号数的加法将产生一个有符号的32位结果;而如果int是16位的,那么同样运算会产生一个无符号的 16 位结果。
位运算中符号的改变:
当位运算符应用在无符号短整型时,整数提升会有某些特别不利的反响。
比如,在一个unsigned char类型的操作数上做位补运算通常会产生其值为负的(signed)int类型结果。在运算之前,操作数被提升为int类型,并且多出来的那些高位被补运算置1。那些多余位的个数,若有的话,依赖于int的大小,而且在补运算后接右移运算是危险的。
为了避免上述问题产生的危险,重要的是要建立一些准则以限制构建表达式的方式。这里首先给出某些概念的定义。
总结
今天整理了数据类型转相关的内容,类型转换我们平时用得不少,但具体的细节容易被我们所忽略,今天总结的相关内容能填补我们在认识上的一些空缺,后面还会有C语言相关的内容,大家敬请期待哈,。
免责声明:本文内容来源于技术文档及网络,版权归原作者所有。如涉及侵权问题,请与我联系删除。
相关文章:
专辑推荐: