005+limou+C语言入门知识——(4)操作符

1、操作符的分类(也叫运算符)

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

下面就各操作的重点和坑点讲解

2、算术操作符

  • +,加
  • -,减
  • *,乘
  • /,除
  • %,取模

(1)除了%以外都可以作用于整数和浮点数,%左右必须是两个整数

(2)对于/,左右两个数只要有一个数是浮点数就执行浮点数的除法,否则都按照整形的除法

注意:整数在内存中存的都是补码,下面的位操作都是发生在补码身上,再用原码显示出来。

注意:浮点数是不能用移位操作的,下面的操作数都是整数。

注意:原码------>补码(先反码再减一;先减一再反码)。

3、移位操作符(让一个数的二进数位发生移动)

(1)<<左移操作符

int num = 10;
int a = num << 1;
//0000 0000 0000 0000 0000 0000 0000 1010
//0000 0000 0000 0000 0000 0000 0001 0100
//a的值为20
//注意num本身没有发生改变,除非使用<=

(2)>>右移操作符(分为两种)

  • 逻辑右移:左边用0填充,右边丢弃
  • 算术右移:左边用符号位填充,右边丢弃

那么究竟什么时候用逻辑右移,什么时候用算术右移呢?就测试情况来看的话,绝大多数编译器用的都是算术右移。

另外移动的位数必须是正数,负数是未定义的!

4、位操作符号

  • &,按位与,全1才为1
  • |,按位或,有1就为1
  • ^,按位异或,同0异1

(1)不能创建临时变量(第三个变量),实现两个数交换(本质上 ^ 自己和自己是互逆运算)

#include 
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;
}
//a ^ a = 0
//0 ^ a = a 
//a ^ 0 = a
//假设^存在逆运算v
//则a ^ a v a= 0 v a,a= 0 v a
//则0 v a = a = 0 ^ a,故“v”<-->“^”。
//本方法缺陷在于:只能是整数,并且可读性差,效率也不一定提高

(2)求一个整数存储在内存中的二进制中1的个数

//方法1:
#include 
int main()
{
    int num  = 10;
    int count=  0;//计数
    while(num)
    {
        if(num%2 == 1)
        count++;
        num = num/2;
    }
    printf("二进制中1的个数 = %d\n", count);
    return 0;
}//思考这样的实现方式有没有问题?
//方法2:
#include 
int main()
{
    int num = -1;
    int i = 0;
    int count = 0;//计数
    for(i=0; i<32; i++)
    {
        if( num & (1 << i) )
        count++;
    }
    printf("二进制中1的个数 = %d\n",count);
    return 0;
}//思考还能不能更加优化,这里必须循环32次的。
//方法3:
#include 
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;
}//这种方式是不是很好?达到了优化的效果,但是难以想到。

5、赋值操作符

(1)“=”可以做到赋值给一个变量

(2)C语言允许连续赋值,并且从左到右赋值

//连续赋值
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1;
//上面连续赋值的等价写法
int a = 10;
int x = 0;
int y = 20;
x = y + 1;
a = x;

(3)复合操作符

  • 有+=、-=、/=、*=等等,例如a+=2与a=a+2等价。

(4)注意:赋值和初始化是有区别的,这是两个概念!

int a = 3;//与a = 3不一样

6、单目操作符

  • !逻辑反操作
  • -负值
  • +正值
  • &取地址
  • sizeof(操作数)求出操作数的类型长度(以字节为单位)
  • “~”对一个是的二进制按位取反
  • ++前置、后置加加
  • - -前置、后置减减
  • ".“和”->"间接访问操作符
  • (类型)强制类型转换

下面就重点讲解

(1)sizeof()

#include 
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)(3)(4)的值为40、10、8、8

因为(3)(4)是指针变量,本程序是在x64环境下运行的,所以得出的是指针类型的大小,如果是x86环境则值为4。

(2)++、–操作符要注意先后

//前置++和--
#include 
int main()
{
    int a = 10;
    int x = ++a;//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
    int y = --a;//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
    printf("x=%d, y=%d", x, y);
    return 0;
}
//后置++和--
#include 
int main()
{
    int a = 10;
    int x = a++;//先对a先使用,再增加,这样x的值是10;之后a变成11;
    int y = a--;//先对a先使用,再自减,这样y的值是11;之后a变成10;
    printf("x=%d, y=%d", x, y);
    return 0;
}

还有一道易错题目

#include int main()
{
    int a, b, c;
    a = 5;
    c = ++a;
    b = ++c, c++, ++a, a++;
    b += a++ + c;//注意这里先用a再++
    printf("a = %d b = %d c = %d\n:", a, b, c);
    return 0;
}

最后的结果是: a = 9,b= 23,c = 8

(3)“~”运算,将数的二进制0转化为1,1转化为0,并且结果是补码,输出需要转化为原码

#include
int main()
{
    int a = 0;
    //0000 0000 0000 0000 0000 0000 0000 0000经过~后变成
    //1111 1111 1111 1111 1111 1111 1111 1111生成一个补码
    //1111 1111 1111 1111 1111 1111 1111 1110返回反码
    //1000 0000 0000 0000 0000 0000 0000 0001返回原码
    printf("%d\n", ~a);//故输出-1
    return 0;
}

注意有一种写法的理解

while(~scanf("%d", &number))//这里就是将scanf读取失败返回EOF(-1)的二进制按位取反为-1
{
    //某些代码
}

(4)注意强制转化是一种临时转化,不是永久转化

int main()
{
    int a = 1;
    printf("%f %zd\n", (float)a, sizeof(a));
    printf("%d %zd\n", a, sizeof(a));
    return 0;
}//可以看到两次输出sizeof都是4(x64)平台

7、关系操作符

  • >和<
  • >=和<=
  • !=和==

注意:等于“==”不是“=”,这一点经常会出错

8、逻辑操作符

  • &&逻辑与
  • ||逻辑或

注意:逻辑操作符的短路特性,可以减小运算。当a&&b中,a表达式为假,b表达式就不会被执行;当a||b中,a表达式为真,b表达式就不会被执行

#include 
int main()
{
    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\nd = %d\n", a, b, c, d);
    return 0;
}
//程序输出的结果是什么?因为a为0(先使用未++),后面的表达式就不重要了
//a = 1
//b = 2
//c = 3
//d = 4
//如果换成i = a++ || ++b || d++;呢?
//a = 1
//b = 3
//c = 3
//d = 4

9、条件操作符

  • exp1 ? exp2 : exp3,其意义是若exp1为真,执行exp2;若exp1为假,执行exp3
//分支语句写法
if(a > 5)
{
    b = 3;
}
else
{
    b = -3;
}
//使用三目操作符改写
a > 5 ? b = 3 : b = -3;

10、逗号表达式

* exp1, exp2, exp3, ……

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

//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//整个表达式的值是最后一个表达式的值 

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

  • 下标引用操作符[],例如:arr[<某下标>],可以访问数组的成员,注意其操作数有两个,一个是<数组名>,另一个是<数组下标/索引>
  • 函数调用操作符(),例如使用一个自定义函数:function(x),绝不能省略成“function x”,这个()是在提醒编译器,前面的关键词是一个函数!函数有多个参数的话,就有多少个操作数(采用“,”分割参数)
  • 访问结构的成员有两种方法:
    • “.”,“结构体.成员名”
    • “->”,“结构体指针->成员名”
//演示了结构体的成员访问方法
#include 
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;
}

12、表达式求值

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

Ⅰ、整型提升

(1)整型提升的概念

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

(2)整型提升的意义

  • 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
  • 通用CPU(general-purpose CPU)是难以直接实现两个8比特(1字节)字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

(3)整型提升的例子

char a, b = 3, c = 127;//b和c都被提升为整型类型,然后执行加法运算
//0000 0000 0000 0000 0000 0000 0000 0011 = 3
//0000 0000 0000 0000 0000 0000 0111 1111 = 127
//先截断存储在char中0000 0011和0111 1111,由于不溢出,故值不变

a = b + c;
//由于需要运算,需要整型提升来提高计算精度,因此0000 0011和0111 1111又变成了
//0000 0000 0000 0000 0000 0000 0000 0011 
//0000 0000 0000 0000 0000 0000 0111 1111 
//相加后就是//0000 0000 0000 0000 0000 0000 1000 0010 =  130
//加法运算完成后,结果被截断为1000 0010,然后被存储在a中。

//由于整个过程没有负数和溢出问题,所以平时使用运算的时候,整型提升会观察不出来

为什么说“没有负数”呢?

  • 因为负数的整形提升是用符号位提升的,上述正数用0来提升(例如0000 0011变成0000 0000 0000 0000 0000 0000 0000 0011 ) ,而负数的符号位是1,负数整型提升需要用1来补回。
//①负数的整形提升
char c1 = -1;
//变量c1的二进制位(补码)中只有8个比特位:1111111
//因为 char 默认为有符号的 char,所以整形提升的时候,高位补充符号位,即为1
//提升之后的结果是:11111111111111111111111111111111

//②正数的整形提升
char c2 = 1;
//变量c2的二进制位(补码)中只有8个比特位:00000001
//因为 char 默认为有符号的 char,所以整形提升的时候,高位补充符号位,即为0
//提升之后的结果是:00000000000000000000000000000001

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

(4)整型提升的验证

int main()
{
    char a = 0xb6;
    short b = 0xb600;
    int c = 0xb6000000;
    if(a==0xb6)
    {
        printf("a");
    }
    if(b==0xb600)
    {
        printf("b");
    }
    if(c==0xb6000000)
    {
        printf("c");
    }
    return 0;
}
//c只要参与表达式运算,就会发生整形提。

Ⅱ、算术转化

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

  • long double
  • double
  • float
  • unsigned long int
  • long int
  • unsigned int
  • int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运
算。C相信程序员,不会在这方面要求程序员转换,但是程序员必须承担相应的责任。例如下面的例子,就展示了算术转换一些潜在的风险。

float f = 3.14;
int num = f;//隐式转换,会有精度丢失

Ⅲ、操作符的属性

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

Ⅳ、一些问题表达式

①问题表达式1

a*b + c*d + e*f
//可能顺序1;
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
//可能顺序2;
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f

②问题表达式2

c + ++c;
//无法明确前一个c用的是加加之前的c值还是之后的c值

③问题表达式3

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(); 
//只能通过操作符的优先级得知:先算乘法,再算减法,函数的调用先后顺序无法通过操作符的优先级确定。

注意:上述的表达式在不同的环境可能会有不同的代码。

你可能感兴趣的:(C语言学习笔记,c语言,c++,开发语言)