本文是对【初识C语言】中操作符部分的详细解读。自学b站”鹏哥C语言“笔记。
操作符分类:
- 算数操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
- 表达式求值
算数操作符
加+ 减- 乘* 除/ 取余%
- 加+ 减- 乘* 除/ 可以用于整数和浮点数,取余% 只能作用于整数。
- 除/ 如果两个操作符都是整数,则执行整数除法;只要有浮点数,则执行浮点数除法。
- 除/ 得到的结果是商。
例1:除/ 示例
#include <stdio.h>
int main()
{
int a = 5 / 2;//两个操作符都是整数,执行整数除法,即商2余1
printf("a = %d\n", a);//a = 2
int a = 5 / 2.0;//有一个操作符是浮点数,执行浮点数除法,即商2.5,默认6位小数
printf("a = %d\n", a);//a = 2.500000
return 0;
}
移位操作符
左移<< 右移>>
- 只能作用于整数。
- 移动的是二进制位。
- 右移一位有除以2的效果,右移一位有乘2的效果。
- 不要移动负数位,如
a>>-1
是错误的。
右移操作符
- 算数右移(一般情况下都是这种):整体右移,右边丢弃,左边补原符号位(正0负1)
- 逻辑右移:整体右移,右边丢弃,左边补0
左移操作符
原码、反码、补码
结论:
① 有符号的整型,第一位数是符号位(正0负1)
② 只要是整型,内存中存储的都是二进制补码
③ 正整数:原码、反码、补码 相同
负整数:原码 → 符号位不变,其它位取反 → 反码 → +1 → 补码
补码 → -1 → 反码 → 符号位不变,其它位取反 → 原码
例1:正整数右移1位
#include <stdio.h>
int main()
{
int a = 16;
int b = a >> 1;
printf("%d\n", b);//输出结果是原码,是8
return 0;
//a的原码=反码=补码:00000000000000000000000000010000
//b的补码,a右移1位:00000000000000000000000000001000
//b的反码,-1:1111111111111111111111111110111
//b的原码,符号位不变其它位取反:1000000000000000000000000001000,即8
//实际上,正整数可以直接由a原码右移1位得到b原码
}
例2:负整数右移1位
#include <stdio.h>
int main()
{
int a = -1;
int b = a >> 1;
printf("%d\n",b);//输出结果是原码,是-1
return 0;
//a的原码,即-1的原码:10000000000000000000000000000001
//a的反码,符号位不变其它位取反:11111111111111111111111111111110
//a的补码,+1:11111111111111111111111111111110
//b的补码,a右移1位:11111111111111111111111111111111
//b的反码,-1:1111111111111111111111111111110
//b的原码,符号位不变其它位取反:1000000000000000000000000000001,即-1
}
位操作符
按位与& 按位或| 按位异或^
- 三者的操作数都必须是整数。
- “按位”按照的是二进制位补码。
- 真1假0。
按位与&
同位都为1(真)才为1
#include <stdio.h>
int main()
{
a = 3;
b = 5;
int c=a&b;
//a是00000000000000000000000000000011
//b是00000000000000000000000000000101
//同位都为1(真)才为1,c=00000000000000000000000000000001
printf("%d\n", c);//输出为1
return 0;
}
按位或|
同位只要有1(真)就为1
#include <stdio.h>
int main()
{
a = 3;
b = 5;
int c=a|b;
//a是00000000000000000000000000000011
//b是00000000000000000000000000000101
//同位只要有1(真)就为1,c=00000000000000000000000000000111
printf("%d\n", c);//输出为7
return 0;
}
按位异或^
同位相同为0,相异为1
#include <stdio.h>
int main()
{
a = 3;
b = 5;
int c=a^b;
//a是00000000000000000000000000000011
//b是00000000000000000000000000000101
//同位相同为0,相异为1,c=00000000000000000000000000000110
printf("%d\n", c);//输出为6
return 0;
}
例1(灵活题):在不创建临时变量的条件下,实现两个数的交换
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("before: a = %d b = %d\n", a, b);
a = a + b;//a=8,b=5
b = a - b;//a=8,b=3
a = a - b;//a=5,b=3
printf("after: a = %d b = %d\n", a, b);
return 0;
}
实际上,上述代码是存在缺陷的,一旦a和b的值都很大,a+b有可能会溢出,导致部分值缺失。接下来我们转变思路,用位操作符进行优化。
优化代码:异或的方法
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("before: a = %d b = %d\n", a, b);
a = a ^ b;//a011,b101 → a110,b101
b = a ^ b;//→ a110,b011
a = a ^ b;//→ a101,b011
printf("after: a = %d b = %d\n", a, b);
return 0;
}
例2:求一个整数存储在内存中的二进制中1的个数
#include <stdio.h>
int main()
{
int num = 0;
int count = 0;
scanf("%d", &num);
while(num)
{
if(num % 2 == 1)
count++;
num = num / 2;
}
printf("%d\n", count);
return 0;
}
上述代码可以正确处理正整数,但在处理负整数上是错误的。下面用位操作符进行改进。
改进代码:
#include <stdio.h>
int main()
{
int num = 0;
int count = 0;
scanf("%d", &num);
int i = 0;
for(i=0; i<32; i++)
{
if(1 == ((num >> i) & 1))
count++;
}
printf("%d\n", count);
return 0;
}
赋值操作符
= += -= *= /= %= >>= <<= &= |= ^=
- 赋值操作符的作用是给变量重新赋值。
- 赋值操作符可以连续使用,如
a = x = y+1
,但不建议这样写。
单目操作符
! |
逻辑反操作 |
- |
负值 |
+ |
正值 |
& |
取地址 |
* |
解引用 |
sizeof |
操作数的类型长度(单位:字节) |
~ |
对一个数的二进制按位取反 |
++ |
前置、后置++ |
-- |
前置、后置-- |
(类型) |
强制类型转换 |
逻辑反操作!
真1假0
!真 = 0,!假 = 1
例1:
#include <stdio.h>
int main()
{
//c语言中0表示假,所有非0表示真
int a = 10;
int b = 0;
printf("%d\n", !a);//输出0
printf("%d\n", !b);//固定输出1
return 0;
}
例2:常见用法 放在if的判断语句中
#include <stdio.h>
int main()
{
int a = 0;
if(a)//如果a为真,则执行
{
……
}
if(!a)//如果a为假,则执行
{
……
}
return 0;
}
取地址&,解引用*
例1:
int main()
{
int a = 10;
int* p = &a;//指针变量p:用来存放地址
*p = 20;//*:解引用操作符
return 0;
}
操作数的类型长度sizeof
sizeof 计算的是变量/类型所占空间的大小,单位是字节。
例1:
#include <stdio.h>
int main()
{
int a = 10;
char c = 'r';
char* p = &c;
int arr[10] = {0};
printf("%d\n", sizeof(a));//输出4
printf("%d\n", sizeof(int));//输出4
printf("%d\n", sizeof a);//输出4,说明可省略括号
printf("%d\n", sizeof int);//报错!说明不可省略括号
printf("%d\n", sizeof(c));//输出1
printf("%d\n", sizeof(char));//输出1
printf("%d\n", sizeof(p));//输出4
printf("%d\n", sizeof(char*));//输出4
printf("%d\n", sizeof(arr));//输出40
printf("%d\n", sizeof(int [10]));//输出40
return 0;
}
例2(易错):
#include <stdio.h>
int main()
{
short s = a + 5;
int a = 10;
printf("%d\n", sizeof(s = a + 5));
printf("%d\n", s);
}
输出结果是 2 0
解析:
- 赋值给哪个变量,最终就是哪个变量的类型,故
s = a + 5
的类型是short。short类型存储空间是2字节。
- sizeof( )中的表达式是不进行真实运算的,所以s的值没有被改变,仍然是0。
对一个数的二进制按位取反~
“按位”按照的是二进制位补码
例1:
#include <stdio.h>
int main()
{
int a = 0;//4个字节,32bit位
int b = ~a;
printf("%d\n",b);//输出结果是原码,是-1
return 0;
//有符号的整型,第一位数是符号位(0为正,1为负)
//只要是整数,内存中存储的都是二进制【补码】
//正数:原码、反码、补码 相同
//负数:原码 → 符号位不变,其它位取反 → 反码 → +1 → 补码
// 补码 → -1 → 反码 → 符号位不变,其它位取反 → 原码
//00000000000000000000000000000000
//补码:11111111111111111111111111111111
//→反码:1111111111111111111111111111110
//→原码:1000000000000000000000000000001,即-1
}
例2(灵活):①把a补码右边数起第三位改为1,②改回原来的a
#include <stdio.h>
int main()
{
int a = 11;
a = a | (1<<2);
printf("%d\n", a);//15
a = a & (~(1<<2));
printf("%d\n", a);//11
return 0;
}
例3:sizeof和数组
#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);
return 0;
}
16-19行代码的输出结果:40 10 4或8 4或8
解析:
16行:arr[10]是10个int,所占空间为10个元素×4个字节=40个字节
17行:ch[10]是10个char,所占空间为10个元素×1个字节=10个字节
18行:经函数传参后的arr是arr[10]首元素的地址,一个地址所占空间是4或8(由设备决定)
19行:经函数传参后的ch是ch[10]首元素的地址,一个地址所占空间是4或8(由设备决定)
前置、后置++ --
例1:
#include <stdio.h>
int main()
{
int a = 10;
int b = a++;//后置++,先使用,自身再+1
printf("a = %d b = %d\n",a,b);//a = 11 b = 10
int b = ++a;//前置++,自身先+1,再使用
printf("a = %d b = %d\n",a,b);//a = 11 b = 11
int b = a--;//后置--,先使用,自身再-1
printf("a = %d b = %d\n",a,b);//a = 9 b = 10
int b = --a;//前置++,自身先-1,再使用
printf("a = %d b = %d\n",a,b);//a = 9 b = 9
return 0;
}
关系操作符
> >= < <= != ==
注意:“==”和“=”的区别,编程过程中容易出错。
逻辑操作符
逻辑与&& 逻辑或||
注意:区分逻辑与 逻辑或 和 按位与 按位或
-
逻辑与 逻辑或 是判断值本身的真假,结果只可能是1或0
-
按位与 按位或 是判断二进制补码的真假
1&&2 → 1
1&2 → 0
1||2 → 1
1|2 → 3
例1(易错):
#include <stdio.h>
int main()
{
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);
int i = 0, a = 1, 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);
int i = 0, a = 1, 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);
return 0;
}
输出结果:
6行:a = 1 b = 2 c = 3 d = 4
10行:a = 2 b = 3 c = 3 d = 5
14行:a = 2 b = 2 c = 3 d = 4
解析:
5行:a++是先使用再自身+1,a=0,遇到第一个&&就已经可以确定i=0,因此后面的++b && d++
都没有执行,那么b和d的值也就没有变化。
9行:同理,因为没有出现0,&&一直执行下去,则abd都给自身+1。
13行:a=1,非零,遇到第一个||就已经可以确定i=1,因此后面的++b && d++
都没有执行,那么b和d的值也就没有变化。
结论:
逻辑与和逻辑或 只要能推出结果,则终止读取代码。
- 逻辑与&&左边只要是0,右边不管是什么都不再读取,直接得到结果为0
- 逻辑或||左边只要是非零,右边不管是什么都不再读取,直接得到结果为1
条件操作符/三目操作符
exp1 ? exp2 : exp3
若满足exp1,则执行exp2;若不满足,则执行exp3
例1:
int main()
{
int a = 10;
int b = 20;
int max = 0;
max = (a > b ? a : b);
//若?前表达式为真,则值为a,否则值为b
return 0;
}
逗号表达式
exp1, exp2, exp3, …, expN
从左向右依次执行,最后一个表达式的结果就是整个表达式的结果。
例1:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);
return 0;
}
//最终c值为13,其中a>b,a,都没有输出值
例2:
int main()
{
a = get_val();
count_val(a);
while(a>0)
{
……
a = get_val();
count_val(a);
}
}
等价于
int main()
{
while(a = get_val(), count_val(a), a>0)
{
……
}
}
下标引用、函数调用和结构成员
下标引用[ ] 函数调用() 访问一个结构的成员 . 和 ->
下标引用操作符[ ]
下标引用的操作数是:一个数组名 + 一个索引值
例1:
int main()
{
int a[10] = {0};
a[4] = 10;//使用下标引用操作符,[ ]的两个操作数是a和4
return 0;
}
函数调用操作符( )
函数调用的操作数是:一个函数名 + 函数的参数
例1:
#include <stdio.h>
int get_max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 20;
int max = get_max(a, b);//使用函数调用操作符,( )的操作数是get_max,a,b
printf("max = %d\n", max);
return 0;
}
访问一个结构的成员 . 和 ->
结构体 . 成员名
结构体指针 -> 成员名
例1:
//创建一个结构体类型(struct) ,类型名叫Stu
//所谓结构体类型,和int,float等是类似的
struct Stu
{
//放学生的一些相关属性
char name[20];
int age;
char id[20];
}
int main()
{
//使用struct Stu创建了一个对象s1,并初始化
struct Stu s1 = {"张三", 20, "20230104"};
//用.来访问结构的成员
printf("%s\n", s1.name);
printf("%d\n", s1.age);
printf("%s\n", s1.id);
struct Stu* ps = &s1;
printf("%s\n", (*ps).name);
printf("%d\n", (*ps).age);
printf("%s\n", (*ps).id);
//等价于 用->来访问结构的成员
printf("%s\n", ps->name);
printf("%d\n", ps->age);
printf("%s\n", ps->id);
return 0;
}
表达式求值
- 表达式求值的顺序一部分是由操作符的优先级和结合性决定的。
- 有些表达式的操作数在求值过程中可能需要转换为其他类型。
操作符的属性
复杂表达式求值的三个影响因素
- 操作符的优先级:从高到低求值
- 操作符的结合性:优先级相同再考虑
- 是否控制求值顺序
表格:
①优先级:由高到低,第n行比第n+1行优先级高
②结合性:N/A无结合性,L-R从左到右运算,R-L从右到左运算
③是否控制求值顺序:是,则说明不一定全部参与运算
操作符 |
描述 |
结合性 |
是否控制求值顺序 |
( ) |
聚组 |
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 |
是 |
一些问题表达式
例1:
该表达式的计算顺序不唯一,可能是
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;
}
存在歧义。
例4:
int fun()
{
static in count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);
return 0;
}
answer中三个fun()的调用顺序无法确认,而由于fun()中的++count语句,不同的函数调用顺序会有不同的answer结果。
隐式类型转换
整型提升
- 非整型的类型进行算术运算时,为了提升运算的精度,往往先将操作数转换为整型,运算后再按照原类型进行截断输出。
- 如果是有符号数,整型提升时给高位补符号位的数,无符号数补0,补到32位为止。
负数的整型提升:
char c = -1;//char只有8个比特位
//原码10000001
//反码11111110
//补码11111111
//char是有符号位的,整型提升:11111111111111111111111111111111
正数的整型提升:
char c = 1;
//原码=反码=补码:00000001
//整型提升:00000000000000000000000000000001
例1:
#include <stdio.h>
int main()
{
char a = 3;
//3看作int型:00000000000000000000000000000011
//但是char只能放一个字节,截断原则是从低位截断,故a:00000011
char b = 127;
//127看作int型:00000000000000000000000001111111
//b:01111111
char c = a + b;
//a+b的过程中有【整型提升】:
// 00000000000000000000000000000011
//+00000000000000000000000001111111
//=00000000000000000000000010000010,截断得10000010
printf("%d\n", c);
//printf的过程中也有【整型提升】:
//11111111111111111111111110000010 - 补码
//11111111111111111111111110000001 - 反码
//10000000000000000000000001111110 - 原码,即-126
return 0;
}
例2:
#include <stdio.h>
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0x6000000;
if(a == 0xb6)
printf("a");
if(b == 0xb600)
printf("b");
if(c == 0xb6000000)
printf("c");
return 0;
}
//运行结果是c
//因为"=="也是一种算术运算,而char类型的a和short类型的b都会进行整型提升,只有本来就是整型的c不会变
例3:
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(!c));
}
//输出结果是1 4 1
//"+"是一种算术运算,所以c进行了整型提升
算术转换
比整型存储空间大的类型,当超过一种类型要进行算术运算时,存储空间小的类型要先转换为大的类型。
存储空间由大到小:
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int