摘要:注不要移动负数位标准未定义行为这种行为属于标准未定义行为语言中并没有规定移动负数位。按进制位与规则两个二进制数,有则为,全则为。为假的时候,打印语言中表示假,非表示真无论是正数还是负数。
C语言操作符详解
C语言中的操作符有:
算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、
条件操作符、逗号表达式、下标引用操作符、函数调用操作符、结构成员访问操作符
+(加) -(减) *(乘) /(除) %(取模)
+(加)-(减)*(乘)与数学中的类似,这里就不过多的叙述了。
(1)% : 取模(取余)得到的是相除之后的余数
(2)/ :除法 得到的是商
总结:
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 /(除) 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
<<:左移操作符
>>:右移操作符
注:移位操作符的操作数只能是整数
移位操作符移动的是整数的二进制位
先了解一下C语言中的二进制位
二进制与十进制的转换
例:二进制数:1 1 1 1 ---------> 1*2^3+1*2^2+1*2^1+1*2^0 = 15 (十进制)
二进制不好理解,可以想一想十进制的: 1 2 3 ---------> 1*10^2+2*10^1+3*10^0 = 123
十进制:5 转换为二进制: 0101 ---------> 0*2^3 + 1*2^2 + 0*2^1 + 1*2^0 = 5
二进制、八进制、十进制、十六进制只是数值的表示形式
整数有三种二进制的表示形式:原码、反码、补码
正整数:原码、反码、补码相同
负整数:原码、反码、补码不同,要进行计算!
正整数:int a = 5;a是整型,a占4个字节 -->32bit
直接将十进制数转换成为对应的二进制数得到的是原码
a的原码:00000000000000000000000000000101
a的反码:00000000000000000000000000000101
a的补码:00000000000000000000000000000101
最高位0表示正数
负整数:int b = -5;b是整型,b占4个字节 -->32bit
负数原码、反码、补码的计算:
原码:直接将十进制数转换成为对应的二进制数
反码:原码的符号位(最高位)不变,其他位按位取反
补码:反码+1
b的原码:1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
b的反码:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0
b的补码:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1
最高位1表示负数
整数在内存中存储的是补码
可以看一下内存中存储是是不是补码
使用-1来验证
-1的原码:1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
-1的反码:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
-1的补码:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
十六进制数使用 0 1 2 3 4 5 6 7 8 9 a b c d e f 表示
二进制 1 1 1 1 是十进制的15;十六进制的 f 是十进制的15
ff ff ff ff 写成二进制形式:
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
由此可以知道整数在内存中存储的是补码。
1、左移操作符: <<
移位规则: 左边抛弃、右边补0
正数:
#includeint main(){ int a = 5; //将a在内存中存储的二进制位向左移动两位 int b = a << 2; printf("%d/n", b); printf("%d/n", a); return 0;}
代码分析
虽然变量a左移两位后值发生了变化,但a的值不会发生变化,就相当于 b = a+1,b的值发生了变化,a的值不会变。
运行结果
负数:
#includeint main(){ int b = -5; //将b在内存中存储的二进制位向左移动两位 int c = b << 2; printf("%d/n", c); printf("%d/n", b); return 0;}
代码分析
运行结果
2、右移操作符: >>
(1)算术右移
规则:右边丢弃,左边补原来的符号位
(2)逻辑右移
规则:右边丢弃,左边补0(如果是负数,左边补0得到的是正数)
算术右移与逻辑右移取决于编译器,C语言中并没有规定具体使用哪种右移。我们常见的编译器下都是算术右移。
正数:
#includeint main(){ int a = 5; //将a在内存中存储的二进制位向右移动1位 int b = a >> 1; printf("%d/n", b); printf("%d/n", a); return 0;}
代码分析
运行结果
负数:
#includeint main(){ int a = -5; //将a在内存中存储的二进制位向右移动1位 int b = a >> 1; printf("%d/n", b); printf("%d/n", a); return 0;}
代码分析
运行结果
由代码的运行结果得到,当前编译器采用的是算术右移(左边补符号位)。
注:不要移动负数位
int a = 10;int b = a >> -2; //标准未定义行为
这种行为属于标准未定义行为(C语言中并没有规定移动负数位)。
&:按位与 |:按位或 ^:按位异或
注:他们的操作数必须是整数,操作的是整数的补码。
1、按(2进制)位与:&
规则:两个二进制数,有0则为0,全1则为1。
#includeint main(){ int a = -3; int b = -5; int c = a & b; printf("%d/n", c); return 0;}
计算过程
运行结果
2、按(2进制)位或:|
规则:两个二进制数,有1则为1,全0则为0。
#includeint main(){ int a = 3; int b = -5; int c = a | b; printf("%d/n", c); return 0;}
计算过程
运行结果
3、按(2进制)位异或:^
规则:两个二进制数,相同为0,相异为1。
#includeint main(){ int a = 3; int b = -5; int c = a ^ b; printf("%d/n", c); return 0;}
计算过程
运行结果
4、位操作符练习
不创建临时变量(第三个变量),实现两个数的交换。
方法一:
#includeint main(){ int a = 3; int b = 5; printf("交换前:a=%d b=%d/n", a, b); a = a + b; b = a - b; a = a - b; printf("交换后:a=%d b=%d/n", a, b); return 0;}
运行结果
这种方法虽然可以实现不创建临时变量交换两个数的效果,但是当两个数加起来的结果超过了整型的范围就会出错,这种方法不能满足任意两个整数的交换。
方法二:
#includeint main(){ int a = 3; int b = 5; printf("交换前:a=%d b=%d/n", a, b); a = a ^ b; b = a ^ b;//b = a ^ b ^ b a = a ^ b;//a = a ^ a ^ b printf("交换后:a=%d b=%d/n", a, b); return 0;}
运行结果
这种方法可读性太差,实际中交换两个变量应用最多的方法是创建临时变量来进行交换。这里只是练习使用位操作符。
1、= :赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
int a = 10; a = 100; //赋值操作符
赋值操作符可以连续赋值,但是不建议这样写。
#includeint main(){ int a = 10; int b = 0; int c = 20; a = b = c + 1;//连续赋值 printf("%d/n", a); return 0;}
运行结果
上面的写法等同于
int a = 10; int b = 0; int c = 20; b = c + 1; a = b;
补充:
赋值操作符必须保证左边是变量
2、复合赋值符
+=:加等 -=:减等 *= :乘等 /=:除等 %=:取模等
>>=:右移等 <<=:左移等 &=:按位与等 |=:按位或等 ^=:按位异或等
#includeint main(){ int a = 10; int b = -5; int c = 0; a += 10;//等同于:a = a + 10; c ^= b;//等同于:c = c ^ b; printf("%d/n", a); printf("%d/n", c); return 0;}
其他运算符一样的道理。这样写更加简洁。
3 + 5:
- 3 是左操作数
- + 是一个操作符
- 5 是右操作数
+ 有两个操作数,它是双目操作符
单目操作符只有一个操作数
!:逻辑反操作 -:负值 +:正值 &:取地址
sizeof:计算操作数的类型长度(以字节为单位)
~:对一个数的二进制按位取反 --:前置、后置--(减1) ++:前置、后置++(加1)
*:间接访问操作符(解引用操作符) (类型):强制类型转换
1、!:逻辑反操作
将一个表达式的结果,真变为假,假变为真。
#includeint main(){ int flag = 0; //flag 为假的时候,打印hehe if (!flag) { printf("hehe/n"); } return 0;}
C语言中0表示假,非0表示真(无论是正数还是负数)。
2、 - :负值
#includeint main(){ int i = 0; int a = -10; int flag = 1; for (i = 0; i < 10; i++) { printf("%d ", i * flag); flag = -flag; } return 0;}
运行结果
3、+:正值
这个符号没什么用,对于正数加上+还是正数,对于负数加上+还是负数。
4、&取地址
#includeint main(){ int a = 10; &a;//& - 取地址操作符,获取变量的地址 return 0;}
5、sizeof:计算操作数的类型长度(以字节为单位)
#includeint main(){ int a = 10; int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d/n", sizeof(a)); printf("%d/n", sizeof(int)); printf("%d/n", sizeof a);//因为sizeof是操作符,所以不加括号也行 //下面这种写法不可以,sizeof在计算变量大小时才可以省略括号, //sizeof在计算类型大小时不可以将括号省略。建议在写的时候都加上括号。 printf("%d/n", sizeof int);//error //计算数组的大小: printf("%d/n", sizeof(arr)); printf("%d/n", sizeof(int [10]));//int [10] ->是数组 arr 的类型 return 0;}
运行结果
#includevoid 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); return 0;}
代码分析
运行结果
6、~ 按(内存中补码的二进制)位取反
#includeint main(){ int a = 0; int b = ~a; printf("%d/n", b); return 0;}
计算过程
0的补码:00000000000000000000000000000000
按位取反(~0)后:
补码:11111111111111111111111111111111
反码:11111111111111111111111111111110
原码:10000000000000000000000000000001结果: - 1
~ 按位取反的使用:
#includeint main(){ int a = 10; a |= (1 << 2); printf("%d/n", a); a &= ~(1 << 2); printf("%d/n", a); return 0;}
运行结果
7、++:前置、后置++(加1)
前置++:先++,后使用
#includeint main(){ int a = 10; int b = ++a; printf("a=%d b=%d/n", a, b); return 0;}
运行结果
后置++:先使用,再++
#includeint main(){ int a = 10; int b = a++;//先将a的值赋给b,再++ printf("a=%d b=%d/n", a, b); return 0;}
运行结果
#includeint main(){ int a = 10; printf("%d/n", a++); //值为10 return 0;}
#includeint main(){ int a = 10; printf("%d/n", ++a); //值为11 return 0;}
8、 --:前置、后置--(减1)
前置--:先--,后使用
#includeint main(){ int a = 10; int b = --a;//先将a的值赋给b,再++ printf("a=%d b=%d/n", a, b); return 0;}
运行结果
后置--:先使用,再--
#includeint main(){ int a = 10; int b = a--;//先将a的值赋给b,再++ printf("a=%d b=%d/n", a, b); return 0;}
运行结果
自增自减(++、--)不要整的太复杂
#includeint main(){ int a = 1; int b = (++a) + (++a) + (++a); printf("%d/n", b); printf("%d/n", a); return 0;}
运行结果
同样的代码在Linux环境下使用 gcc 编译器运行的结果
由结果可以看出,这个代码是一种错误的代码,在不同的编译器下运行会得到不同的结果。
9、 *:间接访问操作符(解引用操作符)
#includeint main(){ int a = 10; int* pa = &a; *pa = 20;//* ->解引用操作符(间接访问操作符) printf("%d/n", a); return 0;}
代码分析
运行结果
10、(类型)强制类型转换
#includeint main(){ int a =(int)3.14;//3.14 ->double类型 printf("%d/n", a); return 0;}
将double类型的数据强制类型转换为整型后,得到是结果是整数部分,小数部分直接舍去。
运行结果
强制类型转换不建议大量的使用,创建变量时尽量将类型匹配。
>:大于 >=:大于等于 <:小于 <=:小于等于 !=:不等于 ==:等于
关系操作符比较简单,直接使用就可以了。
要注意:在编程的过程中容易将==和=不小心写错。==是用于测试两个值相等,=是赋值。
&&:逻辑与 ||:逻辑或
区分逻辑操作符和按位操作符:
1、 &&:逻辑与
练习
#include int main(){ int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; printf("a = %d/nb = %d/nc = %d/nd = %d/n", a, b, c, d); return 0;}
代码分析
a=0,a++先返回a=0,a再自增(a的值为1)。当a返回的值为0时就不会再计算&&后面的表达式,所以,a的值为1,b的值为2,c的值为3,d的值为4。最会输出结果,a = 1,b = 2 ,c = 3,d = 4。
运行结果
2、||:逻辑或
#include int main(){ int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++||++b||d++; printf("a = %d/nb = %d/nc = %d/nd = %d/n", a, b, c, d); return 0;}
代码分析
a=0,a++先返回a=0,a再自增(a的值为1)。当a返回的值为0时计算 || 后面的表达式,b=2,++b先执行b自增1,再返回自增后的结果为3。当b=3时,表达式(++b)的结果为真,不会再计算||后的表达式。所以a的值为1,b的值为3,c的值为3,d的值为4。最会输出结果,a = 1,b = 3 ,c = 3,d = 4。
运行结果
总结
&&:左操作数为假,右边不计算
||:左操作数作为真,右边不计算
表达式1 ? 表达式2 : 表达式3
如果表达式1的结果为真,则执行表达式2,整个表达式的结果为表达式2执行的结果
如果表达式1的结果为假,则执行表达式3,整个表达式的结果为表达式3执行的结果
#includeint main(){ int a = 10; int b = 20; int max = 0; max = (a > b ? a : b); printf("%d/n", max); return 0;}
表达式1,表达式2,表达式3 …… 表达式n
逗号表达式,就是由逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
#includeint main(){ int a = 3; int b = 5; int c = 6; int d = (a += 2, b = a - c, c = a + 2 * b); printf("%d/n", d); return 0;}
代码分析
- 先计算,a += 2,a = 3 + 2 = 5
- 再计算b = a - c,b = 5 - 6 = -1
- 再计算c = a + 2 * b,c = 5 + 2 * (-1) = 3
- d的结果为最后一个表达式的结果,所以输出d的值为3。
运行结果
逗号表达式的使用
#includeint main(){ int a = 5; int count = 0; while (count < 10) { a = a+1; count++; } //简洁的写法 while (a = a + 1, count++, count < 10) { ; } return 0;}
1、 [ ],下标引用操作符
操作数:数组名+元素下标
#includeint main(){ int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d/n", arr[7]);//[]就是下标引用操作符,arr 和 7是操作数 return 0;}
运行结果
补充:
其实编译器在编译arr[7]时,是将arr[7]转换成 *(arr+7)再对数组访问。
*(arr+7) :arr是数组首元素的地址,arr+7就表示从数组首元素来说找到第8个元素(数组下标是从0开始的,所以找到的是第8个元素)。
arr[7] ---> *(arr+7) --->*(7+arr) --->7[arr]
这里只是推导一下有这种形式,但在写代码的时候还是正常写比较好。
2、 ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
无参函数,函数名就是操作数,有参函数,函数名和参数都是操作数。
3、访问一个结构的成员
#includestruct Stu{ char name[20]; int age; double score;};int main(){ struct Stu s = { "zhangsan",20,85.5 }; // . 操作符 //结构体变量.结构体成员 printf("%s %d %.1f/n", s.name, s.age, s.score); // -> 操作符 struct Stu* ps = &s; printf("%s %d %.1f/n", (*ps).name, (*ps).age, (* ps).score); //结构体指针->结构体成员 printf("%s %d %.1f/n", ps->name, ps->age, ps->score); return 0;}
运行结果
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1、隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
(1)整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度再计算。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
(2)整型提升的计算过程
整型提升是按照变量的数据类型的符号位来提升的。
#includeint main(){ char a = 5; char b = 126; char c = a + b; printf("%d/n", c); return 0;}
执行过程
运行结果
参与运算的数据类型是char和char计算,char和short计算,short和short计算都会发生整型提升。
整型提升的例子
(1)
#includeint main(){ char a = 0xb6; short b = 0xb600; int c = 0xb6000000; if (a == 0xb6) printf("a/n"); if (b == 0xb600) printf("b/n"); if (c == 0xb6000000) printf("c/n"); return 0;}
代码分析
- 变量 a 和 b要进行整形提升,但是变量c是整型所以不需要整形提升。
- a和b整形提升之后就变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果为假。
- c不发生整形提升,所以表达式 c==0xb6000000 的结果是真. 所程序输出的结果是: c
运行结果
(2)
#includeint main(){ char c = 1; printf("%u/n", sizeof(c)); printf("%u/n", sizeof(+c)); printf("%u/n", sizeof(-c)); return 0;}
代码分析
- 变量c只要参与表达式运算,就会发生整形提升。
- 表达式 +c 就会发生提升,所以 sizeof(+c) 是4个字节。表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节。
- 最后输出的结果是:1 4 4
运行结果
补充:sizeof(),括号内的表达式不参与运算。
int a = 10;
int b = 20;
a + b;
- 表达式有两个属性:值属性,类型属性
- a+b 中:30就是值属性,int就是类型属性
- 当知道类型属性时,sizeof是通过类型属性判断表达式有几个字节,所以不需要计算。表达式的值。
#includeint main(){ short s = 20; int a = 5; printf("%d/n", sizeof(s = a + 4)); printf("%d/n", s); return 0;}
代码分析
- a是整型,a+4表达式的类型是int,将a+4放到变量s中,但是s是short类型,所以表达式的类型就变成了short类型。所以sizeof计算的是short类型字节的大小,结果是2。
- sizeof()括号中的表达式不会真实计算,所以s的值还是20。
- 最后输出的结果是 2 20
运行结果
2、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类 型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int
- 上面这些类型之间转换是由下往上转换。
- 例:int 类型的数与flaot类型的数进行计算,最终计算的结果应该是float类型。
如果某个操作数的类型在上面这些列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
#includeint main(){ int a = 5; float b = 3.14; float r = a + b;//算术转换 return 0;}
小于4个字节的类型之间计算是整型提升,其他类型之间计算时算术转换。
注: 算术转换要合理,要不然会有一些潜在的问题。
3、操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
(1)操作符优先级
#includeint main(){ int a = 10; int b = 20; int c = a + b * c; return 0;}
乘法的优先级高于加法,所以先计算乘法再计算加法。
(2)操作符结合性
相邻操作符的优先级相同的情况下,取决于结合性。
#includeint main(){ int a = 10; int b = 20; int c = a + b + c; return 0;}
加法的结合性是从左向右计算。所以先计算a+b,再将a+b计算出的结果与c进行计算。
一些问题表达式有了优先级和结合性也不能准确的确定表达式的计算结果
(1)代码1
a * b + c * d + e * f
这个表达式没有唯一确定的计算顺序
第一种:
- 计算a * b
- 计算c * d
- 计算e * f
- 计算a * b + c*d
- 计算a * b + c * d + e * f
第二种:
- 计算a * b
- 计算c * d
- 计算a * b + c*d
- 计算e * f
- 计算a * b + c * d + e * f
如果a,b,c,d,e,f 每个都是一个表达式,那么两种计算顺序得到的结果肯定不同。
由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不 能决定第三个*比第一个+早执行。
(2)代码2
#includeint main(){ int c = 5; c + --c; return 0;}
这个代码中--的优先级高于+,但是c的取值不确定。
操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
(3)代码3
#inclu
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/124773.html
摘要:一大家都知道一般就是用来检查对象是否为类或子类的实例。中两者均指向,因此添加到的属性,也会出现在的中。四其实是建议实现者如采用的底层优化手段。因为中定义函数啥都一样,所以底层实现可以不再生成一个新的,从而从空间和时间上降低消耗。 一、Breif 大家都知道instanceof一般就是用来检查A对象是否为B类或子类的实例。那问题是...
摘要:找找出别人扩展真么写的。这次主要说了下写扩展要准备的一些基本知识。比如不同编译方式这个你看别的扩展源码的时候就会注意到具体作用。后面再来慢慢学习老司机的各种姿势。包括,函数,函数参数,函数返回值,对象,类,命名空间等等等。 PHP扩展开发系列01 - 我要成为一名老司机 1. 关于扩展的教程貌似挺全了,为啥还写? 记录下我写扩展的历程 自认为会写的更容易理解 我的宗旨就是 先用再识 ...
摘要:另外,通过指针可以更便捷地操作数组。在一定意义上可以说,指针是语言的精髓。野指针成因除了未初始化还有就是越界访问或者指针指向空间已经释放。所以不难知道两个地址相减就是元素的个数,这个表达式的前提是两个指针指向同一块空间。 ...
摘要:我们常用的结构,就是小端模式,什么则为大端模式没学我也不知道是个啥,但还是摆出来。 目录 传统艺能?过渡区?正片开始?共用体原理?字节顺序?大小端存储?共用体判断...
阅读 1073·2021-11-24 09:39
阅读 3601·2021-09-02 15:21
阅读 2139·2021-08-24 10:01
阅读 694·2021-08-19 10:55
阅读 2387·2019-08-30 15:55
阅读 1197·2019-08-30 14:16
阅读 2970·2019-08-29 15:17
阅读 3200·2019-08-29 13:53