c语言修炼秘籍【第四章】操作符
【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译
文章目录
前言
本文会对c语言中使用到的操作符进行介绍,包括
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
- 表达式求值
一、算术操作符
+ // 加
- // 减
* // 乘
/ // 除
% // 取模
- 除
%
外,其他的几个操作符都可以作用于整数和浮点数;- 对于
/
操作符,如果两个操作数都是整数,则执行整数除法;而只要有一个操作数为浮点数,就执行浮点数除法。%
操作符的两个操作数必须都是整数。返回整除之后的余数。
#include <stdio.h>
int main()
{
int a = 2;
int b = 3;
float c = 3.0f;
printf("a / b == %f\n", a / b);
printf("a / c == %f\n", a / c);
return 0;
}
运行结果:
可以看到,2 / 3
以浮点数的形式打印出来,仍然是0;2 / 3.0
打印的0.666667才是正确的浮点类型结果。
二、移位操作符
>> // 右移操作符
<< // 左移操作符
// 注:移位操作符的操作数只能是整数
1.左移操作符
移位规则:
左边抛弃,右边补0
移位操作符对操作数进行操作时,是对它的保存在内存中的二进制形式进行操作。
如,5 << 1;
在移位过程中,5本身并没有被改变(没有进行赋值)。
2.右移操作符
右移的移位规则分成两种:
- 逻辑右移:右边抛弃,左边补0
- 算术右移:右边抛弃,左边补符号位
再这里会涉及到符号位,所以简单介绍一下,
计算机中只有两种状态1
和0
,所以计算机中的数都是以二进制的形式保存并使用的,
而二进制数又分为有符号数和无符号数,其中有符号数中的符号的正
,负
对机器而言是无法识别的,但刚好对应着两种状态,所以正
,负
这两种符号也被数字化了,规定0
表示正
,1
表示负
,最高位为符号位。
有符号数可分为:原码
,补码
,反码
这三种。计算机中用补码保存二进制数
- 原码:
正数:它的原码就是它自身;
负数:符号位为1,其余位和它的绝对值的原码相同;
- 反码:
正数:它的反码就是它自身;
负数:符号位不变,其余位是它的原码取反;
- 补码:
正数:它的补码就是它自身;
负数:它的反码+1;例如:5
它的二进制形式为:
0000000 00000000 00000000 00000101 - - 原码
0000000 00000000 00000000 00000101 - - 反码
0000000 00000000 00000000 00000101 - - 补码
正数这三种形式相同
再例如:-3
它的绝对值的原码为:00000000 00000000 00000000 00000011
10000000 00000000 00000000 00000011 - - 原码 - - 改变了符号位
11111111 11111111 11111111 11111100 - - 反码 - - 符号位之外全部把按位取反
11111111 11111111 11111111 11111101 - - 补码 - - 反码 + 1
对于逻辑移位
,正如它的名字,它只负责把内存中的二进制位往右移动指定的位数,使用这种方法进行移位时,它压根不管符号位,既然不管符号位,缺位直接补0
就行。
例如:-2 >> 1
首先,将-3转换为补码形式
1000000 0000000 0000000 0000010 - - 原码 - - 改变了符号位
11111111 11111111 11111111 11111101 - - 反码 - - 符号位之外全部把按位取反
11111111 11111111 11111111 11111110 - - 补码 - - 反码 + 1
右移1位:(逻辑右移)
01111111 11111111 11111111 11111111 - - 补码
此时符号位为0,机器认为它是一个正数,补码和原码相同,
-2 >> 1就变成了有符号int类型能表示的最大值;
对于算术移位
,既然是算术,肯定要考虑符号了,所以以这种方式进行右移操作时,左边补位时是使用它的符号位来补的
同样的例子:-2 >> 1
右移1位:
11111111 11111111 11111111 11111111 - - 补码
转换为原码:
10000000 0000000 00000000 00000000 - - 反码
10000000 0000000 00000000 00000001 - - 原码
-2 >> 1就变成了-1;
那么我们的计算机中的右移操作是使用的哪种方式呢?
使用下面的代码进行验证:
#include <stdio.h>
#include <limits.h>
int main()
{
int a = -2;
printf("a >> 1 == %d\n", a >> 1);
printf("INT_MAX == %d\n", INT_MAX);
return 0;
}
运行结果:
可以看到,a >> 1输出的结果为-1
,符合算术右移
的分析。
注:移位操作符不可移动负数位,这是标准未定义的操作
int a = -10;
a >> -1 // error,标准未定义
三、位操作符
& // 按位与
| // 按位或
^ // 按位异或
注意:它们的操作数必须是整数
下面代码会输出什么呢?
#include <stdio.h>
int main()
{
int a = 5;
int b = 6;
printf("a & b == %d\n", a & b);
printf("a | b == %d\n", a | b);
printf("a ^ b == %d\n", a ^ b);
return 0;
}
5 - - 补码 - - 00000000000000000000000000000101
6 - - 补码 - - 00000000000000000000000000000110
1 & 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000100 - - 4
1 | 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000111 - - 7
1 & 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000011 - - 3
运行结果:
一道面试题:
不能创建临时变量,实现两个数的交换
#include <stdio.h>
// 方法一
// 使用临时变量进行交换
void swap1(int* num1, int* num2)
{
int tmp = 0;
tmp = *num1;
*num1 = *num2;
*num2 = tmp;
}
// 方法二
// 不使用临时变量进行交换
void swap2(int* num1, int* num2)
{
*num1 = *num1 + *num2;
*num2 = *num1 - *num2;
*num1 = *num1 - *num2;
}
// 方法三
// 不使用临时变量进行交换
void swap3(int* num1, int* num2)
{
*num1 = *num1 ^ *num2;
*num2 = *num1 ^ *num2;
*num1 = *num1 ^ *num2;
}
int main()
{
int a = 5;
int b = 6;
swap1(&a, &b);
printf("a == %d, b == %d\n", a, b);
swap2(&a, &b);
printf("a == %d, b == %d\n", a, b);
swap3(&a, &b);
printf("a == %d, b == %d\n", a, b);
return 0;
}
运行结果:
注意:
方法二看上去虽然可以实现不使用临时变量就交换两个数,但实际上,这个代码是存在问题的 - - num1 + num2
可能会溢出。
虽然,在VS编译器中,该方法实际执行过程中,在出现溢出时的结果并没有出错,但这并不意味着,该方法正确;在c语言中有符号整数的溢出是未定义行为,因此该代码的“正确性”是依赖于编译器和硬件的,具有偶然性。
交换两个整数
应该使用第一种或第三种方法。
练习
求一个整数存储在内存中的二进制中1的个数。
方法一,请思考该方法是否存在问题
#include <stdio.h>
int main()
{
int num = 7;
int count = 0;
int n = num;
while (n)
{
if (n % 2 == 1)
{
count++;
}
n = n / 2;
}
printf("%d中1的个数 == %d\n", num, count);
return 0;
}
分析
当num为负数时,例如,-1 - - 内存中的数是以补码形式保存 - -> 11111111111111111111111111111111 - - 有32个1
但 -1 % 2 == 1
、-1 / 2 == 0
,此时count == 1
循环结束,结果错误。
显然,该方法无法计算负数的二进制中1的个数。
方法二
#include <stdio.h>
int main()
{
int num = 7;
int i = 0;
int count = 0;
// int类型内存中占32bit
for (i = 0; i < 32; i++)
{
if ((num >> i) & 1)
{
count++;
}
}
printf("%d中1的个数 == %d\n", num, count);
return 0;
}
分析
该方法能够完成所需功能,但应该注意到,该方法对于所有的数都会进行32次循环,当它处理1时 - - 仅仅只有1个二进制1
,但却也需要处理32次,能否进行优化呢?
方法三
#include <stdio.h>
int main()
{
int num = 7;
int count = 0;
int n = num;
while (n)
{
n = n & (n - 1);
count++;
}
printf("%d中1的个数 == %d\n", num, count);
return 0;
}
分析
该方法的处理次数与待处理数有关,它有多少个1
就循环多少次。
举个例子:
7的二进制形式
第一次
0111 - - n
0110 - - n - 1
0110 - - n = n & (n - 1)
第二次
0101 - - n - 1
0100 - - n = n & (n - 1)
第三次
0011 - - n - 1
0000 - - n = n & (n - 1)
n = n & (n - 1)
这个运算每次都将n最后一个1化为0。
四、赋值操作符
赋值操作符,能让你给一个变量赋一个你任意想要的值。
int height = 160; // 身高
height = 180; // 不满意就赋值
float score = 3.0f; // 评分
score = 13.0f; // 赋值为13.0
赋值操作符=
,可以连续使用。
int x = 1;
int y = 2;
int z = 3;
z = y = x + 2;
// 对比上述代码
y = x + 2;
z = y;
// 下面的代码更让人容易接受,可读性更高。
一些复合赋值操作符:
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
//x += 3 等价于 x = x + 3
// 其他的与之用法相同
五、单目操作符
1.单目操作符的介绍
单目操作符的种类
! // 逻辑反操作
- // 负值
+ // 正值
& // 取地址
sizeof // 操作数的类型长度(以字节为单位)
~ // 按二进制位取反
++ // 前置、后置++
-- // 前置、后置--
* // 间接访问操作符(解引用操作符) -- 指针的使用
(类型) // 强制类型转换
// 其中 x++ 等价于 x += 1
// -- 用法类似
// 除此之外,++ 和 -- 还分为前置和后置之分
// x++ 和 --x,虽然都是对x自增1,但x++是用后再加,++x是用前先加
// 举个例子
int x = 3;
int y = 3;
printf("x++ == %d\n", x++);
printf("++y == %d\n", ++y);
// 输出结果为:
// x++ == 3
// ++y == 4
// x是先被输出再+1
// y是先+1再输出
// 使用示例
#include <stdio.h>
int main()
{
if (!1)
printf("!1\n");
else
{
printf("0\n");
}
int a = 1;
printf("a == %d, -a == %d, +a == %d\n", a, -a, +a);
printf("a`s address == %p\n", &a);
printf("sizeof(a) == %d\n", sizeof(a));
a = -1;
printf("~a == %d\n", ~a);
int x = 3;
int y = 3;
printf("x++ == %d\n", x++);
printf("++y == %d\n", ++y);
printf("*(&a) == %d\n", *(&a));
a = 97;
printf("(char)a == %c\n", (char)a);
return 0;
}
示例运行结果:
关于
sizeof
,它其实是一个操作符,并非是一个函数,验证代码如下
#include <stdio.h>
int main()
{
int a = 10;
printf("sizeof int == %d\n", sizeof a);
return 0;
}
可以看到,输出确实是4,这里的sizeof并没有使用()函数调用,这也证明了sizeof
的确不是函数。
注:sizeof int
这是不允许的
2.sizeof 和数组
看下面的代码,你觉得它们会输出什么?
#include <stdio.h>
void test1(int a[])
{
printf("sizeof a == %d\n", sizeof(a));
}
void test2(char b[])
{
printf("sizeof b == %d\n", sizeof(b));
}
int main()
{
int a[10] = { 3 };
char b[10] = { 'a' };
printf("sizeof a == %d\n", sizeof(a));
printf("sizeof b == %d\n", sizeof(b));
test1(a);
test2(b);
int x = 10;
printf("sizeof x == %d\n", sizeof(x += 2));
printf("x == %d\n", x);
return 0;
}
得出你的结果了吗?特别是最后的
x
的输出是多少?
运行结果如下:
有没有出乎你的预料?
test1()
和test2()
中的输出都为4是因为数组名表示的首元素的地址,地址在32位平台中占4个字节,所以输出4
而在main()
函数中的输出为40和10是因为数组名的表示含义有两个特例:
sizeof(<数组名>),这里的数组名表示整个数组;
&<数组名>,这里的数组名也表示整个数组;
所以主函数中的输出为整个数组在内存中所占的字节数。
对于最后的x
输出的值为4,首先,我们要先明确一个表达式有两种属性:
- 值属性
- 类型属性
例如:
int a = 10;
a这个表达式它的值属性此时为10;类型属性为int
char b = ‘a’;
b这个表达式它的值属性此时为98 - - 字符在内存中以ASCII码形式存储;类型属性为char
sizeof
这个操作符在使用时,仅需要使用表达式的类型属性即可 - - 类型决定了它在内存中所占据的空间,不关心它的值为多少,其中的表达式不会真的计算。
也就是说sizeof(x += 2)中的x+=2并没有计算,仅仅是获取到x是一个int类型即可,所以x的值没有发生改变
你觉得下面的代码会输出什么?
#include <stdio.h>
int main()
{
char a[] = "hello";
char b[] = "hello world";
if (sizeof(a) - sizeof(b) < 0)
{
printf("a shorter than b\n");
}
else
{
printf("a longer than b\n");
}
return 0;
}
运行结果:
这是为什么呢?a明明比b短,为什么会输出a比b更长呢?
让我们来看看sizeof
的定义:
通过size_t
的定义可知,它是无符号整型的别名。所以,sizeof
返回的值是个无符号数,两个无符号数的运算恒大于等于0,所以上面的代码一定会输出else中的语句。
六、关系操作符
<
<=
>
>=
==
!=
这些关系操作符用法简单,就不再赘述。只需要注意不要将 = 和 == 混用即可。
七、逻辑操作符
&& // 逻辑与
|| // 逻辑或
// 结果只有真和假
注意区分按位与和逻辑与,按位或和逻辑或
1&2 == 0
1&&2 == 1 // 真
1|2 == 3
1||2 == 1 // 真
一道360的面试题:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++ || ++b || d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
i = a++ && ++b && d++;
这里的输出结果是什么呢?
a = 1, b = 3, c = 3, d = 5?
运行结果:
i = a++ || ++b || d++;
这里的输出结果是又是什么呢?
a = 1, b = 3, c = 3, d = 5?
分析
从第一张图中可以看出i = a++ || ++b || d++;
这条语句中,仅执行了a++
,后续的操作并没有执行,这里因为a++是后置++,在&&
操作的过程中是0
为假,逻辑与&&
中只要有一个操作数为假,则整个语句为假,后续的操作没有执行,所以结果为1 2 3 4;
第二张图中能看出执行了a++
和++b
,与上面分析类似,逻辑或||
中只要有一个操作数为真,则整个语句为真,++b前置++,结果为真,所以,后续的d++就没有执行,最终输出结果为1 3 3 4。
八、条件操作符
exp1 ? exp2 : exp3
使用示例:
int a = 1;
int b = 2;
if(a > b)
{
printf("%d", a);
}
else
{
printf("%d", b);
}
// 用条件操作符改写
(a >b) ? printf("%d", a) : printf("%d", b);
九、逗号表达式
exp1, exp2, exp3, …, expN
逗号表达式就是用,
隔开的多个表达式。
从左往右依次计算。整个表达式的结果为最后一个表达式的结果。
#include <stdio.h>
int main()
{
// 代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("c == %d\n", c);
// c是多少?
// 代码2
int d = -1;
if (a = b + 1, c = a / 2, d > 0)
{
printf("if\n");
}
else
{
printf("else\n");
}
// 输出什么?
// 代码3
a = get_val();
count_val(a);
while (a > 0)
{
// 业务代码
get_val();
count_val(a);
}
// 上面代码能改成
while (a = get_val(), count_val(a), a > 0)
{
// 业务代码
}
return 0;
}
- 代码1:此代码是用逗号表达式对c进行赋值操作,将逗号表达式从左到右依次执行,且此逗号表达式的结果为最后一个表达式b = a + 1,此时b == 13,所以c被赋值为13
- d > 0这个关系表达式的值为整个逗号表达式的值,d == -3,d > 0 为假,所以输出else
十、下标引用、函数调用和结构成员
下标引用操作符
[]
有两个操作数 - - 一个数组名 + 一个索引值,
例如:
int a[10] = { 0 };
a[0] = 1; // 使用下标引用操作符
// a 和 0 是操作数
// 实际上等价于 *(a + 0) -- 指针的使用,后续章节会详细介绍
函数调用
()
- - 接受一个或多个操作数:第一个是函数名,剩余的操作数就是传递给函数的参数
例如:
#include <stdio.h>
void test1()
{
printf("test1\n");
}
void test2(const char* str)
{
printf("test2:");
printf("%s\n", str);
}
int main()
{
test1(); // 使用了()操作符,只有一个操作数 -- 函数名test1
test2("This is test2"); // 使用了()操作符,有两个操作数 -- 函数名test1,字符串参数
return 0;
}
结构体成员访问操作符
.
和->
- - 都有两个操作数
.
- - 使用示例:结构体.成员名
->
- - 使用示例:结构体指针->成员名
例如:
#include <stdio.h>
typedef struct
{
int age;
char name[20];
char sex[5];
} Stu;
int main()
{
Stu stu = { 30, { 0 }, { 0 } };
Stu* pstu = &stu;
// .操作符
stu.age = 18;
printf("age == %d\n", stu.age);
// ->操作符
pstu->age = 20;
printf("age == %d\n", stu.age);
return 0;
}
运行结果:
十一、表达式求值
表达式的求值的顺序一部分是由操作符的优先级和结合性来决定的。
同样,有些表达式的操作数在求值过程中可能需要转换为其他类型。
1.隐式类型转换
c的整型算术运算总是至少以缺省的整数类型的精度来进行的 - - 求值数在内存中的基础长度为4字节,短了需要补足。
为了获得这个精度,表达式中的字符和短整型操作数要在使用之前转换为普通整型,这种转换称为整型提升
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件中执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使是两个char类型的数相加,在CPU中执行的时,也需要先将其转换为CPU内的整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以实现8bit直接相加的运算的。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送往CPU去执行运算。
例如:
char a, b, c;
...
a = b + c;
// b 和 c 的值被提升为普通整型,然后执行加法运算,加法运算完成后,对结果进行截断,然后再存储到a中。
如何进行整型提升呢?
整型提升是基于变量的数据类型的符号位来提升的
// 负数的整型提升
char a = -1;
// 变量a的二进制位(补码)只有8个bit位:
// 11111111
// char是有符号的 char
// a 的符号位为1,在整型提升时,高位补充符号位,即1
// 提升后的结果为:
// 11111111111111111111111111111111
// 正数的整型提升
char b = 1;
// 变量b的二进制位(补码)只有8个bit位:
// 00000001
// char是有符号的 char
// a 的符号位为0,在整型提升时,高位补充符号位,即0
// 提升后的结果为:
// 00000000000000000000000000000001
// 如果是无符号数的整型提升,高位直接补 0
整型实例1:下面代码会输出什么?
#include <stdio.h>
int main()
{
char a = 0x80;
short b = 0x8000;
int c = 0x800000;
if (a == 0x80)
{
printf("a\n");
}
if (b == 0x8000)
{
printf("b\n");
}
if (c == 0x800000)
{
printf("c\n");
}
return 0;
}
运行结果:
可以看到仅输出了一个c,说明在判断中a != 0x80
,b != 0x8000
,
这是因为,在进行a == 0x80
和b == 0x8000
时,a
和b
的类型长度小于4,需要进行整型提升,且它们的符号位为1,提升是补1,实际上a == 0x80
是11111111111111111111111110000000 == 00000000000000000000000010000000
为假,b == 0x8000
是11111111111111111000000000000000 == 00000000000000001000000000000000
为假,只有c == 0x800000
为真。
整型实例2:下面代码会输出什么?
#include <stdio.h>
int main()
{
char c = 1;
printf("c == %u\n", sizeof(c));
printf("-c == %u\n", sizeof(-c));
printf("+c == %u\n", sizeof(+c));
return 0;
}
运行结果:
c只要参与表达式运算,就需要整型提升,表达式+c
和-c
都参与了表达式运算,就都发生了提升,所以输出为4 - - int类型的大小,sizeof©中的c没有进行整型提升,所以输出1。
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后再执行运算。
算术转换要合理,否则会有一些潜在的问题
float f = 3.14f;
int num = f; // 隐式转换,会有精度丢失。
3.操作符属性
复杂表达式的求值有三个影响因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行哪个?取决于它们的优先级,优先级相同,则取决于它们的结合性。
操作符 | 优先级 | 描述 | 用法示例 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 1 | 聚组 | (表达式) | 与表达式相同 | N/A |
() | 1 | 函数调用 | 函数名(参数, …, 参数) | L-R | 否 |
[] | 1 | 下标引用 | 数组名[下标] | L-R | 否 |
. | 1 | 访问结构成员 | 结构体.成员名 | L-R | 否 |
-> | 1 | 访问结构指针成员 | 结构体指针->成员名 | L_R | 否 |
++ | 2 | 后置自增 | 表达式++ | L-R | 否 |
-- | 2 | 后置自减 | 表达式- - | L-R | 否 |
! | 2 | 逻辑反 | !表达式 | R-L | 否 |
~ | 2 | 按位取反 | ~表达式 | R-L | 否 |
+ | 2 | 单目,表示正值 | +表达式 | R-L | 否 |
- | 2 | 单目,表示负值 | -表达式 | R-L | 否 |
++ | 2 | 前置自增 | ++表达式 | R-L | 否 |
-- | 2 | 前置自减 | - -表达式 | R-L | 否 |
* | 2 | 间接访问 | *指针变量 | R-L | 否 |
& | 2 | 取地址 | &变量名 | R-L | 否 |
sizeof | 2 | 取其长度,以字节为单位 | sizeof(表达式) | R-L | 否 |
(类型) | 2 | 强制类型转换 | (类型)表达式 | R-L | 否 |
* | 3 | 乘法 | 表达式 * 表达式 | L-R | 否 |
/ | 3 | 除法 | 表达式 / 表达式 | L-R | 否 |
% | 3 | 整数取余 | 表达式 % 表达式 | L-R | 否 |
+ | 4 | 加法 | 表达式 + 表达式 | L-R | 否 |
- | 4 | 减法 | 表达式 - 表达式 | L-R | 否 |
<< | 5 | 左移 | 变量 << 表达式 | L-R | 否 |
>> | 5 | 右移 | 变量 >> 表达式 | L-R | 否 |
> | 6 | 大于 | 表达式 > 表达式 | L-R | 否 |
>= | 6 | 大于等于 | 表达式 >= 表达式 | L-R | 否 |
< | 6 | 小于 | 表达式 < 表达式 | L-R | 否 |
<= | 6 | 小于等于 | 表达式 <= 表达式 | L-R | 否 |
== | 7 | 等于 | 表达式 == 表达式 | L-R | 否 |
!= | 7 | 不等于 | 表达式 != 表达式 | L-R | 否 |
& | 8 | 按位与 | 表达式 & 表达式 | L-R | 否 |
| | 9 | 按位或 | 表达式 | 表达式 | L-R | 否 |
^ | 10 | 按位异或 | 表达式 ^ 表达式 | L-R | 否 |
&& | 11 | 逻辑与 | 表达式 && 表达式 | L-R | 是 |
|| | 12 | 逻辑或 | 表达式 || 表达式 | L-R | 是 |
?: | 13 | 条件操作符 | 表达式 ? 表达式 : 表达式 | N/A | 是 |
= | 14 | 赋值 | 变量 = 表达式 | R-L | 否 |
+= | 14 | 加后赋值 | 变量 += 表达式 | R-L | 否 |
-= | 14 | 减后赋值 | 变量 -= 表达式 | R-L | 否 |
*= | 14 | 乘后赋值 | 变量 *= 表达式 | R-L | 否 |
/= | 14 | 除后赋值 | 变量 /= 表达式 | R-L | 否 |
%= | 14 | 取模后赋值 | 变量 %= 表达式 | R-L | 否 |
<<= | 14 | 左移后赋值 | 变量 <<= 表达式 | R-L | 否 |
>>= | 14 | 右移后赋值 | 变量 >>= 表达式 | R-L | 否 |
&= | 14 | 按位与后赋值 | 变量 &= 表达式 | R-L | 否 |
|= | 14 | 按位或后赋值 | 变量 |= 表达式 | R-L | 否 |
^= | 14 | 按位异或后赋值 | 变量 ^= 表达式 | R-L | 否 |
, | 15 | 逗号表达式 | 表达式, 表达式, …, 表达式 | R-L | 否 |
一些问题表达式
表达式1
// 表达式的求值部分由操作符的优先级决定
// 表达式1
a * b + c * d + e * f
// * 的优先级比 + 高,只能保证第一个 * 比 + 更早运算,
// 但无法保证e * f 比a * b + c * d 更早运算。
// 即可能出现
// a.
// (1).(a * b)
// (2).(c * d)
// (3).(e * f)
// (4).(1) + (2)
// (5).(4) + (3)
// b.
// (1).(a * b)
// (2).(c * d)
// (3).(1) + (2)
// (4).(e * f)
// (5).(3) + (4)
表达式2
// 表达式2
c + -- c;
// 同样的从优先级中只能获知,--比 + 更早运算,
// 但左边的c是在--c之前获取的,还是在其之后获取的这并不确定,该代码同样存在歧义。
// 假设 c = 2
// 上述代码可能为:
// 1. 2 + --2; // --c执行之前,前面的c就获取值
// 2. 1 + --2; // --c执行之后,前面的c才获取值
表达式3
// 表达式3
#include <stdio.h>
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i == %d\n", i);
return 0;
}
这是一个很典型的错误代码,在不同的编译器中的结果都不相同,如果,那个程序员写了这样的代码,一定会被人骂"猪队友"。
表达式4
// 代码4
#include <stdio.h>
int func()
{
static int count = 1;
return count++;
}
int main()
{
int answer;
answer = func() - func() * func();
printf("answer == %d\n", answer);
return 0;
}
这个代码有问题吗?
有问题!
虽然表达式中的 - 和 * 可以通过优先级来决定谁先计算,但func()的计算顺序是依赖于编译器的,有可能先计算第一个func(),再计算第二个func(),最后计算第三个func(),这样的结果为 1 - 2 * 3
;
也可能先计算第二个和第三个,最后计算第一个,这两种的计算结果是不同。
所以这样的代码也是不可靠的。
表达式5
#include <stdio.h>
int main()
{
int i = 1;
int ret = 0;
ret = (++i) + (++i) + (++i);
printf("ret == %d\n", ret);
printf("i == %d\n", i);
return 0;
}
这个代码在不同的编译器中的执行结果也是不同的,无法确认表达式中的计算顺序,
比如,上述代码可能出现以下几种计算顺序:
- i先自增3次,此时i == 4,之后再进行 + 操作,这样得出的结果为12
- 先执行前两个++i,此时i == 3,再执行 + ,表达式转化为 6 + (++i),此时再次执行++i,i == 4,最后执行6 + 4,这样得到的结果为10。
总结
我们写出的表达式的执行顺序,无法通过操作符的属性来唯一确认的话,那么这个表达式就是个问题表达式。
总结
本文对c语言中会使用到的操作符进行较为详细的介绍说明,特别是对移位操作符中的右移操作的两种情况进行了分析,详细说明了sizeof的使用以及使用时可能遇到的问题,逻辑操作符运算过程中可能出现的反直觉问题,表达式求值中整型提升、算术转换、操作符的优先级,并列出、分析了一些问题表达式。