1.操作符分类
1.算数操作符
+ - * / %
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
例如:
int a=5;
int b=4;
int c=5/3 --结果为1 除法结果为取商
// int c=5/3.0 -- 此时,由于除号两边有一边为浮点数,那么结果就为浮点
// 型-- 1.25
int d=a%b; -- 商 1 余1 因此 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 二进制中的每一位数
for(int 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=5;
int b=4;
if(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=1;
if(!flag)// 等同于 if(0)
printf("hehe");
这里会输出 呵呵 嘛?答案是不会的
!为逻辑反操作符,意味真的变成假的,假的变成真的。
需要注意的是,这里的真假并非直接指得是对错问题,计算机中真用非0数值表示,假则用0表示
2.正、负值
对于正负值需要注意一个点
int a=-5;
int 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=1;
int b=2;
int 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.总结
以上便是我们基本的操作符,理解清楚操作符将会对我们写代码带来很多方便。