【C语言】操作符

目录

进制

字节

原码、反码与补码

移位操作符

位操作符

按位取反操作符

sizeof

条件操作符

逗号表达式

隐式类型转换(整形提升)

算术转换 

操作符的优先级、结合性


进制

 进制也就是进位计数制,是人为定义的带进位的计数方法。

 一、常见进制

 二进制

基数为2,使用0和1两个数字来表示数。在计算机中,二进制被广泛应用,因为计算机的硬件电路可以很方便地表示0和1两种状态

十进制

基数为10,使用0 1 2 3 4 5 6 7 8 9 这十个数字表示数。这是我们日常生活中最常用的进制。

十六进制

使用0到 9以及A 到F这十六个符号来表示数,其中A 到 F 分别表示10 11 12 13 14 15。十六进制常用于表示计算机内存地址等信息,因为它可以用较短的符号表示较大的数

二、进制转换
 

十进制数 → R进制数
● 分整数和小数两部分处理:
● 整数部分:连续除以R,取余反序排列,直到商为0。
● 小数部分:连续乘以R,取整正序排列,直到乘积的小数部分为0,或者满足误差要求。

例如:

十进制数 13 转二进制

13 ÷ 2 = 6 ⋯ ⋯ 1

13÷2=6  ⋯⋯     1

6 ÷ 2 = 3 ⋯ ⋯   0

6÷2=3⋯⋯         0

3 ÷ 2 = 1 ⋯ ⋯   1

3÷2=1⋯⋯         1

1 ÷ 2 = 0 ⋯ ⋯    1

1÷2=0⋯⋯1 从下往上将余数排列得到1101,就是13的二进制表示。

十六进制转二进制:每一位十六进制数可以转换为4位二进制数

例如,十六进制数2A,2转换为二进制是0010,A(即10)转换为二进制是1010,所以2A转换为二进制就是00101010。

二进制转十六进制:将二进制数从右到左每4位一组,分别转换为十六进制数。

例如,二进制数11011010,分组为1101和1010,1101转换为十六进制是D,1010转换为十六进制是A,所以11011010转换为十六进制是DA。

八进制与二进制互化方法与上述相似:


原码、反码与补码

计算机中的整数有三种2进制表示方法 , 即原码、反码和补码。
三种表示方法均有符号位数值位两部分,符号位都是用 0 表示 “正” , 用 1 表示 “负”

正数的原码、反码、补码是相同的

负数的原码、反码、补码要经过计算的

原码

按照一个数的正负 , 直接写出它的二进制表示形式得到的就是原码
 

反码

反码是原码的符号位不变 , 其他位按位取反
 

补码

补码是反码+1

例:
整型占4个字节 (32bit) 

如 10 的:
00000000000000000000000000001010 - 原码
00000000000000000000000000001010 - 反码
00000000000000000000000000001010 - 补码
-10 的
10000000000000000000000000001010 - 原码
111111111111111111111111111111110101 - 反码
111111111111111111111111111111110110 - 补码

原码、反码、补码的互相转化:

(取反:符号位不变 , 其他位按位取反,减去一不好得出结果时,也可以取反+1)

在计算机中,为什么要用用补码存储一个数呢?


在计算机系统中 , 数值一律用补码来表示和存储。原因在于,使用补码 , 可以将符号位和数值域统一处理 ; 同时 , 加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

一个简单的例子:

如果计算机用原码存储一个数:

00000000 00000000 00000000 00000001 - 1 的原码

10000000 00000000 00000000 00000001 - 负1的原码

我们计算 1 - 1 就是 1 + (-1)

10000000 00000000 00000000 00000010 - 1 + (-1)的结果是 -2

这就出现问题了,结果显然不对。

如果我们使用补码

00000000 00000000 00000000 00000001 - 1 的补码

11111111 11111111 11111111 11111111 - 负1的补码

现在用 1 的补码和 -1 的补码相加:

1 00000000 00000000 00000000 00000000

由于整形占 4 个字节,所以最高位的那个 1 会溢出(截断),最终的结果是:

00000000 00000000 00000000 00000000 

这样结果就正确了,所以计算机要用补码存储一个数。
 


移位操作符

左移操作符:b = a << m,作用:

a的补码左边丢弃m位,右边补m个零,然后作为b的补码赋值给b,a 的值没有变化

右移操作符:b = a >> m,作用:

1、算术右移(大多数)

a的补码右边丢弃m位,左边补上原来的符号,然后作为b的补码赋值给b,a 的值没有变化

(这个操作等价于将 a 除以 2 的 m 次幂,比如 a = a >> 1;等价于 a /= 2;)

2、逻辑右移(略)

左移操作符例子:

int a = 10; 10 的 补码:00000000000000000000000000001010

int b = a << 1; a << 1 :  00000000000000000000000000010100  — 值为 20 赋给 b

但 a 的值没有变化

printf("%d",b)//打印出来的是原码

若a为负数,如:

int a = -10;

-10 的
10000000000000000000000000001010 - 原码
111111111111111111111111111111110101 - 反码
111111111111111111111111111111110110 - 补码

int b = a << 1;

a的补码左移一位,作为b的补码:

111111111111111111111111111111101100 - b的补码

由b的补码得到b的原码:

111111111111111111111111111111101100 - b的补码

111111111111111111111111111111101011 - b的反码

10000000000000000000000000010100 - b的原码


位操作符

按位与:c = a &

作用:a 和 b 的补码中,对应位的二进制数有 0 则 c 的对应位为 0,都为 1 则 c 的对应位才为 1

得到的结果作为 c 的补码。

一个神奇的表达式:

a = a & ( a - 1 )

这个表达式的作用:每执行一次,a 的二进制表示去掉最右边的一个 1,可以用来计算一个数的二进制表示中有多少个一。

(对于一个二进制数,减去 1 一定能将最右边的 1 所在的数位变成 0 ,如 0000000 1 减去 1 变成 0000000 0,000000 0 减去 1 变成 000000 1 ,再 “与上”减 1 之前的数,减 1 之前的数最右边的 1 对应的一定是 0)

十进制数中 2 的 k 次方的二进制表示只有若干 0 和一个 1。

(十进制)2 =(二进制)00000010

(十进制)4 =(二进制)00000100

(十进制)8 =(二进制)00001000

利用这个性质,可以设计出一个检查一个数是不是 2 的 k 次方的代码,只需一条 If 语句搞定:

if (a & (a - 1) == 0)

按位或:c = a | b

作用:a 和 b 的补码中,对应位的二进制数有 1 则 c 的对应位为 1,都为 0 则 c 的对应位才为 0

得到的结果作为 c 的补码。

按位异或:c = a ^ b

作用:a 和 b 的补码中,对应位的二进制数相同则 c 对应位为 0 ,不同则 c 对应位为 1 ,

得到的结果作为 c 的补码。

性质:1、a ^ a = 0

           2、a ^ 0 = a

           3、a ^ b ^ c = a ^ c ^ b ( 交换律 )

           4、(a ^ b) ^ c = a ^ (c ^ b) (结合律)

应用举例:(不使用中间变量,交换两个变量的值)

int a = 5;

int b = 3;

a = a ^ b;----1

b = a ^ b;----2    把 1 式当作公式代入:b = a ^ b ^ a;而 a ^ b ^ a 等于 a 

a = a ^ b;           把 2 式当作公式代入:a = a ^ a ^ b;而a ^ a ^ b 等于  b

其实这种方法效率不如使用中间变量,且可读性差,只能交换整数。也可以使用这种方法:

int a = 5;

int b = 3;

b = a + b;//可能会溢出

a = b - a;

b = b - a;

但若 a、b 都是很大的数字,则 b = a + b;这一步可能会溢出。


按位取反操作符

b = ~ a;

作用:将 a 的原码按位取反(包括符号位),然后作为 b 的补码赋给 b 。

如:

int a = 0,b = 0;

b = ~ a;

a 的:

原码:00000000000000000000000000000000

取反:1111111111111111111111111111111111111 —— 把它作为 b 的补码 ,b 的值为 -1 。

移位操作符、位操作符、按位取反操作符都是针对二进制数的运算符。

综合运用:

1、修改一个变量的 bit 位

想要修改一个变量的 bit 位,用指针是不行的,因为指针访问的是字节。

可以使用移位操作符、位操作符、按位取反操作符:

将一个数的二进制表示的第 x 位修改为 1:

a |= (1 << x);

将一个数的二进制表示的第 x 位修改为 0:

a &= ( ~ (1 << x) );

int main()
{
    int a = 9; 
    //00000000000000000000000000001001 
    //00000000000000000000000000010000 1 << 4
    //把a的二进制中第5位改成1 
    a |= (1 << 4); 
    
    //把a的二进制中的第5位改回来,变成0 
    //00000000000000000000000000011001 
    //11111111111111111111111111101111 
    //00000000000000000000000000001001
    a &= (~(1 << 4));

    return 0;
}

 2、得到二进制任一位的数

( m >> i ) & 1


sizeof

sizeof 是既是操作符,也是关键字。

作用:计算并返回操作数的类型长度(以字节为单位,返回值的类型是 size_t ,这是专门为 sizeof 运算符创造的类型,它其实是 unsigned int 类型)

 操作数可以是类型名、普通变量、数组名、数组元素等。

 函数调用的时候要加( ),但 sizeof 可以不加 ,说明 sizeof 不是函数:

但是如果 sizeof 的操作数是类型名的时候,( )不能省略:

sizeof int -----错误

请分析以下代码:

 为什么 sizeof(a = b + 1) 这个表达式的值为 2 ?

sizeof(a = b + 1) 中明明已经将 b + 1 赋给了 a ,为什么 a 的值仍然是 1 ?

这是因为:

 sizeof(a = b + 1) 中将整形数据赋值给短整形数据,结果为短整形

sizeof 在编译时已确定结果

sizeof(a = b + 1) 这条语句在编译时已确定为 2,在运行时相当于:

printf("%d\n", 2);

sizeof ( ) 括号内的表达式不计算

所以 a 的值依然是 1 。

字符串长度的求法
1、strlen函数,如 char a[ ] = "abc" 则 strlen(a)= 3 , 不包括\0
2、sizeof操作符,如 char a[ ] = "abc" 则 sizeof(a)= 4 ,包括\0 ,

字符数组的最后一个元素的下标 = sizeof(a)/sizeof(a[0]) - 2

减去2是因为要减去 \0 和数组下标从0开始
 

sizeof 与数组:

上图中,a 是数组名,a 数组有 10 个类型为 int 的元素, sizeof (a)计算的是数组的所有元素的类型所占字节的总和,也就是一个数组占用的字节数。

请分析以下代码:

为什么 test 函数中 sizeof(arr) 的值是 8 呢?不应该是 40 吗?

这是因为:

数组传参传的是指针,即使形参写成数组的形式,本质上还是指针。

所以 arr 其实是指针变量,sizeof(arr) 计算的是指针变量 arr 所占的字节数,而不是数组所占的字节数。

数组的类型:
int arr[5];   int 是数组元素的类型, int [5] 才是数组的类型
printf("%zd\n",sizeof(arr));  //输出5*sizeof(int)=20
printf("%zd\n",sizeof(int [5]));  //输出5*sizeof(int)=20

数组名表示整个数组只有两种情况:

1. sizeof(数组名) , 数组名表示整个数组。计算的是整个数组的大小

只有数组名单独放在括号里才表示整个数组,若 a 是一维数组名,则:

sizeof(a+i),此时 a + i 表示 a[ i ] 这个数组元素的地址,只要是地址,它的大小就是 4 字节或 8 字节。所以 sizeof(a+i)这个表达式的结果就是 4 或 8。

sizeof(&a),&a 是整个数组的地址,只要是地址,它的大小就是 4 字节或 8 字节,所以 sizeof(a+i)这个表达式的结果就是 4 或 8。

sizeof(*&a)等价于 sizeof(a),这个表达式的结果是整个数组所占的字节数

2. &数组名 , 数组名表示整个数组。取出的是整个数组的地址

除此之外 , 所有的数组名都是数组首元素的地址

//一维数组

int a[ ] = { 1,2,3,4 };// 所占字节数 4*4=16

printf("%d\n", sizeof(a));//16

printf("%d\n", sizeof(a + 0));//a+0 其实是数组第一个元素的地址,是地址就是 4/8 字节

printf("%d\n", sizeof(*a));//*a是数组首元素,计算的是数组首元素的类型(int)大小,4

printf("%d\n", sizeof(a + 1));//a+1是第二个元素的地址 , 是地址大小就是 4/8

printf("%d\n", sizeof(a[1]));//a[1] 是第二个元素,计算的是第二个元素的类型(int)大小,4

printf("%d\n", sizeof(&a));//&a是整个数组的地址,整个数组的地址也是地址 , 地址的大小就是4/8字节//&a --- > 类型: int(*)[4]

printf("d\n", sizeof(*&a));//&a是数组的地址,*&a就是拿到了数组 , *&a -- > a , a就是数组名 , sizeof(*&a) -- >sizeof(a) /计算的是整个数组的大小,单位是字节 - 16

printf("d\n", sizeof(&a + 1));//&a是整个数组的地址 , &a+1,跳过整个数组 , 指向数组后边的空间 , 是一个地址 , 大小是4/8字节

printf("%d\n", sizeof(&a[0]));//&a[0]是首元素的地址 , 计算的是首元素地址的大小 , 4/8 字节

printf("%d\n", sizeof(&a[0] + 1));//&a[0] + 1是第二个元素的地址 , 地址的大小就是 4/8 字节
 

 //字符数组

char arr[] = { 'a' , 'b' , 'c' , 'd' , 'e' , 'f' } ; \\ f 后面没有 \0
printf("%d\n", strlen(arr));//从‘a'开始找 \0 , 随机值

printf("%d\n", strlen(arr + 0));//也是从‘a'开始找 \0,随机值

//printf("%d\n", strlen(*arr));//strlen('a')->strlen(97) , 非法访问(0x00 00 00 61 这个地址未分配,不能访问)-err

//printf("%d\n", strlen(arr[1]));// 'b' - 98 , 和上面的代码类似 , 是非法访问 - err

printf("%d\n", strlen(&arr));//&arr虽然是数组的地址 , 但是也是从数组起始位置开始找 \0 的 , 还是随机值

printf("%d\n", strlen(&arr +1)); //&arr是数组的地址 , &arr+1是跳过整个数组的地址,求字符串长度也是随机值

printf("%d\n", strlen(&arr[0] + 1));//&arr[0] + 1是第二个元素的地址,是 'b' 的地址 , 求字符串长度也是随机值


条件操作符

条件操作符,也称为三目操作符

通常的语法形式是:condition? expression1 : expression2 

作用:如果condition为真(或满足某些条件),则计算并返回expression1的值;否则,计算并返回expression2的值。


逗号表达式

基本语法:使用逗号将两个或多个表达式分隔开,例如:expression1, expression2

求值顺序:从左到右依次对每个表达式进行求值,整个逗号表达式的值是最后一个表达式的值。

 应用场景

for循环中的初始化和更新:例如:

for(int i = 0 ,  j = 10; i < j; i++ ,  j--) { // 循环体 }。

函数调用中的多个表达式:例如:

int a = 1 ,  b = 2;

printf( "%d\n"  ,  ( a += 1 ,  b += 2 ,  a*b) ); // 输出 6。

赋值语句中的多个操作

逗号表达式可以用于在单个赋值语句中执行多个操作。例如:

int x;

x = (1 ,  2 ,  3);  // x的值为3。


隐式类型转换(整形提升)

C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。

为了获得这个精度,表达式中的字符(1个字节)短整型(两个字节)操作数在使用之前被转换为普通整型(int),  这种转换称为整型提升

整型提升的意义:
       

         表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。                                                           

        因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。

        所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

例如:

char a = 1;

char b = 2;

char c = a + b;

a 和 b 的值被提升为整形,然后再执行加法运算。

加法运算完成之后,结果将被截断,然后存储于 c 中。

如何进行整形提升?

整形提升是按照变量的数据类型的符号位来提升的,有符号的数据类型整形提升时补码的高位补充符号位的数,无符号的数据类型整形提升时补码的高位补充0.

以 char 为例:

char c1 = - 1;

变量c1的二进制位(补码)中只有8个比特位:

11111111

因为 char 为有符号的 char 所以整形提升的时候 , 高位补充符号位 , 即为1

提升之后的结果是:

11111111111111111111111111111111

char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为 char 为有符号的 char 所以整形提升的时候 , 高位补充符号位 , 即为0

提升之后的结果是:

00000000000000000000000000000001

//无符号整形提升,高位补0

分析以下代码:

计算过程:

char a = 3 ;

3 的原码:00000000000000000000000000000011

由于 char 类型只占 1 个字节,所以将 3 存储到 a 时要发生截断,因此:

a 的补码:00000011(内存中以补码形式存储)

char b = 127;

b 的补码:01111111

char c = a + b;

要先对 a、b 进行整形提升:

a 整形提升后:00000000000000000000000000000011

b 整形提升后:00000000000000000000000001111111

a + b :            00000000000000000000000010000010

截断后存储到 c :

c 的补码:10000010(最高位是 1 ,c 已经是负数了)

printf("%d", c);

%d 是以有符号十进制整数形式打印二进制原码,所以要将 c 进行整形提升,再找到 c 的原码。

c 进行整形提升: 11111111111111111111111110000010

取反+1得到原码:100000000000000000000001111110 (这是 -126 的原码)

所以打印的是 -126 。

有符号的 char 类型(signed char)的取值范围是 -128 到 127 ,如何得来的?

        signed char 类型占 1 个字节即 8 bit ,1 个 bit 有 0 或者 1 两种可能,8 个 bit 可以有 2 的 8 次方即 256 种组合,可以表示十进制的0 到 257,但最高位为符号位,即表示数值实际有 7 个 bit 位(128 种组合),当最高位为 0 (表示正数)时可以表示十进制的 0 到 127 ,当最高位为 1 (表示负数)时可以表示十进制的 -128(10000000 规定为 -128) 到 -1 (11111111),所以,有符号的 char 类型(signed char)的取值范围是 -128 到 127 。其他类型,如 int 类型可以依此类推。

        对于 unsigned char (无符号字符型)最高位不再表示符号,此时 8 个 bit 位都表示数值,所以 unsigned char 的取值范围是 0 到 255 ,其他类型,如 unsigned int 类型也可以依此类推。

        char a = 127;(a:01111111)此时对 a 加一,(a + 1:10000000,这个二进制数规定为 -128)a 的值为 -128,再对 a 加一,(a + 2:10000001,-127)a 的值为 -127,一直对 a 加一,a 的值会从 -127 到 -126 ... 一直到 -1(11111111)再加一(100000000,1 溢出,a 的值变为00000000,即0)a 的值变为 0。

        所以说,对一个 char 类型的数无休止的加一,这个数不会一直增加,它的值呈现为在一定范围内周期性的变化。

请分析以下代码:


最后只打印出了 c ,请思考为什么

(tip:0xb6 表示十六进制数字 b6 ,二进制形式为10110110,十六进制数字每一位占半个个字节,  ==运算有时也要整形提升 )

下面这个例子也说明了整形提升的存在:( “+”也是操作符,对 c 要整形提升)


算术转换 

C语言中的算术转换是指在执行算术运算时,操作数的类型会被自动转换为一种公共类型,以确保运算的正确性。这种转换通常发生在二元运算符(如 +、-、*、/ 等)的操作数之间。

 如果某个操作数的类型在上面这个列表中排名较低 , 那么首先要转换为另外一个操作数的类型后执行运算。

如:

int a = 1;

foalt b = 3.14f ;

a + b ;

在执行 a + b 时,会把 a 转换为 float 类型,再进行相加。


操作符的优先级、结合性

总结:

下标圆括号成员选择 > 单目运算符 > 算术运算符 > 位移运算符 > 关系运算符 > 按位运算符 > 逻辑运算符 > 三目运算符 > 赋值运算符 > 逗号运算符

当多种操作符在一个表达式时:

首先确定优先级,相邻操作符按照优先级高低计算 ,优先级相同的情况下,结合性才起作用。

有些表达式的计算路径不是唯一确定的,这种表达式容易出 BUG ,称为问题表达式

比如:

例1

a*b + c*d + e*f ;                                                                                                                                    1   3   2   5  4  ----计算顺序1                                                                                                                1   4   2   5  3  ----计算顺序2

如果 a*b 的结果影响 c*d 的结果,那么就容易出现BUG

例2

​
int fun()

{

static int count = 1;

return ++count;

}

int main()

{

int a = fun() - fun() * fun() //这里可能会出现BUG,
                              //因为你不知道编译器会先调用 - 号前面的 fun()还是 * 前面的 fun()

return 0;

}

​

例3

可以在不同的编译器运行以上代码,ret 的结果可能不同。 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值