Bootstrap

一次带你理清 ‘ || ’ 和 ‘ && ’ 和 ‘ ^ ’ 等常用操作符以及其余基本操作符

1.操作符分类

1.算数操作符

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

例如:

int a=5;
int b=4;
int c=5/3   --结果为1  除法结果为取商
//   int c=5/3.0  --   此时,由于除号两边有一边为浮点数,那么结果就为浮点
                    // 型-- 1.25
int d=a%b;   --11  因此 d= 1
//  取模的结果是余数,且取模符号 %两边都只能是整形
注意:  d的结果一定是再 0<=d<b 的
若 int d=a%b +1 ,则结果为1~b 之间

2.位移操作符

 <<   左移操作符
 >>   右移操作符

对于位移操作符,操作对象为二进制位的补码并且只能是整数

1. << 左移操作符:左边丢弃,右边补0

例如:
int a=5;
正整数原码 反码 补码相同–再内存中存储的为补码

00000000000000000000000000000101 – a的补码
此时 a<<1 意味将a向左移动 1 位
在这里插入图片描述

2.右移操作符:右移操作符相比左移操作符规则上有所不同

 分为两种:
1.逻辑位移:左边用0填充,右边丢弃
2.算数位移:左边用原该值的符号位(最高位)填充,右边丢弃

例如:
int a=5
a再内存中存储的是补码
00000000000000000000000000000101 --补码

a>>1
若为逻辑位移
在这里插入图片描述
若为算数位移:
对于正数而言,最高位符号位位0,算数位移和逻辑位移结果一致。

若为负数,int a= -5;再内存中为补码
10000000000000000000000000000101–原码

符号位(最高位不变)其他位按位取反(1为0,0为1)

111111111111111111111111111111111010–反码

反码加1为补码

111111111111111111111111111111111110–补码

若为逻辑位移
a>>1
在这里插入图片描述
若为算数位移:
111111111111111111111111111111111110-- -1 的补码
在这里插入图片描述
不难发现:正数的逻辑位移和算数位移是结果相同的,但是负数的逻辑位移和算数位移不一样,应当注意。

那么对于负数的右移到底是算数位移还是逻辑位移呢?
在不同的编译器上采取的处理方式不一样,为其中的一种,但多数都是算数位移

3.位操作符

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

注意:它们之间操作的都是整数且操作的是二进制位的补码

1. 按位与(&)

运算规则:两个整数对应的二进制位同时为1,结果才为1,否则为0。
int a=5;
int b=1;
int c=a&b; c的结果是多少呢?
a再内存中存储的是二进制补码

a&b:
00000000000000000000000000000101   --  a 补码
00000000000000000000000000000001   --  b 补码

有一个0则为0,只有同为1才为1

00000000000000000000000000000001  -- c 的补码
由于正整数的原码 反码 补码相同,打印时打印的是原码的结果
因此 c 结果为5

对于含负数的按位或:
int a=5;
int b=-2;
int c=a&b;

00000000000000000000000000000101  -- a 补码

10000000000000000000000000000010  -- b 原码

11111111111111111111111111111101  -- b 反码

11111111111111111111111111111110  -- b 补码  

a&b:

00000000000000000000000000000101  -- a 补码

11111111111111111111111111111110  -- b 补码

有一个0则为0,只有同为1才为1

00000000000000000000000000000100  -- c 的补码
由于打印时打印的是原码,因此 c 结果为 4

在这你有没有发现 一个正整数& 1 结果为它本身?
那么 我们就可以通过前面的位移操作符和 按位与结合,获取一个整数二进制中的每一位数
例如:获取 5 二进制中的每一位数

forint i;i<32;i++);
{
int c=5>>i)&1}

2.按位或( | )

运算规则:两个整数对应的二进制位有一个1则为1,只有同为0才为0
int a=5;
int b=2;
int c=a&b; c的结果是多少呢?

 a | b:
00000000000000000000000000000101   --  a 补码
00000000000000000000000000000010   --  b 补码

有一个1则为1,只有同为0才为0

00000000000000000000000000000111  -- c 补码

打印时,打印的是原码,因此 c 的 结果为 7

对于含负数的按位或:
int a=5;
int b=-2;
int c=a|b;

00000000000000000000000000000101  -- a 补码

10000000000000000000000000000010  -- b 原码

11111111111111111111111111111101  -- b 反码

11111111111111111111111111111110  -- b 补码

a|b:

00000000000000000000000000000101  -- a 补码

11111111111111111111111111111110  -- b 补码

有一个1则为1,只有同为0才为0

11111111111111111111111111111111  -- c 补码

打印时,打印的是原码,因此负数的补码还需要还原为原码

11111111111111111111111111111111  -- c 补码
补码-1 得到反码
11111111111111111111111111111110  -- c 反码
反码符号位不变,其余按位取反
10000000000000000000000000000001  -- c 原码
所以 c 的结果为 -1

3.按位异或(^)

运算规则:两个整数对应的二进制位相同,则结果为0,否则为1
int a=5;
int b=3;
int c=a^b;

000000000000000000000000000000101  -- a 补码

000000000000000000000000000000011  -- b 补码

a^b:

000000000000000000000000000000101  -- a 补码

000000000000000000000000000000011  -- b 补码

相同为0,相异为1

00000000000000000000000000000110  -- c 原码
打印c 的结果为 6

对于含负数的按位与:
int a=5;
int b= -3;
int c=a^b;

000000000000000000000000000000101  -- a 补码

100000000000000000000000000000011  -- b 原码
111111111111111111111111111111100  -- b 反码
111111111111111111111111111111101  -- b 补码

a^b:

000000000000000000000000000000101  -- a 补码
111111111111111111111111111111101  -- b 补码

相同为0,相异为1

111111111111111111111111111111000  -- c 补码

打印出 c 的结果为 -8

对于按位异或,我们可以从下面的代码发现如下规律:

0^0 = 0;
1^0 = 1; //0异或一个数 = 这个数
0^1 = 1; //异或具有交换律
1^1 = 0;//自己异或自己 = 把自己置为0

再看一道经典例题:

实现两个值的交换,而不必使用临时变量。
    例如交换两个整数a=5,b=3的值,可通过下列语句实现:
    a = a^b;   //
    b = b^a;   //b=a^b^b = a^0 = a
    a = a^b;   //a=a^b^^a = a^a^b = 0^b = b

利用我们上面的规则,很容易就将两个数交换值,并且这样不会存在溢出的问题

其次,还可以利用按位与快速判断两个整数是否相等

int a=5int b=4if(a^b==0) //根据我们上述发现的规律可以知道
            //如果a b相等,那么 a^b一定为0
 a=1;
 else
 a=2;

4.复合操作符

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

对于上述操作符,都是由两个操作符复合组成的,那应该如何理解呢?
例如: +=,就是加上一个数后 = 什么
int a=2;
a+=2 // a先加再赋值,最后a变成了4;
同理 a-=2;//此时a先减,最后再赋值变成里 0
后面的 *= /= >>= … 均是如此。

5.单目操作符

!           逻辑反操作-           
-           负值
+           正
&           取地址
sizeof      操作数的类型长度(以字节为单位)
~           对一个数的二进制按位取反
--          前置、后置--
++          前置、后置++
*           间接访问操作符(解引用操作符) 
(类型)      强制类型转换
1.逻辑反操作符
int flag=1if!flag)// 等同于 if(0)
printf("hehe");
这里会输出 呵呵 嘛?答案是不会的
!为逻辑反操作符,意味真的变成假的,假的变成真的。

需要注意的是,这里的真假并非直接指得是对错问题,计算机中真用非0数值表示,假则用0表示

2.正、负值

对于正负值需要注意一个点

int a=-5int b=+a;//+a不会对a有任何影响  +a = a
int c=-a  //负号和我们算数中一样会对一个数产生影响 此时c = 5
3.取地址
#include<stdio.h>
int main()
{
	int a = 5;
	printf("%p\n", &a);//取出a这个值存储再内存中的地址
}

注意,每一次去取这个地址的时候都不一样,因为每次编译器给这个数开辟空间存储时都是随机分配的地址
在这里插入图片描述

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

对于sizeof,想必大家再使用其求解数组中的个数经常使用到,不会太陌生,那我们就来探讨一下它的其它特点。
在这里插入图片描述
我们都知道sizeof可以求一个数组、常变量、类型等再内存中所占空间大小
sizeof也有许多形式,例如上图。

注意的时,你是不是也发现最有一个sizeof求解编译器报错了?这种写法需要注意,是不支持的。

其次,我们发现第一种写法和第三种写法居然是结果一样的,对于sizeof来说,你是不是认为它是一个函数呢? 类似 strlen 这个库函数一样,但从上面的结果来看 不是的
由于函数例如 strlen() 这个括号便是函数的操作符,是不能省略的,而siziof 却可以省略,因此足以说明sizeof是一个操作符,并非一个函数

最后,我们再来说一说sizeof计算内存大小。

sizeof计算规则:返回所有的数所占内存包括 \0

#include<stdio.h>
#include<string.h>
int main()
{
	int a[]={1,2,3,4,5};
	printf("%d\n", sizeof(a));//  20
	int 类型占四个字节,一共有5个元素,因此在内存中占20 byte
	
	char b[] = { 1,2,3,4, 5}printf("%d\n", sizeof(b));//  5
	char 类型占一个字节,一共五个元素,因此在内存中占 5 byte
	
	int c[]="abcd";
    printf("%d\n",sizeof(c)); // 5
    abcd \0,这个字符串后面有一个\0,用sizeof计算内存大小,\0也会被计算进去
    
}

特别需要注意的是:

对于sizeof计算类型来说 无论是X86(32位) 还是X86(65位)下
结果是一样的,每个类型所占的空间大小是规定好的

#include<stdio.h>
int main()
{
    printf("%zu\n", sizeof(int));  // 4
    printf("%zu\n", sizeof(char)); // 1
    printf("%zu\n", sizeof(double));// 8
    printf("%zu\n", sizeof(float));// 4
    return 0;
}

但是 对于指针类型来说,由于指针变量的实质就是地址,在不同位的平台下不一样:
X86(32位)下:
在这里插入图片描述
X64(64位)下:
在这里插入图片描述

在这,相必有一些分不清sizeof和strlen的朋友,在此也说一说strlen

strlen的计算规则:读到 \0 则停止,返回读到\0 之前的字符长度

#include<stdio.h>
#include<string.h>
int main()
{
	
	char a[] = "abcd";
	对于字符串,它的结尾有一个 \0
	printf("%d\n", strlen(a));  // 因此结果为 4
	
    char b[] = { 1,2,3,4,5 };
    对于这个数组而言,当读到 5 过后,编译器并不知道这个数组里 \0 再何处
    只能一直往下读取,直到\0出现才返回之前的字符长度,因此是随机值
    printf("%d\n", strlen(b));  // 随机值

	char c[] = { 1,2,3,'\0',4,5 };
	原本这个数组的字符串长度是一个随机值,但是由于我们手动往里填加了\0,
	因此,再读到第三位过后就遇到了\0,返回之前的三个数的字符长度,结果为3
    printf("%d\n", strlen(c));  // 3
}
6. - - 的前置和后置 以及 ++ 的前置和后置

对于前置++或者前置 - - 而言:先对数进行自增(自减),然后对使用该数,也就是表达式的值是该数自增(自减)之后的值。

#include <stdio.h>
int main()
{
    int a = 10;
    int x = ++a;
    //先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
    int y = --a;
    //先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
    return 0; }
    
    上述代码如果理解不清楚,可以按如下理解
    int a=10//int x=++a;
    可以写成
    a=a+1;此时 a=11;
    int x=a; //x=11
   同理:
    //int =y=--a;可以写成
    a=a-1;此时a使用的是上面++a后a的值 -- a=10;
    int y=a;// y=10

对于后置++或者 - - 而言:先对该数使用,在对该数进行自增(自减)

    int a=10;
    int x=a++;
    //先使用a,然后再自增。先使用a时,x=10;之后自增,a=11;
    int y=a--;
    //先使用a,然后自增。先使用a时,由于上面++a后的影响,现在a=11;
    //先使用a,则y=11;在自减,a=10;
    
    若对上述代码理解不清楚,可以按如下理解
    int a=10;
    //int x=a++;
    可以写成
    int x=a; //x=10;
    a=a+1; //a=11;
    同理:
    //int y=a--;
    int y=a;//由于上述a++的影响,此时a已经变成了11
            //先使用a,则y=11;
    a=a-1;  //a=10;
7. * 间接访问操作符(解引用操作符)

对于 * 这个操作符,我们常见于指针当众,因此我们可以大抵将它和指针变量认为是一对

#include<stdio.h>
int main()
{
    int a = 10;
    int* p = &a;//p当中存储的是a的地址
    p = 0;//由于p中存的是a的地址,改变P的值,也就间接的改变了a地址里的值
    printf("%d", a);
    return 0;
}

在这里插入图片描述
又或者在数组传参当中:

void print(int* arr)
{
    printf("%d", *(arr+1));
       //arr相当于首元素地址,+1 向后跳过一个整形找到第二个元素
       //等同于arr[1],所以打印的结果应该是2
}

#include<stdio.h>
int main()
{
    int arr[5] = { 1,2,3,4,5 };
    print(arr); //数组传参传的是首元素地址
    return 0;
}

在这里插入图片描述

8. (类型) 强制类型转换

对于强制类型转换,可以将一些你不需要的类型强制转换为你需要的类型,一般使用在一些表达式的返回值可能不是我们需要的时候,将其强制类型转换。

例如:
我们常使用的随机值

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
    srand((unsigned int) time(NULL));
    //随机数的起点设置我们用时间戳来表示--time函数,它的返回类型是time_t
    //因此我们需要将它强制类型转换成和X的类型一致。
    int x = rand();//使用随机数时,需要设置一个随机数的生成起点
    printf("%d", x);
    return 0;
}

4.关系操作符

=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”

对于上述关系操作符中,需要注意的是 判断是否相等的操作符 == ,在使用
时需要注意不要和赋值号写混

5.逻辑操作符

&&     逻辑与
||     逻辑或

对于逻辑与:两个条件都是真结果就是真,期中一个为假那么结果就是假

选自360笔试题:

#include <stdio.h>
int main()
{
    int i = 0,a=0,b=2,c =3,d=4;
    
    (表达式)&&(表达式)这是&&的使用形式,因此我们在这不需要考虑
    前置++和后置++的优先级比&&高的问题。他们都是表达式的情况下,优先级
    相同,此时只需要按照&&的结合方式从左往右结合。
    
    i = a++ && ++b && d++;
    
    根据上面介绍的前置后置++的用法
    a先使用在++,a=0  我们知道,对于表达式结果为0,则是一个假命题,&&只要
    有一个假则结果为假,那么a++&&++b =0 同理 0&&d++=0 ,因此++b 和d++ 
    表达式都没有执行
    
    此时 a=1 b=2 c=3 d=4
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}

从上我们可以得到结论:对于 && 而言,如果表达式一边为假,另一边则不执行

对于逻辑或:一个条件为真,则结果为真,两个条件都是假则结果为假

#include <stdio.h>
int main()
{
    int i = 0,a=0,b=2,c =3,d=4;
    同样,||&&一样,是表达式之间的使用形式,在这也不用考虑优先级
    
    i = a++||++b||d++;
    
    a先使用,后++,a=0,对于||,两边为假结果才为假,还需要看表达式++b;
    b先++ 后使用,b=3;则a++||++b结果为真
    那么,a++||++b||d++,前面为真,无论d++ 是否为真结果一定是真,因此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);
    return 0; 
}

从上我们可以得到结论:对于 || 而言,如果表达式一边为真,则结果一定为真,另一边的表达式则不执行

6.条件操作符

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

可以理解为表达式1成立吗?成立执行表达式2,不成立执行表达式3.。
由于他有三个操作数,它也是一个三木操作符。

例如下面这个代码:

#include<stidio.h>
int main()
{
   int a=1;
   if(a>2)
     a=1;
   else
     a=-1;
   return 0;
}


更改为条件表达式则为
#include<stdio.h>
int main()
{ 
   int a=1;
   (a>2)?(a=1):(a=-1);
   return 0;
}

例如我们比较两个数的大小:

#include<stdio.h>
int main()
{
  int a=1int b=2int max=(a>b)?a:b;
  //通过一个条件操作符,我们很容易就能得出两个数的大小关系
  printf("%d",max);
  return 0;
}

7.逗号表达式

表达式1, 表达式2, 表达式3, …表达式N

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

int a = 1;
int b = 2;
if (a =b + 1, c=a / 2, d > 0)
可以看到if()里面是一个逗号表达式,最终结果为最有一个表达式结果
等同于if(d>0

需要注意的是,虽然逗号表达式最终结果为最后一个表达式的结果,但是前面的表达式也有可能影响最后表达式的结果

例如:

int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
括号里面是逗号表达式,但是c并不是直接为b=a+1=2;
逗号表达式从左往右依次执行中,由于前面的表达式中的值影响了最后a的值,
因此最后结果并不直接为2,a=b+10=12, b=a+1=12+1=13;
最终结果为13

8.下标引用

[ ]  下标引用操作符
#include<stdio.h>
int main()
{
   int arr[5]={1,2,3,4,5};
   printf("%d",arr[4]);
   这里的arr[4]就是下标引用操作符
   对于[]这个操作符,arr是它操作的数组名,4是它的索引值
   return 0;
}

9.函数调用操作符

()  函数调用符

可以有一个或多个操作数

int Add(int x, int y)
{
	return x + y;
}

#include<stdio.h>
int main()
{
	int a = 1;
	int b = 2;
	int c = Add(a, b);
	对于Add这个函数来说,函数调用操作符(),它的操作数首先是函数名Add
	其次是里面的传给函数的参数,a和b
	对于操作的参数可以是多个
	printf("%d", c);
}

10.访问一个结构的成员

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

1. 结构体.成员名:

#include<stdio.h>
#include<string.h>
struct Book //定义一个书的结构体
{
	char name[20]; //这本书构成的内容包括书名
	short price;  // 价格
};

int main()
{
	struct Book b1 = { "C语言程序设计",55 };
	//结构体中 b1这本书规定它的书名和价格
	b1.price = 55;//规定价格
	strcpy(b1.name, "C语言程序设计");//规定书名

当我们b1 这本书已经构建好以后,我们该怎么获取这本书里的价格和书名信息呢?
通过 结构体.成员名的形式
   b1 -- 结构体
   prince和name 都是成员名
	printf("价格:%d\n", b1.price);
	printf("书名:%s\n", b1.name);
    return 0;
}

在这里插入图片描述

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

#include<stdio.h>
#include<string.h>
struct Book
{
	char name[20];
	short price;
};

int main()
{
	struct Book b1 = { "C语言程序设计",55 };
	b1.price = 55;
	strcpy(b1.name, "C语言程序设计");
	struct Book* pb = &b1;//获取结构体中 b1这本书的地址
	printf("书名:%s\n", (*pb).name);    // . 结构体变量.成员
	printf("价格:%d\n", (*pb).price);
	由于*(pb)存放了b1这个接头体的地址,因此*(pb)也可以认为是一个结构体变量,同理它也可以用上述方式访问里面的成员
	
	printf("书名:%s\n", pb->name);       // -> 结构体指针 ->成员
	printf("价格:%d\n", pb->price);
	pb是一个指针,可以由指针-> 直接访问结构体成员
    return 0;
}

在这里插入图片描述

2.总结

以上便是我们基本的操作符,理解清楚操作符将会对我们写代码带来很多方便。
在这里插入图片描述

;