分三部分来讲
一、左值与右值
参看:左值与右值
首先我们需要理解左值和右值的定义:
左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以做为一个左值。
右值指的是引用了一个存储在某个内存地址里的数据。
从上面的两个定义可以看出,左值其实要引用一个对象,而一个对象在我们的程序中又肯定有一个名字或者可以通过一个名字访问到,所以左值又可以归纳为:左值表示程序中必须有一个特定的名字引用到这个值。而右值引用的是地址里的内容,所以相反右值又可以归纳为:右值表示程序中没有一个特定的名字引用到这个值除了用地址。
好这些都是从定义上理解左值右值,那么我们再用这些定义作为我们的理论基础来总结一下哪些是左值,哪些是右值:
左值:
Expression | Lvalue |
x = 4 | x |
*ptr = newvalue | *ptr |
++a | ++a |
b[0] = 100 | b[0] |
const int m = 10 | m |
int & f() | The function call to f() |
右值:
Expression | Rvalue |
100 | 100 |
a * b | The expression of a * b |
a++ | a++ |
int f() | The function call to f() that does notreturn reference |
以上这些内容都可以用定义来解释为什么这些为左值,而那些为右值。但我要特殊解释一下为什么函数的调用只能作为右值除了这个函数返回的是引用。其实这个也非常好解释,因为如果一个函数返回的值是内建类型,那么这个返回值是没有办法通过一个名字或者表达式引用到的,同理如果一个函数返回的是一个对象,那么这个对象是一个临时的,也不可能用一个名字访问到。所以函数的调用通常只能作为右值,但如果一个函数返回引用,那么它的返回值就有意义了,因为它是另一个名字的别名,有名字了,所以它就变成了左值。
注意:左值能转化为右值,但反之不行。
好了,讲了这么多我觉得已经足够,但还要多讲一点,这点就是哪些操作符必需左值.
Operator | Requirement |
& | Operand must be an lvalue |
++ -- | Operand must be an lvalue.This applies to both prefix and posfix forms |
= += -= *= %= <<= >>= &= ^= |= | Left operand munst be an lvalue |
附:《C primer plus》中左值、右值的定义
“数据对象”是泛指数据存储区的术语,数据存储区能用于保存值。
“左值”指用于标识一个特定的数据对象的名字或表达式。即对象指实际的数据存储,而左值是用于识别或定位那个存储的标识符(或表达式)。
“右值”指可能赋给可修改左值的量。
二、运算符优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | -- |
() | 圆括号 | (表达式)/函数名(形参表) | -- | ||
. | 成员选择(对象) | 对象.成员名 | -- | ||
-> | 成员选择(指针) | 对象指针->成员名 | -- | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
~ | 按位取反运算符 | ~表达式 | |||
++ | 自增运算符 | ++变量名/变量名++ | |||
-- | 自减运算符 | --变量名/变量名-- | |||
* | 取值运算符 | *指针变量 | |||
& | 取地址运算符 | &变量名 | |||
! | 逻辑非运算符 | !表达式 | |||
(类型) | 强制类型转换 | (数据类型)表达式 | -- | ||
sizeof | 长度运算符 | sizeof(表达式) | -- | ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | |||
% | 余数(取模) | 整型表达式%整型表达式 | |||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | |||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | |||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | |||
< | 小于 | 表达式<表达式 | |||
<= | 小于等于 | 表达式<=表达式 | |||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | |||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
| |||||
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | -- |
/= | 除后赋值 | 变量/=表达式 | -- | ||
*= | 乘后赋值 | 变量*=表达式 | -- | ||
%= | 取模后赋值 | 变量%=表达式 | -- | ||
+= | 加后赋值 | 变量+=表达式 | -- | ||
-= | 减后赋值 | 变量-=表达式 | -- | ||
<<= | 左移后赋值 | 变量<<=表达式 | -- | ||
>>= | 右移后赋值 | 变量>>=表达式 | -- | ||
&= | 按位与后赋值 | 变量&=表达式 | -- | ||
^= | 按位异或后赋值 | 变量^=表达式 | -- | ||
|= | 按位或后赋值 | 变量|=表达式 | -- | ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | -- |
说明:
同一优先级的运算符,运算次序由结合方向所决定。
简单记就是:! > 算术运算符 > 关系运算符 > && > || > 赋值运算符
一些容易出错的优先级问题
优先级问题 | 表达式 | 被误认为的结果 | 正确的结果 |
.优先级高于*,-> | *p.f | (*p).f | *(p.f)对p取f偏移,作为指针, 然后进行解除操作 |
==和!=高于位操作 | (var&mask !=0) | (var&mask)!=0 | Var & (mask !=0) |
==和!=高于赋值 | C=getchar() !=EOF | (c=getchar())!=EOF | C=(getchaor()!=EOF) |
算术运算符高于位移运算法 | Mask << 4+3 | (Mask <<4)+3 | Mask << (4+3) |
逗号运算符在所有运算符中 优先级最低 | i=1,2 | I=(1,2) | (i=1),2 结果为i=2 |
三、扩展总结
1、赋值运算符:=
赋值运算符的动作是从右到左,且不能将一个值赋给一个常量。赋值,从C的角度来看,主要目的是对表达式来求值。
2、除法运算符:/
7/4 = 1;
7./4 = 1.75;
7./4. = 1.75;
11 / 5 = 2; 11 / -5 = -2; -11 / -5 = 2; -11 / 5 = -2;
在C中,整数除法结果的小数部分都被丢弃,没有把整数除法运算的结果四舍五入到最近的整数。
当对整数与浮点数进行混合运算时,结果是浮点数。
3、取余运算符:%
取余运算符用于整数运算,该运算符计算出用它右边的整数去除它左边的整数得到的余数。
注意:不要对浮点数使用该运算符,那将是无效的。
负数取余规则:
如果第一个操作数为负数,那么得到的余数也为负数;如果第一个操作数为正数,那么得到的余数也为正数。
11 % 5 = 1;11 % -5 = 1;-11 % 5 = -1;-11 % -5 = -1;
4、自增和自减:++和--
后缀: a++; a--; 使用a的值之后改变a;
前缀: ++a; --a; 使用a的值之前改变a;
++a = 10;
++a 的结果是a值的拷贝,并不是变量本身,你无法向一个值进行赋值。赋值运算的左操作数必须是左值。
后续补充几个例题。。。
四、布尔值
通过包含stdbool.h头文件,可以用bool代替关键字_BOOL表示这种类型,并用标识符true和false代替1和0.
五、逻辑运算符
与(&&)、或(||)、非(!)
或(||)和与(&&)都具有短路特征,如果前一个逻辑表达式的结果可以决定整个表达式结果则计算机会忽略后面的逻辑表达式。
短路特性具体为:
表达式一 && 表达式二 如果表达式一为假,就不会进行表达式二的计算
表达式一 || 表达式二 如果表达式一为真,就不会进行表达式二的计算
六、三目表达式
三目操作符可以在两个不同的计算机规则中选择一个
三目操作符的格式如下
布尔值 ? 公式一: 公式二
//布尔值需要有定义的 ret=(num >=0) ? num : 0-num;
如果布尔值为真则采用公式一计算结果
如果布尔值为假则采用公式二计算结果
七、== 和 =不同
符号 = 作为赋值运算,符号 ==作为比较。注意,用于测试两个表达式是否相等的操作符是==,如果无用了=操作符,虽然它也是合法的表达式,但其结果几乎肯定和你的本意不一样,它将执行赋值操作而不是比较操作。
比如下例,该语句本意似乎是要检查 x 是否等于 y
if (x = y)
break;
而实际上是将 y 的值赋给了 x,然后检查该值是否为零。
还需注意:任何非零值都是真.比如,while (-1) == while (true)
再看下面的例子:
while (c = ' ' || c == 't' || c =='\n')
c = getc (f);
本例中循环语句的本意是跳过文件中的空格符、制表符和换行符。由于程序员在比较字符 ' ' 和变量 c 时,误将比较运算符 == 写成了赋值运算符 = 。因为赋值运算符 = 的优先级要低于逻辑运算符 || ,因此实际上是将以下表达式的值赋给了 c
' ' || c == 't' || c == '\n'
因为 ' ' 不等于零 (' ' 的ASCII 码为 32),那么无论变量 c 此前为何值,上述表达式求值的结果是 1,因此循环将一直进行下去直到整个文件结束。
另一方面,如果把赋值运算符写成比较运算符,同样会造成混淆:
if ((filedesc == open(argv[i] , 0)) < 0)
error ();
在本例中,如果函数open执行成功,将返回 0 或者正数;而如果函数 open 执行失败,将返回 -1.上面这段代码的本意是将函数 open的返回值存储在变量 filedesc 之中,然后通过比较变量 filedesc 是否小于 0 来检查函数 open 是否执行成功。但是,此处的 == 本应是 = 。而按照上面代码中的写法,实际进行的操作是比较函数 open 的返回值与变量 filedesc ,然后检查比较的结果是否小于 0.因此比价运算符 == 的结果只可能是 0 或 1.永远不可能小于 0,所以函数 error ( )将没有机会被调用。如果代码执行,似乎一些正常,除了变量 filedesc 的值不再是函数 open 的返回值。
八、在表达式中使用无符号数
库函数 strlen 的原型如下:
size_t strlen (char const *string);
注意:strlen 返回一个类型为 size_t 的值。这个类型是在头文件 stddef.h 中定义的,它是一个无符号整数类型。在表达式中使用无符号数可能导致不可预料的结果。例如下面的表达式:
#include <stdio.h>
#include <string.h>
int main (void)
{
char ptr1[] = "beijing";
char ptr2[] = "hello world";
if (strlen (ptr1) - strlen (ptr2) >= 0)
{
printf ("1111111111\n");
}
printf ("2222222222\n");
return 0;
}
strlen (ptr1) - strlen (ptr2) 为无符号类型,得不到想要的结果,应该为:
#include <stdio.h>
#include <string.h>
int main (void)
{
char ptr1[] = "beijing";
char ptr2[] = "hello world";
if (strlen (ptr1) >= strlen (ptr2))
{
printf ("1111111111\n");
}
printf ("2222222222\n");
return 0;
}