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

零中频架构实战:用Matlab模拟AD9361收发信号(附完整代码) 本文详细介绍了如何使用Matlab模拟AD9361射频收发器的零中频架构,解决镜像干扰等关键挑战。通过完整的代码示例和系统级建模方法,帮助工程师掌握RFIC设计中的自适应校准算法和性能优化技巧,特别适用于SDR系统开发。 阅读详情

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. 算术右移:右边抛弃,左边补符号位

再这里会涉及到符号位,所以简单介绍一下,
计算机中只有两种状态1和0,所以计算机中的数都是以二进制的形式保存并使用的,
而二进制数又分为有符号数和无符号数,其中有符号数中的符号的正,负对机器而言是无法识别的,但刚好对应着两种状态,所以正,负这两种符号也被数字化了,规定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 != 0x80,b != 0x8000,
这是因为,在进行a == 0x80和b == 0x8000时,a和b的类型长度小于4,需要进行整型提升,且它们的符号位为1,提升是补1,实际上a == 0x80是11111111111111111111111110000000 == 00000000000000000000000010000000为假,b == 0x8000是11111111111111111000000000000000 == 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的使用以及使用时可能遇到的问题,逻辑操作符运算过程中可能出现的反直觉问题,表达式求值中整型提升、算术转换、操作符的优先级,并列出、分析了一些问题表达式。

Abaqus6.14.4 Linux详细图文安装教程 Abaqus6.14.4 Linux详细图文安装教程 20170409 1. 准备安装环境 1.1 安装CentOS 6.5,更新库 1.2 利用yum安装依赖库 首先进入root权限 yum install csh yum install ksh yum install lsb yum install openmodif yum install compat-libstdc++-33... 阅读详情

相关推荐

【操作系统】 Operation System 第二章:操作系统基础操作

2.1 操作系统的启动 (1)CPU, I/O, 内存通过总线连接。 (2)DISK:存放OS; BIOS:基本I/O处理系统( basic I/O system); Bootloader: 加载OS到内存中。 (3)当电脑通电时,段寄存器CS和指令寄存器IP能够确定一个内存地址,例如CS:IP = 0xf000:fff0. (4)POST(加电自检),寻找显卡和执行BIOS。(显示器,键盘…是否...

iwanderu的博客 1万+

C语言修炼秘籍之——操作符

内些个操作符到底怎么个事儿啊?二进制及其及其进制转换是个什么玩意儿啊?原反补我到现在都还妹学明白你能给我教会唠?这位客官,咱里边儿请,且听我娓娓道来~

2301_80029060的博客 1532

Cadence16.6安装教程

如需安装所需的软件,请到文章末尾处查看 安装步骤: 安装前先关闭杀毒软件和360卫士,注意安装路径不能有中文,安装包路径也不要有中文。 试装系统:win10 64bit 1.解压安装包(共5个压缩包,每个都要解压) 2.以管理员身份运行安装程序。 3.选择License Manager。 4.点击NEXT。 5.选择I accept,然后点击NEXT。 6.选择License Manage...

谢谢关注“小鱼教你模数电”,各平台同号 8万+

C语言修炼秘籍【第零章】c语言概述

c语言是一门编程语言。语言的作用就是交流,人们可以利用它与计算机进行交流,让计算机知道你需要它完成什么工作。本系列文章是对笔者在c语言学习的总结记录。本文会先粗略的介绍c语言中需要学习的内容。

PLKDTHFS的博客 1664

【项目解析】2KW 230V 数字逆变器控制系统(二)

【项目解析】 2KW 230V 数字逆变器控制系统(一) 项目的 交流输出电压 并不是单纯一个电压环就能控制好的,而是典型的 双环控制架构:最终占空比控制量会经过计算: 然后写入 单片机 TMM 定时器的比较寄存器 (TMGRA0/TMGRD0/TMGRC1), → 定时器输出互补 PWM, → 驱动 IGBT 全桥, → 经过 LC 滤波形成 230 VAC / 50Hz 的正弦电压。总结交流电压是双环控制的结果:目标/作用:把输出 RMS 电压稳到目标(例如 230 VAC);在母线波动与负载变化下

Naiva的博客 1296

6、深入解析Microsoft 365核心服务对比与迁移策略

本文深入解析了Microsoft 365的核心服务,包括身份和设备管理、现代设备配置体验、Exchange和SharePoint的迁移策略以及Microsoft Teams的协作功能。同时探讨了迁移过程中需要考虑的安全控制措施,为组织规划和实施云迁移项目提供了全面的指导和建议。

k8s6orchestrator的博客 106

c语言修炼秘籍【第五章】指针

本文会从介绍指针是什么开始,循序渐进地介绍指针的使用、指针使用时会遇到的问题、指针能进行哪些运算、指针与数组之间的关系,以及指针数组这些内容。本文对c语言中使用的指针进行了较简略的介绍,包括什么是指针,指针类型有什么用,指针的一些运算,什么是野指针,如何利用指针访问数组,二级指针使用中会出现的问题,以及指针数组。

PLKDTHFS的博客 1763

【AI大模型:前沿】46、AI+无人机:智能飞行革命全景指南——从技术突破到行业应用与未来趋势

AI+无人机的融合不仅是技术迭代,更是“空间生产力”的革命——它将人类活动范围从地面拓展至低空,重构物流、农业、应急等行业的生产方式。当前,技术突破(如多模态感知、群体智能)与法规松绑(超视距飞行许可)正加速产业化,预计2030年全球低空经济市场规模将突破万亿美元。技术层:轻量化AI模型、边缘-云端协同、安全冗余系统;应用层:垂直领域解决方案(如电网AI巡检、精准农业);生态层:无人机巢建设、空管系统、数据服务平台。

2025博客之星Top81。专注AI工程化与架构实战。从分布式思维到模型部署,用工程化视角为你厘清AI落地的真实路径。 5591

c语言修炼秘籍【第二章】函数

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归,并在最后对汉诺塔、青蛙跳台阶和八皇后问题进行详细分析(包含全部代码)

PLKDTHFS的博客 1443

Altium Designer20 PCB板子绘制

版层 top layer 顶层 bottom layer 底层 设置板子大小 布局 过孔 top layer层连到bottom layer层。通过via连接。 设置线宽 在design rules里可以设置线宽。 右键new rule 可以新增线宽,电源设置成20mil 还可以设置很多其他的线宽 修改安全距离 安全距离太短容易出现毛刺,然后短路,一般设置成20mil 修改元器件旋转角度 修改rotation step,每次旋转45°。 修改光标大小 下面的cursor type是修改光标

及时总结 2581

华为OD机试真题 新系统 2026-04-19 Java&Go 实现【分辨率排序】

华为OD机试真题 新系统 2026-04-19 Python&JS 实现【分辨率排序】

MISAYAONE的博客 303

c语言修炼秘籍【第六章】结构体

将结构体与数组进行对比:数组是一组相同类型的元素集合;结构体是一些值的集合,它们类型可以不同,这些值被称为成员变量。使用结构体类型定义变量int x;int y;} p1;// 声明类型的同时定义变量p1// 定义结构体变量 p2// 初始化:定义变量的同时赋初值struct Stu// 名字int age;// 年龄int data;// 结构体嵌套的初始化// 结构体嵌套的初始化本文对c语言中使用到的结构体进行了简单介绍,更详细的会在后续攻略中详解。

PLKDTHFS的博客 761

AI赋能数字人:打造与语音节奏完美匹配的高质量手势动画

想象一个场景:当你与一位数字人交谈时,它不仅能理解你的言语,还能恰到好处地挥动手臂、比划手势,就像真实的人类交流一样自然流畅。这正是当前AI技术致力于实现的目标——通过分析语音内容,自动生成与之相匹配的手势动作,为数字人赋予更加丰富的情感表达和非言语沟通能力。AI手势生成技术的出现,标志着数字人在模仿人类自然交流方式上迈出了重要一步。随着技术的不断成熟与应用场景的拓宽,未来我们可以期待更多富有表现力和情感互动的数字人出现在我们的日常生活中,开启人机交互的新篇章。

这里汇聚了前沿的技术分享与实用的开发技巧,带你探索从创意到企业的技术创业之路。 792

c语言修炼秘籍【第三章】数组

本文将会介绍常用到的一维数组和二维数组是如何使用及它们在内存中是如何存储,说明何为数组越界,并讨论数组在作为函数参数时应该如何使用。在最后附带三子棋和扫雷的代码实现详解。本文对一维和二维数组的初始化、创建、使用,作出的简要的介绍,并通过冒泡排序算法的实现,详细说明了当数组作为函数参数时的性质 - - 传递的是首元素地址。在最后,给出了两个数组的使用实例,附带了完整的实现代码,并配有详细注释。

PLKDTHFS的博客 1257

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

c语言修炼秘籍【第一章】分支与循环语句

在处理现实中的问题时,我们总会遇到需要作出选择或是需要重复多次的完成一种事件。你去图书馆学习,需要日复一日的坚持,每多学一天,你的代码功力就能+1,当你的代码功力,达到了某一程度,你就能够迎娶白富美,走上人生巅峰。为了实现这些目的,c语言提供了分支语句,用以实现选择;提供循环语句,用以实现重复多次的机械活动;表达式语句函数调用语句控制语句复合语句空语句分支、循环语句属于控制语句。控制语句用于控制程序的执行流程,以实现程序的各种结构方式,它们由各种特定的语句定义符构成;

PLKDTHFS的博客 1244

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

c语言修炼秘籍【第七章】const与c语言中一些错误代码

而数组内部的内存使用分配是从低到高的,arr[0]的地址在最低位,当循环进行到第11次 - - arr[11]设置的其实是i的值,此时将 i 的值设为了0,循环继续。这是因为,程序要进行报错也是需要执行时间的,访问arr[11]时程序仍处于循环中,没有时间报错就又进入了下一次循环。test1()可以正常使用指针对n为进行修改,也可以对p指向的位置进行修改。test2()不能通过指针对n进行修改,可以对p指向的位置进行修改。,arr后分配,地址会比i的地址低,分配到的地址为。,i先分配空间,地址为。

PLKDTHFS的博客 674

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第一式】数据的存储

本章节的重点是数据类型详细介绍整型在内存中的的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析char // 字符数据类型short // 短整型int // 整型long // 长整型long long // 更长的整型float // 单精度浮点数double // 双精度浮点数// c语言有没有字符串类型?// 没有以及他们所占存储空间的大小。使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角什么是大端小端?

PLKDTHFS的博客 1487

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

字符指针数组指针指针数组数组传参和指针传参几种数组、指针之间的比较函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组的笔试题指针是一个存储地址的变量,地址是一块内存空间的唯一标识;指针的大小是固定的(4字节或8字节);指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;接下来,就让我们继续探讨指针的进阶主题。对于普通的指针,我们都已经很熟悉了。

PLKDTHFS的博客 1518

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译

程序的翻译环境程序的执行环境c语言程序的编译+链接预定义符号介绍预处理指令#define宏和函数的对比预处理操作符#和##的介绍命令定义预处理指令#include预处理指令#undef条件编译__FILE__ // 进行编译的源文件__LINE__ // 文件当前行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义。

PLKDTHFS的博客 1195

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。为什么存在动态内存分配动态内存函数的介绍mallocfreecallocrealloc常见的动态内存错误。

PLKDTHFS的博客 1293

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作

为什么使用文件什么是文件文件的打开和关闭文件的顺序读写文件的随机读写文本文件和二进制文件文件读取结束的判定文件缓冲区磁盘上的文件就是文件。但是在程序设计上,我们谈到的文件一般有两种:程序文件、数据文件;本节详细的对c语言中数据文件的使用进行了详细的介绍,包括对文件读写操作使用的函数,以及通过文件位置指针的操作函数来对文件进行随机读写,并指出了feof函数可能出现的使用错误,在最后,介绍了文件缓冲区的概念;

PLKDTHFS的博客 1226

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

c语言中对字符和字符串的处理非常频繁,但c语言本身是没有字符串类型的,字符串通常存储在常量字符串或字符数组中。常量字符串适用于不需要对其进行修改的字符串函数。strlen函数的作用是获得一个字符串的长度;接收一个const修饰的字符指针变量,这个指针指向的字符串必须有结束符\0;返回一个无符号(易错点)的int类型的数值,表示这个字符串的长度(不包括结束符\0如果没有返回值,则表示出错;// 使用示例int main()printf("a的长度 = %d\n", strlen(a));

PLKDTHFS的博客 926

JAVA修炼秘籍第二章《逐渐魔化》

JAVA修炼秘籍第一章《痛苦的折磨》:https://blog.youkuaiyun.com/Coach_zhang/article/details/119217583?spm=1001.2014.3001.5501 第二章目录1 — 数据类型细节部分byte类型short,int,long类型float,double类型boolean类型final 常量关键字操作符介绍+,-,*,%,\符合运算符++,- -逻辑运算符位运算符移位运算 1 — 数据类型细节部分 上一章节中,我们说到数据类型的介绍,这一章节我们先把

Coach_zhang的博客 606

【c++修炼秘籍】C/C++内存管理

c/c++的内存管理相似但也不同,本文简要分析c/c++的内存管理方式,主要会使用new和delete进行c++的动态内存管理

m0_62824408的博客 322

重生之我在蓝星用C修仙直到成就仙王

不知道过去了多久蓝星的灵气早已枯竭,后来的人们开发出了新的修炼流派,用一行行的代码来进行修炼,我遇见了一个大成修士他说他叫鹏哥,我也顺利成为了他的弟子,当我第一次看到新的修炼秘籍时我便被上面的技巧深深吸引,无论是操作符还是数组亦或是算法与数据结构都让我深深着迷,太精妙了,我惊呼道,这秘籍里面的一切都那么深奥与精妙绝伦,我发誓要好好修炼直到我成为最厉害的那一个。从今天开始我将开始我的C语言修仙征途,诸位道友请不吝赐教。

soyoung246的博客 145

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。结构体:结构体类型的声明结构的自引用结构体变量的定义和初始化结构体内存对齐结构体传参结构体实现位段(位段的填充&可移植性)枚举枚举类型的定义枚举的优点枚举的使用联合联合类型的定义联合的特点联合的使用有了结构体类型,之后应该如何定义变量呢?

PLKDTHFS的博客 989
上一篇: c语言修炼秘籍【第三章】数组
下一篇: c语言修炼秘籍【第五章】指针
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值