@toc

1.算数操作符

图片.png

   +  -  *  /   %
// 加 减 乘 除 取余

1.除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数

2.对于 / 操作符如果两个操作数都为整数,执行整数除法(求商舍余)。而只要有浮点数,执行的就是浮点数除法。

3.% (取模)操作符的两个操作数必须为整数,若其中一个不为整数则报错,返回的是整除之后的余数。

#include<stdio.h>
int main()
{
    int a = 5;
    int b = 2;
    int c = 0;
    printf("a+b = %d\n", a + b);//相加
    printf("a-b = %d\n", a - b);//相减
    printf("a*b = %d\n", a * b);//相乘
    printf("a/b = %d\n", a / b);//求商
    printf("a%%b = %d\n", a % b);//取模
    return 0;
}

图片.png

#include <stdio.h>
int main()
{
    /*double a = 5.0 / 2;*/
    double a = 5 % 2.0;//报错
    printf("a = %lf", a);
    return 0;
}

2.移位操作符

图片.png

    >>      <<  
//右移      左移        

移位操作符可分为 算术移位 和 逻辑移位
算术移位: 先移位,然后补足符号位 (通常是这个)
逻辑移位: 直接移位,0补齐

总结:

1.左移移动一位正数相当于直接乘以2,负数也一样,移动n位,乘以2 ^ n。

2.左位移一位 *2,右位移一位 /2(-1右位移还是-1)

#include <stdio.h>
#include <windows.h>
int main()
{
    int a = 16;
    int b = a << 1;
    int c = a >> 2;
    //>>整数移位,移动的是二进制
    //0000 0000 0000 0000 0000 0000 0001 0000 
    int d = -1;
    int e = d >> 1;
    //在计算机中,数据是用补码存储的
    //移位运算符操作的是补码
    //-1的补码:
    //1111 1111 1111 1111 1111 1111 1111 1111
    //移位后-1的二进制码不变
    //1111 1111 1111 1111 1111 1111 1111 1111 1
    //算数位移补符号位

    printf("%d\n", b);//输出结果为32
    printf("%d\n", c);//输出结果为4
    printf("%d\n", e);//输出结果仍为-1,移位后-1的二进制码不变
    system("Pause");
    return 0;
}

图片.png

右移是以算术移位 的方式来进行右移的


例如:

我们先按照算术移位的方式去推导最终结果,然后再修改变量大小成 - 16进行验证。
图片.png

警告:

1.移位操作符只能作用于整数,不能用于浮点数,因为浮点数的存储方式跟整数完全不同

2.对于移位运算符,不要移动负数位,这个是标准未定义的。

eg: int num = 10;
num >> -1;//error


3.位操作符

C语言运算符表示 含义 示例
& 位与 x & y
| 位或 x | y
^ 异或 x ^ y
~ 按位取反 x ~ y

& (按位与) 二进制补码 两真才真(1) 一假则假(0)
| (按位或) 二进制补码 一真则真(1) 两假才假(0)
^ (按位异或) 二进制补码 相异为真(1) 相同为假 (0)

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

    int c1 = a & b;   //00000000 00000000 00000000 00000001  =1
    int c2 = a | b;   //00000000 00000000 00000000 00000111  =7
    int c3 = a ^ b;   //00000000 00000000 00000000 00000110  =6

    printf("c1 = %d\n", c1);
    printf("c2 = %d\n", c2);
    printf("c3 = %d\n", c3);
    return 0;
}

图片.png


练习1:不用临时变量,交换a,b的值

不用临时变量,交换a,b的值

假设我们给定两个数,a = 3, b = 5;

要实现两个数的交换,我们会想到醋和酱油交换的生活案例,我们会先找一个空瓶子,先将醋或者酱油倒到空瓶子中,(假设将醋先倒入空瓶子中)然后我们会将酱油倒入原醋瓶中,将原空瓶中的醋倒入酱油瓶中。
方法根据这种方式,我们会先创建一个临时变量来用于两者的数值交换

#include <stdio.h>
int main()
{
    int a = 3;
    int b = 5;
    int temp = 0;
    printf("before:a = %d,b = %d\n", a, b);
    temp = a;
    a = b;
    b = temp;
    printf("after:a = %d,b = %d", a, b);
    return 0;
}

但是我们题目中明确要求不能创建变量,所以这种方式明显不合适。

这时候我们考虑另外一种方式,从数学的角度出发:如果我先将 a与b之和放到a到当中 然后再用两者之和减去b不就得到a了吗,并把这个值赋给b,实现了a的值转移给b 。同理,用两者之和减去刚刚赋予a值到b中的b,就能得到原b的值(听起来有点绕是不,简单来说:就是 值:a + b - a = b = > 变量a

//方法一:加减法
#include<stdio.h>
//不创建临时变量实现两个数的交换
int main()
{
    int a = 3;
    int b = 5;
    printf("before:a = %d,b = %d\n", a, b);
    a = a + b;
    b = a - b;//将a的值给b
    a = a - b;//将b的值给a
    printf("after:a = %d,b = %d", a, b);
    return 0;
}

图片.png

虽然上面说的这种方式实现了不用创建临时变量就交换两个数值,但是仍然存在一定的缺陷,比如我们的变量a, b并不等于3和5,而是一个很大或者离int边界阈值很接近的值,如果我们用两者相加的方法,a + b很有可能就超出int类型的边界阈值,得到一个并非我们想要的结果,再拿着这个结果去减去a或者b中一个数,并不能得到原本的另一个数的正确值。也就是存在“可能溢出”的缺陷。

既然这种加减法因为阈值的问题存在缺陷,我们就要去考虑一种既能够成功交换两个数的值,还不会存在阈值的缺陷,更深层次的思考,数值的存储方式-- - 二进制,二进制能实现两者交换……a thousand years later—>异或操作符 ^ 这种位操作符不会产生进位的情况,所以不存在溢出的可能。

//异或办法
#include<stdio.h>
//不创建临时变量实现两个数的交换
int main()
{
    int a = 3;
    int b = 5;
    printf("before:a = %d,b = %d\n", a, b);
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    printf("after:a = %d,b = %d", a, b);
    return 0;
}

image-20210802151211902


练习2:求一个整数存储在内存中的二进制中1的个数

编写代码实现:求一个整数存储在内存中的二进制中1的个数

方法①:看到这个问题,我们可以想到利用整数求其二进制原码,只要在求的过程中判断余数是否为1,是就计数 + 1,根据这种想法,编写我的代码可以得到:

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

图片.png

这种方式看似正确,但是当我们输入负数的时候,比如 - 1, - 1补码为11111111 11111111 11111111 11111111 正确答案应该为:32 ,而我们程序运行却得到0。
(实际上这种方式还有补救方法,只要将 负数看作无符号位的整型,将a定义从 int a = 0; 成更改成unsigned int a - 1就会被当作一个超级大的数字11111111 11111111 11111111 11111111 来处理)

图片.png

方法②:既然这种方法不可行,我们就应该换一种思路,寻找新的方法,比如我能否得到输入数字存储的二进制补码,然后统计该补码中1的个数?答案是:可以,我们应该要想到任何一个数如果和 1进行按位与操作,若其最后一位数为1,则结果为1,若为0,结果为0。再借助移位操作符右移便可以统计1的个数(int类型的大小 为32位bit),因此可以编写代码为:

#include<stdio.h>
int main()
{
    int a = 0;
    int count = 0;
    int i = 0;
    scanf("%d", &a);
    for (i = 0; i < 32; i++)
    {
        if (1 == ((a >> i) & 1))
        {
            count++;
        }
    }
    printf("count = %d\n", count);
    return 0;
}

这里我们的判断条件是:
if(1 == ((a >> i) & 1))

也就是将上面的1进行左移操作,然后将两者进行按位与操作,再比较1左移操作的结果 与按位与之后的结果是否相等,若相等,代表该二进制位置 为 1

方法③:通过对输入数字的二进制操作,从最后一位1开始,每一步减少一个1

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

a - 1能够让a的最后一位变成0 这种奇妙的感觉就像是三体小说中描写的“高维世界像低维世界塌陷”
有这样的一个公式 n & (n - 1)
假设n = 13 其后四位二进制补码为 1101

1101 ---- n
1100 ---- n - 1
1100 ---- n = n & (n - 1)
1011 ---- n - 1
1000 ---- n = n & (n - 1)
0111 ---- n - 1
0000 ---- n = n & (n - 1)

我们可以观察n的变化,发现每一次进行n = n & (n - 1)后,其最后一位的1(最右边的1)都会变成0(每执行一次,最右边的1都会消失,直到变成0,停止执行),那么在n变成0之前,n = n & (n - 1)能执行多少次,就代表最初的n中二进制补码就有多少个1。
这种算法的执行次数不用每次都执行32次,有多少个1,就执行多少次,所以效率很高!

注意:位操作符在进行运算的时候,不需要考虑符号位的影响,所有位都当做需要参与运算的二进制位,
比如 - 1 ^ -1 = 0, 即符号位也正常参与位操作符运算。


4.复合赋值符

图片.png

①简单赋值操作符:
= 赋值操作符(注意:一个等号 = 表示赋值,两个等号 == 表示条件判断)
通过赋值操作符,我们可以给初始变量赋值,变更调整变量的值(也就是重新给变量赋值)
例如:

int weight = 80;//单位kg 
weight = 60;//对80不满意,重新赋值成60
double salary = 10000.0;
salary = 30000.0;//对salary只有10000.0不满意,变成30000.0
//赋值操作符也可以连续使用,比如
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1; //连续赋值  //赋值操作符是从右往左计算的
//等同于:
x = y + 1;
a = x;

一般我们都是用分开的形式,不用连续赋值的形式,因为分开表示的方法更加容易阅读和理解,且易于调试。

②复杂赋值操作符
+= 、-= 、 *= 、 /=、 %= 、 >>=、 <<=、 &=、 |=、 ^=
举例:
int x = 10;
x = x + 10;
等同于:x += 10;

#include<stdio.h>
int main()
{
    int a = 1;
    printf("%d\n", a += 1);
    printf("%d\n", a -= 1);
    printf("%d\n", a *= 2);
    printf("%d\n", a /= 2);
    printf("%d\n", a %= 2);
    printf("%d\n", a <<= 1);
    printf("%d\n", a >>= 1);
    printf("%d\n", a &= -1);
    printf("%d\n", a |= 1);
    printf("%d\n", a ^= 1);
    return 0;
}

图片.png


5.单目操作符

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

sizeof 详细讲解:

举例一:

#include<stdio.h>
int main()
{
    int a = 1;//一个整形为4的字节
    char b = 'A';
    int arr[20] = { 0 };
    printf("%d\n", sizeof(a));
    printf("%d\n", sizeof a);
    printf("%d\n", sizeof(int));

    printf("%d\n", sizeof(b));
    printf("%d\n", sizeof(char));

    printf("%d\n", sizeof(arr));
    printf("%d\n", sizeof(int[20]));
    return 0;
}

图片.png

举例二:

#include<stdio.h>
int main()
{
    short i = 0;
    int j = 10;
    printf("%d\n", sizeof(i = j + 5));//sizeof计算所占空间的大小
    //不管这里的j是什么类型  j+5的结果都是放到变量 i当中  而i又是short类型
    //sizeof(i=j+5)等于 sizeof(i)等于sizeof(short)等于2
    printf("%d\n", i);
    //sizeof里面放的表达式并不会真实进行运算,只是一个摆设
    //也就是说 i=j+5 这一步操作并未真实执行,所以i还是原来的值:0
    return 0;
}

图片.png

注意:sizeof括号中的表达式不参与实际运算!


~ 按位取反详解:
按二进制补码取反
例如 0:00000000 00000000 00000000 00000000
~0:11111111 11111111 11111111 11111111 补码 等于 - 1

#include<stdio.h>
int main()
{
    int a = 0;
    printf("%d\n", ~a);
    return 0;
}

图片.png

举例1:

#include<stdio.h>
int main()
{
    int i = 11;
    //00000000 00000000 00000000 00001011 让倒数第三位变成1,其余不变
    //00000000 00000000 00000000 00000100  按位或  就能得到
    //1 << 2 1左移2位得到 00000000 00000000 00000000 00000100

    //int j = i | 4;
    int j = i | (1 << 2);
    printf("%d\n", j);
    //将j变成原来的i  只需要按位与操作
    //00000000 00000000 00000000 00001111
    //11111111 11111111 11111111 11111011
    //00000000 00000000 00000000 00001011
//而  11111111 11111111 11111111 11111011 可以由按位取反得到
//    00000000 00000000 00000000 00000100  这个数又可以通过1左移2位得到
    j = j & (~(1 << 2));
    printf("%d\n", j);
    //int a = 0;
    //printf("%d\n", ~a);
    return 0;
}

图片.png


减减-- 、 加加 ++ 详解:

#include<stdio.h>
int main()
{
    int a = 10;
    //printf("%d\n", ++a);//前置++,先++,后打印
    //printf("%d\n", a++);//后置++,先打印,后++
    //printf("%d\n", --a);//前置--,先--,后打印
    printf("%d\n", a--);后置--,先打印,后--
    printf("%d\n", a);
    return 0;
}

图片.png


(类型)强制类型转换详解:

#include<stdio.h>
int main()
{
    int a = (int)3.14;
    printf("%d\n", a);
    return 0;
}

图片.png


练习:

#include<stdio.h>
void test1(int arr[])
{
    printf("%d\n", sizeof(arr));
}
void test2(char ch[])
{
    printf("%d\n", sizeof(ch));
}
int main()
{
    int arr[10] = { 0 };
    char ch[10] = { 0 };
    printf("%d\n", sizeof(arr));
    printf("%d\n", sizeof(ch));
    test1(arr);
    test2(ch);
    return 0;
}

图片.png


6.关系操作符

图片.png

关系运算符释义 C语言表示 数学表示
大于 > >
大于等于 >=
等于 == =
不等于 !=
小于 < <
小于等于 <=

关系运算符的应用

  • 关系运算符的运算结果只有 0 或 1。当条件成立时结果为 1,条件不成立结果为 0。
  • 我们来看一段代码,如下:
#include <stdio.h>
int main() {
    printf("%d\n", 1 > 2);
    printf("%d\n", 1 < 2);
    return 0;
}

得到的输出结果为:

0
1
  • 原因就是1 &gt; 2在数学上是不成立的,所以结果为0;而1 &lt; 2在数学上是不成立的,所以结果为1

7.逻辑操作符

1.&& 逻辑与 判断规则:两真则真,一假就假
2.|| 逻辑或 判断规则:一真则真,两假才假

详解:
位操作符中的 & 和 | 操作的是数的二进制补码 逻辑操作符中的 && 和 || 操作的是数本身 (数为0为假,数为非0为真)
使用逻辑操作符之后,若表达式整体判断为真,则值为1(1表示真,固定值) 若表达式整体判断为假,则值为0(0表示假,固定值)

举例一:

#include<stdio.h>
    int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
    i = a++ && ++b && d++;
    //a++ 先使用a的值0,再++, 判断a=0后,后面的 ++b && d++均不执行
    printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
    return 0;
}

图片.png

总结:
逻辑与操作&& : 左边判断为假后,右边的表达式不再执行,直接停止,返回值0(假)
逻辑或操作 || 左边判断为真后,右边的表达式不再执行,直接停止,返回值1(真)


8.条件操作符

exp1 ? exp2 : exp3

解释:表达式1进行判断,若结果为真,则 exp1 ? exp2 : exp3 整体的结果为 表达式 exp2 的结果
若结果为假,则exp1 ? exp2 : exp3 整体的结果为 表达式 exp3 的结果
exp是expression的缩写,中文意思为“表达”

举例:

#include<stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int max;
    //if (a > 5)
    //  b = 3;
    //else
    //  b = -3;
    //b = a > 5 ? 3 : -3;
    max = a > b ? a : b;
    printf("a = %d\nb = %d\nmax = %d\n", a, b, max);
    return 0;
}

图片.png


9.逗号表达式

exp1, exp2, exp3, …….expN

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

举例1:

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

讲解:这里我们要输出c的值,那么c的值是什么呢?

int c = (a > b, a = b + 10, a, b = a + 1);
这里初始化c的时候应用了逗号表达式,逗号表达式从左向右依次执行,整个表达式结果为最后一个表达式的结果,我们按照这个原则去推导,a > b 执行但是没有结果 a = b + 10 相当于a = 2 + 10 = 12, a执行但不产生结果,b = a + 1 = 12 + 1 = 13,整个表达式的值也等于b 即c = 13。

对于这个结果我们可以运行程序验证一下。
图片.png


拓展:那这里a, b的值分别为多少呢?

图片.png

a = 12, b = 13 不再是原本的1和2,说明int c = (a > b, a = b + 10, a, b = a + 1); 中运算的结果有效接保留了下来。


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

1.下标引用操作符 [ ]
操作数: 一个数组名 + 一个索引值

#include<stdio.h>
int main()
{
    int a[10] = { 0 };//1.定义一个数组
    //2.如果我们想要访问第5个数组元素,并给其赋值
    a[4] = 10;//3.用数组变量名+[]+下标索引数字
    //4.[ ] 对应的两个操作数一个是变量名a  另外一个就是下标/索引值 4
    printf("%d\n", a[4]);
    return 0;
}

图片.png


2.函数调用操作符()
接收一个或者多个操作数,第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

#include<stdio.h>
//2.这个地方的()不是函数调用操作符,是函数定义的语法规则
int get_max(int x, int y)
{
    return x > y ? x : y;
}
int main()
{
    int a = 10;
    int b = 20;
    //1.调用函数的时候使用的() 就是函数调用操作符
    int max = get_max(a, b);
    //3.这里的函数调用操作符()的操作数为 函数名get_max,函数参数a,函数参数b  总共三个操作数
    //4.对于函数调用操作符()而言,其操作数至少要有一个(函数名),可以有多个
    printf("max = %d\n", max);
    return 0;
}

image-20210802163207093


3.访问一个结构的成员 . ->
思考:如何创建结构体类型和结构体类型变量呢?

#include<stdio.h>
//1.现实中为了描述复杂的对象,构建结构体类型
//2.比如学生,有姓名,年龄,学号等信息
//创建一个结构体类型
struct Stu  //3.struct Stu是一个结构体类型,表示学生类型
            //类型是用来创建变量的
{
    char name[20];//姓名
    int age;//年龄
    char id[20];//学号
};
int main()
{
    int a = 10;
    //使用struct Stu这个类型创建了一个学生对象s1,并初始化
    struct Stu s1 = { "张三",20,"20210403" };
    return 0;
}

结构体类型和结构体类型变量详解:

结构体类型和结构体类型变量的关系类似于图纸和房子的关系
在盖房子之前,需要有房屋盖建的设计图纸 。有了图纸,才能根据图纸盖出好房子。
在设计图纸环节,我们并不会真正去盖房子,所以就不会占用土地。
同样的道理,在创建结构体类型的时候,我们并不会真正去存储什么信息,只有在利用结构体类型创建变量的时候,才会去存储相应的信息,向内存申请存储空间。

创建好结构体变量后,我们想要打印相关的信息来看一下,这时候就会用到访问一个结构的成员.或 ->

1.结构体变量.成员名.操作符的操作数一个是结构体变量,另一个是成员名

image-20210802163310971

2.将结构的地址取出来放入指针,通过指针进行访问

图片.png

第2中方式有点太啰嗦了,先要对指针解引用,如何通过.操作符访问结构体变量的成员。
实际上通过指针访问的方式可以更加精简一些

3.结构体指针->成员名 通过指针的方式直接访问到变量的成员。

图片.png


完整代码:

#include<stdio.h>
//1.现实中为了描述复杂的对象,构建结构体类型
//2.比如学生,有姓名,年龄,学号等信息
//创建一个结构体类型
struct Stu  //3.struct Stu是一个结构体类型,表示学生类型
            //类型是用来创建变量的
{
    char name[20];//姓名
    int age;//年龄
    char id[20];//学号
};
int main()
{
    int a = 10;
    //使用struct Stu这个类型创建了一个学生对象s1,并初始化
    struct Stu s1 = { "张三",20,"20210403" };
    struct Stu* ps = &s1;
    //结构体指针->成员名
    printf("%s\n", ps->name);
    printf("%d\n", ps->age);
    printf("%s\n", ps->id);

    //printf("%s\n", (*ps).name);
    //printf("%d\n", (*ps).age);
    //printf("%s\n", (*ps).id);

    //printf("%s\n", s1.name);
    //printf("%d\n", s1.age);
    //printf("%s\n", s1.id);
    //结构体变量.成员名  .操作符的操作数一个是结构体变量,另一个是成员名
    return 0;
}

图片.png


表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

隐式类型转换(悄悄的进行类型转换)

①C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
是不是看的不太懂?这里我们举例说明:

#include<stdio.h>
int main()
{
    char a = 3;
    //00000000 00000000 00000000 00000011  整数3
    //00000011  ->a   a是char类型,只有一个字节,这时候就会发生截断
    //截断的规则:挑最小最低位的字节内容
    char b = 127;
    //00000000 00000000 00000000 01111111  整数127
    //01111111  ->b
    //a+b  a和b如何相加 按照变量数据类型的符号位来提升的
    //00000011 ->00000000 00000000 00000000 00000011
    //01111111 ->00000000 00000000 00000000 01111111
    //           00000000 00000000 00000000 10000010
    char c = a + b;//这时候a,b被提升为普通整型
    //10000010 ->c
    //后面我们要以%d的形式打印,需要进行整型提升:按照变量数据类型的符号位来提升的 
    //11111111 11111111 11111111 10000010  ---补码
    //11111111 11111111 11111111 10000001  ---反码
    //10000000 00000000 00000000 01111110  ---原码 -126
    printf("%d\n", c);
    printf("%c\n", c);
    return 0;
}

图片.png

至于为什么% 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

整型提升举例:

图片.png

解析:
0xb6 b:10 / 1011 6 / 0110 b6 10110110 整型提升后 高位补符号位1 变成了
11111111 11111111 11111111 10110110
负数 0xb600 10110110 00000000
整型提升后 高位补符号位1
变成了 11111111 11111111 10110110 00000000 负数


实例1中if语句里面进行了比较,比较也是一种运算,其中的a, b要进行整型提升, 但是c不需要整型提升a, b整型提升之后, 变成了负数, 所以表达式a == Oxb6, b == Oxb600的结果是假, 但是c不发生整型提升, 则表达式 c == Oxb6000000的结果是真, 所程序输出的结果是 : c


图片.png

解析:
实例2中的, c只要参与表达式运算, 就会发生整型提升
表达式 + c, 就会发生提升, 所以sizeof(+c)是4个字节.
表达式c,表达式(!c)中c实际参与了运算,会发生整型提升,在vs编译器中显示的这个结果实际上是有问题的,在Linux,gcc编译器下结果为4,实际结果也应该为4。

总结:什么时候会参与整型提升?
总要字节长度小于int,参与到实际运算后就会进行整型提升。

②算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long doub1e
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。(比如说int类型跟float类型参与运算,先要将int转换为float类型,然后float类型与float类型运算)
int a = 1;
float b = 2.0
b = a + b;
(会先将a转换为float类型 a = 1.00000)

警告 : 但是算术转换要合理,要不然会有一些潜在的问题。
例如:排名高的类型向低位转换的时候会出现精度丢失。
float f = 3.14;int num = f;//隐式转换,会有精度丢失

③操作符的属性
复杂表达式的求值有三个影响的因素。
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序。
两个相邻的操作符先执行哪个 ? 取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。


C语言操作符优先级、结合性参考表:

图片.png

常见问题解答:
1、如何记住运算符的15种优先级和结合性?
C语言中运算符种类比较繁多,优先级有15种,结合性有两种。
2、如何记忆两种结合性和15种优先级?下面讲述一种记忆方法。
结合性有两种,一种是自左至右,另一种是自右至左,大部分运算符的结合性是自左至右,只有单目运算符、三目运算符的赋值运算符的结合性自右至左。
优先级有15种。

记忆方法:

记住一个最高的:构造类型的元素或成员以及小括号。
记住一个最低的:逗号运算符。
剩余的是一、二、三、赋值。
意思是单目、双目、三目和赋值运算符。
在诸多运算符中,又分为:
算术、关系、逻辑。
两种位操作运算符中,移位运算符在算术运算符后边,逻辑位运算符在逻辑运算符的前面。再细分如下:
算术运算符分 * , / , % 高于 + , - 。
关系运算符中,〉,〉 = , < , <= 高于 == ,! = 。
逻辑运算符中,除了逻辑求反(!)是单目外,逻辑与( && )高于逻辑或( || )。
逻辑位运算符中,除了逻辑按位求反(~)外,按位与(&)高于按位半加(^),高于按位或( | )。
这样就将15种优先级都记住了,再将记忆方法总结如下:
去掉一个最高的,去掉一个最低的,剩下的是一、二、三、赋值。双目运算符中,顺序为算术、关系和逻辑,移位和逻辑位插入其中。


问题表达式

下面我们来看一些问题表达式:

//表达式的求值部分由操作符的优先级决定。

表达式1

a * b + c * d + e * f;

注释∶代码1在计算的时候,由于比 + 的优先级高,只能保证,的计算是比 + 早,但是优先级并不能决定第三个 比第一个 + 早执行。
所以表达式的计算机顺序就可能是︰
a
bc d
a
b + c de f
a b + c d + e f
或者 :
a
bc de f
a b + c d
a b + c d + e * f


表达式2

c + --c;

注释︰同上,操作符的优先级只能决定自减–的运算在 + 的运算的前面,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。


代码3
非法表达式

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

表达式3在不同编译器中测试结果不同,例如:

image-20210802171121897


代码4

#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;
  }

image-20210802171217722

这个代码有没有实际的问题 ?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知︰先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。


代码5

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

尝试在Linux环境gcc编译器,vS2019环境下都执行,看结果。
Linux运行结果:10 4
VS2019运行结果:12 4

看看同样的代码产生了不同的结果,这是为什么 ?
简单看一下汇编代码.就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置++的先后顺序。

总结∶我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。