目录
前言
1.关于操作符(运算符)
1.1操作符分类
1.2算术操作符
1.2.1 加减乘除取模
1.2.2指数运算
1.3移位操作符
1.3.1 左移操作符
1.3.2 右移操作符
1.4位操作符
1.4.1位操作符分类
1.4.2实例+分析+总结
(1)不能创建临时变量(第三个变量),实现两个数的交换
(2)求一个整数存储在内存中的二进制中1的个数
(3)两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同
(4)获取一个整数二进制序列中所有的偶数位和奇数位
1.5 赋值操作符
1.5.1 单独赋值
1.5.2复合赋值
1.5.3 左值、右值与数据对象
1.6 单目操作符
1.6.1 sizeof()
1.6.2 自增++或自减--
1.7 关系操作符
1.8 逻辑操作符
1.8.1 逻辑与和按位与
1.8.2 逻辑或和按位或
1.8.3 典题剖析
1.9 条件操作符
1.10 逗号操作符
1.11 [ ] 下标引用操作符
1.12 ( ) 函数调用操作符
1.13 访问一个结构的成员的操作符
2. 操作符的属性
2.1 求值因素
2.2 例题分析
2.2.1 例1
2.2.2 例2
2.2.3 例3
2.2.4 例4
2.2.5 例5
2.2.6 总结
3.深入理解——副作用与序列点(了解一下)
4. 自增运算符计算路径问题
表达式匹配——贪心法
5. 表达式和语句的那些事儿
5.1 表达式
5.2 语句
5.3 关系
敬请期待更好的作品吧~
本文内容还挺丰富的,干货满满了属于是,这不写着写着就近万字了嘛,主要分享一波C语言运算符、表达式和语句,重点在于运算符,这篇幅不可谓不“浩荡”。
有一点C语言基础就可以阅读此文,希望我的分享心得能帮助到大家,同时由于作者水平实在有限,难免存在纰漏,大伙各取所需即可。
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
操作符(运算符)对象是操作数(运算对象),不只是单单的一个数,实际上对象也可以是表达式的值。
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
浮点数除法的结果是浮点数,而整数除法的结果是整数。
在C语言中,整数除法结果的小数部分被直接舍弃而非四舍五入,这一过程被称为截断。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
C没有指数运算符,不过标准库里提供了pow()函数用于指数运算:
函数原型:
double pow( double x, double y );
所属头文件:
功能:求取x的y次方的值,注意参数和返回值都是双精度浮点型。
比如pow(2.0, 3.0)得到的值是8.0。
要注意的取值:
移位操作符的操作数只能是整数,并且移位针对补码操作。
移位规则:
左边抛弃、右边补0,每移动一位相当于*2。
移位规则:
右移运算分两种:
1. 逻辑移位
左边用0填充,右边丢弃
2. 算术移位
左边用原该值的符号位填充,右边丢弃
到底使用哪一种取决于编译器。
VS使用算术右移。
警告⚠ : 对于移位运算符,不要移动负数位(如a << -1),这个是标准未定义的。
& //按位与,两个操作数二进制位都为1才为1,否则为0
| //按位或,两个操作数二进制位有1就为1,否则为0
^ //按位异或 ,两个操作数二进制位相同为0,相异为1
注:他们的操作数必须是整数,而且操作对象是补码!
示例:
#include
int main()
{
int num1 = 1;
int num2 = 2;
num1 & num2;
num1 | num2;
num1 ^ num2;
return 0;
}
分析:
方法一:
int a = 3;
int b = 5;
a = a + b;
b = a - b;//得到原来的a
a = a - b;//得到原来的b
方法二:
关于异或的小总结:3^3 = 0, 3^0 = 3
也就是说一个数异或它本身结果为0,一个数异或0结果为它本身。
3^5^3 = 5,说明异或运算具有交换律,即3^5^3 = 3^3^5
所以可以这样:
int a = 4;
int b = 7;
a = a ^ b;
b = a ^ b;//也就是a^b^b=b^b^a=0^a=a得到原来的a
a = a ^ b;//也就是a^b^a=a^a^b=0^b=b得到原来的b
方法一:
循环进行以下操作,直到n被缩减为0。
用该数据模2,检测其是否能够被2整除,若可以则该数据对应二进制比特位的最低位一定是0,否则是1。如果是1给计数加1,如果n不等于0,继续模2除2。(类比一下十进制)
十进制数除10去掉一个十进制位,二进制数除2去掉一个二进制位。
int count_one_bit(int n)
{
int count = 0;
while(n)
{
if(n%2==1)
count++;
n = n/2;
}
return count;
}
这样测不了负数,需要小小改动一下,把函数形参改为无符号整型unsigned int,当传入负数时,会自动转换类型,看成一个很大的正数。
缺陷:进行了大量的取模以及除法运算,取模和除法运算的效率本来就比较低。
方法二:
一个数&1,若该数二进制第0位为1则结果为1,为0则结果为0
那我们可以这样来设计,循环地把目标整数的二进制位右移,让每一个位上的数都&1,以此来判断每一位是不是1,结果为1则计数+1。正数负数都可以。
实际上二进制位移位&1相当于取出每一位来判断。
int count = 0;
int a = 65;
for(int n = 0; n < sizeof(int); n++)
{
if((a >> n)&1)
count++;
}
方法三:
或者这样(更巧妙高效):
int tmp = 0;
scanf("%d", &tmp);
int count = 0;
while(tmp)
{
tmp = tmp&(tmp-1);
count++;
}
这样做每次与运算都会让目标数的二进制位减少最低位的一个1。
为什么呢?
每次-1,即n-1,最靠后的1变为0,它后面的0全变为1,这时候从这一位开始往后的所有位都与n上对应位相反,比如
这时候n&(n-1)的话,dist前面的不变,后面的全变为0,得到的结果相较于n,最靠后的1就被消掉了。
此种方式,数据的二进制比特位中有几个1,循环就循环几次,而且中间采用了位运算,处理起来比较高效。
拓展:判断一个数是不是2的n次方
2 ^ 0 = 1 -> 1
2 ^ 1 = 2 -> 10
2 ^ 2 = 4 -> 100
……
我们发现2的n次方的二进制位上只有一个1,那么只要一个数的二进制位上去掉一个1后为0就说明它的二进制位上只有一个1,也就是2的n次方。
if(n & (n - 1) == 0)条件成立说明是2的n次方。
思路一:
把两个数各个二进制位都比较一下,用到了前面讲过的移位&1法
int count_diff_bit(int m, int n)
{
int count = 0;
int i = 0;
for (i = 0; i < 32; i++)
{
if (((m >> i) & 1) != ((n >> i) & 1))
{
count++;
}
}
return count;
}
思路二:
直接用异或比较每一个二进制位,相异为1,再统计结果的二进制位有几个1,用n&(n-1)法
int count_diff_bit(int m, int n)
{
int count = 0;
//^ 异或操作符
//相同为0,相异为1
int ret = m ^ n;
//统计一下ret中二进制位有几个1
while (ret)
{
ret = ret & (ret - 1);
count++;
}
return count;
}
其实是取出每一位的变式题,取出每一位就一位一位地右移,那取出奇数位或偶数位就两位两位地移,注意一下起点终点即可。这里从高位开始取出,如图
int main()
{
int i = 0;
int num = 0;
scanf("%d", &num);
//获取奇数位的数字
for (i = 30; i >= 0; i -= 2)
{
printf("%d ", (num >> i) & 1);
}
printf("\n");
//获取偶数位的数字
for (i = 31; i >= 1; i -= 2)
{
printf("%d ", (num >> i) & 1);
}
return 0;
}
赋值操作符就是将右边操作对象的值赋给左边的。
int a, x, y = 3;
a = 8;
x = a;
a = x = y+1;//连续赋值
//不推荐,因为可读性较差且不易调试。
int x = 10;
x = x + 10;
x += 10;//复合赋值,将两个表达式复合在一起
//其他运算符一样的道理,这样写更加简洁
2002 = val;
这样的语句在C语言中式无意义的,因为2002被称为右值,只是字面常量,而常量不可以被赋值,常量本身就是它的值。
实际上,赋值运算符的左侧必须引用一个存储位置,最简单的方法就是使用变量名(变量名标识特定内存空间),还可以使用指针(指向一个存储位置)。总的来说,C语言使用可修改的左值标记那些可赋值的实体。
! 逻辑反操作 对一个值逻辑取反,根据C语言非零为真零为假,比如!8=0
- 负值 对一个数取负数
+ 正值 实际上对一个数没什么影响,比如a = -10; b = +a;算出来b的值还是-10
& 取地址 取出一个变量的地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反 比如~65,65补码(省略前面的24位0):01000001,按位取反得到10111110,再转成原码得到11000010即-66
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
sizeof运算符以字节为单位返回运算对象的大小,返回值时无符号整数类型,运算对象可以是具体的数据对象(如变量名),也可以是类型(如int),如果是类型就必须用圆括号()括起来。
()里的操作数可以直接是一个变量,如:
int a = 10;
printf("%d\n", sizeof(a));
也可以是一个常数,如:
printf("%d\n", sizeof(10));
会根据该常数对应类型来计算内存大小。
还可以是类型关键字,如:
printf("%d\n", sizeof(int));
但要注意的是,这里计算的肯定不可能是int的内存,int只是规定的关键字,不是变量,压根没有存在内存中,那这里是什么意思呢?就是计算int类型的变量在内存所占用空间大小。
等会,不是说计算变量的内存大小吗,那为什么常数的也可以求?实际上是根据操作数的类型来计算该类型的变量所占内存空间大小,你放入一个常数比如10到sizeof()里,它会根据10对应数据类型int来计算int类型的变量大小。
printf("%d\n", sizeof a);//这样写行不行?
可以,结果不影响。
printf("%d\n", sizeof int);//这样写行不行?
不行,会出问题。
看看sizeof()和数组以及函数传参
#include
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)两个地方分别输出多少?
一个int四个字节,数组arr带有10个int就是40个字节,一个char一个字节,ch数组大小就是10个字节。
函数传参传数组名实际上传的是数组首元素地址,而函数形参看起来是数组,但编译器会把它看成对应类型的指针,这时候再使用sizeof计算的就是指针大小,32位系统下指针大小4个字节,64位系统下指针大小8个字节。
关于++和--,要分前置和后置的情况
后置的话先使用后自增
前置的话先自增后使用
这里讲的使用是作为表达式去使用
在数组中,比如arr[r++] = 10;,就是arr[r] = 10;r++;,而arr[++r] = 10;就是r++;arr[r] = 10;。
在函数调用中,比如add(r++);就是add(r);r++;,而add(++r);就是r++;add(r);。
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
关系表达式的值为1或0,当关系满足时,表达式值为真即1,关系不满足时,表达式值为假即0。
&& 逻辑与 两边操作数(运算对象)都为真表达式值才为真
|| 逻辑或 两边操作数(运算对象)有一个为真表达式值即为真
注意区分:
逻辑与:左右操作数都为真(非0),表达式的值才为真(为1),否则为假(为0)
按位与:左右操作数的二进制位补码一一进行“全为1则为1,否则为0”的操作
逻辑或:左右操作数有一个为真(非0),表达式的值就为真(为1),否则为假(为0)
按位或:左右操作数的二进制位补码一一进行“有一个为1则为1,否则为0”的操作
其实感觉比较相像,可以这么看,逻辑与和逻辑或是对于数值本身进行逻辑真假的判断,而按位与和按位或是对于数值的二进制位进行逻辑真假的判断,逻辑真假即非0与0。
程序输出的结果是什么?
#include
int main()
{
int i = 0;
int a=0, b=2, c =3, d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
其实上面故意少提了一点:
对于逻辑与和逻辑或,都是左结合性,逻辑与表达式要是第一个表达式值为0则整个表达式的值就为0,后面的表达式可以不用计算,相似地,逻辑或表达式要是第一个表达式值为1则整个表达式的值就为1,后面的表达式可以不用计算。
再回到上面的例题,注意a++是后置++,所以(a++)&&…相当于a&&…再a++,而a的值为0,为假,则不管++b是否为真,a++&&++b都为假,则无论d++是否为真,整个逻辑与表达式的值都为假,后面的++b和d++表达式就不会再计算了,所以只有a自增了1,程序输出结果就为1 2 3 4。
如果稍微修改一下这段代码结果会怎样呢?看看:
程序输出的结果是什么?
#include
int main()
{
int i = 0;
int a=1, b=2, c =3, d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
答案是2 3 3 5
因为这次a为非0,后面的++b和d++都被执行了。
那我们再来看看逻辑或表达式:
程序输出的结果是什么?
#include
int main()
{
int i = 0;
int a=1, b=2, c =3, d=4;
i = a++ || ++b || d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
注意a为非0,不管++b是否为真,a++||++b都为真,则无论d++是否为真,整个逻辑或表达式都为真,所以后面的表达式不会再计算,只有a自增了1,输出结果就是2 2 3 5。
我们再改改看看:
程序输出的结果是什么?
#include
int main()
{
int i = 0;
int a=0, b=2, c =3, d=4;
i = a++||++b || d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
注意a为0,而++b非0,则a++||++b为真,所以无论d++是否为真,整个逻辑或表达式都为真,d++也就不会计算了,故输出结果为1 3 3 4。
exp1 ? exp2 : exp3
如果exp1为真,则整个表达式的值为exp2的值,而exp3不执行;
如果exp1为假,则整个表达式的值为exp3的值,而exp2不执行。
要注意的就是不宜把表达式搞得太复杂,可以换成用if…else语句。
exp1, exp2, exp3, …expN
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
不过由于逗号的运算优先级是最低的,要使用逗号表达式要使用圆括号。
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);
//代码2
if (a =b + 1, c=a / 2, d > 0)//判断条件也可以是逗号表达式
不用圆括号括起来的话,很容易出错!
比如:
int d = ++a, b++, c++, a++;实际上是先执行赋值表达式(d = ++a),而不是将后面的逗号表达式都执行完取最后一个的值,那是有圆括号的情况,因为逗号运算符优先级最低啊。
操作数:一个数组名 + 一个索引值
比如:
arr[9]的操作数是arr和9
骚操作:9[arr] 等价于 arr[9]
我直呼好家伙 Σ(っ °Д °;)っ
为什么可以这样呢?
究其本质,arr[9]等价于*(arr + 9),实际上[ ]就是*( )操作数放在圆括号里相加,所以9[arr] 等价于 *(9+arr),而加法具有交换律,这两种表示方式的意义都一样。
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1();
test2("hello bit.");
return 0;
}
所以说函数调用其实也是表达式语句,操作符为()。
. 结构体.成员名
-> 结构体指针->成员名
#include
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;
stu.age = 20;//直接访问结构成员
pStu->age = 20;//间接访问结构成员
return 0;
}
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。当运算符共享一个运算对象时,优先级决定了求值顺序。
如果两者的优先级相同,则取决于他们的结合性。
但是大部分运算符都没有规定同优先级下的求值顺序。
实际上只有逻辑与,逻辑或,条件操作符和逗号操作符控制求值顺序,其他操作符没有控制求值顺序,这就有可能产生一些问题代码。
a*b + c*d + e*f
其实计算路径不唯一,有两种顺序:
代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行,因为算术操作符并没有控制求值顺序,所以就可能会有多条计算路径。
结合性只适用于共享同一运算对象的运算符,例如,在表达式12/3*2中,/和*优先级相同,共享运算对象3,因此从左往右的结合性起作用,表达式简化为4*2即8,但是如果从右往左计算的话会得到12/6即2,这种情况下计算的先后顺序会影响最终结果。
对于如y = 6 * 12 + 5 * 20;这样的语句,两个*运算符并没有共享同一个运算对象,因此左结合性不起作用,*优先级比+高,肯定先算乘法运算对吧,那到底先进行哪一个乘法呢?C标准未定义,实际上得根据不同硬件或编译环境来确定,只不过该例中先算哪个对结果没有影响而已。那万一有影响呢?
int c = 2;
int d = c + --c;
自增自减与算术操作符复合的时候很容易产生歧义,因为求值顺序不是唯一的,实际上这类是C标准未定义行为,结果有多种可能值,与编译器有关(相当于甩锅给编译器)。
编译器:我太难了……ಥ_ಥ
+左边c的值是什么时候确定的呢?是--c后吗?还是在--c之前呢?因为没有统一的标准,所以都有可能。
若是--c后确定左边c的值,则1+1=2,若是--c前确定左边c的值,则2+1=3。
操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这个就纯属恶心人的,估计能够到世界乱码大赛的门槛了(笑)。
有前辈测试过,发现在各个编译器上的值都大不相同。
这段代码堪称究极反面教材,提醒我们不要自作聪明玩一些花里胡哨的东西(恼),没有什么实际意义,本身就存在歧义,可读性就更别提了,无论是学习还是工作都要极力避免写出过于复杂的表达式。
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
在VS下是先一个一个地进入fun()后再进行算术运算的,所以变成了2 - 3 * 4 = -12 -10。
还可以这样,先分别调用后面两个fun()进行乘法运算后再调用前面的fun()再进行减法运算,就变成了4 - 2 * 3=-6 -2。
我们只能通过操作符的优先级得知:先算乘法,再算减法。
然而函数的调用先后顺序无法通过操作符的优先级确定
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
没错又是自增的锅。
可以先执行前两个++i再执行(++i)+(++i)得到2+3,再执行最后一个++i后执行2+3+(++i)得到9
还可以先把三个自增表达式执行完后再执行加法运算,即4+4+4=12。
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第 三个前置 ++ 的先后顺序。
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
为什么会有问题,因为计算顺序对表达式的值有影响,一般都和++或--脱不了干系,因为自增自减会改变变量的值,导致不清楚什么时候确认变量的值。
所以我们尽量不要写太复杂的表达式,尤其是带有自增或自减操作符的。
遵循以下规则,不要滥用自增或自减运算符
1.如果一个变量出现在一个函数的多个参数中,不要对其使用自增或自减运算符。
2.如果一个变量多次出现在一个表达式中,不要对其使用自增或自减运算符。
为什么计算路径不唯一?编译器识别表达式,是同时加载至寄存器还是分批加载完全不确定而导致的。
为什么不确定呢?因为对此没有明确的规定,这也涉及到序列点的问题。
比如对于int b = (++i)+(++i)+(++i);,(++i)并不是一个完整表达式,而是作为整个表达式的子表达式,整个赋值表达式语句才是一个完整表达式,分号标记了序列点,也就是在执行下一条语句之前肯定会完成表达式中的三次自增,只是以怎样的顺序进行并没有被明确规定,到底是先把三次自增都进行完(同时加载至寄存器)再进行加法运算,还是先把前两个自增完(分批加载)后相加,再进行后面的自增,然后几个数相加,或者其他顺序都有可能。
具体实现细节因为标准未定义,相当于一股脑甩给了编译器,让它自己决定(编译器:我**&%¥#@),不同编译器下的过程可谓五花八门了。
比如a+++10,在读到a+时,由于要读尽可能多的字符,所以会继续往后读也就是a++,此时若再往后读就无法组成有意义的符号了,所以a++是一个整体,原式相当于a++ + 10。
这个规则不保证表达式正确,只是“贪多”。
表达式由运算符和运算对象组成。最简单的表达式是一个单独的运算对象。
下面是一些表达式:
4
-6
a*(b + c / d) / 20
q > 3
p = 5 * 2
运算对象可以是常量、变量或二者的结合。
每一个表达式都有对应值
赋值表达式的值与运算符左边的值相同,也就是赋给的是什么表达式的值就是什么,如p = 6*7的值就是42。
关系表达式(如q>3)的值不是0就是1,关系成立就是1,不成立就是0。
语句是C程序的基本构建块,一条语句相当于一条完整的计算机指令。在C语言中,大部分语句都以分号结尾。
比如legs = 4没有分号就只是个表达式。
最简单的语句是空语句:
;(只有一个分号)
C把末尾加上一个分号的表达式都看作是一条语句(表达式语句)。
有些语句实际上不起作用(如3+4;),算不上真正的语句,语句应该是可以改变值或调用函数的。
不是所有的指令都是语句,如:
x = 6 + (y = 5);
y = 5是一条完整的指令,但它只是语句的一部分。
复合语句使用花括号括起来的一条或多条语句,也被称为块。
C的基本程序步骤由语句组成,而大多数语句都由表达式构成。