算术操作符
+ - * / %
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
例如:
//整数除法
5 / 2 => 2
//浮点数除法
5 / 2.0 => 2.5
5.0 / 2 => 2.5
5.0 / 2.0 => 2.5
//取余操作
5 % 2 => 1
5.0 % 2 //err
移位操作符
移位操作符移动的是二进制数。
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
整型分为有符号数和无符号数:
- 有符号数的二进制
- 符号位(最高位)为0,表示正数。
- 符号位(最高位)为1,表示负数。
- 无符号数的二进制
- 每一位都有其权值,无正负之分。
有符号数二进制的具体表示方法:
-
采用原码、反码、补码表示。
-
正整数的原码、补码、反码都相同。
-
例如:
//15 - 整数,在c语言可以存放到int类型变量中,int类型是4个字节,32bit
00000000000000000000000000001111 //原码
00000000000000000000000000001111 //反码
00000000000000000000000000001111 //补码
- 负整数则有相应的转化规则。
- 原码 -> 反码:原符号位不变,其它位按位取反
- 反码 -> 补码:反码+1
- 例如:
//-15
00000000000000000000000000001111 //原码
00000000000000000000000000010000 //反码
00000000000000000000000000010001 //补码
注:整数在内存中存储的是二进制的补码。 因此当我们使用移位操作符操作整数时,移动的是其存储在内存中的补码。
左移操作符
移位规则:
左边抛弃、右边补0
从图中可以看出,每次左移1位,就相当于将该数乘以2。
右移操作符
移位规则:
首先右移运算分两种:
- 逻辑移位:左边用0填充,右边丢弃
- 算术移位:左边用原该值的符号位填充,右边丢弃
警告⚠ :
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num>>-1;//error
位操作符
位操作符有:
& //按位与(全1为1,其余为0)
| //按位或(全0为0,其余为1)
^ //按位异或(相同为0,相异为1)
注:他们的操作数必须是整数。
练习一下:
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
num1 & num2;
//0001 - 1
//0010 - 2
//0000 - 0 -> 结果
num1 | num2;
//0001 - 1
//0010 - 2
//0011 - 3 -> 结果
num1 ^ num2;
//0001 - 1
//0010 - 2
//0011 - 3 -> 结果
return 0;
}
一道面试题:
不能创建临时变量(第三个变量),实现两个数的交换。
- 异或满足交换律:(a ^ b) ^ b = a ^ (b ^ b)
- 任意数异或本身恒为0:a ^ a = 0,b ^ b = 0
- 任何数异或0恒为本身:a ^ 0 = a,b ^ 1 = b
- 所以我们可以利用这三个原则进行下列操作实现两个数的交换。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b; // b = (a^b)^b = a
a = a^b; // a = a^(a^b) = b
printf("a = %d b = %d\n", a, b);
return 0;
}
练习:
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
参考代码:
//方法1
//利用分解每一位的方法遍历二进制中1的个数
#include <stdio.h>
int main()
{
int num = 10;
int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
//思考这样的实现方式有没有问题?
//方法2
//num = -1
//1111 1111 1111 1111 1111 1111 1111 1111
//0000 0000 0000 0000 0000 0000 0000 0001 1<<0
//0000 0000 0000 0000 0000 0000 0000 0010 1<<1
//...
//1000 0000 0000 0000 0000 0000 0000 0000 1<<31
//通过将1左移i位(0~31)遍历相与,如果为1,则说明num该位为1。计数累加即可得到1的个数。
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//思考还能不能更加优化,这里必须循环32次的。
//方法3
//num = -1
//第一轮:
//1111 1111 1111 1111 1111 1111 1111 1111
//1111 1111 1111 1111 1111 1111 1111 1110 ->num-1
//1111 1111 1111 1111 1111 1111 1111 1110 ->num&(num-1)
//第二轮:
//num = num&(num-1)
//1111 1111 1111 1111 1111 1111 1111 1110
//1111 1111 1111 1111 1111 1111 1111 1101 ->num-1
//1111 1111 1111 1111 1111 1111 1111 1100 ->num&(num-1)
//我们可以看出每轮num&(num-1)都会失去一个位上的1,循环操作即可完成1的统计。直到num等于0为止。
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//这种方式是不是很好?达到了优化的效果,但是难以想到。
赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
int weight = 120; //体重
weight = 89; //不满意就赋值
double salary = 10000.0;
salary = 20000.0; //使用赋值操作符赋值。
//赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
//连续赋值
a = x = y + 1;
//那同样的语义:
x = y + 1; a = x;
//这样的写法是不是更加清晰爽朗而且易于调试。
复合赋值符
这些运算符都可以写成复合的效果。
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
比如:
int x = 10;
x = x * 2;
x *= 2; //复合赋值
//其他运算符一样的道理。这样写更加简洁。
int a = 10;
a = a >> 1; //a >>= 1;
单目操作符
单目操作符介绍
! //逻辑反操作
- //负值
+ //正值
& //取地址
sizeof //操作数的类型长度(以字节为单位)
~ //对一个数的二进制按位取反
-- //前置、后置--
++ //前置、后置++
* //间接访问操作符(解引用操作符)
(类型) //强制类型转换
详解举例
! 逻辑反操作
逻辑反操作符就是把 真变成假,假变成真。把flag变成真,不会是别的数字,只会是1。
int flag = 0;
//当flag为假的时候如何打印admire
if (!flag) //只需用上逻辑反操作符!就能打印了
{
printf("admire\n");
}
+正值、-负值
int a = +3; // +可以省略
int b = -1;
&取地址、 * 解引用操作符
&操作符可以取出该变量的地址(变量 -> 地址),如果想要得到该变量的值,即需要*解引用操作符(地址 -> 变量)。
int a = 10;
char c = 0;
printf("%p\n", &a);
printf("%p\n", &c);
int* pa = &a; //& - 取地址操作符
*pa = 20; //* - 解引用操作符
int arr[10];
&arr; //取出数组的地址
~ 按(内存中补码的2进制)位取反
int a=0;
printf("%d\n",~a);
// 0000 0000 0000 0000 0000 0000 0000 0000 - 因为0是正数,原反补相同
// 1111 1111 1111 1111 1111 1111 1111 1111 - 把0的补码每一位进行取反
// 又因为要打印出来,得把补码转换成原码
// 1111 1111 1111 1111 1111 1111 1111 1111 - 补码
// 1111 1111 1111 1111 1111 1111 1111 1110 - 反码
// 1000 0000 0000 0000 0000 0000 0000 0001 - 原码
// 打印出来的结果为 -1
- - 前置、后置减减
- 前置- -,先- -,再使用。
- 后置–,先使用,再- -。
++ 前置、后置加加
- 前置++,先++,再使用。
- 后置++,先使用,再++。
//++ 前置
int a = 10;
int b = ++a; //前置++,先进行++,再赋值。
printf("a=%d b=%d\n",a,b); // a的结果为11 , b的结果也为11
//后置 ++
int a = 10;
int b = a++; //前置++,先进行++,再赋值。
printf("a=%d b=%d\n",a,b); // a的结果为11 , b的结果也为10
//-- 类似
(类型)强制类型转换
int a = 1;
short b = (int)a; //注意二进制位数的问题
*(int*)0x0012ff40 = 100; //注意野指针问题
sizeof操作数的类型长度
关于sizeof其实我们之前已经见过了,可以求变量(类型)所占空间的大小。
举例:下面代码输出结果是什么呢?
输出的格式是长度型(size_t)的无符号整型值。其中size_t 在不同的位数操作系统中定义是不同的,因此在程序设计的时候,如果参数是 size_t 或者 ssize_t,在进行格式化输入或输出的时候务必使用 ‘z’ 修饰符,以防止显示异常。
/* 32位 */
typedef unsigned int size_t;
typedef int ssite_t;
/* 64位 */
typedef unsigned long size_t;
typedef long ssize_t;
int main(){
short s = 10;
int a = 2;
printf("%zu\n",sizeof(s = a + 5));
printf("%zu\n",s);
}
- 由上述结果我们可以看出,sizeof()内部表达式不参与真实运算,编译时计算运算值。
- 即使sizeof内部写有赋值表达式,也不会去实际操作,sizeof在计算时只看操作数的类型,不会访问相应的空间。
sizeof和数组
举例代码:
#include <stdio.h>
void test1(int arr[])
{
printf("%zu\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%zu\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%zu\n", sizeof(arr));//(1)
printf("%zu\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
//(1)、(2)两个地方分别输出多少?
//(3)、(4)两个地方分别输出多少?
结果:
从结果中我们可以看出:
-
sizeof(数组名),数组表示的整个数组,不是首元素的地址,sizeof(数组名)计算的是整个数组的大小,单位是字节。
-
而函数传参输入的数组名则是首元素地址,所以在函数内部使用传参过去的sizeof(数组名),则是计算的首元素地址的大小,也就是一个指针所占的字节数。
关系操作符
这些关系操作符比较简单,与数学上的判断类似。
>
>=
<
<=
!= //用于测试“不相等”
== //用于测试“相等”
举例:
int main()
{
char arr[] = "abcdef";
//==不能比较2个字符串的内容,实际上比较的是2个字符串的首字符的地址
if (arr == "abcdef")
{
printf("==\n");
}
return 0;
}
注意:
- 在编程的过程中== 和=不小心写错,导致的错误。
逻辑操作符
逻辑上的与、或只需要判断左右两边表达式结果的真假即可(0为假,其余为真)得到的结果只有1(真)和0(假)。按位与、或则需要将两边数据的二进制每位进行计算得出结果。
&& //逻辑与
|| //逻辑或
区分逻辑与、或和按位与、或
//按位与
//01 - 1
//10 - 2
//00 - 0
1&2----->0
//逻辑与
1&&2---->1
//按位或
//01 - 1
//10 - 2
//11 - 3
1|2----->3
//逻辑或
1||2---->1
逻辑与、或举例:
int age = 20;
//18~36 青年
if (age >= 18 && age <= 36)
{
printf("青年\n");
}
int year = 2000;
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){}
- 逻辑逻辑与、或通常可以用在if()表达式的判断语句中,来构成所需要的逻辑。
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;
}
//程序输出的结果是什么?
我们一步步分析:
- 首先a++,先使用a = 0的值。
- 当到这一步时,我们发现结果里面b和d并没有进行自增的计算,所以&&操作符到这里就结束了。
- 这是为什么呢?其实是因为当a等于0时,0&&任何数都等于0,所以逻辑操作符直接发生截断,不计算后面表达式的值。
逻辑或||:
#include <stdio.h>
int main()
{
int i = 0,a=1,b=2,c=3,d=4;
i = a++ || ++b || d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
- 同理:当a==1时,a||任何数结果都为1,所以后面也不再进行计算。最后只有a进行了自增,a = 2。其它数都没改变。
总结
- 逻辑操作符的计算,有截断效应,当能够明确判断出结果时,就不再计算后续的表达式。
条件操作符
三目操作符:exp1列出判断语句,exp2 : exp3 分别为为真和为假时的返回值。
exp1 ? exp2 : exp3
练习:
转换成条件表达式,是什么样?
int b;
if (a > 5)
b = 3;
else
b = -3;
//逗号表达式
b = (a > 5) ? 3 : -3;
使用条件表达式实现找两个数中较大值
int max = a > b ? a : b;
逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
exp1, exp2, exp3, …expN
举例:
代码1
int a = 1;
int b = 2;
//逗号表达式
int c = (a>b, a=b+10, a, b=a+1);
//c是多少?
- 从左到右依次计算:a>b == 1>2 -> 0; a = b + 10 -> a = 2 +10 =12; a -> 12; b = a+1 -> b = 12 + 1 = 13;
- 从上述过程的结果为最后一个表达式的结果:13
代码2
if (a=b+1, c=a/2, d>0){}
//等价
a = b + 1;
c = a / 2;
if(d>0){}
- 从左到右依次计算:a = b + 1 ; c = a / 2 ; d > 0;
- 而b>0则为表达式的结果,因此该i的判断如f(d>0)。
代码3
a = get_val();
count_val(a);
while (a > 0) {
//业务处理
a = get_val();
count_val(a);
}
//如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0) {
//业务处理
}
- 在不影响循环判断的情况下,利用逗号表达的特质我们可以结合一些表达式。
下标引用、函数调用和结构成员
[ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
int arr[10]; //创建数组
arr[9] = 10; //实用下标引用操作符。
//[ ]的两个操作数是arr和9。
( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
void test1()
{
printf("hello world\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //实用()作为函数调用操作符。
test2("hello everyone"); //实用()作为函数调用操作符。
return 0;
}
访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu) {
stu.age = 18;
}
void set_age2(struct Stu* pStu) {
pStu->age = 18;//结构成员访问
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 20;//结构成员访问
set_age1(stu);
pStu->age = 20;//结构成员访问
set_age2(pStu);
return 0;
}
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
//实例1
//b和c的值被提升为普通整型,然后再执行加法运算。
//加法运算完成之后,结果将被截断,然后再存储于a中。
char a,b,c;
...
a = b + c;
整型提升实现
整形提升是按照变量的数据类型的符号位来提升的。
//负数的整形提升
char c1 = -1;
//变量c1的二进制位(补码)中只有8个bit位:
1111111
//因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为1
//提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
//变量c2的二进制位(补码)中只有8个比特位:
00000001
//因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为0
//提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
举例:
//实例1
int main()
{
char a = 0xb6; // 1011 0110
short b = 0xb600; // 1011 0110 0000 0000
int c = 0xb6000000; // 1011 0110 0000 0000 0000 0000 0000 0000
if(a==0xb6)
// 1111 1111 1111 1111 1111 1111 1011 0110 -> a
// 0000 0000 0000 0000 0000 0000 1011 0110 -> 0xb6
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
- 实例1中的a,b要进行整形提升,但是c不需要整形提升。
- a,b整形提升之后,变成了负数(补最高位符号位的1),所以表达式 a == 0xb6 ,b==0xb600 的结果是假。
- 但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。
- 所以程序只会输出:c
//实例2
int main()
{
char c = 1;
printf("%zu\n", sizeof(c)); // 1
printf("%zu\n", sizeof(+c)); // 4
printf("%zu\n", sizeof(-c)); // 4
return 0;
}
- 实例2中c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节。
- 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof( c ),就是1个字节。
- 因此隐式转换规则也适用于sizeof()内部,所以你必须记住整型提升规则,以免发生一些整型溢出的问题。
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
- 如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
- 例如:当无符号数和有符号数比较时,会将有符号数参数强制类型为无符号数,并假设这两个数都是非负的,来执行这个运算。
警告:
- 但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f; //隐式转换,会有精度丢失
操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。
如果两者的优先级相同,取决于他们的结合性。
操作符优先级
优先级 | 操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式同 | N/A | 否 | |
() | 函数调用 | rexp(rexp,…,rexp) | rexp | L-R | 否 | |
1 | [ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
初等运算符 | . | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 | |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 | |
– | 后缀自减 | lexp – | rexp | L-R | 否 | |
! | 逻辑反 | ! rexp | rexp | R-L | 否 | |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 | |
- | 单目,表示正值 | - rexp | rexp | R-L | 否 | |
2 | + | 单目,表示负值 | + rexp | rexp | R-L | 否 |
单目运算符 | ++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
– | 前缀自减 | – lexp | rexp | R-L | 否 | |
* | 间接访问 | * rexp | lexp | R-L | 否 | |
& | 取地址 | & lexp | rexp | R-L | 否 | |
sizeof | 取其长度,字节表示 | sizeof rexp sizeof(类型) | rexp | R-L | 否 | |
(类型) | 类型转换 | (类型) rexp | rexp | R-L | 否 | |
* | 乘法 | rexp * rexp | rexp | L-R | 否 | |
3 | / | 除法 | rexp / rexp | rexp | L-R | 否 |
双目运算符(算术运算符) | % | 整数取余 | rexp % rexp | rexp | L-R | 否 |
+ | 加法 | rexp + rexp | rexp | L-R | 否 | |
- | 减法 | rexp - rexp | rexp | L-R | 否 | |
4 | << | 左移位 | rexp << rexp | rexp | L-R | 否 |
双目运算符(移位运算符) | >> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
> | 大于 | rexp > rexp | rexp | L-R | 否 | |
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 | |
5 | < | 小于 | rexp < rexp | rexp | L-R | 否 |
双目运算符(关系运算符) | <= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
== | 等于 | rexp == rexp | rexp | L-R | 否 | |
!= | 不等于 | rexp != rexp | rexp | L-R | 否 | |
6 | & | 位与 | rexp & rexp | rexp | L-R | 否 |
双目运算符(位操作运算符) | ^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
| | 位或 | rexp | rexp | rexp | L-R | 否 | |
7 | && | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
双目运算符(逻辑运算操作符) | || | 逻辑或 | rexp || rexp | rexp | L-R | 是 |
8 | ? : | 条件操作符(三目运算符) | rexp ? rexp : rexp | rexp | N/A | 是 |
= | 赋值 | rexp = rexp | rexp | R-L | 否 | |
+= | 以…加 | rexp += rexp | rexp | R-L | 否 | |
-= | 以…减 | rexp -= rexp | rexp | R-L | 否 | |
*= | 以…乘 | rexp *= rexp | rexp | R-L | 否 | |
9 | /= | 以…除 | rexp /= rexp | rexp | R-L | 否 |
赋值运算符 | %= | 以…取模 | rexp %= rexp | rexp | R-L | 否 |
<<= | 以…左移 | rexp <<= rexp | rexp | R-L | 否 | |
>>= | 以…右移 | rexp >>= rexp | rexp | R-L | 否 | |
&= | 以…与 | rexp &= rexp | rexp | R-L | 否 | |
^= | 以…异或 | rexp ^= rexp | rexp | R-L | 否 | |
|= | 以…或 | rexp |= rexp | rexp | R-L | 否 | |
10 | , | 逗号运算符 | rexp , rexp | rexp | L-R | 是 |
记忆图解:
问题表达式
代码1
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
- 代码1在计算的时候,由于* 比 +的优先级高,只能保证,* 的计算是比+早。
- 但是优先级并不能决定第三个*比第一个+早执行。
- 所以表达式的计算机顺序就可能是
//第一种
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
//第二种
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
代码2
//c = 2
c + --c;
//2 + 1
//1 + 1
- 同上,操作符的优先级只能决定自减–的运算在+的运算的前面。
- 但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
代码3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
- 表达式3在不同编译器中测试结果:非法表达式程序的结果
值 | 编译器 |
---|---|
-128 | Tandy 6000 Xenix 3.2 |
-95 | Think C 5.02(Macintosh) |
-86 | IBM PowerPC AIX 3.2.5 |
-85 | Sun Sparc cc(K&C编译器) |
-63 | gcc,HP_UX 9.0,Power C 2.0.0 |
4 | Sun Sparc acc(K&C编译器) |
21 | Turbo C/C++ 4.5 |
22 | FreeBSD 2.1 R |
30 | Dec Alpha OSF1 2.0 |
36 | Dec VAX/VMS |
42 | Microsoft C 5.1 |
代码4
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
// 从左到右调用:2-3*4 -> -10
// 先调用进行乘法运算的函数,然后再是加法: 4-2*3 -> -2
printf( "%d\n", answer);
return 0;
}
- 代码answer = fun() - fun() * fun();虽然在大多数的编译器上求得结果都是相同的。
- 由操作符优先级可知:先算乘法,再算减法。
- 但函数的调用顺序无法通过操作符的优先级决定。
代码5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在linux环境gcc编译器,VS2019环境下都执行,看结果。
- 这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
Linux环境的结果:
- 根据输出结果可知:3 + 3 + 4 = 10
- 顺序:先计算第一个(++i),再计算第二个(++i),然后将它们相加(++i) + (++i),最后再计算第三个(++i),再相加即可。
VS2019环境的结果:
- 根据输出结果可知:4 + 4 + 4 = 12
- 顺序:先将所有(++i)计算完成,最后再进行相加即可。
总结:
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。