本章重点
- 各种操作符的介绍。
- 表达式求值
文章目录
一、操作符
1.算术操作符
+ - * / %(取余)
对于 /
除法操作符:
int ret1 = 5 / 2; //2
float ret2 = 5 / 2; //2.000000
float ret3 = 5 / 2.0;//2.500000
float ret4 = 5.0 / 2;//2.500000
在计算机中,不是存放在什么样的类型变量,
5 / 2
都是2。
若要得到正确的结果,可改成5 / 2.0
或5.0 / 2
,当然5.0 / 2.0
也可以。
对于 %
取余操作符:
含%
表达式必须包含整型。
总结:
- 除了
%
操作符之外,其他的几个操作符可以作用于整数和浮点数。- 对于
/
操作符若两个操作数都为整数,执行整数除法,只要有浮点数就执行浮点数除法。%
操作符的两个操作数必须为整数,返回的是整除之后的余数,且结果范围是[0,除数-1]。
2.移位操作
2.1.整数存储规则
在介绍移位操作之前,需要了解整数在内存中的存储规则。
整数在内存中的存储:以二进制补码的形式存储
正整数:原码、反码、补码相同
负整数:
- 原码:按负整数写出二进制序列
- 反码:符号位(左边第一位,1表示负,0表示正)不变,其他位按位取反
- 补码:反码+1
(实例看下文)
2.2.左右移位规则
移位操作符操作对象是数的二进制。
<< //左移操作符
>> //右移操作符
a.左移操作符
左边舍弃,右边补0
int a = 5;
int b = a << 1;
a << 1
意为a的补码左移1位,此时a的值仍为5,类似b = a + 1;
a的值不改变。
int c = -1;
int d = c << 1;
思路:写出-1的原码,反码,补码,对补码进行左移操作,将操作得到的补码按相同规则换算成原码,得到对应的整数-2。
10000000 00000000 00000000 00000001 一 -1的原码
11111111 11111111 11111111 11111110 一 -1的反码
11111111 11111111 11111111 11111111 一 -1的补码
11111111 11111111 11111111 11111110 一 -1<<1的补码
11111111 11111111 11111111 11111101 一 -1<<1的反码
10000000 00000000 00000000 00000010 一 -1<<1的原码,即-2
b.右移操作符
右移规则分为两种:
①逻辑右移:左边补0,右边舍弃。
②算术右移:左边补符号位,右边舍弃。
绝大部分编译器采用算术右移。
int a = -1;
int b = a >> 1;//仍是-1
10000000 00000000 00000000 00000001 一 -1的原码
11111111 11111111 11111111 11111110 一 -1的反码
11111111 11111111 11111111 11111111 一 -1的补码
11111111 11111111 11111111 11111111 一 -1>>1的补码,即-1
总结:
规律:左移,数据
×2
(变大);右移,数据÷2
(变小)
左移右移操作数必须是整数。
移位操作数不可移动负数,如1 >> -1
,标准未定义行为。
3.位操作符
& //按位与
| //按位或
^ //按位异或
注: 他们的操作数必须是整数。
运算规则: 操作的是整数的补码
按位与
&
: 全1则1;有0则0
按位或|
: 有1则1;全0则0
按位异或^
:相同为0;相异为1
int a = 3;
int b = 5;
int c = a & b;//c=1
//1.求a的补码
00000000 00000000 00000000 00000011 一 3的原反补码相同
//2.求b的补码
00000000 00000000 00000000 00000101 一 5的原反补码相同
//3.求c = a & b
00000000 00000000 00000000 00000001 一 c的补码
//4.转化为原码
00000000 00000000 00000000 00000001 一 c的原反补相同,即为1
计算步骤:
- 求两操作数的补码
- 根据按位与、或、异或的运算规则得到结果(补码)
- 将所得补码转化为原码,得到整数
练习一下:
不能创建临时变量(第三个变量),实现两个数的交换。
加减法:灵感:
3 = 3 + 5 - 3 ;
缺点: 当a和b都无限接近int整型变量的范围,两者相加必然会超出范围,导致溢出。
int main()
{
int a = 10;
int b = 20;
printf("a=%d b=%d\n", a, b);
a = a + b;
b = a - b;//(a+b)-b = a,即b = a
a = a - b;//a - (a-b) = b,即a=b
printf("a=%d b=%d\n", a, b);
return 0;
}
运行结果:
按位异或法:运算规则知 :对于两个数的补码的每一位,相同为0;相异为1。
细品一下,若相同两数异或,结果则为0,(即a ^ a = 0
);任何数和0异或,结果则为任何数本身,(即a ^ 0 = a
)。
缺点: 可读性差,只支持正数。
int main()
{
int a = 10;
int b = 20;
printf("a = %d b = %d\n", a, b);
a = a ^ b;
b = a ^ b;//(a ^ b) ^ b = a,即b = a
a = a ^ b;//(a ^ b) ^ a = b,即a = b
printf("a = %d b = %d\n", a, b);
return 0;
}
运行结果:
1.若要a和b交换数值,方法:
a ^ b
,a ^ b
^ b意为a的值赋给b;a ^ b
^ a意为b的值赋给a。
2.a ^ a = 0
a ^ 0 = a
3.(a ^ b) ^ b = a,可见异或支持交换律。
4.赋值操作符
赋值操作符 =
:给变量赋值。
可对一个变量多次赋值,以最后一次为准:
double salary = 10000.0;
salary = 50000.0;//不满意,再赋值
可连续赋值:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值,这样的代码风格不推荐
相同的语义,下面的代码更爽朗且易于调试
x = y+1;
a = x;
①y + 1的值赋给x ②x = y + 1的值赋给a
复合赋值操作符:
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
拿 +=
来举例:其他运算符一样的道理,这样写更加简洁。
x += 10;//复合赋值
其意义如下:
int x = 10;
x = x + 10;
5.单目操作符
单目:只有一个操作数
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
逻辑反操作:
0为假,非0为真,!0 = 1
取地址操作符 &
和解引用操作符 *
:
int a = 10;
int* p = &a;//int* p创建指针变量p(存放地址),&a:取变量a的地址//整体:取a地址放进p中
*p = 20;//解引用访问其存储的地址中的内容
printf("%d", a);//打印20
return 0;
类型长度操作符 sizeof
:
sizeof
:计算变量或类型所占内存空间的大小,与其变量中所存数据是什么无关。
int arr[4] = {
0 };
printf("%d\n", sizeof arr);//16
printf("%d\n", sizeof (arr));//16
sizeof
后面的()
是表达式的括号,而不是函数调用的操作符。sizeof
是操作符,后面的()可省略。
int a = 2;
short b = 0;
printf("%d\n", sizeof(b = a + 1));//2
printf("%d\n", b);//0
1.
sizeof
括号中有两个变量类型,打印左边变量类型的大小。解释:int
类型的a + 1
赋值给short
类型的b
会发生整型截断,还是short
类型的数据。
2.()
中的表达式不参与运算,所以打印出的b显示仍为0。
sizeof
和 strlen()
的区别:
sizeof
:计算变量或类型所占内存空间的大小,与其变量所存数据无关。strlen()
:计算字符串长度的函数,返回存放的数据中\0
前的字符个数。
sizeof
和数组:
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = {
0 };
char ch[10] = {
0 };
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
(1)、(3)都是数组名放在
sizeof()
里,计算的是整个数组的大小,分别是40和10。
(2)、(4)都是数组名作函数参数,传过去的是数组首元素地址,也就是第一个元素的地址,表面上函数是用数组接收,实际上编译器会自动将数组降级优化为指针,用于接收地址,因此计算的是指针的大小,都为4。
++
--
操作符:
int a = 10;
int x = a++;
//后置++:先使用a,后对a自增,所以`x = 10,a = 11`
int a = 10;
int x = ++a;
//前置++:先对a自增,后使用a,所以`a = 11,x = 11`
++
--
懂得以上用法即可,不要弄无用且负责的形式,如下:
int a = 0;
int b = (++a) + (a++) + (a++);
//不同的编译器运行结果不同,这样的代码纯属浪费时间
强制类型转换操作符 (type)
:把变量从一种类型转换为另一种数据类型。
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;//mean = 3.400000
这里要注意的是强制类型转换运算符的优先级大于除法,因此
int
类型的sum
的值首先被转换为double
型,然后除以int
类型的count
,得到一个类型为double
的值。
6.关系操作符
>
>=
<
<=
!= 用于测试"不相等"
== 用于测试"相等"
这些关系运算符比较简单,没什么可讲的。
警告: 敲代码时别千万不要把 ==
写成 =
7.逻辑操作符
a&&b 逻辑与,即并且。a和b为真,整体为真
a||b 逻辑或,即或者。a和b至少有1个为真,整体为真
a&&b&&c&&d 从左向右所有表达式都为真(非0),那整体就为真(1),否则为假(0)
a||b||c||d 从左向右所有表达式至少有一个为真(非0),那么整体就为真(1),只有所有表达式都为假时整体才为假(0)
区分 逻辑与和按位与 区分 逻辑或和按位或:
//操作对象:整数的二进制序列
1&2----->0
1|2----->3
//操作对象:整数
1||2---->1
1&&2---->1
下面是一道有趣的题:
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++
先使用a(a=0为假),后a + 1
,到此这个逻辑与的表达式为假,后面的表达式将不执行,即++b
和d++
不执行。①的结果为a=1,b=2,c=3,d=4
②i = a++||++b||d++;
a++
先使用a(a=0为假),后a+1
,继续执行, 到++b
先b+1,后使用b(此时b=3,为真),到此这个逻辑或的表达式为真,后面的表达式将不执行,即d++
不执行。②的结果为a=1,b=3,c=3,d=4
8.条件操作符
exp1 ? exp2 : exp3
若表达式exp1
为真,则执行表达式exp2
;若表达式exp1
为假,则执行表达式exp3
练习:写一个找出较大值的函数
int FindMax(int a, int b)
{
return (a > b) ? a : b;
}
9.逗号表达式
exp1, exp2, exp3, …expN
从左向右依次进行计算每个表达式,整个表达式的结果为最后一个表达式计算的结果。
疑惑:最后一个表达式计算的结果是整个表达式的结果,那为什么还要计算前面的表达式呢?看代码:
int main()
{
int a = 0;
int b = 1;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("c = %d", c);//c = 12
return 0;
}
从左往右依次计算:a>b为假,值为0;a = b + 10,此时a = 11;a=11;b = a + 1,此时b = 12,因此c = 12
若直接计算最后一个表达式,c = 1
实际上打印c = 12
逗号表达式可以和while循环结合,使语句更简洁
#include <stdio.h>
int main()
{
int a = 0;
while (a++, a < 10)
{
printf("hello world!\n");
}
return 0;
}
10.下标引用、函数调用和结构成员
[] () . ->
下标引用操作符 []
:
操作数:一个数组名 + 一个索引值
int arr[10];//创建数组
arr[9] = 10;//使用下标引用操作符对其赋值。
[ ]的两个操作数是arr和9。
函数调用操作符 ()
:
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //使用()作为函数调用操作符。
test2("hello bit.");//使用()作为函数调用操作符。
return 0;
}
结构成员操作符:
访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
struct Stu
{
char name[20];//名字
int age; //年龄
char sex[5]; //性别
};
int main()
{
struct Stu s = {
"张三", 20, "男" };//创建一个学生s,并对其进行初始化赋值
//.为结构成员访问操作符,能够访问结构体的成员
printf("name = %s age = %d sex = %s \n", s.name, s.age, s.sex);
//创建一个结构体指针,用来存放s的地址
struct Stu* ps = &s;
//->操作符可以通过指针来访问到结构体的具体成员
printf("name = %s age = %d sex = %s \n", ps->name, ps->age, ps->sex);
}
使用结构体类型struct Stu
创建了结构体变量s
,s有三个成员:name
、age
、sex
。
当我们想修改年龄时,可以s.age = 21;
但当我们想修改名字时,如s.name = "法外狂徒";
这是错误的。
因为
char name[20];
name是一个数组。s.name是数组名,即数组的首元素地址,不能对地址进行赋值。
我们可以试一试解引用访问其地址中的内容再修改:
*(s.name) = "法外狂徒";
运行结果:
错误的原因:不能用赋值语句将一个字符串常量直接给一个字符数组赋值。
那应该如何修改结构体变量的数组成员呢?答案是:使用库函数strcpy()实现。
strcpy(s.name,"法外狂徒");
运行结果:
二、表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型,这就是隐式类型转换。
1.隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
1.1.整型提升
整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。
char b = 1, c = 2;
...
a = b + c;
b和c的值会被提升为普通整型,执行加法运算,所得结果将被截断,截断后放入a。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整型提升?
整形提升是按照变量的数据类型的符号位来提升的。
有符号(正负)的整型提升:
- 写出变量的二进制补码
- 按最高位符号位进行填充
- 得到的补码再转换成原码
负数的整型提升:
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111 11111111 11111111 11111111
正数的整型提升:
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000 00000000 00000000 00000001
无符号的整型提升: 高位补0
举个例子体验一下:
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d", c);
return 0;
}
运行结果:
程序分析:
①先把a、b当成是整型写出其补码
00000000 00000000 00000000 00000011 -> int a
c -> int b
因为a、b都是 char 类型,char 类型只有一个字节,会发生截断。
00000011 -> char a
01111111 -> char b
**注:**截断——挑最小最低位的字节内容(最右边的8个bit)
②在进行运算之前,char 类型 小于 int 类型,会发生整型提升
根据整型提升规则:高位补充符号位
00000011 -> char a
补0 -> 00000000 00000000 00000000 00000011
01111111 -> char b
补0 -> 00000000 00000000 00000000 01111111
③运算
00000000 00000000 00000000 00000011
+
00000000 00000000 00000000 01111111
=
00000000 00000000 00000000 10000010 -> int c
因为c是char 类型,会发生截断
10000010 -> char c
④以%d的形式打印c,需要整型提升
10000010 -> char c
根据整型提升规则:高位补充符号位
11111111 11111111 11111111 10000010 -> c的补码
11111111 11111111 11111111 10000001 -> c的反码
10000000 00000000 00000000 01111110 -> c的原码,即-126
如图:
这就是打印出-126,而不是130的原因。
再看一个例子:
int main()
{
char a = 0xb6; //10110110
short b = 0xb600;//10110110 00000000
int c = 0xb6000000;//10110110 00000000 00000000 00000000
if (a == 0xb6)//a整型提升:11111111 11111111 11111111 10110110 补码
printf("a");
if (b == 0xb600)//b整型提升:11111111 11111111 10110110 00000000 补码
printf("b");
if (c == 0xb6000000)//c本就是整型,不用提升
printf("c");
return 0;
}
运行结果:c
因为有运算(
==
)且a,b类型小于int
类型,所以a,b要进行整形提升。
a,b整形提升之后,变成了负数,所以表达式(a == 0xb6) , (b == 0xb600)
结果是假,但是c不发生整形提升,则表达式(c == 0xb6000000)的结果是真。
注意:只要有运算且类型小于 int 或unsigned int 类型,就会整型提升。
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(!c));
return 0;
}
sizeof(c)
,没有运算,不发生整型提升,则是一个字节。
sizeof(+c)
和sizeof(-c)
,+c
、-c
也是运算的一种,会发生整型提升,则是4的字节。
1.2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换,包含其优先级:
long double 高精度
double
float
unsigned long int
long int
unsigned int
int 低精度
例:
int a = 4;
float f = 3.5f;
float r = a + f;
printf("%f\n", r);
计算
r = a + f
时,需要将a( int型 )
转换成float
型
但算数转换要合理,否则会造成精度丢失。如:
float f = 3.14;
int num = f;
高精度类型向低精度类型转换,会造成精度丢失。
2.操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
问:两个相邻操作符先执行哪个?
取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
即优先级 > 结合性
下面是关于操作符属性的一份表:
操作符具有优先级和结合性,容易写出一些有问题的表达式。
例1:
a*b + c*d + e*f;
优先级只能决定两个相邻操作符,在计算时,
*
的优先级比+
高,只能保证*
的计算比+
早,但不能确保第三个*
比第一个+
早。
上述表达式的计算顺序可能有:
第一种:
第二种:
当一个变量(如a)多次出现在表达式中,会可能因计算顺序而导致错误。
例2:
c + --c;
操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们无法知道最左边的c是自减后的c还是没自减前的c,因此有两种可能:
例3:
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
在不同编译器有着不同的结果:
值 | 编译器 |
---|---|
-128 | Tandy 6000 Xenix 3.2 |
-95 | Think C 5.02(Macintosh) |
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();
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编译器:10 4
//VS2013环境:12 4
这段代码中的第一个
+
在执行的时候,第三个++
是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+
和第三个前置++
的先后顺序。
以上这些代码在不同的编译器下运行的结果都是不同的,因为不同的编译器其运算和函数调用的顺序都是不同的。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
解决方法: 将复杂的表达式拆开来写(按照正常的逻辑)。
如例1:
a*b + c*d + e*f;
改写:
int num1 = a*b;
int num2 = c*d;
int num3 = e*f;
int sum = num1 + num2 + num3;