算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
+ - * / %
<<(左移操作符) >>(右移操作符)
注意:
1.移动的是补码的二进制位,移位操作符的操作数只能是整数
(补充:整数在内存中存储的都是补码的二进制序列)
原码、反码和补码的关系:
正整数的原码、反码和补码相同
负整数的反码=原码除符号位(即首位)全部取反,补码=反码+1
因此,把补码变成原码,可以选择:
(1)补码-1,再除符号位全部取反
(2)补码先除符号位全部取反,再+1
int main()
{
int m = 7;
int n = m << 1;//左移一位
printf("m=%d n=%d", m, n);//打印的是原码
return 0;
}
//m=7 n=14
若m的值在没被赋值的情况下,自身的值不会改变
7的原码(反码、补码)是:
00000000 00000000 00000000 00000111(正整数的原码,反码,补码相同)
左移一位后:
00000000 00000000 00000000 00001110 = 14
左移多位同理
当m为负数时:
int main()
{
int m = -7;
int n = m << 1;//左移一位
printf("m=%d n=%d", m, n);//打印的是原码
return 0;
}
//m=-7 n=-14
-7的原码是:
10000000 00000000 00000000 00000111
11111111 11111111 11111111 11111000(反码)(符号位不取反)
11111111 11111111 11111111 11111001(补码)
左移一位后
11111111 11111111 11111111 11110010(补码)
10000000 00000000 00000000 00001110(n的原码)= -14
逻辑移位:左边用0填充,右边丢弃
算术移位:左边用原该值的符号位填充,右边丢弃(大多数编译器默认)
int main()
{
int m = -7;
int n = m >> 1;//右移一位
printf("m=%d n=%d", m, n);//打印的是原码
return 0;
}
//m=-7 n=-4
-7的原码是:
10000000 00000000 00000000 00000111
11111111 11111111 11111111 11111000(反码)(符号位不取反)
11111111 11111111 11111111 11111001(补码)
右移一位后:
逻辑右移:
01111111 11111111 11111111 11111100(补码)
00000000 00000000 00000000 00000100(n的原码)= 4
算术右移:
11111111 11111111 11111111 11111100(补码)
10000000 00000000 00000000 00000100(n的原码)= -4
警告⚠:
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int main()
{
int m = 7;
int n = m << -1;//error
printf("m=%d n=%d", m, n);//打印的是原码
return 0;
}
&(按位与) |(按位或) ^(按位异或)
&:两个整数的相同位置:
(1)都为1时,&在该位置为1
(2)都为0时,&在该位置为0
(3)一个为0,一个为1时,&在该位置为0
int main()
{
int a = 3;
int b = -5;
int c = a & b;
printf("c=%d ", c);
return 0;
}
//c=3
//00000000 00000000 00000000 00000011 -- 3的原码
//10000000 00000000 00000000 00000101 -- -5的原码
//11111111 11111111 11111111 11111011 -- -5的补码
//00000000 00000000 00000000 00000011 -- 3的补码
//00000000 00000000 00000000 00000011 = 3 -- 3 & -5
&1和<<、>>一起使用可以知道一个整数补码的二进制序列的任何一位
想知道3的补码的最后一位数字是什么
int main()
{
int a = 3;
int c = a & 1;
printf("c=%d", c);
return 0;
}
//c=1
//00000000 00000000 00000000 00000011 -- 3的补码
//00000000 00000000 00000000 00000001 -- 1的补码
//00000000 00000000 00000000 00000001 -- 3 & 1 //为1
想知道3的补码的倒数第二位数字是什么
int main()
{
int a = 3;
int c = a & 1;
printf("c=%d", c);
return 0;
}
//c=1
//(3>>1)&1
//00000000 00000000 00000000 00000001 -- 3>>1的补码
//00000000 00000000 00000000 00000001 -- 1的补码
//00000000 00000000 00000000 00000001 -- (3>>1) & 1 //为1
|:两个整数的相同位置:
(1)都为1时,| 在该位置为1
(2)都为0时,| 在该位置为0
(3)一个为0,一个为1时,| 在该位置为1
int main()
{
int a = 3;
int b = -5;
int c = a | b;
printf("c=%d", c);
return 0;
}
//c=-5
//00000000 00000000 00000000 00000011 -- 3的原码
//10000000 00000000 00000000 00000101 -- -5的原码
//11111111 11111111 11111111 11111011 -- -5的补码
//00000000 00000000 00000000 00000011 -- 3的补码
//11111111 11111111 11111111 11111011 -- 3 | -5的补码
//10000000 00000000 00000000 00000101 = -5 -- 3 | -5的原码
^:两个整数的相同位置:相同为0,相异为1
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
printf("c=%d", c);
return 0;
}
//c=-8
//00000000 00000000 00000000 00000011 -- 3的原码
//10000000 00000000 00000000 00000101 -- -5的原码
//11111111 11111111 11111111 11111011 -- -5的补码
//00000000 00000000 00000000 00000011 -- 3的补码
//11111111 11111111 11111111 11111000 -- 3 ^ -5的补码
//10000000 00000000 00000000 00001000 = -8 -- 3 ^ -5的原码
易知:a ^ a = 0 a ^ 0 = a
^ 具有交换律,即 a ^ b ^ c = a ^ c ^ b
int main()
{
int a = 3;
int b = -5;
int c = a ^ b ^ a;
printf("c=%d ", c);
return 0;
}
//c=-5
//一般方法
//00000000 00000000 00000000 00000011 -- 3的原码
//10000000 00000000 00000000 00000101 -- -5的原码
//11111111 11111111 11111111 11111011 -- -5的补码
//00000000 00000000 00000000 00000011 -- 3的补码
//11111111 11111111 11111111 11111000 -- 3 ^ -5的补码
//00000000 00000000 00000000 00000011 -- 3的补码
//11111111 11111111 11111111 11111011 -- 3 ^ -5 ^ 3的补码
//10000000 00000000 00000000 00000101 = -5 -- 3 ^ -5 ^ 3的原码
//交换律
//a ^ b ^ a = a ^ a ^ b = 0 ^ b = b = -5
一道变态的面试题:
不能创建临时变量(第三个变量),实现两个数的交换
int main()
{
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;//b = a^b^b = a^0 = a
a = a ^ b;//a = a^b^a = 0^b = b
printf("a = %d b = %d\n", a, b);
return 0;
}
//a=20 b=10
=
赋值操作符是一个很棒的操作符,它可以让你修改某些之前不满意的值
int main()
{
int a = 1;//不是赋值,是初始化
a = 20;//赋值
return 0;
}
赋值操作符可以连续使用,例如:
int main()
{
int a = 1;
int x = 0;
int y = 2;
a = x = y + 1;//连续赋值
return 0;
}
连续赋值确实会比较简洁,那下面的代码呢?
int main()
{
int a = 1;
int x = 0;
int y = 2;
x = y + 1;
a = x;
//这样的写法是不是更加清晰爽朗而且易于调试。
return 0;
}
-= += *= /= %=
>>= <<= &= |= ^=
int main()
{
int x = 9;
x = x + 2;
x += 2;//复合赋值
//两种方式是一个意思
//其他运算符一样的道理。这样写更加简洁、高效
return 0;
}
! 逻辑反操作
- 负值
+ 正值
& 取地址
* 间接访问操作符(解引用操作符)
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
- - 前置、后置- -
++ 前置、后置++
(类型) 强制类型转换
即 ! 真 = 假 , ! 假 = 真
int main()
{
int a = 5;
int b = !a;
printf("b=%d", b);
return 0;
}
//b=0
+ - 比较简单,就不赘述了
不知道你看见它会不会觉得有些眼熟呀,没错!它也是上文 位操作符 中的 &按位与
但虽然它们两都长这样,但所代表的意思却截然不同哦
&取地址操作符是单目操作符,而 &按位与是双目操作符
int main()
{
int a = 10;
int* p = &a;//a变量的地址
int arr[10];
int(*pa)[10] = &arr;//这是整个数组的地址
return 0;
}
有 & 取地址操作符, 就有 * 解引用操作符
int main()
{
int a = 10;
int* p = &a;//a变量的地址
printf("*p=%d", *p);
//对p进行解引用操作,*p是通过p中存放的地址,找到p指向的对象
//*p其实就是a
return 0;
}
//*p=10
sizeof计算的结果是 sizeof_t 类型的,sizeof_t 是无符号整型的
对sizeof_t 类型的数据进行打印,可以使用%zd,%u也可,用%d也能运行,但一些编译器可能会报警告,但对结果没有影响
int main()
{
int a = 10;
printf("%zd", sizeof(a));//4
printf("%zd", sizeof(int));//4
return 0;
}
sizeof后面的括号中写的不是类型的时候,括号可以省略,
这就可以说明sizeof不是函数,而是单目操作符
int main()
{
int a = 10;
printf("%d", sizeof a );//4
printf("%zd", sizeof(int));//4
return 0;
}
问:下面代码中
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
#include
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(3)
}
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));//(2)
test1(arr);
test2(ch);
return 0;
}
答案:
(1) 40 (2)10 (3) 4/8 (4)4/8
(1) 40: arr数组中有10个元素,每个元素都是int类型,所以所占空间大小=4 * 10=40
(2) 10: ch数组中有10个元素,每个元素都是char类型,所以所占空间大小=1 * 10=10
(3)(4) 4/8:
传参传过去的不是数组,而是数组首元素的地址
所以可以把arr想成是指向数组首元素的指针变量
32位平台下有32个比特位即4个字节,因此指针变量的大小全为4
64位平台下有64个比特位即8个字节,因此指针变量的大小全为8
int main()
{
int a = 0;
printf("~a=%d\n", ~a);
return 0;
}
//~a=-1
//00000000 00000000 00000000 00000000 -- 0的补码
//11111111 11111111 11111111 11111111 -- ~a的补码(符号位也取反)
//10000000 00000000 00000000 00000001 = -1 -- ~a的原码
如何让某个整数的补码的二进制序列的某一位数发生改变,而其它位不变?
int main()
{
int a = 10;
//00000000 00000000 00000000 00001010
//如何让10的补码的二进制序列的倒数第五位从0变成1,其它位不变
// 让10 | (1<<4)
//00000000 00000000 00000000 00010000 -- 1<<4 的补码
//00000000 00000000 00000000 00011010
a |= (1 << 4);
printf("a=%d\n", a);//a=26
//如何再让a的补码的二进制序列的倒数第五位从1变成0,其它位不变
//00000000 00000000 00000000 00011010
//让 a & 下面的数
//11111111 11111111 11111111 11101111(可以看出这是 ~(1<<4) )
//00000000 00000000 00000000 00001010
a &= (~(1 << 4));
printf("a=%d\n", a);//a=10
return 0;
}
前置++ : 先自增,再使用
后置++ : 先使用,再自增
- -同理
int main()
{
int a = 3;
int b = 5;
int c = ++a;
int d = b++;
printf("a=%d c=%d", a, c);//a=4 c=4
printf("b=%d d=%d", b, d);//b=6 d=5
return 0;
}
如果你遇见了这样的代码,请不要懵逼,因为编译器和你一样懵逼o(≧口≦)o
//这个代码是有问题的
int main()
{
int a = 1;
int b = (++a) + (++a) + (++a);
printf("b=%d", b);
return 0;
}
在vs2019编辑器上,b=12
所以,以后再遇见这种类型的题目不要懵逼,要相信,这是题目的问题︿( ̄︶ ̄)︿
注意:强制类型转换只有再万不得已的情况下使用
int main()
{
int a = 3.14;
//3.14 会被编译器识别为double类型
return 0;
}
这种情况下,编译器能运行,但会报警告
当我们需要随机值时,会调用rand()函数,
但在调用该函数前又要调用srand()函数,srand()函数里又要使用time()函数,
time()函数的返回值时time_t,而srand()函数需要的是unsigned int 类型的数,
所以需要强制类型转换,从而使用
srand((unsigned int)time(NULL))
> = < >= <=
!= 用于测试“不相等” == 用于测试“相等
这些关系运算符比较简单,没什么可讲的,但是注意不要将==写成=
&& 逻辑与 | | 逻辑或
等等,我们是不是又在哪里见过它们 Σ( ° △ °|||)
让我想想,哦(拍大腿),它们分别是double个 &按位与 和 | 按位或
那它们该如何区分呢?
&按位与 和 |按位或 需要根据整数的二进制判断,
但 &&逻辑与 和 ||逻辑或 是根据数的值来判断
其实逻辑与和逻辑或在我们高中就有学过,但如果你不记得的话,叙述起来也很简单,就是
&& 按位与两边数字全为非0即为真,否则为假
|| 按位或两边数字有 >=1个为非0即为真,否则为假
1&2 ---->0 1|2 ---->3
1&&2 ---->1 1||2 ---->1
了解了这些之后,让我们开始做题吧φ(≧ω≦*)♪
下面程序输出的结果是什么?
int main()
{
int i = 0,j = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d b = %d c = %d d = %d ", a, b, c, d);
printf("i = %d\n", i);
return 0;
}
答案是:a=1 b=2 c=3 d=4 i=0
在说明答案之前,让我们先来了解什么叫短路操作
所谓短路操作,简单理解为左右两边数字,只要确定了左边数字的真假,右边无需计算
例如:
&&逻辑与: 左边操作数如果为假,那么这个表达式就为假,不用判断右边数字
| | 逻辑或: 左边操作数如果为真,那么这个表达式就为真,不用判断右边数字
解析:
a++,即a先使用后自增,所以 i = 0 && ++b && d++,
根据短路操作,因为左边为0,所以右边的b无需计算,所以第一个表达式为假(即a++ && ++b等于0),
所以 i = 0 && d++,又根据短路操作,因为左边为0,所以右边的d无需计算,第二个表达式为假
因此a自增后为1,bcd不变,i=0
那将&&变为 | |结果又是什么呢?
int main()
{
int i = 0,j = 0, a = 0, b = 2, c = 3, d = 4;
i = a++||++b||d++;
printf("a = %d b = %d c = %d d = %d ", a, b, c, d);
printf("i = %d\n", i);
return 0;
}
答案是:a=1 b=3 c=3 d=4 i=1
详解:
a++,即a先使用后自增,所以 i = 0 || ++b || d++,此时还要判断++b是否为非0,
++b即b先自增后使用,所以第一个表达式为0||3为真,所以i=1||d++,
根据短路操作,因为左边为非0,所以右边的d无需计算,
因此a,b自增后分别为1,3, c,d不变, i=1
exp1 ? exp2 : exp3
流程:先判断exp1的真假,
若为真,则只计算exp2且整个表达式结果为exp2的结果,exp3不计算
若为假,则只计算exp3且整个表达式结果为exp3的结果,exp2不计算
int main()
{
int a = 10;
int b = 8;
int max;
//if (a > b)
// max = a;
//else
// max = b;
max = (a > b ? a : b);//上面注释行可简写为该条件表达式
printf("%d\n", max);
return 0;
}
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
从左向右依次执行,前面表达式的结果会被“丢弃”(但是如果表达式对变量的值产生影响,那么该变量的值就会改变),只有最后一个表达式的结果会被留下并成为该逗号表达式的结果。
下面程序输出的结果是什么呢?
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, b = a + 1);//逗号表达式
printf("c=%d\n", c);
return 0;
}
//c=13
解析:
由题意,对c进行初始化时,先要计算逗号表达式的值,逗号表达式从左向右依次执行
因此,先执行a>b为假,结果为0,因为不是最后一个表达式的结果,所以0被丢弃;
接着执行a=b+10,所以a=12,因为不是最后一个表达式的结果,所以a=12被丢弃,但此时a的值已由1变为12;
再执行b=a+1=13,因为是最后一个表达式的结果,所以c=b=13
下面程序中fun()函数有多少个实参呢?
int main()
{
int v1, v2, v3, v4, v5, v6;
fun((v1, v2), (v3, v4), v5, v6);
return 0;
}
答案:4个
(v1, v2)属于第一个实参,逗号表达式,真实的参数时v2
(v3, v4)属于第二个实参,逗号表达式,真实的参数是v4
v5属于第三个实参
v6属于第四个实参
操作数:一个数组名 + 一个索引值
int main()
{
int arr[10] = { 0 };
int arr[3] = 4;//[]里面的值叫做索引值
return 0;
}
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
只要是函数,那么()函数调用操作符就不可省略
int main()
{
test1(); //使用()作为函数调用操作符。
test2("hello bit.");//实使用()作为函数调用操作符。
return 0;
}
. 结构体.成员名(非指针变量)
-> 结构体指针->成员名(指针变量)
struct card
{
char name[20];
int age;
};
void Print(struct card* pa)
{
printf("%s %d\n", pa->name, pa->age);//pa是指针变量,用->
}
int main()
{
struct card a = { "zhangsan",18 };
printf("%s %d\n", a.name, a.age);//a不是指针变量,用.
Print(&a);
return 0;
}
既然操作符了解完了,那么就该带入表达式求解了
表达式求值的顺序一部分是由操作符的 优先级 和 结合性 决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的 字符 和 短整型 操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算
简单来说,就是 字符 和 短整型 操作数 在需要 算术运算 或者需要 以%d形式打印 时,就要 整型提升
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
无符号整形提升,高位补0
例如:
int main()
{
char a = 5;
//00000101
char b = 126;
//01111110
char c = a + b;//a和b要进行算术运算,且都为char类型,因此要进行整型提升
//a、b的符号位(即最高位)都为0,所以用0提升
//00000000 00000000 00000000 00000101 -- a整型提升后
//00000000 00000000 00000000 01111110 -- b整型提升后
//00000000 00000000 00000000 10000011 -- a+b
//10000011 -- c时char类型,所以只能放8个比特位
printf("c=%d\n", c);
//c要以%d形式打印,又是char类型,就要整型提升
//c的符号位是1,所以用1提升
//11111111 11111111 11111111 10000011 -- c整型提升后(补码)
//10000000 00000000 00000000 01111101 = -125 -- c整型提升后(原码)
return 0;
}
//c=-125
int main()
{
char a = 0xb6;//1011 0110
short b = 0xb600;//1011 0110 0000 0000
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
//c
答案: c
解析:
a,b,c都要进行算术运算,又a,b分别是char和short int,所以a,b要整型提升,但c不用
a,b整形提升之后,变成了负数(符号位是1,用1提升,再将补码变为原码时符号位不变),所以表达式 a == 0xb6 ,b == 0xb600 的结果是假,
但是c不发生整形提升,则表达式 c == 0xb6000000 的结果是真.
int main()
{
char c = 1;
printf("%u ", sizeof(c));
printf("%u ", sizeof(+c));
printf("%u ", sizeof(-c));
return 0;
}
//1 4 4
解析:
c只要参与表达式运算,就会发生整形提升,表达式 +c ,发生提升,所以 sizeof(+c) 是4个字节.
表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof( c) ,就是1个字节
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果转换,上表中的数据类型由下往上转换,
如:int+float,那么就将int类型转化为float类型,再相加
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级:(由上往下,优先级降低)
控制求值顺序:&&逻辑与 ||逻辑或 ?:条件赋值符 , 逗号表达式
- &&逻辑与:当&&左边的值为0时,表达式为假,不用再计算右边的值是否为0
- | | 逻辑或:当 | | 左边的值为非0时,表达式为真,不用再计算右边的值是否为0
- exp1?exp2:exp3 条件表达式:
(1)当exp1为真时,计算exp2的值,而exp3不用计算;
(2) 反之则计算exp3的值,而exp2不用计算;- 逗号表达式:执行顺序只能是从左往右执行
我们已经了解了操作符的属性,但在一些情况下我们仍不能某些代码的执行顺序,因此,我们要尽量避免写成以下代码
a*b + c*d + e*f
注释:代码在计算的时候,由于 * 比+的优先级高,只能保证 * 的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。所以可能出现以下两种循序
你或许会问,不管是哪种顺序,计算结果难道不是一样吗?
如果这些都是单个变量,你说得对
但如果它们是一些较长的表达式,那是不是结果就可能不一样了呢