第4章 表达式和赋值
本章描述如何在C语言中构造表达式和对其赋值。常量、标识符、字符串和函数调用都是在表达式中操作的操作数。C语言具有所有常用的语言运算符。本章讨论这些运算符以及对C或Microsoft是唯一的运算符。讨论的主题包括:
*操作数和表达式
*运算符
*类型转换
一个“操作数”是一个运算符作用的一个实体。一个“表达式”是执行这些动作的任何组合的运算符和操作数序列:
*计算一个值
*设计一个对象或函数
*产生副作用在C中的操作数包括常量、标识符、字符串、函数调用、下标表达式、成员选择表达式和通过运算符组合操作数或在圆括号中包括操作数而形成的复杂表达式。这些操作数的语法在下一节“基本表达式”中给出。
基本表达式
表达式中的操作数称为“基本表达式”。语法基本表达式:
标识符
常量
字符串文字
(表达式)表达式:
赋值表达式
表达式,
赋值表达式
基本表达式中的标识符
标识符有整数、float、enum、struct、union、数组、指针或函数类型。一个标识符是一个基本表达式,它被说明用于设计一个对象(这种情况下它是一个l值)或一个函数(这种情况下它是一个函数指示符)。有关l值的定义参见“L值和R值表达式”。
由一个数组标识符表示的指针值不是变量,因此,一个数组标识符不能形成一个赋值运算的左边操作数。因此不是一个可修改的l值。
一个作为函数说明的标识符表示一个其值是该函数地址的指针。该指针地址指向一个返回指定类型值的函数。因此,函数标识符也不能是赋值操作中的l值。有关更多信息,参见第1章“C的基本元素”中的“标识符”。
基本表达式中的常量
一个常量操作数有它给定常量值的值和类型。一个字符常量有int类型。一个整数常量有int、long、unsigned int或unsigned long类型,这取决于该整数的尺寸和指定值的方式。有关更多信息参见第1章“C的基本元素”中的“常量”。
基本表达式中的字符串文字
一个“字符串文字”是一个字符、宽字符或用双引号括起的相邻字符序列。由于它们不是变量,因此字符串文字或任何它们的元素都不能作为一个赋值操作的左边操作数。一个字符串文字的类型是char的数组(或宽字符串文字的wchar_t数组)。在表达式中数组都转换成指针。有关字符串的更多信息,参见第1章“C的基本元素”的“字符串文字”。
圆括号中的表达式
你可以在圆括号中括起任何操作数而不改变该括起的表达式的类型或值。例如,在表达式中:
(10+5)/5
圆括号中的10+5意味着首先对10+5求值,它变成除法(/)运算符的左操作数。(10+5)/5的结果是3。没有圆括号的10+5/5的结果是11。虽然圆括号影响一个表达式中组合操作数的方式,它们不能保证在所有情况下的一个特殊求值次序。例如,如下表达式的圆括号和从左到右的组合都不能保证i在每个子表达式中是什么样的值。
(i++ +1)*(2+i)
编译器自由以任何次序计算乘法两边的值。如果i的初值为0,整个表达式以如下两个语句求值:
(0+1+1)*(2+1)
(0+1+1)*(2+0)
副作用导致的异常在本章后面“副作用”中讨论。
值和R值表达式
指的是存储器位置的表达式称为“l值”表达式,一个l值表示一个存储区域的“定位器”值或“左边”值,它隐含出现在等号(=)的左边。l值经常是标识符。
指的是右修改的位置的表达式称为“可修改的l值”。一个可修改的l值不能有一个数组类型,一个不完整类型或具有const属性的类型,对于可以是可修改l值的结构和联合,它们必须没有const属性的成员。标识符的名称指示一个存储位置,而该变量的值是存储在该位置的值。
如果一个标识符指向一个存储器位置,其类型是算术、结构、联合或指针,则该标识符是一个可修改的l值。例如,如果ptr是一个存储区域的指针,那么*ptr是一个可修改的l值,指示该存储区域由ptr指向。
如下任何C表达式可以是l值表达式:
*一个整数、浮点、指针、结构或联合类型的标识符。
*一个不对数组求值的下标([])表达式。
*一个成员选择表达式(->或.)
*一个不指向一个数组的单目间接访问(*)表达式。
*一个圆括号中的l值表达式。
*一个const对象(一个不能修改的l值)。
本语“r值”有时用来描述一个表达式的值以区别于一个l值。所有l值都是r值,但不是所有r值都是l值。
Microsoft特殊处
C包括ANSI C标准的一个扩充是允许l值造型用作l值,以及该对象的尺寸不会通过该造型而伸长(有关更多信息,参见本章末尾的“类型造型转换”)。如下例子说明了这种特征:
char *p;
short i;
long l;
(long *) p=&l; /*合法造型*/
(long) i=l; /*非法造型*/
C缺省能使Microsoft扩充。使用/Za编译器选项禁止这种扩充。
Microsoft特殊处结束
量表达式
一个常量表达式在编译时求值,而不是在运行时求值,可以使用在一个常量可以被使用的任何地方。常量表达式必须求值为一个在该类型可表示值的范围内的常量。一个常量表达式的操作数可以是整数常量、字符常量、浮点常量、枚举常量、类型造型、sizeof表达式和其它常量表达式。
语法
常量表达式:
条件表达式
条件表达式:
逻辑OR表达式
逻辑OR表达式 ? 表达式 : 条件表达式表达式:
赋值表达式 表达式,
赋值表达式赋值表达式:
条件表达式
单目表达式 赋值运算符 赋值表达式
赋值运算符:如下之一:
= *= /= %= += -= <<= >>= &= ^= |=
结构说明符、枚举器、直接说明符、直接抽象说明符和标号语句的非终结符包含常量表达式非终结符。
一个整数常量表达式必须用于指定一个结构位域成员的尺寸、一个枚举常量的值、一个数组的尺寸或一个case常量的值。
用在预处理器命令中的常量表达式满足一些附加限制。结果它们是众所周知的“限制的常量表达式”。一个限制的常量表达式不能包含sizeof表达式、枚举常量、到任何类型的类型造型或浮点类型常量。但它可以包含定义的特定常量表达式(标识符)。
表达式求值
涉及赋值、单目增1、单目减1或调用一个函数的表达式具有伴随它们求值的结果(副作用),当到达一个“顺序点”时,该顺序点之前的任何事情包括任何副作用都保证在开始计算该顺序点之后的任何事情之前进行求值。
通过对一个表达式求值导致改变“副作用”。副作用出现在一个变量的值被一个表达式求值所改变的任何时候。所有赋值操作都有副作用。如果函数调用通过直接赋值或使用一个指针间接赋值来改变一个外部的可见项的值,则该函数调用也有副作用。
副作用
一个表达式的求值次序是由指定的实现确定的,除了当该语言保证一个特殊的求值次序外(正如本章后面的“优先级和求值次序”中描述的)。例如,在如下函数调用中出现副作用:
add(i+1,i=j+2);
myproc(getc(),getc());
一个函数调用的参量可以以任何次序求值。表达式i+1可以在i=j+2之前求值,或者i=j+2可以在i+1之前求值,每种情况下的结果是不同的,同样,不可能保证哪个字符实际传送给myproc。由于单目增1和减1操作涉及到赋值,如下例子中的操作可能导致副作用:
x[i]=i++;
在这个例子中,被修改的x的值是不可预料的。下标的值可以是i的新值或旧值。其结果可能在不同编译器下或不同优化层都是不同的。由于C不确定副作用的求值次序,上面讨论的求值方法都是正确的,也可以实现。
为了确保你的代码是可移植和清楚的,避免使用其副作用依赖于特殊求值次序的语句。
顺序点
连贯的“顺序点”之间,一个对象的值仅能被一个表达式改变一次。C语言定义如下顺序点:
*逻辑AND运算符(&&)的左操作数。逻辑AND运算符的左操作数在继续之前完成求值和完成所有的副作用。如果该左操作数求值为假(0),不对其它操作数求值。
*逻辑OR运算符(||)的左操作数。逻辑OR运算符的左操作数在继续之前完成求值和完成所有的副作用。如果该左操作数求值为真(非0),不对其它操作数求值。
*逗号运算符的左操作数。逗号运算符的左操作数在继续之前完成求值和完成所有的副作用。逗号运算符的两个操作数总是求值。注意在一个函数调用中的逗号运算符不保证求值的次序。l函数调用运算符。在进入一个函数之前对该函数的所有参量求值和完成所有副作用,在参量中不指定求值的次序。
*条件运算符的第一个操作数。在继续之前完成该条件运算符的第一个操作数的求值并完成所有的副作用。
*一个完整初始化表达式结束(也就是,一个不是另一个表达式部分的表达式,例如,在一个说明语句中一个初始化的结果)。
*一个表达式语句中的表达式。表达式语句由一个任选表达式后跟一个分号(;)。该表达式为了副作用而求值,并紧跟这个求值求一个顺序点。
*在一个选择(if或switch)语句中的控制表达式。在依赖于该选择的代码执行之前完成该表达式的求值,并完成所有的副作用。
*一个while或do语句的控制表达式。在while或do循环的下一次迭代中的任何语句执行之前完成该表达式的求值,并完成所有的副作用。
*一个for语句的所有三个表达式。在for循环的下一次迭代中的任何语句执行之前完成该表达式的求值,并完成所有的副作用。
*一个return语句中的表达式。在控制返回到调用函数之前完成该表达式的求值,并完成所有的副作用。
有三种类型的运算符。单目表达式由一个单目运算符加到一个操作数组成,或者sizeof关键字跟一个表达式,该表达式可以是一个变量的名称或一个造型表达式。如果该表达式是一个造型表达式,它必须包括在圆括号中。一个双目表达式由两个操作数加上连接的双目运算符组成。一个三目表达式由三个操作数加上连接的条件表达式运算符组成。C中包括如下单目运算符:
符号 名称
- ~ ! 取反和取非运算符
* & 间接访问和取地址运算符
sizeof 尺寸运算符
+ 单目加法运算符
++ -- 单目增1和减1运算符
双目运算符从左向右结合。C提供如下双目运算符:
符号 名称
* / % 乘法运算符
+ - 加法运算符
<< >>移 位运算符
<> <= >= == != 关系运算符
& | ^ 按位运算符
&& || 逻辑运算符
顺序求值运算符条件表达式运算符比双目表达式具有较低的优先级,且在右结合上不同于它们。带运算符的表达式也包括赋值表达式,它使用单目或双目赋值运算符。单目赋值运算符是增1(++)和减1(--)运算符;双目赋值运算符是简单赋值运算符(=)和复合赋值运算符。每个复合赋值运算符是另外的双目运算符与简单赋值运算符的组合。
优先级和求值次序
C运算符的优先级和结合律影响表达式中操作数的组合和求值。一个运算符的优先级仅在出现更高或更低优先级的另外运算符时才有意义。具有高优先级运算符的表达式首先求值。优先级也可以通过词“绑定”(binding)来描述。具有更高优先级的运算符被说成具有更紧密的联结。
表4.1总结了C运算符的优先级和结合律(运算符求值的次序),以优先级从高到低列出。而几个运算符只能一起出现,它们具有相等的优先级并根据它们的结合律进行求值。该表中的运算符在以“后缀运算符”开头的小节中。本节的余下部分给出了优先级和结合律一般的信息。
表4.1 C 运算符的优先级和结合律
符号 | 操作类型 | 结合律 |
[] () . -> 后缀++和后缀-- | 表达式 | 从左向右 |
前缀++和前缀-- sizeof & + _~ ! | 单目 | 从右向左 |
类型造型 | 单目 | 从右向左 |
* / % | 乘法 | 从左向右 |
+ - | 加法 | 从左向右 |
<< >> | 位移 | 从左向右 |
<> <= >= | 关系 | 从左向右 |
== != | 相等 | 从左向右 |
& | 按位AND | 从左向右 |
^ | 按位异或 | 从左向右 |
| | 按位OR从左向右&&逻辑AND从左向右||逻辑OR从左向右?:条件表达式 | 从右向左 |
= *= /= %=+= -= <<= >>=&= ^= |= | 简单和复合赋值 | 从右向左 |
, | 顺序求值 | 从左向右 |
1. 运算符以优先级递降次序列出。如果同一行或同一组中有几个运算符出现,它们具有相同的优先级。
2.所有简单和复合赋值运算符具有相同的优先级。
一个表达式可以包含几个具有相同优先级的运算符。当在一个表达式的同一层中出现几个这样的运算符时,根据这些运算符的结合律从右向左或从左向右进行求值。求值的方向不影响这样的表达式的结果,该表达式在同层中可以包含多个乘法(*)、加法(+)或双目按位(&|^)运算符。优先级的次序不是由语言定义的。编译器如果能够保证一致的结果,可以以任意次序对表达式求值。
只有顺序求值(,)、逻辑AND(&&)、逻辑OR(||)、条件表达式(?:)和函数调用运算符设立顺序点,因此保证这些运算符的特殊求值次序。函数调用运算符是该函数标识符之后的一组圆括号。顺序求值运算符(,)保证从左向右对它的运算符求值(注意,在一个函数调用中逗号运算符号不同于顺序求值运算符,不提供任何这样的保证)。有关更多信息,参见本章前面的“顺序点”。
逻辑运算符也保证它们的运算符从左向右求值,但它们求值需要确定该表达式结果的最少的运算符个数。这称之为“短路”求值。因些,该表达式的一些运算符可能不求值,例如,在表达式中:
x && y++
只有x为真(非0),第二个运算符y++才被求值,因此,如果x为假(0),则y不增1。
例子
下表说明一个编译器如何自动绑定几个基本表达式。
表达式 自动联结
a&b||c (a&b)||c
a=b||c a=(b||c)
q&&r||s-- (q&&r)||s--
在第一个表达式中,按位AND运算符的优先级比逻辑OR运算符(||)的优先级高,因此,a&b形成了逻辑OR运算的第一个操作数。
在第二个表达式中,逻辑OR运算符(||)比简单赋值运算符(=)有更高的优先级,因此b||c在赋值中作为右边运算符进行组合。注意赋给a的值是0或1。
第三个表达式说明一个正确地组成的表达式可能产生一个不可预料的结果。逻辑AND运算符(&&)比逻辑OR运算符(||)的优先级高,因此,q&&r结合成一个操作数。由于逻辑运算符保证从左向右求值,q&&r在s--之前被求值。如果q&&r求值为非0值,s --不再求值,s不减1。
如果s不减1,可能导致程序出问题,s--应在表达式中作为第一个操作数出现,或者s在一个分开的操作中减1。
下面的表达式是非法的,在编译时产生一个诊断消息:
非法表达式 缺省组合
p==0?p+=1:p+=2 (p==0?p+=1:p)+=2
在这个表达式中,相等运算符(==)有最高的优先级,因此p==0组合成一个操作数。条件表达式运算符(?:)有次高的优先级,它的第一个操作数是p==0,第二个操作数是p+=1。但该条件表达式运算符的最后操作数被认为是p而不是p+=2,这是由于p与该条件表达式运算符的结合比与该复合赋值运算符的绑定更紧密。因为+=2没有左边运算符,出现一个语法错误。
你可以使用圆括号防止这种错误出现并产生更可读的代码。例如,你可以使用如下圆括号以改正前面的例子并使之更清楚
(p==0)?(p+=1):(p+=2)
常用的算术转换
大多数C运算符执行类型转换把一个表达式的操作数转换成共同的类型或扩充短的值为机器操作中使用的整数尺寸。C运算符执行的转换依赖于特定运算符和操作数的类型或操作数。但很多运算符在整数和浮点类型的操作数上执行相似的转换,这些转换就是众所周知的“算术转换”。一个操作数值转换成一个兼容的类型使其值不发生改变。
下面总结的算术转换称为“常用的算术转换”。这些步骤仅应用于算术类型的双目运算符以及两个操作数不是相同类型的情况。其目的是产生一个公共的类型,它也是最后结果的类型。为了确定实际发生的转换,编译器把如下算法应用于表达式中的双目操作。下面步骤的次序不是按优先级给出的:
1. 如果操作数之一类型为long double,则其它操作数转换成long double类型。
2. 如果上述条件不满足,且操作数之一类型为double,则其它操作数转换成double类型。
3.如果上述两个条件不满足,且操作数之一为float类型,则其它操作数转换成float类型。
4.如果上述三个条件都不满足(没有操作数为浮点类型),那么在操作数上执行如下整数转换:
* 如果操作数之一为unsigned long类型,则其它操作数转换成unsigned long类型。
* 如果上述条件不满足,且操作数之一为long类型,其它为unsignedint类型,则操作数都转换成unsigned long类型。
* 如果上述两个条件都不满足,且操作数之一为long类型,则其它操作数转换成long类型。
* 如果上述三个条件都不满足,且操作数之一为unsigned int,则其它操作数转换成unsigned int 类型。
* 如果上述条件都不满足,则操作数都转换成int类型。
以下例子说明了这些转换规则:
float fVal;
double dVal;
inti Val;
unsigned long ulVal;
dVal = iVal * ulVal; /*iVal使用步骤4转换为unsigned long
*乘法的结果转换成double */
dVal = ulVal + fVal; /*ulVal使用步骤3转换为float
*加法的结果转换成double */
后缀运算符
后缀运算符在表达式值中具有最高的优先级(最紧密的绑定)。
语法
后缀表达式:
基本表达式
后缀表达式[表达式]
后缀表达式[参量表达式表opt]
后缀表达式.标识符
后缀表达式->标识符
后缀表达式++
后缀表达式--
在这个优先级层中的运算符有数组下标、函数调用、结构和联合成员以及后缀增1和后缀减1运算符。
一维数组
一个后缀表达式后跟一个方括号([])中的表达式是一个数组对象的一个元素的下标表示。一个下标表达式表示这样地址的值,其地址是该后缀表达式加上表达式中指定值的位置,表示为:
后缀表达式[表达式]
通常,由后缀表达式表示的值是一个指针值,例如一个数组标识符和一个表达式是一个整数值。但所有语法上需要的是一个为指针类型的表达式,另一个为整数类型。因此,整数值可以在后缀表达式位置,指针值可以在方括号的表达式中或“下标”位置。例如,如下代码是合法的:
int sum *ptr,a[10];
int main
{
ptr=a;
sum=4[ptr];
}
下标表达式通常用于指向数组元素,但你可以将下标应用于任何指针。无论值的次序,表达式必须包括在方括号([])中。
下标表达式通过把整数值加上指针值,然后在结果上应用间接访问运算符(*)求值(有关间接访问运算符的讨论,参见本章后面的“间接访问和取地址运算符”)。实际上,如下四个表达式是相等的,假设a是一个指针,b是一个整数:
a[b]
*(a+b)
*(b+a)
b[a]
根据加法运算符的转换规则(在“加法运算符”中给出),该整数值通过将它乘以指针地址类型的长度来将其转换成一个地址偏移量。
假如,假设标识符line指向一个int类型的数组,如下过程用于计算下标表达式ling[i]的值:
1. 整数值I被一个int项长度确定的字节个数相乘。i的转换值表示i个int位置。
2. 这个转换的值加上最初指针值(line)产生一个地址,它是距离line偏移i个int的位置。
3. 将间接访问运算符应用于该新地址。其结果是该位置数组元素的值(直观地为line[i])。
下标表达式line[0]表示line的第一个元素的值,由于它偏移line所表示的地址为0。类似地,一个表达式例如line[5]指的是偏移line为5个位置的元素或该数组的第6个元素。
多维数组
一个下标表达式也可以有多个下标,如下:
expression1[expression2][expression3]...
下标表达式从左向右结合。最左下标表达式expression1[expression2]首先求值。其地址加上expression1和expression2构成一个指针表达式;expression3加上这个指针表达式构成一个新的指针表达式,如此等等直到最后下标表达式已加上了。在最后下标表达式求值之后应用间接访问运算符(*),除非最后的指针值是一个数组类型的地址(参见下面的例子)。
具有多个下标的表达式指的是“多维数组”。一个多维数组是一个其元素是数组的数组。例如,一个三维数组的第一个元素是一个两维数组。
例子
对于如下例子中,一个命名为prop的数组用三个元素说明,每个是一个int类型的4×6数组。
int prop[3][4][6];
int i,*ip,(*ipp)[6];
如下引用prop数组:
i=prop[0][0][1];
上面例子说明了如何引用prop的第2个int元素。数组以行存储,因此最后的下标最快地变化;表达式prop[0][0][2]指的是该数组的下一个(第3个)元素,等等。
i=prop[2][1][3];
这个语句更加复杂地引用prop的单个元素。该表达式求值如下:
1. 第1个下标为2,由4×6的int数组的尺寸相乘,再加上prop的指针值,其结果指向prop的第3个4×6数组。
2. 第2个下标为1,由6个元素int数组的尺寸相乘,再加上prop[5]表示的地址。
3. 该6元素数组的每一个元素是一个int值,因此,最后下标3,在加到prop[2][1]之前由一个int的尺寸相乘。结果指针是该6元素数组的第4个元素。
4. 将间接访问运算符应用到该指针值。其结果是该地址处的int元素。如下两个例子说明了不应用间接访问运算符的情况:
ip=prop[2][1];
ipp=prop[2];
在上述语句的第一个语句中,表达式prop[2][1]是该三维数组prop的有效引用。它指的是第一个6元素数组(上述说明的)。由于该指针值是一个数组的地址,不能应用间接访问运算符。
类似地,在第二个语句ipp=prop[2]中,表达式prop[2]的结果是一个二维数组的地址的指针值。
函数调用
一个“函数调用”是一个包含被调用函数的名称或函数指针值或者传送给该函数的任选参量的表达式。
语法
后缀表达式:
后缀表达式(参量表达式表opt)
参量表达式表:
赋值表达式
参量表达式表,赋值表达式
后缀表达式必须计算一个函数地址(例如,一个函数标识符或一个函数指针的值),参量表达式表是一个其值(“参量”)传送给该函数的表达式表(用逗号分隔表达式)。参量表达式表参量可以为空。
一个函数调用表达式具有该函数返回的值和类型,一个函数不能返回一个数组类型的对象,如果该函数的返回类型为void(也就是,该函数说明为
没有返回值),则该函数调用表达式也有void类型(有关更多信息,参见第6章“函数”中的“函数调用”)。
结构和联合成员
一个“成员选择表达式”指的是结构和联合的成员,这样的一个表达式具有选择的成员的值和类型。
语法
后缀表达式.标识符
后缀表达式->标识符
这个表描述了成员选择表达式的两种格式:
1. 在第一种格式中,后缀表达式表示一个struct或union类型的值,标识符命名该指定结构或联合的一个成员。该操作的值就是该标识符的值,如果后缀表达式是一个l值,则该操作的值也是一个l值。有关更多信息,参见本章前面的“L值和R值表达式”。
2. 在第二种格式中,后缀表达式表示一个结构或联合的一个指针,标识符命名该指定结构或数组的一个成员。其值是该标识符的值,且是一个l值。
成员选择表达式的两种格式具有类似的作用。事实上,涉及成员选择运算符(->)的表达式是这样的表达式的速记版本,这个表达式使用句点且该句点之前的表达式由作用于一个指针值的间接访问运算符组成。因此:表达式->标识符
等价于:
(*表达式).标识符
这里表达式是一个指针值。
例子
如下例子指的是这种结构说明,有关在这些例子中使用间接访问运算符(*)的信息,参见本章后面的“间接访问和取地址运算符”。
struct pair
{ int a;
int b;
struct pair *sp;
} item,list[10];
item的一个成员选择表达式如下:
item.sp=&item在上述例子中,将item结构的地址赋给该结构的sp成员。这样item包含自身的一个指针。
(item.sp)->a = 24;
在这个例子中,指针表达式item.sp与成员选择运算符(->)一起使用把一个值赋给a:
list[8].b = 12;这个语句说明如何从一个结构数组选择单个结构成员。
后缀增1和减1运算符
后缀增1和减1运算符的操作数是可修改的l值的标量类型。
语法
后缀表达式:
后缀表达式++
后缀表达式--
后缀增1或减1操作的结果是该操作数的值。在获得结果后,该操作数增1(或减1)。如下代码说明该后缀增1运算符。
if (var++>0)
*p++=*q++;
本例中,变量var是与0进行比较,如果var在增1之间是正数,则执行下一个语句。首先,由q所指对象的值赋给由q所指的对象,然后,q和p都增1。
单目运算符
单目运算符出现它的操作数之前并从右向左结合。
语法
单目表达式:
后缀表达式
++单目表达式
--单目表达式
单目运算符 造型表达式
sizeof 单目表达式
sizeof(类型名称)单目运算符:如下之一
& * + - ~ !
前缀增1和减1运算符
当增1和减1运算符出现在操作数之间时,单目运算符(++和--)被称为“前缀”增1和减1运算符。
后缀增1和减1的优先级比前缀增1和减1运算符的优先级高。操作数必须是整数、浮点或指针类型以及必须是一个可修改的l值表达式(一个没有const属性的表达式)。其结果是一个l值。
当运算符出现在操作数之前时,该操作被增1或减1,其新值是该表达式的结果。
一个整数或浮点类型的操作数增大或减小整数值1,其结果的类型与操作数类型相同。
一个指针类型的操作数增大或减小它地址所指对象的尺寸,一个增1的指针指向下一个对象,一个减1的指针指向前一个对象。
例子
这个例子说明单目前缀减1运算符:
if (line [--i]!=′/n′)
return;
在这个例子中,变量i在作为line的下标之前减1。
间接访问和取地址运算符
间接访问运算符(*)通过一个指针间接访问一个值。该操作值必须是一个值。该操作数必须是一个指针值。其操作结果是该操作数所指的值,也就是该操作数所指地址处的值。其结果类型是该操作数所指地址的类型。
如果操作数指向一个函数,其结果是一个函数指示符,如果它指向一个存储位置,其结果是一个l值指示该存储位置。
如果该指针值是无效的,其结果是不确定的。如下表包括无效指针值的一些最普遍的条件:
* 该指针是一个空指针。
* 该指针指出一个在引用时不可见的局部项的地址。
* 该指针指出一个不适合赋给该对象所指类型的地址。
* 该指针指出一个执行程序不能使用的地址。
取地址运算符(&)给出它的操作数的地址。取地址运算符的操作数可以是一个函数指示符或一个l值,该l值指示一个不是位域的对象,且没有用register存储类指示符说明。
取地址运算符的结果是该操作数的指针,该指针所指地址的类型是操作数的类型。
取地址运算符只能应用于在文件范围层说明的基本类型、结构或联合类型或者下标数组的引用。在这些表达式中,在该地址表达式中可以加上
或减去一个不包括取地址运算符的常量表达式。
例子
下面的例子使用三个说明:
int *pa,x;
int a[20];
double d;
下面的语句使用取地址运算符:
pa=&a[5];
该取地址运算符(&)取数组a的第6个元素的地址,把结果存储在指针变量pa中。
x=*pa;
在这个例子中使用间接访问运算符(&)访问存储在pa地址处的int值,把该值赋给整数变量x。
if (x == *&x)
printf("True/n");
这个例子打印单词True,从而证实把间接访问运算符应用于x的地址的结果与x是相同的。
int roundup(void); /*函数说明 */
int *proundup=roundup;
int *pround=&roundup;
一旦说明了函数roundup,该roundup的两个指针也说明和初始化了。第一个指针proundup仅使用该函数的名称初始化,而第二个指针pround在初始化中使用取地址运算符。这两种初始化是相同的。
单目算术运算符
在下表中讨论C单目加、算术取反、按位取反和逻辑非运算符。
运算符 说明
+ 单目加运算符位于圆括号中一个表达式的前面强制组合括起的操作。它用在涉及多个结合或交换双目运算符的表达式中。该操作数必须是算术类型。其结果是操作数的值。一个整数操作数要进行整数提升,结果的类型是提升的操作数的类型。
- 算术取反运算符产生该操作数的取反值(2的补码)。该操作数必须是整数或浮点值。这个运算符执行常用的算术转换。
~ 按位取反(或按位NOT)运算符产生其操作数的按位取反。该操作数必须是整数类型。这个运算符执行常用的算术转换,其结果具有转换后的操作数的类型
! 逻辑非(逻辑NOT)运算符在操作数为真(非0)时产生值0,在操作数为假(0)时产生值1。其结果具有int类型。该操作数必须是一个整数、浮点或指针值。
指针上的单目算术操作是非法的。
例子
如下例子说明单目算术运算符:
short x=987;
x=-x
;在上述例子中,x的新值是987的算术取反即-987。
unsigned short y=0xAAAA;
y=~y;在这个例子中,y的新值是无符号值0xAAAA的按位取反即0x5555。
if (!(x<y))
如果x大于或等于y,该表达式的结果是1(真)。如果x小于y,其结果是0(假)。
sizeof运算符
sizeof运算符以字节为单位给出存储该操作数的类型的对象所需要的存储数目。这个运算符允许你在程序中避免指定依赖于机器的数据尺寸。
语法
sizeof单目表达式
sizeof(类型名称)
该操作数是一个为单目表达式或类型造型表达式(也就是,一个类型指示符放在圆括号中)的标识符。该单目表达式不能表示一个位域对象、一个不完整类型或一个函数指示符。其结果是一个无符号整数参量。标准头文件STDDEF.H定义这个类型为size_t。
当你把该sizeof运算符应用到一个数组标识符时,其结果是整个数组的尺寸而不是该数组标识符表示的指针的尺寸。
当你把该sizeof运算符应用到一个结构或联合类型名称,或者一个结构
或联合类型的标识符时,其结果是该结构或联合的字节个数,包括内部的和尾部的填充。这个尺寸可以包括用于在存储器边界上对齐结构或联合的成员而使用的内部和尾部填充。因此,其结果可能不对应于所有成员所需存储相加的结果。
如果一个没有尺寸的数组是一个结构的最后元素,则sizeof运算符返回没有该数组的结构的尺寸。
buffer=calloc(100,sizeof(int));
这个例子使用sizeof运算符传送一个int的尺寸,它在不同的机器中可能不同,作为一个参量传送给名称为calloc的运行函数。由这个函数返回的值存储在buffer中。
static char *strings[] =
{
"this is string one",
"this is string two",
"this is string three",
};
const int string_no = (sizeof strings) / sizeof strings[0]);
在这个例子中,strings是一个char的指针的数组。该指针的成员是数组中的元素个数,但不是指定的。使用sizeof运算符计算该数组中的元素个数容易确定指针个数。const整数值string_no初始化为这个数。因为这是一个const值,string_no不能修改。
造型运算符
一个类型造型提供了在特定状态下显式转换一个对象类型的方法。
语法
造型表达式:
单目表达式
(类型名称)造型表达式
在类型造型强制转换之后,编译器处理造型表达式为类型名称所指的类型。造型可以把任何标量类型的对象转换成另一种标量类型的对象。显式类型造型也遵守本章后面“赋值转换”中的讨论的隐含转换规则。造型上的另外限制来自指定类型的实际尺寸或表示。有关整数类型的实际尺寸的信息,参见第3章“说明和类型”中的“基本类型的存储”。有关类型造型的更多信息,参见本章后面的“类型造型转换”。
乘法运算符乘法
运算符执行乘法(*)、除法(/)和余数(%)操作。
语法
乘法表达式:
造型表达式
乘法表达式 * 造型表达式
乘法表达式 / 造型表达式
乘法表达式 % 造型表达式
余数运算符(%)的操作数必须是整数。乘法(*)和除法(/)运算符可以使用整数或浮点类型操作数,操作数的类型可以不同。
乘法运算符对操作数执行常用的算术转换,其结果的类型是转换后操作数的类型。
注意:由于乘法运算符执行的转换不提供上溢出和下溢出条件,如果一个乘法运算的结果不能以转换后的操作数的类型表示,则可能丢失信息。
c乘法运算符的描述如下:
运算符 说明
* 该乘法运算符导致它们两个操作数相乘
/ 该除法运算符导致第一个操作数被第二个操作数相除如果两个整数相除且结果不是一个整数,则根据如下规则截除它:
* 根据ANSI C标准,除0的结果是不确定的,C编译器在编译时或运行时产生一个错误
* 如果两个操作数是正数或无符号数,其结果向0进行截除
* 如果有一个操作数为负数,该运算的结果是小于或等于代数商的最大整数或 者大于或等于代数商的最小整数,这被实现所定义(参见下面的Microsoft 特殊处小节)
运算符 说明
% 余数运算符的结果是第一个操作数除以第二个操作数所得的余数。当除法不精确时,其结果由如下规则确定:
* 如果右边操作数是0,其结果是不确定的
* 如果两个操作数都是正数或无符号数,其结果为正数
* 如果有一个操作数为负数,其结果是不精确的,它们被实现所定义(参见下面 的Microsoft特殊处小节)
Microsoft特殊处
在有一个操作数为负数的除法中,截除的方向朝向0。
如果使用余数运算符的除法中有一个操作数为负数,其结果与被除数(表达式中的第一个操作数)的符号相同。
Microsoft特殊处结束例子
下面的说明使用以下例子:
int i=10,j=3,n;
double x=2.0,y;
这个语句使用乘法运算符:
y=x*i;
在这种情况下,x乘以i得到值20.0,其结果为double类型。
n=i/j;
在这个例子中,10被3相除,结果朝向0进行截除,产生整数3。
n=i%j;
这个语句赋给n为10除以3的余数即1。
Microsoft特殊处
余数的符号与被除数的符号相同,例如:50%-6=2-50%6=-2以下情况下,50与2具有相同的符号。
Microsoft特殊处结束
加法运算符
加法运算符执行加法(+)和减法(-)
语法
加法表达式:
乘法表达式
加法表达式 + 乘法表达式
加法表达式 - 乘法表达式
注意:虽然加法表达式的语法包括乘法表达式,这并不表示一定需要使用乘法。有关乘法表达式、造型表达式和单目表达式,参见本书后面的附录A“C语言语法总结”。
操作数可以是整数或浮点值。有些加法运算也可以在指针值上执行,正如每个运算符的讨论中概述的。
加法运算符在整数和浮点操作数上执行常用的算术转换。其结果的类型是转换后操作数的类型。由于加法运算符执行的转换不提供上溢出或下溢出条件,如果一个加法运算的结果不能由转换后操作数的类型表示,则可能丢失信息。
加法(+)
加法运算符(+)导致两个操作数相加。两个操作数可以是整数或浮点类型,或者一个操作数且另一个操作数是一个整数。
当把一个整数加上一个指针时,整数值(i)通过将它乘以该指针所指值的尺寸来转换。在转换之后,整数值表示i存储器位置,而每个位置都有其指针类型指定的长度。不转换的整数值加到该指针值时,其结果是一个新的指针值以表示距离最初地址i个位置的地址。该新的指针值指向与最初指针值相同类型的值,因此与数组指标相同(参见本章前面的“一维数组”和“多维数组”)。如果和的指针指向超过了该数组范围,除了在超过最高端的第一个位置外,其结果是不确定的。有关更多信息参见本章后面的“指针算术”。
减法(-)
减法运算符(-)从第一个操作数减去第二个操作数。两个操作数可以是整数或浮点类型,或者一个操作数可以是一个指针且另一个操作数是一个整数。
当两个指针相减时,其差通过除以该指针所指类型的值的尺寸来转换成一个有符号整数值。该整数值的尺寸由包括在标准头文件STDDEF.H中的类型ptrdiff_t定义。其结果是表示两个地址之间相差的存储器位置的个数,该结果仅对相同数组的两个元素才是有意义的,正如“指针算术”中讨论的。
当从一个指针值减去一个整数时,减法运算符通过该整数值(i)乘以该指针所指值的尺寸来转换,在转换之后,该整数值表示i存储位置,而每个位置都有该指针类型所指的长度。当从该指针值减去该转换的整数值时,其结果是最初地址的前i个存储器位置。新的指针指向最初指针值所指的类型的值。
使用加法运算符
下例说明了加和减运算符,使用了这些说明:
int i=4,j;
float x[10];
float *px;
如下语句是等价的:
px=&x[4+i];
px=&x[4]+i;
i的值乘以一个float的长度并加到&x[4]中,其结果指针值是x[8]的地址。
j=&x[i]-&x[i-2];
在这个例子中,从x 的第五个元素的地址(由x[i]给出)减去x的第三个元素的地址(由x[i-2]给出)。其差除以一个float的长度,结果为整数2。
指针算术
涉及到一个指针和一个整数的加法运算只存在该指针操作数指向一个数组的元素以及该整数产生同一数组中的偏移量时才有意义。当该整数值转换成一个地址偏移量时,编译器假设只有相同尺寸的存储器位置位于最初地址和该地址加上偏离量的结果之间。
对于数组成员这个假设是有效的。通过定义,一个数组是相同类型的值的序列,它的元素存储在相邻存储器位置。但除数组外,其它任何类型的存储不能保证由相同类型的标识符填充,也即,存储器位置之间可能出现空格,即使相同类型的位置也是如此。因此,除数组元素外任何值的地址加或减的结果都是不确定的。
类似地,当两个指针值相减时,其转换假设只有相同类型的值,没有空格,且在操作数给定的地址空间。
按位位移运算符
位移运算符左移(<<)或右移(>>)第一个操作数的位置个数,由第二个操作数指定。
语法
位移表达式:
加法表达式
位移表达式 << 加法表达式
位移表达式 >> 加法表达式
两个操作数必须是整数值,这些运算符执行常用的算术转换,其结果的类型是转换后左操作数的类型。
对于向左移位移,腾出的右边位设置为0。对于向右位移,腾出的左边位基于转换后第一个操作数的类型进行填充。如果该类型是unsigned,它们设置为0;否则,它们用符号位的拷贝进行填充。对于向左位移运算符,没有上溢出,语句:
expr1 << expr2
等价于乘以2expr2。对于右移运算符:
expr1 >> expr2
如果expr1是无符号的或有一个非负值,它等价于除以2expr2。
如果第二个操作数是负数,或者如果右操作数大于或等于提升的左操作数中的位为单位的宽度,则一个位移运算的结果是不确定的。由于位移运算符执行的转换没有提供上溢出或下溢出条件,如果一个位移运算的结果不能由转换后第一个操作数的类型所表示,则可能丢失信息。
unsigned int x,y,z;
x=0x00AA;
y=0x5500;
z=(x<<8)+(y>>8);
在这个例子中,x左移8个位置,y右移8个位置。位移的值相加,结果为0xAA55并赋给z。将一个负数右移一位产生的结果是其绝对值舍入的一半,例如,-253(二进制1111111100000011)右移一位产生-127(二进制1111111110000001)。一个正数253右移一位产生+126。
右移保留符号位。当一个有符号整数右位移时,保留设置最重要的位。当一个无符号整数右位移时,该最重要位被清除。
如果0xF000是无符号数,其结果是0x7800。如果0xF0000000是有符号的,右移一位产生0xF8000000。右移一个正数32次产生0xF0000000。右移一个负数32次产生0xFFFFFFFF。
关系和相等运算符
双目关系和相等运算符将其第一个操作数与第二个操作数进行比较以测试指定关系的有效性。如果测试的关系为真,则该关系表达式的结果为1,如果为假则为0。其结果的类型为int。
语法:
关系表达式:
位移表达式
关系表达式 < 位移表达式
关系表达式 > 位移表达式
关系表达式 <= 位移表达式
关系表达式 >= 位移表达式 相
等表达式:
关系表达式
相等表达式 == 关系表达式
相等表达式 != 关系表达式
关系和相等运算符测试如下关系:
运算符 测试的关系
< 第一个操作数小于第二个操作数
> 第一个操作数大于第二个操作数
<= 第一个操作数小于或等于第二个操作数
>= 第一个操作数大于或等于第二个操作数
== 第一个操作数等于第二个操作数
!= 第一个操作数不等于第二个操作数
上表中的开头四个运算符比相等运算符(==和!=)具有更高的优先级。参见表4.1中的优先级信息。
操作数可以是整数、浮点或指针类型。操作数的类型可以不同。关系运算符在整数和浮点类型操作数上执行常用的算术转换。另外,你可以用关系和相等运算符构成如下操作数类型的组合:
* 任何关系或相等运算符的两个操作数可以是相同类型的指针。对于等于(==)和不等于(!=) 运算符,比较的结果指出两个指针是否指向相同存储器位置。对于其它关系运算符(<、>、<=和>=),比较的结果指出所指对象的两个存储器位置之间的关系。关系运算符仅比较偏移量。
只对相同对象的部分定义了指针比较。如果该指针指向一个数组的成员, 该比较等价于对应下标的比较。第一个数组元素的地址“小于”最后一个元素的地址。在结构的情况下,后面说明的结构成员的指针“大于”前面说明的结构成员的指针。对于同一联合成员的指针是相等的。
* 一个指针值可以与常量值0比较是相等(==)或不相等(!=)。具有0值的指针称为“空”指针,也就是,它不指向任何有效的存储器位置。
* 相等运算符遵守与关系运算符相同的规则,但允许另外的可能:一个指针可以与一个具有0值的常量整数表达式或与void的指针进行比较。如果两个指针都为空指针,它们是相等的。相等运算符比较两个段和偏移量。
例子
如下例子说明关系和相等运算符
int x=0,y=0;
if (x<y)
因为x和y相等,这个例子中的表达式产生值0。
char array[10];
char *p;
for (p=array;p<&array[10];p++)
*p=′/0′;
本例中的段设置array的每个元素为一个空字符常量。
enum color {red,white,green} col;
if (col==red)
.这些语句用标志color说明一个名称为col的枚举变量。在任何时候,该变量可以包含0、1或2的一个整数值,它表示枚举集color:分别为红、白和绿之一。当执行if语句时如果col包含0,则执行依赖该if的任何语句。
按位运算符
按位运算符执行按位AND(&)、按位异或(^)和按位OR(|)运算。
语法
AND表达式:
相等表达式
AND表达式 & 相等表达式
异或表达式:
AND表达式
异或表达式 ^ AND表达式
OR表达式:
异或表达式
OR表达式 | 异或表达式
按位运算符的操作数必须有整数类型,但它们的类型可能不同。这些运算符执行常用的算术转换。其结果的类型是转换后操作数的类型。
C按位运算符描述如下:
运算符 说明
& 按位AND运算符将第一个操作数的每个位与第二个操作数的对应位进行比较,如果两个位都为1,对应的结果位设置为1;否则,对应的结果位设置为0。
^ 按位异或运算符将第一个操作数的每个位与第二个操作数的对应位进行比较,如果一个位为0而另一位为1,对应的结果位设置为1,否则,对应的结果位设置为0
| 按位OR运算符将第一个操作数的每个位与第二个操作数的对应位进行比较,如果有一个位为0,对应的结果位设置为1;否则,对应的结果位设置为1
例子
这些说明用于如下三个例子:
short i=0xAB00;
short i=0xABCD;
short n;n=i&j;
在第一个例子中赋给n的结果与i的值(十六进制0xAB00)相同。
n=i|j;
n=i^j;
在第二个例子中按位OR产生结果0xABCD(十六进制数),而在第三个例子中的按位异或产生0xCD(十六进制数)。
Microsoft特殊处
在有符号整数上按位运算的结果是根据ANSI C标准实现定义的。对于C编译器,在有符号整数上的按位运算与在无符号整数上的按位运算相同。例如,-16&99可以以二进制表示为:
11111111 11110000
& 00000000 01100011
-------------------
00000000 01100000
该按位AND的结果是十进制96。
Microsoft特殊处结束
逻辑运算符逻辑运算符执行逻辑AND(&&)和逻辑OR(||)运算。
语法
逻辑AND表达式
OR表达式
逻辑AND表达式 && OR表
达式OR表达式:
逻辑AND表达式
逻辑AND表达式 || 逻辑AND表达式
逻辑运算符不执行常用的算术转换。代替地,它们根据它的0的等值对每个操作数进行求值,一个逻辑运算的结果为0或1,其结果的类型为int。C逻辑运算符描述如下:
运算符 说明
&& 如果两个操作数都为非0值,则逻辑AND运算符产生值1。如果有一个操作数为0,其结果为0。如果一个逻辑AND运算的第一个操作数等于0,第二个操作数不求值
|| 逻辑OR运算符在其操作数上执行一个OR运算。如果两个操作数都为0值,其结果为0。如果有一个操作数为非0值,其结果为1。如果一个逻辑OR运算符的第一个操作数为非0值,则第二个操作数不求值逻辑AND和逻辑OR表达式的操作数从左到右求值。
如果第一个操作数的值能够足够确定该运算的结果,则第二个结果数不进行求值,这称为“短路求值”。在第一个操作数之后有一个顺序点。有关更多信息参见本章前面的“顺序点”。
例子如下例子说明逻辑运算符:
int w,x,y,z;
if (x<y && y<z)
printf("x is less than z/n");在这个例子中,如果x小于y且y小于z则调用printf函数打印一个消息。
如果x大于y,第二个操作数(y<z)不求值且不打印该消息。注意,在这种情况可能出现问题,第二个操作数由于某些其它原因而有副作用。
printf ("%d",(x==w || x==y || x==z));
在这个例子中,如果x等于w和y或z,printf函数的第二个求值为真时打印1;否则,求值假时打印0。只要条件之一求值为真,则求值停止。
条件表达式运算符
C有一个三元运算符:条件表达式运算符(?:)。
语法
条件表达式:
逻辑OR表达式
逻辑OR表达式 ? 表达式 : 条件表达式
逻辑OR表达式必须具有整数、浮点或指针类型。根据等值于0的方式进行求值。一个顺序点紧跟逻辑OR表达式。操作数求值过程如下:
* 如果逻辑OR表达式不等于0表达式求值。该结果由非终结符表达式给出(这意味着表达式仅在逻辑OR表达式为真时求值)。
* 如果逻辑OR表达式等于0,条件表达式求值。该结果是其条件表达式的值(这意味着仅在逻辑OR表达式为假时对条件表达式求值)。注意,表达式或条件表达式之一求值,但不是两者。一个条件表达式结果的类型依赖于表达式或条件表达式操作数的类型如下:
* 如果表达式或条件表达式具有整数或浮点类型(它们的类型可以不同),该运算符执行常用的算术转换。该结果的类型是转换后操作数的类型。
* 如果表达式和条件表达式具有相同的结构、联合或指针类型,该结果类型是同样的结构、联合或指针类型。
* 如果两个操作数都具有void类型,该结果是void类型。
* 如果操作数之一为任何类型的一个对象的指针,另一个操作数是一个void的指针,该对象的指针转换成一个void的指针,该结果是一个void的指针。
* 如果表达式或条件表达式之一是一个指针,另一个操作数是一个具有0值的常量表达式。该结果的类型是void的指针。
在指针的类型比较中,该指针所指的类型中的任何类型修饰符(const或volatile)都是不重要的。但结果类型以两个条件的组成成分继承修饰符。
例子
如下例子说明条件运算符的使用
j=(i<0) ? (-i) : (i);
这个例子把i的绝对值赋给j。如果i小于0,则把-i赋给j;如果i大于或等于0,i赋给j。
void f1(void)
void f2(void);
int x;int y;
(x == y) ? (f1()) : (f2());
在这个例子中,说明了两个函数f1和f2以及两个变量x和y。在程序后面,如果这两个变量具有相同的值,则调用f1函数;否则调用f2函数。
赋值运算符
一个赋值运算把右边操作数的值赋给左边操作数命名的存储位置。因此,一个赋值运算的左边操作数必须是可修改的l值。在赋值之后,一个赋值表达式具有左边操作数的值,但不是一个l值。
语法
赋值表达式:
条件表达式
单目表达式 赋值运算符 赋值表达式
赋值运算符:如下之一
= *= /= %= += -= <<= >>= &= ^= |+
C中的赋值运算符可以在单个运算中进行转换和赋值。C提供了如下赋值运算符:
运算符 执行的运算
= 简单赋值
*= 乘法赋值
/= 除法赋值
%= 余数赋值
+= 加法赋值
-= 减法赋值
<<= 左位移赋值
>>= 右位移赋值
&= 按位AND赋值
^= 按位异或赋值
|+ 按位或赋值
在赋值中,右边值的类型转换成左边值的类型,并在赋值发生后把该值存储在左边操作数中。左边操作数不能是数组、函数或常量。在本章后面的“类型转换”中详细讨论了指定的转换路径,它依赖于两种类型。
简单赋值
简单赋值运算符把它的右操作数赋给左操作数。右操作数的值转换成赋值表达式的类型,并替换存储在左操作数指定的对象中的值。应用赋值的转换规则(参见本章后面的“赋值转换”)。
double x;
int y;
x=y;
在这个例子中,y的值转换成double类型并赋给x。
复合赋值
复合赋值运算符组合简单赋值运算符和其它双目运算符。复合赋值运算符执行另外运算符指定的运算,然后把结果赋给左操作数。例如,这样的一个复合赋值表达式:
expression1+=expression2
可以理解为
expression1=expression1+expression2
但复合赋值表达式不等于扩充的版本,因为在加法运算中和在赋值运算中,复合赋值表达式仅求值expression1一次,而扩充的版本求值expression1两次。
一个复合赋值运算符的操作数必须是整数或浮点类型。每个复合赋值运算符执行对应的双目运算符执行的转换,并相应地限制它的操作数的类型。加法赋值(+=)和减法赋值(-=)运算符也有一个指针类型的左操作数,在这种情况下右边操作数必须是整数类型。一个复合操作数的结果具有左操作数的值和类型。
#define MASK 0xff00
n&=MASK;
在这个例子中,在n和MASK上执行一个按位AND运算,并把结果赋给n。显式常量MASK用一个#define 预处理器命令定义。
顺序求值运算符
顺序求值运算符也称为“逗号运算符”,从左向右顺序计算它的两个操作数。
语法
表达式:
赋值表达式
表达式,
赋值表达式顺序求值运算符的左操作数作为一个void表达式求值。该运算的结果具有和右操作数相同的值和类型,每个操作数可以是任何类型。顺序求值运算符在它的操作数之间不执行任何类型转换,它不产生l值。在第一个操作数之后产生一个顺序点,这意味着从左操作数求值产生的副作用在右操作数求值开始之前均已完成。有关更多信息,参见本章前面的“顺序点”。
顺序求值运算符通常用于对上下文中两个或多个表达式求值,而这里仅允许一个表达式。
逗号在某些上下文中用作分隔符,但你必须小心不要将它作为分隔符使用和作为运算符使用相混淆,这两种使用是完全不同的。
例子
这个例子说明顺序求值运算符:
for (i=j=1;i+j<20;i+=i,j--);
在这个例子中,for语句的第三个表达式的每个操作数都分开求值。左操作数i+=i首先求值,然后是右操作数j--求值。
func_one (x,y+2,z);
func_two((x--,y+2),z);
在func_one函数调用中,三个用逗号隔开的参量分别传送给:x,y+2和z。在func_two函数调用中,圆括号强制编译器解释第一个逗号为顺序求值运算符。这个函数调用传送两个参量给func_two,第一个参量是顺序求值运算(x--,y+2)的结果,它具有y+2表达式的值和类型,第二个参量是z。
类型转换依赖于指定的运算符和操作数或运算符的类型。类型转换在如下情况下执行:
* 当一个类型的值赋给不同类型的变量时,是或者一个运算符在执行该运算之前转换它的一个操作数或多个操作数的类型时。
* 当一个类型的值显式造型转换为一个不同的类型时。
* 当一个值作为一个参量传送给一个函数或者当从一个函数返回一个类型时。
* 一个字符、一个短整数、一个整数位域、所有有符号的或无符号的或者枚举类型的一个对象都可以用在表达式能够使用整数的地方。如果一个int可以表示所有最初类型的值,那么该值转换成int,否则,它转换成unsigned int。这个过程称为“整数提升”。整数提升保持值,也就是,在提升之后的值保证与提升前的值相同。有关更多信息参见本章前面的“常用的算术转换”。
赋值转换
在赋值运算中,赋值的类型转换成接受赋值的变量的类型。C允许整数和浮点类型之间通过赋值进行转换,即使在转换中选择信息也如此。使用的转换方法依赖于赋值中涉及的类型,正如“常用的算术转换”和如下小节描述的:
* 从有符号整数类型转换
* 从无符号整数类型转换
* 从浮点类型转换
* 从指针类型转换和转换到指针类型
* 从其它类型转换类型修饰符不影响转换的允许性,虽然一个const值不能用作赋值的左边。
从有符号整数类型转换
当一个有符号整数用相等或更大的尺寸转换成一个无符号整数,且该有符号整数不是负数时,该值不改变。通过符号扩充该有符号整数来进行转换,一个有符号整数通过高端的位转换成一个更短的有符号整数,其结果解释为一个无符号整数,如下例子:
int i=-3;
unsigned short u;
u=i;
printf("%hu/n",u); /*打印65533*/
当一个有符号整数转换成一个浮点类型时,没有信息丢失,除当一个longint或unsigned long int值转换成一个float值可能丢失某些精度外.
表4.2总结了有符号整数类型的转换。这个表假设缺省char类型为有符号的。如果你使用一个编译选项改变缺省char类型为无符号的,这个转换在表4.3中给出,即应用unsigned char类型代替表4.2中的转换。
表4.2 有符号整数类型的转换
从 | 到 | 方法 |
char | short | 符号扩充 |
char | long | 符号扩充 |
char | unsigned char | 保留模式,丢失高端作为符号的位 |
char | unsigned short | 符号扩充为short,转换short为unsignedshort |
char | unsigned long | 符号扩充为long,转换long为unsigned long |
char | float | 符号扩充为long,转换long为float |
char | double | 符号扩充为long,转换long为double |
char | long double | 符号扩充为long,转换long为double |
shor | tChar | 保留低端字节 |
short | long | 符号扩充 |
short | unsigned char | 保留低端字节 |
short | unsigned short | 保留模式,丢失高端作为符号位的位 |
short | unsigned long | 符号扩充为long,转换long为unsigned long |
short | float | 符号扩充为long,转换long为float |
short | double | 符号扩充为long,转换long为double |
short | long double | 符号扩充为long,转换long为double |
long | char | 保留低端字节longshort保留低端字 |
long | unsigned char | 保留低端字节 |
long | unsigned short | 保留低端字 |
long | unsigned long | 保留模式,丢失高端作为符号位的位 |
long | float | 作为float表示,如果long不能精确表示,丢失精度longdouble作为double表示,如果long不能作为一个double精确表示,丢失精度 |
long | long double | 作为double表示,如果long不能作为一个double精确表示,丢失精度 |
1. 所有char项假设char类型缺省是有符号的。
Microsoft特殊处
对于一个Microsoft 32位C编译器,一个整数等价于long。一个int值转换处理与long的相同。
Microsoft特殊处结束
从无符号整数类型转换
一个无符号整数通过截除高端的位转换成一个更短的无符号或有符号整数,或者通过0扩充(参见表4.3)转换成一个更长的无符号或有符号整数。当整数类型的值用更小的尺寸降级为一个有符号整数,或者一个无符号整数转换成它对应的有符号整数时,如果它能在新类型中表示则不改变它的值。但如果设置符号位,则它表示的值发生改变,如下例子:
int j;
unsigned short k=65533;
j=k;
printf("%hd/n",j); /*打印-3*/
如果它不能表示,其结果是实现定义的。有关C编译器处理整数降级的更多信息,参见本章后面“类型造型转换”。从整数类型转换或从造型整数类型的转换的结果相同。
无符号值只能以保留它们的值的方式进行转换,在C中是不可直接表示的。唯一的异常是从unsigned long转换到float,它丢失了最重要的低端位。否则保留值,无论是有符号的还是无符号的。当一个整数类型的值
转换成浮点时,该值超过了表示范围,其结果是不可预料的(有关整数和浮点类型的范围的信息,参见第3章“说明和类型”中的“基本类型的存储”)。
表4.3总结了从无符号整数类型的转换。
表4.3 从无符号整数类型的转换
从 | 到 | 方法 |
unsigned char | char | 保留位模式,高端位变成符号位 |
unsigned char | short | 0扩充 |
unsigned char | long | 0扩充 |
unsigned char | unsigned short | 0扩充 |
unsigned char | unsigned long | 0扩充 |
unsigned char | float | 转换为long,转换long为float |
unsigned char | double | 转换为long,转换long为double |
unsigned char | long double | 转换为long,转换long为double |
unsigned shor | tchar | 保留低端字节 |
unsigned shor | tshort | 保留位模式,高端位变成符号位 |
unsigned shor | tlong | 0扩充 |
unsigned shor | tunsigned char | 保留低端字节 |
unsigned short | unsigned long | 0扩充 |
unsigned short | float | 转换为long,转换long为float |
unsigned short | double | 转换为long,转换long为double |
unsigned short | long double | 转换为long,转换long为double |
unsigned long | char | 保留低端字节 |
unsigned long | Short | 保留低端字 |
unsigned long | long | 保留位模式,高端位变成符号位 |
unsigned long | unsigned char | 保留低端字节 |
unsigned long | unsigned short | 保留低端字 |
unsigned long | float | 转换为long,转换long为float |
unsigned long | double | 直接转换为double |
unsigned long | long double | 转换为long,转换long为double |
Microsoft特殊处
对于Microsoft 32位C编译器,unsigned int类型等价于unsigned long类型。一个unsigned int值的转换的方式与一个unsigned long的转换方式相同。如果转换的值大于最大正的有符号long值,那么从unsigned long值转换到float是不准确的。
Microsoft特殊处结束
从浮点类型转换
一个float 值可以转换成一个double或long double,或者一个double转换成一个long double,不改变其中的值。如果可能,一个double值转换成一个准确表示的float值。如果该值不能准确表示,则可能丢失精度。如果结果超出了范围,其行为是不可确定的。有关浮点类型的范围,参见第1章“C的基本元素”中的“浮点常量的限制”。
通过把一个浮点值先转换成一个long,然后把该long值转换成指定的整数值,正如表4.4所说明的,把该浮点值转换成一个整数值,在转换成long中,丢弃该浮点值的小数部分。如果仍太大不适合long表示,则该转换的结果是不确定的。
Microsoft特殊处
当把一个double或long double浮点数转换成一个较小的浮点数时,如果发生下溢出则朝向0截除该浮点数的值。一个上溢出导致一个运行错误。注意,C编译器把long double映射成double。
Microsoft特殊处结束
表4.4总结了从浮点类型的转换。
表4.4 从浮点类型的转换
从 | 到 | 方法 |
float | char | 转换为long;转换long为char |
float | short | 转换为long;转换long为short |
float | long | 在小数点处截除。如果结果太大不能表示成long,则其结果是不确定的 |
float | unsigned short | 转换为long,转换long为unsigned short |
float | unsigned long | 转换为long,转换long为unsigned long |
float | double | 改变内部表示 |
float | long chouble | 改变内部表示doublechar转换为float,转换float为char |
double | short | 转换为float,转换float为short |
double | long | 在小数点处截除。如果该结果太大不能用long表示,则结果是不确定的 |
double | unsigned short | 转换为long,转换long为unsigned short |
double | unsigned long | 转换为long,转换long为unsigned longdoublefloat表示为float。如果double值不能以float类型准确地表示,会出现精度丢失。如果该值太大不能作为float表示,其结果是不确定的 |
longdouble | char | 转换为float,转换float为char |
long doubl | short | 转换为float,转换float为short |
longdouble | long | 在小数点处截除。如果结果太大不能用long表示,则结果是不确定的 |
longdouble | unsignedshort | 转换为long;转换long为unsigned short |
longdoub | leunsignedlong | 转换为long;转换long为unsigned long |
longdouble | float | 表示为float。如果double值不能以float类型准确地表示,会出现精度丢失。如果该值太大不能作为float表示,其结果是不确定的 |
longdouble | double | 该long double值作为double处理如果被转换的值大于最大正数long值,则从float、double或long double值到unsigned long的转换是不准确的。 |
转换到指针类型和从指针类型转换
一个值类型的指针可以转换成一个不同类型的指针。虽然这个结果可能因为对齐要求和不同类型的存储尺寸而导致其结果是不确定的。一个对象的指针可以转换成一个其类型需要较少或完全相同的对象的指针,反过来没有改变。
一个void的指针可以转换成任何类型的一个指针,或者从任何类型的指针可以转换成一个void的指针。
如果其结果被回过来转换成最初的类型,则恢复最初的指针。如果一个指针转换成另一个指针,后者有相同的类型,但有不同的或另外的修饰符,该新的指针与老的指针相同,除了强制加上新的修饰符之外。一个指针值也可以转换成一个整数值。根据如下规则,其转换路径依赖于该指针的尺寸和整数类型的尺寸:
* 如果指针的尺寸大于或等于该整数的尺寸,该指针的行为在转换中像一个无符号值一样,除了它不能转换成一个浮点值外。
* 如果该指针小于整数类型,首先把该指针转换成一个与整数类型相同尺寸的指针,然后再转换成整数类型。相反地,一个整数类型也可以根据如下规则转换成一个指针类型:
* 如果该整数类型与指针类型具有相同的尺寸,简单地转换该整数值作为一个指针(一个无符号整数)处理。
* 如果该整数的尺寸不同于该指针类型的尺寸,首先把整数类型转换成该指针的尺寸,这里使用表4.2和表4.3中给出的转换路线;然后把它作为一个指针值处理。
一个具有0型的整数常量表达式或一个强制造型为类型void *的表达式可以通过一个类型造型、赋值或与任何类型的指针进行比较来转换。这产生一个等于另一个相同类型空指针的空指针,但这个空指针不等于一个函数或一个对象的任何指针。不为常量0的其它整数也可以转换成指针类型,但其结果不是可移植的。
从其它类型转换
由于一个enum值是一个定义的int,从一个enum值转换或转换到enum值与int类型的方式相同。对于C编译器,一个整数与一个long是相同的。
Microsoft特殊处
结构或联合类型之间不允许转换。
任何值可以转换成类型void,但这个转换的结果仅用在一个表达式值被丢弃的上下文中,例如在一个表达式语句中。
由定义,void类型没有值,因此它不能转换成任何其它类型,其它类型也不能通过赋值转换成void,然而你可以显式造型转换一个值为类型void,正如下一节“类型造型转换”中讨论的。
Microsoft特殊处结束
类型造型转换
你可以使用类型造型显式转换类型。
语法造型表达式:
单目表达式
(类型名称)造型表达式
类型名称:
指示符修饰符表 抽象说明符opt
类型名称是一个类型,造型表达式是要转换成这个类型的值。一个带有类型造型的表达式不能是一个l值。造型表达式是被转换赋给类型名称指定类型的变量。赋值的转换规则(在前面“赋值转换”中描述)也适用于类型造型。
表4.5说明了可以造型转换为给定类型的类型。
表4.5 合法的类型造型
目的类型 | 可能的源 |
整数类型 | 任何整数类型、浮点类型或一个针对对象的指针 |
浮点类型 | 任何算术类型 |
一个针对对象的指针或(void*) | 任何整数类型、(void*)、一个针对对象的指针或一个函数指针函数指针任何整数类型、 |
void类型 | 任何类型 |
任何标识符都可以造型转换为void,但如果在一个类型造型表达中的指定的类型不是void,那么该标识符造型转换成不是一个void表达式的类型。任何表达式都可以造型为void,但一个类型为void的类型不能造型转换为任何其它类型。例如,一个具有void类型的函数不能有另一个类型的返回值。
注意,一个void表达式有一个void的类型指针,而不是类型void。如果一个对象类型为void类型,其结果表达式不能赋给任何项。类似地,一个类型造型对象不是一个可接受的l值,因此不能赋给一个类型造型对象。
Microsoft特殊处
只要该标识符的尺寸不发生改变,一个类型造型可以是一个l值表达式。有关l值表示式的信息,参见本章开头的“L值和R值表达式”。
Microsoft特殊处结束
你可以用一个造型把一个表达式转换为类型void,但结果表达式只能用于不需要一个值的地方。一个转换为void *的对象指针,在回过来转换为最初的类型时将返回它的最初值。
函数调用转换在一个函数调用中参量上执行的转换类型依赖于用于说明该被调用函数的参量的函数原型(向前说明)。
如果有一个函数原型上包括说明的参量类型,编译器执行类型检测(参见第6章“函数”)。
如果没有函数原型,则在函数调用中参量上执行常用的算术转换。这些转换在调用中的每个参量上独立执行。这意味着一个float值可以转换为一个double值;一个char或short值可以转换为一个int;一个unsignedchar或unsigned sort可以转换为一个unsigned int。