文章目录
- 前言
- 一、算术操作符
- 二、移位操作符
- 三、位操作符
- 四、赋值操作符
- 五、单目操作符
- 六、关系操作符
- 七、逻辑操作符
- 八、条件操作符
- 九、逗号表达式
- 十、下标引用、函数调用和结构成员
- 十一、操作符的优先级
前言
在之前学习的过程中,有大致了解过一些操作符,但还有一些操作符没见过,所以这篇文章将会把操作符都过一遍。本文章所有代码依赖的平台:(Visual Studio2017 && Windows)
一、算术操作符
+ - * / %
算术操作符这里加减乘除就不过多的了解了
这里需要注意的有2点:
a. / 和 %的区别
#include<stdio.h> int main() { int a1 = 3 / 5;//取商 int a2 = 3 % 5;//取余 printf("a1 = %d\na2 = %d\n", a1, a2);//a1 = 0; a2 = 3; return 0; } //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ int main02() { int a = 5 / 3.0;//ok //int a = 5 % 3.0;//err return 0; }
b. 小数除法
#include<stdio.h> int main() { //观察a1、a2、a3的结果 float a1 = 3.0 / 5; float a2 = 3 / 5.0; float a3 = 3.0 / 5.0; printf("a1 = %.1f\na2 = %.1f\na3 = %.1f\n", a1, a2, a3);//a1,a2,a3 = 0.6 return 0; } //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //当执行上面上面这段程序时 - warning:从double到float截断 //因为直接写6.0(没有定义的情况)这种小数时,编译器会默认为double类型 //更适合的写法: //float a1 = 3.0f / 5f; //double a2 = 3.0 / 5;
小结:
- / 和 % 都是执行除法运算,但是 / 算的是商,%算的是余数
- 除了 % 操作符之外 ,其它的操作符可以用于整数和浮点数;% 操作符的2个操作数必须为整数
- / 操作符如果2个操作数都为整数,执行整数除法;而只要有任何1个操作数为float类型或doble类型,执行的是小数除法
二、移位操作符
<< >>
<<:左移操作符
#include<stdio.h> int main() { int a = 2; //把a的二进制位向左移动1位 int b = a << 1; printf("b = %d\n", b);//4 //注意在对a进行移位后,并不会改变a本身 printf("a = %d\n", a);//2 return 0; }
`>>:右移操作符:
1.算术右移:右边丢弃,左边补原符号位
2.逻辑右移:右边丢弃,左边补0
/***********************************************************************
目的:验证当前机器是算术右移还是逻辑右移
分析:在右移操作符下,正数的算术右移和逻辑右移是一样的。所以这里采用负数来验证。(对于移位操作时,移动的是二进制补码:而正数的原反补相同,移动原码 = 移动补码;而负数需要转换为补码再移动)
平台:Visual studio 2017 && windows
*************************************************************************/
整数的二进制表示形式:
原码:直接根据数值写出的二进制序列就是原码(相当于屏幕上能看到的数)
反码:原码的符号位不变,其它位按位取反
补码:反码+1 (内存中的存储形式)
#include<stdio.h> int main() { int a = -1; //把a的二进制位向右移动1位 //由些可见,当前编译器采用的是算术右移 int b = a >> 1; printf("b = %d\n", b);//-1 return 0; }
注意:
int a = 10;
int b = a << -5; // err,这是标准未定义的语法。属于垃圾代码
小结:
- 移位操作符移动的是二进制位
- 左移操作符遵循左边丢弃,右边补0的原则
- 右移操作符则有2个情况:1.算术右移(右边丢弃,左边补原符号位) 2.逻辑右移(右边丢弃,左边补0)
经验证当前机器使用算术右移
三、位操作符
& | ^
#include<stdio.h> //&:按(二进制位)位与 -> 对应的二进制位有1个或2个为0,则为0;否则为1 int main() { int a = 3; int b = 5; //...011 -> 3 //...101 -> 5 //...001 -> 1 int c = a & b; printf("c = %d\n", c);//1 return 0; } //|:按(二进制位)位或-> 对应的二进制位有1个或2个为1,则为1;否则为0 int main02() { int a = 3; int b = 5; //...011 -> 3 //...101 -> 5 //...111 -> 7 int c = a | b; printf("c = %d\n", c);//7 return 0; } //^:按(二进制)位异或 -> 对应的二进制位,相同为0,相异为1 int main03() { int a = 3; int b = 5; //...011 -> 3 //...101 -> 5 //...110 -> 6 int c = a ^ b; printf("c = %d\n", c);//6 return 0; }
小结:
- &:按(二进制位)位与:对应的二进制位,同真为真,其余为假
- | :按(二进制位)位或:对应的二进制位,同假为假,其余为真
- ^:按(二进制)位异或 :对应的二进制位,相同为0,相异为1
- 它们的操作数必须为整数
四、赋值操作符
= += -= *= /= %= >>= <<= &= |= ^=
1.连续赋值
int main() { int a = 10; int x = 0; int y = 20; //a = x = y + 1;//连续赋值语法支持,但不建议这样写 //这样写可读性更高 x = y + 1; a = x; return 0; }
2.复合赋值,这里就介绍1个,其它的复合赋值都是相同的用法
int main() { int a = 10; a = 100; //a = a + 100; a += 100;//复合赋值 -> 同a = a + 100 return 0; }
3.注意:=是赋值操作符
==等于
五、单目操作符
! - + & sizeof ~ – ++ * (类型)
注:单目操作符只有一个操作数
#include<stdio.h> //!:非,即真为假,假为真 int main() { int flag = 5; if(flag) printf("hehe\n"); if(!flag) printf("haha\n"); return 0; } //----------------------------------------------------------------------------- //-:负 int main02() { int a = 10; a = -a; printf("%d\n", a);//-10 return 0; } //----------------------------------------------------------------------- //sizeof:计算变量所占内存的大小(单位:字节) int main03() { int a = 10; //sizeof的三种形式: printf("%d\n", sizeof(a)); printf("%d\n", sizeof(int)); printf("%d\n", sizeof a);//有的人会认为sizeof是一个函数:函数名()。这种写法说明了sizeof是一个操作符 //printf("%d\n", sizeof int);//err return 0; } //sizeof计算数组大小 int main04() { int arr[10] = {0}; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof arr); printf("%d\n", sizeof(int [10]));//sizeof(数据类型);对于数组的类型把数组名去掉,剩下的就是数据类型 return 0; } //观察下面程序,分析结果? int main05() { short s = 5; int a = 10; printf("%d\n", sizeof(s = a + 2));//2 printf("%d\n", s);//5 return 0; //sizeof括号中放的表达式是不参与运算的 } //----------------------------------------------------------------------- //~:按(二进制位)位取反 int main06() { int a = -1; //10000000 00000000 00000000 00000001 - 原 //11111111 11111111 11111111 11111110 - 反 //11111111 11111111 11111111 11111111 - 补 //按位取反(包括符号位): //00000000 00000000 00000000 00000000 int b = ~a; printf("%d\n", b);//0 printf("%d\n", a);//-1 - 并不会改变a return 0; } //++:自增 int main07() { int a1 = 10; int b1 = a1++;//后置++,先使用,再++ printf("a1 = %d\n", a1);//11 printf("b1 = %d\n", b1);//10 //++++++++++++++++++++++++++++++++++++++++++++ int a2 = 10; int b2 = ++a2;//前置++,先++,后使用 printf("a2 = %d\n", a2);//11 printf("b2 = %d\n", b2);//11 return 0; //++++++++++++++++++++++++++++++++++++++++++++++ //--也是如此 int a3 = 10; printf("%d\n", a3--);//10 printf("%d\n", a3);//9 } //+++++++++++++++++++++++++++++++++++++++++++++++++++ //对于自增或自减,可能会见到很多一些没有意义的代码,甚至是错误的: int main08() { int a = 1; int b = (++a) + (++a) + (++a); printf("%d\n", b); return 0; //结果(已验) //在VS2017下结果是12 //在Linux_Ubuntu下结果是10 } //-------------------------------------------------------- //&:取地址 //*:指针 int main09() { int a = 10; printf("%p\n", &a);//取a的地址以十六进制打印 int* pa = &a;//将a的地址存放于pa,pa就是一个指针变量,pa的类型就是int* *pa = 20;//这里的* - 解引用操作符或间接访问操作符;这里就是通过pa存的地址找到a,并把a赋值20 printf("%d\n", 20);//20; return 0; //这里注意区别&:按位与;&:取地址。当&有2个操作数时,它就是按位与;当&仅有1个操作数时,它就代表取地址 } //-------------------------------------------------------- //(类型):强制类型转换 int main10() { int a1 = 3.14;//warning:从double类型转换到int可能会丢失数据 int a2 = (int)3.14;//将double类型的数据强转为int return 0; }
六、关系操作符
> >= < <= != ==
对于关系操作符这里没啥可讲的,相对简单。值的注意的是:
1.==是判断相等;而=是赋值
2.==不能比较2个字符串相等
七、逻辑操作符
&& ||
#include<stdio.h> int main() { int a = 3; int b = 0; if(a && b)//同真为真,其余为假。这里只要有1个为假,则为假 { printf("hehe\n"); } if(a || b)//同假为假,其余为真。这里只要有1个为真,则为真 { printf("haha\n"); } return 0; }
八、条件操作符
exp1 ? exp2 :exp3
注:也被称为三目操作符
#include<stdio.h> int main() { int a = 3; int b = 0; if(a > 5) b = 1; else b = -1; //上面的if...else语句使用三目操作符只要一句代码: //三目操作符:表达式1的结果为真,则执行表达式2,否则执行表达式3 b = (a > 5 ? 1 : -1); return 0; }
九、逗号表达式
exp1, exp2, exp3, … expN
#include<stdio.h> int main() { int a = 3; int b = 5; int c = 0; //逗号表达式:从左向右依次计算,但是整个表达式的结果是最后一个表达式的结果,而最后一个表达式的结果可能会受到前面表达式的影响 int d = (c = 5, a = c + 3, b = a - 4, c += 5); printf("%d\n", d);//10 return 0; }
十、下标引用、函数调用和结构成员
[ ] () . ->
#include<stdio.h> //[]:下标引用操作符,有两个参数:第一个是数组名,第二个是元素大小或索引值 int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10};//这里的[]里面是元素个数 printf("%d\n", arr[4]);//数组名[索引值] -> 可以访问数组元素 return 0; } //-------------------------------------------------------- //():函数调用操作符,接受一个或多个操作数:第一个操作数是函数名,剩余的操作数是传递给函数的参数 //定义函数 int Add(int x, int y) { return x + y; } int main01() { int a = 10; int b = 20; //调用函数 int ret = Add(a, b);//这里的()就是函数调用操作符 printf("%d\n", ret); return 0; } //-------------------------------------------------------- //.: //-> //这里先介绍结构体:struct: //定义结构体类型 struct Book { char name[20]; char id[20]; int price; }; int main02() { //对于生活中的一些事物,我们要怎样描述它的信息:(这些信息又是不同的类型的,有字符串,整型等等。这时C语言提供了struct结构体关键字,它可以囊括不同的数据类型) //书:书名、书号、作者、定价、书号 //人:名字、年龄、性别 //初始化结构体:通过之前的了解我们知道初始化变量:int a = 10 -> 类型 + 变量 = 值 -> 所以这里结构体的类型就是struct Book struct Book b = {"C语言", "cc202005011", "55"}; //打印结构体成员 //1.使用.来访问结构体成员 printf("书名:%s\n", b.name);//通过(变量+.+成员名)可以访问结构体成员 printf("书号:%s\n", b.id); printf("定价:%d\n", b.price); printf("-------------分割线-------------\n"); //2.使用*和.来访问结构体成员 struct Book* pb = &b;//存储结构体的地址 printf("书名:%s\n", (*pb).name); printf("书号:%s\n", (*pb).id); printf("定价:%d\n", (*pb).price); printf("-------------分割线-------------\n"); //3.使用->来访问结构体成员 printf("书名:%s\n", pb -> name);//通过(指针变量 -> 成员名)可以访问结构体成员 printf("书号:%s\n", pb -> id); printf("定价:%d\n", pb -> price); return 0; }
十一、操作符的优先级
在程序设计中也是有操作符的优先级的,同加减乘除,优先级高的先算。
影响表达式求值的有三个因素:
1.操作符的优先级
2.操作符的结合性(优先级相同的情况下,结合性决定了运算顺序)
3.是否控制求值顺序(比如&&,如果左边为假,则整体为假)
以下整理了C语言中操作符的优先级:
注:N\A是无结合性
L-R是从左向右
R-L是从右向左
1、一些有问题的表达式
此类表达式没有办法确定唯一的计算路径,未来应该避免这类的表达式
a * b + c * d + e * f;
这个表达式执行的顺序可能就有2种:
c + --c;
比如:c = 3时:
同上,操作符的优先级只能决定–的运算在+的运算的前面,但是我们不知道,+操作符的左操作数的获取是在右操作数的之前还是之后的值,所以是有歧义的。
此代码来自《C和指针》
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;//注意count通过static修饰后,下一次进来时并不会销毁
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);
return 0;
}
在VS2017下的结果为2 - 3 * 4 = -10
3、… …
对于fun()函数的调用有可能会有不同的先后调用顺序
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
}
在VS2017中是12 -> 4 + 4 + 4:
在Linux下是10 -> 3 + 3 + 4:
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那个就是存在问题的。
十二、趁热打铁
1、交换变量,不能使用第3个变量(出自品茗股份c++笔试题)
/***********************************************************************
目的:不能创建临时变量(第3个变量),实现2个数的交换
分析:1.借助2数之和与变量的运算:
2.使用异或:
平台:Visual studio 2017 && windows
*************************************************************************/
实现代码1:
#include<stdio.h> int main() { int a = 3; int b = 5; printf("a = %d b = %d\n", a, b);//3 5 a = a + b; b = a - b; a = a - b; printf("a = %d b = %d\n", a, b);//5 3 return 0; }
这种写法是有问题的:当这2个数足够大时,相加可能会造成数值溢出
实现代码2:
#include<stdio.h> int main() { int a = 3; int b = 5; printf(a = %d b = %d\n", a, b);//3 5 a = a ^ b;//以下的括号没有任何意义 b = a ^ b;//(a ^ b) ^ b -> a a = a ^ b;//(a ^ b) ^ ((a ^ b) ^ b) -> b printf(a = %d b = %d\n", a, b);//5 3 return 0; }
2、二进制中的1
/***********************************************************************
目的:求一个整数存储在内存中的二进制中1的个数
分析:1.利用十进制转二进制的方法(除2反序取余法),如果余数等于1,就统计
2.让这个数的二进制位不断和1的二进制位按位&与(总共&32次,>>31次),如果得到1则统计
3.观察以下规律:
平台:Visual studio 2017 && windows
***********************************************************************/
实现代码1:
#include<stdio.h> int main() { int a = 13;//要求的数 int temp = a;//拷贝一份 int count = 0;//计数 //...00001101 while(temp) { if(temp % 2 == 1) { count++; } temp = temp / 2; } printf("%d存储在内存中的二进制中1的个数是%d\n", a, count); return 0; }
这个代码也是有局限性的 —— 只能计算正数
实现代码2:
#include<stdio.h> int main() { int i = 0; int count = 0;//计数器 int a = 13;//要求的数 int temp = a;//拷贝一份 //...00001101 for(i = 0; i < 32; i++) { /* if(a & 1 == 1) { count++; } temp = temp >> 1; */ //或者: if (1 == ((a >> i) & 1)) count++; } printf("%d存储在内存中的二进制中1的个数是%d\n", a, count); return 0; }
实现代码3:
#include<stdio.h> int main() { int i = 0; int count = 0;//计数器 int a = 13;//要求的数 int temp = a;//拷贝一份 //...00001101 while(temp) { count++; temp = temp & (temp - 1); } printf("%d存储在内存中的二进制中1的个数是%d\n", a, count); return 0; }
3、1置0,0置1
/***********************************************************************
目的:将一个整数存储在内存中的二进制中的某一个1置为0,某一个0置为1
分析:将某一个二进制位0置1,借助左移操作符,然后将这个位或上1即可;
将某一个二进制位1置0:
如…00001010 -> 00000010 将这个数与上11110111即可
需要注意的是将这个数所要变换的位与上0,其它位与1不变。而这个数通过左移操作符和按位取反得到
平台:Visual studio 2017 && windows
***********************************************************************/
实现代码:
#include<stdio.h> int main() { int a = 13;//要操作的数 //...00001101 //1.把a的二进制中的第5位置为1(从右至左) a = a | (1 << 4); printf("a = %d\n", a);//29 //2.把a的二进制中的第5位置为0 //...00011101 //& //...11101111 -> 这个数如何得到? //...00001101 a = a & ~ (1 << 4); printf("a = %d\n", a);//13 return 0; }
4、判断以下代码的输出结果(出自360的笔试题)
#include<stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);//1 2 3 4 return 0; }
分析:a++ && ++b && d++;这里的操作符是&& -> 同真为真,其余为假。此时这里的a又是后置++,所以a为0。对于&&操作符,只要前面为假,后面则都为假,所以不会执行
#include<stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ || ++b || d++; printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);//2 3 3 4 return 0; }
分析:a++ || ++b || d++;这里的操作符是|| -> 同假为假,其余为真。此时a为0但后面的b为3,所以为真。对于||操作符,只要前面为真,后面则都为真,所以不会执行
总结:熟悉了解 && 和 || 的习性
&& 只要有一个为假,后面则为假,并且不会执行
|| 只要有一个为真,后面则为真,并且不会执行