1. 操作符的分类2. ⼆进制和进制转换3. 原码、反码、补码4. 移位操作符5. 位操作符、|、^、~6. 单⽬操作符7. 逗号表达式8. 下标访问[]、函数调⽤()9. 结构成员访问操作符10. 操作符的属性优先级、结合性11. 表达式求值1.操作符的分类以下就是大部分的操作符算术操作符 、- 、* 、/ 、%移位操作符: 位操作符: | ^赋值操作符: 、 、 - 、 * 、 / 、% 、 、 、 、| 、^单⽬操作符 、、--、、*、、-、~ 、sizeof、(类型)关系操作符: 、 、 、 、 、 !逻辑操作符 、||条件操作符 ? :逗号表达式 ,下标引⽤ []函数调⽤ ()结构成员访问 . 、-2.二进制和进制转换我们都知道计算机只认识二进制而除了二进制还有八进制十进制十六进制。他们的不同仅在于对一个数值的表达形式不同。例如∶数值15的各种进制表达形式15的2进制1111 15的8进制17 15的10进制15 15的16进制F十六进制的数值前要写0x而八进制的数值前要写0。我们重点了解一下二进制首先我们要先从了解十进制关于十进制我们知道其一些底层逻辑。十进制中满十进一十进制中每一位的数字都由0到9组成而二进制也是一样的二进制满二进一二进制中每一位的数字由0和1组成2.1二进制转换十进制十进制的123代表的值是一百二十三为什么这样呢因为十进制的每一位都有权重十进制的数字从左向右是个位十位百位……每一位的权重分别是十的零次方十的一次方十的平方……如下图所示∶二进制与十进制类似不过二进制的每一位权重从右到左分别是二的零次方二的一次方二的平方……例如二进制的1101怎么理解呢2.1.1 十进制转换成二进制数字2.2二进制转换成八进制和十六进制2.2.1二进制转换成八进制八进制的每一位数字是0到7如果各自写成二进制最多有三个二进制比如7的二进制是111所以二进制转换成八进制时从二进制序列右边到左边每3个二进制转换成一个八进制剩余不够3个二进制位直接换算比如∶二进制的01101011换成八进制∶01530开头的数字会被当做八进制。2.2.2二进制转换成十六进制十六进制的每一位数字是0到9a到f代表10到15。各自写成二进制最多有4个二进制比如f的二进制是1111所以二进制转换成十六进制二进制序列从右到左每4位组成一个十六进制数剩余不够的直接转换例如∶二进制的01101011换成十六进制∶0x6b16进制表示的时候前面加0x。3.原码、反码、补码整数的二进制表达方式有三种即原码、反码、补码有符号整数的三种表达方式均有符号位和数值位二进制中最左边一位是符号位其余都是数值位符号位中0代表“正”1代表“负”。正整数的原、反、补码都相同。负整数的三种表⽰⽅法各不相同。原码直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。反码将原码的符号位不变其他位依次按位取反就可以得到反码。补码反码1就得到补码。补码得到原码也是可以使⽤取反1的操作。⽆符号整数的三种 2 进制表⽰相同没有符号位每⼀位都是数值位对于整形来说数据存放内存中其实存放的是补码。为什么呢在计算机系统中数值⼀律⽤补码来表⽰和存储。原因在于使⽤补码可以将符号位和数值域统⼀处理同时加法和减法也可以统⼀处理CPU只有加法器此外补码与原码相互转换其运算过程是相同的不需要额外的硬件电路。4.移位操作符左移操作符右移操作符移位操作符的操作数只能是整数4.1左移操作符移位规则∶左边抛弃右边补0#include stdio.h int main() { int num 10; int n num 1; printf(n %d\n, n); printf(num %d\n, num); return 0; }4.2右移操作符移位规则∶右移操作运算有两种∶1.逻辑右移∶左边用0填充右边丢弃2.算数右移∶左边用符号位填充右边丢弃#include stdio.h int main() { int num -1; int n num 1; printf(n %d\n, n); printf(num %d\n, num); return 0; }逻辑右移一位算数右移一位对于移位操作符不要移动负数位这个是标准未定义例如∶int num 10; num -1;5.位操作符、|、^、~位操作符是在数值的二进制位进行操作的其次结果是补码如果是正数不用转换如果是负数要符号位不变其余位要转换成原码然后得到它代表的负数。位操作符有 //按位与对应位有1则为1 | //按位或对应位都是1则为1 ^ //按位异或对应位不同则为1 ~ //按位取反二进制0变11变0他们的操作数必须是整数。#include stdio.h int main() { int num1 -3; int num2 5; printf(%d\n, num1 num2); printf(%d\n, num1 | num2); printf(%d\n, num1 ^ num2); printf(%d\n, ~0); return 0; }运行结果如下接下来来⼀道变态的⾯试题问题不能创建临时变量第三个变量实现两个整数的交换。#include stdio.h int main() { int a 10; int b 20; a a ^ b; b a ^ b; a a ^ b; printf(a %d b %d\n, a, b); return 0; }用异或的方式可以这样实现快速算出交换后的结果还不会占用额外的内存练习1编写代码实现求⼀个整数存储在内存中的⼆进制中1的个数。//⽅法1 #include stdio.h int main() { int num 10; int count 0;//计数 while(num) { if(num % 2 1) count; num num / 2; } printf(⼆进制中1的个数 %d\n, count); return 0; }//这是我们容易想到的方法但是如果这个数是负数的话就不能用这个方法了。 //⽅法2 #include stdio.h int main() { int num -1; int i 0; int count 0;//计数 for(i 0; i 32; i) { if( num (1 i) ) count; } printf(⼆进制中1的个数 %d\n, count); return 0; } //这样是对二进制的每一位进行了判断而且可以对正数和负数都进行判断 //但能不能对这个方法进行优化呢 //⽅法3 #include stdio.h int main() { int num -1; int i 0; int count 0;//计数 while(num) { count; num num (num - 1); } printf(⼆进制中1的个数 %d\n, count); return 0; } //虽然这个方法优化了方法二对每个数的判断而且没有额外的移位操作但是这种方法很难想到练习2⼆进制位置0或者置1编写代码将13⼆进制序列的第5位修改为1然后再改回013的2进制序列00000000000000000000000000001101将第5位置为1后00000000000000000000000000011101将第5位再置为000000000000000000000000000001101#include stdio.h int main() { int a 13; a a | (1 4); printf(a %d\n, a); a a ~(1 4); printf(a %d\n, a); return 0; }6.单⽬操作符单⽬操作符有这些、、--、、*、、-、~、sizeof、(类型)1. ! 逻辑非把逻辑值取反规则是“非0为真0为假”取反后真变假、假变真。- 比如 !0 的结果是1真 !5 的结果是0假。- 常用场景 if(!flag) 当 flag 为0时条件成立。2. 自增让操作数的值加1分两种用法- 前置自增 a 先把 a 的值加1再用加完的结果参与后续运算。- 后置自增 a 先用 a 当前的值参与运算运算结束后再把 a 的值加1。3. -- 自减和自增类似只是让操作数的值减1也分前置和后置- 前置自减 --a 先减1再参与运算。- 后置自减 a-- 先参与运算再减1。4. 取地址符获取变量在内存中的地址常用来给指针变量赋值。- 比如 int *p a; 意思是让指针 p 指向变量 a 的内存地址。5. * 解引用符和取地址符相反用来访问指针指向的内存地址里存的值。- 比如 *p 10; 就是把10赋值给 p 指针指向的那个变量。6. 正号只是显式表示一个数是正数对数值本身没有任何改变。- 比如 int a 5; 和 int a 5; 效果完全一样。7. - 负号对数值取反得到它的相反数。- 比如 int b -a; 如果 a 是3那 b 就是-3如果 a 是-2那 b 就是2。8. ~ 按位取反把整数的二进制位全部取反0变1、1变0是位运算里的操作符。- 比如32位系统里 ~0 会把所有0位变成1结果就是-1。9. sizeof 求字节数获取变量或数据类型在内存中占的字节数它不是函数是C语言的内置操作符。- 比如 sizeof(int) 在多数系统里结果是4 sizeof(char) 结果永远是1。10. (类型) 强制类型转换把表达式的结果强制转换成指定的数据类型常用于解决类型不匹配的问题。- 比如 (float)5 / 2 先把5转成浮点数5.0再除以2结果是2.5而不是整数除法的2。7. 逗号表达式表达形式exp1, exp2, exp3, …expN逗号表达式就是⽤逗号隔开的多个表达式而且从左向右依次执⾏。整个表达式的结果是最后⼀个表达式的结果。8.下标访问[]、函数调⽤()8.1[ ] 下标引⽤操作符操作数⼀个数组名 ⼀个索引值(下标)例如intarr[10]{0};//创建并初始化数组arr[9] 10;//实⽤下标引⽤操作符。[ ]的两个操作数是arr和9。8.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; }运行结果9. 结构成员访问操作符9.1 结构体C语⾔已经提供了内置类型如char、short、int、long、float、double等但是只有这些内置类型还是不够的假设我想描述学⽣描述⼀本书这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等描述⼀本书需要书名、作者、出版社、定价等。C语⾔为了解决这个问题增加了结构体这种⾃定义的数据类型让程序员可以⾃⼰创造适合的类型。9.1.1 结构的声明描述⼀个学⽣structStu{charname[20];//名字intage;//年龄charsex[5];//性别charid[20];//学号};//分号不能丢9.1.2 结构体变量的定义和初始化//代码1 struct Point { int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //定义结构体变量p2 //代码2 struct Point p3 {10, 20}; struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s1 {zhangsan, 20};//初始化 struct Stu s2 {.age20, .namelisi};//指定顺序初始化 //代码3 struct Node { int data; struct Point p; struct Node* next; }n1 {10, {4,5}, NULL}; //结构体嵌套初始化 struct Node n2 {20, {5, 6}, NULL};//结构体嵌套初始化这上面的三个代码分别说明了结构体的定义结构体的初始化结构体的嵌套初始化。9.2 结构成员访问操作符9.2.1 结构体成员的直接访问结构体成员的直接访问是通过点操作符.访问的。点操作符接受两个操作数。如下所⽰#include stdio.h struct Point { int x; int y; }p {1,2}; int main() { printf(x: %d y: %d\n, p.x, p.y); return 0; }使⽤⽅式结构体变量.成员名9.2.2 结构体成员的间接访问有时候我们得到的不是⼀个结构体变量⽽是得到了⼀个指向结构体的指针。如下所⽰#include stdio.h struct Point { int x; int y; }; int main() { struct Point p {3, 4}; struct Point *ptr p; ptr-x 10; ptr-y 20; printf(x %d y %d\n, ptr-x, ptr-y); return 0; }使⽤⽅式结构体指针-成员名10. 操作符的属性优先级、结合性C语⾔的操作符有2个重要的属性优先级、结合性这两个属性决定了表达式求值的计算顺序。10.1 优先级优先级指的是如果⼀个表达式包含多个运算符哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。34*5;如果按照我们学数学的方法我们会先乘除后加减但对于计算机却有优先级的限制。表达式3 4 * 5⾥⾯既有加法运算符⼜有乘法运算符*。由于乘法的优先级⾼于加法所以会先计算4 * 5⽽不是先计算3 4。10.2 结合性如果两个运算符优先级相同优先级没办法确定先计算哪个了这时候就看结合性了则根据运算符 是左结合还是右结合决定执⾏顺序。⼤部分运算符是左结合从左到右执⾏少数运算符是右 结合从右到左执⾏⽐如赋值运算符 。5*6/2;上面示例例中*和/的优先级相同它们都是左结合运算符所以从左到右执行先计算5 * 6 再计算 / 2。运算符的优先级顺序很多下⾯是部分运算符的优先级顺序按照优先级从⾼到低排列建议⼤概记住这些操作符的优先级就⾏其他操作符在使⽤的时候查看下⾯表格就可以了。•圆括号 ()•⾃增运算符 ⾃减运算符--•单⽬运算符 和-•乘法 *除法/•加法 减法-•关系运算符 、等•赋值运算符 由于圆括号的优先级最⾼可以使⽤它改变其他运算符的优先级。11. 表达式求值11.1 整型提升C语⾔中整型算术运算总是至少以缺省默认整型类型的精度来进⾏的。为了获得这个精度表达式中的字符和短整型操作数在使⽤之前被转换为普通整型这种转换称为整型提升。整型提升的意义表达式的整型运算要在CPU的相应运算器件内执⾏CPU内整型运算器(ALU)的操作数的字节⻓度⼀般就是int的字节⻓度同时也是CPU的通⽤寄存器的⻓度。因此即使两个char类型的相加在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准长度。通⽤CPUgeneral-purpose CPU是难以直接实现两个8⽐特字节直接相加运算虽然机器指令中 可能有这种字节相加指令。所以表达式中各种⻓度可能⼩于int⻓度的整型值都必须先转换为int或unsigned int然后才能送⼊CPU去执⾏运算。如何进行整体提升呢1. 有符号整数提升是按照变量的数据类型的符号位来提升的2. 无符号整数提升高位补0//负数的整形提升char c1 -1;变量c1的⼆进制位(补码)中只有8个⽐特位1111111因为 char 为有符号的 char所以整形提升的时候⾼位补充符号位即为1提升之后的结果是11111111111111111111111111111111//正数的整形提升char c2 1;变量c2的⼆进制位(补码)中只有8个⽐特位00000001因为 char 为有符号的 char所以整形提升的时候⾼位补充符号位即为0提升之后的结果是00000000000000000000000000000001//⽆符号整形提升⾼位补011.2 算术转换如果某个操作符的各个操作数属于不同的类型那么除⾮其中⼀个操作数的转换为另⼀个操作数的类 型否则操作就⽆法进⾏。下⾯的层次体系称为寻常算术转换。long doubledoublefloatunsigned long intlong intunsigned intint如果某个操作数的类型在上⾯这个列表中排名靠后那么⾸先要转换为另外⼀个操作数的类型后执⾏ 运算。11.3 问题表达式解析11.3.1 表达式1a * b c * d e * f表达式1在计算的时候由于 *比 的优先级高只能保证 * 的计算是比 早但是优先级并不能决定第三个 * ⽐第⼀个 早执行。所以表达式的计算机顺序就可能是a*bc*da*b c*de*fa*b c*d e*f或a*bc*de*fa*b c*da*b c*d e*f11.3.2 表达式2c --c;同上操作符的优先级只能决定⾃减--的运算在的运算的前⾯但是我们并没有办法得知操作符的左操作数的获取在右操作数之前还是之后求值所以结果是不可预测的是有歧义的。11.3.4 表达式3#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();中我们只能通过操作符的优先级得知先算乘法再算减法。函数的调⽤先后顺序⽆法通过操作符的优先级确定。11.4 总结即使有了操作符的优先级和结合性我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径那这个表达式就是存在潜在⻛险的建议不要写出特别复杂的表达式。