8.0、C语言——操作符
算数操作符:
+ - * / %
1、除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数
2、对于 / 操作符如果两个操作数都为整数,执行整数除法,而只要有浮点数执行的就是浮点数除法
3、% 操作符的两个操作数必须为整数,返回的是整除之后的余数
移位操作符【只能作用于整数】:
<< 左移操作符
>> 右移操作符
右移位运算 分为以下两种:
1、算术右移:
右边丢弃,左边补原符号位【正数补0,负数补1】,当前的编译器基本上用的都是 算术移位
2、逻辑右移:
右边丢弃,左边补0【不挂正数还是负数都补0】
其实总结一下:只有负数在算术右移运算时才会补 1 其他情况都是补 0;虽然有两种移位方式但是通常我们见到的都是算术右移;右移移位是除 2,左移一位是乘 2
位操作符:
&(与):都是 1 则为 1 ,有 0 则为 0
|(或):有 1 则为 1 ,全 0 才为 0
^(异或):相同为 0 ,不同为 1
来看几道面试题:
1、不准使用中间变量,然后交换两个变量的值,int a = 5; int b = 3; 实现代码如下所示:
方式一:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 5;
int b = 3;
a = a + b;
b = a - b;
a = a - b;
printf("a = %d , b = %d",a,b);
return 0;
}
该方法有一定的缺陷,如果我们的a ,b两个变量数值很大,相加后可能会溢出
方式二:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 5;
int b = 3;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d , b = %d",a,b);
return 0;
}
这里我们可以将第一次 a^b 看成一个译码 x,经计算后会发现 x ^ a 会得到 b 原来的值,x ^ a 会得到 a 原来的值
2、计算一个整数的二进制补码中有多少个 1
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int input = 0;
int count = 0;
scanf("%d",&input);
for (int i = 0;i < 32;i++) {
if (1 == ((input >> i) & 1)) {
count++;
}
}
printf("count = %d",count);
return 0;
}
举个例子:
3 的二进制为:00000000 00000000 00000000 00000011
1 的二进制为:00000000 00000000 00000000 00000001
那么每次 右移 1 位 依次和 1 的二进制 进行 &(与) 运算,如果为 1 则说明是 1 则 count++,因为int类型为4字节32位,所以循环32次~
单目操作符:
! 逻辑反操作,正变假,假变真
- 负值
+ 正数
& 取地址
sizeof 操作数的类型长度(一字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置 --
++ 前置、后置 ++
* 间接访问操作符
(类型) 强制类型转换
!(取反),比如 int a = 10; !a 等于 0【因为0为假,非0为真】,int a = 0;那么 !a 等于 1
sizeof(计算占用内存大小) ->
1、sizeof(a);计算变量可以省略括号
2、sizeof(int);计算类型不可以省略括号
3、sizeof()括号里的表达式,是不参与运算的,例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 10;
short s = 0;
printf("%d\n", sizeof(s = a + 5));
printf("%d",s);
return 0;
}
1、第一个输出 sizeof(s = a + 5) 等于 2,因为最终的结果是存放到 short 里所以占用的内存空间是 2 byte
2、第二个输出的 s 仍然是 0,因为 sizeof() 括号里的表达式不参与运算,所以 s 的值不会发生变化
知识扩展:当去掉数组名后剩下的就是数组的类型,比如 int arr[10]; 他的数组类型就是 int [10]
逻辑运算符中:
&&(与):如果 && 左边计算出来为 0(假) ,那么后面的将不再计算,直接得出 0(假)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 0;
int b = 2;
int c = 3;
int i = a++ && ++b && c;
printf("%d%d%d%d", a, b, c, i);
return 0;
}
直接输出1230
|| (或):如果 || 左边计算出来的结果为 1,那么将不再往后计算,直接得出结果 1(真)
三目操作符:
表达式1 ? 表达式2 :表达式3 (如果表达式1为真,则整个表达式的结果为表达式2,如果表示式为假则整个表达式的结果为表达式3)
struct 结构体:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//创建了一个结构体类型-struct Student
struct Student
{
int age;
int phone;
char name[10];
};
int main() {
//使用 struct Student 这个类型创建了一个学生变量叫做student1
struct Student student1 = {18,123456789,"小明" };
return 0;
}
可以把 struct Student 看成一个类型,就像是 int float char 类型一样都是一种数据类型,然后通过类型就可以定义变量 student1 就是由 struct Student 类型定义声明出来的变量,只不过赋值有多个属性值要用 { } 花括号括起来赋值。
那么在我们声明一个struct类型的时候,不会占用内存的空间,但是一旦创建该类型的变量之后,就会向内存空间申请一块空间进行存放~
如果想要访问该对象 student1 的属性可以通过下面三种方式去访问:
1、通过 “ . ” 去访问,例如 student1.age
2、通过 取地址 去访问,例如 :
struct Student* ps = &student1;
printf("age = %d", (*ps).age);
3.通过 -> (这是结构体指针的操作符)去访问【当然这种方式本质上和第二种是一样的,但是会更加简洁直观一些】,例如:
struct Student* ps = &student1;
printf("age = %d", ps->age);
表达式求值:
表达式求值的顺序一部分是由操作符的优先级和结合性决定
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
隐式类型转换:
C的整数算术运算总是至少以缺省整型类型的精度来进行
为了获得这个精度,表达式中的 字符 和 短整型 操作数在使用之前被转换为 普通整型 ,这种转换称为 整型提升
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是cpu的通用寄存器的长度
因此,即使两个char类型的相加,在cpu执行时实际上也要先转换为cpu内整型操作数的标准长度
通用cpu(genneral-purpose cpu)是难以直接实现两个8bit直接相加运算(虽然机器指令中可能有这种字节相加指令)所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入CPU去执行运算
如何进行整型提升呢?
整型提升是按照变量的数据类型的符号位来提升的
下面给大家举个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
char a = 3;
char b = 127;
char c = a + b;
printf("c = %d",c);
return 0;
}
a、b、c 三个变量都是 char 类型【8bit】,但是赋值确实 int 类型【32bit】,
这时候会将 int 类型从最低位强行阶截断出8位,赋给变;
比如 3 -> 00000000 00000000 00000000 00000011 会强行截断成 00000011;
最后在 a + b 相加的时候,由于计算机无法进行 8bit 与 8bit 数据的运算,所以会将整型提升至 32bit ,然后再进行运算;
所以 a + b 计算得到的就是 00000000 00000000 00000000 10000010,但是存到c中的依然是被截断后的八位二进制即 10000010;
最后执行 printf() 打印语句的时候,由于要打印出整型但是c又是char类型,所以这时候又要整形提升,变成 11111111 11111111 11111111 10000010【因为最高位是 1 所以前面补 1,有符号数在整型提升时 符号位是0就补0、符号位是1就补1,但是如果是无符号数就直接补0】
但要知道这是补码,我们得将其 减一 再 取反 转化为十进制后得到的就是 -126,打印出来的就是 -126 了
算术转换:
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为 寻常算术转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型后,再去执行运算
警告:但是算术转换要合理,要不然会有一些潜在的问题
操作符的属性:
复杂表达式的求值有三个影响的因素:
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序
两个相邻的操作符先执行哪个?
取决于他们的优先级。如果两者的优先级相同,取决于他们的操作符优先级【优先级高的先执行,比如说 * 比 + 优先级高,所以先算 *】 、结合性【当优先级相同时,从左向右执行】
一些问题表达式:
问题表达式一: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 + e * f
我们不要只是将他看成一个运算式子,要将他看成一个表达式,如果我们的表达式前面计算的结果会影响到后面的话,那么如果这个表达式的优先级执行顺序没法确定就会出现问题;
所以说如果我们即使拥有优先级、结合性、求值顺序,表达式依然无法确定唯一的计算路径,那么这就是一个有问题的表达式。
问题表达式二:c+ --c
注释:同上,操作符的优先级只能决定自减 " -- " 的运算在 " + " 的运算前面,但是我们并没有办法得知 -> " + " 操作符的做操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的
问题表达式三:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d",i);
return 0;
}
这个表达式每个编译器算出来的都不一样
问题表达式四:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
虽然说可以确定先计算 " * " 在计算" + ",但是无法确定 这三次调用 fun()函数谁先被调用执行,不同的调用顺序会导致最后计算的结果不同,所以这也是一个有问题的表达式
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的