C语言初阶——6操作符


前言

作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言操作符),是为了与大家分享自己学习经验和所遇到的困难,同大家一起进步。


日志

不记得首发了
2024.5.12首发

1.操作符分类

  1. 算术操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 单目操作符
  6. 关系操作符
  7. 逻辑操作符
  8. 条件操作符
  9. 逗号表达式
  10. 下标引用、函数调用和结构成员

2.算术操作符

+ - * / %
前三者和数学上的一样,就不赘述了。

2.1 C语言的两种除法

  1. 整数除法(除号两边都是整数)
    这种除法的结果为商,小数部分丢弃
    在这里插入图片描述
  2. 浮点除(除法两端至少有一个浮点数)
    这种除法完全是数学上的除法,小数部分不会舍弃
    在这里插入图片描述
  3. 除法中,除数不可以是0
    在这里插入图片描述

2.2%取模(取余)

  1. %取模(取余),得到的是整除后的余数
    在这里插入图片描述

  2. 不能对0取模
    在这里插入图片描述

  3. %的范围
    对于%来说,取的是余数
    在这里插入图片描述
    即%n的范围是在[0, n-1]

  4. %的2个操作数必须是整数
    在这里插入图片描述

3.移位操作符

<<和>>移动的都是二进制位,而且位操作符的操作数只能是整数

3.1数据在计算机中以二进制存储

计算机是一个通电的硬件,只能产生高低电平的电信号。电信号正负转化为1/0的数字信号。所有的数据都在存储的时候,都会转化成二进制。也就是说其实,计算机能处理的只是二进制的信息
在这里插入图片描述

因此我们看起来的十进制,在计算机中实际上是用二进制来储存和运算的

3.2原码、反码、补码

3.2.1整数的三码存储

正负整数和0则则是通过原码、反码、补码在计算机中表示、存储、运算

  1. 正整数和0的原码、反码、补码是相同的
  2. 负整数的原码、反码、补码需要计算

3.2.2 整数的二进制表达形式有三种(整数才有)

  1. 原码:直接通过二进制形式写出来的就是原码
    在这里插入图片描述
    比如说int类型的整数3,它的大小有32个二进制位。而正整数和0,三码都是一样的。

  2. 反码:对原码符号位不变,其他为按位取反
    在这里插入图片描述
    在这里插入图片描述

  3. 补码:反码+1得到补码。
    在计算机中整数是以补码进行存储和运算的
    在这里插入图片描述

3.2>>右移操作符

在整数的二进制位上向右移动

  1. >>操作符移动的具体过程
    在这里插入图片描述

  2. C语言的>>有两种,本身没有规定。但大多编译器选择算术右移
    算术右移:右边丢弃,左边补原来的符号位
    通过上面,也可以看出来在我的VS2022也是算术右移
    逻辑右移:右边丢弃,左边直接补0
    如果刚刚的-3>>1是逻辑右移,结果会非常大,不可能是-2
    在这里插入图片描述

3.3<<左移操作符

<<左移值有一种规则:左边丢弃,右边补0
在这里插入图片描述

3.4<<和>>的范围

  1. <<和>>可移动有效范围内的2进制,来到你想要的的位置上
    int a = 1;
    int类型就可以移动的范围是0~32,可以把二进制序列的某一位通过移位操作符来移动到你想要的位置上

  2. 不要移动负数位
    这是标准未定义行为,C语言也不支持这种写法。可能能算,但怎么算取决于编译器。
    比如说能不能<<-1来实现右移1呢?
    在这里插入图片描述
    能算但是会报警告,而且具体怎么算,谁也不知道。所以为了良好的代码风格,想左移就<<,右移就>>,不要乱来。

4.位操作符

也是在二进制位上进行操作的,位操作符有

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

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

4.1&按位与

有0则0,全1则1

#include <stdio.h>
int main()
{
	int a = 3;
	//0011 - 三码
	int b = 5;
	//0101 - 三码
	int c = a & b;
	//0001 - 三码
	printf("%d\n", c);//打印1
	return 0;
}

4.2|按位或

有1则1,全0则0

	int a = 3;
	//0011 - 三码
	int b = 5;
	//0101 - 三码
	int c = a | b;
	//0111 - 三码
	printf("%d\n", c);//打印7

4.3^按位异或

相同为0,相异为1

	int a = 3;
	//0011 - 三码
	int b = 5;
	//0101 - 三码
	int c = a ^ b;
	//0110 - 三码
	printf("%d\n", c);//打印6

4.4交换两个整数,不能创建临时变量(变量)

  1. 我们一般写的交换两个变量是通过创建tmp来进行的
	int a = 3;
	int b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	//交换
	int tmp = a;
	a = b;
	b = tmp;
	printf("交换后:a=%d b=%d\n", a, b);

但是人家说不能创建临时变量

  1. 利用ab的和来交换
	int a = 3;
	int b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	//交换
	a = a + b;
	b = a - b;
	a = a - b;
	printf("交换后:a=%d b=%d\n", a, b);

在这里插入图片描述
但是这种方法有一个缺陷,那就是当a特别大但是没有超过int范围,b也特别大,也没有超过int范围。a+b就会超过int范围,超过的部分存不下,被丢失了。因此会出先截断问题,精度丢失

  1. 终极解法
	int a = 3;
	int b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	//交换
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
	printf("交换后:a=%d b=%d\n", a, b);

在这里插入图片描述

首先要明白^操作符的一些定理
在这里插入图片描述

^支持交换律
在这里插入图片描述
现在在回来看这道题
在这里插入图片描述
这个方法是这道题的终极解,它只是在二进制进行^,不会产生进入操作,因此不会发生溢出。

  1. 各自优缺点
    虽然说第三种方法是终极解,但是还是建议使用第一种方法

第一种:可读性高,效率高

第二种:可读性不高,还存在溢出风险

第三种:不会溢出,但可读性不高

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

  1. 我们做题首先就要有这个思路
    在这里插入图片描述

  2. 我们可以这样获取一个整数的32个bit位。对1个数&1,就能得到这个二进制最低位的数
    在这里插入图片描述

  3. 当把一位通过>>,使得它来到最后一位。再&1,得到1说明该二进制位是1,得到0说明该位是0。就可以用得到的结果进行判断,来让计数器++。如此>>1或得一位,&1,判断,>>1…这样的过程进行32次,就能解决这道题了

  4. 前面说可以通过&1的结果来判断该位是1还是0之外,还有一种判断方式。因为在二进制表达的形式下,如果最低位是1,那么它一定是奇数。所以是奇数说明最低位是1,计数器++也可以。

  5. 版本1

int main()
{
	int count = 0;
	int n = 0;
	scanf("%d", &n);
	int i = 0;
	for (i = 0; i < 32; i++)
	{
		//if ((n >> i) % 2 != 1)
		if (((n >> i) & 1) == 1)
		{
			count++;
		}
	}
	printf("count=%d\n", count);
	return 0;
}

这种方法的缺陷是,不论什么数据都会计算32次。
版本2

int main()
{
	int n = 0;
	scanf("%d", &n);
	int count = 0;
	while (n)
	{
		if (n % 2 == 1)
			count++;
		n = n / 2;
	}
	return count;
}

这个版本优点是当数字很小的时候,不用循环32次。缺点是进行了大量的%,%运算的效率本身就低。而且如果用a/2来达到获得最低位的效果的话,因为除法的效率也很低,那就更低了。

  1. 版本3
    采用相邻两个数进行按位与运算。每次&运算都会是n不断减少
    在这里插入图片描述
    最后,n的二进制位有多少个1,就进行多少次循环。而且采用的是&运算,效率提高
int NumberOf1(int n)
{
	int count = 0;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
	return count;
}

5.赋值操作符

=、+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^=
后面都是复合赋值符,没什么好讲的。主要讲=

  1. 最好不要连续赋值
int main()
{
	int a = 0;
	int x = 2;
	int y = 1;
	//a = x = y + 1;
	//可读性不高(监视也看不到这一步的过程),容易出问题,不建议使用
	//a = x = y + 1;完全等价下面的
	x = y + 1;
	a = x;
	//写法明确且易于调试
	return 0;
}
  1. =和==不要搞混
    在这里插入图片描述
    n等于3,应该不打印的。可是缺因为少写一个=,使得变成了赋值。导致5赋值给n,5为真,所以进入if。
    因此当我们在比较一个数字和一个变量时候,可以尝试把数值写在左边。
    在这里插入图片描述
    这样编译器会逼着你去改。

6.单目操作符

6.1单目操作符分类

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

-、+不讲了

6.2!逻辑反操作

对真假进行转换
在这里插入图片描述
看一下它的值
在这里插入图片描述
在C语言中,&&、||、!这些操作符只关注真假。默认是,0为假,非0为真。计算出表达式的结果为真时,则默认用1表示。

6.3&、sizeof

&取地址操作符:把地址取出来
在这里插入图片描述
sizeof操作符,计算变量或类型的大小,单位字节。且sizeof返回的是无符号的整型
在这里插入图片描述
sizeof是计算变量或类型大小的,上面a的类型显然是int,所以计算变量a的大小实际上也就是计算int类型的大小。那么数组的类型是什么?实际上数组的类型就是创建数组的格式去掉数组名。

在这里插入图片描述

6.4sizeof与数组

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

需要清楚的是数组名通常就是数组首元素地址,但有两个例外。
sizeof(数组名)计算的是整个数组的大小
&(数组名)取出的是整个数组的的地址
在这里插入图片描述
因为传的是首元素地址,地址又是指针。所以当进入函数后,再使用sizeof计算,计算的就是一个指针或者说地址的大小。指针的大小只与平台有关,32/64平台上大小是4/8。
当数组作为实参传进函数里头时,传的实际上是首元素地址,所以此时在函数内部不可能求得数组元素个数(行和列)。所以函数内部要使用到sz、row、col这些数据时候,必须在外面就准备好,从而作为实参传入函数。

6.5++和–

分为前置,和后置。
在这里插入图片描述

–同理不赘述了。

6.6sizeof和strlen

  1. sizeof是操作符,strlen是函数
  2. sizeof是计算变量和类型所占有的大小,单位字节,不关注里面的内容
  3. strlen是求字符串长的,只能针对字符串。计算的是字符串中’\0’出现的字符个数
    在这里插入图片描述
    像ch1这样创建的字符数组,里面会多一个’\0’作为字符串内容,所以sizeof计算它的长度时,会多一个字节。而正是因为ch2是一个个字符放进去的,所以没有’\0’,所以什么时候遇到’\0’是随机的,因此打印随机值。

6.7*和.操作符

  1. *解引用操作符
    *用在指针中。对指针变量解引用,找到指针所指向的空间
    在这里插入图片描述

  2. .和->
    二者用在访问结构体成员中

struct book
{
	char name[30];//成员(书名)
	char author[20];//成员(作者)
	double price;//成员(价格)
};
void test(struct book* p)
{
	printf("%s %s %.1lf\n", (*p).name, (*p).author, (*p).price);//结构体.成员
	printf("%s %s %.1lf\n", p->name, p->author, p->price);//结构体指针->成员
}
int main()
{
	struct book b1 = { "活着", "余华", 66.6 };
	struct book b2 = { "高质量的C/C++编程", "林锐", 50.0 };
	printf("%s %s %.1lf\n", b1.name, b1.author, b1.price);
	printf("%s %s %.1lf\n", b2.name, b2.author, b2.price);
	printf("\n");
	test(&b1);
	test(&b2);
	return 0;
}

在这里插入图片描述
结构体.成员
结构体->指针

6.8~按位取反

  1. ~对一个数的二进制位按位取反
    在这里插入图片描述
  2. 有了逻辑操作符之后,就丰富了我们多组输入的写法
	int n = 0;
	//scanf读取失败返回EOF(-1)
	//while (scanf("%d", &n) == 1);
	//while (scanf("%d", &n) != EOF);
	while (~scanf("%d", &n));
	//-1
	//10000000000000000000000000000001 - 原码
	//11111111111111111111111111111110 - 反码
	//11111111111111111111111111111111 - 补码
	//~-1
	//00000000000000000000000000000000 - 原码
	//结果为0,说明读取失败,while的判断0假,不执行

6.9()强制类型转换

把一个类型强制转换为其他类型
在这里插入图片描述
对于浮点数来说,强制转换为整数,会丢掉小数点后的数据。强制转换少用,容易出错

7.关系操作符

=
<
<=
!= 用于测试“不相等”
== 用于测试“相等

注意:小心=和==写错,导致错误

8.逻辑操作符

看的是真假。而且计算结果为真用1表示,为假用0表示。

8.1&&逻辑与

一假则假,全真则真。数学上并且的意思
在这里插入图片描述

8.2||逻辑或

一真则真,全假则假。数学上或者的意思
在这里插入图片描述

8.3&&和||会控制表达式求值顺序

8.3.1&&左假,右边不算

因为&&是两个都为真,结果才为真。只要有一个假,那整个表达式结果为0假。因此只要左边为0了,右边就不会就算。
在这里插入图片描述

8.3.2||左真,右边不算

对于||来说,一真则真,全假真假。所以只要左边为真,右边是没有机会计算的。
在这里插入图片描述

8.3.3陷阱

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

来猜猜看,这段代码的结果是什么?
在这里插入图片描述
再来一个||的
在这里插入图片描述

9.条件操作符

exp1 ? exp2 : exp3;条件表达式,也叫三目操作符
在这里插入图片描述
3不大于5所以不执行++a,执行++b。所以a还是3,++b,b变成6

10.逗号表达式

  1. exp1, exp2, exp3, …, expn
    从左向右依次计算,整个条件表达式的结果是expn。前往不要以为逗号表达式的结果是expn,前面的就不算了。因为前面的表达式,可能会影响expn的值。
    在这里插入图片描述
    假设这里你以为逗号表达式只需要算最后一个的话,就会漏掉a = b + 10;就会影响后面b的结果。所以一定要从左向右计算
  2. 逗号表达式的应用
    在这里插入图片描述
    两种代码是等价的

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

11.1[]下标引用

下标引用操作符的操作数是:数组名[索引的下标]

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//创建数组的[]不是下标引用,只是个形式
	arr[0] = 100;//通过[]用下标找到第一个元素
	//[]的操作数:arr和0
	printf("%d\n", arr[0]);//打印100
	

11.2()函数调用

  1. ()的操作数:函数名和参数
//()的操作数:函数名和参数
	int len = strlen("abc");//操作数:strlen和"abc"
	printf("%d\n", len);//操作数:printf和len

因此()的操作数最少是1个,即函数无参,只有函数名作为参数的时候。

  1. 可变参数列表
    像printf这种在手册里有(…)的,这个东西叫做可变参数列表
    在这里插入图片描述
    他的参数是可以变化的,不固定。

11.3.结构体成员

结构体.成员

struct book
{
	char name[30];//成员(书名)
	char author[20];//成员(作者)
	double price;//成员(价格)
};
int main()
{
	struct book b1 = { "活着", "余华", 66.6 };
	printf("%s %s %.1lf\n", b1.name, b1.author, b1.price);
	return 0;
}

给我们的感觉就像是在访问一个叫b1的文件,(.)能打开这个文件,访问里面的内容。

12.表达式求值

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

12.1隐式类型转换

C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前会被转换为普通整型,这种转换称为整型提升。

  1. 整型提升的意义

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。

是不是很懵逼?我学的时候也这么觉得,大概可以这么理解。
在这里插入图片描述
在这里插入图片描述

  1. 整型提升的具体过程
    整型提升是按照变量的数据类型的符号位来提升的
int main()
{
	//5是一个整数int,char的c1会放不下,会发生截断。127也是一样
	char c1 = 5;
	//00000000000000000000000000000101 - 5的原码
	//00000101 - 截断   保留的是最低位的那8位,保留最高位没有意义
	char c2 = 127;
	//00000000000000000000000001111111 - 127的原码
	//01111111 - 截断
	char c3 = c1 + c2;
	//5和127,发生了整型运算,而c1和c2不是整型,所以要发生整型提升
	//00000000000000000000000000000101 - 5
	//00000000000000000000000001111111 - 127
	//00000000000000000000000010000100 - 5+127
	//c3放不下,所以又要发生截断
	//10000100 - 补码 最终c3放的值
	//在以%d的形式打印的时候要发生整型提升
	//%d - 以10进制的形式打印有符号的整数
	//c3此时是char类型,人家要打印的是整型,所以c3就要发生整型提升才能打印
	//整型提升是按照变量的数据类型的符号位来提升的,所以这里提升1
	//11111111111111111111111110000100 - 补码
	//11111111111111111111111110000011 - 反码
	//10000000000000000000000001111100 - 原码
	printf("%d\n", c3);//-124
	return 0;
}

而整型提升这个过程是悄悄发生的,我们看不见,所以叫做隐式类型转换。

  1. 整型提升真的存在吗?
    这种0x形式的数字,都是无符号。无符号数直接用0来提升
    在这里插入图片描述
    虽然它们的二进制序列一样,但发生整型提升时,a高位补1,0xb600高位补0。所以最后它们肯定不等,下面b也是如此。
    因为c本来就是整型,不需要发生整型提升,所以值打印了c
//%u - 以16进制的形式打印无符号的整数
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));//打印1
	printf("%u\n", sizeof(+c));//打印4
	printf("%u\n", sizeof(-c));//打印4
	return 0;
}

本来sizeof计算char类型的c,就是1。当给上±号之后,发生了整型算术运算。而c又是char类型,所以发生了整型提升。

12.2算术转换

  1. 如果某个操作符的操作数不属于相同的类型,那么除非将其中一个操作数的型转换为另一个操作数的类型,否则表达式求值操作就无法进行。因此在计算的时候会发生算术转换。下面的层次体系称为寻常算术转换
    在这里插入图片描述

比如说有float类型和int类型发生运算,那么int类型就要转换为float类型

float f = 3.14f;
int n = 10;
f + n;//n必须转换为float类型,才能与f相加

注意:转换都是临时的,参与运算的瞬间提升,n本身回来之后是不变的。包括上面的整型提升也是如此。

12.3操作符的属性

复杂表示求值顺序有三个影响因素

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

两个相邻操作符先执行哪个?取决于他们的优先级,如果优先级相同,取决于它们的结合性。最后是否控制求值顺序也会影响表达式求值的计算路径。

操作符描述用法示例结果类型结合性是否控制求值顺序
()聚组(表达式)与表达同N/A
()函数调用rexp (rexp, …,rexp)rexpL-R
[ ]下标引用rexp[rexp]lexpL-R
.访问结构成员lexp.member_namerexpL-R
->访问结构指针成员rexp->member_namlexpL-R
++后缀自增lexp ++rexpL-R
后缀自减lexp –rexpL-R
!逻辑反! rexprexpR-L
~按位取反~ rexprexpR-L
+单目,表示正值+ rexprexpR-L
-单目,表示负值- rexprexpR-L

12.3.1操作符的优先级

  1. 相邻操作符优先级高的先算,低的后算

在这里插入图片描述
一看就知道先算哪个

  1. 相邻操作符的优先级相同的情况下,结合性起作用
    在这里插入图片描述

  2. 是否控制求值顺序
    像&&、||、条件表达式、逗号表达式等都会控制求值顺序
    &&左假,右边不算了
    ||左真,有边不算了
    条件表达式,表达式1为真,表达式2计算,表示式3不算。表示1为假,表示2不计算,表达式3计算
    ,逗号表达式,从左向右依次计算,只有最后一个表达式的结果才是整个表达式的结果
    所以像这种能控制求值顺序的操作符在一定程度上,也会影响表达式求值。

12.4无法确定唯一计算路径

  1. 就算影响控制顺序的三个条件都知道了,表达式求值顺序也可能不唯一
    在这里插入图片描述
    这种代码是有问题的。给上括号,能很好避免这种问题。
  2. 表达式求值顺序唯一,结果依旧可能不一样
    在这里插入图片描述
    所以就算你知道了求值顺序,但仍然因为操作符左边的值什么时候准备好,也有可以会发生不一样的结果。这种也是问题代码。
    因此我们写代码的时候,怕写出这种代码就可以适当加上括号或者写得足够简单。
  3. 通过上面,我们知道即使你完全了解操作符的优先级和结合性等等,也依旧可能写出问题代码。
    比如C和指针作者对下面代码做了一个测试
int main()
{
 int i = 10;
 i = i-- - --i * ( i = -3 ) * i++ + ++i;
 printf("i = %d\n", i);
 return 0;
}

结果他在各个编译器上测完的结果各不相同。所以说这种代码是有问题的,可能他的语法没有错看,但是无法确定唯一的就算路径,是垃圾代码。不要写这种代码。

  1. 再来一个问题代码
int fun()
{
	static int count = 1;
	return ++count;
}
int main()
{
	int ret;
	ret = fun() - fun() * fun();
	printf("%d\n", ret);//输出多少?
	return 0;
}

在这里插入图片描述
通过这段代码ret = fun() - fun() * fun();我们可以知道先算*,再算-。但是这些函数什么时候调用?因为函数什么时候调用,也会导致没有唯一的计算路径。
所以真的真的不要这样写,或者适当加括号。

13.总结

还没想好

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值