avatar🌌
荡凌荡凌の赛博实验室

Technology change our life

醉后不知天在水,满船清梦压星河

一生一芯学习记录(E阶段)(二)

一生一芯学习记录(E 阶段)(二)

E2 C语言程序设计

在学习完C语言入门部分之后,接下来几章内容将逐渐探究C语言的本质。

第14章:计算机中数的表示

这章的大部分内容在F 阶段学习数字电路的时候已经学习过,下面只对习题部分和浮点数进行记录。

2、不同进制的换算

习题1:二进制小数可以这样定义:(0.A1A2A3...)2=A1×21+A2×22+A3×23+...(0.A_1A_2A_3...)2=A_1×2^{-1}+A_2×2^{-2}+A_3×2^{-3}+... 这个定义同时也是从二进制小数到十进制小数的换算公式。从本节讲的十进制转二进制的推导过程出发类比一下,十进制小数换算成二进制小数应该怎么算?

十进制小数转二进制小数只需要每次将小数部分乘以2,积的整数部分就是二进制小数的一位,剩下的小数部分继续乘以2,直到小数部分为0,或者达到想要的精度。

习题2:再类比一下,八进制(或十六进制)与十进制之间如何相互换算?

类似于二进制与十进制的换算:

  • 八进制(十六进制)转十进制:每位加权求和。
  • 十进制转八进制(十六进制):除8(或16)取余。

4、浮点数

浮点数在计算机中的表示是基于科学计数法(Scientific Notation)的,我们知道32767这个数用科学计数法可以写成 3.2767×1043.2767×10^4,3.2767称为尾数(Mantissa,或者叫Significand),4称为指数(Exponent)。浮点数在计算机中的表示与此类似,只不过基数(Radix)是2而不是10。

下面我们用一个简单的模型来解释浮点数的基本概念。我们的模型由三部分组成:符号位、指数部分(表示2的多少次方)和尾数部分(小数点前面是0,尾数部分只表示小数点后的数字)。

 sign bit     exponent      significand
|  1 bit  |    5 bits   |     8 bits     |

如果要表示17这个数,我们知道 17=17.0×100=0.17×10217=17.0×10^0=0.17×10^2,类似地,17=(10001)2×20=(0.10001)2×2517=(10001)_2 ×2^0=(0.10001)_2×2^5,把尾数的有效数字全部移到小数点后,这样就可以表示为:

 sign bit     exponent      significand
|    0    |    00101   |     10001000     |

如果我们要表示0.25就遇到新的困难了,因为 0.25=1×22=(0.1)2×210.25=1×2^{-2}=(0.1)_2×2^{-1},而我们的模型中指数部分没有规定如何表示负数。我们可以在指数部分规定一个符号位,然而更广泛采用的办法是使用偏移的指数(Biased Exponent)。规定一个偏移值,比如16,实际的指数要加上这个偏移值再填写到指数部分,这样比16大的就表示正指数,比16小的就表示负指数。要表示0.25,指数部分应该填16-1=15:

 sign bit     exponent      significand
|    0    |    01111   |     10000000     |

现在还有一个问题需要解决:每个浮点数的表示都不唯一,例如 17=(0.10001)2×25=(0.010001)2×2617=(0.10001)_2×2^5=(0.010001)_2×2^6,这样给计算机处理增加了复杂性。为了解决这个问题,我们规定尾数部分的最高位必须是1,也就是说尾数必须以0.1开头,对指数做相应的调整,这称为正规化(Normalize)。由于尾数部分的最高位必须是1,这个1就不必保存了,可以节省出一位来用于提高精度,我们说最高位的1是隐含的(Implied)。这样17就只有一种表示方法了,指数部分应该是 16+5=21=(10101)216+5=21=(10101)_2,尾数部分去掉最高位的1是0001:

 sign bit     exponent      significand
|    0    |    10101   |     00010000     |

正是因为位数限制的问题,因此浮点运算时要注意精度损失(Significance Loss)问题,有时计算顺序不同也会导致不同的结果。

第15章:数据类型详解

1、整型

在C语言中char型占一个字节的存储空间,一个字节通常是8个bit。如果这8个bit按无符号整数来解释,取值范围是0~255,如果按有符号整数来解释,采用2’s Complement表示法,取值范围是-128~127。C语言规定了signedunsigned两个关键字,unsigned char型表示无符号数,signed char型表示有符号数。

注意,ASCII码的取值范围是0~127,所以不管char型是有符号的还是无符号的,存一个ASCII码都没有问题,一般来说,如果用char型存ASCII码字符,就不必明确写是signed还是unsigned,如果用char型表示8位的整数,为了可移植性就必须写明是signed还是unsigned

除了char型之外,整型还包括short int(或者简写为short)、intlong int(或者简写为long)、long long int(或者简写为long long)等几种,这些类型都可以加上signedunsigned关键字表示有符号或无符号数。

还有一点要注意,除了char型以外的这些类型如果不明确写signedunsigned关键字都表示signed,这一点是C标准明确规定的,不是Implementation Defined。

除了char型在C标准中明确规定占一个字节之外,其它整型占几个字节都是Implementation Defined。通常的编译器实现遵守ILP32或LP64规范,如下表所示。

类型ILP32(位数)LP64(位数)
char88
short1616
int3232
long3264
long long6464
指针3264

ILP32这个缩写的意思是int(I)、long(L)和指针(P)类型都占32位,通常32位计算机的C编译器采用这种规范,x86平台的gcc也是如此。LP64是指long(L)和指针占64位,通常64位计算机的C编译器采用这种规范。

八进制整数常量以0开头,后面的数字只能是0~7,例如022,因此十进制的整数常量就不能以0开头了,否则无法和八进制区分。十六进制整数常量以0x或0X开头,后面的数字可以是0~9、a~f和A~F。

后缀十进制常量八进制或十六进制常量
int
long int
long long int
int
unsigned int
long int
unsigned long int
long long int
unsigned long long int
u或Uunsigned int
unsigned long int
unsigned long long int
unsigned int
unsigned long int
unsigned long long int
I或Llong int
long long int
long int
unsigned long int
long long int
unsigned long long int
既有u或U,又有I或Lunsigned long int
unsigned long long int
unsigned long int
unsigned long long int
II或LLlong long intlong long int
unsigned long long int
既有u或U,又有II或LLunsigned long long intunsigned long long int

2、浮点型

C标准规定的浮点型有floatdoublelong double,和整型一样,既没有规定每种类型占多少字节,也没有规定采用哪种表示形式。浮点数的实现在各种平台上差异很大,有的处理器有浮点运算单元(FPU,Floating Point Unit),称为硬浮点(Hard-float)实现;有的处理器没有浮点运算单元,只能做整数运算,需要用整数运算来模拟浮点运算,称为软浮点(Soft-float)实现。大部分平台的浮点数实现遵循IEEE 754,float型通常是32位,double型通常是64位。

以前我们只用到最简单的浮点数常量,例如3.14,现在看看浮点数常量还有哪些写法。由于浮点数在计算机中的表示是基于科学计数法的,所以浮点数常量也可以写成科学计数法的形式,尾数和指数之间用e或E隔开,例如314e-2表示 314×102314×10^{-2},注意这种表示形式基数是10。

浮点数的后缀和类型之间的对应关系比较简单,没有后缀的浮点数常量是double型的,有后缀f或F的浮点数常量是float型的,有后缀l或L的浮点数常量是long double型的。

3、类型转换

3.1 Integer Promotion

在一个表达式中,凡是可以使用intunsigned int类型做右值的地方也都可以使用有符号或无符号的char型、short型和Bit-field。如果原始类型的取值范围都能用int型表示,则其类型被提升为int,如果原始类型的取值范围用int型表示不了,则提升为unsigned int型,这称为Integer Promotion。做Integer Promotion只影响上述几种类型的值,对其它类型无影响。C99规定Integer Promotion适用于以下几种情况:

  • 如果一个函数的形参类型未知,例如使用了Old Style C风格的函数声明,或者函数的参数列表中有...,那么调用函数时要对相应的实参做Integer Promotion,此外,相应的实参如果是float型的也要被提升为double型,这条规则称为Default Argument Promotion。
  • 算术运算中的类型转换。有符号或无符号的char型、short型和Bit-field在做算术运算之前首先要做Integer Promotion,然后才能参与计算。
3.2 Usual Arithmetic Conversion

两个算术类型的操作数做算术运算,比如a + b,如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算,这称为Usual Arithmetic Conversion。转换规则如下:

  • 如果有一边的类型是long double,则把另一边也转成long double
  • 否则,如果有一边的类型是double,则把另一边也转成double
  • 否则,如果有一边的类型是float,则把另一边也转成float
  • 否则,两边应该都是整型,首先按上一小节讲过的规则对ab做Integer Promotion,然后如果类型仍不相同,则需要继续转换。首先我们规定charshortintlonglong long的转换级别(Integer Conversion Rank)一个比一个高,同一类型的有符号和无符号数具有相同的Rank。转换规则如下:
    • 如果两边都是有符号数,或者都是无符号数,那么较低Rank的类型转换成较高Rank的类型。
    • 否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。
    • 剩下的情况是:一边有符号另一边无符号,并且无符号数的Rank低于有符号数的Rank。这时又分为两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。
    • 否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转成有符号数的Rank对应的无符号类型。

到目前为止我们学过的+ - * / % > < >= <= == !=运算符都需要做Usual Arithmetic Conversion,因为都要求两边操作数的类型一致,在下一章会介绍几种新的运算符也需要做Usual Arithmetic Conversion。单目运算符+ - ~只有一个操作数,移位运算符<< >>两边的操作数类型不要求一致,这些运算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion,运算符~ << >>将在下一章介绍。

3.3 由赋值产生的类型转换

如果赋值或初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。

函数调用传参的过程相当于定义形参并且用实参对其做初始化,函数返回的过程相当于定义一个临时变量并且用return的表达式对其做初始化,所以由赋值产生的类型转换也适用于这两种情况。

3.4 强制类型转换

以上三种情况通称为隐式类型转换(Implicit Conversion,或者叫Coercion),编译器根据它自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符(Cast Operator)自己规定某个表达式要转换成何种类型,这称为显式类型转换(Explicit Conversion)或强制类型转换(Type Cast)。

第16章:运算符详解

1、位运算

整数在计算机中用二进制的位来表示,C语言提供一些运算符可以直接操作整数中的位,称为位运算,这些运算符的操作数都必须是整型的。

1.1 按位与、或、异或、取反运算

C语言提供了按位与(Bitwise AND)运算符&、按位或(Bitwise OR)运算符|和按位取反(Bitwise NOT)运算符~,此外还有按位异或(Bitwise XOR)运算符^

1.2 移位运算

移位运算符(Bitwise Shift)包括左移<<和右移>>

移动的位数必须小于左操作数的总位数。

当操作数是有符号数时,右移运算的规则比较复杂:

  • 如果是正数,那么高位移入0。
  • 如果是负数,那么高位移入1还是0不一定,这是Implementation-defined的。

综上所述,由于类型转换和移位等问题,用有符号数做位运算是很不方便的,所以,建议只对无符号数做位运算,以减少出错的可能

习题1:下面两行printf打印的结果有何不同?请读者比较分析一下。

int i = 0xcffffff3;
printf("%x\n", 0xcffffff3>>2);
printf("%x\n", i>>2);

%x是将一个整数转换为十六进制表示的字符串。最终输出第一行是33fffffc,第二行是f3fffffc,由于第一行0xcffffff3是无符号数,因此移位时在最前面补两个0;而第二行中的i的格式为int类型,是有符号数,编译器处理移位时在最高位补两个1

1.3 掩码

如果要对一个整数中的某些位进行操作,怎样表示这些位在整数中的位置呢?可以用掩码(Mask)来表示。比如掩码0x0000ff00表示对一个32位整数的8~15位进行操作。

其实就是用掩码和逻辑结合移位来实现对位进行操作。

习题1:统计一个无符号整数的二进制表示中1的个数,函数原型是int countbit(unsigned int x);

这个程序思路比较简单,每次检查最低位是不是1即可,如果是,就进行计数,然后对原来的整数右移一位;不是的话直接移位,最后返回计数值即可。

c
int countbit(unsigned int x)
{
    int i = 0;
    while(x != 0){
        if((x & 1) == 1){
            i++;  
        }
        x = (x >> 1);
    }
    return i;
}

习题2:用位操作实现无符号整数的乘法运算,函数原型是unsigned int multiply(unsigned int x, unsigned int y);。例如:(11011)2×(10010)2=((11011)2<<1)+((11011)2<<4)(11011)_2×(10010)_2=((11011)_2<<1)+((11011)_2<<4)

同样运用移位检查y中每个1所在的位置,之后对x进行相应的移位,最后累加求和即可。

c
unsigned int multiply(unsigned int x, unsigned int y)
{
    unsigned int result = 0;
    int i = 0;
    while (y != 0){
        if(y & 1){
            result = result + (x << i);
        }
        y = y >> 1;
        i++;
    }
    return result;
}

习题3:对一个32位无符号整数做循环右移,函数原型是unsigned int rotate_right(unsigned int x);。所谓循环右移就是把低位移出去的部分再补到高位上去,例如rotate_right(0xdeadbeef, 16)的值应该是0xefdeadbe

一生一芯学习记录(E阶段)(一)
Valaxy v0.28.4 驱动|主题-Yunv0.28.4