C语言:操作符详解

目录

一、二进制和进制转换

1.二进制转十进制

2.十进制转二进制

3.二进制转八进制

4.二进制转十六进制

二、原码、反码、补码

三、移位操作符

1.左移操作符

2.右移操作符

四、位操作符 & | ^ ~

1.位操作符的性质

2.不创建临时变量实现两个数的交换

3.求一个整数存储在内存中的二进制中1的个数

4.二进制位置0或者置1

五、逗号表达式

六、下标引用、函数调用、结构成员

1.下标引用操作符[ ]

2.函数调用操作符()

3.结构体

4.结构成员访问操作符

七、操作符的优先级和结合性(完整)

1.优先级

2.结合性

3.操作符的优先级和使用顺序(完整)

八、表达式求值

1.整型提升

2.算术转换


在这篇文章中,我会将所有操作符都介绍到,有些在目前阶段还无法用到的操作符将在后期系统讲解!


一、二进制和进制转换

其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表示形式而已。

比如数值15的各种进制的表示形式:

15的2进制:1111

15的8进制:17

15的10进制:15

15的16进制:F

我们重点介绍一下二进制

首先我们还是得从10进制讲起。其实10进制是我们生活中经常使用的:10进制中满10进;10进制的数字每一位都是0~9的数字组成

其实二进制也是一样的:2进制中满2进1;2进制的数字每一位都是0~1的数字组成。那么 1101 就是二进制的数字了。

由于在C语言中整数一般表示为int类型,而int类型占4个字节,1个字节占8个bit位,所以一个整型数字转译成二进制后共有32位。比如十进制数字64转换成二进制就应该是00000000 00000000 00000000 01000000,以此类推。我们在前面提到过char类型也是整型家族的一员,但是char类型只占一个字节,一共8个bit位,所以char类型的64转换成二进制应该是01000000;short类型也是如此。

1.二进制转十进制

10进制的123表示的值是一百二十三,为什么是这个值呢?其实10进制的每一位是有权重的。10进 制的数字从右向左分别是个位、十位、百位....,每一位的权重分别是 10^0 , 10^1 , 10^2 ...

2进制和10进制是类似的,只不过2进制的每一位的权重,从右向左分别是:2^0 , 2^1 , 2^2 ... 如果是2进制的1101,该怎么理解呢?

2.十进制转二进制

3.二进制转八进制

八进制的数字每一位是0~7。0~7的数字,各自写成二进制,最多有3个二进制位就足够了。比如7的二进制是111,所以在二进制转八进制数的时候,从二进制序列中右边低位开始向左每3个二进制位会换算一个八进制位,剩余不够3个二进制位的直接换算。

如:二进制的01101011,换成八进制是0153。0开头的数字,会被当做八进制。

4.二进制转十六进制

十六进制的数字每一位是0~9或a ~f,各自写成2进制,最多有4个二进制位就足够了。二进制转十六进制与二进制转八进制的原理相同。比如f的⼆进制是1111,在二进制转十六进制数的时候,从二进制序列中右边低位开始向左每4个二进制位会换算一个十六进制位,剩余不够4个二进制位的直接换算。

如:二进制的01101011,换成16进制是0x6b。用16进制表示的时候要在数字的前面加0x

二、原码、反码、补码

原码、补码和反码在后期我们理解内存相关的知识时有重要作用,所以这里要作为重点了解~

整数的二进制表示方法有三种,即原码反码补码。有符号整数的三种表示方法均有符号位数值位两部分。二进制序列中最高位被当做符号位,剩余的都是数值位。符号位用0表示正1表示负

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码:反码+1就得到补码。

补码得到原码可以使用:符号位不变,其余位取反后-1的操作。

正数和负数的原码、反码和补码的表示又各不相同:

正整数的原、反、补码都相同。

负整数的三种表示方法各不相同。

对于整形来说,数据存放内存中的其实是补码

在计算机系统中,数值一律用补码来表示和存储。原因在于使用补码可以将符号位和数值位统一处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

三、移位操作符

>>代表右移操作符,<<代表左移操作符。移位操作符的操作数只能是整数左移操作和右移操作操作的都是一个整数的二进制位的补码。左移操作符和右移操作符与四则运算一样,都能简写成:>>=或<<=。左移操作符和右移操作符的操作规则是不一样的。

1.左移操作符

移位规则:左边抛弃、右边补0

#include <stdio.h>

int main()
{
    int num = 10;
    int n = num<<1;
    printf("n= %d\n", n);
    printf("num= %d\n", num);
    return 0;
}

下面是左移操作符的实际操作过程,与我们提到过的移位规则一样,编译器先是将左边的一位扔掉,随后在右边又补了一个0。

上面展示左移操作符使用的是正整数,那如果是负整数呢?假若我们输入的是-10:

移位后的结果就是-20。为什么呢?我们知道无论是左移还是右移操作的都是整数的二进制位的补码。那么首先我们先将-10的二进制位转为补码:符号位不变,其他位按位取反再+1后得到的就是-10的补码。随后对-10的补码进行左移操作。将补码整体向左移动一位,空出来的位就用0补上,随后再将左移后的补码-1得到的就是左移后的反码,左移后的反码按位取反得到的就是左移后的结果的二进制位即原码。将原码再翻译成整型得到的就是最后的结果。

那我们就可以得出负整数进行左移的结论:

将负整数的二进制位整体向左移动指定的位数,右边空出的位用0填充,这就意味着最高位的符号位1被保留,而低位的符号信息丢失

这也就是说,负整数进行逻辑左移后得出的结果依然是负数。与正整数的逻辑左移操作不同,负整数进行逻辑左移反而是保留最高位然后在二进制位的最右边进行补0。

2.右移操作符

右移操作就比较特殊了,它分为两种:逻辑右移算术右移。那么右移操作的规则分别如下:

1.逻辑右移规则:左边用0填充,右边丢弃

2.算术右移规则:左边用原该值的符号位填充,右边丢弃

#include <stdio.h>

int main()
{
    int num = 10;
    int n = num>>1;
    printf("n= %d\n", n);
    printf("num= %d\n", num);
    return 0;
}

由于正整数的原码反码补码都相同,无论是逻辑右移还是算术右移,得到的结果都相同,与左移操作的操作原理是相同的,这里就不再过多解释了。结果: 

 而分别对负整数进行逻辑右移和算术右移得出的结果又不一样了:

但是这里要注意的是,在编写程序时右移操作符进行的时逻辑右移还是算术右移,这是取决于编译器的。大部分编译器默认进行的是算术右移。如果要详细了解算术右移和逻辑右移的具体实现办法,建议到编译器的官网或者其他论坛进行相关信息的搜索。当然,我们也可以通过输入的值分别进行算术右移和逻辑右移进而判断编译器的默认移动方式。

数据类型的不同也会导致右移操作的结果不同:如果是无符号类型,则用0填充空出的位置,也就是采用逻辑右移:如果是有符号类型,其结果取决于编译器。

特别提示一下,严禁出现以下写法:

int a = 10;

a >> -5;//无论是左移还是右移

四、位操作符 & | ^ ~

在C语言中,位操作符有: 

& //按位与

| //按位或

^ //按位异或

~ //按位取反

它们的操作数也和移位操作符一样,必须是整数。也同样是操作整数的补码。它们的计算规则如下:

&B
               0                                1
A

0

1

00
01
|B
               0                                1
A

0

1

01
11
^B
               0                                1
A

0

1

01
10

按位取反(~)就是将整数的二进制各位取反(除符号位)。

口诀

&:全1为1,否则为0

|:有1为1,否则为0

^:相同为0,否则为1

为了便于理解我们敲代码试试:

#include <stdio.h>

int main()
{
    int num1 = -3;
    int num2 = 5;
    printf("%d\n", num1 & num2);
    printf("%d\n", num1 | num2);
    printf("%d\n", num1 ^ num2);
    return 0;
}

由于位操作符同样是操作整数的补码,所以我们需要先将-3的补码写出来:

然后分别进行按位与、按位或和按位取反的操作。由上面的规则我们可以知道:

-3按位与5后得到的补码为:

00000000 00000000 00000000 00000101

-3按位或5后得到的补码为:

11111111 11111111 11111111 11111101

-3按位异或5后得到的补码为:

11111111 11111111 11111111 11111000

/*然后我们再将得到的补码分别转换为原码*\

-3按位与5后转换得到的原码为:

00000000 00000000 00000000 00000101    正整数二进制位的原码、反码和补码相同

-3按位或5后转换得到的原码为:

10000000 00000000 00000000 00000011

-3按位异或5后转换得到的原码为:

10000000 00000000 00000000 00001000

*/然后我们再将得到的原码翻译为十进制整型*\

-3 & 5 = 5

-3 | 5 = -3

-3 ^ 5 = -8

我们来看结果:

1.位操作符的性质

位操作符的性质在这里就不做详细推理了,作为结论记住即可,如果想要写出推导过程需要自行解决!

按位与(&)的性质:

交换律:a & b = b & a

结合律:a & (b & c) = (a & b) & c

同一律:a & 0 = 0

零律:a & a = a

分配律:a & (b | c) = (a & b) | (a & c)

吸收律:a & (a | b) = a 以及 a & (a ^ b) = a 

按位或(|)的性质:

交换律:a | b = b | a
结合律:a | (b | c) = (a | b) | c
同一律:a | 0 = a
零律:a | a = a
分配律:a | (b & c) = (a | b) & (a | c)

按位异或(^)的性质:

交换律:a ^ b = b ^ a
结合律:a ^ (b ^ c) = (a ^ b) ^ c
同一律:a ^ 0 = a
零律:a ^ a = 0

2.不创建临时变量实现两个数的交换

我们在实现两个数的交换时,一般都是创建一个临时变量来储存其中一个值,然后在进行交换。在后续学到指针时也可以通过地址来交换两个变量的值。

那我们到目前为止该如何做到不创建临时变量来实现两个数的交换呢?这就要用到我们的位操作符来对两个变量的二进制位改动来实现了。

假设定义两个变量a和b,分别赋值10和20。根据按位异或的性质:

int a = 10;

int b = 20;

a = a ^ b;

我们先让a = a ^ b,再让b = a ^ b,这时a的值已经改变,所以这个式子可以写成:b = (a ^ b) ^ b。由按位异或性质中的结合律可以再改写成:b = a ^ (b ^ b)。然后再由零律和同一律可得b = a,这里的a就是我们初始化a时候的值,所以就变相得让b = a。同理再让a = a ^ b,这里的a与a的值是相同的,只有b是与上次不同的,所以也是变相得让a = b了。所以就有:

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

3.求一个整数存储在内存中的二进制中1的个数

那这道题该怎么做呢?我们先举个例子,我们创建一个变量a,然后初始化为13。我们先对它进行模2的操作,也就是13%2。在数学中13除以2得到的结果为商6余1。我们不妨再写出13的二进制形式(这里为了方便只写4位):1101。而6的二进制形式为:0110。我们可以发现,我们得到的余数也就是13%2的结果正好是13的二进制位的最右边一位。那根据上述原理我们可以写出以下代码:

#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("二进制中1的个数为 %d\n", count);
    return 0;
}

但是这样就真的可以解决问题了吗?这里注意我们while圆括号中的语句——num,我们在编写这段程序的时候是默认循环能运行的,所以num必须是正数,假如我们输入了-1,这就代表着num为-1。我们知道C中默认非0为假,所以如果我们一旦输入了负数,while循环就不会执行。就会输出0:

所以这需要我们再对代码进行改进,在我们输入负数时,这段代码也可以运行出结果。那要写出一段代码对正数和负数同时生效该怎么做呢?最有效也是最直接的办法就是遍历我们输入的数的每一个二进制位,然后让每一个二进制位按位与(&)上1。这里为什么要使用按位与呢?因为同1为1,只有对应的二进制位都为1时结果才是1。为了遍历每一个二进制位,需要定义一个循环32次的循环(因为int类型的二进制形式共有32个bit位)。

该如何让某个数的每个二进制位都遍历1呢?这里我想到了两种办法,但实际上都是一种办法。

第一种就是让我们输入的数的二进制位每循环一次按位与上1,完成按位与计算后,让这个数的二进制位整体向右移动一位,这也就意味着该数的最右边会不断丢失,左边的数不断向右移动,最终每个二进制位都能与1进行按钮位于计算。

第二种就是让1的二进制位每结束一次计算和循环后向左移动一位。这种也能起到让某个数的二进制位进行按位与计算。

#include <stdio.h>

int main()
{
    int num = 0;
    int i = 0;
    int count = 0;
    scanf("%d", &num);
    for(i=0; i<32; i++)
    {
        if( (num >> i) & 1 )
            count++; 
    }
    printf("二进制中1的个数 = %d\n",count);
    return 0;
}

可是一旦我们再输入一遍负数,我们会发现,输出的结果依然不尽人意:

-1的二进制位只有一个1——最右边的1,可这里却输出了32。所以我们还需要对代码进行改进。可这两段代码已经很接近答案了,还有哪里需要改动呢?只需对条件进行改动:

#include <stdio.h>

int main()
{
	int num = 0;
	int i = 0;
	int count = 0;
	scanf("%d", &num);
	for (i = 0; i < 32; i++)
	{
		if (((num >> i) & 1) == 1)
			count++;
	}
	printf("二进制中1的个数 = %d\n", count);
	return 0;
}

为什么是这样呢?

我们先从正数考虑。正数的原码、反码和补码相同。我们以13为例

1的补码:

00000000 00000000 00000000 00000001

13的补码:

00000000 00000000 00000000 00001101

第一次循环:

(13 >> 0) & 1 == 1

第二次循环:

(13 >> 1) & 1 == 0

第三次循环:

(13 >> 2) & 1 == 1

第四次循环:

(13 >> 3) & 1 == 1

由于1的二进制位只有一个,其他位都是0,无论我们输入的数除了最右边的一位,其他位都是1,最后的计算结果都是0,只有最右边的二进制位才决定计算结果。也就是说,只有最右边二进制为1的数与1按位与计算后才是1,这样才能保证if语句符合我们的期望,所以才加上==1这个条件。

对于负数,以-1为例,-1的补码全是1,这里只用一句话概括,向右移动几位,右边就多几个0。-1进入循环后:

11111111 11111111 11111111 11111111 //-1的补码

00000000 00000000 00000000 00000001 //1的补码

两个补码进行按位与计算后,得到结果就是1,后面再进入循环后,计算的结果都是0,所以最终的答案就是1。

这里还有第三种方法,也是最妙的一种方法,属于是把C中的操作符玩活的那种!我们先观摩一下大佬的代码:

#include <stdio.h>

int main()
{
    int num = -1;
    int i = 0;
    int count = 0;//计数
    while(num)
    {
        count++;
        num = num&(num-1);
    }
    printf("⼆进制中1的个数 = %d\n",count);
    return 0;
}

这段代码的精髓就在于——num = num&(num-1);这段代码。我们举个例子,假设我们输入的值为15来感受一下大佬的代码:

① num:1111

     num-1:1110

     num&(num-1):1110

② num:1110

     num-1:1101

     num&(num-1):1100

③ num:1100

     num-1:1001

     num&(num-1):1000

④ num:1000

     num-1:0111

     num&(num-1):0000

最后一次结果为0,无法进入while循环,这也就是count++在num&(num-1)前面的原因。

4.二进制位置0或者置1

假设我们要将13的二进制序列的第5位修改为1,然后再改回0,该怎么做呢?

13的2进制序列: 00000000000000000000000000001101

将第5位置为1后:00000000000000000000000000011101

将第5位再置为0:00000000000000000000000000001101

13的二进制序列的第5位为0,要将其变为1,其实很简单,利用我们的按位或(|)操作符即可实现(有1为1)。那要再将其改为0该怎么办呢?只需要让其按位或0就可以了。这个问题还是比较简单的,直接上代码:

#include <stdio.h>

int main()
{
    int a = 13;
    a = a | (1<<4);
    printf("a = %d\n", a);
    a = a & ~(1<<4);
    printf("a = %d\n", a);
    return 0;
}

五、逗号表达式

逗号表达式,就是用逗号隔开的多个表达式。

exp1, exp2, exp3, …expN

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果

六、下标引用、函数调用、结构成员

1.下标引用操作符[ ]

下标引用操作符用于数组。操作数:一个数组名 + 一个索引值

int arr[10];//创建数组

arr[9] = 10;//实用下标引用操作符

[ ]的两个操作数是arr和9。

2.函数调用操作符()

函数调用操作符接受一个或多个操作数。第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

3.结构体

C语言已经提供了内置类型。如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的。假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。

结构是一些值的集合,这些值称为它的成员。结构的每个成员可以是不同类型的变量,如:常量、数组、指针,甚至是其他结构体。

想要声明结构体,必须遵循以下格式:

struct tag

{

    member-list;

}variable-list;

这里有几个例子:

这个声明创建了一个名叫x的变量,它包含三个成员:一个整数、一个浮点数和一个字符

struct 

{

    int a;

    float b;

    char c;

}x;

如果这里没有学到指针没有关系,先了解结构体是怎么声明的。

这个声明创建了y和z。y是一个数组,它包含了20个结构;z是一个指针,它指向这个类型的结构

struct 

{

    int a;

    float b;

    char c;

}y[20],*z;

这是不是意味着某种特定类型的所有结构都必须使用一个单独的声明来创建?并不是。

标签tag允许为成员列表提供一个名字,这样它就可以在后续的声明中使用。标签允许多个声明使用同一个成员列表,并且创建同一种类型的结构。假如我们为上面例子中的结构体创建一个标签:

struct BQ

{

    int a;

    float b;

    char c;

};

这里要注意,因为我们并没有在结构体的最后写上变量名,所以上述代码没有创建任何结构体变量!这个声明把标签BQ和这个成员列表联系在一起。在后面要创建这个类型的结构体时,就可以写成以下形式:

struct BQ x;

struct BQ y[20], *z;

这样就可以很简单地创建同种类型的结构体变量了。

那我们要对结构体变量初始化该怎么做呢?以变量x为例,我们要将其初始化,就写成这样:

① struct BQ x = {10, 15.0, A};

struct BQ

{

    int a;

    float b;

    char c;

}x = {10, 15.0, A};

4.结构成员访问操作符

结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。使用方式:结构体变量.成员名。如下所示:

#include <stdio.h>

struct Point
{
 int x;
 int y;
}p = {1,2};

int main()
{
    printf("x: %d y: %d\n", p.x, p.y);
    return 0;
}

既然有直接访问,那就肯定有间接访问了。有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针箭头操作符(→)就专门被用来解决这一情况的。如果这里没有学到指针没有关系,先作为了解即可,后面会讲到~

#include <stdio.h>
#include <string.h>

struct Stu
{
 char name[15];
 int age;
};

void print_stu(struct Stu s)
{
    printf("%s %d\n", s.name, s.age);
}

void set_stu(struct Stu* ps)
{
    strcpy(ps->name, "李四");
    ps->age = 28;
}

int main()
{
    struct Stu s = { "张三", 20 };
    print_stu(s);
    set_stu(&s);
    print_stu(s);
    return 0;
}

讲到这里就只是对结构体的一个简单了解,在后续我会继续向大家详细介绍结构体,这部分的内容到后面也会重新写出~~

·

七、操作符的优先级和结合性(完整)

C语言的操作符有2个重要的属性:优先级结合性。这两个属性决定了表达式求值的计算顺序

1.优先级

优先级指的是:如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。

3 + 4 * 5;

上面示例中,表达式 3 + 4 * 5 里面既有加法运算符( + ),又有乘法运算符( * )。由于乘法的优先级高于加法,所以会先计算 4 * 5 ,而不是先计算 3 + 4 。

2.结合性

如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了。根据运算符 是左结合还是右结合决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。

5 * 6 / 2;

上面示例中,* 和 / 的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算 5 * 6 再计算 6 / 2 。

3.操作符的优先级和使用顺序(完整)

以上是C中所有操作符的详细解析。

八、表达式求值

因为在第一篇博客已经讲过整型提升和算术提升的相关知识C语言数据类型和变量-CSDN博客,在这里只做一些补充~

1.整型提升

C语言中整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

//实例1

char a,b,c;

...

a = b + c;

b和c的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a中。

那我们如何进行整体提升呢?

1.有符号整数提升是按照变量的数据类型的符号位来提升的

2.无符号整数提升,高位补0

//负数的整形提升

char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为char为有符号的 char

所以整形提升的时候,高位补充符号位,即为1

提升之后的结果是:

11111111111111111111111111111111

//正数的整形提升

char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为char为有符号的 char

所以整形提升的时候,高位补充符号位,即为0

提升之后的结果是:

00000000000000000000000000000001 

//无符号整形提升,高位补0

2.算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double

double

float

unsigned long

long

unsigned int

int

如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。

以上就是C语言操作符的详细解析了~希望大家能够掌握!

  • 36
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值