操作符
一、操作符的分类
1.算术操作符
2.移位操作符
3.位操作符
4.赋值操作符
5.单目操作符
6.关系操作符
7.逻辑操作符
8.条件操作符
9.逗号表达式
10.下标引用、函数调用和结构成员
二、详解操作符
1.算术操作符
+ - * / %
前面三个操作符相信大家都已经非常清楚 这里就不过多描述了。
1)/
接下来先详细介绍下 / 这个操作符
这个操作符有除的意思(从下面这个代码是不难看出的)
除法有两种除法
1.整数除法 (除号两端都是整数)
2.浮点数除法(除号两端至少有一个浮点数类型)
关于浮点数除法有个比较重要的点就是:它在于怎么算,而不是在于怎么存。
(除号两端没有一个浮点数)
(除号两端有浮点数)
ps:1.浮点数储存在整形里面 会被截断,只保留整数部分。
2.除数不可以为0。
2)%
取模只能用于整形类型,取模符号左右都得是整数
从下面这个代码不难看出 如果两边任意一边出现了浮点数 程序就会报错。
3)练习1
讲一串数字按倒序打印出来(例:输入1234 输出4 3 2 1)
上面就很好利用了 /和%这俩操作符的特点。
4)练习2
将一串数字按顺序打印出来(例:输入1234 输出1 2 3 4)
#include<stdio.h>
void test(int b)
{
if(b >= 10)
{
test(b / 10);
printf("%d ", b % 10);
}
else if(b < 10)
{
printf("%d ",b);
}
}
int main()
{
int a = 0;
scanf("%d", &a);
test(a);
return 0;
}
结果:
这个问题也算是/ %的扩展应用 这个地方不止运用了操作符的特点 还利用了递归的方法(与递归有关的代码 最好用图文的形式表达出来 能更好的理解,有更清晰的认识)
2.移位操作符
<< >>
1)注意:
在讲移位操作符之前 需要注意的 移位操作符只能对整数使用。 而且记住移位只是操作 需要储存下来。
ps:1字节=8byte
2)二进制
整数的二进制形式有三种;
原码
反码
补码
1.正数的原码 反码 补码都是相同的
2.负整数的原码 反码 补码是要计算的
首先不管是正整数还是负整数都可以直接根据其数值写出相关的二进制的原码
计算机内存中储存的是变量的二进制形式的补码
3)左移操作符 (<<)
移位的规则是:左边抛弃、右边补0
4)右移操作符 (>>)
右移操作符分为两种:
- 逻辑移位
左边用0填充,右边丢弃 - 算术移位
左边用原该值的符号位填充,右边丢弃
如果是算术右移 那么结果就还是-1 如果是逻辑右移结果就变成了127了
而从结果可以明显看出这是算术右移(C语言虽然没有明确规定 但是一般编译器采用的是算术右移)
ps:对于移位运算符,不要移动负数位,这个是标准未定义的。 a = a<< -1这种就是错误的!!!
3. 位操作符
1)常见位操作符
& 按位与
| 按位或
^ 按位异或
ps:他们的操作数必须是整数。
2) &
规则:对应二进制(补码) 有0则为0 两个同时为1 才是1
例子:
3) |
规则:对应二进制(补码) 有1则为1 两个同时为0 才是0
例子:
4) ^
规则:对应二进制(补码) 相同为0 相异为1
例子:
5) 练习1
题目:不能创建临时变量(第三个变量),实现两个数的交换
方法一: 加减法的实现
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
a = a + b;
b = a - b;
a = a - b;
printf("%d %d",a ,b);
}
运行结果:
而这一个方法有很大的弊端:那就是如果a和b都很大 那么a = a+b这个操作 可能会导致a超出整形最大范围
方法二:利用位操作符
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("%d %d", a, b);
}
运行结果:
开始讲解为什么可以实现:
首先我们可以通过计算了解到 任何数 和 0相异 都还等于它原本这个数
例如:
然后位操作符 实际上是符合乘法分配律的
例如:
那么现在了解了 按位异或操作符的两个规律之后 再回头看这题就会变的很简单
ps:实际表达式里面的a b都是最开始的变量a b。
6) 练习2
4.赋值操作符
1)使用方法
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
而且可以连续赋值
例如:
ps:虽然可以连续赋值 但是一般都不使用 因为这样的代码的可读性太低了
2)复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
例如:
int x = 10;
x += 5;
//实际就是下面这个代码:
x = x + 5;
//这样写是为了更加简洁
5. 单目操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
ps: 单目操作符的意思是 操作数只有一个; 双目操作符的话也是顾名思义 操作数有俩
1) !
! 代表逻辑取反,即把所有非0的数值(相当于1)变为0,0变为1
例1:
例2:
2) & 和 *
& 作用:是获取当前变量的内存地址
*作用:解引用操作符(间接访问操作符)->通过指针中存放的地址,找到指向的空间(内容)
例如:
这里也讲到了 指针 这里只是供大家了解一下,后续文章中 会专门出一节关于指针的内容
ps: pa里面存的是a的地址 所以 不要忘记对a取地址!
错误案例1(如果忘记对a取地址就会出现这种情况):
错误案例2(如果输出控制符不是%p 写成%d 他就会把地址这个二进制转化为10进制 并且打印出来):
可以理解为
*p好像表示的是一个指针
&p表示的是一个地址
3)sizeof
sizeof的作用: 计算出变量(类型)的所占空间大小
例子:
#include<stdio.h>
int main()
{
int a = 0;
int ab = 1;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a);//这样写行不行?
printf("%d\n", sizeof int);//这样写行不行?
printf("%d\n", sizeof ab);
return 0;
}
结果:(可以明显看出这样写 是会报错的)
所以从上面这个代码可以看出 sizeof后面要是没括号的情况下 后面只能跟变量名 不能跟数据类型!
sizeof 和 数组
例:
#include <stdio.h>
void test1(int arr[])
{
printf("(2)%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("(4)%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("(1)%d\n", sizeof(arr));//(1)
printf("(3)%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
运行结果:
接下来就一个一个解释为什么。
(1)和(3)是 40和10的原因是 这俩都是数组 并且分别是 整形 和 字符 类型数组,一个元素占 4 和 1个字节 又都是 10个元素 这个数组就占40和10字节
至于(2)和(4)为什么不是 40和10呢 是因为 在传参的时候 传过去的其实是数组的首元素地址 ,是以指针形式传过去的 而指针大小取决于操作系统的位数 如果是64位那么结果就是8 如果是32位系统 那么结果就和上面一样都是4
扩:
去掉名字就是类型 所以写成上面这种 int[10] char[10]类型也是完全可以的
4)~
~作用:按补码二进制位取反
例子:
5)前后置++ 和- -
前置++ / - -
前置++/- -会在语句执行前 就+/-
#include <stdio.h>
int main()
{
int a = 3;
int b = 0;
int c = 0;
b = ++a; //在执行赋值前 先对a进行了+1 此时a为4进行赋值
c = --a; // 在a为4的前提下 在赋值前 对a进行了-1 所以a为3的时候才进行赋值
printf("%d\n", b);
printf("%d", c);
return 0;
}
运行结果:
后置++ / - -
后置++/- -会在语句执行后才去执行+/-
#include <stdio.h>
int main()
{
int a = 3;
int b = 0;
int c = 0;
b = a++; //先执行了赋值 然后进行 a++ 所以此时 b=3 而a在执行语句后是4
c = a--; //同样先进行赋值 所以此时c= 4 然后再执行 a-1
printf("%d\n", b);
printf("%d", c);
return 0;
}
运行结果:
6.关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
这些关系运算符用法还算简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。记得只能用在适合的类型上,这几个只能用于整形变量和字符变量 不能进行字符串的判断。
7.逻辑操作符
&& 逻辑与 (并且)
|| 逻辑或 (或者)
1)与& |的区别
区分逻辑与(&&)和按位与(&) 和 区分逻辑或(||)和按位或(|)
例子:
例子:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
while(scanf("%d %d",&a ,&b) != EOF)
{
if (a == 3 && b == 3)
{
printf("haha\n");
}
if (a == 3 || b == 3)
{
printf("hehe\n");
}
}
return 0;
}
运行结果:
2)练习1
题目:闰年判断
int main()
{
int year = 0;
while (scanf("%d", &year) != EOF)
{
if ((year % 4 == 0) && (year % 100 != 0) || year % 400 == 0)
{
printf("%d是闰年!\n", year);
}
else
{
printf("%d不是闰年\n",year);
}
}
return 0;
}
运行结果:
3)练习2
题目:下面这个代码的运行结果(即a,b.c.d的值)
#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);
return 0;
}
运行结果:
解析:这题核心就是关于&&的特点: 如果&&(逻辑与)左边假的 那么右边都不执行(因为只有在&&左右两边都是真的时候才为真 所以左边是假的 右边都执行) 所以在判断a++ a是0后 后面的都不执行 然后a自增后a =1 其他还是初始化的值
4)练习3
题目:下面这个代码的运行结果(即a,b.c.d的值)
#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);
return 0;
}
运行结果:
解析:这题也是用到了逻辑操作符的特点 如果逻辑或(||)左边是真,则右边不执行(因为如果||左边为真 那个整个都为真,则右边都不会进行) 这题就是 因为a为0 则 表达式 ++b会执行 而++b是真 则 d++不执行 所以结果就是如图所示。
ps:逻辑操作符的结果都是 0/1(真/假)来表示。
8.条件操作符
exp1 ? exp2 : exp3
先执行exp1,如果exp1真 那么就执行 exp2 ;如果exp1假 那么就执行exp3
有些时候是可以替代简单形式的if else 分支语句
例如:
把其中的if else语句 用条件操作符表达出来
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
if (a > 5)
b = 3;
else
b = -3;
return 0;
}
改了之后:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
b = (a > 5 ? 3 : -3);
printf("%d", b);
return 0;
}
9. 逗号表达式
exp1, exp2, exp3..... expN
逗号表达式,就是用逗号隔开的多个表达式。它从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
代码1:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d %d %d",a ,b ,c);
return 0;
}
运行结果:
这个就按照逗号操作符的正常流程走 就能得出结果
ps:通过这个地方我了解到了 > < != 执行后所返回的是表达式的真假 而 = 这种表达式的结果就是 等号左边这个变量的结果
代码2:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
if (a = b + 1, c = a / 2, d > 0)
{
printf("%d %d %d %d", a, b, c, d);
}
return 0;
}
解析:即使在if从句里面 也会一步一步执行 而是 真/假 看的也还是最后一个表达式的结果。
改写:
原来的:
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)下标引用操作符 []
它的操作数:一个数组名 + 一个索引值
例子:
ps:数组是有下标的,引用都是从下标开始!
2)函数调用操作符()
它的操作数:一个或者多个操作数;第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
例子:
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char* str)
{
printf("%s\n", str);
}
int main()
{
test1();
//实用()作为函数调用操作符。
test2("hello world.");
//实用()作为函数调用操作符。
return 0;
}
运行结果:
ps: strlen也是函数,以**strlen(“abc”)**为例,操作数就是 strlen 和 “abc”
还有strlen函数里面的变量不是可变参数列表 printf里面是可变参数列表
3)结构成员
. 结构体.成员名
-> 结构体指针->成员名
将之前先简单介绍下结构体 方便代码的理解:
像char int short long这种类型都是C语言的内置类型 而内置类型无法表达全部,所以就有了复杂类型(结构体),能够创建的自定义类型
而且以下面的代码来讲:Book只是数据类型 book才是变量名
例子(结构体的初始化):
int main()
{
struct Book
{
char name1[20];
char name2[20];
float price;
};
//初始化方法1:
struct Book book1 = { "数据结构", "严蔚敏", 50.5};
//初始化方法2:
//strcpy(book1.name1, "数据结构"); 但是使用前要先引用头文件 string.h
printf("%s\n", book1.name1);
printf("%s\n", book1.name2);
printf("%f\n",book1.price);
return 0;
}
结构体成员访问的例子:
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 28;
printf("%d\n", stu.age);
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
printf("%d\n", pStu->age);
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 30;//结构成员访问
printf("%d\n", stu.age);
set_age1(stu);
printf("\n");
pStu->age = 20;//结构成员访问
printf("%d\n", stu.age);
set_age2(pStu);
return 0;
}
运行结果:
结构体传参:
#include<stdio.h>
struct Book
{
char name1[20];
char name2[20];
float price;
};
//形参接收时 是struct Book类型
void Printf(struct Book* p)
{
//这两种方式都是可以的
//printf("%s\n", p->name1);
//printf("%s\n", p->name2);
//printf("%f\n", p->price);
printf("%s\n", (*p).name1);
printf("%s\n", (*p).name2);
printf("%f\n", (*p).price);
}
int main()
{
struct Book book1 = { "数据结构", "严蔚敏", 50.5 };
Printf(&book1); //book1属于变量名 传入地址 所以要对book1进行取地址
return 0;
}
11. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定.同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1)隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
1.表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
2.因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
3.CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须转
换为int或unsigned int,然后才能送入CPU去执行运算。
例子1:
int main()
{
char a = 5;
//因为是正数所以 原/反/补 三码相同
//00000000 00000000 00000000 00000101 补码
//00000101 -a
char b = 127;
//因为是正数所以 原/反/补 三码相同
//00000000 00000000 00000000 01111111 补码
//01111111-b
char c = 0;
c = a + b;
//先进行整形提升:
//00000000 00000000 00000000 00000101
//00000000 00000000 00000000 01111111
//00000000 00000000 00000000 10000100 补码
//10000100 (截断后补码)
//11111100 -c (原码)
printf("%d\n", c);
return 0;
}
解析:这个地方就用到了整型提升,具体过程是:b和c的值被提升为普通整型,然后再执行加法运算,加法运算完成之后,结果将被截断,然后再存储于a中。
2)如何进行整体提升
整形提升是按照变量的数据类型的符号位来提升的
负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
无符号整形提升,高位补0
整形提升的例子1:
#include<stdio.h>
int main()
{
char a = 0xb6; //0x是16进制数
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
运行结果:
解析:只打印了c 再根据上面的整型提升来分析 a和b都进行了整形提升,但是a,b整形提升之后,变成了负数,所以前两个判断都是假的,只有c这个判断是真的,所以只有c打印出来了。
整形提升的例子2:
#include<stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
运行结果:
解析:c只要参与表达式运算,就会发生整形提升;表达式 +c和-c,就会发生提升,所以 sizeof(+c) 和sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节
ps:%u输出:无符号10进制整数
3) 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。 (切记是排名靠后的向排名向前的转换)
例1:
解析:通过这个例题主要是想告诉大家:n的本身属性是不会发生改变的(他在运算完后还是整形变量),只有在计算的瞬间会转化一下。
警告:算术转换要合理,要不然会有一些潜在的问题。
例2:
4) 操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级。
- 操作符的结合性。
- 是否控制求值顺序。
Q:两个相邻的操作符先执行哪个?
A:取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级表
(此图图表用的一位CSDN里叫 根号五 的博主的图)
5)练习1:
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
解析:代码1在计算的时候,由于*比+的优先级高,只能保证,的计算是比+早,但是优先级并不能决定第三个比第一个+早执行。
则执行顺序:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
6)练习2:
//表达式2
c + --c;
解析: 光知道- -的优先级比+高 但是不知道c在- -后是先运算 + 再影响前面的c 还是 先影响前面的c后执行+ 有问题的原因是:无法确定唯一的计算路径
所以结果可能是8 也可能是9
7)练习3:
//代码3-非法表达式
#include<stdio.h>
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3在不同编译器中测试结果:
值 | 编译器 |
---|---|
128 | Tandy 6000 Xenix 3.2 |
—95 | Think C 5.02(Macintosh) |
—86 | IBM PowerPC AIX 3.2.5 |
—85 | Sun Sparc cc(K&C编译器) |
—63 | gcc,HP_UX 9.0,Power C 2.0.0 |
4 | Sun Sparc acc(K&C编译器) |
21 | Turbo C/C++ 4.5 |
22 | FreeBSD 2.1 R |
30 | Dec Alpha OSF1 2.0 |
36 | Dec VAX/VMS |
42 | Microsoft C 5.1 |
8)练习4:
#include<stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);//输出多少?
return 0;
}
解析:这个代码也是有问题的,就算在大多数的编译器上求得结果都是相同的。但是上述代码answer = fun() - fun() * fun();中我们只能通过操作符的优先级得知:先算乘法,再算减法。但是函数的调用先后顺序无法通过操作符的优先级确定,所以还是有可能出现多种结果的。
结果:
9)练习5:
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
解析:这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
在vs2022上的结果:
但是在gcc编译器上的结果是:
通过那么多例子总结出:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。