11.1 C语言里边都有哪些操作符
在 C 语言里边有很多的操作符,都有哪些操作符呢?一共有10种操作符
1. 算术操作符:+ - * / %(取模、取余)
2. 移位操作符:>> <<
3. 位操作符:& | ^
4. 赋值操作符:= += -= *= /= &= |= ^= >>= <<=
5. 单目操作符:! - + & sizeof ~ -- ++ * (类型)
6. 关系操作符:> >= < <= == !=
7. 逻辑操作符:&& ||
8. 条件操作符:exp1 ?exp2 :exp3
9. 逗号表达式:exp1,exp2,exp3,...expN
10. 下标引用、函数调用和结构成员:[ ] ( ) . ->
11.2 整数除法与小数除法
除的话是怎么除的呢?举个例子,int a = 9/2,打印一下,那这个地方 a 得到结果是多少呢?这里是得不到4.5的,为什么呢?因为9/2应该是商4余1的,使用这个地方结果是4,就是说得到的是商,不会得到小数的,如图所示:
有同学就说,是不是 a 的问题?我们换成 float a = 9/2,打印单精度浮点数用 %f,这个地方能不能算出4.5呢?还不是4.5,如图所示:
从根本上说,9/2,除号两端,如果都是整数,执行的叫整数除法,算出的就是4,所以这个地方这前面放的是什么类型,它最终都是4或者4.000000,但是我们就想4.5,难道就得不到了吗?不是,除号的两端有一个数是小数,那执行的就是小数除法。比如说2改成2.0,9/2.0的话,就按照 小数的算法来算的,那这个地方算出的就是4.5了,如果再存到单精度浮点数的变量里边去,再用单精度浮点数的形式打印的话,那打印出来的结果就是4.5了,所以要想得到一个小数的时候,应该怎么算,我们要心里有谱,如图所示 :
如果写成 a = 9%2,那结果又是多少呢?9%2又是什么意思呢?这个%叫取模,也叫取余。也就是说我们得到的是余数,9/2商4余,余的是1,所以这个地方 a 里边放的是1,取模操作符得到的一定是余数,如图所示:
11.3 左/右移操作符
我们定义一个变量 a = 2,然后 a 向左移动一位,即 a<<1,然后结果放到 b 里边去,然后打印一下 b,其实这用到的就是一个向左移动的两个箭头,就是左移操作符。左移操作符移动的是二进制位,所以对 a 向左移动的话,a 结果是2,2的二进制序列是10,实际上10还不够,因为 a 是放到 int 里边去,a 是个 int,2是放到 int 里边。a 是一个 int,int 是4个 byte,1个 byte 是8个 bit,所以如果我要写出 a 的话,2的二进制序列的话,我们得写出32个 bit 位,即00000000 00000000 00000000 00000010这才是2的二进制序列,32个位
所以这个时候2的二进制序列在内存里边其实就是这么存的-----现在我说把 a 的二进制序列向左移动1位,其实就是把它拿起来,然后向左移动了一位,当我们向左移动1位的时候,左边的一位就丢了,右边的一位补0,那补完之后的结果就是100,二进制的这一位如果是1得到话,它代表的权重就是2的2次方,那其实就是4,也就是说,a 向左移动一位之后这个数就变成4了,变成4之后,4放到 b 里边去,如图所示:
11.4 赋值操作符
假设我们定义一个变量 a = 2,然后 a = a + 5,就是我们给 a 增加一个5,这里的等号就叫赋值。当然这种赋值也可以写成 a += 5(第314行代码),这种叫复合赋值。当然你也可以说 a = 6,这也叫赋值。这个等号就叫赋值,这个加等叫复合赋值,当然其实减也是一样的: 比如 a = a - 3,就可以写成 a -= 3(第317行代码)。刚刚其实前面的一些操作符,比如 a = a % 3,就可以写成 a %= 3(第320行代码)。如图所示:
11.5 单目操作符(重点)
11.5.1 逻辑反操作
什么叫单目操作符呢?当我写出 a + b 的时候,这个 + 是个操作符,+有2个操作数,一个是 a,一个是 b,所以这个 + 叫双目操作符。那这个单目操作符是什么意思呢?其实意思就是只有一个操作数的操作符
感叹号 ! 是怎么用的呢?感叹号 ! 的意思就是逻辑取反,假设我们定义一个变量 a = 10,现在我们打印一下 !a,这个是多少呢?我们 C 语言里边有个东西,C 语言里边如何表示真假的?0表示假,非0表示真。这是 C 语言里边的规定,10当然是非0了,非0就是真,所以感叹号 ! 的意思就是把真变成假,也能把假变成真。a 里边如果是10的话,感叹号 !a 就把它变成假,那 !a 就是0,如图所示:
当然了,那假设 a 里边是个0,那感叹号 !a 是什么呢?如果 a 里边是9,那是假,我们要把它变成真,真的地方多了去了!!!10也是真,25也是真,69也是真等等,那这个地方怎么办呢?其实,当把假变成真的时候,这个表达式结果我们规定它是1,1就是真。如图所示:
是1的时候,就能够说明一个问题:a 如果是假,就能把 a 变成真;如果 a 本来是真,就可以把它变成假(把真变成假,假变成真)
刚刚只是语法的讲解,感叹号! 真正用是这么用的:比如当 a 为真,要做一件事情的时候,就是下图,因为 a 如果为真的话,我们就进到 if 语句里边做事,如图所示:
如果我们表达的逻辑是 a 为假,我们要做一件事情的话,那就是 if(!a),也是下图,因为 a 如果为假的时候,!a 为真,我们就做这件事情,如图所示:
11.5.2 负值
- 其实是负,不是减号,是负值,举个例子:a = -5,a = -a,当然我们也可以写 a = +a,第342行代码这个正号其实完全是可以省略的,其实等于没有,一般都是省略掉的。因此,正号和负号也是单目操作符
11.5.3 sizeof
sizeof 是一个操作符,其实之前我们以及见过 sizeof 了,sizeof 是用来计算类型或者变量的大小的,它返回某种数据类型或者某个值占用的字节数量
举个例子,写上一个 int a = 10,打印一个计算 int 的大小,可以写 sizeof(int),当然,我们把 int 也可以换成 a,因为 a 的类型就是 int,int 也创建了 a,这两个大小是一样的,如图所示:
所以,当我们去算 a 的大小的时候,和 a 的类型的大小,其实是一个结果,sizeof 都可以算
当然,对于 sizeof,这个地方它有一些特殊的点,比如说第352行代码 sizeof 后面的括号能不能省略掉?可以省略。sizeof 后面的括号可以省略,就说明 sizeof 是一个操作符,不是函数,因为函数后面的括号是不能省略掉的。如图所示:
那我们把这个 int 后面的两个括号省略掉行不行?这个时候就不行了,这是语法不支持的,但我们 a 两端能够省略的话,说明其实 a 就不是函数了。因此,sizeof 用的是时候,对于求变量,两端的括号可以省略;但是求类型的时候,两端的括号不能省略。如图所示:
通常我们在使用的时候,它两端的这个括号我们都不会省略,我们只是讲一下这种语法是支持的
sizeof 当然除了这些功能之外,它还有一些其他的功能,刚刚我们说的是计算一个变量的大小,那它能不能计算一个数组的大小呢?当然也是可以的
假设写上个 arr[10],这个数组我们暂时全放成0,这是10个元素,每个元素是一个 int,每个元素都是 int,那它总大小是多大?那我们就写一下 sizeof(arr),然后打印一下。值得注意的是:这个数组是用名字来表达的,不是 arr[10],这个数组不叫 arr[10],这个数组叫 arr。所以,当我们写的时候,我们就写 sizeof(arr)
那这个地方结果是多少呢?结果是40,为什么是40呢?因为我们是10个元素,每个元素是1个 int,一个 int 是4个字节,使用乘起来就是40个字节。所以第363行代码中 sizeof(arr) 计算的是数组的总大小,单位是字节。
假设再写一个 sizeof(arr[0]),arr[0]这是数组的第一个元素,那我们 sizeof 算出它的第一个元素大小又是多少呢?第一个元素大小是4,因为每个元素都是 int,它的第一个元素当然也是 int,如图所示:
当我们这个地方要求这个数组元素个数的时候,是不是就可以这样写了?int sz = sizeof(arr) / sizeof(arr[0]),sizeof(arr)这是总大小,sizeof(arr[0]) 第一个元素大小,所以得到的就是我们的元素个数,我们打印出来结果是多少?是10!我们这个数组就是10个元素,所以 C 语言是如何计算数组元素个数?答案就是 int sz = sizeof(arr) / sizeof(arr[0]) ,如图所示:
在 sizeof 这个地方,我们就讲到这里,当然 sizeof 还有一些其他的补充,就不再继续了,能学完这些也不错
11.5.4 ~
波浪号~ 也是个操作符,它是对一个数的二进制按位取反,那这个操作符是什么意思呢?举个例子,int a = 0,然后打印 ~a,这个代码结果是什么呢?是-1,如图所示:
接下来就讲一讲为什么是-1,波浪号其实刚刚说了,叫按位取反,这个位是二进制位,千万不要以为这个 a = 0,~a 就是把它假变成真,不是这个概念,那我们应该用 !a,而现在~a 就是把所有的二进制位中数字,1变成0,0变成1。a 是个int,它里边放的是0,0的二进制怎么表示呢?0其实是个int,int 是一个32 bit 的整型,那这个时候如果我们要写出32个 bit 的话,我们要写出32个0,00000.....00000这就是0的二进制表示形式,因为0是放到一个 int 变量里边去的,int 变量是4个 byte,一个 byte 是8个 bit,所以要写出32个 bit 位,才能够表示真正的0
当我们写出来之后,~a 叫按位取反,就是把原来的这些二进制位,原来是0的,变成1;原来是1的,变成0。所以很明确的一点,就是变完之后,就变成全1了,这是我们 ~a 的一个结果,那为什么打印出来又是-1?因为整数在内存里边存储的是补码,一个整数的二进制表示有3种:第一种叫原码;第二种叫反码;第三种叫补码。
所以当对我们这个整数 a,它里边存0的时候,对于0来说,它里边存的就是00000000 00000000 00000000 00000000,这是内存里边存的二进制序列。然后按位取反,取反得到结果是11111111 11111111 11111111 11111111,当我们按位取反之后的结果,这个结果依然是内存里边的值,因为在内存里边进行计算的,所以 ~按位取反之后32个全1也是内存的值,所以32个全1就是补码。而当我们算出是一个补码的时候,我们现在以 %d 的形式打印,那我们打印出来应该是它的真实值,即原码。
内存里边存的是补码,而我们要得到原码,然后我们发现它的最高位是1,那表示它是负数,是负数的话,我们就要反着推回来,我们的补码得到反码,才能得到原码。原码取反+1得到补码,所以倒回去的步骤应该是:补码-1得到反码,反码再取反得到原码。
原码得到反码是符号位不变,其他位按位取反;反码得到补码是反码+1,所以倒回去就是-1得到反码,再取反得到原码。然后这个二进制序列如果是补码的话,跟下面的是一样的,倒回去得到的就是10000000 00000000 00000000 00000000,那就是-1。所以当我们对0进行按位取反之后,得到的二进制序列就是-1,所以打印的是-1,如图所示:
现在有一个数字-1,那怎么才能写出它的原码呢?照着这个数字,直接写出它的二进制序列就是原码,一个整数的最高位表示符号位,负数最高位是1,又因为是一个 int,所以要写出32个 bit 位,10000000 00000000 00000000 0000001,我们读出来之后,最高位的1是符号位,后面的数字是有效位,所以后面这一串十进制值的话就是1,所以读出来就叫-1,也就是说,这就是-1的原码,然后符号位不变(最高位叫符号位,符号位后面的叫有效位),其他位按位取反,每一位0变成1;1变成0,这个时候得到的结果叫反码,反码的二进制序列加1,得到的就补码
一个整数的二进制表示形式有3种:可以写出它的原码;可以写出它的反码;也可以写出它的补码。但是在内存中存的补码,也就是说,-1如果要在内存里边存,就是11111111 11111111 11111111 11111111,如图所示:
原反补的计算是针对负数的,对于正整数原码、反码、补码相同,就是写出原码,就是它的反码,就是它的补码
面试题1:~按位取反考虑符号位不变吗?
不用,按位取反是所有位(包括符号位)该变的都变。就比如刚才:原来是0的,变成1;这就是按位取反 ,按位取反不在乎你是什么数字,不论是整数,还是负数,符号位照常变
面试题2:只要看见最高位是1,%d是不是就要取原码?
内存里边的值,如果发现最高位是1,如果是%d 的形式打印的话,那我们就要把它的内存里边的值,先转换成原码,然后才能是我们真实打印出来的值
11.5.5 -- 和 ++
--和++这两个操作符也是非常常见的,而且用的也非常多,还是老样子,int a = 10,然后当我写的时候,我写 int b = ++a,++放在 a 的前面去了,这叫前置++,那这个前置++算的结果到底是是什么呢?我们看一下 b 的结果是什么,我们再看一下 a 的结果是什么,答案是11和11
前置++的运算规律是非常重要的,它是先++,后使用。什么意思呢?第20行代码++a 表达式的结果是要赋给 b 的,b 是取决于 ++a 表达式结果是多少,而这个表达式又是先++,后使用。也就是说,a 原来是10,先++,那这个地方让 a 就变成了11,这个表达式结果就是11,11赋给 b,b 就是11了,当然 a 也以及增长了,也变成11了。如图所示:
所以,前置++的计算规则是先++,后使用
接着我们修改一下第20行代码:int b = a++,其余的都不变,那现在的结果又是多少呢?答案是11和10,为什么是11和10呢?
讲解一下:这个后置++,++放到 a 的后面去了,叫后置++,后置++的计算规则是先使用,再++。什么意思呢?就是 a++ 这个表达式的结果先用,a 的值是10,先用赋给 b,b 的结果就是10了,然后 a 再++,a 变成了11。即先使用 a 的值,使用完之后,再让它++一下,这就是它的一个规则,如图所示:
11.5.6 (类型)
括号()里边放个类型,叫强制类型转换,类型不匹配的时候才会用到它。举个例子,假设 int a = 3.14,如果用%d 去打印 a,这个时候 Ctrl+F7 编译一下代码,会报一个警告,报的这个警告很明确:warning:初始化,从 double 到 int 可能丢失数据,因为3.14是 double 类型的,而 a 是 int 类型的,如图所示:
所以从右边的 double 到左边的 int 会就会丢失精度,所以这个时候它就报这个警告,而如果你以后不想看到这个警告,这个地方能不能避免这个警告出现呢?可以。我们就可以强制类型转换,怎么强制类型转换呢?3.14是个 double 类型,而 a 是个 int 类型,我们希望强转成 int 类型,所以在3.14前面放个括号,括号里边放个 int 就可以了,可以把一个双精度浮点型强转成一个 int,如图所示:
括号()里边放个类型,叫强制类型转换,类型不匹配的时候才会用到它。当然我们也不推荐使用强制类型转换,如果在使用的过程中类型不匹配,说明设计的时候可能就会有一些缺陷,但不管怎么说,这种语法是存在的
11.6 关系操作符
关于关系操作符,有一个东西讲的是,我们原来在数学里边可能写大于等于是 ≥,小于等于是 ≤。但是在 C 语言里边大于等于是 >=;小于等于是 <=;等于是两个 == 来判断的;!= 这叫不等于。
11.7 逻辑操作符
11.7.1 并且&&
逻辑操作符其实描述的就是我们生活中的并且、或者的关系,我说我想买蔡徐坤的篮球🏀和背带裤,篮球🏀并且背带裤,那我们说的是篮球🏀和背带裤都要买,那这个时候我说的是:篮球🏀和背带裤同时都要买,它俩都得买,这才是满足我的要求的,所以对于逻辑与,它其实描述的是满足条件真假的问题
举个例子,int a = 3,int b = 5,然后如果 a 并且 b,a 现在是3,b 现在是5,a 并且 b,a 和 b 现在都为非0,都为真,并且就是左边为真,右边为真,并且就是两边都为真的时候,中间函数体才为真,整个表达式才为真,如图所示:
再比如,int a = 3,int b =5,int c = a && b,然后打印 c,这个时候 a 是真,b 是真,并且肯定也是真,如图所示:
这就是并且的意思,假设 b 是个0,这两个里边有一个为假,发现结果是0,并且里边只要有1个为假,结果就为假了。两个同时为真,结果才为真。就说我说篮球🏀和背带裤这两个,篮球🏀并且背带裤我想买,那就是两个都买才满足我的条件,有一个没买,那就不是我的预期结果,这个事情就为假了,没有成功。如图所示:
11.7.2 或者||
篮球🏀或者背带裤我想买,那篮球🏀和背带裤买一个就可以了,所以 a 和 b,现在说 a 为真,a 是3;b 为假,那这个时候我买了其中一个,结果就是真了,买一个东西就满足条件了。如图所示:
假设 a 和 b 都是0,篮球🏀和背带裤都没有买,都为假,所以也就为假,如图所示:
而两个都买呢,那就更好了,还是为真,如图所示:
11.8 条件操作符
什么叫条件操作符呢?也叫三目操作符,它是“问号”“冒号”的这样一种形式来表现的,exp 1 ?exp 2 :exp 3这样一个形式来呈现的。对于我们这个地方的“问号”和“冒号”这个操作符来说,它其实有3个操作数的,一个是表达式1,一个是表达式2,还有一个是表达式3。这三个东西就是3个操作数,所以它也叫三目操作符,如图所示:
那这个三目操作符具体是怎么描述的呢?其实是这样的:
exp1如果成立,exp 2计算,整个表达式的结果是 exp 2的结果
exp1如果不成立,exp 3计算,整个表达式的结果是 exp 3的结果
那我们具体举例子吧,在这讲一个代码,还是假设 int a = 0,int b = 3,int max = 0,我想求出 a 和 b 的较大值,我们怎么求呢?我说 if(a>b),那较大值就是 a,max 就被赋成了 a,else 的话,max 就被赋成了 b。这样的话 a 和 b 的较大值我们放到 max 里边去了, a 如果大于 b 的话,那我们 a 较大值就放到 max 里边去,否则 b 较大值就放到 max 里边去,然后打印 max 就行,如图所示:
但是,有的人肯定会说第75-82行代码有点多,如果我们要三目操作符能表达的话,怎么表达呢?也很简单, a 如果大于 b 这个表达式成立的话,问号,然后整个表达式的结果就是 a 的结果。冒号,否则就是 b 的结果。刚刚 if 语句写成的代码,我们用一句代码就搞定了,然后打印 max 就行,如图所示:
当我写出 max = a > b ? a : b 代码的时候,和 exp 1 ?exp 2 :exp 3代码就对应起来了,a > b 就是 exp1,表达式1如果成立,表达式2计算(a 计算),但是这个没什么可计算的,整个这个表达式的结果就是 a 的结果,a 赋给 max,否则 b 赋给 max,如图所示:
总结:未来有合适的场景,我们也可以用我们的三目操作符来表示这种逻辑,很简单,表达式1的结果如果为真,表达式2要计算,表达式2的结果是整个表达式的结果,否则就是表达式3的结果
11.9 逗号表达式
逗号表达式是逗号隔开的一串表达式,逗号表达式其实更简单一些
在这里简单的举个例子,括号里边写个 (2,4+5,6),这就是个逗号表达式,由逗号隔开的一串表达式,只不过这个表达式好像没产生什么效果。6就是6,4+5也就是4+5,但是它算不算逗号表达式呢?算,就是逗号表达式。如图所示:
当然我们也可以这么写,int a = 0,int b = 3,int c = 5,int d = (a = b + 2,c = a - 4,b = c + 2),这就是个逗号表达式,括号里边写的这个就是逗号表达式。
那这个逗号表达式它的特点是什么呢?逗号表达式是从左向右依次计算的,整个表达式的结果是最后一个表达式的结果
接下来,我们就打印一下 d,根据从左向右依次计算:首先 b+2赋给 a,这个表达式要算,b 是3,加2就是5了,a 变成了5了;也就是说,这一步完了之后,a 现在变成了5
然后再往后,c = a -4,a 是5,5-4=1,所以 c 就变成了1
然后 b = c +2,c 就是1,所以 b 就变成了3,
最后这个表达式的结果就是3,3赋给 b,这个表达式的结果就是3,如图所示:
所以,更加坚定的一点:逗号表达式一定要从左向右依次计算,它不可能凭空就算到后面去了,这是不可能的,整个表达式的结果是最后一个表达式的结果
11.10 下标引用、函数调用和结构成员
11.10.1 下标引用操作符
[ ] 这个操作符用于什么呢?举个例子,写个数组 int arr[10] = {1,2,3,4,5,6,7,8,9,10},假设我们把它里边的一个元素拿出来,我们要把它的第6个元素拿出来,第6个元素的下标是多少呢?是5。
所以我们写出 arr[5]的时候,就是我们的第6个元素,我们可以把它打印出来,我们说数组的下标是从0开始的,数组是由下标来访问的,如图所示:
我们可以看到:这个结果就出来了,确实打印出来了我们的6。那这个时候注意:第104行代码,使用的这个[ ]就是下标引用操作符,数组访问元素的时候,会用到一个[ ],这个[ ]下标引用操作符。
注意:第103行代码中使用的[ ]不是下标引用操作符,这是我们在定义数组的时候的一种语法
11.10.2 函数调用操作符
什么是函数调用操作符呢?举个例子,比如说我现在想在屏幕上打印一个 jiniitaimei ,那这个时候,调用这个 printf 函数的时候,后面这个地方有个圆括号,这个圆括号就是函数调用操作符。因此,调用函数的时候,函数名后边的()就是函数调用操作符,
也就是说,第114行代码这一对圆括号,就是函数调用操作符,它是个操作符,如图所示:
当然,我们还可以这样写:(%d, 100),我们打印个100,对于第114行代码这个圆括号来说,它这个操作符有几个操作数呢?在第114行代码传参,好像就传了一个字符串,感觉好像是1个参数。第115行代码传参,也可以传2个参数。如图所示:
因此,函数调用操作符可以传1个或多个参数,一个参数不传也行,它比较特殊一些。
11.10.3 结构成员操作符
请看第15章中的15.3.1 和15.3.3
补充:
1. * 是一颗星,就是说我们在 C 语言里边是敲不出乘号的,除非敲个x,那是x,是数学里的乘号,不是 C 语言里的乘号,C语言里的乘号用 * 号代表
2. 最后一位永远补0(如果左移的话),这是规定死的
3. %是取模、取余
4. >>(右移操作符) <<(左移操作符) { 右移左移主要是看箭头!}
5. 移位操作符移的是二进制位
6. 其实对于右移来说,也是一样的,就是把它的二进制位向右移动,但是具体细节,我们现在这里先不讲解
7. 整数在内存里边存储的是补码,一个整数有3种表示形式:原码、反码、补码。而 %d 打印出来的值是原码,实际上内存里边存的是补码。对于正整数来说,它的原码、反码、补码相同,当我们写出它的原码的时候,就是它的反码,就是它的补码。
8. 正数的原反补一样,这是规定,这个没有为什么(就比如说为什么有个正数和负数,这个没法解释)