操作符详解
文章目录
- 操作符详解
- 1.操作符分类
- 2.算数操作符
- 3.移位操作符
- 3.1整型二进制的表示
- 3.1.1整数二进制的种类
- 3.1.2二进制原码反码补码的表示
- 3.2移位运算符使用规则
- 3.2.1正数的左移运算符
- 3.2.2负数的左移运算符
- 3.2.3右移操作符
- 3.2.3.1右移运算符的两种形式:
- 3.2.3.2怎么判断是算数右移还是逻辑右移
- 4.位操作符
- 4.1异或(^)的特点
- 4.2计算一个整数换算为二进制时1的个数
- 5.赋值操作符
- 5.1复合赋值符
- 6.单目操作符
- 6.1单目操作符简单介绍
- 6.2单目操作符与双目操作符
- 6.3"!"操作符
- 6.4"&"操作符
- 6.5"sizeof"操作符
- 6.6"~"操作符
- 6.7"++"和"--"的前置与后置
- 6.8"*"操作符
- 6.9强制类型转换
- 6.10"sizof"操作符
- 7.关系操作符
- 7.1"=="操作符
- 8.逻辑操作符
- 9.条件操作符
- 10.逗号表达式
- 11下标引用、函数调用和结构成员
- 11.1下标引用操作符"[]"
- 11.1.1下标引用操作符"[]"的其他用法
- 11.2函数调用操作符"()"
- 11.3结构体
- 12.表达式求值
- 12.1运算符的优先级
- 12.2隐式类型转换
- 12.2.1整型提升的意义
- 12.3算数转换
1.操作符分类
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用,函数调用和结构成员
2.算数操作符
+ - * /
其他的运算符就正常的该++,该–。
除(/):简单来说就是去余取整(去掉余数部分取整数部分)
整型的除法 1/2 ——> 0 就是去余取整(去掉余数部分取整数部分)
浮点型的除法 1.0/2 ——> 0.5 除数或被除数有一个是0,那么结果就可以是浮点型
取模(%):简单来说就是去整取余,取模操作符的两端必须是整型
7%2 ——> 1//7/2...1
3.移位操作符
<< 左移位操作符
>> 右移位操作符
注:移位操作符的操作数只能是整数。
使用的对象:移位操作符,移动的是二进制
警告:对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num >> -1;//error
3.1整型二进制的表示
3.1.1整数二进制的种类
整数的二进制表示有3种
原码 反码 补码
正的整数的原码,反码,补码相同
负的整数的原码,反码,补码是要计算的
3.1.2二进制原码反码补码的表示
以7为例(7是整数,单位是字节)
7的二进制是111
正的整数
00000000000000000000000000000111
最高位是0表示正数,表示7 -原码 -反码 -补码
负的整数
10000000000000000000000000000111
最高位是1表示负数,表示-7 -原码
1111111111111111111111111111000
-7 -反码(原码的符号位(最高位)不变,其他位按位取反就是反码)
1111111111111111111111111111001
-7 -补码 (反码+1就是补码)
电脑在内存中存储的是补码
3.2移位运算符使用规则
3.2.1正数的左移运算符
“左边丢弃,右边补0”
int a = 7;
int b = a << 1;// 14
printf("%d\n",a);
prnttf("%d",b);
//输出: 7
// 14
由输出结果可得,a数值没有改变,被赋值的b数值变了。与b = a + 1;的理解相似。
如图所示:
把a向左移动一位,就是把从这个状态
移动到这个状态
移位前是111也就是7,移位后1110也就是14
3.2.2负数的左移运算符
实行移动操作是对这个整数的补码进行操作的。
-7的补码为1111111111111111111111111111001
int a = -7;
int b = a << 1;// -14
printf("%d\n",a);
prnttf("%d",b);
//输出: -7
// -14
如图所示:
-7的补码
左移动1位后(<<1)
移动后b里面的补码是
如果需要看出补码的大小,就需要计算一下:
补码 = 原码——>反码——>反码+1,所以原码 = 补码-1——>取反
3.2.3右移操作符
3.2.3.1右移运算符的两种形式:
- 算术移位
“右边丢弃,左边补原符号位”
- 逻辑移位
“右边丢弃,左边补0”
以7为例子00000000000000000000000000000111
如下图所示:
如果去掉右边的1没有问题,那么最高位表示正负就成了问题。这就涉及了两种移位。
3.2.3.2怎么判断是算数右移还是逻辑右移
使用负数就可以判断。
一般的编译器都是算数移位。如vs,
4.位操作符
位操作符有
& //按(二进制)位与
| //按(二进制)位或
^ //按(二进制)位异或
注:它们的操作数必须是整数
电脑内存的是整数的补码,所以使用补码计算
# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 3;
int b = -5;
//按位与
//11111111111111111111111111111011 -5的补码
//00000000000000000000000000000011 3的补码
//00000000000000000000000000000011 3 & -5 = 3;两个都是1才为1,有一个是0,就是0。为正数,所以原码就是这个。
int c = a & b;
//按位或
//11111111111111111111111111111011 -5的补码
//00000000000000000000000000000011 3的补码
//11111111111111111111111111111011 3|-55 = -5;两者其中一个是1就为1,两个都是0,为0。为负数,所以另求原码
int d = a | b;
//按位异或
//11111111111111111111111111111011 -5的补码
//00000000000000000000000000000011 3的补码
//11111111111111111111111111111000 3 ^ -5 = -8;两者相同为0,两者相异为1。为1,为负数,所以另求原码。
int e = a ^ b;
printf("c=%d\n", c);
printf("c=%d\n", d);
printf("c=%d\n", e);
}
4.1异或(^)的特点
3 ^ 3 = 0;
3 ^ 0 = 3;
3 ^ 3 ^ 5 =0;
3 ^ 5 ^ 3 =0;//说明异或支持交换律
我们可以得出
a ^ a = 0;
a ^ 0 = a;
a ^ a ^ b = b;
a ^ b ^ b = a;
然后感受一下下面的代码:
不使用第三个变量交换两个整数变量的值。
# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 3;
int b = 5;
printf("交换之前的a=%d,b=%d\n", a, b);
a = a ^ b;//3^5
b = a ^ b;//3^5^5=3
a = a ^ b;//3^3^5=5
printf("交换之后的a=%d,b=%d", a, b);
}
4.2计算一个整数换算为二进制时1的个数
小提示:可以使用a&1,如果第一位为1,那么&1后的结果会是1,如果第一位不是1,那么&1后的结果就会使0……以此类推。
5.赋值操作符
“=”:可以给变量重新的赋值
int weight = 200;//体重
weight = 89;
赋值操作符可以连续使用,比如:
int a = 10;
int b = 0;
int c = 20;
a = b = c+1;//连续使用,连续赋值,但是可读性较差,一般不这么些
//一般写成这样
a = c+1;
a = b;
初始化与赋值的区别:
int a = 3;//在定义变量的时候直接给数值,就是初始化
a = 5;//重新指定数值为赋值
5.1复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果。比如:
int a = 3;
a = a + 5;
//等同于
a += 5;
a = a<< 1;
//等同于
a <<= a;
6.单目操作符
6.1单目操作符简单介绍
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型的大小(以字节为单位)
对一个数的二进制按位取反
– 前置、后置–
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
6.2单目操作符与双目操作符
单目操作符:只有一个操作数
以"+"为例,+既可以表示正值 (虽然意义不大),又可以表示相加
a+b;//双目操作符 +操作符有两个操作数
6.3"!"操作符
"!"操作符就是取反的操作,真的取反就变成假的。
int main() {
int flag = 1;
if (flag)
{
printf("通过第一个判断语句");
}
//取反
if (!flag)
{
printf("这个语句是输出不了的");
}
}
什么是真假?在c语言中0表示假,1表示真。
6.4"&"操作符
"&"操作符就是取地址。
int a = 10;
printf("%p",&a);//取a的地址,并输出
6.5"sizeof"操作符
"sizeof"操作符就是计算类型或者变量所占空间的大小。(单位都是字节)
int a = 10;
int n = szieof(int);//计算类型所占内存空间的大小
printf("%d",n);
如果传入的参数是数组名,那么计算的将会是整个数组所占空间的大小。
6.6"~"操作符
"~"操作符:作用就是按(二进制)位取反。
int main() {
//操作符~
int a = 0;
//00000000000000000000000000000000 - a 原码
//11111111111111111111111111111111 - ~a 取反操作(但数据形式还是补码)
//11111111111111111111111111111110 - 反码(补码-1等于反码)
//10000000000000000000000000000001 - 原码(反码取反就是原码,取反时最高位不变)
printf("%d", ~a);//输出:-1(10000000000000000000000000000001)
}
6.7"++“和”–"的前置与后置
“++“和”–”,等同于n = n + 1;就是自增1和自减1
++ 和 --前置后置,++前置先自增,后置先使用;–前置先自减,后置先使用;
int main() {
int a = 10;
int b1 = a++;//输出:10,后置++,先赋值,后自增
int c = 10;
int d1 = ++c;//输出:11,前置++,先自增,后赋值
printf("%d\n", b1);
printf("%d\n", a);//输出:11
printf("%d\n", d1);
printf("%d\n", c);//输出:11
}
可能会误判的情况:
int a = 10;
printf("%d",a--);//输出:10
printf("%d",a);//输出:9
这种情况也是使用。
6.8"*"操作符
"*"操作符,间接访问操作符(又称解引用操作符),与指针配合使用
int a = 10;
int *p = &a;
*p = 20;
printf("%d",a);//输出:20
*就是通过指针访问变量本体,就是说*p 等同于a。
6.9强制类型转换
(想要修改的类型):就可以将其修改为想要的类型。
int e = 3.14;//字面的浮点型系统会默认是double类型的
int f = (int)3.14;//括号 类型 括号(类型),强制类型转换,这里将double类型转换为整型
printf("%d\n", e);
printf("%d", f);
6.10"sizof"操作符
"sizof"操作符:计算操作数的字符长度(以字节为单位).
sizeof作为单目操作符与函数的区别:
调用sizeof可以不加括号,在语法允许的情况下。
#include <stdio.h>
int main() {
int a = 0;
printf("%d\n", sizeof(a));//输出:4
printf("%d\n", sizeof(int));//输出:4,int的大小必须要加括号,否则语法不允许
printf("%d\n", sizeof a);//输出:4,因为是单目操作符,所以括号可写可不写
int arr[10] = { 0 };
//计算整个数组的大小,单位是字节
printf("%d\n", sizeof(arr));//输出:40
//计算一个元素的大小
printf("%d\n", sizeof(arr[0]));//输出:4,一个整型元素的大小
//常用操作计算元素的个数
printf("%d\n", sizeof(arr) / sizeof(arr[0]));//输出:10
return 0;
}
试着判断下面4处的输出结果:
# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void test1(int arr[]) {
printf("%d\n", sizeof(arr));//第三处
}
void test2(char ch[]) {
printf("%d\n", sizeof(ch));//第四处
}
int main() {
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//第一处
printf("%d\n", sizeof(ch));//第二处
test1(arr);
test2(ch);
}
输出结果:
第一处:40,整型的大小为4,整型数组元素个数为10,所以是40.
第二处:10,字符的大小为1,字符数组元素个数为10,所以是10
第三处:4或8,看起来传过去的时数组其实是该数组第一个元素的地址,也就是指针,指针变量的大小的一样所以是4或8(32位4,64位8)。
第四处:4或8,看起来传过去的时数组其实是该数组第一个元素的地址,也就是指针,指针变量的大小的一样所以是4或8(32位4,64位8)。
7.关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
7.1"=="操作符
"=="操作符用于判断是否相等
"=="用于判断浮点型的数据时可能会不准确
if(3.0==5.0){
return 0;
}
"=="用于比较字符串时,其实比较的是两个字符串的首字符地址。
两个字符串的比较应该使用strcmp函数来比较。
8.逻辑操作符
&& 逻辑与
|| 逻辑或
观察下面代码得出结论:
int main() {
int i = 0, a = 0, b = 2, c = 3;
i = a++ && ++b && c++;
printf("a=%d,b=%d,c=%d", a, b, c);
}
//输出:a=1,b=2,b=3
//因为a=0,所以逻辑与不成立,判定为假,后续就不计算了
int main() {
int i = 0, a = 1, b = 2, c = 3;
i = a++ && ++b && c++;
printf("a=%d,b=%d,c=%d", a, b, c);
}
//输出:a=2,b=3,c=4;
//因为a!=0,为真,b!=0为真,c!=为真,所以3个都计算过去
int main() {
int i = 0, a = 1, b = 2, c = 3;
i = a++ || ++b || c++;
printf("a=%d,b=%d,c=%d", a, b, c);
}
//输出:a=2,b=2,c=3;
//因为a!=0,为真,所以逻辑或后面不计算了
&& 左边为假,右边就不计算了
||左边为真,右边就不计算了
9.条件操作符
又称三目操作符
exp1 ? exp2 : exp3
表达式exp1成立,则执行表达式exp2,表达式exp1不成立,执行表达式exp3.
例子:
int b = 0;
int a = 3;
//判断a是否大于5,大于5,b=3,小于5,b=-3
b = (a > 5 ? 3:-3);
//显然a<5,所以b=-3.
10.逗号表达式
公式从左到右依次计算,计算结果为最右边的公式的结果
exp1,exp2,exp3……expn
例子:
//代码1
int a = 6;
int b = 4;
int c = (a + b, a - b, a * b);//最后的结果为:a*b(最右边的公式)
printf("%d", c);
//输出:
//代码2
if(a = n + 1, c = a/2 , d < 0);//虽然逗号表达式是看最后一个结果的,但是前面的过程是不可忽略的
//代码3
a = get_val();
count_val(a);
while(a > 0){
a = get_val;
count_val(a);
}
//如果使用逗号表达式,改写:
// 1 2 3
while(get_val,count_val(a),a<0){//虽然只看最后一步,但前两步必不可少
//业务处理
}
11下标引用、函数调用和结构成员
[] () . ->
11.1下标引用操作符"[]"
下标引用操作符是**“[]”,但定义数组的"[]"**,不是下标引用操作符
//这里的[]是定义数组的符号,不是下标引用操作符
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int a = 3;
//[]就是下标引用操作符,arr和3就是操作数
printf("%d", arr[a]);
11.1.1下标引用操作符"[]"的其他用法
int main(){
int arr[10] = 0;
//想象一下arr[7],就是arr数组首个元素的指针+7,就是指向了第8个元素
//指针+7与7+指针大小是相等的
//arr[7] --> *(arr+7) -->*(7+arr) -->7[arr]
//arr是数组元素的首个地址
//arr+7就是跳过7个元素,指向了第8个元素
//*(arr+7)就是第8个元素
arr[7] = 8;
//这种写法是没有错误的
7[arr] = 9;
//所以可以正常使用
}
11.2函数调用操作符"()"
int add(int a, int b){
int z = a + b;
return z;
}
int main() {
int a = 2;
int b = 3;
//()就是函数调用的操作符,add,a,b就是()的操作数,括号不能省略
printf("%d", result);
int result = add(a, b);
return 0;
}
11.3结构体
结构体是把一些单一类型的组合在一起。
定义结构体的关键字是:struct
定义结构体,创建结构体实例,并输出
# define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
//使用struct关键字创建结构体my_info
struct my_info {
int age;
char name[20];
char sex[10];
char tel[12];
};
//通过指针获取结构体的信息,并设置
void set_struct(struct my_info* ss) {
//使用*指针来获取元素并赋值
/*(*ss).age = 21;
strcpy((*ss).name , "czl");
strcpy((*ss).sex ,"man");
strcpy((*ss).tel ,"123456");*/
//使用箭头运算符,通过指针来访问成员并赋值
ss->age = 21;
strcpy(ss->name, "czl");
strcpy(ss->sex, "man");
strcpy(ss->tel, "123456");
}
//通过指针获取结构体信息,并输出
void print_info(struct my_info* sss)
{
//可通过*指针获取元素
/*printf("%d\n", (*sss).age);
printf("%s\n", (*sss).name);
printf("%s\n", (*sss).sex);
printf("%s\n", (*sss).tel);*/
//使用箭头运算符,通过指针来访问其成员
printf("%d\n", sss->age );
printf("%s\n", sss->name);
printf("%s\n", sss->sex);
printf("%s\n", sss->tel);
};
int main() {
//初始化自定义结构体
struct my_info s= { 0 };
set_struct(&s);
print_info(&s);
}
上述代码种可能存在的疑问!
- 为什么给字符串赋值要调用strcpy函数?
在c语言中,字符串是以字符数组的形式存在,没有直接的赋值操作。因此,需要将字符串赋值到另一个字符串就需要strcpy函数来复制内容。
如果使用=来赋值,那么只会复制该字符串(字符数组)的首元素地址,不会复制全部的内容。
- 传参为什么要传指针?
普通的传参只是函数内多出一个参数的副本,并非参数本参。
如果需要在函数内部修改参数的值,则需要传递变量的指针(即变量的地址),然后通过指针直接访问参数本参,进行的修改操作将直接作用于传递的参数本省,而非副本。
12.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1运算符的优先级
12.2隐式类型转换
c的整型整数运算总是以缺省整型类型的精度来计算的。
为了获取这个精度,表达式的字符和短整型操作数在使用之前通常会被转化为普通类型,这种转换被称为整型提升。
12.2.1整型提升的意义
整型提升的意义:
表达式的整型运算要在CPU的相应运算器内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使是两个char类型的相加,在CPU执行时实际上也要先转换位CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加的指令)。所以,表达式中各种长度小于int长度的整型值,都必须先转换为int或unsigned int),然后才能送入CPU去执行运算。
比如:
int a,b,c;
...
a = b + d;
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行型体提升呢?
整型提升是按照变量的数据类型的符号位来提升的。
//负数的整型提升
char a= -1;
//10000000000000000000000000000001 -1
//11111111111111111111111111111110 -反码
//11111111111111111111111111111111 -补码(32比特)
//变量a的二进制(补码)只有8个比特位:
//11111111 -char类型(后8位)
//因为char,为有符号的char
//所以整型提升的时候高位补充符号位,即1
//提升之后的结果是:
//11111111111111111111111111111111
//正数的整型提升
char b = 1;
//00000000000000000000000000000001 -1
//变量b的二进制(补码)只有8个比特位:
//00000001 -char类型
//因为char,为有符号的char
//所以整型提升的时候,高位补充符号位,即0
//提升之后的结果是:
//00000000000000000000000000000001
//无符号整型提升,高位补0
整型提升运用的例子:
计算字符类型的整数的相加。
int main() {
char a = 5;
//相加需要整型提升
//00000000000000000000000000000101 -a原码
//00000101 -a内存储的
//00000000000000000000000000000101 -整型提升后
char b = 126;
//00000000000000000000000001111110 -b原码
//01111110 -b内存储的
//00000000000000000000000001111110 -整型提升后
//a + b = 00000000000000000000000010000011
//10000001 -c
//11111111111111111111111110000011 -c补码 -整型提升
//11111111111111111111111110000010 -c反码
//10000000000000000000000001111101 -c原码 -125
char c = a + b;
//使用整型输出字符c
printf("%d", c);
}
简单的整型提升题:
char a = 0xb6;
short = 0xb600;
int c = 0xb6000000;
if(a == 0xb6){
printf("a");
}
if(short == 0xb600){
printf("b");
}
if(c == 0xb6000000){
printf("c");
}
//输出:c
因为char和short都不是整型都要进行相应的整型提升数值就变了。
12.3算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作将无法进行。下面的层次体系称为寻常算数转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型再上面这个列表中排名较低,那么首先要转换为另一个操作数的类型后执行运算。(比如,一个int类型的与一个double类型的计算,int类型的会被转换为double类型进行计算)
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
函数的调用先后顺序无法通过操作符的优先级确定。
即使知道操作符的优先级和结构性也可能无法确定一个函数的唯一计算路径。
问题代码
int fun(){
static int count = 1;
return ++count;
}
int main(){
int answer;
// 1 2 3
// 2 3 1
// 3 2 1
//...等等·
answer = fun()-fun()*fun();
printf("%d",answer);
return 0;
}
上述代码的运算符优先级可以判断,但其实函数调用的顺序是不确定的,虽然运算符优先级明确但是函数调用的顺序是随机的。
问题代码
int main(){
int a = 1;
int b = (++a) + (++a) + (++a);
printf("%d",b);
return 0;
}
上述代码的结果也并非是所想的9,10,12等。不同的编译器运行结果不一样。
**总结:**我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作将无法进行。下面的层次体系称为寻常算数转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型再上面这个列表中排名较低,那么首先要转换为另一个操作数的类型后执行运算。(比如,一个int类型的与一个double类型的计算,int类型的会被转换为double类型进行计算)
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
函数的调用先后顺序无法通过操作符的优先级确定。
即使知道操作符的优先级和结构性也可能无法确定一个函数的唯一计算路径。
问题代码
int fun(){
static int count = 1;
return ++count;
}
int main(){
int answer;
// 1 2 3
// 2 3 1
// 3 2 1
//...等等·
answer = fun()-fun()*fun();
printf("%d",answer);
return 0;
}
上述代码的运算符优先级可以判断,但其实函数调用的顺序是不确定的,虽然运算符优先级明确但是函数调用的顺序是随机的。
问题代码
int main(){
int a = 1;
int b = (++a) + (++a) + (++a);
printf("%d",b);
return 0;
}
上述代码的结果也并非是所想的9,10,12等。不同的编译器运行结果不一样。
**总结:**我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
表达式不必要复合繁琐,要明确目的,写一步看一步。