C语言笔记:操作符

目录

1.算数操作符

2.移位操作符

2.1 左移操作符

2.2 右移操作符

3. 位操作符

3.1 一道变态的面试题

3.2 经典面试题(需重视)

4.赋值操作符

5.单目操作符

5.2 sizeof和数组

5.3 ++和--运算符

6.关系操作符

7.逻辑操作符

8.条件操作符(三目操作符)

9.逗号表达式

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

10.1 [ ]:下标引用操作符

10.2 ( ):函数调用操作符

10.3 访问一个结构的成员

11.表达式求值

11.1 隐式类型转换

11.2 算术转换

11.3 操作符的属性


1.算数操作符

+ - * / %

注:

1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。

2. 对于 / 操作符如果两个操作数都为整数执行整数除法。而只要有浮点数执行的就是浮点数除法

3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数

void test1()
{
	//6.0 % 2.0; //不是整型,err
	int a= 6 / 2;
	int b = 7 / 2;  
	double c = 7 / 2.0;
	printf("a = %d b = %d c = %lf", a, b,c);//a=3 b=3 c=3.500000 
}

2.移位操作符

<< 左移操作符

>> 右移操作符    

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

2.1 左移操作符

移动的是二进制位

移位规则: 左边抛弃,右边补0

void test2()
{
	int a = 7;//a是一个int变量,4个字节,32个bit位 
	int b = a << 1;

	printf("b = %d\n", b); //14
}

2.2 右移操作符

移位规则:

首先右移运算分两种:

1. 逻辑移位:左边用0填充,右边丢弃

2. 算术移位:左边用原符号位填充,右边丢弃  (原来是正数则补0,反之补1)

补充:整数存放在内存中的时候,放的是二进制,表示形式有三种:

原码、反码、补码

对于-1:

10000000000000000000000000000001  原码

11111111111111111111111111111110        反码(符号位不变,其他位按位取反)

11111111111111111111111111111111        补码(反码 + 1)

对于整数,在计算机内是以补码形式存储的

对于int b = -1;

可见,-1的二进制为11111111111111111111111111111111 =>  补码形式

void test3()
{
	int a = 1; 
	int b = -1;
	int c = a >> 1;
	int d = b >> 1;
	//b从11111111111111111111111111111111
	//=> 11111111111111111111111111111111
	printf("c = %d d = %d\n", c, d); //c = 0,d = -1 说明此时编译器用的是算数右移
}

 警告⚠ : 对于移位运算符,不要移动负数位,这个是标准未定义的。 例如:

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

3. 位操作符

位操作符有:

& :按位与:有0结果就是0,全为1时结果为1

|   :按位或:有1结果就是1,全为0时结果为0

^   :按位异或:对应的二进制位,相同为0相异为1

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

void test4()
{
	int a = 3;
	int b = 5;
	int c = a & b; // & - 按(二进制)位与 - 有0结果就是0,全为1时结果为1
	//a:011 
	//b:101
	//c:001
	int d = a | b; // | - 按(二进制)位或 - 有1结果就是1,全为0时结果为0
	//a:011
	//b:101
	//d:111
	int e = a ^ b; // ^ - 按(二进制)位异或 - 对应的二进制位,相同为0相异为1
	//a:011
	//b:101
	//e:110
	printf("c = %d d = %d e = %d", c, d, e); //c = 1 d = 7 e = 6
}

3.1 一道变态的面试题

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

void test5()
{
	int a = 3;  //011
	int b = 5;  //101

	a = a ^ b;  // a = 110
	b = a ^ b;  // b = 011
	a = a ^ b;  // a = 101

	printf("a = %d b = %d\n", a, b); //a = 5 b =3
}

结论:a^b^a = b,b^a^b = a ,a^a = 0,0^a = a

(a^b^a中: a与a抵消得到b,且结论符合"交换律",即a^a^b = b,两个相同的数字异或结果为000)

3.2 经典面试题(需重视)

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

//方法一
int count_one1(int n)//该方法存在缺陷,计算负数会出错
{
	int count = 0;
	while (n) //只要不是0 二进制序列中就会有1
	{
		if (n % 2 == 1)
		{
			count++;
		}
		n = n / 2;
	}
	return count;
}

//方法二
//当a与1按位与:
//11111111111111111111111111111111
//00000000000000000000000000000001 
//00000000000000000000000000000001 
//可见:1的最后一位数字与a的最后一位保持一致
//所以判断一次就右移一次a,每次都与1相与 总共右移32次
int count_one2(int n)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 32; i++)
	{
		if ( ( (n >> i) & 1 ) == 1 )
		{
			count++;
		}
	}
	return count;
	
}

//方法三(最好的办法)
//n = n & (n-1)  假设n = 7
//   111 & 110 = 110
//再来一次:
//n = n & (n-1)
//   110   100 = 100
//结论:n = n & (n-1) 把n的二进制中从右向左的第一个1变成了0
//方法三的具体步骤为 : 在n = 0之前,不停地用n = n & (n-1),执行了几次,则有多少个1
int count_one3(int n)
{
	int count = 0;
	while (n != 0)
	{
		n = n & n - 1;
		count++;
	}
	return count;
}

void test6()
{
	int a = -1; 
	
	int ret = count_one3(a);
	printf("%d\n", ret);
}

总结:

1.a & 1,可以达到的目的是:1的最后一位数字与a的最后一位保持一致,这样配合a的右移操作,可以计算出二进制中存在多少个1

2.n = n & (n-1),该方法把n的二进制中从右向左的第一个1变成了0,配合循环:在n = 0之前,不停地用n = n & (n-1),循环执行了几次,则有多少个1

4. 赋值操作符

void test7()
{
	int a = 0;
	a = 3;   //赋值操作符
}

 赋值操作符可以连续使用(但不建议使用):

void test8()
{
	int a = 10;
	int x = 0;
	int y = 20;
	a = x = y + 1;//连续赋值
	printf("a = %d\n", a); //a = 21
}

先执行x = y + 1,再执行 a = x

复合赋值符 :+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^= 这些运算符都可以写成复合的效果。 如:

int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。

练习:已知14的二进制位为:00000000000000000000000000001110

现在让倒数第5位的0变成1,实现后再变成0

思路:想让从右向左第 i 位从0变成1,其余位不变,则让原本的二进制序列和只有倒数第i位是1,其余位全是0的二进制序列相或操作如:

序列a:00000000000000000000000000001110

序列b:00000000000000000000000000010000  序列b由1向左移4 (i-1) 个单位得到

相或:  ------------------------------------------------------

序列c:00000000000000000000000000011110

而想要从序列c变回a,即把倒数第5位从1变回0的方法:让序列c与除了倒数第5位为0,其余位全为1的序列d相与操作:

序列c:00000000000000000000000000011110

序列d:1 1 11 1 1 1 1111111111111111111101111

相与:  ------------------------------------------------------

序列a:00000000000000000000000000001110

不难看出:序列d正是由序列b按位取反操作得到

所以代码如下:

void test10()
{
	int a = 14;
	a |= (1 << 4);
	printf("%d\n", a); //30

	a &= (~(1 << 4));

	printf("%d\n", a); //14
}

5.单目操作符

!           逻辑反操作

-           负值

+           正值

&           取地址 sizeof      操作数的类型长度(以字节为单位)

~           对一个数的二进制按位取反

--          前置、后置--

++          前置、后置++

*           间接访问操作符(解引用操作符)

(类型)       强制类型转换

void test9()
{
	3 + 5; // +有两个操作数 双目操作符
	//单目操作符:只有一个操作数的操作符
	//!操作符
	int a = 10;
	printf("a = %d\n", a); //10非0 - 真
	printf("!a = %d\n", !a);//0
	
	int flag = 1;
	if (!flag)
	{
		//表示当flag为假的时候执行if语句
	}
	//&与*操作符 取地址与解引用
	int b = 10;
	int* pb = &b; //pb为指针变量,b的地址存在pa中
	*pb = 20; //解引用操作 通过*pb访问或修改b的值
	printf("b = %d\n", b); //20 

	//sizeof操作符 而不是函数! 还可以计算数组总大小
	int c = 10;
	int arr[10] = { 0 };
	printf("sizeof(c) = %d\n", sizeof(c)); //4
	printf("sizeof(int) = %d\n", sizeof(int)); //4
	printf("sizeof(arr) = %d\n", sizeof(arr)); //40
	printf("sizeof(int[10]) = %d\n", sizeof(int[10]) ); //40

	// ~操作符 按位取反
	int d = 0;
	printf("d = %d\n", ~d); //-1
	//00000000000000000000000000000000 //d的补码
	//11111111111111111111111111111111 //按位取反后的补码
	//11111111111111111111111111111110 //反码 (补码-1)
	//10000000000000000000000000000001 //源码 (符号位不变,其他按位取反)
	//-1

	//(强制类型转换)
	double e = 3.14;
	printf("e = %d\n", (int)e); // e = 3

}

5.2 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)两个地方分别输出多少?               答:40 10

(3)、(4)两个地方分别输出多少?               答:4     4(注意)

注:传入test2的是ch数组第一个元素的指针,为4或8个字节,不要理解成ch第一个元素的大小1

5.3 ++和--运算符

void test11()
{
	int a = 10;
	int b = 10;
	int c = a++; //后置++:先使用再自增1
	int d = ++b; //前置++ 先自增1后使用
	printf("c = %d d = %d a = %d b = %d \n", c, d, a, b);//10 11 11 11

	/*int e = 10;
	int f = e + 1; //1
	f = ++e; //带有副作用 - e变成了12*/

	int g = 1;
	printf("%d\n", g++); // 1
	printf("%d\n", ++g); // 3

}

例题:下面代码的结果是多少?

#include <stdio.h>
int main()
{
	int a, b, c;
	a = 5;
	c = ++a;
	b = ++c, c++, ++a, a++;
	b += a++ + c;
	printf("a = %d b = %d c = %d\n:", a, b, c);// a = 9 b = 23 c = 8
	return 0;
}

6. 关系操作符

>

>=

<

<=

!=   用于测试“不相等”

==      用于测试“相等”

 这些关系运算符比较简单,但是我们要注意一些运算符使用时候的易错点

1.在编程的过程中== 和=不小心写错,导致的错误

2.字符串是不能直接用==比较是否相等的,对象也不能用==判断相等

3.判断时不能直接用连等如:1<a<7 而应该是 a>1 && a<7 (若用if(1<a<7),当a=8时,会先判断1<8?,为真,则继续判断1<7?,为真,所以整个表达式为真,if语句内容就会执行导致出错)

7. 逻辑操作符

&&     逻辑与

||        逻辑或

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

void test1()
{
	int a = 2; //010
	int b = 5; //101
	int c = a & b;   //按(2进制)位与:000
 	int d = a && b; //逻辑与:1
	printf("c = %d d = %d\n", c, d);//0 1

	int e = a | b;   //按(2进制)位或:111
	int f = a || b;  //逻辑或:1
	printf("e = %d f = %d\n", e, f);//7 1
}

面试题:

void test2()
{
	//程序输出的结果是什么?
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;       //a=1 b=2 c=3 d=4
	//i = a++||++b||d++;         //a=1 b=3 c=3 d=4
	printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
}

注:当使用&&判断时(a && b),当a已经为假时,编译器不会再判断b的真假,因为结果肯定为假

同理,当使用 || 判断时(a || b),当b已经为假时,编译器不会再判断b的真假,因为结果肯定为真

8.条件操作符(三目操作符)

表达式1 ? 表达式2 : 表达式3

表达式1若为真,则整个表达式的结果为表达式2的结果,若为假,则整个表达式的结果为表达式3的结果

void test3()
{
	int a = 10;
	int b = 20;
	int max = 0;
	max = a > b ? a : b;
	printf("max = %d", max);
}
//上述代码等价于:
void test3()
{
	int a = 10;
	int b = 20;
	int max = 0;
	if (a > b)
		max = a;
	else
		max = b;
	printf("max = %d", max);
}

9.逗号表达式

表达式1,表达式2,表达式3...表达式n

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

注:前面的表达式的结果可能会影响最后一个表达式,所以不能只看最后一个表达式而忽略前面的

void test4()
{
	//代码1
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	int d = 0;
	printf("c = %d", c); //13

	//代码2
	if (a = b + 1, c = a / 2, d > 10);
	//该表达式最终以d>10的真假做判断

	//代码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)
		{
			//业务处理
		}
		//判断条件仍是a>0,但前面的代码依旧会进行,该代码看起来更为简洁
}

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

10.1 [ ]:下标引用操作符

操作数为:1.数组名 2.索引值

void test5()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	arr[4]; //下标引用操作符
	//系统会把arr[4] ==> *(arr + 4)
	
	printf("%d\n", *(arr + 4)); //5  注:整型指针+1 地址跳过4个字节
	printf("%d\n", arr[4]);     //5
	printf("%d\n", 4[arr]);     //5  说明数组名和索引值可以互换位置
	
}

10.2  ( ):函数调用操作符

int test6()
{
	printf("hehe"); //函数调用操作符,即便没有传参也不能省略( )
}

10.3 访问一个结构的成员

.                 结构体.成员名

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

void test7()
{
	struct Book
	{
		char name[20];
		int price;
	};
	struct Book b = {"C语言程序设计", 35};
	//结构体变量.成员变量
	printf("书名:%s 定价:%d\n", b.name, b.price);

	struct Book* pb = &b;
	//结构体指针->成员变量
	printf("书名:%s 定价:%d\n", pb->name, pb->price);

}

11.表达式求值

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

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

11.1 隐式类型转换

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

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

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

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

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

如何进行整体提升呢?

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

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


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

整形提升的例子:

void test8()
{
	char a = 3;
	char b = 127;
	char c = a + b;
	//a和b要发生整型提升
	//3的二进制:      00000000000000000000000000000011
	//把3存入a中,此时的a:00000011,由于最高位为0,正数
	//所以a整形提升为:00000000000000000000000000000011   -提升后的a

	//127的二进制:    00000000000000000000000001111111
	//存入b中后:      01111111
	//整形提升后:     00000000000000000000000001111111   -提升后的b
	//           +   00000000000000000000000000000011  
	//a+b            00000000000000000000000010000010
	//存入c中:10000010
	//打印%d的c - 又要发生整形提升
	//此时打印的c:    11111111111111111111111110000010   -提升后的c - 补码
	//               11111111111111111111111110000001              - 反码
	//               10000000000000000000000001111110              - 原码 -126

	printf("c = %d\n", c); //-126
}
void test9()
{
	char a = 0xb6;       //10110110         
	short b = 0xb600;    //1011011000000000
	int c = 0xb6000000;  //101101100000000000000000
	if (a == 0xb6)
		printf("a");
	if (b == 0xb600)
		printf("b");
	if (c == 0xb6000000)
		printf("c");     
	//c
}  
void test10()
{
	char c = 1;
	printf("%u\n", sizeof(c));   //1 
	printf("%u\n", sizeof(+c));  //4 +c是表达式,会发生整形提升(变成int 4字节)
	printf("%u\n", sizeof(-c));  //4 与+c同理
}

11.2 算术转换

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

long double

double

float

unsigned long int

long int

unsigned int

int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。(如:int和long int相加,会把int转换为long int)

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

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

 例题:求下列代码的结果

#include <stdio.h>
int i; //全局变量 - 默认初始化为0
int main()
{
    i--; 
    if (i > sizeof(i))  
    {
        printf(">\n");
    }
    else
    {
        printf("<\n");
    }
    return 0; 
}

输出结果为:>

解析:对于表达式i > sizeof(i),i是int类型,而sizeof(i)是unsigned int类型,此时会发生算数转换i会转换为unsigned int类型,算数转换前i = -1,在内存中为:1111111111111111111111111111,此时二进制序列的符号位(最高位)为1表示是负数,转换为无符号整型后,最高位也为有效数字,i 成了一个非常大的整数,那么肯定大于4了

11.3 操作符的属性

1. 操作符的优先级

2. 操作符的结合性

3. 是否控制求值顺序

一些问题表达式:

表达式的求值部分由操作符的优先级决定。
表达式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;

注释:注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。 假设c = 1,那么计算顺序可能是 1 + 0 = 1,也可能是 0 + 0 = 0

//问题代码3
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(); 中我们只能通过操作符的优先级得知:先算乘法, 再算减法。,函数的调用先后顺序无法通过操作符的优先级确定。

//问题代码4
#include <stdio.h>
int main()
{
    int i = 1;
    int ret = (++i) + (++i) + (++i);
    printf("%d\n", ret);  //12   4 + 4 + 4
    printf("%d\n", i);    //4
 return 0;  
}

但在linux系统下 输出的结果为 ret = 10(3+3+4) ,i = 4

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

猜你喜欢

转载自blog.csdn.net/m0_62934529/article/details/123436088
今日推荐