c语言修炼秘籍【第四章】操作符

c语言修炼秘籍【第四章】操作符

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

本文会对c语言中使用到的操作符进行介绍,包括

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员
  • 表达式求值

一、算术操作符

+ // 加
- // 减
* // 乘
/ // 除
% // 取模
  1. %外,其他的几个操作符都可以作用于整数和浮点数;
  2. 对于/操作符,如果两个操作数都是整数,则执行整数除法;而只要有一个操作数为浮点数,就执行浮点数除法。
  3. %操作符的两个操作数必须都是整数。返回整除之后的余数。
#include <stdio.h>

int main()
{
	int a = 2;
	int b = 3;
	float c = 3.0f;
	printf("a / b == %f\n", a / b);
	printf("a / c == %f\n", a / c);

	return 0;
}

运行结果:
在这里插入图片描述
可以看到,2 / 3以浮点数的形式打印出来,仍然是0;2 / 3.0打印的0.666667才是正确的浮点类型结果。

二、移位操作符

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

// 注:移位操作符的操作数只能是整数

1.左移操作符

移位规则:
左边抛弃,右边补0


移位操作符对操作数进行操作时,是对它的保存在内存中的二进制形式进行操作。
如,5 << 1;
在这里插入图片描述
在移位过程中,5本身并没有被改变(没有进行赋值)。

2.右移操作符

右移的移位规则分成两种:

  1. 逻辑右移:右边抛弃,左边补0
  2. 算术右移:右边抛弃,左边补符号位

再这里会涉及到符号位,所以简单介绍一下,
计算机中只有两种状态10,所以计算机中的数都是以二进制的形式保存并使用的,
而二进制数又分为有符号数和无符号数,其中有符号数中的符号的对机器而言是无法识别的,但刚好对应着两种状态,所以这两种符号也被数字化了,规定0表示1表示,最高位为符号位。
有符号数可分为:原码补码反码这三种。计算机中用补码保存二进制数

  1. 原码:
    正数:它的原码就是它自身;
    负数:符号位为1,其余位和它的绝对值的原码相同;
  1. 反码:
    正数:它的反码就是它自身;
    负数:符号位不变,其余位是它的原码取反;
  1. 补码:
    正数:它的补码就是它自身;
    负数:它的反码+1;

例如:5
它的二进制形式为:
0000000 00000000 00000000 00000101 - - 原码
0000000 00000000 00000000 00000101 - - 反码
0000000 00000000 00000000 00000101 - - 补码
正数这三种形式相同


再例如:-3
它的绝对值的原码为:00000000 00000000 00000000 00000011
10000000 00000000 00000000 00000011 - - 原码 - - 改变了符号位
11111111 11111111 11111111 11111100 - - 反码 - - 符号位之外全部把按位取反
11111111 11111111 11111111 11111101 - - 补码 - - 反码 + 1


对于逻辑移位,正如它的名字,它只负责把内存中的二进制位往右移动指定的位数,使用这种方法进行移位时,它压根不管符号位,既然不管符号位,缺位直接补0就行。
例如:-2 >> 1
首先,将-3转换为补码形式
1000000 0000000 0000000 0000010 - - 原码 - - 改变了符号位
11111111 11111111 11111111 11111101 - - 反码 - - 符号位之外全部把按位取反
11111111 11111111 11111111 11111110 - - 补码 - - 反码 + 1
右移1位:(逻辑右移)
01111111 11111111 11111111 11111111 - - 补码
此时符号位为0,机器认为它是一个正数,补码和原码相同,
-2 >> 1就变成了有符号int类型能表示的最大值;


对于算术移位,既然是算术,肯定要考虑符号了,所以以这种方式进行右移操作时,左边补位时是使用它的符号位来补的
同样的例子:-2 >> 1
右移1位:
11111111 11111111 11111111 11111111 - - 补码
转换为原码:
10000000 0000000 00000000 00000000 - - 反码
10000000 0000000 00000000 00000001 - - 原码
-2 >> 1就变成了-1;
那么我们的计算机中的右移操作是使用的哪种方式呢?
使用下面的代码进行验证:

#include <stdio.h>
#include <limits.h>

int main()
{
	int a = -2; 
	printf("a >> 1 == %d\n", a >> 1);
	printf("INT_MAX == %d\n", INT_MAX);

	return 0;
}

运行结果:
在这里插入图片描述
可以看到,a >> 1输出的结果为-1,符合算术右移的分析。

注:移位操作符不可移动负数位,这是标准未定义的操作

int a = -10;
a >> -1 // error,标准未定义

三、位操作符

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

注意:它们的操作数必须是整数

下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	int a = 5;
	int b = 6;

	printf("a & b == %d\n", a & b);
	printf("a | b == %d\n", a | b);
	printf("a ^ b == %d\n", a ^ b);

	return 0;
}

5 - - 补码 - - 00000000000000000000000000000101
6 - - 补码 - - 00000000000000000000000000000110


1 & 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000100 - - 4
1 | 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000111 - - 7
1 & 2
00000000000000000000000000000101
00000000000000000000000000000110
结果为:
00000000000000000000000000000011 - - 3

运行结果:
在这里插入图片描述

一道面试题:
不能创建临时变量,实现两个数的交换

#include <stdio.h>

// 方法一
// 使用临时变量进行交换
void swap1(int* num1, int* num2)
{
	int tmp = 0;
	tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

// 方法二
// 不使用临时变量进行交换
void swap2(int* num1, int* num2)
{
	*num1 = *num1 + *num2;
	*num2 = *num1 - *num2;
	*num1 = *num1 - *num2;
}

// 方法三
// 不使用临时变量进行交换
void swap3(int* num1, int* num2)
{
	*num1 = *num1 ^ *num2;
	*num2 = *num1 ^ *num2;
	*num1 = *num1 ^ *num2;
}

int main()
{
	int a = 5;
	int b = 6;

	swap1(&a, &b);
	printf("a == %d, b == %d\n", a, b);
	swap2(&a, &b);
	printf("a == %d, b == %d\n", a, b);
	swap3(&a, &b);
	printf("a == %d, b == %d\n", a, b);

	return 0;
}

运行结果:
在这里插入图片描述
注意:
方法二看上去虽然可以实现不使用临时变量就交换两个数,但实际上,这个代码是存在问题的 - - num1 + num2可能会溢出。
虽然,在VS编译器中,该方法实际执行过程中,在出现溢出时的结果并没有出错,但这并不意味着,该方法正确;在c语言中有符号整数的溢出是未定义行为,因此该代码的“正确性”是依赖于编译器和硬件的,具有偶然性。

交换两个整数应该使用第一种或第三种方法。

练习

求一个整数存储在内存中的二进制中1的个数。

方法一,请思考该方法是否存在问题

#include <stdio.h>

int main()
{
	int num = 7;
	int count = 0;

	int n = num;
	while (n)
	{
		if (n % 2 == 1)
		{
			count++;
		}
		n = n / 2;
	}
	printf("%d中1的个数 == %d\n", num, count);

	return 0;
}

分析
当num为负数时,例如,-1 - - 内存中的数是以补码形式保存 - -> 11111111111111111111111111111111 - - 有32个1
-1 % 2 == 1-1 / 2 == 0,此时count == 1循环结束,结果错误。
显然,该方法无法计算负数的二进制中1的个数。

方法二

#include <stdio.h>

int main()
{
	int num = 7;
	int i = 0;
	int count = 0;

	// int类型内存中占32bit
	for (i = 0; i < 32; i++)
	{
		if ((num >> i) & 1)
		{
			count++;
		}
	}
	printf("%d中1的个数 == %d\n", num, count);

	return 0;
}

分析
该方法能够完成所需功能,但应该注意到,该方法对于所有的数都会进行32次循环,当它处理1时 - - 仅仅只有1个二进制1,但却也需要处理32次,能否进行优化呢?

方法三

#include <stdio.h>

int main()
{
	int num = 7;
	int count = 0;

	int n = num;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
	printf("%d中1的个数 == %d\n", num, count);

	return 0;
}

分析
该方法的处理次数与待处理数有关,它有多少个1就循环多少次。
举个例子:

7的二进制形式
第一次
0111 - - n
0110 - - n - 1
0110 - - n = n & (n - 1)
第二次
0101 - - n - 1
0100 - - n = n & (n - 1)
第三次
0011 - - n - 1
0000 - - n = n & (n - 1)


n = n & (n - 1)这个运算每次都将n最后一个1化为0。

四、赋值操作符

赋值操作符,能让你给一个变量赋一个你任意想要的值。

int height = 160; // 身高
height = 180; // 不满意就赋值
float score = 3.0f; // 评分
score = 13.0f; // 赋值为13.0

赋值操作符=,可以连续使用。

int x = 1;
int y = 2;
int z = 3;
z = y = x + 2;

// 对比上述代码
y = x + 2;
z = y;

// 下面的代码更让人容易接受,可读性更高。

一些复合赋值操作符:

+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=

//x += 3 等价于 x = x + 3
// 其他的与之用法相同

五、单目操作符

1.单目操作符的介绍

单目操作符的种类

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

// 其中 x++ 等价于 x += 1
// -- 用法类似
// 除此之外,++ 和 -- 还分为前置和后置之分
// x++ 和 --x,虽然都是对x自增1,但x++是用后再加,++x是用前先加
// 举个例子
int x = 3;
int y = 3;
printf("x++ == %d\n", x++);
printf("++y == %d\n", ++y);

// 输出结果为:
// x++ == 3
// ++y == 4
// x是先被输出再+1
// y是先+1再输出

// 使用示例
#include <stdio.h>

int main()
{
	if (!1)
		printf("!1\n");
	else
	{
		printf("0\n");
	}

	int a = 1;
	printf("a == %d, -a == %d, +a == %d\n", a, -a, +a);

	printf("a`s address == %p\n", &a);

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

	a = -1;
	printf("~a == %d\n", ~a);

	int x = 3;
	int y = 3;
	printf("x++ == %d\n", x++);
	printf("++y == %d\n", ++y);

	printf("*(&a) == %d\n", *(&a));

	a = 97;
	printf("(char)a == %c\n", (char)a);

	return 0;
}

示例运行结果:
在这里插入图片描述

关于sizeof,它其实是一个操作符,并非是一个函数,验证代码如下

#include <stdio.h>

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

	return 0;
}

在这里插入图片描述
可以看到,输出确实是4,这里的sizeof并没有使用()函数调用,这也证明了sizeof的确不是函数。

注:sizeof int这是不允许的
在这里插入图片描述

2.sizeof 和数组

看下面的代码,你觉得它们会输出什么?

#include <stdio.h>

void test1(int a[])
{
	printf("sizeof a == %d\n", sizeof(a));
}

void test2(char b[])
{
	printf("sizeof b == %d\n", sizeof(b));
}

int main()
{
	int a[10] = { 3 };
	char b[10] = { 'a' };
	printf("sizeof a == %d\n", sizeof(a)); 
	printf("sizeof b == %d\n", sizeof(b));
	test1(a);
	test2(b);
	int x = 10;
	printf("sizeof x == %d\n", sizeof(x += 2));
	printf("x == %d\n", x);

	return 0;
}

得出你的结果了吗?特别是最后的x的输出是多少?
运行结果如下:
在这里插入图片描述
有没有出乎你的预料?
test1()test2()中的输出都为4是因为数组名表示的首元素的地址,地址在32位平台中占4个字节,所以输出4
而在main()函数中的输出为40和10是因为数组名的表示含义有两个特例:

  1. sizeof(<数组名>),这里的数组名表示整个数组;
  2. &<数组名>,这里的数组名也表示整个数组;

所以主函数中的输出为整个数组在内存中所占的字节数。


对于最后的x输出的值为4,首先,我们要先明确一个表达式有两种属性:

  1. 值属性
  2. 类型属性

例如:
int a = 10;
a这个表达式它的值属性此时为10;类型属性为int
char b = ‘a’;
b这个表达式它的值属性此时为98 - - 字符在内存中以ASCII码形式存储;类型属性为char
sizeof这个操作符在使用时,仅需要使用表达式的类型属性即可 - - 类型决定了它在内存中所占据的空间,不关心它的值为多少,其中的表达式不会真的计算。
也就是说sizeof(x += 2)中的x+=2并没有计算,仅仅是获取到x是一个int类型即可,所以x的值没有发生改变

你觉得下面的代码会输出什么?

#include <stdio.h>

int main()
{
	char a[] = "hello";
	char b[] = "hello world";

	if (sizeof(a) - sizeof(b) < 0)
	{
		printf("a shorter than b\n");
	}
	else
	{
		printf("a longer than b\n");
	}

	return 0;
}

运行结果:
在这里插入图片描述
这是为什么呢?a明明比b短,为什么会输出a比b更长呢?
让我们来看看sizeof的定义:
在这里插入图片描述
在这里插入图片描述
通过size_t的定义可知,它是无符号整型的别名。所以,sizeof返回的值是个无符号数,两个无符号数的运算恒大于等于0,所以上面的代码一定会输出else中的语句。

六、关系操作符

<
<=
>
>=
==
!=

这些关系操作符用法简单,就不再赘述。只需要注意不要将 = 和 == 混用即可。

七、逻辑操作符

&& // 逻辑与
|| // 逻辑或

// 结果只有真和假

注意区分按位与和逻辑与,按位或和逻辑或

1&2 == 0
1&&2 == 1 // 真

1|2 == 3
1||2 == 1 // 真

一道360的面试题:

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

i = a++ && ++b && d++;
这里的输出结果是什么呢?
a = 1, b = 3, c = 3, d = 5?
运行结果:

在这里插入图片描述
i = a++ || ++b || d++;
这里的输出结果是又是什么呢?
a = 1, b = 3, c = 3, d = 5?
在这里插入图片描述


分析
从第一张图中可以看出i = a++ || ++b || d++;这条语句中,仅执行了a++,后续的操作并没有执行,这里因为a++是后置++,在&&操作的过程中是0为假,逻辑与&&中只要有一个操作数为假,则整个语句为假,后续的操作没有执行,所以结果为1 2 3 4;
第二张图中能看出执行了a++++b,与上面分析类似,逻辑或||中只要有一个操作数为真,则整个语句为真,++b前置++,结果为真,所以,后续的d++就没有执行,最终输出结果为1 3 3 4。

八、条件操作符

exp1 ? exp2 : exp3
使用示例:

int a = 1;
int b = 2;
if(a > b)
{
	printf("%d", a);
}
else
{
	printf("%d", b);
}

// 用条件操作符改写
(a >b) ? printf("%d", a) : printf("%d", b);

九、逗号表达式

exp1, exp2, exp3, …, expN
逗号表达式就是用,隔开的多个表达式。
从左往右依次计算。整个表达式的结果为最后一个表达式的结果。

#include <stdio.h>

int main()
{
	// 代码1
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("c == %d\n", c);
	// c是多少?
	
	// 代码2
	int d = -1;
	if (a = b + 1, c = a / 2, d > 0)
	{
		printf("if\n");
	}
	else
	{
		printf("else\n");
	}
	// 输出什么?

	// 代码3
	a = get_val();
	count_val(a);
	while (a > 0)
	{
		// 业务代码
		get_val();
		count_val(a);
	}
	// 上面代码能改成
	while (a = get_val(), count_val(a), a > 0)
	{
		// 业务代码
	}

	return 0;
}
  1. 代码1:此代码是用逗号表达式对c进行赋值操作,将逗号表达式从左到右依次执行,且此逗号表达式的结果为最后一个表达式b = a + 1,此时b == 13,所以c被赋值为13
  2. d > 0这个关系表达式的值为整个逗号表达式的值,d == -3,d > 0 为假,所以输出else

在这里插入图片描述

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

下标引用操作符[]有两个操作数 - - 一个数组名 + 一个索引值,
例如:

int a[10] = { 0 };
a[0] = 1; // 使用下标引用操作符
// a 和 0 是操作数
// 实际上等价于 *(a + 0) -- 指针的使用,后续章节会详细介绍

函数调用() - - 接受一个或多个操作数:第一个是函数名,剩余的操作数就是传递给函数的参数
例如:

#include <stdio.h>

void test1()
{
	printf("test1\n");
}

void test2(const char* str)
{
	printf("test2:");
	printf("%s\n", str);
}

int main()
{
	test1(); // 使用了()操作符,只有一个操作数 -- 函数名test1
	test2("This is test2"); // 使用了()操作符,有两个操作数 -- 函数名test1,字符串参数

	return 0;
}

结构体成员访问操作符.-> - - 都有两个操作数
. - - 使用示例:结构体.成员名
-> - - 使用示例:结构体指针->成员名
例如:

#include <stdio.h>

typedef struct
{
	int age;
	char name[20];
	char sex[5];
} Stu;

int main()
{
	Stu stu = { 30, { 0 }, { 0 } };
	Stu* pstu = &stu;

	// .操作符
	stu.age = 18;
	printf("age == %d\n", stu.age);


	// ->操作符
	pstu->age = 20;
	printf("age == %d\n", stu.age);

	return 0;
}

运行结果:
在这里插入图片描述

十一、表达式求值

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

1.隐式类型转换

c的整型算术运算总是至少以缺省的整数类型的精度来进行的 - - 求值数在内存中的基础长度为4字节,短了需要补足。
为了获得这个精度,表达式中的字符和短整型操作数要在使用之前转换为普通整型,这种转换称为整型提升

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

char a, b, c;
...
a = b + c;
// b 和 c 的值被提升为普通整型,然后执行加法运算,加法运算完成后,对结果进行截断,然后再存储到a中。

如何进行整型提升呢?

整型提升是基于变量的数据类型的符号位来提升的

// 负数的整型提升
char a = -1;
// 变量a的二进制位(补码)只有8个bit位:
// 11111111
// char是有符号的 char
// a 的符号位为1,在整型提升时,高位补充符号位,即1
// 提升后的结果为:
// 11111111111111111111111111111111 

// 正数的整型提升
char b = 1;
// 变量b的二进制位(补码)只有8个bit位:
// 00000001
// char是有符号的 char
// a 的符号位为0,在整型提升时,高位补充符号位,即0
// 提升后的结果为:
// 00000000000000000000000000000001

// 如果是无符号数的整型提升,高位直接补 0

整型实例1:下面代码会输出什么?

#include <stdio.h>

int main()
{
	char a = 0x80;
	short b = 0x8000;
	int c = 0x800000;

	if (a == 0x80)
	{
		printf("a\n");
	}
	if (b == 0x8000)
	{
		printf("b\n");
	}
	if (c == 0x800000)
	{
		printf("c\n");
	}

	return 0;
}

运行结果:
在这里插入图片描述
可以看到仅输出了一个c,说明在判断中a != 0x80b != 0x8000
这是因为,在进行a == 0x80b == 0x8000时,ab的类型长度小于4,需要进行整型提升,且它们的符号位为1,提升是补1,实际上a == 0x8011111111111111111111111110000000 == 00000000000000000000000010000000为假,b == 0x800011111111111111111000000000000000 == 00000000000000001000000000000000为假,只有c == 0x800000为真。

整型实例2:下面代码会输出什么?

#include <stdio.h>

int main()
{
	char c = 1;
	printf("c == %u\n", sizeof(c));
	printf("-c == %u\n", sizeof(-c));
	printf("+c == %u\n", sizeof(+c));

	return 0;
}

运行结果:
在这里插入图片描述
c只要参与表达式运算,就需要整型提升,表达式+c-c都参与了表达式运算,就都发生了提升,所以输出为4 - - int类型的大小,sizeof©中的c没有进行整型提升,所以输出1。

2.算术转换

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

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

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后再执行运算。
算术转换要合理,否则会有一些潜在的问题

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

3.操作符属性

复杂表达式的求值有三个影响因素:

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

两个相邻的操作符先执行哪个?取决于它们的优先级,优先级相同,则取决于它们的结合性。

操作符优先级描述用法示例结合性是否控制求值顺序
()1聚组(表达式)与表达式相同N/A
()1函数调用函数名(参数, …, 参数)L-R
[]1下标引用数组名[下标]L-R
.1访问结构成员结构体.成员名L-R
->1访问结构指针成员结构体指针->成员名L_R
++2后置自增表达式++L-R
--2后置自减表达式- -L-R
!2逻辑反!表达式R-L
~2按位取反~表达式R-L
+2单目,表示正值+表达式R-L
-2单目,表示负值-表达式R-L
++2前置自增++表达式R-L
--2前置自减- -表达式R-L
*2间接访问*指针变量R-L
&2取地址&变量名R-L
sizeof2取其长度,以字节为单位sizeof(表达式)R-L
(类型)2强制类型转换(类型)表达式R-L
*3乘法表达式 * 表达式L-R
/3除法表达式 / 表达式L-R
%3整数取余表达式 % 表达式L-R
+4加法表达式 + 表达式L-R
-4减法表达式 - 表达式L-R
<<5左移变量 << 表达式L-R
>>5右移变量 >> 表达式L-R
>6大于表达式 > 表达式L-R
>=6大于等于表达式 >= 表达式L-R
<6小于表达式 < 表达式L-R
<=6小于等于表达式 <= 表达式L-R
==7等于表达式 == 表达式L-R
!=7不等于表达式 != 表达式L-R
&8按位与表达式 & 表达式L-R
|9按位或表达式 | 表达式L-R
^10按位异或表达式 ^ 表达式L-R
&&11逻辑与表达式 && 表达式L-R
||12逻辑或表达式 || 表达式L-R
?:13条件操作符表达式 ? 表达式 : 表达式N/A
=14赋值变量 = 表达式R-L
+=14加后赋值变量 += 表达式R-L
-=14减后赋值变量 -= 表达式R-L
*=14乘后赋值变量 *= 表达式R-L
/=14除后赋值变量 /= 表达式R-L
%=14取模后赋值变量 %= 表达式R-L
<<=14左移后赋值变量 <<= 表达式R-L
>>=14右移后赋值变量 >>= 表达式R-L
&=14按位与后赋值变量 &= 表达式R-L
|=14按位或后赋值变量 |= 表达式R-L
^=14按位异或后赋值变量 ^= 表达式R-L
,15逗号表达式表达式, 表达式, …, 表达式R-L

一些问题表达式

表达式1

// 表达式的求值部分由操作符的优先级决定
// 表达式1
a * b + c * d + e * f
// * 的优先级比 + 高,只能保证第一个 * 比 + 更早运算,
// 但无法保证e * f 比a * b + c * d 更早运算。
// 即可能出现
// a. 
// (1).(a * b) 
// (2).(c * d) 
// (3).(e * f)
// (4).(1) + (2)
// (5).(4) + (3)
// b. 
// (1).(a * b) 
// (2).(c * d) 
// (3).(1) + (2)
// (4).(e * f)
// (5).(3) + (4)

表达式2

// 表达式2
c + -- c;
// 同样的从优先级中只能获知,--比 + 更早运算,
// 但左边的c是在--c之前获取的,还是在其之后获取的这并不确定,该代码同样存在歧义。
// 假设 c = 2
// 上述代码可能为:
// 1. 2 + --2; // --c执行之前,前面的c就获取值
// 2. 1 + --2; // --c执行之后,前面的c才获取值

表达式3

// 表达式3
#include <stdio.h>

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

	return 0;
}

这是一个很典型的错误代码,在不同的编译器中的结果都不相同,如果,那个程序员写了这样的代码,一定会被人骂"猪队友"。

表达式4

// 代码4
#include <stdio.h>

int func()
{
	static int count = 1;
	return count++;
}

int main()
{
	int answer;
	answer = func() - func() * func();
	printf("answer == %d\n", answer);

	return 0;
}

这个代码有问题吗?
有问题!
虽然表达式中的 - 和 * 可以通过优先级来决定谁先计算,但func()的计算顺序是依赖于编译器的,有可能先计算第一个func(),再计算第二个func(),最后计算第三个func(),这样的结果为 1 - 2 * 3
也可能先计算第二个和第三个,最后计算第一个,这两种的计算结果是不同。
所以这样的代码也是不可靠的。

表达式5

#include <stdio.h>

int main()
{
	int i = 1;
	int ret = 0;
	ret = (++i) + (++i) + (++i);
	printf("ret == %d\n", ret);
	printf("i == %d\n", i);

	return 0;
}

这个代码在不同的编译器中的执行结果也是不同的,无法确认表达式中的计算顺序,
比如,上述代码可能出现以下几种计算顺序:

  1. i先自增3次,此时i == 4,之后再进行 + 操作,这样得出的结果为12
  2. 先执行前两个++i,此时i == 3,再执行 + ,表达式转化为 6 + (++i),此时再次执行++i,i == 4,最后执行6 + 4,这样得到的结果为10。

总结
我们写出的表达式的执行顺序,无法通过操作符的属性来唯一确认的话,那么这个表达式就是个问题表达式。


总结

本文对c语言中会使用到的操作符进行较为详细的介绍说明,特别是对移位操作符中的右移操作的两种情况进行了分析,详细说明了sizeof的使用以及使用时可能遇到的问题,逻辑操作符运算过程中可能出现的反直觉问题,表达式求值中整型提升、算术转换、操作符的优先级,并列出、分析了一些问题表达式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值