Day 3

Day 3

前期回顾

日历作业讲解:

编写程序代码的主线思路和支线细节

数据类型转换

C语言的不同类型之间是允许进行数据类型转换的,这些类型转换可以分为两大类:

  1. 隐式类型转换(自动类型转换):由编译器自动处理和完成类型转换,程序员无需进行任何额外操作,类型转换自动完成。
  2. 强制类型转换(显式类型转换):由C程序员通过强制类型转换的语法,手动实现类型的转换。

下面我们先讨论隐式类型转换,再讨论强制类型转换。

当不同类型的参数共同组成一个表达式时,为确保表达式最终结果的类型是唯一的,编译器会自动进行隐式类型转换。

整数提升

**当表达式中仅有int以及int以下等级的类型参与时,表达式的结果一律是int(或者unsigned int)。**比如:

  1. char + char = int
  2. short + short = int
  3. char + short = int

这是因为在C语言的设计中,转换等级小于int的类型参与表达式运算时,先将它们提升到int(或unsigned int),这种语法设计就叫做"整数提升"。

常用算术转换

**表达式的最终结果类型是,表达式中取值范围和精度最大的那个操作数的类型。**这种语法设计就叫做"常用算术转换"。

注意事项(重要)

在讲上述表达式相关的隐式类型转换时,我们刻意避开了无符号的整数类型。

实际上C语言有以下明确的语法规定:

同一转换等级的有符号整数和无符号整数一起参与运算时,有符号整数会转换成对应的无符号整数。

也就是说:

unsigned + int = unsigned

unsigned long + long = unsigned long

针对无符号数的类型转换,我们给出以下建议:

  1. 在一般的应用级C程序开发中,无符号整数是用不到的,往往只有在底层开发中才会使用它。
  2. 鉴于无符号整数带来的一系列复杂性和坑爹的陷阱,请大家不要在日常代码中使用无符号数。
  3. 假如你有一天能接触到底层开发,有使用无符号整数的需求,也建议不要混合使用无符号整数和有符号整数。

强制类型转换

强制类型转换的语法非常简单,如下:

(type_name) expression    // type代表想要强转成的类型

你只需要把需要进行强转的表达式(或值)前面加上"(type_name)"即可。

在C语言中,"(type_name)"被视为一元运算符,而一元运算符的优先级是高于二元运算符的。(这意味着强转在一个表达式中往往会优先运算,这是一个非常重要的设计)

搞清楚强转的语法后,下面只要搞清楚它有啥用,或者说在什么场景下会用到它,就算是学会这个语法了。

强转在以下场景中比较常用:

  1. 在发生隐式类型转换的地方使用强制类型转换语法,显式地表明将要发生类型转换。如下列代码:

    int a;
    
    float (int)b = a; // a会隐式类型转换成int,但这里用强转显式标注转换
    
  2. 提升表达式结果的精度。一个非常典型的例子就是:一个表达式因所有操作数都是整数,结果会得到一个截断数据的整数,如两个整数相除。在这种情况下就可以使用强转来提升表达式结果的精度,如下列代码:

    float result, result2;
    
    int a = 10;
    
    int b = 4;
    
    result = a / b;
    
    result2 = (float)a / b;   // (float)是一元运算符优先级高于/除号
    
  3. 小数取整。这个比较简单,参考代码如下:

    float a = 7.1f;
    
    int num = (int)a;   // 当然这里本身就会隐式类型转换,但强转标注出来会具有更好的可读性
    
  4. 计算浮点数的小数部分。参考代码如下:

    float f = 3.14159, fractional_part;
    
    fractional_part = f - (int)f;
    
  5. 强转类型转换还经常用于避免数据溢出。比如下列代码:

    // 此表达式右值的结果在int范围内不会溢出,所以这里隐式转换就可以了
    
    long long millisPerDay = 24 * 60 * 60 * 1000; 
    
    // 此表达式右值的结果超出int范围溢出,这里如果只隐式转换就会数据失真,所以需要强转
    
    long long nanosPerDay = (long long)24 * 60 * 60 * 1000 * 1000 * 1000; 
    

注意事项:

**C语言的强制类型转换语法非常灵活,理论上允许程序员将变量从一种类型转换为任意的另一种类型。**但这种灵活的语法应当谨慎使用,多多思考,避免无意义或不安全的强转导致数据错误或者未定义行为。

给类型起别名

给类型定义别名,是C语言中极其常用和重要的语法。我们可以使用typedef关键字用于为现有的数据类型创建新的名称,即类型别名

它的语法格式如下所示**(定义别名语句一般和预处理指令放在一起,放在文件的开头)**:

typedef 现有类型的名字 别名;

现在解释一下这个语法:

  1. typedef是一个关键字,读作"type define",表示定义类型别名。
  2. 现有类型可以是C语言的基本数据类型,也可以是复合类型如结构体等。
  3. 关于别名的命名规范,首先C语言没有强制的规范去约束别名的命名风格,但基于编程实践,给出以下几点要求:
    1. 别名应该是在原有类型名的基础上,更明确更清晰的表明类型作用的,应该具有好的可读性。“只有起错的名字,没有起错的外号。”
    2. 为了区分类型名和别名,建议别名和类型名采用不同的命名风格。比如类型名采用"下划线式命名风格",那么别名可以采用"驼峰命名风格"。
    3. 在C语言标准库中,常用"_t"作为后缀结尾表明此类型名是一个别名。常见的如:size_tint32_t等。我们也可以模仿采用这种风格,当然关于命名,一切以公司的要求为最高要求。

给某个类型起别名后,就可以在后续的代码中使用这个别名代替原有的类型名,在具体使用时,别名和原类型名完全等价。如下列代码:

typedef int E;
// 等价于int作为返回值
E test(void) {}
int main(void) {
  E a = 10; // 等价于 int a = 10;
  return 0;
}

以上。


为什么要给类型定义别名/定义别名的好处是什么


搞清楚定义别名的语法是非常容易的,对于这个语法的学习,我们重点应该是理解:为什么要给类型定义别名/定义别名的好处是什么?

其实不外乎以下三个优点:

  1. 提升代码的可读性。这个很容易理解,不多赘述。原类型名往往是一个通用的称呼,而别名是此场景下的一个精准描述。
  2. 提升代码的扩展性。这一点在后续数据结构阶段会体现的很明显,在后续课程我们将展开讲解这部分内容。
  3. 提升代码的跨平台性移植性。类型别名的语法最重要的用途就是增强代码的跨平台移植性,下面将详细讲一个作用。
类型别名如何提升跨平台性移植性?

我们都知道,C语言由于编译器、平台之间的差异,经常会出现同一个类型,但存储方式不同的情况。比如:

  1. int类型在绝大多数现代桌面平台下,占用4个字节32位内存空间。大多数应用级C程序员接触的int类型,也是4个字节的int类型。
  2. 但是在某些小型机器或者嵌入式平台下,int类型可能就会变成占用2个字节16位内存空间的整数类型。(因为要节省内存空间)

于是代码在跨平台移植时,就会出现以下问题:

int a = 100000;这句代码在32位存储int时没有问题,但如果int变为16位存储,就会出现数据溢出失真的问题。

那如何避免这种情况呢?

只需要在移植代码后,使用一个32位的整数类型来存储这个a就可以了。那么具体可以用以下两种方式实现:

  1. 直接把a声明为更大的整数类型(比如long),这样大家都能装得下了,移植时就避免了溢出情况。
  2. 为每一个平台选择最合适的类型。原平台继续用int,新平台使用更大的,比如long类型。

很显然,方式一会带来空间的浪费,性能下降,方式2是更好的选择。

那么如何实现方式二呢?

很简单,使用类型别名。

我们可以在原平台上,将int类型定义别名为BigInteger:

typedef int BigInteger;

在移植后,不需要改变任何其它代码,只需要改变这个别名定义中的int为一个合适的类型即可,比如:

typedef long BigInteger;

这样就可以在几乎不用修改任何额外代码的前提下,将整个代码移植到了新平台。这里也同时体现了定义别名,增强了代码的可扩展性。

在C语言标准库中,有非常多这样为了兼顾平台移植性而被定义的类型别名,比如:

  1. **size_t类型。**此类型在任何平台下,都代表一个无符号的整数类型。在大多数情况下,它被设计为和平台位数一致的存储大小,比如32位平台下,它就是一个32位的无符号整数。size_t非常有用常用,它广泛用于表示那些在逻辑上不会为负数的概念,如:

    1. 数组的长度
    2. 字符串的长度
    3. 某个数据结构中的元素、结点的数量
    4. 内存大小,比如sizeof运算符的结果类型就是size_t

    size_t非常常见常用,要记住这个类型别名。

  2. int8_t, int16_t, int32_t, int64_t: 无论任何平台下,分别表示确切的8、16、32、64位有符号整数。

  3. uint8_t, uint16_t, uint32_t, uint64_t: 无论任何平台下,分别表示确切的8、16、32、64位无符号整数。

总之,为类型定义别名非常有用,请大家要重视这个语法。以上。

布尔值表示

关于在C语言中使用布尔值的建议

首先,我们强烈不建议大家直接把一个整数、指针类型变量作为布尔值在代码中直接使用。比如

int a = 0;
if (!a) { // 等价于 a == 0
  printf("a is 0.\n");
}

这样的做法虽然使得代码简洁,但牺牲了很多可读性,使得代码不够直观明确的表达含义,在现代C编程中是比较得不偿失的。(如果把a改成一个指针类型,这个代码将更加丑陋)

所以应该写成以下格式:

if (a  == 0) 

除此之外,我们还建议,当你需要把布尔值作为函数的返回值和形参时,也尽量不要直接用int类型,而是包含头文件使用类型别名bool。

比如:

代码块 32. 使用bool布尔类型作为函数返回值和形参类型-演示代码

#include <stdbool.h>

bool fun(void) {
  // ...
  return true;
}

总之,在现代的C编程中,我们更追求更好的可读性和明确性,尤其是当确定代码会在C99标准及以后的编译器平台上运行时,使用布尔类型bool是一个好习惯。

运算符和表达式

概述

在C语言中,表达式和语句是构成程序的基本元素。本节和下一章节我们就围绕它们展开讲一讲其中的C语言基础语法。

首先,让我们区分这两个概念:

  1. 语句(statement),语句是代码中的一个完整的,可以执行的步骤。
    1. 语句要么以";“分号结尾(简单语句),要么以”{}"代码块结尾(复合语句)
    2. 语句的作用复杂多样,常用于构建程序逻辑,如循环语句、条件判断语句、跳转语句等。
  2. 表达式(expression),表达式是由**变量、常量(称之为操作数)运算符(也叫操作符)**组成的序列,它总是会计算出一个值。
    1. 表达式可以非常简单,如一个单独的常量或变量,或者非常复杂,如包含多个运算符和函数调用的组合。
    2. 表达式的作用就是计算值、赋值、函数调用等。

C 语言拥有异常丰富的运算符,比较常见和常用的有以下运算符:

  1. 算术运算符
  2. 赋值运算符
  3. 关系运算符
  4. 逻辑运算符
  5. 位运算符
  6. 三目运算符

这些运算符,又可以根据操作数的多少,分为:

  1. 一元运算符,只需要一个操作数的运算符
  2. 二元运算符,需要两个操作数的运算符,多数运算符都属于二元运算符。
  3. 三目运算符,自然就是需要三个操作数的运算符。

下面逐一学习一下。

表达式的主要作用和副作用

为了更好的描述C语言当中运算符组成的表达式作用,我们引入两个重要的概念:

  1. 表达式的主要作用。表达式的一个特点就是必然会得到一个结果,表达式的主要作用就是表达式会计算出一个结果值。
  2. 表达式的副作用。**表达式在计算结果值之外产生的效果都可以称得上是副作用。**比如说:
    1. 修改了变量的值(最常见的)
    2. 执行了某些I/O操作,如打印到控制台或从文件读取数据。
    3. 调用函数,函数中执行了一些操作

算术运算符

算术运算符包含最常见的+, -, *, /, %,它们分别表示加,减,乘,除以及取余(取模)。这些运算符比较简单,只需要注意:

  1. 它们都是二元运算符,需要两个操作数。

  2. 如果 / 的两个操作数都是整数,那么结果也是整数 (向零取整)。因此,1 / 2 的结果为 0 而不是 0.5。

  3. % 取模运算要求两个操作数都是整数,而其它的算术运算符可以用于浮点数。

  4. 在C语言中,%运算的结果符号总是和被除数保持一致。取模运算的结果遵循以下公式:

    (a % b) = a - (a / b) * b

运算符的优先级和结合性

现在根据这张表格,我们尝试来分析几个复杂表达式的运算过程:

移位运算和算术运算结合:

对于表达式a + (b - a >> 1),该表达式是如何进行计算的呢?分析如下:

  1. "()"具有最高优先级,所以(b - a >> 1)最先计算。
  2. 右移位运算符>>的优先级低于二元运算符-,再加上小括号的存在,所以b - a最先计算。
  3. 随后将b - a结果右移。
  4. 最后,将 a 加上上述计算的结果。
解引用运算符和自增自减运算符结合:

p是一个指向int变量的指针类型,对于表达式int num = *p++,该表达式是如何进行计算的呢?分析如下:

  1. 后缀自增自减符号在表达式中拥有最高的计算优先级,所以整个表达式中p++最先进行计算。
  2. 但后缀形式的p++,它的主要作用是直接返回当前p的值副作用是返回p的值后将p的值加1。
  3. 所以*(p++)就是*p,也就是对指针p执行解引用运算。
  4. 最后执行赋值运算符=,将*p的结果赋值给变量num。整个表达式执行完毕。
  5. 当然,整个表达式执行完毕后,p会自增1。这是表达式p++带来的副作用。
  6. 整个表达式的运算过程是:将*p的结果赋值给变量num,并且p指针自增1。

对于表达式int num = ++*p,该表达式是如何进行计算的呢?分析如下:

  1. 在表达式 ++*p 中,前缀自增运算符 ++ 和解引用运算符 * 都作用于指针 p
  2. 虽然,前缀自增运算符优先级要比解引用运算符高,但它必须结合一个操作数才能计算,根据书写的位置,前缀运算符只能结合*p。所以整个表达式最先计算的是*p
  3. 前缀形式意味着主要作用是返回自增自减后的结果,副作用是自增自减1。
  4. 所以=会将*p的结果自增1后再赋值给变量num。
  5. 整个表达式的运算过程是:将*p的结果自增1后赋值给变量num。
关于运算符优先级和结合性的建议

运算符的优先级和结合性显然是运算符非常核心的重要概念,但强行把这张表格记下来显然是不现实的,所以我们给出以下总结和建议:

  1. 一元运算符的优先级总是高于二元运算符。
  2. 算术运算符的优先级仅次于单目运算符,是二元运算符中优先级最高的。
  3. 赋值运算符(包括复合赋值运算符)的优先级几乎是最低的。这很好理解,如果运算还没结束,赋值就完成了,这不是想看到的。
  4. 当不确定优先级或者为了代码清晰时,使用括号来明确运算顺序。这不仅可以避免优先级引起的错误,还可以使代码更易于理解。
  5. 在实践中不要写出非常长,非常复杂、可读性非常差的的表达式。优秀的程序员不应以"写出别人不理解的代码"为荣:
    1. 如果表达式有过长的趋势,不妨改成两个式子
    2. 表达式中即便优先级没问题,也最好用合适的小括号括起来关键位置,明确代码意图,增强代码可读性。
  6. C语言经历了漫长的发展,有很多关于复杂表达式的惯用法,除此接触它们你可能会很头疼,但基于习惯,我们还是建议程序员记住它们。
简单赋值运算符

在C语言中,简单赋值操作符 = 是最基本的赋值运算符,用于将右侧表达式的值赋给左侧的变量。这种操作是C语言中最常见的操作之一,几乎出现在每个程序中。

=赋值运算符组成的赋值表达式也具有两个作用,即主要作用和副作用。

  1. 主要作用:表达式的主要作用是计算值,赋值表达式也不例外。但赋值表达式的值比较特殊,它计算出来的值就是那个要赋给变量的值,一般就是赋值表达式的右值。
  2. 副作用:**对于赋值表达式,我们实际上更关注它的副作用。**赋值表达式的副作用就是会改变表达式左边变量的取值。

举例:

(ch = getchar()) != '\n'

这个表达式是我们前面讲基本数据类型时,给出的一个惯用法,现在我们来分析一下这个表达式:

  1. 由于小括号的存在,我们先分析表达式ch = getchar()
    1. getchar()是函数调用表达式,它的优先级最高,先执行。
    2. 函数调用会返回一个从stdin读取到的字符,然后将该字符赋值给ch变量
    3. 整个赋值表达式的值,就是getchar()函数的返回值,也就是这一次读到的字符
  2. (ch = getchar()) != '\n'整体就是:
    1. 判断getchar()函数的返回值,也就是这一次读到的字符是否是换行符
    2. 如果不是换行符,则执行xxx

除此之外,以下细节也需要稍微注意下:

赋值的过程中如果右值和左边类型不匹配,会自动进行隐式类型转换,当然这个过程中往往伴随精度损失。如下:

int i;
float f;

i = 72.99f;   /* i is now 72 */
f = 136;    /* f is now 136.0 */

由于赋值表达式的主要作用存在,所以可以用=串联几个变量一起进行赋值,但这种方式可读性差,不推荐:

i = j = k = 0;    // 三个变量的取值都是0

由于赋值运算符是右结合的,上述表达式等价于:

i = (j = (k = 0));
自增自减运算符

前缀自增自减,具有右结合性

对于一个简单的前缀运算表达式:--a

  1. 对于表达式--a我们先分析它的主要作用和副作用。
    1. 主要作用是:返回变量a自减1后的结果,这就是此表达式计算出来的一个值。
    2. 副作用是:变量a自减1。
  2. 前缀运算符的优先级非常高,如果--a处于一个复杂的表达式当中,那么它的主要作用就发挥作用。在整个表达式中它代表a自减1后的值。
  3. 比如表达式是--a * 10 + 10这样,--a会优先计算,并且它在整个表达式中代表a自减1后的值。

所以我们可以对前缀自增自减运算符做一个总结:

前缀运算符总会先进行自增自减,表达式的主要作用是返回自增自减后的结果。副作用则是变量自增自减1。

小练习

int a = 1;
int b = ++a;

printf("%d\n", --b);

printf("a is %d now.\n", a);
printf("b is %d now.\n", b);

输出的结果分别是什么呢?

下面,我们可以用同样的手段来分析后缀运算符:

后缀自增自减,具有左结合性

对于一个简单的后缀运算表达式:a++

  1. 表达式a++同样具有主要作用和副作用:
    1. 主要作用是:直接返回变量a的值,这就是此表达式计算出来的一个值。
    2. 副作用是:变量a自加1。
  2. 后缀运算符的优先级同样很高,如果a++处于一个复杂的表达式当中,那么它的主要作用就发挥作用。在整个表达式中它代表a变量原本的值。
  3. 比如表达式是a++ * 10 + 10这样,a++会优先计算,并且它在整个表达式中就代表a的值。

所以我们可以对后缀自增自减运算符做一个总结:

后缀运算符总会先直接返回被运算变量的值,表达式的主要作用是返回原本变量的值。副作用则是变量自增自减1。

注意事项(重要)

首先我们来看一段代码:

代码块 9. 自增自减运算符陷阱-演示代码

int a = 1;
// a = a++;
// int b = a++ + --a;

分别释放两段注释处的代码,结果是什么呢?符合上述我们对自增自减符号的理解吗?

实际上,上述代码在不同平台编译器运行的结果,可能是不一样的。这又涉及到C语言的一个坑爹的语法陷阱。

在C语言中,类似表达式i = i++这样,被自增自减的变量在同一个表达式中出现多次,会引发未定义行为。这意味着不同平台编译器可以选择任意方式来处理这种情况,包括但不限于返回任何值、程序错误崩溃或其余难以预测的结果。

那么为什么这种代码会导致未定义的行为呢?

这是因为同一个变量 i 在同一条语句中被修改了多次。

按照 C 语言标准,同一个变量在同一个条语句之间只能被修改一次,如果多次修改会引发未定义行为。

于是不同的编译器平台,就可以自由地决定,同一变量多次修改的执行顺序(执行顺序不同结果必然不同),甚至可以返回任意值或者程序崩溃。

总之,基于程序员日常开发的经验以及出于规避C语言语法陷阱的考量。对于自增自减符号的使用,我们给出以下建议:

  1. 在for循环中使用自增自减符号是最常见的、且最清晰、稳定准确不出错的用法。
  2. 自增自减运算符尽量不要用于连接表达式,尽量单独成行,这样就不会因副作用产生歧义。
  3. 如果一定要将自增自减符号写在表达式中,那么严禁被自增自减的变量在同一个表达式中出现多次
逻辑运算符

一个非常经典的问题是:

表达式i < j < k在 C 语言中是合法的,但可能不是你所期望的含义。

比较运算符是左结合性的,所以i < j < k等价于(i < j) < k,换句话说,该表达式首先检测 i 是否小于 j,然后用比较后产生的结果 (0 或者 1) 和 k 进行比较。

若要测试 j 是否位于 i 和 k 之间,我们应该使用:i < j && j < k

位运算符(面试重要)

在C语言中,一共提供了6个位运算符,它们都可以对整数数据进行位运算操作。

首先,我们讨论两个移位运算符(因为它们最常用),然后再讨论其他 4 个按位运算符 (按位取反~,按位与&,按位异或^,按位或|)。

为了更好的描述位运算符,这里给出一个概念定义:

**位模式:**某个变量的位模式,指的是该变量在计算机中的二进制存储形式。对于整数变量来说,位模式也就是该整数在内存中的补码形式。

移位运算符

在C语言中,有两种类型的移位运算符,它们分别是左移位运算符(<<)和右移位运算符(>>)。

移位运算符,可以通过将整数数据的二进制位向左或向右移动,来变换整数的二进制表示,从而改变整数值。

下面用两个表达式来描述移位运算符的基本作用,其中i是整数,j是一个正整数:

  1. i << j:将 i 的位模式向左移动 j 位。
    1. 移出去的高位丢弃,并在低位补0。
    2. 实际上该操作相当于将i乘以 2^j(如果没有溢出的话)<注意>:左移位是在一定范围内表示乘以2,移多少位就乘以多少个2
    3. 此表达式的主要作用是返回i移位运算后的结果
    4. 此表达式一般没有副作用,不会改变i变量的取值,移位运算符没有赋值的作用!!
  2. i >> j:将 i 的位模式向右移 j 位。
    1. 移出去的低位丢弃,如果i是无符号数或者非负值,则在左边补 0。
    2. 如果 i 是负值,其结果会根据编译器平台实现不同而不同:一些实现会在左边补 0,一些实现会在左边补 1。
    3. i是非负数的情况下,实际上该操作相当于将i除以2^j(注意整数除法的结果还是整数)
    4. 此表达式的主要作用是返回i移位运算后的结果
    5. 此表达式一般没有副作用,不会改变i变量的取值,移位运算符没有赋值的作用!!

注意事项/细节/建议

右移位运算符会根据平台实现不同,会在左边补0或者补1,这是非常坑爹的一个设计。因为当你尝试对一个负整数做右移运算时:

  1. 有些平台(左边补0的),可能会将负整数右移后得到一个正数。
  2. 而有些平台(左边补1的),会保持右移后数值的符号不变。

所以我们给出关于关于位运算符使用的第一个建议:

在实际开发中,进行移位运算的数据往往是内存地址、数组长度、某个数据结构的容量等非负数数值,所以为了更好的平台移植性,我们建议大家最好仅对无符号数进行移位运算。

一个参考的代码示例如下:

unsigned short i, j;

i = 10;   // 0000 0000 0000 1010
j = i << 2; // 0000 0000 0010 1000 十进制40
j = i >> 2; // 0000 0000 0000 0010 十进制2
***//此表达式一般没有副作用,不会改变`i`变量的取值,移位运算符没有赋值的作用!!***

总结:

从上面的例子可以看出,对无符号整数左移 j 位,相当于乘以 2^j (不发生溢出的情况下);对无符号整数右移 j 位,相当于除以 2^j。

左移时如果发生了溢出将产生未定义行为,所以程序员在执行左移位运算时,需要认真思考移位的极限,避免溢出产生。

按位运算符

按位位运算符包含:按位取反~,按位与&,按位异或^,按位或|。其中按位取反是一元运算符,其余都是二元运算符。

对于以下表达式:

~a:对a进行按位取反运算。也就是将 a 的每一位进行取反操作。即 0 变成 1,1 变成 0。

i & j:对 i 和 j 的每一位进行逻辑与运算。只有两个数的相应位都为1时,结果位才为1,否则为0。

i | j:对 i 和 j 的每一位进行逻辑或运算。只要两个数的相应位中有一个为1,结果位就为1,否则为0。

i ^ j:对 i 和 j 的每一位进行异或运算。如果两个数的相应位一个为0,另一个为1,则结果位为1,否则为0。(对应位不同结果是1)

注意:

  1. 这些表达式的主要作为是返回位运算后得到的结果,没有副作用,不会改变任何一个操作数的值!!
  2. 在C语言中,&|不是逻辑运算符,而是位运算符,不要搞混淆了。

除了按位取反运算符外,其余的位运算符都有对应的复合赋值运算符形式:

i = 21;
j = 56;
i <<= 2;    
i >>= 2;    
i &= j;     
i |= j;
i ^= j;

需要注意的是:复合赋值运算符组成的表达式是存在副作用的,是会改变变量取值的。

按位运算符中,比较需要留意的是按位异或。它具有以下一些非常优秀的性质:

a ^ 0 = a; 任何整数异或0得到的都是它本身

a ^ a = 0; 任意整数异或自己得到的都是0

a ^ b = b ^ a; 异或运算满足交换律

(a ^ b) ^ c = a ^ (b ^ c); 异或运算满足结合律

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如是我闻艺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值