一、算术操作符
以下是在使用算术操作符的注意事项:
①当操作符左右两边都是int 类型整数,那么计算结果也为int 类型整数(两个整数相除,如果不是整除,会舍掉小数点后面的数)
②当操作符两边只要有一边的数是小数,那么两边的数都会变为浮点型(小数)再计算,计算结果也为浮点型(小数)。
③对于%取余操作符,该操作符左右两边必须为int 类型的整数,否则编译器报错,无法运行。
我们简单写个代码来使用这些算术操作符:
二、二进制及其操作符
1. 二进制的概念
在说二进制之前,我们聊聊我们最熟悉的十进制。
我们一直所学的数学基本上都是使用十进制表示,例如:333,十进制是使用0~9的数字表示,每逢10进1。
那么,二进制跟十进制很像,只不过是使用0~1的数字表示(即0或1),每逢2进1,例如:10110。
同理:对于之后会涉及到的8进制和16进制也是这样的,8进制使用0~7的数字表示,每逢8进1,例如:357。
这时,有人肯定会想16进制里如何表示10~15的数字呢,规定使用了字母来表示10~15的数字,a表示10,b表示11,c表示12,d表示13,e表示14,f表示15。所以16进制使用0~9和a~f表示,每逢16进1,例如:39c。
2. 进制转换
1️⃣2进制转换为10进制
在10进制中,例如123,我们可以写成:1*10^2 + 2*10^1 + 3*10^0;1在百位,2在十位,3在个位,而个位,十位,百位......等的权重分别是10^0, 10^1, 10^2……
而在2进制中,我们同样有权重,例如10110,可以写成:1*2^4 + 0*2^3 + 1*2^2 + 1*2^1 + 0*2^0;
所以,对于一个2进制数,我们只需要将每一位乘以对应的权重后相加,即可得到十进制的值。
2️⃣10进制转换为2进制
对于一个10进制数,我们只需要将该数不断除以2(直至为0),每次的得到余数放到上一个前面一位(第一个余数在最后一位,最后的余数在第一位)。
我们以22为例:
3️⃣8进制和16进制与2进制的互相转换
对于8进制:我们都直到8是2的3次方,也就是说2进制中3个位等价于8进制中1个位,例如:110110110111101,我们将这个2进制数用8进制表示:66675
将8进制改为2进制就是相反的过程,相信大家也已经会操作。
对于16进制:16是2的4次方,也就是说2进制中的4个位等价于16进制中的1个位,例如:10110110,这个2进制数用16进制表示:b6
16进制转换成2进制也是这个相反过程。
3. 原码,反码,补码
对于一个2进制数,有三种表现形式:原码,反码,补码。
对于有符号数,最高位表示符号位:0表示正数,1表示负数。其余的位表示数值。
对于无符号数,所有数都表示数值,因为没有符号位,所以只能表示正数。
对于一个正数,原码,反码,补码都是一个值。
对于一个负数,这三个表现形式都不相同:
原码:最高位1表示负号,其余位表示除了负号以外的数值,通过我们上面的求法得到。
反码:将除了符号位的所有数值位上的数依次按位取反(0变为1,1变为0)。
补码:在反码的基础上+1就是补码。
注意:数据存储在计算机中是以补码形式存储的。
有人可能会想:为什么数据存储要使用补码,原码不是挺方便得到吗?
我们通过一个小例子来告诉大家:假设我们要计算3 + (-8)的值
如果我们直接使用原码计算:
那我们使用补码来计算:
想必大家也清楚为什么使用补码,就是方便计算,能够将符号位和数值位统一处理,而且加减法也可以统一处理(CPU只有加法处理器)。对于补码转换成原码,也只需要取反后+1即可。
4. 移位操作符
移位操作符是对2进制数操作,且操作数只能是整数。
1️⃣ << 左移操作符
我们先来感受一下左移操作符的效果:
我们能看到,i的值变为原来的2倍。
那么,<<左移操作符是怎么操作的呢?将左边的操作数的2进制形式,全部向左移右边操作数值的位,例如:i<<1, 将i向右移1位,j<<3,将j向右移3位。对于右边的空位用0补充。
由于是对2进制进行操作,所以我们通过2进制具体感受左移操作符如何操作。
由此,我们可以推断出,a<<b,这个表达式的结果为a*2^b。
注意:a<<b,这个表达式不会改变a的值,就行a+1一样,不改变a的值。
在这里我们要注意:左移操作符会将符号位上的数也向左位移,使得数值位上的数成为新的符号位的值,如果数字较大或者左移的位较大,左移后可能改变符号(有符号数)。
2️⃣ >> 右移操作符
右移操作符和左移操作符十分相似,操作符同样只能是整数,依然是a>>b,a中的数向右移b位,
相当于a / 2^b。但是,右移时对于有符号数因为要位移符号位上的数,补充的数也是直接补充到符号位上,那么我们究竟是直接补充0呢,还是补充原先的符号位上的数呢?(无符号数都是补充0)
这就涉及到两种位移方式:算术右移和逻辑右移。
逻辑右移:就是直接补充0。 算术右移:补充的数是符号位上的数。
那么这两种方式有什么区别呢?由于正数的符号位是0,补充的数相同,没有区别;
但是,对于负数,逻辑右移后,结果变为了正数,不符合数学上的除法;而算术右移后结果任然为负数,更贴近数学的除法。(由于VS采用的是算术右移,这里只能展示算术右移的结果)。
警告:有些小聪明可能对右边操作数有些想法,想让其为负数,也就是a>>b,b为负数,这个结果是未定义的,这个表达式究竟是想要向右移还是向左移,如果向左移,那么使用<<即可,为什么会出现负数。
5. 位操作符
位操作符同样,操作数必须是整数,且是对2进制进行操作。总共有这4种位操作符:
(这些位操作符同样也会对符号位进行操作)
要与这几个操作符区分开:
1️⃣ & 按位与
例如:
2️⃣ | 按位或
3️⃣ ^ 按位异或
4️⃣ ~ 按位取反
6. 二进制的相关练习
1️⃣一道变态的面试题
题目:不能创建临时变量(第三个变量),实现两个数的交换。
首先:我们思考一下使用临时变量咋写的,创建一个变量t,使得t = a,a = b,b = t即可。
既然不能使用临时变量,那么我们思考一下我们刚学会的位操作符(我们此时只能依赖它)。
a&b?好像不太行,遇到0都变为0了; a|b?依然不太行,遇到1都变为1了;~只能有一个操作数,也不行。此时只剩下了^按位异或,它的功能是相同为0,相异为1,那么a^a不就是0,a^a^b就是b了,恍然大悟,我们让a = a^b; b = a^b; a = a^b即可了,而且没有创建临时变量。
2️⃣编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数
为了判断2进制中1的个数,我们很自然的想到对每一位进行判断,是1就使计数器+1。那么我们如何对每一位判断呢?&能够使遇到0就为0,那么我们让a&1,不就能判断最低位的数了,然后让a右移,循环32次(int类型有32个位)。
这个虽然能得到答案,但是需要循环32次,有没有更高效的方法呢?
我们来看看这个代码:
每次-1 使得a2进制中的最低位1减少一个,a & (a-1)后,使得减少的1后面的数重新变为0,如此循环,最终所有含1的位都计算了,得到结果,这个循环次数就是a中1的个数。
3️⃣⼆进制位置0或者置1
让2进制某一位置1,只需要使得那个位置是1,其他位置是0的数 | a,这个数相比能够很容易得到:1 << (n - 1),n为置换为1 的位置,即让a中那个位置置1;
让2进制某一位置0,需要使那个位置是0,其他位置是1的数 & a,如何得到这个数呢,我们可以使用 b = 1 << (n - 1),n为置换的位置,然后 ~b就是我们需要的数,最后 (~b) & a。
三、赋值操作符
1. 赋值操作符 =
赋值操作符能够让变量变为我们想要的值,可以对变量多次赋值。
注意:要区分 = 赋值操作符和 == 等于操作符
2. 复合赋值操作符
会改变a的值
通过一个例子让大家更好理解一下:
结果:
四、单目操作符
1. ++自增操作符,--自减操作符
同样,会改变a 的值,来个例子更好理解一下:
在自增,自减中,有前置和后置之分:
大家可以思考打印结果是什么。
结果:
2. & 取地址操作符
取地址&:对一个对象使用该操作符,会返回该对象在内存中的地址,例如:&a,返回a的地址。
我们通过一个代码理解一下:
3. * 解引用操作符
解引用*:对于一个地址或含有地址的变量(指针)使用该操作符,会返回这个地址中存放的数据,例如:*p(p是一个地址(指针)),返回p指向的地址的数据。
同样来个实例:
4. sizeof
sizeof() 可以计算括号中变量的内存大小或者一个数据类型的内存大小(单位:字节)
我们清楚:int 类型在内存中占4个字节,char占1个字节,double占8个字节,我们来看看能不能得到对应的结果。
在这里,有些人看到sizeof需要括起对象,认为sizeof是一个函数,只有函数才会这么写,那我们来看看下面这些代码:
结果证明,sizeof不需要括号也能计算变量内存大小,所以sizeof不是函数,而是操作符。
注意:sizeof的对象如果是一个数据类型,必须使用括号将该数据类型括起来,否则出错。
5. 强制类型转换
强制类型转换:当我们想要江一中数据类型的变量中的值赋值给另一个数据类型的变量时,就需要强制类型转换成被赋值的变量的数据类型。
为了更好理解,举了下面的例子:
所以,我们只需要在右边的变量前面加个()括号里是右边变量的数据类型。
五、关系操作符
这些关系操作符用来判断两个对象之间的大小关系,如果表达式结果为真,返回1,结果为假返回0
对于关系操作符有一点特别要注意:
如果是比较两个字符串的大小,比较的不是字符串中内容的大小,而是比较的两个字符串地址的大小,我们对字符串使用可能得不到我们想要的结果,要使用strcmp函数比较,在字符串函数中讲。
这个结果不符合我们的预期,所以最好别对字符串使用关系操作符。
警告:不要连续使用关系操作符,可能结果不符合预期。
如何计算这种表达式呢?在逻辑操作符中讲解。
六、逻辑操作符
1. && 逻辑与操作符
我们可以使用逻辑与操作符完成那个连续比较:
2. || 逻辑或操作符
3. !逻辑反操作符
4. 判断一个年份是否是闰年
闰年的定义:一个年份如果能被4整除但不能被100整除,或者能被400整除
等价于: year % 4 == 0 并且 year % 100 != 0 ,或者 year % 400 == 0
所以代码是这样:
七、条件操作符
我们通过一个代码使用一下:
八、逗号表达式
逗号表达式:用逗号将多个表达式分隔开。
逗号表达式,从左向右依次执行。整个表达式的结果是最后表达式的结果。
大家思考一下下面这个代码输出什么。
结果:
计算过程:
九、其他操作符
1. 下标引用:[ ]
操作数:一个数组名 + 一个索引值
例如:arr[3],arr是数组名,3是索引值。
我们创建并使用数组来用一下这个操作符:
2. 函数调用:( )
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。
例如:Add(a,b);Add是函数名,a和b是传递给函数的参数。
我们就写个加法的函数作为例子:
十、 结构体成员访问操作符
1. 结构体
C语言中已经提供了内置数据类型,例如:int,char,double,float等,但是在日常生活中,我们发现只有这些数据类型是完全不够的,例如:我们要描述一个人,一个人要有:名字,年龄,身高,体重,身份证号……C语言为了解决这些问题,增加了结构体这种自定义数据类型,让程序员可以创造自己需要的数据类型。
在介绍结构体之前,我们会想一下数组的定义:数组是一组相同类型元素的集合;
结构体:由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构。
是不是跟数组有点类似,但结构体中的成员变量可以是标量,数组,指针,甚至其他结构体等,成员变量可以是不同数据类型,而数组中的元素必须是相同的数据类型。
1️⃣结构体声明
struct tag
{
member-list;
}
例如:我们声明一个描述人:姓名,年龄,身高,体重,身份证号的结构体:
2️⃣结构体变量的定义和初始化
定义变量有两种定义方式:1. 在声明结构体时就定义变量;2. 在声明结构体之后定义变量
1. 我们写个例子感受一下:
2. 也用例子感受一下:
初始化:
我们还可以指定顺序初始化:
结构体嵌套初始化:
2. 结构体成员访问操作符
1️⃣结构体成员的直接访问 (.)
结构体成员的直接访问是通过点操作符(.)访问的。
点操作符接受两个操作数:一个是结构体变量名,一个是结构体成员名。
例如:boy.name,boy是结构体变量名,name是结构体成员名
2️⃣结构体成员的间接访问 (->)
假设我们得到的是一个结构体变量的地址(指针),我们应该怎么通过这个指针访问该结构体里的成员呢?这就是->发挥作用的时候。
->操作符接受两个操作数:一个是结构体指针,一个是成员名。
例如:p->name,p是结构体指针,name是成员名。
十一、操作符属性
C语⾔的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
1. 优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是 不⼀样的。
我们都知道,* / 的优先级高于+ - ,对于这个表达式:3 + 5 * 2,结果是13,是因为先计算5 * 2,得到结果为10,再用3 + 10,得到最终结果13。
由于操作符的优先级顺序有很多,在这里建议记住以下部分常用的优先级顺序就行(按照优先级从高到低排序),使用到其他操作符时可以查看下面的总表格。
- 圆括号(())
- 自增运算符(++),自减运算符(--)
- 单目运算符(+和-)
- 乘法(*),除法(/)
- 加法(+),减法(-)
- 关系运算符(<, >等)
- 赋值运算符(=)
- 注意:括号的优先级最高,可以使用括号改变其它运算符的优先级。
2. 结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符 是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右 结合(从右到左执⾏),⽐如赋值运算符( = )。
同样,对于 + - 这两个优先级相同,那么对于 3 + 5 - 2怎么计算呢,这就涉及到了结合性,对于+ -
的结合性,是从左到右的,所以,先计算3 + 5得到结果8,再计算8 - 2得到最终结果6。