5.操作符
5.1 算术操作符
+ - * / %
-
除%操作符之外,其他几个操作符可以作用于整数和浮点数。
-
对于/操作符如果两个操作符都为整数,执行整数除法。只要有一个浮点数,则执行浮点数除法。
float num = 1 / 2; // num=0.000000; float num = 1 / 2.0; // num=0.500000; // 这里的2.0 是double类型 而num为float类型 // 可以做如下调整 float num = 1f / 2.0f; double num = 1 / 2.0;
-
%操作符两端必须都是整数,可以是负数,但不能为0。
5.2 移位操作符
>> <<
5.2.1 非负整数的二进制表示形式
- 原码:直接根据数值写出二进制
- 反码:原码符号位不变,其他位按位取反
- 补码:反码加1
// 非负整数在内存中存放的是原码(非负整数的原码、反码、补码相同)
// 负整数在内存中存放的是补码
5.2.2 左移
算数左移和逻辑左移相同,都是左边抛弃,右边补0。
5.2.3 右移
-
算术右移(通常采用)
向右移动,首位补原二进制位的第一位
-
逻辑右移
向右移动,首位补0
// 只能移动非负整数位
int main()
{
int ret = -1 >> 1;
printf("%d\n", ret);
return 0;
}
5.3 位操作符
& | ^ // 操作数必须为整数
5.3.1 按位与
int main()
{
int ret = 3 & 4;
// 00000000000000000000000000000011
// &
// 00000000000000000000000000000100
printf("%d\n", ret);
return 0;
}
5.3.2 按位或
// 按位或
int main()
{
int ret = 3 | 4;
// 00000000000000000000000000000011
// |
// 00000000000000000000000000000100
printf("%d\n", ret);
return 0;
}
5.3.3 按位异或
相同为0,相异为1
int main()
{
int ret = 3 ^ 4;
// 00000000000000000000000000000011
// ^
// 00000000000000000000000000000100
printf("%d\n", ret);
return 0;
}
// 奇数^奇数=偶数
// 奇数^偶数=奇数
// 偶数^偶数=偶数
5.3.4 题目
交换两个int变量的值,不使用第三个变量
-
方法一
int main() { int a = 3; int b = 4; a = a + b; b = a - b; a = a - b; printf("a=%d, b=%d\n", a, b); return 0; } // 问题:数字过大,内存溢出
-
方法二
int main() { int a = 3; int b = 4; a = a ^ b; b = a ^ b; a = a ^ b; printf("a=%d, b=%d\n", a, b); return 0; } // 没有进位,不可能发生溢出 // 异或特点: // 1.任何数和他本身异或,结果为0 // 2.任何数和0异或,结果为他本身 // 底层逻辑: // b = a ^ b ^ b = a (前两行代码) // a = a ^ b ^ b ^ a ^ b = b (第三行代码)
5.3.5 练习
编写代码实现:求一个整数存储在内存中的二进制中1的个数
-
方法一
int main() { int num = 5; int count = 0; for (int i = 0; i < 32; i++) { if (num % 2 == 1) { count++; } num = num >> 1; } printf("count=%d\n", count); return 0; }
缺点:不能统计负数的二进制中1的个数。
-
方法二
int main() { int num = -5; int count = 0; for (int i = 0; i < 32; i++) { if ((num >> i) & 1) { count++; } } printf("count=%d\n", count); return 0; } // -5 // 原码 10000000000000000000000000000101 // 补码 11111111111111111111111111111011 // 右移0位 // 11111111111111111111111111111011 & 00000000000000000000000000000001 // 00000000000000000000000000000001 -> 1 // 右移一位 // 01111111111111111111111111111101 & 00000000000000000000000000000001 // 00000000000000000000000000000001 -> 1 // 右移二位 // 00111111111111111111111111111110 & 00000000000000000000000000000001 // 00000000000000000000000000000000 -> 0 // ... // 右移31位 // 00000000000000000000000000000001 & 00000000000000000000000000000001 // 00000000000000000000000000000001 -> 1
-
方法三
int main() { int num = -5; int count = 0; while (num) { num = num & (num - 1); count++; } printf("count=%d\n", count); return 0; } // num & (num - 1) // 11111111111111111111111111111011 => num // 11111111111111111111111111111011 & (11111111111111111111111111111011 - 1) // 11111111111111111111111111111011 & 11111111111111111111111111111010 // 11111111111111111111111111111010 => num // 11111111111111111111111111111010 & (11111111111111111111111111111010 - 1) // 11111111111111111111111111111010 & 11111111111111111111111111111001 // 11111111111111111111111111111000 => num // ... // 每次把num&(num-1)的结果再赋值给num // 每num&(num-1)运算一次,就会将num中的1变成0 // 通过n次num&(num-1)运算使得num=0 // 此时n为num的二进制中1的个数
5.4 赋值操作符
= += -= *= /= %= >>= <<= &= |= ^=
// 连续赋值:
int a = 0;
int b = 0;
int c = 1;
a = b = c + 1; // 从由向左赋值(不推荐)
5.5 单目操作符
只有一个操作数
! // 逻辑反操作
- // 负
+ // 正
sizeof // 操作数类型长度
~ // 按位取反
-- // 前置 后置
++ // 前置 后置
* // 解引用(间接访问)操作符
& // 取址
(type) // 强制类型转换
5.5.1 sizeof
sizeof是一个操作符,不是函数。单位:字节
-
例1
int main() { short s = 5; int a = 10; printf("%d\n", sizeof(s = a + 2)); // sizeof括号中放的表达式,不参与运算 // 在编译期间处理sizeof(s = a + 2); 而s = a + 2在程序运行时计算 // 大数据放在小空间里被截断 printf("%d\n", s); // 5 return 0; }
-
例2
int main() { int a = 10; printf("%d\n", sizeof(a)); printf("%d\n", sizeof a); printf("%d\n", sizeof(int)); //printf("%d\n", sizeof int); char arr[10] = { 0 }; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof arr); printf("%d\n", sizeof(char [10])); }
-
例3
void foo1(int arr[10]) { printf("%d\n", sizeof(arr)); } void foo2(char arr[10]) { printf("%d\n", sizeof(arr)); } int main() { int arr1[10] = { 0 }; char arr2[10] = { 0 }; printf("%d\n", sizeof(arr1)); // 40 printf("%d\n", sizeof(arr2)); // 10 foo1(arr1); // 4或8 foo2(arr2); // 4或8 return 0; } // 数组作为函数参数传递时,实际传递的是数组的指针
5.5.2 ~
按位取反
int main()
{
int a = 13;
// 00000000000000000000000000001101
// 将a的二进制位第五位置零
// 00000000000000000000000000001101 -> 13的二进制
// 00000000000000000000000000010000 -> 1<<4
// | -> 按位或
// 00000000000000000000000000011101 -> 结果29
a = a | (1 << 4);
printf("%d\n", a);
// 将a的二进制位第五位置一
// 00000000000000000000000000011101 -> 29的二进制
// 11111111111111111111111111101111 -> ~(1<<4)
// & -> 按位与
// 00000000000000000000000000001101 -> 结果13
a = a & (~(1 << 4));
printf("%d\n", a);
return 0;
}
5.5.3 ++/—
// 前置:先++,再使用
// 后置:先使用,再++
// (使用包括赋值和函数传参)
-
后++
-
先++
-
垃圾代码
int main() { int a = 1; int b = (++a) + (++a) + (++a); printf("%d\n", b); return 0; } // 运行结果:vs2019中结果是12 // linux中结果是10 // 垃圾代码,不做过多研究
5.5.4 & *
& // 取址
* // 解引用
-
例
int main() { int num = 100; // &获取对象所处的内存地址,取址操作符 int* pn = # // 此处*不是操作符,仅说明pn是一个指针变量 *pn = 200; // 此处*才是解引用操作符,将200赋值给pn所指的对象 printf("pn=%p, num=%d\n", pn, num); return 0; }
5.5.5 (type)
int main()
{
int num = (int)3.1315; // 强制类型转换
printf("%d\n", num);
return 0;
}
5.6 关系操作符
> >= < <= != ==
// = 赋值
// == 判断是否相等
// 比较两个字符串是否相等不能使用==
5.7 逻辑操作符
&& 逻辑与:从前往后找到第一个为假的值返回0,否则返回1
|| 逻辑或:从前往后找到第一个为真的值返回1,否则返回0
5.7.1 习题
如下代码运行结果:
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++ || ++b || d++;
printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
}
// &&: 左边确定为假,后边不用再进行计算
// ||: 左边确定为真,后边不用再进行计算
5.8 三目操作符
exp1 ? exp2 : exp3;
// 如果exp1为真,则计算exp2,整个表达式的结果为exp2的结果,exp3不计算
// 如果exp2为假,则计算exp3,整个表达式的结果为exp3的结果,exp2不计算
-
例
int main() { int a = 3; int b = 4; int ret = 0; ret = a > b ? a++ : b++; printf("%d\n", ret); printf("a=%d, b=%d\n", a, b); return 0; }
5.8 逗号表达式
exp1, exp2, exp3;
// 从前往后依次执行,整个表达式的结果为最后一个表达式exp3的计算结果
5.9 下标引用
下标引用操作符有两个操作数,数组名和下标。
int main()
{
int arr[10] = { 0 }; // 此处[]不是操作符,仅用来指定数组的大小
arr[4] = 100; // 此处[]为操作符,指定数组中索引(或下标)为4的值为100
printf("arr[4]=%d\n", arr[4]);
return 0;
}
5.10 函数调用
有一个或多个操作数,当没有参数时,只有函数名一个操作数;当有参数时,操作时个数为参数个数+1。
// 函数调用
int Add(int x, int y) // 此处()不是操作符,仅用来说明形参及其类型
{
return x + y;
}
int main() {
int ret = Add(3, 4); // 此处()为函数调用操作符,即使没有参数,也要有() 操作数:Add 3 4共3个
printf("%d\n", ret);
return 0;
}
5.11 结构成员访问操作符
.
->
// 使用格式:
结构体变量.结构体成员变量名
结构体指针变量->结构体成员变量名
-
例
int main() { struct Person { char name[10]; int age; float height; }; printf(".操作符\n"); struct Person p = { "listen", 22, 185.0 }; printf("name: %s\n", p.name); printf("age: %d\n", p.age); printf("height: %f\n", p.height); struct Person* pp = &p; printf("name: %s\n", (*pp).name); printf("age: %d\n", (*pp).age); printf("height: %f\n", (*pp).height); printf("->操作符\n"); printf("name: %s\n", pp->name); printf("age: %d\n", pp->age); printf("height: %f\n", pp->height); return 0; }
5.12 表达式求值
表达式求值的顺序一般是由操作符的优先级和结合性决定。同样,有些表达式的操作数再求值的过程中可能需要转换为其他类型。
5.12.1 隐式类型转换
C的整形算数运算总是至少以缺省整形类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整形,这种转换称为整形提升。
包括:
- 整形提升
- 算数转换
5.12.2 整型提升
-
概念
在表达式计算时,各种整形(只有比int小的类型才会发生整型提升)首先要提升为int类型,如果int类型不足以表示的话,就需要提升为unsigned int类型,然后再执行表达式的运算。
-
意义
虽然机器指令中可能有现两个8比特字节这种字节相加指令,但是一般用途的CPU是难以直接实现这样的字节相加运算的。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。而表达式的整型运算要在CPU的相应运算器件内执行。因此,两个char类型的数进行相加运算时,是在CPU中执行,自然而然的需要先转换为CPU内整型操作数的标准长度。
-
如何提升
按照最高位进行整型提升
- 正数整型提升:高位补0
- 负数整形提升:高位补1
- 无符号整数整形提升:高位补0
-
例1
int main() { char a = 3; char b = 127; char c = a + b; printf("%d\n", c) return 0; } // 原理 char a = 3; //二进制: 00000011 char b = 127; //二进制:01111111 char c = a + b; // + 操作符 整型提升 //a 提升: 00000000000000000000000000000011 //b 提升: 00000000000000000000000001111111 // 相加: 00000000000000000000000010000010 //c 截断: 10000010 printf("c=%d\n", c); // c是char类型,而使用%d打印,c 整形提升(高位补1) // 11111111111111111111111110000010 补码 // 11111111111111111111111110000001 反码(补码-1) // 10000000000000000000000001111110 原码(反码符号位不变,其余位取反)
-
例2
int main() { char a = 0xFF; // a整形提升之后为11111111111111111111111111111111 short b = 0xFFFF; // b整形提升之后为11111111111111111111111111111111 int c = 0xFFFFFFFF; if (a == 0xFF) { printf("a\n"); } if (b == 0xFFFF) { printf("b\n"); } if (c == 0xFFFFFFFF) { printf("c\n"); } printf("%d\n", a == b); return 0; } // 其中a, b需要进行整形提升,而c不需要整形提升 // a, b整形提升之后变成了负数,所以a==0xFF和b == 0xFFFF为假
-
例3
int main() { char a = 1; printf("%u\n", sizeof a); printf("%u\n", sizeof -a); // 类型提升为int类型 printf("%u\n", sizeof +a); printf("%u\n", sizeof !a); // vs2022中为1 gcc中为4 return 0; }
5.12.3 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转化为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转化为另一个操作数的类型后执行运算。
-
例
int main() { float a = 1.23; int b = 5; float c = 0; c = a + b; // a为float类型,b为int类型, // float类型值和int类型值相加时, // int类型值被转换为float类型 printf("%f\n", c); return 0; }
5.13 表达式的属性
- 值属性(运行后才能确定)
- 类型属性(可推断,编译时确定)
5.14 操作符的属性
- 复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序(例如
&&
||
,
)
5.14.1 优先级
运算符的优先级确定表达式中项的组合。如果优先级相同,则根据结合性确定计算顺序。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ - - | 从左到右 |
一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | ||
逻辑与 AND | && | 从左到右 |
逻辑或 OR | ||
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %=>>= <<= &= ^= | = |
逗号 | , | 从左到右 |
5.14.2 一些问题表达式
-
表达式1
a * b + c * d + e * f; // 计算原理: // 方法1: a * b; c * d; e * f; a * b + c * d + e * f // 方法2: a * b; c * d; a * b + c * d; e * f; a * b + c * d + e * f;
-
表达式2
c + --c; // 无法确定左操作数是在--c之前还是--c之后确定 // 计算原理: // 方法1: --c; c; c + --c; // 方法2: c; --c; c + --c;
-
代码3-错误代码
int main() { int i = 10; i = i-- - --i * (i = -3) * i++ + ++i; printf("i = %d\n", i); return 0; } // 输出结果在不同的编译器中结果不同
-
代码4- 错误代码
int fun() { static int count = 1; return ++count; } int main() { int answer; answer = fun() - fun() * fun(); // 2 - 3 * 4 (vs2022中) printf("%d\n", answer); // -10(vs2022中) return 0; } // 结果不可控,fun()函数的调用顺序不同,结果不同
-
代码5-错误代码
int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); } // VS -> 12 // 执行原理 ++i; ++i; ++i; i = 4 4 + 4 + 4; // gcc -> 10 ++i; ++i; i = 3; 3 + 3; ++i; i = 4; 3 + 3 + 4;
5.14.3 总结
对于同一个表达式或一段代码,即使有确定的优先级和结合性,仍然有不确定的计算或运行方式,且不同方式计算出来的结果不同,则该表达式或代码是存在问题的,不应该使用这样的表达式和代码。