1. 各种操作符的介绍。
2. 表达式求值
目录
1. 操作符分类:
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
2. 算术操作符
+ - * / %
1. 除了%操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于/操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
3. %操作符的两个操作数必须为整数。返回的是整除之后的余数。
3. 移位操作符
移动的是二进制补码
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
3.1 左移操作符
移位规则:左边抛弃、右边补0
左移,左边丢弃,右边补零
整数在内存中存储的是补码的二进制序列
//int main()
//{
// //整数的二进制表示有三种形式
// //原码 反码 补码
// //正整数得原反补是相同的
// //负整数得原反补需要计算
//
// int a = 5;// 5 (5)2=0101;
// //因为a是整型 4个字节 32个bite位
// //0000 0000 0000 0000 0000 0000 0000 0101 原码
// //0000 0000 0000 0000 0000 0000 0000 0101 反码
// //0000 0000 0000 0000 0000 0000 0000 0101补码
//
// int c = -5;
// //-5;
// //1000 0000 0000 0000 0000 0000 0000 0101 -5的原码
// //1111 1111 1111 1111 1111 1111 1111 1010 -5的反码
// // 由原码到反码,符号位不变,其他位按位取反
// //1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
// // 由反码到补码,需要加一
//
// int b = a << 1;//a本质是不变的
// //移动的是二进制位的补码;
// //000 0000 0000 0000 0000 0000 0000 01010补码
// int d = c << 1;
// //111 1111 1111 1111 1111 1111 1111 10110 -5左移后的补码
// //111 1111 1111 1111 1111 1111 1111 10101 -5左移后的反码
// //100 0000 0000 0000 0000 0000 0000 01010 -5左移后的原码
// //-10
//
// printf("%d\n", a);//5
// printf("%d\n", b);//10
// printf("%d\n", d);//-10
// //打印出来的值是原码
// return 0;
//}
3.2 右移操作符
移位规则:
首先右移运算分两种:
1. 逻辑移位左边用0填充,右边丢弃
2. 算术移位左边用原该值的符号位填充,右边丢弃
》》当前编译器在右移时,采用算术右移
》》到底是算术右移还是逻辑右移取决于编译器的!!!
右移操作符有两种规则:
1.算术右移:右边丢弃,左边补原符号位
2.逻辑右移:右边丢弃,左边补零
//int main()
//{
// int a = 5;
// //0000 0000 0000 0000 0000 0000 0000 0101 5的补码
// //5是正数,符号位为0,算术右移或逻辑右移都一样
// int b = a >> 1;
// //00000 0000 0000 0000 0000 0000 0000 010 5右移后的补码
//
// int c = -5;
// //1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
// int d = c >> 1;
// //11111 1111 1111 1111 1111 1111 1111 101 -5右移后的补码
// //11111 1111 1111 1111 1111 1111 1111 100 -5右移后的反码
// //10000 0000 0000 0000 0000 0000 0000 011 -5右移后的原码
// printf("%d\n", a);//5
// printf("%d\n", b);//2
// printf("%d\n", c);//-5
// printf("%d\n", d);//-3
// return 0;
//}
警告⚠ :对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;num>>-1;//error
4. 位操作符
位操作符有:
& //按位与
| //按位或
^ //按位异或
//位操作符有三种:
//1. & 按(二进制)位与 // a&b——双目操作符
//2. | 按(二进制)位或 // &a——单目操作符
//3. ^ 按(二进制)位异或
int main()
{
int a = 3;
int b = -5;
int c = a & b;
//0000 0000 0000 0000 0000 0000 0000 0011 3的补码
//1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
//按位与——两个同时为1,结果才为1;
//0000 0000 0000 0000 0000 0000 0000 0011 3 & -5的补码
printf("%d\n", c);//3
int d = a | b;
//0000 0000 0000 0000 0000 0000 0000 0011 3的补码
//1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
//按位与—— 一个为1,结果就为1;
//1111 1111 1111 1111 1111 1111 1111 1011 3 | -5的补码
//1000 0000 0000 0000 0000 0000 0000 0101 原码
printf("%d\n", d);//-5
int e = a ^ b;
//0000 0000 0000 0000 0000 0000 0000 0011 3的补码
//1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
//按位异或,相同为0,相异为1;
//1111 1111 1111 1111 1111 1111 1111 1000 3 ^ -5的补码
//1000 0000 0000 0000 0000 0000 0000 1000 3 ^ -5的原码
printf("%d\n", e);//-8
return 0;
}
//总结
//a ^ a = 0; 两个对应的二进制完全相同,异或为0;
//0 ^ a = a;
//例如:0000 0000 0000 0000 0000 0000 0000 0000 0的补码
// 1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
//0^-5 1111 1111 1111 1111 1111 1111 1111 1011 仍是-5
//数组 arr1[100] = {1,2,3,4,5}
//数组 arr2[100] = {1,2,3,4}
//有两个数组,数组中的元素个数未知,但是数组元素
//都是成对出现的,只有一个元素出现了一次,找出
//只出现一次的数字
//答案:
// arr1[100] 异或 arr2[100]
//因为相同的两个元素异或结果是0;
//而 0 和 非0 异或 结果为它本身
注:他们的操作数必须是整数。
一道变态的面试题:
//不能创建临时变量,实现两个整数的交换
int main()
{
int a = 3;
int b = 5;
printf("a = %d,b = %d\n", a, b);
//a = a + b;
//b = a - b;
//a = a - b;
//如果a b值过大 相加则会溢出
a = a ^ b;//011 ^ 101 = 110
b = a ^ b;//110 ^ 101 = 011
//b = a ^ b—带入— b = a ^ b ^ b = a ^ 0 = a
a = a ^ b;//110 ^ 011 = 101
// a = a ^ b ^ a = 0 ^ b = b
printf("a = %d,b = %d\n", a,b);
return 0;
}
//练习
//求一个整数存储在内存中二进制中1的个数
//5
//0000 0000 0000 0000 0000 0000 0000 0101
//按位与
//0000 0000 0000 0000 0000 0000 0000 0001
//=0000 0000 0000 0000 0000 0000 0000 0001 1
//5&1==1
//右移
int main()
{
int i = 0;
int count = 0;
int num = 0;
scanf("%d", &num);
int sz = sizeof(num) * 8;
for (i = 0; i < 32; i++)
{
if (((num >>i)& 1) == 1)
count++;
}
printf("%d\n", count);
return 0;
}
5. 赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
复合赋值符
+
=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
//赋值操作符
//int main()
//{
// int a = 10;
// int x = 0;
// int y = 20;
// a = x = y + 1;
// a = a + 2;
// a += 2;
// printf("%d %d\n", a, x);
// return 0;
//}
6. 单目操作符
6.1 单目操作符介绍
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
//单目操作符
//C语言中C99之前没有表示真假的类型
//C99中引入了布尔类型
//#include<stdbool.h>
//int main()
//{
// _Bool flag1 = false;
// bool flag2 = true;
// if (flag2)
// {
// printf("hehe\n");
// }
// //int num = 10;
// //if (num)
// //{
//
// //}
// //if (!num)//num为假,做事情
// //{
//
// //}
// return 0;
//}
struct S
{
char name[20];
int age;
};
int main()
{
//&——取地址操作符
//*——解引用操作符(间接访问操作符)
int a = 10;
int* pa = &a;
*pa = 20;//解引用操作符
int arr[10] = { 0 };
&arr;//取出数组的地址,数组的地址应该放到[数组指针]中
struct S s = { 0 };
struct S* ps = &s;
return 0;
}
// ~ 按二进制位按位取反
int main()
{
int a = 0;
//0000 0000 0000 0000 0000 0000 0000 0000
//1111 1111 1111 1111 1111 1111 1111 1111 ~a的补码
//1000 0000 0000 0000 0000 0000 0000 0001 ~a的原码
printf("%d\n", ~a);//-1
return 0;
}
6.2 sizeof 和 数组
//struct S
//{
// char name[20];
// int age;
//};
//int main()
//{
// //&——取地址操作符
// //*——解引用操作符(间接访问操作符)
//
// int a = 10;
// int* pa = &a;
// *pa = 20;//解引用操作符
// // *&a ==>a
// int arr[10] = { 0 };
// &arr;//取出数组的地址,数组的地址应该放到[数组指针]中
// struct S s = { 0 };
// struct S* ps = &s;
// return 0;
//}
//sizeof()
//计算类型创建的变量所占内存的大小
int main()
{
//int a = 10;
//printf("%d\n", sizeof a);
sizeof是一个操作符,是c语言的关键字,不是函数
//printf("%d\n", sizeof(a));
//printf("%d\n", sizeof(int));
/*int arr[10] = { 0 };
printf("%u\n", (unsigned int)sizeof(arr));*/
int a = 10;
short s = 0;
printf("%d\n",(unsigned int)sizeof(s = a + 2));//2
//sizeof()中的表达式不参与计算;
//sizeof()是在编译期间处理的
printf("%d\n", s);//0
return 0;
}
//sizeof()和数组
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//4
}
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));//40
printf("%d\n", sizeof ch);//10
test1(arr);
test2(ch);
return 0;
}
// ~ 按二进制位按位取反
int main()
{
//int a = 0;
0000 0000 0000 0000 0000 0000 0000 0000
1111 1111 1111 1111 1111 1111 1111 1111 ~a的补码
1000 0000 0000 0000 0000 0000 0000 0001 ~a的原码
//printf("%d\n", ~a);//-1
int a = 11;
//0000 0000 0000 0000 0000 0000 0000 1011 11的补码
//0000 0000 0000 0000 0000 0000 0000 0100 11|4
//0000 0000 0000 0000 0000 0000 0000 1111 15
//0000 0000 0000 0000 0000 0000 0000 0001 1
// 1<<2;
a |= (1<<2);
printf("%d\n", a);//15
//a ^= (1 << 2);
a &= (~(1 << 2));
printf("%d\n", a);//11
return 0;
}
7. 关系操作符
关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
int main()
{
//比较的不是字符串,而是首字符串的地址
if ("abcdef" == "abbq")
{
}
return 0;
}
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告:
在编程的过程中== 和=不小心写错,导致的错误。
8. 逻辑操作符
逻辑操作符有哪些:
&& 逻辑与
|| 逻辑或
区分逻辑与和按位与
区分逻辑或和按位或
1&2----->0 1&&2---->1
1|2----->3 1||2---->1
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
//i = a++ && ++b && d++;//a=1,b=2,c=3,d=4,i=1
//对于逻辑与操作符,左边的左值为假,后面全部就不计算了
//若 int a = 1;
//i = a++ && ++b && d++;//a=2;b=3;c=3;d=5
i = a++||++b||d++;//a=1;b=3;c=3;d=4
//a先使用,后++;b先++,后使用
//i = 0 || 3 一真为真,后面都不计算
//若 int a = 1;
//i = a++ || ++b || d++;//a=2;b=2;c=3;d=4
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
9. 条件操作符
exp1 ? exp2 : exp3
10. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, b = a + 1); //(0,a=12,b=13)
printf("%d\n", c);//13
return 0;
}
11. 下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值 ———————拥有两个操作数 arr 和 4
//下标引用操作符【】
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[4]);//5 //arr[4]-->*(arr+4)
printf("%d\n", 4[arr]);//5
//arr
//arr[4] = 5; //[]就是下标引用操作符
//
return 0;
}
由此可知,【】就是相当于一个操作符,arr和4只是两个操作数,例如a+b和b+a是等价的
2. ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
//()函数调用操作符
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(2, 3);//操作数是:Add,2,3
printf("%d\n", ret);//5
return 0;
}
3. 访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
//结构体成员访问操作符
#include<string.h>
typedef struct
{
char name[20];
int age;
float score;
}stu;
//struct stu
//{
// char name[20];
// int age;
// float score;
//};
void print1(stu s)
{
printf("name:%s age:%d score:%f\n", s.name, s.age, s.score);
}
void print2(stu* pc)
{
//printf("name:%s age:%d score:%f\n", (*pc).name, (*pc).age, (*pc).score);
// 结构体成员操作符 优先级 高于 解引用操作符
printf("name:%s age:%d score:%f\n", pc->name, pc->age, pc->score);
}
int main()
{
stu s = { "张山",20,90.5f };
//s.name="张山峰";//数组名是地址无法直接更改
strcpy(s.name,"张三丰");
//scanf("%s", s.name);
print1(s);
print2(&s);
return 0;
}
12. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
int main()
{
char c1 = 3;
//整型 存入 字符型,4个字节存入1个字节中,会发生截断
//0000 0000 0000 0000 0000 0000 0000 0011
//c1 = 0000 0011
char c2 = 127;
//0000 0000 0000 0000 0000 0000 0111 1111
//c2 = 0111 1111
char c3 = c1 + c2;
//当c1 和 c2 计算时先进行整型提升,才能相加
//c1 = 0000 0000 0000 0000 0000 0000 0000 0010
//c2 = 0000 0000 0000 0000 0000 0000 0111 1111
//c1+c2 = 0000 0000 0000 0000 0000 0000 1000 0010
//c3 = c1 + c2;进行截断
//c3 = 1000 0010
printf("%d", c3);
//打印的又是整形,进行整型提升
//1111 1111 1111 1111 1111 1111 1000 0010;补码
//1000 0000 0000 0000 0000 0000 0111 1110;原码 = -(2^7-1)
return 0;
}
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)
//a是字符型,转为整型,需要进行整型提升
//1111 1111 1111 1111 1111 1111 1011 0110 a
//0000 0000 0000 0000 0000 0000 1011 0110 //不相等
{
printf("a");
}
if (b == 0xb600)
//b是短整型,与整型比较,需要进行整型提升
//1111 1111 1111 1111 1011 0110 0000 0000 b
//0000 0000 0000 0000 1011 0110 0000 0000 //不相等
{
printf("b");
}
if (c == 0xb6000000)
//c是整形,整型与整型数据进行比较,不需要进行整型提升
{
printf("c");
}
return 0;
//打印c
}
int main()
{
char c = 1;
printf("%u\n", sizeof(c));//1
printf("%u\n", sizeof(+c));//4
printf("%u\n", sizeof(-c));//4
//正号负号说明表达式进行运算,就要进行整型提升
return 0;
}
12.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
由下至上进行转换
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?
取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
一些问题表达式
注释:代码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();
printf( "%d\n", answer);//输出多少?
return 0;
}
这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。但是上述代码answer = fun() - fun() * fun();中
我们只能通过操作符的优先级得知:先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。