操作符

时间:2023-01-05 09:58:02

本文是对【初识C语言】中操作符部分的详细解读。自学b站”鹏哥C语言“笔记。

操作符分类:

  • 算数操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员
  • 表达式求值

算数操作符

加+   减-   乘*   除/   取余%

  1. 加+  减-  乘*  除/  可以用于整数和浮点数,取余% 只能作用于整数。
  2. 除/ 如果两个操作符都是整数,则执行整数除法;只要有浮点数,则执行浮点数除法。
  3. 除/ 得到的结果是商。

例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;
}

移位操作符

左移<<   右移>>

  1. 只能作用于整数。
  2. 移动的是二进制位。
  3. 右移一位有除以2的效果,右移一位有乘2的效果。
  4. 不要移动负数位,如​​a>>-1​​是错误的。

右移操作符

  • 算数右移(一般情况下都是这种):整体右移,右边丢弃,左边补原符号位(正0负1)
  • 逻辑右移:整体右移,右边丢弃,左边补0

左移操作符

  • 整体左移,左边丢弃,右边补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. 三者的操作数都必须是整数。
  2. “按位”按照的是二进制位补码
  3. 真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;
}

赋值操作符

=    +=    -=    *=    /=    %=    >>=    <<=    &=    |=    ^=   

  1. 赋值操作符的作用是给变量重新赋值。
  2. 赋值操作符可以连续使用,如​​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

解析

  1. 赋值给哪个变量,最终就是哪个变量的类型,故​​s = a + 5​​的类型是short。short类型存储空间是2字节。
  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;
}

表达式求值

  1. 表达式求值的顺序一部分是由操作符的优先级和结合性决定的。
  2. 有些表达式的操作数在求值过程中可能需要转换为其他类型

操作符的属性

复杂表达式求值的三个影响因素

  1. 操作符的优先级:从高到低求值
  2. 操作符的结合性:优先级相同再考虑
  3. 是否控制求值顺序

表格:

①优先级:由高到低,第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 + 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;

该表达式无法得知+前面的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结果。

隐式类型转换

整型提升

  1. 非整型的类型进行算术运算时,为了提升运算的精度,往往先将操作数转换为整型,运算后再按照原类型进行截断输出。
  2. 如果是有符号数,整型提升时给高位补符号位的数,无符号数补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