(C语言入门)四、操作符

一、操作符分类

操作符也是运算符

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号表达式

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

二、操作符介绍 

1.算术操作符

+     -     *    /

/除法得到的是商;%取模(取余)得到的是余数

除法操作符的两个操作数都是整数的话,执行的是整数除法

除法操作符的两个操作数只要有一个浮点数,执行的是小数除法

总结:

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

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

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

2.移位操作符

<< >>

<<左移操作符

>>右移操作符

注:操作符的操作数只能是整数

移动的是二进制位,二进制是由0和1组成的。比如数值10,在二进制中为1010。

同时,整数的二进制表示形式共有三种形式,即原码、反码、补码

原码:把一个数按照正负直接翻译成二进制就是原码。

反码:正整数与原码一致;负整数原码符号位不变,其他位按位取反。

补码:正整数与原码一致;反码+1

最高位是符号位(0为正,1为负),正整数的原码、反码、补码是相同的。

例如:5,-5是整数(整数是存放在整型变量中的一个整形变量是4个字节,也就是32个比特位)。

数值5的二进制表示为:00000000000000000000000000000101

数值-5的二进制表示为:

10000000000000000000000000000101(原码)

11111111111111111111111111111010(反码)

11111111111111111111111111111011(补码)

整数在内存中存储的是补码形式。

通过调试可以看到内存的存储为0x ff ff ff fb

回到左右移操作符(左移操作符的规则是:左边丢弃,右边补0)

当对正数进行操作的时候:

int main()
{
	int a = 3;
	//00000000000000000000000000000011
	int b = a << 1;
	//00000000000000000000000000000110
	printf("%d\n", b);//6
	printf("%d\n", a);//3

	return 0;
}

解析:虽然对数进行操作移动,但是给赋值的变量的大小本身没有发生改变

a没有发生变化,仅仅表达式结果变化。如果想要改变可以写成a=a>>1

当对负数进行操作的时候:

int main()
{
	int a = -3;
	//10000000000000000000000000000011
	//11111111111111111111111111111100
	//11111111111111111111111111111101 - 补码

	int b = a << 1;
	//11111111111111111111111111111010
	//11111111111111111111111111111001
	//10000000000000000000000000000110

	printf("%d\n", b);//-6
	printf("%d\n", a);//-3

	return 0;
}

0.56.27

左移操作符的规则是:左边丢弃,右边补0。

右移操作符

又分为两种:算术右移逻辑右移

右移的时候,采用逻辑右移还是算术右移的方式是取决于编译器的。

算术右移:右边丢弃,左边用原来的符号位填充。比如负整数,所以符号位为1,即左边补1。

逻辑右移:右边丢弃,左边直接用0填充。

int main()
{
	int a = -5;
	//10000000000000000000000000000101
	//11111111111111111111111111111010
	//11111111111111111111111111111011

	int b = a >> 1;
	//11111111111111111111111111111101//向右移动一位,左边加入1(与符号位一致)
	//11111111111111111111111111111100
	//10000000000000000000000000000011
	//-3
	printf("b = %d\n", b);
	printf("a = %d\n", a);//-5

	return 0;
}

vs用的是算术右移。

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

例如:

int num=10;

num>>-1;//这种是错误的,不可以移动负数位

3.位操作符

&     |     ^

&//按位与 -只有对应的两个二进位都为1时,结果位才为1
|//按位或 -只要对应的二个二进位有一个为1时,结果位就为1
^//按位异或 -对应的二进制位,相同为0,相异为1

注:他们的操作数必须是整数。这里的位是二进制位。

  例如:

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

在解决这个问题之前,我们先思考一下之前学过的实现两个数的交换,通过中间变量的方法。

 int temp=0;
 int a=3;
 int b=5;
 temp=a;
 a=b;
 b=temp;
 printf("交换后:a=%d b=%d\n");//使用中间值进行交换

而从此可以想到一个简单的方法。

	int a = 3;
	int b = 5;
	a = a + b;
	b = a - b;
	a = a - b;
	printf("交换后:a=%d b=%d\n", a, b);//较为简单的方法,但有缺陷

但是这种方法有缺陷,如果两个数都非常大,可能相加时会导致数据溢出。

我们通过今天的操作符对数据进行处理,因此有了:

    int a = 3;
	int b = 5;
	a = a ^ b;
	b = a ^ b;//a^b ^b
	a = a ^ b;//a^b ^a
    //到异或的运算(二进制位相同为0,相异为1)比如:
    //a^a=0
    //0^a=a
    //3^3^5=5
    //3^5^3=5(异或运算支持类似于交换律一般)
    printf("交换后:a=%d b=%d\n", a, b);
	return 0;

注意:a^0=a;   a^a=0;   a^a^b=b;   a^b^a=b

该解法的缺陷:1、只能适用于整数 2、代码可读性不高,如果不熟悉可能会混淆。

4.赋值操作符

赋值操作符是对之前不满意的数字重新赋值一个自己所需值的操作符。赋值操作符可以连续赋值。

复合赋值符

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

 例如:

	int a = 3;
	a = a + 3;
	a += 3;

	a = a >> 3;
	a >>= 3;

	a = a ^ 5;
	a ^= 5;

 初始化与赋值:

初始化是没有这个变量,对他定义类型的同时并给定一个值;例如int a =3;

赋值是已经定义好这个变量,现在重新给定一个值。例如 a=5;

5.单目操作符

!            逻辑反操作

-            负值

+           正值

&           取地址

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

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

--           前置,后置--

++         前置,后置++

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

(类型)    强制转换类型

1.有关sizeof的问题: 

sizeof计算类型长度的时候,如果计算的是变量名,sizeof后面的括号可加可不加;但是如果是类型的关键字的时候,必须加上括号。sizeof计算时只看数据类型,不看数据内容。

	int a = 10;
	printf("%d\n", sizeof a);
	printf("%d\n", sizeof(a));//可加可不加
	printf("%d\n", sizeof(int));//必须加括号

sizeof求数组大小

	int arr[10] = { 0 };
	printf("%d\n", sizeof(arr));//求整个数组大小
	printf("%d\n", sizeof(arr[0]));//一个元素的大小
	int sz = sizeof(arr) / sizeof(arr[0]);//元素个数
	printf("%d\n", sz);

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

 

 答案是40 10 4 4(在x86是4 4)

解析:主函数sizeof(arr)和sizeof(ch),前者计算的是arr整个数组的大小所以是4*10=40;后者

是1*10=10;而在test1和test2函数中,数组传参是首元素地址,所以传入的是int*的指针和char*的指针。而指针的大小为4或者8,在x86环境下大小为4,在x64环境下大小为8.因此为40 10 4 4

2.有关~(按位取反)的问题

①.例:a=0,那么~a的值?

	int a = 0;
	//00000000000000000000000000000000
	//11111111111111111111111111111111
	//11111111111111111111111111111110
	//10000000000000000000000000000001
	//
	printf("%d\n", ~a);
	return 0;

 

②.运用: 通过操作符对二进制位进行修改

    int a = 3;
	//00000000000000000000000000000011
	//现在要求把倒数第四位为1,那么需要怎么做?
	//00000000000000000000000000000001
	//00000000000000000000000000001000
	a |= (1 << 3);
	printf("%d\n", a);

	//00000000000000000000000000001011
	//现在要求把倒数第四位为0,那么需要怎么做?
	//00000000000000000000000000000001
	//00000000000000000000000000001000
	//11111111111111111111111111110111
	a &= (~(1 << 3));
	printf("%d\n", a);
	return 0;

 ③.看一个代码段:

while(~scanf("%d",&n));//scanf函数读取失败的时候,会返回EOF EOF为-1
//10000000000000000000000000000001
//11111111111111111111111111111110
//11111111111111111111111111111111
//00000000000000000000000000000000 

即scanf读取失败时,while停止

3.有关++ --的问题

 只需注意前置--和++、后置--和++的位置即可。

4.强制转换类型

    float a = 3.14f;
	int b = (int)a;
	//int b = int(a);//err
	printf("%d", b);
	return 0;

 6、关系操作符

>=

<

<=

!=   用于测试“不相等”

==  用于测试“相等”

 注意:一个“=”叫赋值,两个“=”(==)为相等。

7、逻辑操作符

&&   逻辑与

||      逻辑或

这俩只关注真/假

&     按位与

|      按位或

这俩主要是二进制运算

    int a = 3 && 5;
	printf("%d\n", a);//a=1
	int a = 0 || 0;//1 0
	printf("%d\n", a);//a=0

例题:

#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\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}
//程序输出的结果是什么?

输出为:

 解析:i的表达式先计算a++&&++b,a++的值为0,之后a的值为1,而&&(逻辑与)的操作符使得i的表达式不再继续判断,因此i=a++之后的表达式均未执行,所以b、c、d未发生变化(均为初始化给定的值)。换成i = a++||++b||d++;也是同理,如果判断出逻辑则不再执行后面的表达式。

即:&&操作符左边为假,右边不再计算。||操作符左边为真,右边不再计算。(短路)

8.条件操作符

exp1?exp2:exp3

如果表达式exp1的结果为真,则表达式2为整个表达式的结果,否则表达式3为整个表达式的结果。

1.练习:如何将下面的代码转成条件表达式?

if (a > 5)
b = 3;
else
b = -3;//问题

所以可以写成:

	int a = 0;
	int b = 0;
	scanf("%d", &a);
	b = ((a > 5) ? 3 : -3);
  //b=(a>5)?(b=3):(b=-3);//或者
	printf("%d\n", b);

	return 0;

2.使用条件表达式实现找两个数中较大值。

	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int m = (a > b ? a : b);
	printf("%d\n", m);

	return 0;

9.逗号表达式

exp1,exp2,exp3,...,expN

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

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

//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
//c是多少?//c=13
//代码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)
{
         //业务处理
}//更加简便

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

1.[ ]下标引用操作符

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

int arr[10];
arr[9]=10;

在[ ]的两个操作数是arr9

2.()函数调用操作符

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

#include <string.h>
int Add(int x, int y)
{
	return x + y;
}
void test()
{

}
int main()
{
	int len = strlen("abc");//()函数调用操作符
	printf("%d\n", len);
	int c = Add(3, 5);//Add,3,5 都是()的操作数
	test();//只有test函数名为操作数
	return 0;
}

3.访问一个结果的成员

.    (结构体.成员名)

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

第一种方式:. 结构体.成员名直接找成员

struct S
	{
		int num;
		char c;
	};

	int main()
	{
		struct S s = {100, 'b'};//结构体的初始化使用{}
		//打印结构中的成员数据
		printf("%d\n", s.num);
		printf("%c\n", s.c);
		//. 操作符     结构体变量.结构体成员名
		return 0;
	}//输出100,'b'

第二种方式:(*ps) 解引用来找成员

struct S
	{
		int num;
		char c;
	};

void test(struct S* ps)
{
	printf("%d\n", (*ps).num);
	printf("%c\n", (*ps).c);
}
	int main()
	{
		struct S s = {100, 'b'};
		test(&s);
		//. 操作符     结构体变量.结构体成员名
		return 0;
	}

第三种方式:ps->成员

struct S
	{
		int num;
		char c;
	};

void test(struct S* ps)
{
	/*printf("%d\n", (*ps).num);
	printf("%c\n", (*ps).c);*/
	printf("%d\n", ps->num);
	printf("%c\n", ps->c);

}
	int main()
	{
		struct S s = {100, 'b'};
		test(&s);
		//. 操作符     结构体变量.结构体成员名
		return 0;
	}

 11.表达式求值

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

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

11.1隐式类型转换

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

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

整型提升的意义:

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

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

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

 目的是为了适应cpu的运算长度。

比如:a、b、c是怎样实现整形提升的过程的呢?c的值为多少?

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

a的二进制为:00000000000000000000000000000011

b的二进制为:00000000000000000000000001111111

变量a和b的二进制位(补码)中只有8个比特位,分别是:00000011 和 01111111(截断)

根据通过符号位进行整形提升,高位补充符号位,而a b截断后的第一位为0,所以补0,因此变成00000000000000000000000000000011和00000000000000000000000001111111

若char c1=-1;char的二进制位(补码)中只有8个比特位,则11111111,第一位充当符号位,所以整形提升的时候,整形提升补1,则11111111111111111111111111111111。

如果是无符号整形提升,高位补0。

 因此,我们可以根据整形提升求得c的值。

int main()
{
	char a = 3;
	//00000000000000000000000000000011
	//00000011-截断
	char b = 127;
	//00000000000000000000000001111111
	//01111111-截断
	
	char c = a + b;
	//00000000000000000000000000000011
	//00000000000000000000000001111111
	//00000000000000000000000010000010
	//10000010 - c
	//整型提升
	printf("%d\n", c);
	//11111111111111111111111110000010
	//11111111111111111111111110000001
	//10000000000000000000000001111110
	//-126

	return 0;
}

整形提升的例子:

//实例1
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;
}

 一种解释是a,b超出范围,a=0xb6=1011 0110=182(大于char的范围-128~127),b=0xb600=1011011000=46592(大于short的范围-32768~+32767)所以只输出c

另一种解释就是整形提升,a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表 达式 c==0xb6000000 的结果是真

另外:整形提升主要针对char和short

//实例2
int main()
{
 char c = 1;
 char d = 2;
 printf("%u\n", sizeof(c));
 printf("%u\n", sizeof(+c));
 printf("%u\n", sizeof(-c));
 printf("%u\n", sizeof(c+d));
 return 0;
}

例2中的c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节.

表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节

 

 11.2算术转换

如果某个操作数的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。(一般算术转换只考虑int及以上的类型,即无char、short,char和short转换主要是整形提升)

long double

double

float

unsigned long int

long int

unsigned int

int

例如int和float一起运算会转换为float,long int和int一起运算会转换为long int。一般是由下往上转换。

总结:计算类型不统一的时候,需要对类型进行转换。类型不大于整形(int)的时候,char short均用整形提升,提升为整形(int)来进行计算;大于整形(int)的时候,类型不统一则需要算术转化。

11.3操作符的属性

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

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

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

(操作符优先级表格略,从上到下优先级逐渐降低)

注:N/A表示无结合性,L-R是从左到右的结合,R-L同理。逻辑与、逻辑或等等存在控制求值顺序。

我们写出的表达式一定可以通过操作符的属性确定唯一的计算路径吗?答案是否定的。

一些问题表达式

1.下面这个代码的计算顺序是什么?

//例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的数值是什么时候获取的,是在--c之前还是--c之后,所以结果是不确定的,是存在歧义的。

例题3:

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

这段非法的表达式就是前两道题中的代码的综合,因为存在歧义,导致在不同的编译器在不同的编译环境下产生的结果也不同。

例题4:

//代码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();我们只能通过操作符的优先级可以知道他的运算顺序,但是每个函数的调用先后顺序我们无法确定,不同的函数调用顺序会导致answer的值不同。

例题5:

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

结果为: 

但是这段代码也是存在问题的,如果尝试在不同的编译器下,这段代码会有不同的结果。

我们可以通过汇编代码来观察:

1.

2.取消显示符号名:

 ​​​​​​

 注:ebp,esp 一般是用来存放地址。eax,ebx,ecx,edx都是来存放数据的。

3、把[ebp-8]的值移动到eax,eax=1;eax通过add1,eax=2,;eax再把值放入[ebp-8],因此[ebp-8]=2

4、+号部分的运算 

 

此时i=4, 所以就是4+4+4=12

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

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

(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值