C语言初阶——操作符、表达式求值

一、操作符分类

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号操作符

1. 算数操作符 ------ + - * / %

  • % - 取余/取模:只能作用于整数、不可作用于浮点数,返回的是整除之后的余数;其余算数操作符都可作用于整数和浮点数。
  • / - 除法:若操作符左右两边的两个操作数都为整数,则执行整数除法,返回的是整除之后的商;有任意一边为浮点数,则执行浮点数除法。
cpp 复制代码
 //算数操作符
int a = 5 / 2;        //结果:2
float b = 5.0 / 2;    //结果:2.5000000
float c = 5 / 2.0;    //结果:2.5000000

int d = 9 % 4;        //结果:1

2. 移位操作符 ------ >> <<

  • 移动的是二进制位;操作数只能是整数。
  • << - 左移操作符:左边丢弃,右边补0。------ 相当于给操作数乘以2。
  • >> - 右移操作符:
  1. 逻辑右移:右边丢弃,左边补0。 ------ 相当于给操作数除以2。
  2. 算数右移:右边丢弃,左边补该值的原符号位。------ 二进制序列的最高位为符号位,负数的符号位是1。
  • 整数的二进制位表现形式有3种,分别为:原码、反码和补码。 正整数三码相同。
  1. 原码:直接根据数值写出的二进制序列;
  2. 反码:原码的符号位不变,其他位按位取反;
  3. 补码:反码加1。

例:a = -1存放到内存中的为二进制的补码

a 的二进制原码为:10000000000000000000000000000001

a 的二进制反码为:11111111111111111111111111111110

a 的二进制补码为:11111111111111111111111111111111

a << 1 的二进制补码为:11111111111111111111111111111110

a << 1 的二进制反码为:11111111111111111111111111111101

a << 1 的二进制原码为:10000000000000000000000000000010 结果为:-2

a >> 1 (逻辑右移) 的二进制补码为:01111111111111111111111111111111

a >> 1 (逻辑右移) 的二进制反码为:01111111111111111111111111111110

a >> 1 (逻辑右移) 的二进制原码为:10000000000000000000000000000001 结果非常大

a >> 1 (算数右移) 的二进制补码为:11111111111111111111111111111111

a >> 1 (算数右移) 的二进制反码为:11111111111111111111111111111110

a >> 1 (算数右移) 的二进制原码为:10000000000000000000000000000001 结果为:-1

  • 在C语言的计算中,通常采用的是算数右移。
cpp 复制代码
 //移位操作符
#include<stdio.h>
int main()
{
	int a = -1;
	int b = a << 1;
	int c = a >> 2;
	printf("b=%d\nc=%d\n", b, c);
	return 0;
}                /*输出结果为:b=-2
                              c=-1*/
  • 对于移位运算符,需要注意的是不要移动负数位。

例:int b = a << -5; 这种书写方式是错误的。

3. 位操作符 ------ & | ^

  • 位指的是二进制位,是对二进制位进行操作的。
  • & - 按位与:对应的二进制位与。------ 有一假则假。
  • | - 按位或:对应的二进制位或。------ 有一真则真。
  • ^ - 按位异或:对应的二进制位异或。------ 相同为0,相异为1。
cpp 复制代码
 //位操作符
#include<stdio.h>
int main()
{
	int a = 3;       //00000000000000000000000000000011      3
	int b = 5;       //00000000000000000000000000000101      5
	int c = a & b;   //00000000000000000000000000000001      1
	int d = a | b;   //00000000000000000000000000000111      7
	int e = a ^ b;   //00000000000000000000000000000110      6
	printf("c=%d\nd=%d\ne=%d\n", c, d, e);
	return 0;
}          /*输出结果为:c=1
                        d=7
                        e=6*/
  • a ^ a = 0; - 任意两个相同数字异或皆为0。
  • 0 ^ a = a; - 0与任意数字异或还是该数值本身。
  • 3次异或可实现两个数值的交换。
cpp 复制代码
 //不创建临时变量实现两个数的交换
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d b=%d\n", a, b);
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
	printf("交换后:a=%d b=%d", a, b);
	return 0;
}             /*输出结果为:交换前:a=10 b=20
                           交换后:a=20 b=10*/

4. 赋值操作符 ------ += -= *= /= %= >>= <<= &= |= ^=

  • 单个 "=" 是赋值,双个 "==" 是判断是否相等。

5. 单目操作符 ------ ! - + & sizeof ~ -- ++ * (类型)

  • 只有一个操作数

5.1 介绍

  • ! ------ 逻辑反操作,取反
    • ------ 负值
    • ------ 正值
  • & ------ 取地址
  • sizeof ------ 计算操作数的类型长度(单位:字节)
  • ~ ------ 对一个数的二进制位进行按位取反
  • -- ------ 前置、后置--
  • ++ ------ 前置、后置++
  • * ------ 解引用操作符(间接引用操作符)
  • (类型)------ 强制类型转换

5.2 & 取地址 和 * 解引用操作符(间接引用操作符)

  • 两者通常放在一起使用
cpp 复制代码
 //取地址和解引用操作符
#include <stdio.h>
int main()
{
	int a = -10;
	printf("%p\n", &a);    //%p用来打印地址
	int* pa = &a;          //取出a的地址,此时pa是指针变量,指向的是a的地址
	*pa = 20;              //*pa对应的即是a,通过改变*pa的大小可以改变a的大小
	printf("%d\n", a);
	return 0;
}         /*输出结果为:007BF868
                       20*/

5.3 sizeof ------ 计算操作数的类型长度

  • sizeof(),()在计算变量时可以省略,其余情况不可省略。好习惯是sizeof后一直加()。
  • sizeof 括号中的表达式是不参与计算的。
cpp 复制代码
#include<stdio.h>
int main()
{
	short s = 5;
	int a = 10;
	printf("%d\n", sizeof(s = a + 2));
	printf("%d\n", s);
	return 0;
}             /*输出结果为:2
                           5*/
5.3.1 sizeof 和数组
cpp 复制代码
 //sizeof 和 数组
#include<stdio.h>

void test1(int arr[])
{
	printf("(3) %d\n", sizeof(arr));   //首元素指针变量的大小,该大小只与操作系统有关,与操作数类型无关
	                                   //32位系统一个指针变量的大小为4,64位系统一个指针变量的大小为8
}

void test2(char ch[])
{
	printf("(4) %d\n", sizeof(ch));    //首元素指针变量的大小
}

int main()
{
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("(1) %d\n", sizeof(arr));    //计算arr数组的长度
	printf("(2) %d\n", sizeof(ch));     //计算ch数组的长度

	test1(arr);     //传递的是首元素的地址
	test2(ch);      //传递的是首元素的地址

	return 0;
}              /*输出结果为:(1) 40
                            (2) 10
                            (3) 4
                            (4) 4*/

5.4 ++ 和 -- 运算符

  • a++ 和 a-- ------ 后置++、后置--:先使用,后运算。
  • ++a 和 --a ------ 前置++、前置--:先运算,后使用。
cpp 复制代码
#include<stdio.h>
int main()
{
	int a = 10;
	int x = a++;     //x=10, a=11
	int y = ++a;     //y=12, a=12
	int p = --a;     //p=11, a=11
	int q = a--;     //q=11, a=10
	return 0;
}

6. 关系操作符 ------ > >= < <= != ==

  • 比较操作数之间的大小、相等关系。
  • 比较两个字符串是否相等,不能使用 == 。

7. 逻辑操作符 ------ && ||

  • && - 逻辑与:有一假则假。
  • || - 逻辑或:有一真则真。
  • 区分按位与与逻辑与,按位或和逻辑或
cpp 复制代码
 //区分逻辑与按位
1&2------->0     //按位与
1&&2------>1     //逻辑与

1|2------->3     //按位或
1||2------->1    //逻辑或

例:360笔试题:

cpp 复制代码
 //360笔试题
#include<stdio.h>
int main()
{
	int i = 0, j = 0, k = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	printf("计算i时:a=%d b=%d c=%d d=%d i=%d\n", a, b, c, d, i);  //此时先运算a++,先使用后运算,a=0为假,逻辑与有一假则假,后面无需再算。
	
	j = a++ || ++b || a++;         //此时a=1 b=2 c=3 d=4
	printf("计算j时:a=%d b=%d c=%d d=%d j=%d\n", a, b, c, d, j);  //此时先运算a++,先使用后运算,a=1为真,逻辑或有一真则真,后面无需再算。
	
	k = a++ && ++b && d++;         //此时a=2 b=2 c=3 d=4
	printf("计算k时:a=%d b=%d c=%d d=%d k=%d\n", a, b, c, d, k);  //此时a b c运算结果全部为真,一直算到最后。

	return 0;
}            /*输出结果为:计算i时:a=1 b=2 c=3 d=4 i=0
                          计算j时:a=2 b=2 c=3 d=4 j=1
                          计算k时:a=3 b=3 c=3 d=5 k=1*/

8. 条件操作符 ------ exp1 ? exp2 :exp3

  • 若表达式1为真,则执行表达式2;若表达式1为假,则执行表达式3.
cpp 复制代码
 //条件操作符
int a = 8;
int b = 5;
max = (a > b ? a : b);     //求a和b的较大值
     //结果为:8

9. 逗号表达式 ------ exp1, exp2, exp3, ...expN

  • 从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
cpp 复制代码
#include<stdio.h>
int main()
{
	int a = 3;
	int b = 5;
	int c = 0;
	int d = (c = 5, a = c + 3, b = a - 4, c += 5);  //a= 8, b=4, c=10
	printf("d=%d\n", d);
	return 0;
}          //输出结果为:d=10

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

10.1 [] 下标引用操作符

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

cpp 复制代码
 //[]下标引用操作符
int arr[10];   //创建数组
arr[9] = 10;   //使用下标引用操作符
// [] 的两个操作数是 arr 和 9

10.2 () 函数调用操作符

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

cpp 复制代码
 // ()函数调用操作符
test();
test(a);
test(a, b);
//test为第一个操作数,a,b为传递给函数的参数

10.3 访问一个结构的成员------自己创建一个新的结构体类型 . ->

  • . - 结构体变量名.成员名
  • -> - 结构体指针名->成员名
cpp 复制代码
 //访问一个结构的成员
#include<stdio.h>
struct Stu
{
	char name[20];
	int age;
	char sex[20];
	double score;
};

int main()
{
	struct Stu b = { "张三", 19,"男",94.5 };
	struct Stu * pb = &b;
	printf("姓名:%s\n", b.name);
	printf("姓名:%s\n", pb->name);
	printf("性别:%s\n", b. sex);
	printf("性别:%s\n", pb->sex);
	return 0;
}              /*输出结果为:姓名:张三
                            姓名:张三
                            性别:男
                            性别:男*/

二、表达式求值

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

1. 隐式类型转换

1.1 整形提升

  • 整形提升:表达式中的字符或短整型在使用之前被转换为普通整形,这种转换称为整形提升。
  • 通常CPU是难以直接实现两个8比特字节直接运算的,所以,表达式中各种长度可能小于 int 长度的整型值,都必须转化为 int 或 unsigned int,然后才能送入CPU去执行运算。
  • 针对自身大小达不到一个整形的类型 ------ char short
  • 整形提升的步骤及演示代码:
  1. 写出整形操作数对应的原码;
  2. 看原本类型大小进行截断;
  3. 根据提升对象进行整形提升补二进制位;------ 补的是变量数据类型的符号位:负数补1,正数补0,无符号补0(%u打印无符号整形)。
  4. 提升后进行运算,再根据运算值所赋给的类型判断是否需要截断;
  5. 截断后若所需打印的类型仍需提升,则同以上规则补充符号位,提升后的二进制序列为补码,根据补码写出原码,即为运算的结果值。
cpp 复制代码
 //整形提升
#include<stdio.h>
int main()
{
	char a = 3;          //00000000000000000000000000000011
	                     //因为是char类型,截断后:00000011 - a

	char b = 127;        //00000000000000000000000001111111
	                     //因为是char类型,截断后:01111111 - b

	char c = a + b;      //此时发现 a 和 b 都是char类型的,没有达到一个int的大小,这里就会发生整形提升
	                     //a提升后 - 00000000000000000000000000000011
	                     //b提升后 - 00000000000000000000000001111111
	                     //  a+b   - 00000000000000000000000010000010
	                     //c为char类型的,截断后:10000010 - C

	printf("%d\n", c);   //%d是打印整形的,此时需对c的值进行整形提升
	                     //c提升后 - 11111111111111111111111110000010    补码
	                     //         11111111111111111111111110000001    反码
	                     //         10000000000000000000000001111110    原码   对应值为-126
	return 0;
}          //输出结果为:-126

例:判断程序输出的结果:

cpp 复制代码
 //代码1
#include<stdio.h>
int main()
{
	char a = 0xb6;       //0xb6对应十进制为182;对应二进制为 00000000000000000000000010010110
	                     //截断后 - 10010110 - a
	                     //整形提升后 11111111111111111111111110010110   补码
	                     //           11111111111111111111111110010101   反码
	                     //           10000000000000000000000001101010   原码      对应十进制数为 -106
	short b = 0xb600;    //同a
	int c = 0xb6000000;  //无需进行整形提升
	if (a == 0xb6)
		printf("a");
	if (b == 0xb600)
		printf("b");
	if (c==0xb6000000)
		printf("c");
	return 0;
}          //输出结果为:c
//因为a和b为char类型,需进行整形提升,整形提升后变成了负数,与原值不相等



 //代码2
#include<stdio.h>
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));     //运算需要进行整形提升
	printf("%u\n", sizeof(-c));     //运算需要进行整形提升

	return 0;
}             /*输出结果为:1
			               4
						   4*/

1.2 算数转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作无法进行。

下面的层次体系称为寻常算数转换。 ------ 由下向上转换

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

1.3 操作符的属性

  • 如果我们写出的表达式不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。
  • 复杂表达式的求值有三个影响的因素:
  1. 操作符的优先级 c=a+b*6
  2. 操作符的结合性 c=a+(b+d)
  3. 是否控制求值顺序
  • 操作符的优先级如下:

|-------|--------|------------------------------------------------------------------------------------------------------------------------|------------------|------|-----------|
| 类别 | 操作符 | 名称及作用 | 用法示例 | 结合方向 | 是否控制 求值顺序 |
| | () | 聚组 | (表达式) | N/A | 否 |
| 后缀操作符 | () | 函数调用 | (表达式)/函数名(形参) | 左到右 | 否 |
| 后缀操作符 | [] | 下标引用 | 数组名[常量表达式] | 左到右 | 否 |
| 后缀操作符 | . | 访问结构成员 | 结构名.成员名 | 左到右 | 否 |
| 后缀操作符 | -> | 访问指针结构成员 | 指针结构名->成员名 | 左到右 | 否 |
| 后缀操作符 | ++ | 后缀自增 | 变量名++ | 左到右 | 否 |
| 后缀操作符 | -- | 后缀自减 | 变量名-- | 左到右 | 否 |
| 单目操作符 | ! | 逻辑反 | !表达式 | 右到左 | 否 |
| 单目操作符 | ~ | 按位取反 | ~表达式 | 右到左 | 否 |
| 单目操作符 | + | 表示正值 | +表达式 | 右到左 | 否 |
| 单目操作符 | - | 表示负值 | -表达式 | 右到左 | 否 |
| 单目操作符 | ++ | 前缀自增 | ++变量名 | 右到左 | 否 |
| 单目操作符 | -- | 前缀自减 | --变量名 | 右到左 | 否 |
| 单目操作符 | * | 间接访问 | *指针变量 | 右到左 | 否 |
| 单目操作符 | & | 取地址 | &变量名 | 右到左 | 否 |
| 单目操作符 | sizeof | 计算长度,单位字节 | sizeof(表达式) | 右到左 | 否 |
| 单目操作符 | (类型) | 类型转换 | (类型)表达式 | 右到左 | 否 |
| 算数操作符 | * | 乘法 | 表达式*表达式 | 左到右 | 否 |
| 算数操作符 | / | 除法 | 表达式/表达式 | 左到右 | 否 |
| 算数操作符 | % | 余数(取模) | 整型表达式%整型表达式 | 左到右 | 否 |
| 算数操作符 | + | 加法 | 表达式+表达式 | 左到右 | 否 |
| 算数操作符 | - | 减法 | 表达式-表达式 | 左到右 | 否 |
| 移位操作符 | << | 左移 | 变量<<表达式 | 左到右 | 否 |
| 移位操作符 | >> | 右移 | 变量>>表达式 | 左到右 | 否 |
| 关系操作符 | > | 大于 | 表达式>表达式 | 左到右 | 否 |
| 关系操作符 | >= | 大于等于 | 表达式>=表达式 | 左到右 | 否 |
| 关系操作符 | < | 小于 | 表达式<表达式 | 左到右 | 否 |
| 关系操作符 | <= | 小于等于 | 表达式<=表达式 | 左到右 | 否 |
| 关系操作符 | == | 等于 | 表达式==表达式 | 左到右 | 否 |
| 关系操作符 | != | 不等于 | 表达式!= 表达式 | 左到右 | 否 |
| 位操作符 | & | 按位与 | 表达式&表达式 | 左到右 | 否 |
| 位操作符 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 否 |
| 位操作符 | | | 按位或 | 表达式|表达式 | 左到右 | 否 |
| 逻辑操作符 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 是 |
| 逻辑操作符 | || | 逻辑或 | 表达式||表达式 | 左到右 | 是 |
| 条件操作符 | ? : | 条件运算符 | 表达式1? 表达式2: 表达式3 | N/A | 是 |
| 赋值操作符 | = | 赋值运算符 | 变量=表达式 | 右到左 | 否 |
| 赋值操作符 | += | 加后赋值 | 变量+=表达式 | 右到左 | 否 |
| 赋值操作符 | -= | 减后赋值 | 变量-=表达式 | 右到左 | 否 |
| 赋值操作符 | *= | 乘后赋值 | 变量*=表达式 | 右到左 | 否 |
| 赋值操作符 | /= | 除后赋值 | 变量/=表达式 | 右到左 | 否 |
| 赋值操作符 | %= | 取模后赋值 | 变量%=表达式 | 右到左 | 否 |
| 赋值操作符 | /= | 除后赋值 | 变量/=表达式 | 右到左 | 否 |
| 赋值操作符 | *= | 乘后赋值 | 变量*=表达式 | 右到左 | 否 |
| 赋值操作符 | %= | 取模后赋值 | 变量%=表达式 | 右到左 | 否 |
| 赋值操作符 | <<= | 左移后赋值 | 变量<<=表达式 | 右到左 | 否 |
| 赋值操作符 | >>= | 右移后赋值 | 变量>>=表达式 | 右到左 | 否 |
| 赋值操作符 | &= | 按位与后赋值 | 变量&=表达式 | 右到左 | 否 |
| 赋值操作符 | ^= | 按位异或后赋值 | 变量^=表达式 | 右到左 | 否 |
| 赋值操作符 | |= | 按位或后赋值 | 变量|=表达式 | 右到左 | 否 |
| 逗号操作符 | , | 逗号运算符 | 表达式,表达式,... | 左到右 | 是 |

相关推荐
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师2 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java4 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山4 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn4 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
睡觉谁叫~~~4 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程4 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust