C语言 操作符
一、操作符分类
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
二、算术操作符
+ - * / %
- 除了 % 操作符外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符,如果两个操作符都是整数,就执行整数除法。而只要有浮点数,就执行浮点数除法。
- % 操作符的两个操作符必须为整数。返回的是整除之后的余数。
三、移位操作符
<< 左移操作符
>> 右移操作符
左移操作符 移位规则:
左边抛弃、右边补 0
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int num = 10;
printf("num is %d\n", num);
printf("num<<1 is %d\n", num << 1);
return 0;
}
右移操作符 移位规则:
首先右移运算分两种:
- 逻辑移位:左边用 0 填充,右边丢弃。
- 算术移位:左边用原该值的符号位填充,右边丢弃。
我们拿 -1 来举个例子,看一下逻辑移位和算术移位的区别:
我们也可以在编译器上写代码试一下,查看一下编译器上默认是那种右移方式。
算术右移的二进制码没有改变,逻辑右移的二进制码发生改变。
int i = -1;
printf("%d\n", i >> 1);
警告:对于移位运算符,不要移动负数位,这个标准是未定义的,计算机也不知道怎么操作。
int i = 1;
printf("%d\n", i >> -1); //error
四、位操作符
位操作符有:
& 按位与
| 按位或
^ 按位异或
注:它们的操作数必须是整数。
按位与 &
比较两个数据的二进制,二者都为真(1),结果才有1。其中一个是假(0),结果只能是0。
int a = 3;
int b = 5;
int c = a & b;
//a的二进制位:0011
//b的二进制位:0101
//按位与的结果:0001
printf("%d\n", c); //1
按位或 |
和上面是反过来的。二者其中有个真(1),结果就是1。都是假(0),结果才是0。
int a = 3;
int b = 5;
int c = a | b;
//a的二进制位:0011
//b的二进制位:0101
//按位与的结果:0111
printf("%d\n", c); //7
按位异或^
按二进制位异或。二者的二进制位相同就是假(0),相异不同就是真(1)。
int a = 3;
int b = 5;
int c = a ^ b;
//a的二进制位:0011
//b的二进制位:0101
//按位与的结果:0110
printf("%d\n", c); //6
练习题一:
不能创建临时变量(第三个变量),实现两个数据的交换。
方法一:
int a = 3;
int b = 5;
a = a + b; //a=8
b = a - b; //b=8-5
a = a - b; //a=8-3
printf("%d\n", a);
printf("%d\n", b);
方法二:异或
相当于先把 a 赋值成 a 和 b 的转换器。b和转换器异或,结果是a;a和转换器异或,结果是b。
int a = 3;
int b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("%d\n", a);
printf("%d\n", b);
练习题二:
编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。
答案解析:
可以把整数的二进制位的最后一位,和 1 的二进制位进行按位与 & 操作。结果是 1 就表示该二进制位上有 1 。
把上述操作放在循环中就可以实现查找整个二进制的 1 的个数。
缺点就是,一定要循环32次,效率不高。
//编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。
//把这个整数的二进制位的最后一位,和1进行按位与操作。结果是1就表示该位上有1。
//可以把二进制放在循环中,比较一次就向右移动一位二进制。
int i = 0;
int count = 0;
int num = -1;
for (i = 0; i < 32; i++) {
if( ((num >> i) & 1) == 1) {
count++;
}
}
printf("%d\n", count);
究极蛇皮之优化:
依次从右向左将 1 取出来,直到为零时跳出循环。相比于第二种方法减少了循环次数。
num-1,相当于取出右边的 1 会导致,前面的高位退位。然后把退位的数据(num-1)和原来的数据(num)取余,把二进制退位导致不相同的真假去掉。
//优化
int num = 10; //1010
int count = 0;
//只要非0,二进制一定至少有一个1。
while (num) {
count++;
//num-1导致二进制退位,变成9(1001)。
//然后10和9按位与,把二者二进制中不同的位数变成0。
//最后num被赋值为8(1000)。
num = num & (num - 1);
//8(1000)参与循环,num-1退位变成7(0111)。
//按位与会导致全部变成0000,num被赋值0,程序结束。
//至此count累计为2。
}
printf("%d\n", count);
五、赋值操作符
简单赋值操作符:
赋值操作符是一个很常用的操作符。
int weight = 120; //初始化的时候赋值
weight = 87; //进行重新赋值
double salary = 1000.0;
salary = 2000.0;
赋值操作符可以连续使用,例如:
int a = 10;
int b = 20;
int c = 30;
a = b = c + 1; //连续赋值
printf("%d\n", a);
其实连续赋值很少用,因为看着不方便,而且不易于调试代码。
相同代码可以执行同样的功能,却又不会有上述的缺点。
int a = 10;
int b = 20;
int c = 30;
b = c + 1;
a = b;
printf("%d\n", a);
复合的赋值操作符:
+= -= *= /= %= >>= <<= &= |= ^=
就是把普通的运算符和赋值符合并简写了,效果都是一样的,而且这样写会更加简洁。
int x = 10;
int y = 10;
x = x + 10;
y += 10;
printf("%d\n", x);
printf("%d\n", y);
六、单目操作符:
单目操作符,就是只能有一个数据参与运算的操作符。
都有什么单目操作符呢?
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对数据的二进制按位取反
-- 前置、后置--
++ 前置、后置++
(类型) 强制类型转换
演示代码:
int a = 10;
printf("%d\n", !10); //任何非0都为真,对真(10)取反,结果等于假(0)
printf("%d\n", -10); //就单纯的把数据变成负数
int* p = &a; //取地址,需要用指针变量来接收。
printf("%p\n", p); //输出地址
printf("%d\n", *p); //*p解引用,输出原来的值
printf("%d\n", sizeof(a)); //a的类型是int,所以sizeof计算出有4个字节
//还能有不同的格式
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a); //可以执行吗?
printf("%d\n", sizeof int); //可以执行吗?
sizeof 是一个操作符,不是函数!
我们也可以使用 sizeof 来求数组元素的个数:
sizeof(arr)表示数组整体的字节长度(40),sizeof(arr[0])表示数组首元素的字节长度(4)。
int arr[] = { 1,2,3,4,5 };
printf("%d\n", sizeof(arr) / sizeof(arr[0]));
sizeof 和数组练习题:
请问程序1,2,3,4处输出结果是多少?
#include <stdio.h>
void test1(int arr[]) {
printf("%d\n", sizeof(arr)); //3
}
void test2(char ch[]) {
printf("%d\n", sizeof(ch)); //4
}
int main() {
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr)); //1
printf("%d\n", sizeof(ch)); //2
test1(arr);
test2(ch);
return 0;
}
运行结果:
答案解析:
1 和 2 很好算,分别是 40 和 10。
3 和 4 输出结果都是 4 或者 8(取决于计算机32/64位系统)。
int 类型在内存中占用 4 个字节,arr 数组创建了 10 个 int 类型空间,那就一共要 40 个字节来存放数据。
char 类型在内存中占用 1 个字节,ch 数组创建了 10 个 char 类型空间,那就一共要 10 个字节来存放数据。
至于 3 和 4,是因为它们由指针变量来接收的数据,sizeof 计算指针变量一般结果都是 4。
我们可以看一下 sizeof 单独计算指针变量的代码:
int* p = NULL;
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(p));
结果都是 4。
+ + 和 - - 运算符:
前置 + + 和前置 - - :先让数据自增或者自减,然后再进行操作。
int a = 10;
//先对 a 进行自增,然后赋值给 x。a为11,x为11。
int x = ++a;
printf("%d\n", a);
printf("%d\n", x);
//先对a进行自减,然后赋值给 y。a为10,y为10。
int y = --a;
printf("%d\n", a);
printf("%d\n", y);
后置 + + 和后置 - - :先让数据参加操作,然后再进行自增或者自减。
int a = 10;
//先把a的值赋值给x,然后a自行+1。此时a=11,x=10。
int x = a++;
printf("%d\n", a);
printf("%d\n", x);
//先把a的值赋值给y,然后a自行-1。此时a=10,y=11。
int y = a--;
printf("%d\n", a);
printf("%d\n", y);
七、关系操作符
> 大于
>= 大于等于
< 小于
<= 小于等于
!= 不相等
== 相等
关系运算符比较简单,就是字面意思。不过要注意,编程过程中可能把 == 和 = 写混。
八、逻辑操作符
&& 逻辑与 需要两边都是真,才是真
|| 逻辑或 两边都是假,才是假
逻辑与 && 和逻辑或 || 的特点:
让我们看一段代码,请问输出结果是多少?
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf(" a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);
结果如下:
为什么呢?
因为逻辑与 && 的特点,它只要在表达式中判断第一个值(a++)的结果,如果是假(0),那么它不继续判断(++b,和d++)了。
所以a++被执行判断过一次,后置++,a=1。结果就是1、2、3、4。
我们看一下段代码,请问输出结果是多少?
int i = 0, a = 0, b = 2, c = 3, d = 4;
//i = a++ && ++b && d++;
i = a++ || ++b || d++;
printf(" a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);
结果如下:
逻辑或 || 。
它先判断a++,发现a是假(因为是后置++,所以a=0先参与运算);就判断下一位++b,发现b是真(因为是前置++,所以b=2先加一再参与运算)。
有一个真就可以不继续判断了,所以d++没有执行到。最终结果是1、3、3、4。
小总结:
通过上面两个例子,我们发现逻辑与 && 和逻辑或 || 有个特点,只要前面一个判断为假/真,逻辑与&&/逻辑真||就会停止判断下去。
逻辑与 && 就好像张三和李四一起才能完成任务,如果前者张三没有去,后者李四去不去都无所谓了(不判断了)。
逻辑或 || 就好像,张三和李四任意一个人就可以完成任务,如果前者张三去了,后者李四去不去都无所谓了。
九、条件操作符
exp1 ? exp2 : exp3;
exp1:是一个判断表达式,结果为真,就执行exp2;结果为假,就执行exp3。
例题:
这是一个简单的判断大小代码,请问怎么改写成条件表达式?
int a = 10;
if (a>5)
a = 1;
else
a = -1;
改写:注意条件表达式有返回值,需要有个变量接收。
int a = 10;
int b = a > 5 ? 1 : -1;
练习题:
使用条件表达式实现找到两个数的较大值。
答案代码:
十、逗号表达式
exp1, exp2, exp3, ...expN
逗号表达式,就是用括号隔开的多个表达式。
从左往右依次执行,整个表达式的结果是最后一个表达式的结果。
请问下面代码分别输出的结果是多少?
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d\n", c);
int a = 3;
int b = 5;
int c = 0;
int d = (c = 5, a = c + 3, b = a - 4, c += 5);
printf("%d\n", d);
我们通过上面两个题,是不是感觉逗号表达式没什么大用啊!最后面那个表达式才把值赋值给前面的变量。
其实逗号表达式用于代码的优化,使得程序更加美观。
例如:循环外和循环内都有同样的代码要执行。
a = get_val();
count_val(a);
while (a > 0) {
a = get_val();
count_val(a);
}
优化:代码还是一样执行,while判断放在逗号表达式最后也不会报错。
while (a = get_val(), count_val(a), a > 0) {
}
十一、下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
int arr[10]; //创建数组
arr[9] = 10; //实用下标引用操作符
[ ] 的两个操作数是 arr 和 9。
2. ( ) 函数调用操作符
接收一个或者多个操作数,
第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
void test1() {
printf("hehe\n");
}
void test2(const char * str) {
printf("%s\n", str);
}
int main() {
test1(); //使用()作为函数调用的操作符
test2("hello~"); //使用()作为函数调用的操作符
return 0;
}
3. 访问一个结构的成员
.结构体 .成员名
->结构体指针 ->成员名
struct Stu {
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu) {
stu.age = 18;
}
void set_age2(struct Stu* pStu) {
pStu->age = 18; //结构成员访问
}
int main() {
struct Stu stu;
struct Stu* pStu = &stu; //结构成员访问
stu.age = 20; //结构成员访问
set_age1(stu);
pStu->age = 20; //结构成员访问
set_age2(pStu);
return 0;
}
十二、表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
有些表达式的操作数在求值的过程中,可能需要转换为其他类型。
隐式类型转换
C的整型算术运算,总是至少以默认的整型类型精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
什么是整型提升?
表达式的整型运算,要在CPU的相应运算器中执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是 int 的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使是两个 char 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU,难以直接实现两个 8 bite 字节直接相加运算。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入CPU去执行运算。
//实例1
char a,b,c;
a = b + c;
b 和 c 的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果被截断,然后再存储于 a 中。
如何进行整型提升呢?
整型提升是按照变量的数据类型的符号位来提升的
//负数的整型提升
char c1 = -1;
变量c1的二进制为(补码)中只有 8 个比特位:11111111
因为 char 为有符号的 char
所以整型提升,高位补符号位 1 。
提升之后结果是:
11111111111111111111111111111111
//正数的整型提升
char c2 = 1;
变量 c2 的二进制为(补码)中只有8个比特位:00000001
因为 char 为有符号的 char
所以整型提升,高位补符号位 0。
提升之后结果为:
00000000000000000000000000000001
//无符号整型提升,高位补 0 即可
说了这么多,我们下面看一下例子:
实例1:请问最后输出结果是多少?
int main() {
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
return 0;
}
实例1解析:
a = 3,在二进制中为 00000011。
b = 127,在二进制中为 01111111。
根据整型提升理论,char类型的 a 和 b 在参与运算要提升至 int 型参与运算。
a就变成00000000000000000000000000000011;
b就提升为00000000000000000000000001111111;
参与运算,结果c为00000000000000000000000010000010。
运算结束,c被截断为char,10000010。
这时你想把二进制转算成整型输出了?其实还早。因为二进制最前面符号位出现了 1,表示这个整型是负数。我们都知道,负数的整型,二进制是补码参与运算,我们要把补码转换成原码才能是最后的结果。
c:补码10000010,补码-1得到反码10000001;符号位不变,其它取反得到原码11111110。
OK(^ o ^)/~,根据原码11111110,我们换算 c 结果是-126。
实例2
请问下列代码最终输出结果是多少?
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6) {
printf("a");
}
if (b == 0xb600) {
printf("b");
}
if (c == 0xb6000000) {
printf("c");
}
因为 a,b 要进行整型提升,所以 a,b 提升后的结果和原本数据不一样,if 判断为假不输出。
c 不需要整型提升,则 if 判断结果为真。
程序最后输出结果是:c
实例3
试着输出一下下列代码,体会一下整型提升~
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
代码中 c 没有参与运算的时候,就没有发生整型提升,sizeof(c)的结果是 1 个字节。
当参与运算,例如(+c)或者(-c),就会发生整型提升,所以 sizeof(+c)和 sizeof(-c)结果是 4 个字节。
十二、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数类型转换为另一个操作数类型,否则不能进行运算。下面的层次体系称为寻常算术转换(类型由大到小排列)。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型之后,再执行运算。
注意:算术转换要合理,不然会有一些潜在的问题。
float f = 3.14;
int num = f; //转换给一个比自己小的数据类型,会导致精度丢失。
十三、操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级 -> 就是谁先谁后。
- 操作符的结合性
例如:L-R 的结合性,表示语句从左向右执行。 - 是否控制求值顺序
例如:逻辑或 | |,前面为真,后面就不执行了。
下面举些例子(优先级从高到低):
具体还是要自行百度…
一些问题表达式
//表达式的求值部分由操作符的优先级决定。
//表达式1
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
//表达式2
c + --c;
同上,操作符的优先级导致自减 - - 的运算在 + 的前面,但是我们没有办法得知,+ 操作符的左操作数的获取,是在右操作数操作之前还是之后。因此,结果是不可预测的,是有歧义的。
//代码3-非法表达式
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
此段代码更是离谱,在不同编译器中的测试,都是不同结果。
值 编译器
—128 Tandy 6000 Xenix 3.2
—95 Think C 5.02(Macintosh)
—86 IBM PowerPC AIX 3.2.5
—85 Sun Sparc cc(K&C编译器)
—63 gcc,HP_UX 9.0,Power C 2.0.0
4 Sun Sparc acc(K&C编译器)
21 Turbo C/C++ 4.5
22 FreeBSD 2.1 R
30 Dec Alpha OSF1 2.0
36 Dec VAX/VMS
42 Microsoft C 5.1
//代码4
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
该代码也是有问题的!
我们可以通过操作符的优先级得知,answer = fun() - fun() * fun(); 先算乘法,再算减法。
但是函数的调用先后顺序无法通过操作符的优先级确定!我们不知道哪个 fun() 会先调用,这会导致结果不一致。
//代码5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这段代码在 Linux 和 window 环境下的编译器结果都不一致。
相同的代码产生不同结果,这是为什么?
这和汇编代码有点关系,代码段的第一个 + 在执行的时候,第三个 ++ 是否要执行呢?
这是不确定的,因为依靠操作符的优先级和结合性,无法判断第一个 + 和第三个前置 ++ 的先后顺序。
总结:我们写出的代码,如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是有问题的!