从零学习C语言---操作符详解

简介: 本章介绍C语言中的操作符。

1、操作符分类

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号操作符

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

2、算数操作符

+      -       *        /        %



//  7 / 2    ----->3
//  7 % 2    ----->1

//那如果想要打印出小数呢?只需要两端有一个浮点数就会执行浮点数得出发
#include <stdio.h>
int main()
{
    float a = 7 / 2.0;
    printf("%.1f\n",a);          //.1就是保留小数点后一位
    return 0;
}

3、移位操作符(二进制)

<< 左移位符          就是左边丢弃,右边补0。

>> 右移位符

注:移位操作符的操作数只能是整数

移位操作符,移动的是二进制位。

警告:对于移位运算符,不要移动负位数,这个是标准定义的。

3.1、左移位

3.1.1、原码、反码、补码介绍

整数的二进制表示有3种:

  • 原码
  • 反码
  • 补码

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

负的整数的原码、反码、补码是要计算的。

1、比如7的原码、反码、补码:

因为7是正值所以,原码 = 反码 = 补码

7是int类型的,占4个字节,一个字节=8bit,所以:

原码 = 00000000000000000000000000000111

注意:最高位(就是最左边的哪一位)0代表是正数,1代表是负数。

2、-7的原码、反码、补码

  • 原码:10000000000000000000000000000111
  • 反码:11111111111111111111111111111000(按照原码的最高位不变,其它的取反)
  • 补码:11111111111111111111111111111001(反码+1=补码)

整数在内存中存储的是补码。

3.1.2、求a=7,a<<1的值

那么接下来言归正传,既然移位操作符,移动的是二进制,那移动的肯定是存储在内存中的补码。

比如:7,补码是:00000000000000000000000000000111。

那如果进行b= 7 <<1;向左移位,就是左边丢弃,右边补0。

那得到的补码结果就是: 00000000000000000000000000001110

那么此时补码的结果转为十进制就是:8+4+2+0=14

看代码效果:

#include <stdio.h>
int main()
{
    int a = 7;
    int b = a << 1;
    printf("%d\n", a);
    printf("%d\n", b);
    return 0;
}

输出:

image.png

可见a左移一位,a本身没有变化。

3.1.3、求a=-7,a<<1的值

-7的补码:11111111111111111111111111111001,那根据左移位:左边丢弃,右边补0的原则来推算,可以得到新的补码为:11111111111111111111111111110010。那这时注意,如果想要得到a<<1的值,需要-7的原码,然后转为十进制即可。

所以现在需要反推,新的补码:11111111111111111111111111110010,那么补码-1就是反码,那求出反码(最高位不取反)为:11111111111111111111111111110001,然后在取反得到原码:10000000000000000000000000001110

这个原码转为十进制是:8+4+2+0=14,又因为是a=-7,所以a<<1=-14。

代码验证:

#include <stdio.h>
int main()
{
    int a = -7;
    int b = a << 1;
    printf("%d\n", a);
    printf("%d\n", b);
    return 0;
}

输出:

image.png

3.1.4、得出左移位的结论

a=7,那a<<1=14

a=-7,那a<<1=-14

所以左移位有原数*2的效果。

3.2、右移操作符

右移操作符分为两类:

  • 算术移位:右边丢弃、左边补原符号位。
  • 逻辑移位:右边丢弃,左边补0。

具体的算数移位和逻辑移位是取决于编译器的。UP🐖见过的都是算数移位。

3.2.1、求a=7,a>>1的值

算术移位:右边丢弃、左边补原符号位。

补原符号位就是a<<1,a如果是正数,就补0,a如果是负数就补1。

比如:7的补码:00000000000000000000000000000111,按照:右边丢弃,左边补原符号位。那得到的新补码就是:0000000000000000000000000000011,所以此结果是:2+1=3。

代码验证:

#include <stdio.h>
int main()
{
    int a = 7;
    int b = a >> 1;
    printf("%d\n", a);
    printf("%d\n", b);
    return 0;
}

输出:

image.png

3.2.2、求a=-7,a<<1的值

7的补码:11111111111111111111111111111001,那根据:右边丢弃,左边补原符号位。那得到的新补码就是:1111111111111111111111111111100(算数移位)或者0111111111111111111111111111100(逻辑移位),那具体是那个移位呢?

VS编译器是算数移位。

分析:-7的新补码:1111111111111111111111111111100,新补码-1=反码=1111111111111111111111111111011,然后取反(最高位不取反)然后得到原码:1000000000000000000000000000100

然后转为十进制结果为:-4。

代码验证:

#include <stdio.h>
int main()
{
    int a = -7;
    int b = a >> 1;
    printf("%d\n", a);
    printf("%d\n", b);
    return 0;
}

输出:

image.png

4、位操作符

位操作符有:

  • & -> 按(2进制)位与
  • | -> 按(2进制)位或
  • ^ -> 按(2进制)位异或

4.1、&按位与

下面先来看那段代码:

#include <stdio.h>
int main()
{
    int a = 3;
    int b = -5;
    int c = a & b;
    printf("c=%d\n", c);
    return 0;
}

输出:

image.png

分析:既然内存中放的是补码,所以我们将3和-5的补码写出来:

//3的原码:00000000000000000000000000000011,因为正数的原码=反码=补码。所以:

//3的补码:00000000000000000000000000000011

//-5的原码:10000000000000000000000000000101,取反:

//-5的反码:11111111111111111111111111111010,根据:反码+1=补码,所以:

//-5的补码:11111111111111111111111111111011

那么现在将3和-5的补码进行按位与。

按位与的规则:只要有0,按位与的结果就是0(两个0按位与也是0),两个全部是1,按位与的结果就是1。

3的补码:00000000000000000000000000000011

-5的补码:11111111111111111111111111111011

按位与后的补码:00000000000000000000000000000011

由于按位与后的补码符号位是0,是正数,正数的补码=反码=原码,所以原码结果为:2+1=3,所以3 & 5 = 3,所以上述代码正确。

4.2、| 按位或

先来看段代码:

#include <stdio.h>
int main()
{
    int a = 3;
    int b = -5;
    int c = a | b;
    printf("c=%d\n", c);
    return 0;
}

输出:

image.png

按位或的规则:两个数只有1结果为1,两个数全部是0,结果为0。

下面还是需要补码。

分析:

3的补码: 00000000000000000000000000000011

-5的补码: 11111111111111111111111111111011

得到按位或的补码:11111111111111111111111111111011

由于得到按位或的补码符号位是1,为负数,所以需要-1为反码,然后在取反为原码,然后转为十进制,取值。

按位或的补码-1=反码:11111111111111111111111111111010,

然后反码取反=原码:10000000000000000000000000000101,转化为是十进制为5,又因为符号位为1,所以为负数,所以最终结果为-5。

4.3、^ 按位异或

先来看段代码:

#include <stdio.h>
int main()
{
    int a = 3;
    int b = -5;
    int c = a ^ b;
    printf("c=%d\n", c);
    return 0;
}

输出:

image.png

按位异或的规则:相同为0(两个都是0结果为0,两个都是1结果为1),相异为1

分析:

3的补码: 00000000000000000000000000000011

-5的补码: 11111111111111111111111111111011

按位异或后的补码:11111111111111111111111111111000

补码-1=反码:11111111111111111111111111110111

反码取反=原码:10000000000000000000000000001000

转为十进制为8,因为符号位为1,所以结果为负,所以结果是-8。

4.3.1、记着结论:

  • a ^ a = 0
  • 0 ^ a = a
  • a ^ a ^ b = a ^ b ^ a(说明按位异或支持交换律)。

4.3.2、不创建临时变量,交换两个整数的值

#include <stdio.h>
int main()
{
    int a = 3;
    int b = 5;
    printf("交换前a=%d b=%d\n", a, b);
    a = a ^ b;         //a=3^5
    b = a ^ b;         //b=3^5^5  ---->  b=3
    a = a ^ b;         //a=3^5^3  ---->  a=5
    printf("交换前a=%d b=%d\n", a, b);
    return 0;
}

输出:

image.png

5、赋值操作符

int weight = 100;         //初始化

weight = 200;             //赋值

a = x = y+1;

6、单目操作符

!                     逻辑反操作
-                     负值
+                     正值
&                     取地址
sizeof                操作数的类型长度(以字节为单位)
~                     对一个数的二进制按位取反
--                    前置--、后置--
++                    前置++、后置++
*                     间接访问操作符(解引用操作符)
(类型)                 强制类型转换

在C语言中0代表假,非0代表为真

6.1、!的介绍

#include <stdio.h>
int main()
{
    int flag = 0;           //flag初始化为0,所以flag此时为假
    if (!flag)              //那既然flag为假,那么!flag就为真
        printf("现在为真");
    return 0;
}

6.2、sizeof的使用介绍

#include <stdio.h>
int main()
{
    int a = 10;
    printf("%d\n", sizeof(a));        //4
    printf("%d\n", sizeof(int));      //4
    printf("%d\n", sizeof a);         //因为size是个操作符,所以不带()也可以使用   输出:4

    return 0;
}

当然sizeof也可以统计数组的类型大小

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%d\n", sizeof(arr));            //40
    printf("%d\n", sizeof(arr[0]));        //4,
    printf("%d\n", sizeof(arr) / sizeof(arr[0]));       //计算数组中元素的个数的方法
    return 0;
}

6.3、--、++的使用

这里只说明一个++即可,--的使用同理

1、先说后置++,eg:a++,后置++遵循一个原则:先使用,后++

#include <stdio.h>
int main()
{
    int a = 10;
    int b = a++;           //先使用,后++,也就是先 int b = a,然后在a = a+1
    printf("%d\n", b);
    printf("%d\n", a);
    return 0;
}

输出:
    10
    11

2、前置++,eg:++a。前置++遵循一个原则:先++,后使用

#include <stdio.h>
int main()
{
    int a = 10;
    int b = ++a;           //先++,后使用,也就是先 a = a+1,然后在int b = a
    printf("%d\n", b);
    printf("%d\n", a);
    return 0;
}

输出:
    11
    11

常规的操作就不说了,下面来看这个代码:

#include <stdio.h>
int main()
{
    int a = 10;
    printf("%d\n", a--); //a--是个表达式,先使用,后--,先使用那表达式结果就为10.
    printf("%d\n", a);  //后--,a=9
    return 0;
}

输出:

image.png

还有这个代码也是一样的道理:

#include <stdio.h>
int main()
{
    int a = 10;
    test(a--);         //这里传参的值也是10
    return 0;
}

在C语言中,for循环中i++,++i效果都是一样的。但在C++中就不一样了。

6.4、强制类型转换

#include <stdio.h>
int main()
{
    int a = (int)3.14;
    printf("%d\n", a);
    return 0;
}

输出:
    3

6.5、~ 二进制按位取反

先来看段代码:

#include <stdio.h>
int main()
{
    int a = 3;
    printf("%d\n", ~a);
    return 0;
}

输出:

image.png

~是按二进制取反,那肯定是对一个数的补码进行取反。

3是个整数,所以:原码=反码=补码:00000000000000000000000000000011

那么~a就是对补码取反得到新补码:11111111111111111111111111111100

注意:这里取反之后,符号位就变成1了,也就意味着是负数了,

然后在求~a的值,新补码-1=反码:11111111111111111111111111111011,

反码取反=原码:1000000000000000000000000000100

转为十进制就是4,又因为符号位位1,所以最终结果为-4。

6.6、* 解引用操作符

#include <stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    *p = 20;        //*对p解引用,通过p里面存储的地址,找到它所指向的对象。
    printf("%d\n", a);
    return 0; 
}

7、关系操作符

>       >=      <      <=     !=

7.1、关于字符串比较

#include <stdio.h>
int main()
{
    if ("abc" == "abcdef") //这样比较不是字符串内容,而是字符串的首字符地址。
    {
        ;
    }
    return 0; 
}

//字符串比较内容需要使用库函数:strcmp()

8、逻辑操作符

&&             逻辑与(并且)

||             逻辑或(或者)

8.1、这里有个重要的规则,如下:

如果:

  • a && b,如果a为假,则b不在执行。
  • a || b,如果a为真,则b也不在执行。

这个用于判断真假。a && b为真结果为1,a && b为假结果为0。如下:

#include <stdio.h>
int main()
{
    int a = 3;
    int b = 5; 
    int c = a && b;           //c=1,1为真
    printf("%d\n", c);
    return 0; 
}

输出:

image.png

8.2、一道360面试题

如果:表达式1 && 表达式2,如果表达式1 为假,那么表达式2无论为真还是为假,表达式2就不会在算了。

如下代码:

#include <stdio.h>
int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
    i = a++ && ++b && d++;
    printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);
    return 0; 
}

输出:

image.png

分析:因为a=0,a++之后,先使用,后++,所以a++表达式的结果为0,然后a后++所以最终a的值为1,既然a++表达式结果为0,所以&&前面的为假,所以后面的++b就不再运行了,所以b的值不变,b=2。由于a++为0,那么在第二个&&之前都是假,因此后面的d++也不在执行,所以d的值也没有发生改变,d=4,所以最终结果为1 2 3 4。(c=3从头就每改变)。

8.2.2、题型改编(1),提问:如果修改为a=1?

#include <stdio.h>
int main()
{
    int i = 0, a = 1, b = 2, c = 3, d = 4;
    i = a++ && ++b && d++;
    printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);
    return 0; 
}

输出:

image.png

8.2.3、题型改编(2):吧&&变为||,结果为?

#include <stdio.h>
int main()
{
    int i = 0, a = 1, b = 2, c = 3, d = 4;
    i = a++ || ++b || d++;
    printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);
    return 0; 
}

输出:

image.png

分析:由于a || b,a为真在,则b在不执行。a++这个表达式的结果为1,为真,然后后++,所以a=2,因为 a++这个表达式的结果为1所以第一个||后面的++b,不在执行,所以b=2。所以整个a++ || ++b为真,所以d++不在执行,所以d=4。最终结果为:2 2 3 4。

9、条件操作符

exp1?exp2:exp3

exp1 ? exp2 : exp3
 真     算     不算      那么整个表达式的结果就是exp2表达式的结果
 假    不算     算       那么整个表达式的结果就是exp3表达式的结果

代码示例:

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int r = a > b ? a : b;      //比较最大值
    printf("%d\n", r);
    return 0;
}

输出:
    20

image.png

10、逗号表达式

exp1,exp2,exp3,...expN

逗号表达式就是用逗号隔开的一串表达式。

逗号表达式的特点是:从左向右依次计算,整个表达式的结果是最后一个表达式的结果。

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int c = 0;
    int d =(c = a - 2, a = b + c, c - 3);    //最后一个表达式c-3=5,所以最终输出结果5
    printf("%d\n", d);
    return 0;
}

输出:
    5

11、下标引用、函数调用和结构体调用

11.1、下标引用

arr[9],[]是操作符,arr 和 9是操作数。也就是说[]它的两个操作数是arr 和 9。

其实在写数组引用时,我们即可以这样写:arr[7],又可以这样写7[arr]。所以这就更说明了[]是个操作符。

其实arr[7]就是 *(arr+7)的解引用:

其实arr[7] ---> (arr+7)---> (7+arr)--->7[arr]。

arr是数组首元素地址

arr+7就是跳过7个元素,指向第8个元素。

*(arr+7)就是第八个元素。

11.2、函数调用操作符--->()

Add(a,b):

  • ()是操作符
  • 操作数:Add,a,b。

11.3、结构体调用---> . ->

代码如下:

#include <stdio.h>

struct Stu
{
    char name [20];
    int age;
    char sex[5];
    char id[15];
};

void print(struct Stu* ps)
{
    //形式:结构体对象.结构体成员变量
    printf("%s %d %s %d\n", (*ps).name, (*ps).age, (*ps).sex, (*ps->id));
    printf("%s %d %s %d\n", ps->name, ps->age, ps->sex, ps->id);
    //形式:结构体指针->结构体成员
    //这两者所产生的效果一样。
}

int main()
{
    struct Stu S = 
    {
        "zhangsan",17,"nan","123456789"
    };
    print(&S);
    return 0;
}

就类比前面说到的指针变量那一节,道理一样。

11.4、关于结构体的简单总结

当使用此结构体时:有两种方法,这两种方法是等效的

  • 结构体对象.结构体成员变量,eg:(*ps).name
  • 结构体指针->结构体成员,eg:ps->name

ps->name等价于(*ps).name。

12、表达式求值

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

12.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中。

那既然知道了什么是整型提升。那我们如何进行整体提升呢?

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

//负数的整型提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
11111111
因为char为有符号的char,(有符号的char可以理解为是char类型的整数变量具有正负)
所以整型提升的时候,高位补充符号位,即为1。
所以提升后的结果是:11111111111111111111111111111111


//正数的整型提升
char c2 = 1;
变量c1的二进制位(补码)中只有8个比特位:
00000001
因为char为有符号的char,(有符号的char可以理解为是char类型的整数变量具有正负)
所以整型提升的时候,高位补充符号位,即为0。
所以提升后的结果是:00000000000000000000000000000001

详解一下这个负数整型提升,char c1 = -1;首先c1是char类型的,应该先整型提升,整型int是4个字节,1字节=8bit,所以4个字节=32bit,所以整型提升后的原码是:1000000000000000000000001,之后取反得到反码:11111111111111111111111111111110,之后+1,得到补码:11111111111111111111111111111111。

然后截断:11111111。

举个例子,比如:

#include <stdio.h>
int main()
{
    char a = 5;
    char b = 126;
    char c = a + b;
    printf("%d\n", c);
    return 0; 
}

输出:

image.png

思考一下:为什么这里的结果是-125呢?其实这就和整型提升有关!!!

分析:

首先正常的5的int类型的十进制补码为:00000000000000000000000000000101,但由于5现在是char类型的,所以,我们应该截断char是一个字节=8bit,所以直接后8位:00000101。注意:这的最左边的一位仍是符号位,0代表正数,1代表负数。这里5是正数,所以为:00000101。

同理,正常的126的int类型的十进制补码为:00000000000000000000000001111110,然后截断为:01111110。

然后此时程序来到:char c = a + b; 这里我们可以发现:a+b,都是char类型的数据,是达不到int类型的,所以需要整型提升。

那就按照上面说到的规则进行整型提升:char类型的需要提升到int类型的,且char是有符号char,需要补24个bit,这补的24个bit,是补0还是补1呢?这需要看符号位。比如:char类型5的补码为:00000101,符号位是0,所以前面的24bit位全部补0,所以整型提升后就是:00000000000000000000000000000101。

(如果是无符号char,就不用考虑了,直接补0即可。)

同理:char类型的126的补码为:01111110,整型提升后就为:00000000000000000000000001111110。

好了,至此为止,整型提升结束,

现在使用它们开始运算:

  • 00000000000000000000000000000101 ---> 5
  • 00000000000000000000000001111110 ---> 126
  • 00000000000000000000000010000011 ---> 相加的到后的结果

同理上面相加得到的结果是int类型的c的大小,但是现在c是char类型的,所以需要再次截断,直接后8bit位:得到:

  • 10000011 --->得到char类型的c,

然后走到打印printf("%d\n", c);这一步,由于是以%d的形式打印,所以c还需要整型提升,由于c的符号位是1,所以c、整型提升后的结果为:11111111111111111111111110000011。但是这是还不能出结果,因为这里存储的只是补码,由于符号位是1,为负数。所以我们需要 补码-1=反码:11111111111111111111111110000010,然后在取反=原码:1000000000000000000000001111101,最后在转为十进制,值为:125,又因为符号位是1,所以是负数,最终结果就是:-125。

这就是一个非int类型的数据,运算过程。

12.2、算术转换

ok。上面我们我们介绍了,小于整型类型(int)的数据在进行运算时,需要进行整形提升。那我们也许会问了,那要是数据类型超过了整型呢?那如何工作呢?这个时候就需要提到算数转换了。

算数转换针对的是整型及整形以上的类型。

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

什么意思呢?简单来说就是:如果是两个不同类型的数据进行运算,其中一个类型要转换为另一个类型,比如:long 和int的数据进行运算,需要int类型的转为long类型,才能运算。

而究竟是int转换未long,还是long转换为int呢?这里有个规则:由低转高。也就是说long和int在一起,需要int转换为long类型的。

long double

double

long int

unsigned int     int   //这里unsigned int的范围高,所以进行算数转换需要将int转为unsigned int。

12.3、操作符的属性

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

  • 操作符的优先级
  • 操作符的结合性
  • 是否控制求值顺序

两个相邻的操作符先执行那个?取决于它们的优先级,如果两者的优先级相同,取决于他们的结合性。

比如:

#include <stdio.h>
int main()
{
    2 + 3 * 4 + 5;
    return 0; 
}

比如:2+34+5,肯定是\优先级高,所以先算乘法,然后算加法,但是有两个加法,这时候优先级一样就需要看符号的结合性。加法是从左向右的,所以先算2+...,再算+5...。

但是就算确定了优先级,结合性,依然在运算中会出现问题,我们要做的就是吧自己的代码书写简单,不要太复杂。

总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

相关文章
|
4天前
|
C语言
C语言之操作符1
C语言之操作符1
21 0
|
4天前
|
编译器 C语言
操作符详解(C语言基础深入解析)
操作符详解(C语言基础深入解析)
|
4天前
|
存储 编译器 C语言
爱上C语言:操作符详解(下)
爱上C语言:操作符详解(下)
|
4天前
|
算法 测试技术 C语言
【C语言】异或(^)操作符
【C语言】异或(^)操作符
18 0
|
4天前
|
存储 算法 编译器
【c 语言 】移位操作符详解
【c 语言 】移位操作符详解
45 0
|
4天前
|
存储 算法 程序员
【c 语言 】位操作符详解
【c 语言 】位操作符详解
59 0
|
4天前
|
存储 编译器 Linux
操作符详解【c语言】
操作符详解【c语言】
|
4天前
|
存储 编译器 C语言
初识C语言——详细入门(系统性学习day4)
初识C语言——详细入门(系统性学习day4)
|
4天前
|
存储 编译器 程序员
C语言第十六弹---操作符(下)
C语言第十六弹---操作符(下)
|
3天前
|
C语言
C语言——算术操作符
C语言——算术操作符
8 0