【C语言初阶(NEW)】五、操作符详解(一)

47 阅读18分钟

前言

        由于之前C语言写的博客很零散,写的也不够好,这里我打算重新整理关于C语言的博客,也方便自己复习C语言,C语言专栏看到旧版本的博客建议移步到新版本相关的博客上

 ----------------我是分割线---------------   

一、操作符分类

操作符可以分为一下几类: 

  1. 算术操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 单目操作符
  6. 关系操作符
  7. 逻辑操作符
  8. 条件操作符
  9. 逗号表达式
  10. 下标引用、函数调用和结构成员

二、算术操作符

算术操作符有:

+ - * / %
  1. 加减就不解释了 
  2. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数
  3. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法
  4. % 操作符的两个操作数必须为整数。返回的是整除之后的余数
  5. 这几个算术操作符都是由两个操作数的,比如 1 + 2 ,左操作数是 1,右操作数是 2

测试代码:

#include <stdio.h>
int main()
{
	//% 取模操作符的两端必须是整数
	int a = 7 % 2;//取余,结果是 1
	int b = 7 / 2;
	printf("%d\n", a);//1
	printf("%d\n", b);//3

	return 0;

 运行结果:

三、移位操作符

3.1 移位操作符简介

在C语言中,移位运算符有双目移位运算符:<<(左移)和>>(右移)

        左移运算是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补 0 

        右移运算是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补 0 ,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为  0 ,负数的符号位为 1 

下面且听详解!!!

3.2 原码、反码、补码

在学习移位操作符之前,首先要了解原码、反码、补码

 整数在内存中存储的形式是补码的二进制

整数的二进制表示:有3种(原码、反码、补码)(假设在 32 位的平台下)

原码:直接根据数值写出的二进制序列就是原码(32位)

反码:原码的符号位不变,其他位按位取反就是反码

补码:反码加1,就是补码

         对于正整数的原码、反码、补码都相同;负数是存放在二进制的补码中,负整数的原码、反码、补码都不相同

 例如:1(正整数的原码、反码、补码都相同)

原码:0000000 00000000 00000000 00000001
 
反码:0000000 00000000 00000000 00000001
 
补码:0000000 00000000 00000000 00000001

 最高位为0 ,也是符号位(最左边的第一位)

例如:-1(负整数的原码、反码、补码都不相同)

原码:10000000 00000000 00000000 00000001
 
反码:11111111 11111111 11111111 11111110(按位取反,符号位不变)
 
补码:11111111 11111111 11111111 11111111(反码加1)

 最高位为1,也是符号位(最左边的第一位)

简单了解到这里,可以继续学习移位操作符了

3.3 << 左移运算符

直接上代码(只演示负整数的,看完正整数的也会了,正整数的比较简单)

#include<stdio.h>
int main()
{
	int a = -5;
	int b = a << 1;
 
	printf("%d\n", a);
	printf("%d\n", b);
 
	return 0;
}

 运行结果是 -10

这是为什么呢,解释如下:

规则:左移运算是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0

简单说就是:左边丢弃,右边补0

先写出 -5 的补码

原码:10000000 00000000 00000000 00000101 (最高位为1)
 
反码:11111111 11111111 11111111 11111010 (按位取反,符号位不变)
 
补码:11111111 11111111 11111111 11111011 (反码加1)

 补码向左移动一位,左边去掉,右边补0,如图:

 此时得到的是补码,还要反推原码才能打印(正数原、反、补相同,补码就是原码,负数要反推回去)

补码:11111111 11111111 11111111 11110110
 
反码:11111111 11111111 11111111 11110101(补码 -1 得到反码)
 
原码:10000000 00000000 00000000 00001010(按位取反得到原码)

此时得到的原码就可以转换为十进制打印了,结果就是 -10

注意:%d 意味着打印一个有符号的整数

注:此时的 a 没有改变,依旧是 -5

3.4 >>右移运算符

直接上代码(只演示负整数的,看完正整数的也会了,正整数的比较简单)

#include<stdio.h>
int main()
{
	int a = -5;
	int b = a >> 1;
 
	printf("%d\n", a);
	printf("%d\n", b);
 
	return 0;
}

 运行结果是 -3

 解释:

        右移运算是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为 0 ,负数的符号位为 1 

简单说就是:(分为 2 种)

  1. 逻辑右移:左边用0填充,右边丢弃
  2. 算术右移:左边用原该值的符号位填充,右边丢弃

到底是逻辑右移还是算术右移取决于编译器

我当前使用的编译器 VS2019,它采用的是算术右移

还是一样,先写出 -5 的补码

原码:10000000 00000000 00000000 00000101 (最高位为1)
 
反码:11111111 11111111 11111111 11111010 (按位取反,符号位不变)
 
补码:11111111 11111111 11111111 11111011 (反码加1)

 补码向右移动一位,右边丢弃,左边补符号位,如图:

此时得到的是补码,还要反推原码才能打印 

补码:11111111 11111111 11111111 11111101
 
反码:11111111 11111111 11111111 11111100(补码 -1 得到反码)
 
原码:10000000 00000000 00000000 00000011(按位取反得到原码)

 此时得到的原码就可以转换为十进制打印了,结果就是 -3

注:此时的 a 没有改变,依旧是 -5,说明左移和右移操作符不改变原操作数 

3.5 警告

对于移位运算符,不要移动负数位,这个是标准未定义的;左移和右移运算符只适用于正数,不支持浮点数

例如:

int a = 5;
int b = a >> -1  //error
float = 1.11;
float d = f << 1;//不支持

四、位操作符

位操作符有:

& //按位与
| //按位或
^ //按位异或
//注:他们的操作数必须是整数

4.1 & 按位与

老样子,直接上代码

#include<stdio.h>
int main()
{
	int a = 5;
	int b = -2;
	int c = a & b;
	printf("%d\n", c);
 
	return 0;
}

 运行结果是:4

解释如下:

& 按位与规则:两个二进制操作数对应位同为 1 ,结果位才为 1 ,其余情况为 0  

也是先写出 5,-2 的补码

 5 的补码:00000000 00000000 00000000 00000101
 
-2 的原码:10000000 00000000 00000000 00000010
-2 的反码:11111111 11111111 11111111 11111101
-2 的补码:11111111 11111111 11111111 11111110

 按照规则,两个二进制操作数对应位同为 1 ,结果 位 才为 1 ,其余情况为 0 

​      5 的补码:00000000 00000000 00000000 00000101
     -2 的补码:11111111 11111111 11111111 11111110
 5 & -2 的补码:00000000 00000000 00000000 00000100

此时得到的是补码,还要反推原码才能打印

 5 &- 2 的原码:00000000 00000000 00000000 00000100 
 (正整数原、反、补相同)

此时得到的原码就可以转换为十进制打印了,结果就是 4

4.2 | 按位或

老样子,先上代码

#include<stdio.h>
int main()
{
	int a = 5;
	int b = -2;
	int c = a | b;
	printf("%d\n", c);
 
	return 0;
}

运行结果是:-1

解释如下:

| 按位或规则:两个二进制操作数对应位只要有一个为 1 ,结果 位 就为 1 ,其余情况为 0  

 也是先写出 5,-2 的补码

​ 5 的补码:00000000 00000000 00000000 00000101
 
-2 的原码:10000000 00000000 00000000 00000010
-2 的反码:11111111 11111111 11111111 11111101
-2 的补码:11111111 11111111 11111111 11111110

也是按照规则,两个二进制操作数对应位只要有一个为 1 ,结果 位 就为 1 ,其余情况为 0 

      5 的补码:00000000 00000000 00000000 00000101
     -2 的补码:11111111 11111111 11111111 11111110
 5 | -2 的补码:11111111 11111111 11111111 11111111

此时得到的是补码,还要反推原码才能打印

5 | -2 的补码:11111111 11111111 11111111 11111111
5 | -2 的反码:11111111 11111111 11111111 11111110
5 | -2 的原码:10000000 00000000 00000000 00000001

此时得到的原码就可以转换为十进制打印了,结果就是 -1

4.3 ^ 按位异或 

 老样子,先上代码

#include<stdio.h>
int main()
{
	int a = 5;
	int b = -2;
	int c = a ^ b;
	printf("%d\n", c);
 
	return 0;
}

运行结果是:-5

原因解释如下:

^ 按位异或 规则:两个二进制操作数对应 位 相同为 0 ,不同为 1 

  也是先写出 5,-2 的补码

​ 5 的补码:00000000 00000000 00000000 00000101
 
-2 的原码:10000000 00000000 00000000 00000010
-2 的反码:11111111 11111111 11111111 11111101
-2 的补码:11111111 11111111 11111111 11111110

也是按照规则来,两个二进制操作数对应 位 相同为 0 ,不同为 1

​      5 的补码:00000000 00000000 00000000 00000101
     -2 的补码:11111111 11111111 11111111 11111110
 5 ^ -2 的补码:11111111 11111111 11111111 11111011

此时得到的是补码,还要反推原码才能打印

5 ^ -2 的补码:11111111 11111111 11111111 11111011
5 ^ -2 的反码:11111111 11111111 11111111 11111010
5 ^ -2 的原码:10000000 00000000 00000000 00000101

此时得到的原码就可以转换为十进制打印了,结果就是 -5

  ----------------我是分割线---------------   

五、赋值操作符

赋值操作符为:=

赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值

比如: 

int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值

赋值操作符可以连续使用,比如:

int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值

但是这种代码不建议写,别人看了增加头发负担量

建议这么写:

x = y+1;
a = x;

赋值操作符也可以写成复合赋值符

  • +=
  • -=
  • *=
  • /=
  • %=
  • >>=
  • <<=
  • &=
  • |=
  • ^=

比如:

int x = 10;
x = x+10;
x += 10;//复合赋值,等价于 x = x+10
//其他运算符一样的道理,这样写更加简洁

  ----------------我是分割线---------------    

 六、单目操作符

6.1 单目操作符介绍

单目操作符意味着只有一个操作数 

!  逻辑反操作符
-  负值
+  正值
&  取地址
sizeof  操作数的类型长度(以字节为单位)
~  对一个数的二进制按位取反
--  前置、后置--
++  前置、后置++
*  间接访问操作符(解引用操作符)
(类型)  强制类型转换

6.2 ! 逻辑反操作符

int flag = 3;
//flag为真,进入if
if (flag)
{}

//flag为假,进入if
if(!flag)
{}

C语言中0表示假,非0表示真 

6.3 - +(负值、正值)

//int a = -10;//负号
int b = -a;//负负得正

int c = +1;//正号一般不写出来

6.4 ~ 取反

直接上代码

#include<stdio.h>
int main()
{
	int a = 5;
	int c = ~a;
	printf("%d\n", c);
 
	return 0;
}

运行结果是:-6

原因解释如下:

~ 取反规则:一个二进制操作数,对应位为 0 ,结果位为 1 ;对应位为 1 ,结果位为 0(作用是将每位二进制取反) 

先写出 5 的补码 

​ 5 的补码:00000000 00000000 00000000 00000101

 按照规则,对应位为 0 ,结果位为 1 ;对应位为1,结果位为 0

​ 5 的补码:00000000 00000000 00000000 00000101
     取反:11111111 11111111 11111111 11111010

此时得到的是补码,还要反推原码才能打印

取反后(补码):11111111 11111111 11111111 11111010
         反码:11111111 11111111 11111111 11111001
         原码:10000000 00000000 00000000 00000110

 此时得到的原码就可以转换为十进制打印了,结果就是 -6

6.5 -- 运算符和 ++ 运算符

-- 分前置--和后置--,++ 也分前置++和后置++

前置++,先++,后使用;后置++,先使用,再++

前置--,先--,后使用; 后置--,先使用,再--

 测试代码,-- 同样道理

#include<stdio.h>
int main()
{
	int a = 1, b = 1;
	//前置++,先++,后使用
	//后置++,先使用,再++

	printf("前置:%d\n", ++a);
	printf("后置:%d\n", b++);

	return 0;
}

运行结果 

6.6 & 取地址和 * 解引用

        & 取地址操作符用于取变量的起始地址;* 叫做间接访问操作符,也叫解引用操作符,一般都是用于解引用指针,拿到指针所指向的内容

测试代码:

#include<stdio.h>
int main()
{
	int a = 10;
	printf("%p\n", &a);//&取地址, %p是打印地址
	//取出的地址是变量的起始地址
	
	int* p = &a;//把 a的地址拿给指针p,指针p 可以间接使用a
	*p = 20;//pa的地址,*p就拿到了地址的内容,即 a的值10,指针p间接修改了a的内容
	printf("%d\n", a);

	return 0;
}

 运行结果

指针后面讲,这里简单了解

6.7 sizeof 操作符

        sizeof 操作符,可以求变量(类型)所占空间的大小,单位是字节

注意:sizeof是操作符,不是函数,strlen是库函数,是用来求字符串长度 

测试代码:

#include <stdio.h>
int main()
{
	int a = 1;
	int arr[10] = { 0 };
	printf("%d\n", sizeof(a));//可以计算变量的大小    4
	printf("%d\n", sizeof(arr));//可以计算数组的大小,4*10
	printf("%d\n", sizeof(int));//可以计算变量类型的大小 4
	printf("%d\n", sizeof(char));//可以计算变量类型的大小 1

	printf("%d\n", sizeof a);//这样写行不行?ok
	//printf("%d\n", sizeof int);//这样写行不行?error
	return 0;
}

运行结果

来看看 sizeof 和 数组的一个面试题

#include <stdio.h>
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)两个地方分别输出多少?

(2)(4)本质上是指针,指针在 32位平台下是 4

运行结果

6.8 (类型)  强制类型转换

 测试代码:

#include <stdio.h>
int main()
{
	int a = (int)3.14;//把 3.14强制类型转换成 int
	printf("%d\n", a);
	return 0;
}

运行结果 

 七、关系操作符

关系操作符有:

>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”

这些我们都是很熟悉,不再解释了,要注意一点:在编程的过程中 == 和 = 不小心写错,导致的错误

   ----------------我是分割线---------------    

八、 逻辑操作符

逻辑操作符有:

&& 逻辑与
|| 逻辑或

 区分逻辑与和按位与

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++;
	printf(" a = %d\n b = %d\n c = %d\n d = %d\n", a, b, c, d);
	return 0;
}

运行结果

说明:&& 左边为假,右边就不计算了 

继续看

#include <stdio.h>
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);
	return 0;
}

 运行结果

说明:|| 左边为真,右边就不计算了 

----------------我是分割线---------------    

九、条件操作符 

条件操作符也叫三目操作符

exp1 ? exp2 : exp3

 表达式1 为真则取 表达式2 的结果,表达式1 为假则取 表达式3 的结果

测试代码

#include <stdio.h>
int main()
{
	int a = 3;
	int b = 0;
	
	int max = (a > b ? a : b);
	printf("%d\n", max);

	/*if (a > 5)
		b = 3;
	else
		b = -3;*/
	//int max = (a > b ? a : b) 等价于上面的if语句

	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);//整个表达式的结果是最后一个表达式的结果
	printf("c=%d\n", c);
	return 0;
}

运行结果

再如:

//代码2
a = get_val();
count_val(a);
while (a > 0)
{
	//代码
	a = get_val();
	count_val(a);
}
如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a > 0)
{
	//代码
}

//代码3
if (a =b + 1, c=a / 2, d > 0)//判断条件为整个表达式的结果是最后一个表达式的结果

----------------我是分割线---------------     

十一、下标引用、函数调用和结构成员

11.1 [ ] 下标引用操作符

操作数:一个数组名 + 一个索引值

nt arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符
[ ]的两个操作数是arr9

//arr[9] --> *(arr+9) --> *(9+arr) --> 7[arr]
//arr[9] 也可以写成 9[arr],但是创建数组就不可以
//arr是数组首元素的地址
//arr+9就是跳过9个元素,指向了第10个元素
//*(arr+9) 就是第8个元素

11.2 ( ) 函数调用操作符

接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数

#include <stdio.h>
void test1()
{
	printf("hello\n");
}
void test2(char* str)
{
	printf("%s\n", str);
}
int main()
{
	test1(); //实用()作为函数调用操作符
	test2("hello");//实用()作为函数调用操作符
	return 0;
}

11.3 访问一个结构的成员

. 结构体.成员名
-> 结构体指针->成员名

直接看代码 

#include <stdio.h>
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};

void set_age1(struct Stu stu)
{
	stu.age = 18;
}

void set_age2(struct Stu* pStu)
{
	pStu->age = 18;//结构成员访问
}
int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;//结构成员访问
	stu.age = 20;// . 结构成员访问

	set_age1(stu);
	pStu->age = 20;// -> 结构成员访问
	set_age2(pStu);
	return 0;
}

----------------我是分割线---------------    

文章就到这里,下篇即将更新