C语言初阶(5) -- 操作符详解

目录

1. 操作符的分类

2.二进制的转换

2.1   2进制转10进制

2.2   10进制转2进制

2.3  2进制转8进制

2.4  2进制转16进制

3.原码反码补码

4.算数操作符

5.移位操作符

5.1 左移操作符 (<<)

5.2 右移操作符(>>)

6.位操作符

6.1 一道面试题:

 6.2 练习:

7. 赋值操作符

(1)  += 、-=、/=、%=

(2) >>=  、 <<=

(3) &=、|=、^=

8.单目操作符

8.1 单目操作符介绍

8.2 !(逻辑反操作)

8.3  + (正值)、 - (负值)

8.4 &(取地址)

8.7 --、++(前后置--、++)

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

8.9  sizeof和数组

9. 关系操作符

10.逻辑操作符

10.1  &&、||(逻辑与、逻辑或)

9、条件操作符

10、逗号表达式

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

11.1 []下标引用操作符

11.2 ()函数调用操作符

12 表达式求值


1. 操作符的分类

C语言为处理数据提供了大量的操作,可以在程序中进行算术运算、比较值的大小、修改变量、逻辑地组合关系等。我们先从基本的算术运算(加、减、乘、除)开始。

  • 算术操作符:+、-、*、/、%
  • 移位操作符:<<  、>>
  • 位操作符:&、 |  、^
  • 赋值操作符:= 、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
  • 单目操作符:!、++、--、&、*、+、-、~、sizeof、(类型)
  • 关系操作符:>、>=、<、<=、==、!=
  • 逻辑操作符:&&、||
  • 条件操作符:表达式1 ? 表达式2 : 表达式3 ;
  • 逗号表达式:exp1, exp2, exp3, exp4, exp5,......
  • 下标引用: [ ]
  • 函数调用: () 
  • 结构成员访问: .  、->

操作符中的一些操作和二进制有关系,我们先来学习一下二进制的一些转换问题。

2.二进制的转换

        我们经常听到二进制、八进制、十进制、十六进制这样的讲法,那这代表什么意思呢?

        通常都是基于数字10来书写数字。例如1234的千位是1,百位是2,十位是3,个位是4,可以写成:1*1000+2*100+3*10+4*1=1234

        注意:1000是10的立方(即三次幂),100是10的平方(即2次幂),10是10的1次幂,而且10(以及任意的正整数)的0次幂都是1。因此,1234也可以写写成:1*10^3+2*10^2+3*10^1+4*10^0.

        因为这种书写数字的方法是基于10的幂,所以称以10为基底书写1234.

        姑且认为十进制系统得以发展是得益于我们都有10根手指。从某种意义上看,计算机的位只有2根手指,因为它只能被设置位0或1,关闭或打开。因此,计算机使用基底为2的数制系统。它用2的幂,而不是10的幂。以2为基底表示的数字被称为二进制数(binary number)。二进制中的2和十进制中的10作用相同。

        八进制(octal)是指八进制记数系统。该系统基于8的幂,用0~7表示数字(正如十进制用0~9表示数字一样)。

        十六进制(hexadecimal或hex)是指十六进制记数系统。该系统基于16的幂,用0~15表示数字。但是,由于没有单独的数(digit, 即0~9这样单独一位的数)表示10~15,所以用字母A~F来表示。

 补充:

        十进制是我们生活中经常使用的已经形成了许多的常识。

        十进制中满10进1;

        10进制数字每一位都是0~9的数字形成的;

其实二进制也是一样的:

        二进制中满2进1;

        2进制的数字每一位都是0~1的数字组成;

举栗说明:

  1. 15的二进制数:1111
  2. 15的八进制数:17
  3. 15的十进制数:15
  4. 15的十六进制数:F
  5. //十六进制的数值之前写:0x
  6. //八进制数值之前写:0
2.1   2进制转10进制

1101=1*2的零次幂+0*2的一次幂+1*2的二次幂+1*2的三次幂。

注:从最低位开始计算(最右边计算最低位),依次往左边的高位计算在累加。

如:1101 = 1*2^3+1*2^2+0*2^0+1*2^1=13

//二进制转十进制
#include<stdio.h>
int main()
{
	int n = 0;		//n为二进制数
	int sum = 0;	//sum为每一位计算所加的和
	int a = 1;		//a为位权
	int m = 0;		//位有效标志
	scanf("%d", &n);
	while (n!=0)
	{
		m = n % 10;			//数字中的所有位
		sum = sum + m * a;  //求和
		a *= 2;				//位权随着位的变化依次增加
		n /= 10;			//进行下一位的输出
	}
	printf("%d\n", sum);
	return 0;
}

其中,代码中的整数a代替的是2的n次方(n代表的是从左往右第n位)。

其运行结果如下:

2.2   10进制转2进制

十进制转二进制其实就是让这个10进制数一直除以2,然后取他的余数,倒着连在一起就是这个十进制数的二进制数。

第一种方法:

//十进制转二进制
#include<stdio.h>
int main()
{
	int n = 0;		//n为二进制数
	int sum = 0;	//sum为每一位计算所加的和
	int a = 1;		//a为位权
	int m = 0;		//位有效标志
	printf("请输入十进制正整数: ");
	scanf("%d", &n);
	while((n + 1) / 2 != 0)
	{
		m = n % 2;			//数字中的所有位
		sum = sum + m * a;  //求和
		a *= 10;			//相当于位权
		n /= 2;				//进行下一位输出
	}
	printf("二进制数位: %d\n", sum);
	return 0;
}

输出结果:

第二种方法:

//十进制转二进制2
#include<stdio.h>
int main()
{
	int num = 0;
	int tmp = 0;
	int i = 0;
	int arr[32];
	printf("请输入一个十进制数: ");
	scanf("%d", &num);
	do
	{
		tmp = num % 2;
		num = num / 2;
		arr[i++] = tmp;
	} while (num != 0);
	int j = 0;
	printf("输出其二进制数: ");
	for (j = i - 1; j >= 0; j--)
	{
		printf("%d", arr[j]);
	}
	return 0;
}

输出结果:

2.3  2进制转8进制

举一个例子:

如:111000110101001转换为八进制

按照八四二一法则

将二进制3个一等分变成:

111 000 110 101 001

按照八四二一法则:111=7;000=0;110=6;101=5;001=1

所以二进制转换为8进制的数为:70651

2.4  2进制转16进制

十六进制:十六进制逢十六进一,所有的数组是0到9和A到F组成,其中A代表10,B代表11,以此类推,字母不区分大小写。

2进制转换16进制的方法:利用8421法则;

如二进制0010110101110001110011010101,将其分割0010 1101 0111 0001 1100 1101 0101(16进制的一位数对应2进制的4位数);

0010利用8421为:2; 

1101利用8421为:8+4+1=13为D;

同理:0111为7,0001为1,1100为12=C,1101为13=D,0101为5

所以二进制数0010 1101 0111 0001 1100 1101 0101转换为16进制数为:2D71CD5

3.原码反码补码

整数的二进制数表示方法有三种,即原码、反码、补码。

有符号整数的三种表示方法分为有符号位和数值位两部分,二进制序列中,最高位的1位是被当作符号位,其余都是数值位。

符号位都是0表示正,1表示负

正整数的原反补码都是相同的,但是负数的有三种的表示方法

原码:直接将数值按照正负数的形式翻译成二进制数就是原码;

原码是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。

原码是人脑最容易理解和计算的表示方式。

反码:将原码的符号位不变,其他为按位取反;

补码:符号位不变,反码+1得到补码。

补码得到原码也是可以使用: 取反, +1的操作。

例如:

-20的原码:10000000 00000000 00000000 00010100

-20的反码:11111111   11111111   11111111   11101011

-20的补码:11111111   11111111   11111111   11101100

对于整形来说:数据存放在内存中其实就是存放的补码。

为什么呢?

        因为在计算机系统中,数值一律用补码来表示和存储,原因在于,使用补码,可以将符号位和数值域统一处理(CPU只有加速器),此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

4.算数操作符

算术操作符有+ (加)、- (减)、* (乘)、/ (除)、% (取余、取模)、++ (自增)、-- (自减).

用于各类数值运算。

        C用运算符 (operator) 表示算术运算符。例如:+运算符使它两侧的值加在一起。如果你觉得术语"运算符"很奇怪,那么请记住东西总得有个名称。与其叫"那些东西" 或 "运算处理符",还不如叫"运算符"。

        加法运算符(addition operator) 用于加法运算,使其两侧的值相加。

        减法运算符(subtraction operator) 用于减法运算,使其左侧的数减去右侧的数。

        + 和 - 运算符都被称为二元运算符(binary operator), 即这些运算符需要两个运算符对象才能完成操作。

        乘法运算符:符号*表示乘法。下面语句用2.54乘以inch,并将结果赋给cm。

        cm = 2.54 * inch;

        除法运算符:C使用符号 / 来表示除法。/ 左侧的值是被除数,右侧的值是除数。

        C语言中的除法返回值是除数,取余返回的是余数。并不像数学那般得到小数。并且C语言中只有除法 / 两边能有小数,取余%则不能。5.0000000因为浮点数编译器默认打印六位0,您可以在f前面加.n代表你要保留几位小数。如:%.3f保留三位小数。

这里有三点要注意:

1. 除了 % 操作符之外,其他的几个操作符(+、-、*、/)可以作用于整数和浮点数。

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

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

5.移位操作符

>>  右移操作符

<<  左移操作符

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

(移动的是二进制位的补码

5.1 左移操作符 (<<)

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

左移操作符实际上就是将二进制序列的补码左移一位,舍弃最左边的那一位,然后右边空出的一位补上一个0;但是这个实际上并不会让数据变化,需要用赋值操作符才能保存下来;

#include<stdio.h>
int main()
{
	int a = 15;
	int b = -15;
	int c = 0;
	int d = 0;
	c = a << 1;
	d = b << 1;
	printf("a = %d\n", c);
	printf("b = %d\n", d);
	return 0;
}

结果是这样的:

这里我们简单的计算一下:

5.2 右移操作符(>>)

移位规则:

右移运算分为两种:

  1. 逻辑右移:左边补0,右边丢弃。
  2. 算术右移:左边用原该值的符号位填充,右边丢弃。

注意:1、2两种用哪一个取决于编译器,但大部分是算术右移;

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

逻辑右移1位演示:

算术右移1为演示:

例如:

int num = 10;

//不可以这样写

num>>-1;//error 

6. 位操作符

位操作符有:

1、&     按位与      (有0为0,全1为1)

2、|       按位或      (有1为1,全0为0)

3、^      按位异或   (相同为0,相异为1)

4、~      按位取反   (0变1,1变0,包括符号位)(单目操作符)

注:他们的操作数必须是整数,作用的对象都是二进制补码

        这三个操作符也对符号位有作用, 所以若是正数和负数相&, 得到的一定为正, 正负相 |,一定为负数,正数负数 ^ 也一定是负数。

浅浅的练习一下:

#include<stdio.h>
int main()
{
	int num1 = 1;
	int num2 = 2;
	int num3 = 0;
	num3 = num1& num2;
	printf("%d\n", num3);
	num3 = num1 | num2;
	printf("%d\n", num3);
	num3 = num1 ^ num2;
	printf("%d\n", num3);
	return 0;
}

结果如下:

6.1 一道面试题:

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

笔记:(n^n=0) (任何一个数异或0都等于这个数本身)

a=a^b;

b=a^b;  // 把a带入: b = (a^b)^b = a^(b^b) = a

a=a^b;  //把a, b 一起带入: a = (a^b)^(a) = a^b^a = b

 6.2 练习:

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

几种参考方法:

//方法1
#include<stdio.h>
int main()
{
	int num = 15;
	int count = 0;
	while (num)
	{
		if (num % 2 == 1)
			count++;
		num = num / 2;
	}
	printf("二进制中1的个数 = %d\n", count);
	return 0;
}

输出结果:二进制中1的个数 = 4

思考这样的实现方式有没有问题?

//方法2
#include<stdio.h>
int main()
{
	int num = -1;
	int i = 0;
	int count = 0;
	for (i = 0; i < 32; i++)
	{
		if(num & (1 << i))
			count++;
	}
	printf("二进制中1的个数 = %d\n", count);
	return 0;
}

输出结果:二进制中1的个数 = 32

思考还能不能更加优化,这里必须循环32次。

//方法3
#include<stdio.h>
int main()
{
	int num = 14;
	int count = 0;
	while (num)
	{
		count++;
		num = num & (num - 1);
	}
	printf("二进制中1的个数 = %d\n", count);
	return 0;
}

输出结果:二进制中1的个数 = 3

//这种方式是不是很好?达到了优化的效果,但是难以想到。

7. 赋值操作符

赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。
也就是你可以给自己重新赋值。
具体作用就是 将值赋给变量 ,并且会根据= 左边的符号对数据变量进行操作并且将结果赋给变量
int weight = 120 ; // 体重
weight = 89 ; //重新 赋值
double salary = 10000.0 ;
salary = 20000.0 ; // 使用赋值操作符赋值。
复合赋值符
(1)  += 、-=、/=、%=

赋值操作符 ,就是将一个数赋值赋给变量,从右向左赋值。

#include <stdio.h>
int main()
{
	// +=
	int a = 20;
	a += 10; //a=a+10  
	printf("%d\n", a);//a=30

	// -=
	int b = 30;
	b -= 20; //b=b-20  
	printf("%d\n", b);//b=10

	// *=
	int c = 10;
	c *= 2; // c=c*2  
	printf("%d\n", c);//c=20

	// /=(除等于)
	int d = 50;
	d /= 10; // d=d/10  
	printf("%d\n", d);//d=5

	// %=
	int e = 51;
	e %= 6; // e=e%6 
	printf("%d\n", e);//e=3
}
(2) >>=  、 <<=
#include <stdio.h>
int main()
{
	//<<=
	int a = 10;
	a <<= 2;  //  a=a<<2  a=40
	//a 补码: 00000000 00000000 00000000 00001010
	//a<<2:   00000000 00000000 00000000 00101000
	printf("%d\n", a);

	//>>=
	int b = 20;
	b >>= 2; // b=b>>2 b=5
	//b补码:00000000 00000000 00000000 00010100
	//b>>2: 00000000 00000000 00000000 00000101
	printf("%d\n", b);
	return 0;
}
(3) &=、|=、^=
int main()
{
	// &=
	// &(按位与):有0为0 同1为1
	// a补码:00000000 00000000 00000000 00001010
	// 2补码:00000000 00000000 00000000 00000010
	//a & 2: 00000000 00000000 00000000 00000010
	int a = 10;
	a &= 2; // a=a&2  a=2
	printf("%d\n", a);

	// |=
	// | (按位或):有1为1,同0为0
	// b补码:00000000 00000000 00000000 00010100
	//12补码:00000000 00000000 00000000 00001100
	// b|12: 00000000 00000000 00000000 00011100
	int b = 20;
	b |= 12; // b=b | 12  
	printf("%d\n", b);//b=28

	// ^=
	// ^ 相同为0 相异为1
	// c补码:00000000 00000000 00000000 00011110
	//16补码:00000000 00000000 00000000 00010000
	//c^16 : 00000000 00000000 00000000 00001110
	int c = 30;
	c ^= 16; // c=c^16 
	printf("%d\n", c);//c=14
	return 0;
}

这些运算符都是一样的道理,这样写更加简洁。

8.单目操作符

8.1 单目操作符介绍

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

8.2 !(逻辑反操作)
#include<stdio.h> 
int main()
{
	int a = 0;
	if (!a)
	{
		printf("beautiful\n");
	}
	return 0;
}

输出结果:beautiful

C语言中0表示假(false)非0表示真(true),所以上述程序a=0,!a就是非0。

所以在任意一个表达前面加!号就是把这个表达式取反的意思。

8.3  + (正值)、 - (负值)
#include<stdio.h>
int main()
{
	int a = 5;
	int b = -8;
	printf("%d ", +a);
	printf("%d ", +b);
	printf("%d ", -a);
	printf("%d\n",-b);
	return 0;
}

输出结果:5  -8  -5  8

我们可以看到+(正值)对各个数字没有影响。-(负值)就是把各个数字取反了。

8.4 &(取地址)

&(取地址)见名知意,就是取出这个变量在内存中的地址,如定义一个整形变量,我想知道这个变量的地址这时候就可以用到&操作符。

#include<stdio.h>
 
int main()
{
	int a = 10;
	printf("%p\n", &a);
	return 0;
}

输出: 006AFC54

注意:定义一个变量,这个变量会在内存中随机找一个地址存储。因此每次输出的地址值都不是一样的。当然我们也可以取一个数组的地址,有以下程序:

#include<stdio.h>
int main()
{
	int arry[3] = { 1,2,3,4,5 };
	printf("%p\n", &arry);
	printf("%p\n", &arry[0]);
	return 0;
}

输出:008FF97C
           008FF97C

可见取数组名的地址就是取整个数组的地址,

也可以认为取数组名的地址就是取该数组第一个元素的地址。

演示代码:

#include <stdio.h>
int main()
{
	int a = -10;
	int* p = NULL;
	printf("%d\n", !2);//0
	printf("%d\n", !0);//1
	a = -a;
	p = &a;
	printf("%d\n", sizeof(a));//4
	printf("%d\n", sizeof(int));//4
	printf("%d\n", sizeof a);//这样写行不行? //4
	printf("%d\n", sizeof int);//这样写行不行?/*err不可以这样写*/
	return 0;
}

printf("%d\n", sizeof int); //不可以这样写

8.5 sizeof(取字节操作符)(单位:字节) 

当我们想要知道数据的类型占多少字节时可以用sizeof来求,

当我们想知道数组的长度的时候也可以用sizeof来求。

#include<stdio.h>
int main()
{
    int arry[] = { 1,2,3,4,5,6,7,8,9,10 };
    printf("%d  ",sizeof(int));
    printf("%d\n",sizeof(arry)/sizeof(arry[0]));
    return 0;
}

输出结果:4  10 

整形int在内存中占4个字节可以用sizeof求得,

数组大小可以由sizeof(数组名)/sizeof(数组第一个元素)

如:sizeof(arry)/sizeof(arry[0])。

练习,以下程序的结果是什么:

#include<stdio.h>
int main()
{
    int a = 10;
    short s = 3;
    printf("%d ", sizeof(s = a + 3));
    printf("%d\n", s);
    return 0;
}

输出:2   3 

sizeof里面的表达式的最终结果返回给了s,而sizeof求的就是s的数据类型大小。也就是short的占字节数 。并且最终的s还是不变的,s只是在sizeof的()里面改变,实质上的s还是原来的s。
 8.6 ~(二进制位取反)
来看一组代码:
#include<stdio.h>
int main()
{
    int a = 7;
    printf("%d", ~a);
    return 0;
}

输出结果:-8

计算过程:先进行原、反、补码的运算求出7的补码。

我们知道正数的原、反、补码相同。

补码:00000000 00000000 00000000 00000111

对3的补码取反得到:11111111 11111111 11111111 11111000

可以看出这是一个负数因为符号位为1。

补码:11111111 11111111 11111111 11111000

反码:11111111 11111111 11111111 11110111

原码:10000000 00000000 00000000 00001000

把这一串二进制化为原码为:10000000 00000000 00000000 00000100,所以最后结果为-8。

所以~波浪号是把一个数值的补码全部取反。因此一个正数或负数可能因为这个操作改变原来的值。

8.7 --、++(前后置--、++)

我们经常用到的循环语句中常遇到这两种操作符,来看组代码:

//前置++和--
#include <stdio.h>
int main()
{
	int a = 10;
	int x = ++a;
	//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
	printf("a=%d x=%d\n", a, x);
	int y = --a;
	//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
	printf("a=%d y=%d\n", a, y);
	return 0;
}

输出结果: a=11 x=11
                a=10 y=10

//后置++和--
#include <stdio.h>
int main()
{
	int a = 10;
	int x = a++;
	//先对a先使用,再增加,这样x的值是10;之后a变成11;
	printf("a=%d x=%d\n", a, x);
	int y = a--;
	//先对a先使用,再自减,这样y的值是11;之后a变成10;
	printf("a=%d y=%d\n", a, y);
	return 0;
}

 输出结果:a=11 x=10
                   a=10 y=11

x=++a时,a先自加了然后再把自加后的值赋值给x,此时运算顺序为先a=11,x=11;

y=--a时,a先自减然后再把自减后的值赋值给y,此时运算顺序为a=10,y=10;

x=a++时,a先把自己赋值给x然后再自增,此时运算顺序为先a=11,x=10;

y=a--时,a先把自己赋值给y然后再自增,此时运算顺序为a=10,y=11;

也就是当--或++在前面时,先进行自减或自增并且把自减或自增的值赋值给前者变量。

当--或++在后面时,先不自减或自增把自身赋值给前者后,再进行自减或自增。以上就是位置导致的赋值优先级。

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

这里的 * 号并不是乘号而是,间接访问的一个符号。通常在指针中我们用到这个符号,我们来看一组代码:

#include<stdio.h>
int main()
{
	int arr[3] = { 1,2,3 };
	int* p1 = arr;
	int* p2 = &arr[2];
	printf("%d ", *p1); // 1
	printf("%d ", *p2); // 3
	int a = 10;
	int* p3 = &a;
	*p3 = 20;
	printf("%d\n", a); // 20
	return 0;
}

输出结果:1  3  20 

int* p1=arr,实际上p1指向的是arr数组的首地址,再对p1解引用也就是*p1得到的值就是p1指向的arr数组首地址的值;

int* p2=arr[2],实际上p2指向的是arr数组的第三个地址因为数组下标从0开始的,再对p2解引用也就是*p2得到值就是p2指向的arry数组第三个元素的值 ;

int* p3=&a,就是p3指向a的地址,再把20赋值给解引用后的p3也就是*p3。因此原来a的地址里面的值被指针p3改变了。

以上就是*(解引用)的基本操作.

8.9  sizeof和数组

我们来看一组代码:

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

 输出结果:40 10  4   4 

我们很容易看到(1)、(2)输出的是两个数组的总字节数。

(1)中10个int类型就是40个字节,(2)中10个char类型就是10个字节。

那为啥 (3)、(4) 输出的各是4呢,原因是数组在传参的时候传给形参的是数组的首地址 。并且数组在传参的时候可以是数组名也可以是指针,但有一点无论你传过去的是数组名亦或者是指针,形参都认为这是一个指针类型。指针类型占4个字节,无论是指针的char类型或者int类型甚至是double类型我们都按照4个字节来算,因此 (3) 、(4) 输出4。

x86->32位->4个字节, x64->8个字节。

9. 关系操作符

关系操作符有这些:>(大于)、>=(大于等于)、<(小于)、<=(小于等于)、!=(不等于)  、==  (等于),相信大家都已经见过这些操作符了,我们来看一组代码:

#include<stdio.h>
int main()
{
    int a = 5;
    int b = 9;
    if (a > b)
        printf("a大于b\n");
    if (a < b)
        printf("a小于b\n");
    return 0;
}

输出结果:a小于b 

关系操作符常用在if判断语句中,它与数学表达的意思一样,只是写法不同。关系操作符判断的关系成立时返回是真,否则返回假。这与布尔类型(true和false)保持一致。注意,C语言中的等于是==而不是=,C语言中的=号是赋值的意思,==是等于的意思

10.逻辑操作符

10.1  &&、||(逻辑与、逻辑或)

逻辑操作符有:&&、||。&&表示的是并且的意思,|| 表示的是或者的意思。

区分逻辑与按位与

区分逻辑或按位或

逻辑操作符只关注真假。

按位与、按位或针对的是二进制位的。

逻辑与和或的特点:

&& - 左操作数为假,右边不计算;

 ||   - 左操作数为真,右边不计算;

我们就拿求闰年来做讲解:

#include<stdio.h>
int main()
{
    int year = 2400;
    if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
    {
        printf("这是一个闰年\n");
    }
    return 0;
}

输出结果:这是一个闰年 

闰年的满足条件是能被4整数但不能被100整除或者能被400整除,这样的类型就可以用到逻辑操作符,并且&&和或者||。注意,&&是两边表达式都成立时才为真,|| 是两边表达式只要有一方成立时就为真。

一道笔试题,程序的输出结果是什么:

#include <stdio.h>
int main()
{
  int i = 0,a=0,b=2,c =3,d=4;
  i = a++ && ++b && d++;
  printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
  i = a++||++b||d++;
  printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
  return 0;
}

输出结果:

a=1 b=2 c=3 d=4
a=2 b=2 c=3 d=4 

i=a++ && ++b && d++; 先拿a&&++b,此时(a&&++b)表达式已经为0因此++b不用执行,(a++&&++b)也为0,因此d++也不执行了。所以最后只有a自增了一次,其余的bcd都保持不变。

i=a++||++b||d++;先拿a与++b做||或运算,因为a非0因此a++为1因此(a++||++b)整个表达式为1,所以++b也不用运算,随之后面的d++也不用运算。

以上的题目就体现出了,&&是两边表达式都成立时才为真,||是两边表达式只要有一方成立时就为真。从侧面可以这样理解,&&两边表达式只要有一边为假整个表达式返回假另一个表达式不用判断了,||两边表达式只要有一边为真整个表达式返回真另一个表达式也不用判断了。

11. 条件操作符

11.1  ?:三目操作符

条件操作符:exp1 ? exp2 : exp3。三个表达式组成

#include<stdio.h>
int main()
{
    int a = 5;
    int b = 3;
    int max = 0;
    max = a > b ? a : b;
    printf("%d ", max);
    max = a < b ? b : a;
    printf("%d\n", max);
    return 0;
}

的操作,因此我们称为的三目运算符

我们来看一个程序,求两数之间的较大值:
输出结果:5 5 

可见 '?' 号操作符是做一个判断作用的跟if语句一样返回的是真或假。':' 号操作符是做一个选择作用,当 '?' 号返回的是真就执行 ':' 号前面的表达式,否则就执行 ':' 号后的表达式。以上程序我们也可以写成这个样子:

#include<stdio.h>
int main()
{
    int a = 5;
    int b = 3;
    int max = 0;
    if (a > b)
    {
        max = a;
    }
    else
    {
        max = b;
    }
    return 0;
}

12. 逗号表达式

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

//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1); 

//结果:13
//代码2
if (a = b + 1, c = a / 2, d > 0)
 
代码1输出的结果是13,尽管c = (a > b, a = b + 10, a, b = a + 1)的结果是最后一个表达式b=a+1的值,但在此之前的所有表达式都进行了运算。

代码2,if语句里面判断是最后一个逗号后面的表达式d>0。在此之前的表达式没有起到任何作用。

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

13.1 []下标引用操作符

[]下标引用操作符就是我们数组中的[],那么[]的两边是有操作数的。

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

我们来看一组代码:

#include<stdio.h>
int main()
{
    int arr[3] = { 1,2,3 };
    arr[2];
    return 0;
}

首先定义了一个整形数组arry有三个元素。那么arr[3]中的arr和3就是[]下标引用操作符前后的两个操作数。arr[2]也是如此,arr和2是[]的两个操作数。通过这一个操作可以找到该数组下标为几的地址里面存在的元素。 

最常见的应该就是定义一个数组,然后打印这个数组,我们用for循环来遍历这个数组,有以下程序:

#include<stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9 };
    int n = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

输出结果:1 2 3 4 5 6 7 8 9 

我们遍历是从0到n-1结束,正好对应了数组的下标。这就是[]下标访问符的作用。 

13.2 ()函数调用操作符

函数调用操作符就是(),当我们创建一个函数的时候通过()来运算,我们来看一组代码:

#include<stdio.h>
#include<string.h>
int main()
{
    int len = strlen("abcdef");
    printf("%d\n", len);
    return 0;
}

输出结果:6 

函数调用操作符()的操作数是什么呢,拿上述程序来说。()左边的strlen是一个操作数,右边的"abcdef"也是操作数,只不过我们称"abcdef"为参数。当()的操作数只有一个的时候,代表这个函数没有任何的参数如strlen();也就是()里面没有参数;

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

#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;
}
13.3 . 、->结构体访问操作符

结构体访问操作符有两个一个是.(点号),->(箭头),

点号是用来访问成员,->也是用来访问成员的。

 . 为结构体.成员名
->为结构体指针->成员名
我们来看一组程序:

#include<stdio.h>
struct student
{
    char name[20];
    int age;
    char sex[5];
    int score;
};
int main()
{
    struct student stu1= { "李四",33,"男",66};
    printf("%s %d %s %d\n", stu1.name, stu1.age, stu1.sex, stu1.score);
    struct student* p = &stu1;
    printf("%s %d %s %d\n", (*p).name, (*p).age, (*p).sex, (*p).score);
    printf("%s %d %s %d\n", p->name, p->age, p->sex, p->score);
    return 0;
}

输出结果:

                李四  33  男  66 

                李四  33  男  66 

                李四  33  男  66 

上述程序中,自定义了一个结构体变量student里面成员变量有四个,和结构体student变量stu1。那么我们可以通过stu1来.号成员变量 ,或者把stu1的地址给一个结构体student指针变量p。p来.号成员变量也可以得到该成员变量,前提是对p进行解引用。

我们也可以用p->箭头来访问成员变量,既然p指向了stu1的地址。那么p指向stu1里面的成员的地址也可以得到该成员的值。

14. 表达式求值

表达式的求值的顺序一部分是按照操作符的优先级和结合性决定的,同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型我们称为强制转换。我们来看一组程序:

#include<stdio.h>
int main()
{
    int a = 3;
    int b = 4;
    int c = a + a * b + b;
    int d = (int)3.14;
    printf("%d ", c);
    printf("%d\n", d);
    return 0;
}

输出结果:19 3

以上代码中c=a+a*b+b;中*号表达式优先级大于+号,所以是先a*b得到12然后再进行+号运算最终得到19。

int d=(int)3.14;中我希望d是一个整形的值,但是我赋值给d的是一个浮点型3.14。那么这时候我们就可以用强制类型转换把3.14强转为整形,如下图所示: 

上述代码中c=a+a*b+b;是先乘后加,那么我们想要先加后乘怎么做呢。我们可以把两个加法用()号括起来,()号的优先级是大于*号的,我们来看代码:

#include<stdio.h>
int main()
{
    int a = 3;
    int b = 4;
    int c = (a + a) * (b + b);
    printf("%d\n", c);
    return 0;
}

 输出结果:48

因此,我们想要表达式的顺序改变,可以将某一块代码用优先级高的操作符引起来,比如说()号。它是所有表达式中优先级最高的。

14.1 隐式类型转换
        C的整型算术运算总是至少以缺省整型类型的精度来进行的。
        为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型 提升。
整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度  一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
// 实例 1
char a , b , c ;
...
a = b + c ;
b c 的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的。
// 负数的整形提升
char c1 = - 1 ;
变量 c1 的二进制位 ( 补码 ) 中只有 8 个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候, 高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
// 正数的整形提升
char c2 = 1 ;
变量 c2 的二进制位 ( 补码 ) 中只有 8 个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为 0
提升之后的结果是:
00000000000000000000000000000001
// 无符号整形提升,高位补 0
整形提升的例子 :
//实例1
#include<stdio.h>
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;
}//c
实例 1 中的 a,b 要进行整形提升 , 但是 c 不需要整形提升
a,b 整形提升之后 , 变成了负数 , 所以表达式 a==0xb6 , b==0xb600 的结果是假 , 但是 c 不发生整形提升 , 则表
达式 c==0xb6000000 的结果是真 .
所程序输出的结果是  :   c 
                     
// 实例 2
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 ;
}
实例 2 中的 ,c 只要参与表达式运算 , 就会发生整形提升 , 表达式 +c , 就会发生提升 , 所以 sizeof(+c) 4 个字节.
表达式 - c 也会发生整形提升 , 所以 sizeof( - c) 4 个字节 , 但是 sizeof(c) , 就是 1 个字节 .
14.2 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换
long double
double
float
unsigned long int
long int
unsigned int
int
自下而上,进行算术转换
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题
float f = 3.14 ;
int num = f ; // 隐式转换,会有精度丢失
14.3 操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级

多个相邻操作符混合计算时,我们就得考虑操作符的优先级

优先级高的先计算

2. 操作符的结合性
有些操作符是具有结合性的既遵从从左向右计算,也遵从特殊的两个先结合计算
3. 是否控制求值顺序。

有些操作符是控制求值顺序的

像,逗号表达式、三目运算符、逻辑与、逻辑或,都是控制求值顺序的,从左向右依次执行

两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
操作符优先级表格(表格在本文末尾)
下表中从上到下优先级是由高到低的,结合性中的N/A是没有结合性,L-R是从左往右,R-L是从右往左。是否控制求值顺序意思什么呢,比如&&只要两边表达式有一个为假。整个表达式为假,那么&&它就是控制求值顺序的。
一些有问题的表达式
//代码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

代码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 ;
注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
代码2同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
//代码3-非法表达式
int main()
{
   int i = 10;
   i = i-- - --i*(i=-3)*i++ + ++i;
   printf("i=%d\n",i);
   return 0;
}

代码3在不同的编译器中测试的结果是不同的,如下表所示:

值    编译器
-128   Tandy 6000 Xenix 3.2
-95  Tink C 5.02(Macintosh)
-86 IBM PowerPC AIX 3.2.5
-85  Sun Sparc cc(K&C编译器)
-63   gcc,HP_UX9.0,Power C 2.0.0
//代码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编译器,VS2013环境下都执行,看结果。
Linux 环境的结果:

VS2013环境的结果:

看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码 . 就可以分析清楚 .
这段代码中的第一个 + 在执行的时候,第三个 ++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第
三个前置 ++ 的先后顺序。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

看图参考:

 

以上就是我分享的操作符笔记,感谢观看。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

dream wings

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

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

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

打赏作者

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

抵扣说明:

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

余额充值