目录
前言
操作符种类较多,也十分常用,因此,需要我们重点关注并区分操作符的功能和应用,不要混淆。
一、算数操作符
+ - * / %
加 减 乘 除 余
+加、-减、*乘操作符可以作用于整数和浮点数:
取余操作符%的两个操作数必须为整数,返回的是整除之后的余数。
/操作符,如果两个操作数都是整数,执行整数除法,若至少有一个浮点数,则执行浮点数除法。
#include<stdio.h>
int main()
{
int a = 7 / 2;
double b = 7 / 2;
int c = 7.0 / 2;
double d = 7 / 2;
int e = 7 % 2;
printf("a = %d\n", a);
printf("b = %lf\n", b);
printf("c = %d\n", c);
printf("d = %lf\n", d);
printf("e = %d\n", e);
return 0;
}
代码运行结果为:
a = 3;
b = 3.000000;//小数点后是6个0
c = 3;
d = 3.000000;//小数点后是6个0
e = 1;
二、移位操作符
左移 <<
右移 >>
移位操作符在之前没有没有提及,那么我们将通过代码展示来了解:
#include<stdio.h>
int main()
{
int n = 7 >> 1;
printf("%d", n);
return 0;
}
运行代码后,结果如下:
其中,要注意的是:
我们看代码,7向右移位1,得出的结果为3,那么移位操作符的原理是什么呢?
移位操作符移动的是二进制的位,移位操作符的操作数只能是整数。
我们需要理解:整数的二进制表示形式有3种:原码、反码、补码。
对于一个整型,是4个字节,32bit;
那么,一个整数写出二进制序列后,就是32个bit位;
对于有符号的整数来说,最高位是符号位,符号位是1表示负数,符号位是0表示正数;
对于无符号整数来说,没有符号位,所有位都是有效位。
2.1 原码
按照数值的正负,直接写出的二进制序列,就是原码。
10
原码:0000 0000 0000 0000 0000 0000 0000 1010
-10
原码:1000 0000 0000 0000 0000 0000 0000 1010
对于正的整数,原码、反码、补码相同,无需计算;对于负的整数,原码、反码、补码是需要计算的。
2.2 反码
原码的符号位不变,其他位,按位取反。
-10
原码:1000 0000 0000 0000 0000 0000 0000 1010
反码:1111 1111 1111 1111 1111 1111 1111 0101
2.3 补码
反码的二进制+1就是补码。
-10
原码:1000 0000 0000 0000 0000 0000 0000 1010
反码:1111 1111 1111 1111 1111 1111 1111 0101
补码:1111 1111 1111 1111 1111 1111 1111 0110
不管是正的整数,还是负的整数,内存中存储的都是补码,因此可以说,整数在内存中存储的都是补码的二进制序列。另外,整数在计算的时候也使用补码。
补码得到源码的途径:
1.先-1,再取反。
2.先取反,再+1。
2.4 左移操作符
移位规则:左边丢弃,右边补零。
基于上述了解,我们可以对代码做出进一步解释。
#include<stdio.h>
int main()
{
int m =7;
int n << 1; //左移操作符
// |0000 0000 0000 0000 0000 0000 0000 0111|
// 0|000 0000 0000 0000 0000 0000 0000 0111 |
// 左边的值直接丢弃,右边空出来一位直接补0 结果是14
printf("%d", m);//7
printf("%d", n);//14
return 0;
}
#include<stdio.h>
int main()
{
int m = -7;
// |1000 0000 0000 0000 0000 0000 0000 0111|
// |1111 1111 1111 1111 1111 1111 1111 1000|
// |1111 1111 1111 1111 1111 1111 1111 1001|
int n << 1;
// 1|111 1111 1111 1111 1111 1111 1111 1001 |
printf("%d\n", m);
printf("%d\n", n);
// |1111 1111 1111 1111 1111 1111 1111 0010|
// |1111 1111 1111 1111 1111 1111 1111 0001|
// |1000 0000 0000 0000 0000 0000 0000 1110|
return 0;
}
此外,我们也可以发现,不论正数还是负数,左移1位,均具有乘2的效果。
2.5 右移操作符
移位规则:
1. 逻辑右移:左边补零,右边丢弃。
2. 算数右移:左边用原该值的符号位进行填充,右边丢弃。
#include<stdio.h>
{
int m = -10;
// |1000 0000 0000 0000 0000 0000 0000 1010|
// |1111 1111 1111 1111 1111 1111 1111 0101|
// |1111 1111 1111 1111 1111 1111 1111 0110|
int n >> 1;
// | 111 1111 1111 1111 1111 1111 1111 1011|0
//
//对于逻辑右移:右边直接丢弃,左边补0;
//对于算数右移:右边丢弃,左边用原该值的符号位填充,即原来是负数,就拿1填充,原来是正数,就拿0填充。
printf("m = %d\n", m);
printf("n = %d\n", n);//VS编译器中采用算数右移
// |1111 1111 1111 1111 1111 1111 1111 1011|
// |1111 1111 1111 1111 1111 1111 1111 1010|
// |1000 0000 0000 0000 0000 0000 0000 0101|
return 0;
}
此外,逻辑右移具有除以2的效果。
值得注意的是:
对于移位运算符,不要移动负数位,这是标准未定义的。
int num = 10;
num >> -1;//err
三、位操作符
位操作符也是对整数进行操作的,位操作符包括:
& //按位与
| //按位或
^ //按位异或
3.1 按(二进制)位与操作符 “&”
与运算:对应2进制位,只要有0则为0,两个同时为1才是1。代码如下:
#include<stdio.h>
int main()
{
int a = 3;
// 0000 0000 0000 0000 0000 0000 0000 0011 3的补码
int b = -5;
// 1000 0000 0000 0000 0000 0000 0000 0101
// 1111 1111 1111 1111 1111 1111 1111 1010
// 1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
int c = a & b;
// 0000 0000 0000 0000 0000 0000 0000 0011 最高位是0,正数,所以补码和原码相同
printf("%d\n", c);//3
return 0;
}
另外,按位与还有一个特点,就是可以获取二进制整数的任意一个位。如得到3的最低位。
#include<stdio.h>
int main()
{
int a = 3;
int b = a & 1;
// 0000 0000 0000 0000 0000 0000 0000 0011 3的补码
// 0000 0000 0000 0000 0000 0000 0000 0001 1的补码
// 0000 0000 0000 0000 0000 0000 0000 0001 a & 1 = 1
printf("%d\n", b);//1
return 0;
}
因此,当我们想知道第n位是多少,只需先右移(n-1)位后,在按位与1。
3.2 按(二进制)位或操作符 “|”
或运算:对应二进制位上有1则为1。代码如下:
#include<stdio.h>
int main()
{
int a = 3;
// 0000 0000 0000 0000 0000 0000 0000 0011 3的补码
int b = -5;
// 1000 0000 0000 0000 0000 0000 0000 0101
// 1111 1111 1111 1111 1111 1111 1111 1010
// 1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
int c = a | b;
// 1111 1111 1111 1111 1111 1111 1111 1011 c的补码
// 1111 1111 1111 1111 1111 1111 1111 1010
// 1000 0000 0000 0000 0000 0000 0000 0101 原码-5
printf("%d\n", c);
return 0;
}
3.2 按(二进制)位异或操作符 “^”
异或运算:相同为假,相异为真。相同为0,相异为1。异或运算的操作数都是整型。
#include<stdio.h>
int main()
{
int a = 3;
// 0000 0000 0000 0000 0000 0000 0000 0011 3的补码
int b = -5;
// 1000 0000 0000 0000 0000 0000 0000 0101
// 1111 1111 1111 1111 1111 1111 1111 1010
// 1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
int c = a ^ b;
// 1111 1111 1111 1111 1111 1111 1111 1000
// 1000 0000 0000 0000 0000 0000 0000 0111 取反
// 1000 0000 0000 0000 0000 0000 0000 1000 +1
printf("%d\n", c);//-8
return 0;
}
基于异或运算,我们可以知道两个条件:
a ^ a = 0;
a ^ 0 = a;
根据上述条件,我们可以在不使用临时变量的前提下,实现两个数的交换。代码如下:
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
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;
}
四、赋值操作符
#include<stdio.h>
int main()
{
int a = 1;//不是赋值,是初始化
a = 20;//赋值
return 0;
}
除此以外,还有复合赋值操作符。
+= -= *= 、= %= >>= <<= &= |= ^=
五、单目操作符
5.1 单目操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
5.1.1 逻辑反操作 “!”
#include<stdio.h>
int main()
{
int flag = 0;//0为假
int c = !flag//1
if (!flag)
{
printf("hehe\n");
}
return 0;
}
5.1.2 取地址 “&”
#include<stdio.h>
int main()
{
int a = 0;
int * p = &a;//取地址
int arr[10];
int (*pa)[10] = &arr;//数组的地址,也叫数组指针
printf("%p\n", p);
return 0;
}
5.1.3 操作数的类型长度 “sizeof”和数组
- sizeof是在计算类型创建变量或者变量的大小,单位是字节
- sizeof计算的结果是size_t类型的
- size_t是无符号整型的
- 对size_t类型的数据进行打印,可以用%zd
- sizeof后面的括号在括号中写的不是类型的时候,括号可以省略,这就说明sizeof不是函数,是单目操作符
#include<stdio.h>
int main()
{
int a = 10;
printf("%zd\n", sizeof(a));//4//也可以写成printf("%zd\n", sizeof a);证明了sizeof是操作符而不是函数。因为函数后边的括号不能省略
printf("%zd\n", sizeof(int));//4
int arr[10] = { 0 };
printf("%zd\n", sizeof arr);//40
return 0;
}
#include<stdio.h>
void test1(int arr[])//本质是指针int* arr
{
printf("%d\n", sizeof(arr));
}
void test2(char ch[])//本质是指针char* ch
{
printf("%d\n", sizeof(ch));
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
test1(arr);
test2(ch);
return 0;
}
一个字符的地址和一个整型的地址都叫地址。是地址,在32位平台下是4个字节,在64位下是8个字节,而如果把地址存到指针变量中去,指针变量的大小也是4或8个字节。
所以,不要在门缝里看指针,把指针看扁了。
5.1.4 按位取反 “~”
对一个数的二进制按位取反(包括符号位和数值位)。
#include<stdio.h>
int main()
{
int a = 0;
printf("%d\n", ~a);
//0000 0000 0000 0000 0000 0000 0000 0000
//1111 1111 1111 1111 1111 1111 1111 1111 内存中的补码
//1000 0000 0000 0000 0000 0000 0000 0000
//1000 0000 0000 0000 0000 0000 0000 0001
//-1
return 0;
}
以a=10为例,二进制表示为:0000 0000 0000 0000 0000 0000 0000 1010,写一个代码把从左向右第5位取反,即0变成1,其他位不变。然后再变回10。
思路:给第5位的0或上一个1。要求其他位不变,则第4、3、2、1位的1或上一个0,因此,或上“0000 0000 0000 0000 0000 0000 0001 0000”这个数字。再变回来,需要第5位按位与0,其他位按位与1。
#include<stdio.h>
int main()
{
int a = 10;
//0000 0000 0000 0000 0000 0000 0000 1010
//0000 0000 0000 0000 0000 0000 0001 0000 //1<<4
a |= (1<<4);
//0000 0000 0000 0000 0000 0000 0001 1010
//1111 1111 1111 1111 1111 1111 1110 1111
//相当于
//0000 0000 0000 0000 0000 0000 0001 0000按位取反
a &= (~(1 << 4));
printf("%d\n", a);
return 0;
}
六、关系操作符
主要用来测试关系操作符两边对象的大小关系。
< > <= >= != ==
值得注意的是,在编程的过程中关系操作符“==”和赋值操作符“=”不小心写错,导致的错误。
七、逻辑操作符
&& 逻辑与
|| 逻辑或
这里,我们要区分好逻辑与(&&)和按位与(&),逻辑或(||)和按位或(|)。逻辑操作符不是对二进制位进行计算。其具有短路操作,即:
&& -- 左边操作数如果为假,右边无需计算
| | -- 左边操作数如果为真,右边无需计算
区分代码如下:
#include<stdio.h>
int main()
{
int a = 1 & 2;//0
int b = 1 && 2;//1
int c = 1 | 2;//3
int d = 1 || 2;//1
return 0;
}
我们通过代码进一步了解逻辑操作符。
#include<stdio.h>
例1:
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\n d = %d\n", a, b, c, d);
printf("%d\n", i);
return 0;
}
例2:
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
printf("a = %d\n b = %d\n c = %d\n d = %d\n", a, b, c, d);
printf("%d\n", i);
return 0;
}
代码结果如下:
例1:
a=1
b=2
c=3
d=4
i=0
例2:
a=1
b=3
c=3
d=4
i=1
分析:
例1:
- 赋值运算符的优先级最低。
- a++,后置++,先使用再+1,先使用为0,表示假。
- 根据短路操作,对于&&来说,遇到一个假,后边的表达式++b、d++就不运算了。
- 所以,i=0,a后自增1。
例2:
- 赋值运算符的优先级最低。
- a++,后置++,先使用再+1,先使用为0,表示假。
- 未触发短路操作,逻辑或继续向后计算,++b,前置++,先自增1,后使用。
- 此时,b=3,在逻辑或中,根据短路操作,看到不是0,必为真,后边就不用算了。
- a自增1,b也自增1,所以i=1。
八、条件操作符
exp1 ? exp2 : exp3
真 计算 不算
假 不算 计算
写一个代码,要求输出两个数中较大的一个:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int m = 0;
if (a < b)
{
m = a;
}
else
{
m = b;
}
printf("%d\n", m);
return 0;
}
结合条件操作符,我们可以对上述代码进行化简:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int m = 0;
m = (a > b ? a : b);
printf("%d\n", m);
return 0;
}
九、逗号表达式
exp1,exp2,exp3,...,expN
用逗号隔开的多个表达式
从左到右依次执行,整个表达式的结果是最后一个表达式的结果
我们通过代码对逗号表达式做进一步了解:
#include<stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
//0 12 12 13
printf("%d\n", c);//13
return 0;
}
我们再来看这个代码:
a = get_val();
count_val(a);
while (a > 0)
{
a = get_val();
count_val(a);
}
使用逗号表达式,还可以得到如下效果:
while (a = get_val(), count_val(a), a > 0)
{
;
}
十、下标引用、函数调用和结构成员访问
10.1 下标引用操作符 “[]”
操作数:数组名+索引值
代码如下:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[5]);//[]下标引用操作符,arr和5分别是左右操作数
return 0;
}
10.2 函数调用操作符 “()”
接收一个或多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
对于函数调用操作符,最少要有1个操作数,因为函数调用时候可以不传参,但不能没有函数名。
代码如下:
#include<stdio.h>
int main()
{
printf("hehe\n");//()函数调用操作符,printf、hehe\n是两个操作数
return 0;
}
10.3 访问一个结构的成员
. 结构体.成员名
-> 结构体->成员名
代码如下:
/3include<stdio.h>
struct Book
{
char name[20];
int price;
};
//拿到结构体变量: 结构体变量.成员
//拿到结构体指针: 结构体指针->成员
void Print(struct Book* pb)//拿结构体的指针来接收
{
printf("%s %d\n", (*pb).name, (*pb).price);//当传过去地址,再通过解引用,有点复杂了
printf("%s %d\n", pb->name, pb->price);//如果是结构体指针,那么就可以通过->找到所指向对象的成员
}
int main()
{
struct Book b = { "C语言指南",65 };
printf("%s %d\n", b.name, b.price);
Print(&b);
return 0;
}