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

使用Python爬虫抓取政府网站政务信息:公告、通知的自动化抓取与分析 通过结合Requests、BeautifulSoup、Selenium等技术,你能够高效地抓取政府网站的数据,并通过MySQL存储与Pandas分析这些数据。通过自动化手段抓取这些信息,不仅可以提高数据收集的效率,还能够实时获取最新的政务动态,为政策研究、舆情分析等提供宝贵数据。我们将通过Python的Requests、BeautifulSoup、Selenium等工具,结合最新的技术实现一个完整的爬虫系统,帮助你轻松抓取并分析政务信息。通过爬虫,我们可以自动化地从指定网站上抓取公告、新闻、通知等信息。 阅读详情

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的使用以及使用时可能遇到的问题,逻辑操作符运算过程中可能出现的反直觉问题,表达式求值中整型提升、算术转换、操作符的优先级,并列出、分析了一些问题表达式。

btstack ble ota开发笔记(2)-mcuboot移植 紧接上一篇文章,要实现一个完整的ota demo,除了实现ota image的传输通道外,还需要“bootloader”这个角色来帮我们实现ota image的校验、安全、引导以及启动,这里我选择开源的组件mcuboot,下面开始介绍mcuboot在stm32f4-discovery上的移植。首先估算下bootloader & application image的大小,确定好flash的分布情况,因为后续会涉及到flash的操作(读、写、擦除),为了方便程序的编写,各个image area在flash中的起 阅读详情

相关推荐

【VAE学习笔记】全面通透地理解VAE(Variational Auto Encoder)

完整笔记:http://www.gwylab.com/note-vae.html 李宏毅老师的教程视频:https://www.bilibili.com/video/av15889450/?p=33 —————————————————————————————————   依据李宏毅老师的讲解,我整理了一番VAE的笔记。先简单介绍一下VAE,VAE作为一个生成模型,其基本思路是很容易理解的:把一堆真...

a312863063的博客 14万+

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

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

2301_80029060的博客 1532

流形优化: Manifold Optimization 的 全网最通俗版本详解 (一)

前言 之前忙于论文, 停更了很久。 接下来的一段时间,出于各种原因, 决定在优快云平台上做 最近在 混合波束成形 及 智能反射面 领域 非常常见的流形优化 (Manifold Optimization) 算法 的专题介绍。尽管目前已有了不少的中文博客, 但出于其各种弊端, 使得本篇的成文仍有其不可或缺的意义。 而如果要盘点最核心的意义, 就在于这篇是对读者的 数学能力 与 阅读理解能力 要求最低的 流形优化讲解了。 唯一参考的文献为 Manopt 工具包作者 Boumal 的 新书: An introd

B417科研笔记 2万+

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

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

PLKDTHFS的博客 1664

DSH-Work:一个让DeepSeek下载即用的Harness客户端

使用大模型工具时,环境配置往往比模型本身更让人头疼。DeepSeek Harness作为一类封装层,将模型调用、上下文管理、参数控制等复杂细节统一收拢,对外提供简洁操作入口。开源的DSH-Work正是这样一款Harness客户端,它通过自带运行时与保守默认配置,实现了下载即用、免配环境的目标,让使用者无需掌握Python、CUDA等专业知识即可快速连接DeepSeek云端或本地服务。对于非技术背景的用户,它降低了AI工具的使用门槛;对于开发者,它把时间从环境排错中解放出来,更专注于提示词调优与工作流设计。无

weixin_29048309的博客 343

人工智能面试总结-分类与聚类

目录说说常用的分类算法,以及各自的优缺点?说说为什么正确率有时不能有效评估分类算法?说说什么样的分类器最好?说说聚类算法有哪些衡量标准?(选看)各聚类算法性能比较汇总说说什么是聚类,你知道哪些聚类算法?具体展开介绍你知道的聚类算法?说说K-Means聚类算法流程?说说K-Means聚类算法如何调优?说说K-Means聚类算法如何选择初始点?说说K-Means聚类聚的是特征还是样本?说说K-Means聚类的特征距离是如何计算的?说说聚类与分类的区别?手撕K-Means聚类 说说常用的分类算法,以及各自的优缺点

啥都生的博客 3862

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

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

PLKDTHFS的博客 1763

大厂面经:大疆嵌入式面试题及参考答案(4万字长文:持续更新)

目录 Linux 系统调用的过程,中间发生了什么? 表格总结 Linux 中断流程,谈谈你对中断上下文的理解 中断流程 中断上下文理解 Linuxschedule()函数的原理和调用的时机 schedule()函数原理 调用时机 页表实现机制,分页的缺点? 页表机制 分页的缺点 介绍操作系统的多级反馈调度策略,时间片轮转,在项目中如何指定优先级来调度进程完成快速响应(nice命令) 多级反馈队列调度 时间片轮转 使用nice命令 聊内存分配,进程内存分配,段页式存

大模型大数据攻城狮的专栏 4169

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

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

PLKDTHFS的博客 1443

“华为杯”研究生数学建模竞赛2017年-【华为杯】B题:面向下一代光通信的 VCSEL 激光器仿真模型

得出以下主要结论:在相同的偏置电流条件下,随着温度的升高,带宽的峰值向。在相同的温度下,随着电流的增大,带宽的峰值越小且曲线由波峰形状。方案,包括可能的数学公式,不同温度和偏置电流下的带宽响应曲线,并与问题。线,得出以下主要结论:在相同的偏置电流条件下,随着温度的升高,带宽的峰。在相同的温度下,随着电流的增大,带宽的峰值越小且曲线由波峰形状逐渐变得。得的激光器特性曲线和模型中仿真得到的特性曲线,之后,可以将两组曲线的差。说明的是,由于数值优化算法的成功与否很大程度上依赖于初始参数的猜测,参。

getusushu的博客 97

【系统分析师】高分论文:论基于构件的软件开发(气象灾害影响评估系统)

本文以某手机气象灾害影响评估系统为例,探讨了基于构件技术的软件开发实践。通过分析开发中常见的功能模块重复问题,提出建立企业构件库、针对性选择构件和加强构件管理的解决方案。项目实施中成功复用登录模块等通用构件,并针对气象数据转换需求开发新构件,最终在8个月内高质量完成系统建设。文章总结了构件开发的三个关键点:构件库建设是基础、构件选择需精准、构件管理要规范。同时指出需注意全新需求开发、性能优化等问题,建议通过培训提升团队对构件的理解力。该案例证明构件技术能有效缩短周期、降低成本,为企业积累可复用的技术资产。

数据知道的博客 4253

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

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

PLKDTHFS的博客 761

RuoYi-Cloud-Plus使用minio进行文件上传图片后无法预览解决_修改minio配置minio桶权限---SpringCloud工作笔记198

在文件管理的位置,发现刚刚上传的图片文件,会显示 预览图片失败 后来经过多方查看,发现是minio的配置的问题。可以看到首先登录RuoYi-Cloud-Plus系统然后,打开文件管理页面可以看到,当上传了图片文件以后。登录以后,选中左侧Buckets,可以看到有自己用的桶,找到以后点击打开。显示文件展示中,文件预览失败,那么这个时候,去修改minio的配置。可以看到,点击配置管理,这里我们启用的是minio对吧。默认是没有配置的这个时候,是允许下载,但不允许预览的.

添柴程序猿的专栏 5566

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

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

PLKDTHFS的博客 1257

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

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

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

PLKDTHFS的博客 1244

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

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

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

PLKDTHFS的博客 674

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

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、付费专栏及课程。

余额充值