【C语言入门必看】操作符

本章重点

  1. 各种操作符的介绍。
  2. 表达式求值

一、操作符

1.算术操作符

+   -   *   /   %(取余)

对于 / 除法操作符:

	int ret1 = 5 / 2;    //2
	float ret2 = 5 / 2;  //2.000000
	float ret3 = 5 / 2.0;//2.500000
	float ret4 = 5.0 / 2;//2.500000

在计算机中,不是存放在什么样的类型变量,5 / 2都是2。
若要得到正确的结果,可改成5 / 2.05.0 / 2,当然5.0 / 2.0也可以。

对于 % 取余操作符:
在这里插入图片描述
%表达式必须包含整型。

总结:

  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于 / 操作符若两个操作数都为整数,执行整数除法,只要有浮点数就执行浮点数除法。
  3. % 操作符的两个操作数必须为整数,返回的是整除之后的余数,且结果范围是[0,除数-1]。

2.移位操作

2.1.整数存储规则

在介绍移位操作之前,需要了解整数在内存中的存储规则。
整数在内存中的存储:以二进制补码的形式存储

正整数:原码、反码、补码相同

负整数:

  1. 原码:按负整数写出二进制序列
  2. 反码:符号位(左边第一位,1表示负,0表示正)不变,其他位按位取反
  3. 补码:反码+1
    (实例看下文)

2.2.左右移位规则

移位操作符操作对象是数的二进制。

<<   //左移操作符

>>	 //右移操作符
a.左移操作符

左边舍弃,右边补0

	int a = 5;
	int b = a << 1;

a << 1意为a的补码左移1位,此时a的值仍为5,类似b = a + 1;a的值不改变。
在这里插入图片描述

	int c = -1;
	int d = c << 1;

思路:写出-1的原码,反码,补码,对补码进行左移操作,将操作得到的补码按相同规则换算成原码,得到对应的整数-2。

10000000 00000000 00000000 00000001-1的原码 
11111111 11111111 11111111 11111110-1的反码
11111111 11111111 11111111 11111111-1的补码

11111111 11111111 11111111 11111110-1<<1的补码
11111111 11111111 11111111 11111101-1<<1的反码
10000000 00000000 00000000 00000010-1<<1的原码,即-2
b.右移操作符

右移规则分为两种:
①逻辑右移:左边补0,右边舍弃。
②算术右移:左边补符号位,右边舍弃。
绝大部分编译器采用算术右移

	int a = -1;
	int b = a >> 1;//仍是-1
10000000 00000000 00000000 00000001-1的原码
11111111 11111111 11111111 11111110-1的反码
11111111 11111111 11111111 11111111-1的补码
11111111 11111111 11111111 11111111-1>>1的补码,即-1

总结:

规律:左移,数据×2(变大);右移,数据÷2(变小)
左移右移操作数必须是整数。
移位操作数不可移动负数,如1 >> -1,标准未定义行为。

3.位操作符

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

注: 他们的操作数必须是整数。

运算规则: 操作的是整数的补码

按位与 & : 全1则1;有0则0
按位或 | : 有1则1;全0则0
按位异或 ^ :相同为0;相异为1

	int a = 3;
	int b = 5;
	int c = a & b;//c=1

//1.求a的补码
00000000 00000000 00000000 000000113的原反补码相同
//2.求b的补码
00000000 00000000 00000000 000001015的原反补码相同
//3.求c = a & b
00000000 00000000 00000000 00000001 一 c的补码
//4.转化为原码
00000000 00000000 00000000 00000001 一 c的原反补相同,即为1

计算步骤:

  1. 求两操作数的补码
  2. 根据按位与、或、异或的运算规则得到结果(补码)
  3. 将所得补码转化为原码,得到整数

练习一下:
不能创建临时变量(第三个变量),实现两个数的交换。

加减法:灵感:3 = 3 + 5 - 3 ;
缺点: 当a和b都无限接近int整型变量的范围,两者相加必然会超出范围,导致溢出。

int main()
{
	int a = 10;
	int b = 20;
	printf("a=%d b=%d\n", a, b);
	a = a + b;
	b = a - b;//(a+b)-b = a,即b = a
	a = a - b;//a - (a-b) = b,即a=b
	printf("a=%d b=%d\n", a, b);
	return 0;
}

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

按位异或法:运算规则知 :对于两个数的补码的每一位,相同为0;相异为1。
细品一下,若相同两数异或,结果则为0,(即a ^ a = 0);任何数和0异或,结果则为任何数本身,(即a ^ 0 = a)。
缺点: 可读性差,只支持正数。

int main()
{
	int a = 10;
	int b = 20;
	printf("a = %d b = %d\n", a, b);
	a = a ^ b;
	b = a ^ b;//(a ^ b) ^ b = a,即b = a
	a = a ^ b;//(a ^ b) ^ a = b,即a = b
	printf("a = %d b = %d\n", a, b);
	return 0;
}

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

1.若要a和b交换数值,方法:a ^ ba ^ b ^ b意为a的值赋给b;a ^ b ^ a意为b的值赋给a。
2.a ^ a = 0 a ^ 0 = a
3.(a ^ b) ^ b = a,可见异或支持交换律。

4.赋值操作符

赋值操作符 = :给变量赋值。

可对一个变量多次赋值,以最后一次为准:

	double salary = 10000.0;
	salary = 50000.0;//不满意,再赋值

可连续赋值:

	int a = 10;
	int x = 0;
	int y = 20;
	a = x = y+1;//连续赋值,这样的代码风格不推荐
	
相同的语义,下面的代码更爽朗且易于调试	
	x = y+1;
	a = x;

①y + 1的值赋给x ②x = y + 1的值赋给a

复合赋值操作符:

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

+= 来举例:其他运算符一样的道理,这样写更加简洁。

	x += 10;//复合赋值
	
其意义如下:
	int x = 10;
	x = x + 10;

5.单目操作符

单目:只有一个操作数

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

逻辑反操作:
0为假,非0为真,!0 = 1
取地址操作符 & 和解引用操作符 *

	int a = 10;
	int* p = &a;//int* p创建指针变量p(存放地址),&a:取变量a的地址//整体:取a地址放进p中
	*p = 20;//解引用访问其存储的地址中的内容
	printf("%d", a);//打印20
	return 0;

类型长度操作符 sizeof
sizeof :计算变量或类型所占内存空间的大小,与其变量中所存数据是什么无关。

	int arr[4] = { 0 };
	printf("%d\n", sizeof arr);//16
	printf("%d\n", sizeof (arr));//16

sizeof后面的()是表达式的括号,而不是函数调用的操作符。sizeof是操作符,后面的()可省略。

	int a = 2;
	short b = 0;
	printf("%d\n", sizeof(b = a + 1));//2
	printf("%d\n", b);//0

1.sizeof括号中有两个变量类型,打印左边变量类型的大小。解释:int 类型的a + 1赋值给short类型的b会发生整型截断,还是short类型的数据。
2.()中的表达式不参与运算,所以打印出的b显示仍为0。

sizeofstrlen()的区别:

  1. sizeof:计算变量或类型所占内存空间的大小,与其变量所存数据无关。
  2. strlen():计算字符串长度的函数,返回存放的数据中 \0 前的字符个数。

sizeof和数组:

#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;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?

(1)、(3)都是数组名放在sizeof()里,计算的是整个数组的大小,分别是40和10。
(2)、(4)都是数组名作函数参数,传过去的是数组首元素地址,也就是第一个元素的地址,表面上函数是用数组接收,实际上编译器会自动将数组降级优化为指针,用于接收地址,因此计算的是指针的大小,都为4。

++ -- 操作符:

	int a = 10;
	int x = a++;
	//后置++:先使用a,后对a自增,所以`x = 10,a = 11`
	int a = 10;
	int x = ++a;
	//前置++:先对a自增,后使用a,所以`a = 11,x = 11`

++ --懂得以上用法即可,不要弄无用且负责的形式,如下:

	int a = 0;
	int b = (++a) + (a++) + (a++);
	//不同的编译器运行结果不同,这样的代码纯属浪费时间

强制类型转换操作符 (type):把变量从一种类型转换为另一种数据类型。

   int sum = 17, count = 5;
   double mean;
   mean = (double) sum / count;//mean = 3.400000

这里要注意的是强制类型转换运算符的优先级大于除法,因此 int 类型的 sum 的值首先被转换为 double 型,然后除以int 类型的 count,得到一个类型为 double 的值。

6.关系操作符

>
>=
<
<=
!=   	用于测试"不相等"
==      用于测试"相等"

这些关系运算符比较简单,没什么可讲的。
警告: 敲代码时别千万不要把 == 写成 =

7.逻辑操作符

a&&b   逻辑与,即并且。a和b为真,整体为真
a||b   逻辑或,即或者。a和b至少有1个为真,整体为真
a&&b&&c&&d	从左向右所有表达式都为真(0),那整体就为真(1),否则为假(0)
a||b||c||d	从左向右所有表达式至少有一个为真(0),那么整体就为真(1),只有所有表达式都为假时整体才为假(0)

区分 逻辑与和按位与 区分 逻辑或和按位或

//操作对象:整数的二进制序列
1&2----->0 
1|2----->3 

//操作对象:整数
1||2---->1
1&&2---->1 

下面是一道有趣的题:

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++先使用a(a=0为假),后a + 1,到此这个逻辑与的表达式为假,后面的表达式将不执行,即++bd++不执行。①的结果为a=1,b=2,c=3,d=4
i = a++||++b||d++; a++先使用a(a=0为假),后a+1,继续执行, 到++b先b+1,后使用b(此时b=3,为真),到此这个逻辑或的表达式为真,后面的表达式将不执行,即d++不执行。②的结果为a=1,b=3,c=3,d=4

8.条件操作符

exp1 ? exp2 : exp3 

若表达式exp1为真,则执行表达式exp2;若表达式exp1为假,则执行表达式exp3

练习:写一个找出较大值的函数

int FindMax(int a, int b)
{
	return (a > b) ? a : b;
}

9.逗号表达式

exp1, exp2, exp3, …expN
从左向右依次进行计算每个表达式,整个表达式的结果为最后一个表达式计算的结果。

疑惑:最后一个表达式计算的结果是整个表达式的结果,那为什么还要计算前面的表达式呢?看代码:

int main()
{
	int a = 0;
	int b = 1;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("c = %d", c);//c = 12
	return 0;
}

从左往右依次计算:a>b为假,值为0;a = b + 10,此时a = 11;a=11;b = a + 1,此时b = 12,因此c = 12
若直接计算最后一个表达式,c = 1
实际上打印c = 12

逗号表达式可以和while循环结合,使语句更简洁

#include <stdio.h>
int main()
{
	int a = 0;
	while (a++, a < 10)
	{
		printf("hello world!\n");
	}
	return 0;
}

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

[]  ()  .   ->

下标引用操作符 [] :
操作数:一个数组名 + 一个索引值

	int arr[10];//创建数组
	arr[9] = 10;//使用下标引用操作符对其赋值。
	[ ]的两个操作数是arr和9

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

#include <stdio.h>
 void test1()
 {
 	printf("hehe\n");
 }
 void test2(const char *str)
 {
 	printf("%s\n", str);
 }
 int main()
 {
 	test1();            //使用()作为函数调用操作符。
 	test2("hello bit.");//使用()作为函数调用操作符。
 	return 0;
 }

结构成员操作符:访问一个结构的成员

.  结构体.成员名
-> 结构体指针->成员名
struct Stu
{
	char name[20];//名字
	int age;      //年龄
	char sex[5];  //性别
};
int main()
{
	struct Stu s = { "张三", 20, "男" };//创建一个学生s,并对其进行初始化赋值
	//.为结构成员访问操作符,能够访问结构体的成员
	printf("name = %s age = %d sex = %s \n", s.name, s.age, s.sex);
	//创建一个结构体指针,用来存放s的地址
	struct Stu* ps = &s;
	//->操作符可以通过指针来访问到结构体的具体成员
	printf("name = %s age = %d sex = %s \n", ps->name, ps->age, ps->sex);
}

使用结构体类型struct Stu创建了结构体变量s,s有三个成员:nameagesex
当我们想修改年龄时,可以s.age = 21;
但当我们想修改名字时,如s.name = "法外狂徒";这是错误的。

因为char name[20]; name是一个数组。s.name是数组名,即数组的首元素地址,不能对地址进行赋值。

我们可以试一试解引用访问其地址中的内容再修改:

	*(s.name) = "法外狂徒";

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

错误的原因:不能用赋值语句将一个字符串常量直接给一个字符数组赋值。

那应该如何修改结构体变量的数组成员呢?答案是:使用库函数strcpy()实现

	
	strcpy(s.name,"法外狂徒");

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

二、表达式求值

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

1.隐式类型转换

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

1.1.整型提升

整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。

	char b = 1, c = 2;
	...
	a = b + c;

b和c的值会被提升为普通整型,执行加法运算,所得结果将被截断,截断后放入a。

整型提升的意义:

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

如何进行整型提升?

整形提升是按照变量的数据类型的符号位来提升的。

有符号(正负)的整型提升:

  1. 写出变量的二进制补码
  2. 按最高位符号位进行填充
  3. 得到的补码再转换成原码

负数的整型提升:

char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111 11111111 11111111 11111111

正数的整型提升:

char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000 00000000 00000000 00000001

无符号的整型提升: 高位补0

举个例子体验一下:

int main()
{
	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d", c);
	return 0;
}

运行结果:在这里插入图片描述
程序分析:
①先把a、b当成是整型写出其补码

	00000000 00000000 00000000 00000011  -> int a
	c  -> int b
	因为a、b都是 char 类型,char 类型只有一个字节,会发生截断。
	00000011  -> char a
	01111111  -> char b 

**注:**截断——挑最小最低位的字节内容(最右边的8个bit)
②在进行运算之前,char 类型 小于 int 类型,会发生整型提升

	根据整型提升规则:高位补充符号位
	00000011  -> char a
	补0 -> 00000000 00000000 00000000 00000011 
	01111111  -> char b 
	补0 -> 00000000 00000000 00000000 01111111

③运算

00000000 00000000 00000000 00000011 
+
00000000 00000000 00000000 01111111
=
00000000 00000000 00000000 10000010  -> int c
	因为c是char 类型,会发生截断
10000010  -> char c

④以%d的形式打印c,需要整型提升

	10000010  -> char c
	根据整型提升规则:高位补充符号位
	11111111 11111111 11111111 10000010  -> c的补码
	11111111 11111111 11111111 10000001  -> c的反码
	10000000 00000000 00000000 01111110  -> c的原码,即-126

如图:
在这里插入图片描述
这就是打印出-126,而不是130的原因。

再看一个例子:

int main()
{
	char a = 0xb6; //10110110
	short b = 0xb600;//10110110 00000000
	int c = 0xb6000000;//10110110 00000000 00000000 00000000
	if (a == 0xb6)//a整型提升:11111111 11111111 11111111 10110110 补码
		printf("a");
	if (b == 0xb600)//b整型提升:11111111 11111111 10110110 00000000 补码
		printf("b");
	if (c == 0xb6000000)//c本就是整型,不用提升
		printf("c");
	return 0;
}
运行结果:c

因为有运算( == )且a,b类型小于 int 类型,所以a,b要进行整形提升。
a,b整形提升之后,变成了负数,所以表达式(a == 0xb6) , (b == 0xb600)
结果是假,但是c不发生整形提升,则表达式(c == 0xb6000000)的结果是真。

注意:只要有运算且类型小于 int 或unsigned int 类型,就会整型提升。

int main()
{
 	char c = 1;
	printf("%u\n", sizeof(c));
 	printf("%u\n", sizeof(+c));
 	printf("%u\n", sizeof(!c));
 	return 0;
}

sizeof(c),没有运算,不发生整型提升,则是一个字节。
sizeof(+c)sizeof(-c)+c-c也是运算的一种,会发生整型提升,则是4的字节。

1.2.算术转换

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

	long double			高精度
	double
	float
	unsigned long int
	long int
	unsigned int
	int					低精度

例:

	int a = 4;
	float f = 3.5f;
	float r = a + f;
	printf("%f\n", r);

计算 r = a + f 时,需要将 a( int型 ) 转换成 float

但算数转换要合理,否则会造成精度丢失。如:

	float f = 3.14;
	int num = f;
	高精度类型向低精度类型转换,会造成精度丢失。

2.操作符的属性

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

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

问:两个相邻操作符先执行哪个?

取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
即优先级 > 结合性

下面是关于操作符属性的一份表:
在这里插入图片描述

操作符具有优先级和结合性,容易写出一些有问题的表达式。

例1:

	a*b + c*d + e*f;

优先级只能决定两个相邻操作符,在计算时,* 的优先级比 + 高,只能保证 * 的计算比 + 早,但不能确保第三个 * 比第一个 + 早。

上述表达式的计算顺序可能有:
第一种:
在这里插入图片描述
第二种:
在这里插入图片描述
当一个变量(如a)多次出现在表达式中,会可能因计算顺序而导致错误。

例2:

	c + --c;

操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们无法知道最左边的c是自减后的c还是没自减前的c,因此有两种可能:
在这里插入图片描述
例3:

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

在不同编译器有着不同的结果:

编译器
-128Tandy 6000 Xenix 3.2
-95Think C 5.02(Macintosh)
36Dec VAX/VMS
42Microsoft C 5.1

例4:

int fun()
{
     static int count = 1;
     return ++count;
}
int main()
{
     int answer;
     answer = fun() - fun() * fun();
     printf( "%d\n", answer);//输出多少?
     return 0;
}

这个代码有没有实际的问题?
有问题!

虽然在大多数的编译器上求得结果都是相同的。 但是上述代码 answer = fun() - fun() * fun();
中我们只能通过操作符的优先级得知:先算乘法,再算减法。
而函数的调用先后顺序无法通过操作符的优先级确定。

例5:

#include <stdio.h>
int main()
{
	 int i = 1;
	 int ret = (++i) + (++i) + (++i);
	 printf("%d\n", ret);
	 printf("%d\n", i);
	 return 0;
}
//linux 环境gcc编译器:10 4
//VS2013环境:12 4

这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 +和第三个前置 ++ 的先后顺序。

以上这些代码在不同的编译器下运行的结果都是不同的,因为不同的编译器其运算和函数调用的顺序都是不同的。

总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

解决方法: 将复杂的表达式拆开来写(按照正常的逻辑)。
如例1:

	a*b + c*d + e*f;

改写:

	int num1 = a*b;
	int num2 = c*d;
	int num3 = e*f;
	int sum = num1 + num2 + num3;
  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大猩猩!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值