操作符的学习

原码反码补码

  1. 原码:最高位表示符号位,其他位存放该数的二进制的绝对值。

    例如:+7的原码是:00000000 00000000 00000000 00000111

    ​ -7的原码是:10000000 00000000 00000000 00000111

  2. 反码:正数的反码等于原码;负数的反码就是它的原码除符号位外,按位取反。

    例如:+7的反码是:00000000 00000000 00000000 00000111

    ​ -7的反码是:11111111 11111111 11111111 11111000

  3. 补码:正数的补码等于它的原码;负数的补码等于反码+1。

    例如:+7的补码是:00000000 00000000 00000000 00000111

    ​ -7的补码是:11111111 11111111 11111111 11111001

总结:

  1. 0为正,1为负
  2. 正数的原码、反码、补码相同
  3. 计算机中均采用补码进行加减运算

1. 操作符分类:

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号表达式

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

2. 算术操作符

假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符描述实例
+把两个操作数相加A + B 将得到 30
-从第一个操作数中减去第二个操作数A - B 将得到 -10
*把两个操作数相乘A * B 将得到 200
/分子除以分母B / A 将得到 2
%取模运算符,整除后的余数B % A 将得到 0
++自增运算符,整数值增加 1A++ 将得到 11
自减运算符,整数值减少 1A-- 将得到 9
  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
int main()
{
	int a = 21;
	int b = 10;
	int c;

	c = a + b;
	printf("a + b的值是 %d\n", c);
	c = a - b;
	printf("a - b的值是 %d\n", c);
	c = a * b;
	printf("a * b的值是 %d\n", c);
	c = a / b;
	printf("a / b的值是 %d\n", c);
	c = a % b;
	printf("a 模 b的值是 %d\n", c);
	c = a++;  // 赋值后再加 1 ,c 为 21,a 为 22
	printf("c = a++的值是%d,a++后a的值是 %d\n", c, a);
	c = a--;  // 赋值后再减 1 ,c 为 22 ,a 为 21
	printf("c = a--的值是%d,a--后a的值是 %d\n", c, a);

	return 0;
}

3. 移位操作符

假设变量 A 的值为 60,变量 B 的值为 13,则:

运算符描述实例
<<二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。A << 2 将得到 240,即为 1111 0000
>>二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。A >> 2 将得到 15,即为 0000 1111

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

3.1 左移操作符

移位规则:

左边抛弃、右边补0

int main()
{
	int a = 7;
	int b = a << 1;

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

	printf(" 7<<1 = %d\n", b);
	printf("-7<<1 = %d\n", d);

	return 0;
}

在这里插入图片描述

3.2 右移操作符

移位规则:

首先右移运算分两种:

  1. 逻辑移位:

左边用0填充,右边丢弃

  1. 算术移位:

左边用原该值的符号位填充,右边丢弃

int main()
{
	int a = 7;
	int b = a >> 1;

	int c = -7;
	int d = c >> 1;

	printf(" 7<<1 = %d\n", b);
	printf("-7<<1 = %d\n", d);

	return 0;
}

在这里插入图片描述

注:

  1. VS2019编译器采用算术移位。

  2. 对于移位运算符,不要移动负数位,这个是标准未定义的。

    int num = 10;
    num>>-1;	//error
    

4. 位操作符

位操作符有:

& 按位与

| 按位或

^ 按位异或

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

pqp & qp | qp ^ q
00000
01011
11110
10011
运算符描述实例
&按位与操作,按二进制位进行"与"运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;(A & B) 将得到 12,即为 0000 1100
|按位或运算符,按二进制位进行"或"运算。运算规则:`00=0; 0
^异或运算符,按二进制位进行"异或"运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;(A ^ B) 将得到 49,即为 0011 0001
int main()
{

	int a = 60;    /* 60 = 0011 1100 */
	int b = 13;    /* 13 = 0000 1101 */
	int c = 0;

	c = a & b;       /* 12 = 0000 1100 */
	printf("a & b的值是 %d\n", c);

	c = a | b;       /* 61 = 0011 1101 */
	printf("a | b的值是 %d\n", c);

	c = a ^ b;       /* 49 = 0011 0001 */
	printf("a ^ b的值是 %d\n", c);

	return 0;
}

一道变态的面试题:

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

//方法一:会有溢出的问题
int main()
{
	int a = 3;
	int b = 5;

	printf("交换前:a = %d b = %d\n", a, b);

	a = a + b;	// a=8, b=5
	b = a - b;	// a=8, b=3
	a = a - b;	// b=3, a=5 

	printf("交换后:a = %d b = %d\n", a, b);

	return 0;
}

//方法二:
//a^ a = 0;
//0 ^ a = a;
//a^ b^ a = b;
int main()
{
	int a = 3;
	int b = 5;

	printf("交换前:a = %d b = %d\n", a, b);

	a = a ^ b;	// a = 3^5
	b = a ^ b;	// b = 3^5^5 = 3
	a = a ^ b;	// a = 3^5^3 = 5

	printf("交换后:a = %d b = %d\n", a, b);

	return 0;
}

练习:

编写代码实现:求一个整数存储在内存中的二进制中1的个数。

//方法1
int main()
{
	int num = 10;	//00000000 00000000 00000000 00001010
	int count = 0;//计数
	while (num)
	{
		if (num % 2 == 1)
			count++;
		num = num / 2;
	}
	printf("二进制中1的个数 = %d\n", count);

	return 0;
}

//方法2:
int main()
{
	int num = 10;	//00000000 00000000 00000000 00001010
					//00000000 00000000 00000000 00000001
	int i = 0;	
	int count = 0;//计数
	for (i = 0; i < 32; i++)
	{
		//每次左移,判断是否为1
		if (num & (1 << i))
			count++;
	}
	printf("二进制中1的个数 = %d\n", count);

	return 0;
}

//方法3:
int main()
{
	int num = 10;
	int i = 0;
	int count = 0;//计数
	while (num)
	{
		count++;
		num = num & (num - 1);
	}
	printf("二进制中1的个数 = %d\n", count);

	return 0;
}

5. 赋值操作符

赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。

运算符描述实例
=简单的赋值运算符,把右边操作数的值赋给左边操作数C = A + B 将把 A + B 的值赋给 C
+=加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数C += A 相当于 C = C + A
-=减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数C -= A 相当于 C = C - A
*=乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数C *= A 相当于 C = C * A
/=除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数C /= A 相当于 C = C / A
%=求模且赋值运算符,求两个操作数的模赋值给左边操作数C %= A 相当于 C = C % A
<<=左移且赋值运算符C <<= 2 等同于 C = C << 2
>>=右移且赋值运算符C >>= 2 等同于 C = C >> 2
&=按位与且赋值运算符C &= 2 等同于 C = C & 2
^=按位异或且赋值运算符C ^= 2 等同于 C = C ^ 2
|=按位或且赋值运算符C |= 2 等同于 C = C | 2
int weight = 120;//体重
weight = 89;//不满意就赋值

double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。

//赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;

a = x = y+1;//连续赋值

//同样的语义:
x = y+1;
a = x;
//这样的写法更加清晰爽朗而且易于调试。

6. 单目操作符

6.1 单目操作符介绍

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

6.2 sizeof 和 数组

void test1(int arr[])
{
	printf("%d\n", sizeof(arr));//4
}
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));//40
	printf("%d\n", sizeof(ch));//10
	test1(arr);
	test2(ch);
	return 0;
}

7. 关系操作符

假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符描述实例
==检查两个操作数的值是否相等,如果相等则条件为真。(A == B) 为假。
!=检查两个操作数的值是否相等,如果不相等则条件为真。(A != B) 为真。
>检查左操作数的值是否大于右操作数的值,如果是则条件为真。(A > B) 为假。
<检查左操作数的值是否小于右操作数的值,如果是则条件为真。(A < B) 为真。
>=检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。(A >= B) 为假。
<=检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。(A <= B) 为真。

8. 逻辑操作符

假设变量 A 的值为 1,变量 B 的值为 0,则:

运算符描述实例
&&称为逻辑与运算符。如果两个操作数都非零,则条件为真。(A && B) 为假。
||称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。(A || B) 为真。
!称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。!(A && B) 为真。

360笔试题:

int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;

    i = a++ && ++b && d++;
    //  a=0,则i=0,后面短路,不需要再算
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d); //a = 1, b = 2, c = 3, d = 4

    return 0;
}


int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;

    i = a++ || ++b || d++;
	//	a++		b=3,则此时i=1,后面无需再算
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d); //a = 1, b = 3, c = 3, d = 4

    return 0;
}


int main()
{
    int i = 0, a = 1, b = 2, c = 3, d = 4;

    i = a++ || ++b || d++;
    //  a=1,则i=1,后面无需再算,短路
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d); //a = 2, b = 2, c = 3, d = 4

    return 0;
}

9. 条件操作符

exp1 ? exp2 : exp3

如果exp1为真 ? 则执行exp2,否则执行exp3

10. 逗号表达式

exp1, exp2, exp3, ... ,expN

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式c是多少?

//代码2
if (a = b + 1, c = a / 2, d > 0)
    
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
	//业务处理
	a = get_val();
	count_val(a);
}

//如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a > 0)
{
	//业务处理
}

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

  1. 下标引用操作符

    操作数:一个数组名 + 一个索引值

int arr[10];//创建数组

//实用下标引用操作符。
arr[9] = 10; <==> 9[arr] = 10;

//[]的两个操作数是arr和9。
  1. ( ) 函数调用操作符

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

int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int a = 3;
	int b = 5;

	int c = Add(a, b);//实用()作为函数调用操作符,操作数是Add, a, b

	return 0;
}
  1. 访问一个结构的成员

    . 结构体.成员名

    -> 结构体指针->成员名

struct Stu
{
	char name[10];
	int age;
	double score;
};
void set_stu(struct Stu* ps)
{
	//strcpy((*ps).name, "zhangsan");
	//(*ps).age = 20;
	//(*ps).score = 66.6;

	strcpy(ps->name, "zhangsan");
	ps->age = 20;
	ps->score = 66.6;
}
void print_stu(struct Stu ss)
{
	printf("%s %d %lf", ss.name, ss.age, ss.score);
}
int main()
{
	struct Stu s = { 0 };
	
	set_stu(&s);
	print_stu(s);

	return 0;
}

12. 表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。

同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

12.1 隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型
提升。

整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。

//实例1
int main()
{
	char a = 5;
	char b = 126;
	char c = a + b;

	printf("%d\n", c);	//-125

	return 0;
}

b和c的值被提升为普通整型,然后再执行加法运算。

加法运算完成之后,结果将被截断,然后再存储于a中。

如何进行整体提升呢?

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

//负数的整形提升
int main()
{
	char c1 = -1;

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

	return 0;
}

//正数的整形提升
int main()
{
	char c2 = 1;

	/*
	00000000 00000000 00000000 00000001
	00000000 00000000 00000000 00000001
	00000000 00000000 00000000 00000001

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

//无符号整形提升,高位补0

实例1:

int main()
{
	char a = 3;
	//3 <==> 00000000 00000000 00000000 00000011
	//a <==> 00000011

	char b = 127;
	//127 <==> 00000000 00000000 00000000 01111111
	//b <==> 01111111

	char c = a + b;
	//a和b都是char型,都没有达到int的大小,这里会发生整形提升
	//整形提升是按照变量的数据类型的符号位来提升的

	//对a和b整型提升:
	//a <==> 00000000 00000000 00000000 00000011
	//b <==> 00000000 00000000 00000000 01111111
	// 相加:
	//	     00000000 00000000 00000000 10000010 
	//c <==> 10000010	   
	//对c整型提升:符号位为1
	//c <==> 11111111 11111111 11111111 10000010
	// 
	//补码: 11111111 11111111 11111111 10000010
	//反码: 11111111 11111111 11111111 10000001
	//原码: 10000000 00000000 00000000 01111110
	// -126

	printf("%d\n", c);	//-126

	return 0;
}

实例2:

int main()
{
	char a = 0xb6;
	short b = 0xb600;
	int c = 0xb6000000;

	if (a == 0xb6)
		printf("a");

	if (b == 0xb600)
		printf("b");

	if (c == 0xb6000000)
		printf("c");

	return 0;
}

实例2中的a,b要进行整形提升,但是c不需要整形提升,a,b整形提升之后变成了负数,所以表达式 a==0xb6, b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。所以输出结果是:c 。

实例3:

int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));	//1
	printf("%u\n", sizeof(+c));	//4
	printf("%u\n", sizeof(-c));	//4
	//printf("%u\n", sizeof(!c));	//vs下为1,gcc下为4(应以gcc为准)

	return 0;
}

实例3中的c只要参与表达式运算,就会发生整形提升。表达式 +c ,会发生提升,所以 sizeof(+c) 是4个字节。表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节。但是 sizeof©,是1个字节。

12.2 算术转换

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

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

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运
算。

警告:
算术转换要合理,要不然会有一些潜在的问题。

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

12.3 操作符的属性

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

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

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

按运算符优先级从高到低列出各个运算符:

运算符优先级

版本一:

在这里插入图片描述

版本二:
类别运算符结合性
后缀() [] -> . ++ - -从左到右
一元+ - ! ~ ++ - - (type)* & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR|从左到右
逻辑与 AND&&从左到右
逻辑或 OR||从左到右
条件?:从右到左
赋值= += -= *= /= %=>>= <<= &= ^= |=从右到左
逗号,从左到右

一些问题表达式:

//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f

表达式1在计算的时候,由于*+的优先级高,只能保证*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。

所以表达式的计算机顺序就可能是:

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

或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
//表达式2
c + --c;

同上,操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

//代码3-非法表达式
int main()
{
	int i = 10;
	i = i-- - --i * ( i = -3 ) * i++ + ++i;
	printf("i = %d\n", i);
	return 0;
}

表达式3在不同编译器中测试结果:非法表达式程序的结果

//代码4
int fun()
{
	static int count = 1;
	return ++count;
}

int main()
{
	int answer;
	answer = fun() - fun() * fun();
	printf("%d\n", answer);	// 2 - 3 * 4
	return 0;
}

虽然在大多数的编译器上求得结果都是相同的。

但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。

函数的调用先后顺序无法通过操作符的优先级确定。

//代码5
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);

	printf("%d\n", ret);	//12
	printf("%d\n", i);		//4

	return 0;
}

Linux环境下结果是:10 4

同样的代码产生了不同的结果,这是为什么?

看一下汇编代码
在这里插入图片描述

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值