操作符
操作符分类
算数操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号表达式、下标引用、函数调用和结构成员。
算数操作符
+ - * / %
除了%操作符之外,其它的几个操作符都可以作用于整数和浮点数。
对于/操作符,如果两个操作数都为整数,执行整除除法。而只要有一个是浮点数执行的就是浮点数除法。
%操作符的两个操作数都必须为整数。返回的是整除之后的余数。
移位操作符
<< 左移操作符
>> 右移操作符
左移操作符移位规则:
左边抛弃,右边补0。
右移操作符移位规则:
首先右移运算分两种:
- 逻辑右移:左边用0填充,右边丢弃。
- 算数右移:左边用符号位填充,右边丢弃。
注意:右移操作对于有符号数来说执行的是算数右移,对于无符号数来说是逻辑右移。
比如说,统计一个数的二进制位中1的数量。
int n;
int cnt = 0;
scanf("%d", &n);
while(n){
if(n & 1){
++cnt;
}
n >>= 1;
}
如果输入的数是负数,这个程序就是死循环,因为n是有符号数,是算数右移,高位一直补1。将n定义为无符号型就可以了。
unsigned int n;
int cnt = 0;
scanf("%d", &n);
while(n){
if(n & 1){
++cnt;
}
n >>= 1;
}
警告:对于移位运算符来说,不要移动负数位,这个是未定义行为。
位操作符
& | ^
注意:它们的操作数必须是整数。
来看几个代码
不创建临时变量,交换两个变量
#include <stdio.h>
int main(){
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d, b = %d\n", a, b);
return 0;
}
运行结果
[sss@aliyun operator]$ gcc swap_without_variable.c -o swap_without_variable
[sss@aliyun operator]$ ./swap_without_variable
a = 20, b = 10
求一个整数二进制形式中1的个数
#include <stdio.h>
int main(){
int num = 0;
int cnt = 0;
printf("Please input the num: \n");
scanf("%d", &num);
while(num){
++cnt;
num &= (num - 1);
}
printf("cnt 1: %d\n", cnt);
return 0;
}
运行结果
[sss@aliyun operator]$ gcc count_ont.c -o count_ont
[sss@aliyun operator]$ ./count_ont
Please input the num:
10
cnt 1: 2
赋值操作符
赋值操作符是一个很棒的操作符,它可以让你对你之前得到的不满意的值,重新自己赋值。
int weight = 120;
weight = 89;
double salary = 10000.0;
salary = 20000.0;
赋值操作可以连续使用:
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1;
拆开写:
x = y + 1;
a = x;
复合赋值符
+= -= *= /= %= >>= <<= &= |= ^=
这些运算符都可以写成复合的效果。比如:
int x = 10;
x = x + 10;
x += 10;
单目操作符
! | 逻辑反操作 |
- | 负值 |
+ | 正值 |
& | 取地址 |
sizeof | 操作数的类型长度(以字节为单位) |
~ | 对一个二进制数按位取反 |
-- | 前置、后置-- |
++ | 前置、后置++ |
* | 间接访问操作符(解引用操作符) |
(类型) | 强制类型转换 |
代码演示
#include <stdio.h>
int main(){
int a = -10;
int* p = NULL;
printf("!2: %d\n", !2);
printf("!0: %d\n", !0);
a = -a;
p = &a;
printf("sizeof(a): %ln\n", sizeof(a));
printf("sizeof(int): %ln\n", sizeof(int));
printf("sizeof a: %ln\n", sizeof a);
printf("sizeof int: %ln\n", sizeof int);
return 0;
}
运行结果
[sss@aliyun operator]$ gcc single_operator.c -o single_operator
single_operator.c: In function ‘main’:
single_operator.c:15:37: error: expected expression before ‘int’
printf("sizeof int: %ln\n", sizeof int);
结论:sizeof 变量;可以,但是sizeof 类型;不行。建议全部带括号。
sizeof和数组
代码演示
#include <stdio.h>
void test1(int arr[]){
printf("test1:sizeof(arr): %lu\n", sizeof(arr));
}
void test2(char ch[]){
printf("test2:sizeof(ch): %lu\n", sizeof(ch));
}
int main(){
int arr[10] = {0};
char ch[10] = {0};
printf("sizeof(arr): %lu\n", sizeof(arr));
printf("sizeof(ch): %lu\n", sizeof(ch));
test1(arr);
test2(ch);
return 0;
}
运行结果
[sss@aliyun operator]$ !gcc
gcc sizeof_arr.c -o sizeof_arr
[sss@aliyun operator]$ ./sizeof_arr
sizeof(arr): 40
sizeof(ch): 10
test1:sizeof(arr): 8
test2:sizeof(ch): 8
结论:sizeof();括号内是数组名的话,求的是整个数组所占的空间。后两个之所以为8,因为数组名隐式转换为指针。
再来看一段代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void StringTest(){
char str1[] = "hehe\0";
char str2[] = "hehe\\\0";
printf("sizeof(str1): %lu\n", sizeof(str1));
printf("strlen(str1): %lu\n", strlen(str1));
printf("sizeof(str2): %lu\n", sizeof(str2));
printf("strlen(str2): %lu\n", strlen(str2));
}
int main(){
StringTest();
system("pause");
return 0;
}
运行结果
这是为什么呢?/开头的位转义字符,所以\\
会被当做一个字符处理。
前置++和后置++
- 前置++,是先++,再使用。
- 后置++,是先使用,再++。
- C++程序猿推荐使用前置++。
- 因为后置++需要对内容进行备份。
- 前置后置
--
同理。
奇怪的程序
同样的代码,产生不同的结果,这是为什么呢?
这段代码中的第一个+在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定的。
总结:
- 这个代码本身不是一个好代码。
- 结果跟表达式的求值顺序有关系。
- 表达式的求值依赖于运算符的优先级和结合性,但操作符的优先级和结合性又不能确定唯一的计算路径。
- 这样的表达式产生的结果是严重依赖于表达式的求值顺序,所以不要写出这样的表达式。
关系操作符
> >= < <= != ==
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告:在编程过程中==和=不小心写错,导致的错误。
逻辑操作符
&& ||
注意:区分逻辑与和按位与,逻辑或和按位或。
1 & 2 --> 0
1&& 2 --> 1
1 | 2 --> 3
1 || 2 --> 1
代码演示
#include <stdio.h>
int main(){
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d, b = %d, c = %d, d = %d\n",
a, b, c, d);
return 0;
}
运行结果
[sss@aliyun operator]$ !gcc
gcc and_or.c -o and_or
[sss@aliyun operator]$ ./and_or
a = 1, b = 2, c = 3, d = 4
分析:逻辑与&&的短路求值。a = 0;后面的条件不再判断,直接为假。
将&&改为||运行结果
[sss@aliyun operator]$ !gcc
gcc and_or.c -o and_or
[sss@aliyun operator]$ ./and_or
a = 1, b = 3, c = 3, d = 4
分析:逻辑或||的短路求值,b为真,后序条件不再判断。
条件操作符
exp1 ? exp2 : exp3
简单练习
将if语句转成条件操作符
if (a > 5){
b = 3;
}
else{
b = -3;
}
a > 5 ? b = 3 : b = -3;
使用条件表达式求两个数中的最大值。
a > b ? a : b;
逗号表达式
exp1, exp2, exp3, exp4, ...expN
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
下标引用、函数调用和结构成员
下标引用操作符[]
操作数:一个数组名 + 一个索引值。
int arr[10];
arr[9] = 10;
[]的两个操作数是arr和9
函数调用操作符()
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
代码演示
#include <stdio.h>
#include <stdlib.h>
void test1(){
printf("hello, ");
}
void test2(const char* str){
printf("%s\n", str);
}
int main(){
test1();
test2("world!");
system("pause");
return 0;
}
运行结果
hello, world!
结构体成员访问操作符
. 结构体.成员名
-> 结构体指针->成员名
代码演示
#include <stdio.h>
#include <string.h>
typedef struct Student{
char name[1024];
int age;
} Student;
void setName(Student* pstu){
char str[1024] = "hehe";
strcpy(pstu->name, str);
}
void setAge(Student* pstu){
pstu->age = 18;
}
int main(){
Student stu;
Student* pstu = &stu;
setName(pstu);
setAge(pstu);
printf("name: %s\n", stu.name);
printf("age: %d\n", pstu->age);
return 0;
}
运行结果
[sss@aliyun operator]$ !gcc
gcc struct.c -o struct
[sss@aliyun operator]$ ./struct
name: hehe
age: 18
表达式求值
表达式求值顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
- C的整型算数总是以缺省整型类型的精度来进行的。
- 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
短数据类型扩展为长数据类型
要扩展的短数据类型为有符号数
短数据类型的符号位填充到长数据类型的高字节位(比短数据类型多出的部分)。
例:
char num1 = 128; short num2 = num1;
char num3 = 127; short num4 = num3;
num1: 1000 0000
num2: 1111 1111 1000 0000
num3: 0111 1111
num4: 0000 0000 0111 1111
代码示例
int main()
{
char num1 = 128;
short num2 = num1;
char num3 = 127;
short num4 = num3;
}
查看num
(gdb) x/10xb &num1
0x80
(gdb) x/10xb &num2
0x80 0xff
(gdb) x/10xb &num3
0x7f
(gdb) x/10xb &num4
0x7f 0x00
要扩展的短数据类型为无符号数
用零来填充到长数据类型的高字节位(比短数据类型多出的部分)。
例:
unsigned char num5 = 128; short num6 = num5;
unsigned char num7 = 127; short num8 = num7;
num5: 1000 0000
num6: 0000 0000 1000 0000
num7: 0111 1111
num8: 0000 0000 0111 1111
代码示例
int main()
{
unsigned char num5 = 128;
short num6 = num5;
unsigned char num7 = 127;
short num8 = num7;
}
查看num
(gdb) x/10xb &num5
0x80
(gdb) x/10xb &num6
0x80 0x00
(gdb) x/10xb &num7
0x7f
(gdb) x/10xb &num8
0x7f 0x00
长数据类型缩短为短数据类型
- 如果长数据类型的高字节位(比短数据类型多出的部分)全为1或全为0,则会直接截取低字节位赋给短数据类型。
- 如果长数据类型的高字节位(比短数据类型多出的部分)不全为1或0,转换就会发生错误。
相同长度的数据类型中有符号数和无符号数相互转换
- 直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。
- 短类型扩展为长类型时,但短类型与长类型分属有符号数与无符号数时,则先按一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;
操作符的属性
复杂表达式的求值有三个影响因素:
- 操作符的优先级。
- 操作符的结合性。
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于它们的结合性。
操作符优先级
操作符 | 描述 | 结合性 | 是否控制求值顺序 |
---|---|---|---|
( ) | 聚组 | N/A | 否 |
( ) | 函数调用 | L-R | 否 |
[ ] | 下标引用 | L-R | 否 |
. | 访问结构成员 | L-R | 否 |
-> | 访问结构体指针成员 | L-R | 否 |
++ | 后缀自增 | L-R | 否 |
-- | 后缀自减 | L-R | 否 |
! | 逻辑取反 | R-L | 否 |
~ | 按位取反 | R-L | 否 |
+ | 单目,表示正值 | R-L | 否 |
- | 单目,表示复制 | R-L | 否 |
++ | 前缀自增 | R-L | 否 |
++ | 前缀自减 | R-L | 否 |
* | 间接访问 | R-L | 否 |
& | 取地址 | R-L | 否 |
sizeof | 取其长度,单位字节 | R-L | 否 |
(类型) | 类型转换 | R-L | 否 |
* | 乘法 | L-R | 否 |
/ | 除法 | L-R | 否 |
% | 整数取余 | L-R | 否 |
+ | 加法 | L-R | 否 |
- | 减法 | L-R | 否 |
<< | 左移位 | L-R | 否 |
>> | 右移位 | L-R | 否 |
> | 大于 | L-R | 否 |
>= | 大于或等于 | L-R | 否 |
< | 小于 | L-R | 否 |
<= | 小于或等于 | L-R | 否 |
== | 等于 | L-R | 否 |
!= | 不等于 | L-R | 否 |
& | 按位与 | L-R | 否 |
^ | 按位异或 | L-R | 否 |
| | 按位或 | L-R | 否 |
&& | 逻辑与 | L-R | 是 |
|| | 逻辑或 | L-R | 是 |
? : | 条件操作符 | N/A | 是 |
= | 赋值 | R-L | 否 |
+= | 以...加 | R-L | 否 |
-= | 以...减 | R-L | 否 |
*= | 以...乘 | R-L | 否 |
/= | 以...除 | R-L | 否 |
%= | 以...取模 | R-L | 否 |
<<= | 以...左移 | R-L | 否 |
>>= | 以...右移 | R-L | 否 |
&= | 以...与 | R-L | 否 |
^= | 以...异或 | R-L | 否 |
|= | 以...或 | R-L | 否 |
, | 逗号 | L-R | 是 |
一些问题表达式
表达式一
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
a * b + c * d
a * b + c * d + e * f
表达式二
c + -- c;
同上,操作符的优先级只能决定自减–的运算在+运算的前面,但是我们没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
表达式三
表达式四
#include <stdio.h>
#include <stdlib.h>
int fun(){
static int count = 1;
return ++count;
}
int main(){
int answer;
answer = fun() - fun() * fun();
printf("answer: %d\n", answer);
system("pause");
return 0;
}
注意:虽然在大多数的编译器上求得的结果是相同的。
但是上述代码answer = fun() - fun() * fun();中我们只能通过操作符的优先级得知:先算乘法,在算减法。但是函数调用的先后顺序通过操作符的优先级无法确定。