<C语言>详解操作符及显隐类型转换


前言

你能分清按位与和逻辑与吗?

你了解右移操作符左端应该补什么吗?

你了解逗号表达式吗?

你知道操作符的优先级吗?

你知道隐式类型转换——整型提升吗?

嗨害!小帅来喽!

本文,小帅将为你详细解读操作符的世界!希望大家可以上机实验。

篇幅较长,但干货很多,请大家耐心阅读!


操作符的分类

算术操作符

位移操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号表达式

下标引用、函数调用和结构成员


 

1.算术操作符

+

-

*

/

%

代码示例: 

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", a + b);
	printf("%d\n", a - b);
	printf("%d\n", a * b);
	printf("%d\n", a / b);
	printf("%d\n", a % b);
	return 0;
}

代码结果:

我们可以从结果发现,相加减乘的结果都符合我们的预期,但是为什么相除的结果不等于理论上的0.5呢?这里有两点注意:

  1. printf()中格式我们用的控制字符串是%d,整数值用%d 输出,实数用%f 输出
  2. 这里隐含类型转换问题,即10和20都是整数,它们是整数之间的运算,只要运算符的两边都是整数,则运算结果也会是整数。正因为这样,10 / 20的值才是0,而不是0.5。而当我们计算10.0 / 20.0,这里10.0和20.0是浮点数,浮点数之间的运算结果是浮点数,因此10.0 / 20.0 = 0.5。即整数 / 整数 = 整数,浮点数 / 浮点数 = 浮点数注意,这里的运算符“/”其实是“多面手”,它既可以做整数除法,又可以做浮点数除法。

那么5 - 0.1的值是什么?5 / 2.0的值是什么?“整数 - 浮点数”是整数还是浮点数?

        整数-浮点数=浮点数,确切的法是:整数先“变”成浮点数,然后浮点数-浮点数=浮点数。整数 / 浮点数相同。 

 问题的思考:

#include<stdio.h> 
int main() 
{ 
 printf("%.1f\n", 8.0/5.0); 
 return 0; 
}

思考1 :符串%.1f 不变,把 8.0/5.0 改成来的 8/5,结果如何?

思考2 :字符串%.1f 改成来的%d8.0/5.0 不变,结果如何?

我们可以上手实验一下:

思考1答案:

思考2答案: 

 这两个答案的真正原因涉及整数和浮点数编码,相信多数初学者对此都不感兴趣。原因并不重要(当然我们也可以自行探索),重要的是规范:根据规范做事,则一切尽在掌握中。

小结:

  1.  除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2.  对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。   

2.移位操作符

<<左移操作符
>>右移操作符

注意:移位操作符的操作数只能为整数,操作对象为二进制数。

 这里我们简单补充一下原码、反码、补码的知识

        原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小。

正数:

        原码、反码、补码相同

例如:(十进制)7——(二进制)00000000 00000000 00000000 00000111——原码

                                                        00000000 00000000 00000000 00000111——反码

                                                        00000000 00000000 00000000 00000111——补码

负数:

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

        反码加1,得到补码

例如:(十进制)-1——(二进制)10000000 00000000 00000000 00000001——原码

                                                          11111111    11111111   11111111   11111110——反码

                                                          11111111    11111111   11111111   11111111——补码

注意:内存单元中存储的是补码,但在输出时是原码

2.1左移操作符 

移位规则:

左边抛弃、右边补0

(即按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零)

 代码示例(<<):

#include<stdio.h>
int main()
{
	int a = 10;
	int b = a << 1;
	//00000000000000000000000000001010 -补码
	printf("%d\n", b);
	printf("%d\n", a);

	return 0;
}

代码结果:

数学意义:

在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方

 2.2右移操作符

右移运算分为两种:

算数移位(常见有符号数)移位规则:

        按二进制形式把所有的数字向右移动对应位移位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1

逻辑移位(无符号数)移位规则:

        按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。

代码示例(>>):

#include<stdio.h>
int main()
{
	int a = -1;
	//10000000000000000000000000000001-原码
	//11111111111111111111111111111110-反码
	//11111111111111111111111111111111-补码
	int b = a >> 1;
	printf("%d\n", b);
	printf("%d\n", a);

	return 0;
}

代码结果: 

 注意:

        对于移位运算符,我们不能想当然的移动负数位,这个是标准未定义的。

例如:7>>-1,向右移-1位,不能认为是左移1位,并且移动负数位也是标准未定义的。

数学意义:

        右移一位相当于除2,右移n位相当于除以2的n次方取整

3.位操作符

&按位与
|按位或
^按位异或

注意:它们的操作数必须是整数,操作对象是二进制数。

3.1 按位与 &

代码示例:

//& - 按2进制位与
//对应的二进制位有0,则为0,两个同时为1,才为1
#include<stdio.h>
int main()
{
	int a = 3;
	//00000000000000000000000000000011-原反补码
	int b = -5;
	//10000000000000000000000000000101 -原码
	//11111111111111111111111111111010 -反码
	//11111111111111111111111111111011 -补码
	//
	int c = a & b;//两数对应的二进制位补码有0,则为0,两个同时为1,才为1
	//00000000000000000000000000000011 -a的补码
	//11111111111111111111111111111011 -b的补码
	//00000000000000000000000000000011 -c的补码
	//
	printf("%d\n", c);

	return 0;
}

代码结果:

 3.2 按位或 |

代码示例:

#include<stdio.h>
//按2进制位或
//对应的二进制位有1则为1,两个同时为0则为0

int main()
{
	int a = 3;
	//00000000000000000000000000000011 -原反补码
	int b = -5;
	//10000000000000000000000000000101 -原码
	//11111111111111111111111111111010 -反码
	//11111111111111111111111111111011 -补码
	//
	int c = a | b;//两数对应的二进制位补码有1则为1,两个同时为0则为0
	//00000000000000000000000000000011 -a的补码
	//11111111111111111111111111111011 -b的补码
	//11111111111111111111111111111011 -c的补码
	//
	printf("%d\n", c);

	return 0;
}

代码结果:

3.3按位异或  ^

异或操作符的特点:

  1. a ^ a = 0
  2. a ^ 0 = a
  3. 异或支持交换律

代码示例:

#include<stdio.h>
//^ - 按2进制位异或
//对应的二进制位:相同为0,相异为1

int main()
{
	int a = 3;
	//00000000000000000000000000000011 -原反补码
	int b = -5;
	//10000000000000000000000000000101 -原码
	//11111111111111111111111111111010 -反码
	//11111111111111111111111111111011 -补码
	//
	int c = a ^ b;
	//00000000000000000000000000000011 -a的补码
	//11111111111111111111111111111011 -b的补码
	//11111111111111111111111111111000 -c的补码
	//11111111111111111111111111110111
	//10000000000000000000000000001000 -c的原码
	//-8
	printf("%d\n", c);

	return 0;
}

代码结果:

3.4 为了更好的了解移位操作符和位操作符,下面我们做些经典题来加深印象。

例题一不创建临时变量(第三个变量),实现两个整数的交换。(某大厂面试题)

方法一:加减法交换

#include<stdio.h>
int main()
{
	int a = 3;
	int b = 5;

	printf("%d %d\n", a, b);
	a = a + b;
	b = a - b;//由上一个语句得,b = a + b - b = a
	a = a - b;//由b = a, a = a + b,得a = a + b - a = b
	printf("%d %d\n", a, b);

	return 0;
}

代码结果: 

方法一弊端:

  • 两数相加可能超过基本数据类型的范围 
  • 可读性差

方法二:异或交换 

代码示例:

#include<stdio.h>
int main()
{
	int a = 3;
	int b = 5;

	printf("%d %d\n", a, b);
	a = a ^ b;
	b = a ^ b;//由上一个语句,得b = a ^ b ^ b(由异或交换律,a ^ a = 0,a ^ 0 =a),可得b = a
	a = a ^ b;//由a = a ^ b, b = a, 得a = a ^ b ^ a = b
	printf("%d %d\n", a, b);

	return 0;
}

代码结果: 

 

方法二弊端: 

  • 异或只能对整数间进行交换
  • 效率不如使用临时变量(在平时交换变量时)
  • 可读性差

例题二 :统计一个整数得二进制位中有几个1。

方法一:

        类比求一个十进制数整数有几位数程序(循环模10除10.......直至该数等于0),我们将整数循环模2除2......若等于1则count++直至该整数等于0。

代码示例:

#include<stdio.h>
int Numberof(int n)//换成无符号数
{
	int count = 0;
	while (n != 0)
	{
		if (n % 2 == 1)
		{
			count++;
		}
		n /= 2;//每次除2,直至n = 0
	}
	return count;
}
int main()
{
	int n = 0, count = 0;
	scanf("%d", &n);
	printf("%d\n\n", Numberof(n));
	return 0;
}

我们输入十进制数——7,预计打印3

代码结果:

符合预期。我们也可以上手试试其他整数。

那我们输入十进制数——  -1 ,预计打印32

代码结果:

不符合预期,即代码设计有错。为什么正数没错 ,而负数有误呢?我们推理发现,-1在第一次循环条件判断时就不符合,-1 % 2 = 0不进入循环,又count = (-1) / 2 = 0,故输出0。

        那这个程序只能统计正数,不能再完善而包含负数吗?答案是可以的,我们可以在函数形参接收实参将形参接收得类型定义为无符号类型(最高位将不再是符号位),这样负数就变为正数!

改善代码如下:

#include<stdio.h>
int Numberof(unsigned int n)//换成无符号数
{
	int count = 0;
	while (n != 0)
	{
		if (n % 2 == 1)
		{
			count++;
		}
		n /= 2;
	}
	return count;
}
int main()
{
	int n = 0, count = 0;
	scanf("%d", &n);
	printf("%d\n\n", Numberof(n));
	return 0;
}

输入-1

代码结果:

方法二: 

        将整数与1按位与后,将1左移1位再次与整数按位与。只要比较的二进制位有1,则按位与的结果就大于等于1。

代码示例:

#include<stdio>
int main()
{
    int res = 0;
    int n = 0;
    scanf("%d", &n);
    for (int i = 0; i < 32; i++)
    {
        //按位比较,每次1左移1位
        if ((n & (1 << i)) != 0)
            res++;
    }
    printf("%d", res);
    return 0;
}

输入-1 代码结果:

思考:还能不能更加优化,因为这里必须循环32次。 

方法三:

        n & ( n - 1 ) 

#include<stdio.h>
int main()
{
	int n = 0,count=0;
	scanf("%d", &n);
	while (n)
	{
		count++;
		n &= n - 1;
	}
	printf("%d", count);
	return 0;
}

 举例:7—— 00000000 00000000 00000000 00000111——原反补码

   6(7-1) —— 00000000 00000000 00000000 00000110——原反补码

按位与后:    00000000 00000000 00000000 00000110——补码

 解读:二进制位最初一位必定是0或1,而减1后二进制中1的总数必定小于等于原先1的总数,两者按位与后有0即为0,两者为1即为1。

        该方法较前两种效率最高,望小伙伴们理解掌握。

 

 4.赋值操作符

赋值操作符是一个很棒的操作符,它可以让你修改一个你之前不满意的值,也就是你可以给自己重新赋值。

=赋值操作符

举例:


int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。

赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值
这样的代码感觉怎么样?
那同样的语义,你看看:
x = y+1;
a = x;
这样的写法是不是更加清晰爽朗而且易于调试。

4.1 复合赋值符 

复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=

这些运算符都可以写成复合的效果。

例如:

int x = 10;
x = x + 10;
x += 10;//复合赋值
int a = a / 10;
a /= 10;
a %= 10;
//其他运算符一样的道理。这样写更加简洁!

5.单目操作符

5.1单目操作符介绍

逻辑反操作
-负值
+正值
&取地址
sizeof操作数的类型长度(以字节为单位)
~对一个数的二进制按位取反
--前置、后置--
++前置、后置++
*间接访问操作符(解引用操作符)
(类型)

强制类型转换

注意:单目操作符,只有一个操作数。操作符有2个操作数的为双目操作符。

 c语言中0表示假,非0表示真

下面用代码展示上述操作符作用:

1. !

#include<stdio.h>
int main()
{
	//C语言中0表示假,非0表示真
	int flag = 0;
	if (flag)//flag如果为真,做.....
	{
		printf("hehe\n");
	}
	if (!flag)
	{
		printf("hehe\n");
	}

	printf("%d\n", flag);
	printf("%d\n", !flag);

	
	return 0;
}


代码结果:

 2. -

int main()
{
	int a = -10;
	printf("%d\n", a);
	printf("%d\n", -a);
	return 0;
}

 代码结果:

3. +

int main()
{
	int a = -10;
	printf("%d\n", a);
	printf("%d\n", +a);

	return 0;
}

代码结果:

 4. &(取地址)

int main()
{
	int a = 10;
	printf("%p\n", &a);// &取出a的地址,%p打印地址
	int* pa = &a;

	char ch = 'w';//定义字符变量ch,赋值为‘w’
	char*pc = &ch;//定义字符指针pc,存储ch的地址
	
	char arr[10] = { 0 };//定义字符数组arr
	char* p2 = arr;      //定义字符指针p2,存储arr数组首元素地址
	char* p3 = &arr[0];  //取arr数组第一个元素的地址,赋值给字符指针p3,p2,p3相同


	const char* p = "abcdef";//定义字符指针p存储字符串首字符地址
	printf("%p\n", p);       //打印p中存储的地址,即字符串首字符地址
	printf("%c\n", *p);      //打印指针中地址指向的字符a

	return 0;
}

5. sizeof

函数调用的时候,要写()
但是sizeof后边的括号可以省略,说明sizeof不是函数

//函数调用的时候,要写()
//但是sizeof后边的括号可以省略,说明sizeof不是函数

int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof a);//ok
	printf("%d\n", sizeof(int));
	int arr[10] = {0};
	printf("%d\n", sizeof arr);//ok
	printf("%d\n", sizeof(arr));//ok
	printf("%d\n", sizeof(int[10]));//ok
	return 0;
}

代码结果: 

 大家可以思考下面这段程序结果:

int main()
{
	int a = 10;
	short s = 5;
	printf("%d\n", sizeof(s = a + 3));
	printf("%d\n", s);

	return 0;
}

 代码结果:

 解析:sizeof是关键字也是操作符,并且sizeof内部的表达式是不计算的,所以sizeof( s = a + 3)只会计算s的字节大小。

6. ~

 注意:

  1. ~ 操作数是整数,操作对象是二进制。
  2. 将二进制所有位按位取反
//~ 按位取反
//00000000000000000000000000000000
//11111111111111111111111111111111 - 补码是全1
//-1

int main()
{
	int a = 0;
	printf("%d\n", ~a);//?

	return 0;
}

代码结果:

 至此,我们学完了所有对二进制数操作的操作符

&|^>><<~
下面给出一道题,来综合运用以上操作符。

例题: 给出整数 (十进制)9——(二进制)00000000 00000000 00000000 00001001,编写程序将该二进制的第5位0转变为1。

int main()
{
	int a = 9;
	//00000000000000000000000000001001
	//00000000000000000000000000010000      1<<4
	//00000000000000000000000000011001
	//
	//把a的二进制中第5位改成1
	a |= (1<<4);
	printf("%d\n", a);

	//把a的二进制中的第5位改回来,变成0
	//00000000000000000000000000011001
	//11111111111111111111111111101111
	//00000000000000000000000000001001

	a &= (~(1 << 4));
	printf("%d\n", a);//9

	return 0;
}

 7. ++(前置++,后置++)

前置++:先将该变量+1,在使用该变量

后置++:先使用该变量,再将该变量+1 

 -- 使用与 ++ 相同

 代码示例:

int main()
{
	int a = 10;
	int d = 10;
	int b = a++;//后置++,先使用,再++
	int c = ++d;//前置++,先++,后使用
	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", c);
	return 0;
}

我们再来看函数传参时:

道理与上面相同

#include <stdio.h>

void test(int b)
{
	printf("b = %d\n", b);
}

int main()
{
	int a = 10;
	//test(a++);
	test(++a);
	return 0;
}

 代码结果:

 注意:++ -- 带有副作用,即使用后该变量会改变大小,如果不想改变原有变量的大小就将该变量加1进行赋值操作即可。

8. ()

注意:强制类型转换可能会使精度缺失! 

int main()
{
	int a = (int)3.14;//将浮点数强制类型转换为整数
	printf("%d\n", a);
	return 0;
}

//int main()
//{
//	//time_t;
//	srand((unsigned int)time(NULL));//平时使用的随机数也是使用强制类型转换
//
//	return 0;
//}

6. 关系操作符

>
>=
<
<=
!=
==

这些关系运算符比较简单,没有什么可讲的,但是我们要注意一些运算符使用时的陷阱。

注意:在编程的过程中 == 和= 不小心写错,将会导致程序错误。

7.逻辑操作符

&&逻辑与(并且)
||逻辑或(或者)

区分逻辑与按位与

区分逻辑或按位或

  1. 按位与或,操作数是整数,操作对象是二进制序列
  2. 逻辑与或,操作对象是表达式

aba&&b
aba||b

//1. 能被4整除,并且不能被100整除
//2. 能被400整除
//判断闰年
int main()
{
	int y = 2048;
	if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
	{
		printf("Yes\n");
	}

	return 0;
}

        和算术表达式类似,逻辑表达式也由运算和值构成, 例如“||”运算“逻辑或”,a || b a 为真,或者 b 为真。换句a b 要 有一个为真a || b 为真如果 a b 为真a || b 也为真。和其他语言不同的是,在 C 语言中单个整数可以表示真,其中 0 ,其他值为真

        细心的读者也许发现了,如果 a 为真b 的值如何,a || b 为真换句话说, 一旦发现 a 为真,就不必计算 b 的值C 语言正是采取了这样的策略,称为短路(short-circuit)。 

注意: C 语言中的逻辑运算符都是短路运算符。一旦能够确定整个表达式的值,就不 再继续计算。

笔试题:

#include <stdio.h>
int main()
{
    int i = 0,a=0,b=2,c =3,d=4;
    i = a++ && ++b && d++;
    //i = a++||++b||d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}

这里就考到了短路,结果如下: 

 你能算出下面这一段程序的结果吗?


#include <stdio.h>
int main()
{
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	//i = a++ && ++b && d++;
	i = a++ || ++b || d++;

	printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);

	return 0;
}

结果如下:

 8.条件操作符(三目操作符)

exp1 ? exp2 : exp3(与if - else语句作用相似)

 如果exp1 为真,则表达式结果为exp2

 如果exp1 为假,则表达式结果为exp3

举例:

int main()
{
	int a = 0;
	int b = (a > 5 ? 3 : -3);
	printf("%d", b);
	return 0;
}

结果如下:

 练习:

if (a > 5)
       b = 3;
else
       b = -3;
转换成条件表达式,是什么样?

9.逗号表达式 

exp1, exp2, exp3, ......expN

  1. 逗号表达式,就是用逗号隔开的多个表达式。
  2. 逗号表达式,从左到右依次执行。整个表达式的结果是最后一个表达式 的结果。

大家可以计算下面一段程序结果 


int main()
{
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
	printf("c=%d\n", c);
	return 0;
}

 10.下标引用、函数调用和结构成员

1. [ ] 下标引用操作符

操作数:一个数组名 + 一个索引值

int main()
{
	int arr[10] = { 1,2,3,4,5 };
	printf("%d\n", arr[4]);//[] - 下标引用操作符,操作数是:arr , 4
	//3 + 4;
	return 0;
}

                                                                                                                                  

 思考下面一段程序时是否正确(大家可以上手实践):

int main()
{
	int arr[10] = { 1,2,3,4,5 };
	printf("%d\n", 4[arr]);//[] - 下标引用操作符,操作数是:arr , 4
	//3 + 4;
	return 0;
}

结果:

看到这里,可能有许多人有疑问,数组访问还能这样写? 

解析:[ ] 是个操作符,i 和 arr 是 [ ]这个操作符的操作数而已,就如同a + b == b + a 一样,所以也可以将两者交换位置,但不建议这样写,因为可读性差,这里只是简单介绍一下它的意义。

 2.()函数调用操作符

接受一个或多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。 

 代码举例:

 #include <stdio.h>
 void test1()
 {
 printf("hehe\n");
 }
 void test2(const char *str)
 {
 printf("%s\n", str);
 }
 int main()
 {
 test1();            //实用()作为函数调用操作符。
 test2("hello bit.");//实用()作为函数调用操作符。
 return 0;
 }

 3.访问一个结构的成员

.      结构体.成员名

->    结构体指针->成员名 

代码举例:

#include <stdio.h>
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};
void set_age1(struct Stu stu)
	{
		stu.age = 18;
	}
void set_age2(struct Stu* pStu)
	{
		pStu->age = 18;//结构成员访问
	}
int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;//结构成员访问

	stu.age = 20;//结构成员访问
	set_age1(stu);//实参为结构体

	pStu->age = 20;//结构成员访问
	set_age2(pStu);//实参为结构体指针
	return 0;
}

11.表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。

同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

 11.1 隐式类型转换

c语言的整型算术运算总是至少以缺少整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数使用之前被转换为普通整形,这种转换成为整型提升

整型提升的意义
表达式的整型运算要在 CPU 的相应运算器件内执行, CPU 内整型运算器 (ALU) 的操作数的字节长度
一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。
因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长
度。
通用 CPU general-purpose CPU )是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转
换为 int unsigned int ,然后才能送入 CPU 去执行运算。

如何进行整型提升呢? 

负数的整形提升

char c1 = - 1 ;
变量 c1 的二进制位 ( 补码 ) 中只有 8 个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候, 高位补充符号位 ,即为 1
提升之后的结果是:
11111111111111111111111111111111

正数的整形提升

char c2 = 1 ;
变量 c2 的二进制位 ( 补码 ) 中只有 8 个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候, 高位补充符号位 ,即为 0
提升之后的结果是:
00000000000000000000000000000001

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

代码举例:

int main()
{
	//char --> signed char
	char a = 3;//表达式,整型3存入字符a中要进行截断
	//截断
	//00000000000000000000000000000011
	//00000011 - a
	//
	char b = 127;
	//00000000000000000000000001111111
	//01111111 - b

	char c = a + b;
	//00000011
	//01111111
	//整型提升
	//00000000000000000000000000000011
	//00000000000000000000000001111111
	//00000000000000000000000010000010
	//10000010 - c
	printf("%d\n", c);
	//%d 是打印十进制的整数
	//11111111111111111111111110000010 - 补码
	//11111111111111111111111110000001
	//10000000000000000000000001111110 - 原码
	//-126
	return 0;
}

解析:

int a = 3; 为一个表达式,3为整型对应二进制为 00000000000000000000000000000011

将整数放进char类型变量 ,就好比9米长杆子放进三米宽的房子,是要折断放进去的,故3截断为00000011放进a中。同理127折断为 01111111 放进b中。char c = a + b ;该表达式运算前要整型提升,a为 00000000 00000000 00000000 00000011 ,b为 00000000 00000000 00000000 01111111 相加为 00000000 00000000 00000000 10000010 截断后为 10000010放进c中,在打印c时打印十进制数,因为是表达式先整型提升c的最高位为1即为负数,所以高位补充符号位1后c为 11111111111111111111111110000010——补码

补码经过取反加1后为原码 10000000000000000000000001111110,我们发现两个正数相加因为整型提升而结果变为了负数,由此可见整型提升的重要性!

这里补充有符号char与无符号char的取值范围 

  • 有符号的char的取值范围:-128~127
  • 无符号的char的取值范围:0~255 

 

整型提升的例子1: 

//实例1
int main()
{
	char a = 0xb6;    //0x为十六进制数,每两个16进制位占一个字节,因为15用二进制位1111表示
	short b = 0xb600;
	int c = 0xb6000000;
	if (a == 0xb6)
		printf("a");
	if (b == 0xb600)
		printf("b");
	if (c == 0xb6000000)
		printf("c");
	return 0;
}

大家认为屏幕上会打印什么呢?

实例1中的a,b要进行整形提升,但是c不需要整形提升,因为c本身就为整型4个字节。

  • a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假。
  • 但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。

 整型提升的例子2: 

//实例2
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(-c));
	return 0;
}

 答案是多少呢?

实例2中的,c只要参与表达式运算,就会发生整形提升,

  • 表达式 +c ,就会发生提升,所以 sizeof(+c) 4个字 节.
  • 表达式 -c 也会发生整形提升,所以 sizeof(-c) 4个字节,但是 sizeof(c) ,就是1个字节

11.2 算术转换

如果某个操作符的各个操作符属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系成为寻常算术转换

long double
double
float
unsigned long int
long int 
unsigned int
int

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

注意:算术转换要合理,要不然会有一些潜在问题

float f = 3.14;
int num = f;//隐式转换,会有精度丢失

例题: 

#include <stdio.h>
int i;
int main()
{
    i--;
    if (i > sizeof(i))
    {
        printf(">\n");
    }
    else
    {
        printf("<\n");
    }
    return 0; 
}

这里隐藏着算术转换!由于sizeof的返回值为无符号整数并且i 的默认值为0,故 i --后为-1算术转换后 -1补码 11111111 11111111 11111111 11111111数值大小为2^32-1远大于4,故输出 >

11.3 操作符的属性

复杂表达式的求职有三个影响因素。

  1. 操作符的优先级
  2. 操作符的结合性
  3. 是否控制求值顺序

两个相邻的操作符先执行哪个?

        取决于它们的优先级。如果两者的优先级相同,取决于它们的结合性。

操作符优先级

操作符

描述

用法示例

结果类型

结合性

是否控制求值

顺序

    ()聚组(表达式)与表达式相同N/A
    ()函数调用rexp (rexp, ...rexp)rexpL-R
      [ ] 下标引用rexp[rexp]lexpL-R
       .访问结构成员rexp.member_namelexpL-R
      ->
访问结构指针成员
rexp->member_name
rexpL-R
      ++
后缀自增
lexp ++
rexpL-R
       --
后缀自减
lexp --
rexpL-R
       !
逻辑反
! rexp
rexpR-L
       ~
按位取反
~ rexp
rexpR-L
       +
单目,表示正值
+ rexp
rexpR-L
       -
单目,表示负值
- rexp
rexpR-L
      ++
前缀自增
++ lexp
rexpR-L
      --
前缀自减
-- lexp
rexpR-L
       *
间接访问
* rexplexpR-L
       &
取地址
& lexprexpR-L
    sizof
取其长度,以字节
表示
sizeof rexp sizeof(类型)rexpR-L
   (类型)
类型转换
(类型)rexprexpR-L
       *
乘法
rexp * rexp
rexpL-R
       /
除法
rexp / rexp
rexpL-R
       %
整数取余
rexp % rexp
rexpL-R
       +
加法
rexp + rexp
rexpL-R
       -
减法
rexp - rexp
rexpL-R
      <<
左移位
rexp << rexp
rexpL-R
      >>
右移位
rexp >> rexp
rexpL-R
       >
大于
rexp > rexp
rexpL-R
      >=
大于等于
rexp >= rexp
rexpL-R
       <
小于
rexp < rexp
rexpL-R
      <=
小于等于
rexp <= rexp
rexpL-R
      ==
等于
rexp == rexp
rexpL-R
      !=
不等于
rexp != rexp
rexpL-R
       &
位与
rexp & rexp
rexpL-R
       ^
位异或
rexp ^ rexp
rexpL-R
       |
位或
rexp | rexp
rexpL-R
       &&
逻辑与
rexp && rexp
rexpL-R
        ||
逻辑或
rexp || rexp
rexpL-R
       ?:
条件操作符
rexp ? rexp : rexp
rexpN-A
        =
赋值
lexp = rexp
rexpR-L
       +=
...
lexp += rexp
rexpR-L
       -=
...
lexp -= rexp
rexpR-L
       *=
...
lexp *= rexp
rexpR-L
       /=
...
lexp /= rexprexpR-L
       %=
...取模
lexp &= rexprexpR-L
      <<=
...左移
lexp <<= rexprexpR-L
      >>=
...右移
lexp >>= rexprexpR-L
       &=
...
lexp &=rexprexpR-L
       ^=
...异或
lexp ^= rexprexpR-L
       |=
...
lexp |= rexp
rexpR-L

 最后为

逗号rexp, rexprexpL-R

一些问题表达式 

示例1: 

//表达式1
c + -- c ;
注释:操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得
知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
示例2:
//代码2-非法表达式
int main()
{
 int i = 10;
 i = i-- - --i * ( i = -3 ) * i++ + ++i;
 printf("i = %d\n", i);
 return 0;
}

结果:该段程序在不同编译器中测试结果各不相同!

示例3:

//代码3
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() ; 我们只能通过操作符的优先级得知:先算乘法, 再算减法。
函数的调用先后顺序无法通过操作符的优先级确定

示例4:

//代码5
#include <stdio.h>
int main()
{
 int i = 1;
 int ret = (++i) + (++i) + (++i);
 printf("%d\n", ret);
 printf("%d\n", i);
 return 0;
}

 结果:

        也是不同编译器产生不同的结果。

        这段代码中的第一个 + 在执行的时候,第三个++ 是否执行,这个是不确定的,因为依靠操作符的优先级 和结合性是无法决定第一个 + 和第 三个前置 ++ 的先后顺序。
总结 :我们写出的表达式 如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的

 总结

        本文至此,小帅用超长的篇幅详细的解读了所有操作符,希望 学习完操作符的世界,我们的编程能力会更上一层楼,对代码深层的理解也会随之加深!大家可以收藏,以便查阅,熟记于心。

 最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

 

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值