C语言_数据类型_浮点型

课件

image-20210818232054410

image-20210818232259910
image-20210818232907404

image-20210818231225754

image-20210818231301755
image-20210818231320838
image-20210818231341094
image-20210818231554889
image-20210818231457059

image-20210818231917144

二进制

二进制怎么转换为十进制?0101 -> 5

1
2
3
4
5
次数:  3  2  1  0
权值: 0 1 0 1
0 + 1 * 2^2 + 0 + 1 * 2^0
= 0 + 4 + 0 + 1
= 5

十进制怎么转换为二进制?5 -> 101

1
2
3
4
5
6
7
2  | 5                     
--------- _
2 | 2 ...... 1 /|\ 后3位
--------- |
2 | 1 ..... 0 | 后2位
------- |
0 ...... 1 | 后1位

负数怎么表示

以下均是在4位机下讨论

负5?

5 + x = 0 -> 0101 + x = 0 -> 0101 + 1010 = 1111, 1111 + 1 = 0, x = 1010 + 1 = 1011

负数用补码表示,是原码的反码加1。

负8?

先求8的原码的反码,再加1:8: 1000 -> 0111 + 1 -> 1000

负8的补码居然是它自己?

搞了半天发现,我们一开始就说明了,在4位机下讨论,那么有符号数的范围只有-8 ~ 7,根本就不存在8一说。所以,无法求得8的原码,也因此无法求8的补码。(为什么1000不能代表8的原码?因为有符号数中第一位代表符号位!)

16进制

1
2
3
 0101 1100
5 C
=> 0x5C

数字字面常量的规则:只要第一位是数字,那么代表这是个数字。而0开头的数字,不带x的是8进制(0___),带x的是16进制(0x___),带b的是2进制(0b___)。

16进制的格式化输出的描述符为%x,代表unsigned hexadecimal integer,是无符号十六进制整型。

1
2
3
4
5
6
int main()
{
unsigned char ucVal = 0x5cu;//0x5c是数字,u是无符号指示
printf("%hhu\n", ucVal); //92
printf("%hhx\n", ucVal); //5c
}

8进制

8进制的格式化输出的描述符为%o,代表unsigned octal,是无符号8进制数。

1
2
3
 01 011 100
1 3 4
=> 0134
1
2
3
4
5
6
int main()
{
unsigned char ucVal = 0134u;//0134是数字,u是无符号指示
printf("%hhu\n", ucVal); //92
printf("%hho\n", ucVal); //134
}

2进制

输出2进制:需要把C语言设置为17标准。

2进制没有格式化输出的描述符。

1
2
      0101 1100
=> 0b 0101 1100
1
2
3
4
5
6
int main()
{
unsigned char ucVal = 0b0101'1100u; // '为分割符号,便于人性化输入,可有可无
printf("%hhu\n", ucVal); //92
printf("%hho\n", ucVal); //134
}

integer

32 bits, 4 Bytes

  1. 区分有符号、无符号,其中有符号的signed可以省略;无符号带int的整型的int可以省略,其他无符号整型不能省;long int中的int可以省略
  2. 字面常量(8、8u)也是有类型的,不带后缀默认是有符号数,带u是无符号数。

long int

ISO标准中提到,long int的大小不得小于int。目前微软long int的大小为32bits、4字节;而在Linux下为64bits、8字节。

long指示类型的长度,涉及到长度,格式化输出时,需要注意加上length specifiers,即长度描述符。

long long int

64bits、8字节

short int

16bits、2字节

针对于整型字面常量的长度描述符没有专门用于short的,因为C语言字面常量最小为32位。如果比32位小的,一律向下兼容。归根结底是因为数据总线最少一次传32位。

但是,针对于printf中的格式化输出,还是要区分长度的,对应short的长度描述符为h。

char

如果要打印十进制整数,那么对应char的长度描述符为hh。格式描述符为iu

而如果要打印字符,那么对应的格式描述符为c

ASCII码:形式上是字符图形,但本质上是整数,如'a'是97。

整型类测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main()
{
/* integer: 32bits */
/*signed*/ int iVal = 8;
unsigned /*int*/ uVal = 8u;
printf("%i\n", iVal);
printf("%u\n", uVal);
/* long int: 32bits */
/*signed*/ long /*int*/ lVal = 9l;
unsigned long /*int*/ ulVal = 9ul;
printf("%li\n", lVal);
printf("%lu\n", ulVal);
/* long long int: 64bits */
/*signed*/ long long /*int*/ llVal = 9ll;
unsigned long long /*int*/ ullVal = 9ull;
printf("%lli\n", llVal);
printf("%llu\n", ullVal);
/* short int: 16bits */
/*signed*/ short /*int*/ sVal = 9; //9后面没有专门用于short的长度指示符
/*unsigned*/ short /*int*/ usVal = 9u;
printf("%hi\n", sVal); //但是格式化输出有,要加上h
printf("%hu\n", usVal);
/* char: 8bits */
/*signed*/ char cVal = 9;
unsigned char ucVal = 9u;
printf("%hhi\n", cVal);
printf("%hhu\n", ucVal);
unsigned char ucVal2 = 'a';
printf("%c\n", ucVal2);
}

浮点型

计算机中整型和浮点型的计算是在不同的处理器下完成的。整型处理器是由x86部分完成的,浮点型处理器是由x87部分完成的。因为整型和浮点型的格式是不一样的。

小数默认都是有符号数。

1010.1101二进制小数化为十进制小数。依然按照每一位的权重展开计算:

\begin{align} & 1\times2^3+0\times2^2+1\times2^1+0\times2^0+1\times2^{-1}+1\times2^{-2}+0\times2^{-3}+1\times2^{-4}\\ & =8+2+\frac{1}{2}+\frac{1}{4}+\frac{1}{16}\\ & =10.8125 \end{align}

10.8125十进制小数如何化为二进制小数?整数部分是一直进行余2运算,而小数部分如何化?即0.1101。

\begin{align} 0.8125\times2&=1.625\cdots1\\ 0.625\times2&=1.250\cdots1\\ 0.25\times2&=0.500\cdots0\\ 0.5\times2&=1.000\cdots1\\ &end \end{align}

与求整数的二进制不同,求余之后,整数二进制的结果从下往上顺位。而小数二进制是从上往下顺位。

0.13化为二进制小数时会遇到无限循环的现象:

\begin{align} 0.13\times2&=0.26\cdots0\\ 0.26\times2&=0.52\cdots0\\ 0.52\times2&=1.04\cdots1\\ 0.04\times2&=0.08\cdots0 -- cycle\\ 0.08\times2&=0.16\cdots0\\ 0.16\times2&=0.32\cdots0\\ 0.32\times2&=0.64\cdots0\\ 0.64\times2&=1.28\cdots1\\ 0.28\times2&=0.56\cdots0\\ 0.56\times2&=1.02\cdots1\\ 0.02\times2&=0.04\cdots0\\ 0.04\times2&=0.08\cdots0 -- cycle\\ &\cdots \end{align}

float(单精度)

全称:single point float,单精度浮点数。32 bits, 4 Bytes

格式描述符用f。代表十进制浮点数(Decimal floating point)

1
2
3
4
5
int main()
{
float fVal = 5.0f; //加f后缀指示其为float类型字面常量
printf("%f\n", fVal);
}

double(双精度)

全称:double point float,双精度浮点数。64 bits, 8 Bytes

格式描述符也是用f。代表十进制浮点数(Decimal floating point)

1
2
3
4
5
int main()
{
double dVal = 5.0; //字面小数常量不加后缀,默认为double类型
printf("%f\n", dVal);
}

long double

Modern Cpp和新的C标准才有的。标准指出long double长度不得小于double。在微软编译器下等于double长度,有些编译器是大于double长度的。为什么微软如此保守呢?因为CPU的字长一般还是64位。如果大小设计超过64位的话,就需要两个时钟周期来完成数据的传输。

需要注意,long double的格式描述符依旧为f%后面的长度描述符不再是l而是大写的L。而字面常量的后缀还是小写的l

1
2
3
4
5
int main()
{
long double ldVal = 5.0l; // 加l后缀
printf("%Lf\n", ldVal); // 加L前缀,格式描述符
}

科学计数法

651.32怎么表示(字面常量)?

  1. 651.32
  2. 6.52132e+2,其中+可以省略

科学计数法的格式描述符为eE。表示:Scientific notation (mantissa/exponent), lowercase/uppercase

0.065132呢?6.5132e-2

1
2
3
4
5
6
7
int main()
{
double dVal = 6.52132e+2;
printf("%f\n", dVal); // 652.132000
printf("%e\n", dVal); // 6.521320e+02
printf("%E\n", dVal); // 6.521320E+02
}

float内存结构(IEEE754标准)

IEEE754标准。所有处理器,无论手机上的ARM架构还是服务器处理器都是遵循这个标准。

三部分:sign(符号位)、exponent(指数)、mantissa(底数)

sign(符号位)

sign(符号位) - 1 bit - 1代表负,0代表正

exponent(指数)

首先,指数是一个有符号数。
负数,是补码表达的。八位二进制来说,0000 0000表示0,则 1111 1111表示-1,1000 0000表示-128,0111 1111表示127,这是有符号正数的最大值,再加1就溢出了。本来的范围是:-128 ~ 127
但是,浮点数中指数位置存储的是偏移值,如果是float,存储的值是加了127的。因此,指数为1时,应该存储1 + 127 = 128,存储1000 0000;指数为2时,应该存储2 + 127 = 129,存储1000 0001。因此,加127后,范围就变成了:-1 ~ 254
而指数全0、全1,在浮点数中有特殊含义。所以,范围就变成了:1 ~ 254。减去127之后,实际的范围就变成了:-126 ~ 127

举一个例子:7.25怎么表示?先转化为二进制:0111.01,带权的形式则是:1.1101 * 2^2,即右移2位,指数为2。
实际存储的二进制形式:

1
2
0     1000 0001            (1)110 1000 0000 0000 0000 0000`
符 指数位(2+127=129) 底数位(23位)前面有个隐藏的1,不在这23位中

exponent(指数) - 8 bits - 标准里规定:

在exp位模式(the bit pattern of exp)既不全为0,也不全为1时,浮点数值为规格化的值
阶码字段在这种情况下,被解释为以偏置(biased)形式表示的有符号整数(原文:the exponent field is interpreted as representing a signed integer in biased form)。
那么,阶码字段的值为:E=eBiasE = e - Bias
其中ee是无符号数,即直接通过exp位模式计算得出。
BiasBias是一个固定值2k11,k=exp的位数,单精度下是8,双精度下是112^{k-1}-1, k = exp的位数, 单精度下是8位, 双精度下是11位。比如,float,指数二进制位数为8位时,Bias就是128-1=127。
单精度下Bias=271=127Bias=2^7-1=127
因此EE范围:(1127)(254127)=126127(1-127)\sim(254-127)=-126\sim127,表示在2进制下可以右移127位、左移126位。
e和最终的移位值之间的对应关系:1 = -1262 = -125,…,253 = 126254 = 127

由此,看出,不能简单地把exp位模式看做有符号数直接计算得到移位数值,如果直接当做有符号数计算的话,范围变成了:1000'0000 ~ 0111'1111 = -128 ~ 127。和标准规定的对应不上!

为什么要预留出来exp位模式全0或全1的情况?

  1. 全0是为了能让浮点数可以表示0或者表示非常接近于0.0的数。此情况在标准中称为:“非规格化的值”。这种情况下,阶码值(移位值)规定为E=1BiasE=1-Bias。并且要特别注意:底数的值是位模式直接计算出来的,也就是小数字段的值,不包含隐含的开头的1。即:0.XXXX,不再是1.XXXX

使阶码值为1Bias1-Bias而不是简单的Bias-Bias​似乎是违反直觉的。但是这种方式提供了一种从非规格化值平滑转换到规格化值的方法。

  1. 非规格化数有两个用途。首先,它们提供了一种表示数值0的方法,因为使用规格化数,我们必须总是使M1M\geq1,因此就不能表示0。实际上,+0.0+0.0的浮点表示的位模式为全0:符号位是0,阶码字段全为0(表明是一个非规格化值),而小数域也全为0,这就得到M=f=0M=f=0。令人奇怪的是,当符号位为1,而其他域全为0时,我们得到值0.0-0.0。根据IEE的浮点格式,值+0.0+0.00.0-0.0在某些方面被认为是不同的,而在其他方面是相同的。
  2. 非规格化数的另外一个功能是表示那些非常接近于0.00.0的数。这提供了一种属性,称为逐渐溢出(gradual underflow),其中,可能的数值分布均匀地接近于0.00.0。而刚才提到的使阶码值为1Bias1-Bias而不是简单的Bias-Bias,就是为这个做铺垫的!详看CSAPP-3rd P80
  3. 全1是为了能让浮点数表示
    1. 无穷大 - 底数全0时
    2. NaN - 底数非0时

mantissa(底数)

  1. mantissa(底数) - 23 bits - 范围、精度lg2247.2247\lg 2^{24} \approx 7.2247,即可以表示7位十进制数。
    1. 因为要用科学计数法,底数第一位必须是1,因此可以省略第一位。因此此处的23位可以表达24位二进制数。

7.25的IEEE754表示:

0 1000'0001 110'1000'0000'0000'0000'0000

浮点数的好处

  1. 虽然精度小,但是可表示的范围大(指数的作用)。
  2. 能表达实数(除了小数,也能表示整数、0)
  3. 能表达NaN(Not A Number),0除以0的结果就是NaN。
    1. 0 1111'1111 100'0000'0000'0000'0000'0000
  4. 能表达正负inf(无穷大),比如1除以0。
    1. 正无穷:0 1111'1111 000'0000'0000'0000'0000'0000
    2. 负无穷:1 1111'1111 000'0000'0000'0000'0000'0000

怎么比较浮点数

首先是不带等号的大小判断(<、>)

  • 对于 a < ba > b 这种​​大小关系比较​​,通常可以​​安全地直接使用运算符​​。
  • 因为即使存在微小的舍入误差,只要这个误差不足以改变大小关系的本质(即误差远小于 a 和 b 本身的差值),结果就是正确的。
  • 判断 a 是否严格大于 b 时,应该使用 a > b,而不是 !(a <= b)(后者涉及相等判断,不精确)。

特殊值的比较

  • 特殊值如 NaN (Not a Number) 与任何值(包括自己)比较,== 都返回 false!= 都返回 true。必须用专门的函数检查:
    • Python: math.isnan(x)
    • C/C++: isnan(x) (来自 math.h 或 cmath)
    • Java: Double.isNaN(x) / Float.isNaN(x)
  • 检查无穷大:
    • Python: math.isinf(x)
    • C/C++: isinf(x)
    • Java: Double.isInfinite(x) / Float.isInfinite(x)

涉及到两个浮点数是否相等时

绝对不能直接用 == 或 != 来判断两个浮点数是否“相等”!​
因为浮点数在计算机内部使用 IEEE 754 标准以二进制存储小数,很多十进制小数无法精确表示(例如 0.1),计算过程中也会积累微小的舍入误差。
正确的比较方法是​​允许一定的误差范围(容差 epsilon)​​:

  1. ​检查近似相等 (Approximate Equality):​
    • 计算两个浮点数 a 和 b 的绝对差值:diff = abs(a - b)
    • 定义一个非常小的正数作为容忍度 epsilon(例如 1e-91e-12, 具体值取决于你的精度要求)。
    • 如果 diff <= epsilon,则认为 a 和 b 在 epsilon 的误差范围内是“相等”的。
    • 但是这个容差是绝对容差,有缺陷,见下:
  • 检查相对相等 (Relative Equality - 更稳健):​
    • 当数值大小差异巨大时,固定绝对容差可能不合适(比如比较 1e9 和 1e9+1e-9 时差值很小,但比较 1e-9 和 2e-9 时用绝对容差 1e-9 会认为相等)。相对容差考虑数值的大小。
    • rel_tol: 相对容差(如 1e-5, 1e-9)
    • abs_tol: 绝对容差下限(保证接近零的数也能比较,例如设为 1e-12

下面主要论述,为什么需要相对相等(使用相对容差)

直接使用绝对相等的容差(比如 abs(a - b) <= 1e-9)在大多数情况下是有效的。但是,它有一个显著的缺点:当比较的数值本身非常大或者非常小(靠近零)时,这个固定大小的绝对容差就显得不合理了。

  1. ​问题场景一:数值巨大​

    • 例子:比较 a = 1, 000, 000, 000 (1e9) 和 b = 1, 000, 000, 001 (1e9 + 1)
    • 它们的绝对差是 |a - b| = 1
    • 如果你设定的绝对容差 epsilon = 1e-9(即 0.000000001),那么 1 > 1e-9,程序会判断它们​​不相等​​。
    • 但直觉上,10亿和10亿零1之间的_相对误差_非常小(大约是 1 / 1e9 = 1e-9)。在很多科学计算或工程领域,这个精度已经足够了,我们可能希望认为它们_相对相等_。
    • ​结论:​​ 当数值本身很大时,一个固定的小绝对容差过于严格,忽略了数值的量级。
  2. ​问题场景二:数值极小(接近零)​

    • 例子:比较 a = 0.000001 (1e-6) 和 b = 0.000002 (2e-6)
    • 它们的绝对差是 |a - b| = 0.000001 (1e-6)
    • 如果你设定的绝对容差 epsilon = 1e-9(即 0.000000001),那么 1e-6 > 1e-9,程序会判断它们​​不相等​​。
    • 但是,它们的相对差非常大(一个是另一个的两倍,相对误差高达 1e-6 / 1e-6 = 1 或 100%)。实际上,它们_不应_被看作是近似相等的。
    • ​结论:​​ 当数值本身很小时,一个固定的小绝对容差又过于宽松,可能把差别很大的两个数判断为相等。
    • ​更麻烦的问题:比较接近零的数​
      • 例子:比较 a = 0.000000001 (1e-9) 和 b = 0.000000002 (2e-9)
      • 绝对差 |a - b| = 1e-9,如果我们设定的绝对容差也是 1e-9,那么程序会认为它们相等。
      • 例子:比较 a = 1e-20 和 b = 2e-20
      • 绝对差 |a - b| = 1e-20。一个合理的绝对容差(比如 1e-9)远远大于这个差值(1e-9 > 1e-20),所以程序也会认为它们相等。但从相对角度看,b 是 a 的两倍!
      • ​总结:​​ 对于非常接近零的数,即使设置了一个看似很小的绝对容差,也可能过于宽松,无法反映数值之间的真实相对误差。这时甚至需要一个更小的、不切实际的绝对容差才能区分它们,而相对相等可以自然地处理这种情况(此时相对误差会很大)。

​相对相等的解决方案:引入相对容差​

相对相等的核心思想是:​​判断两个数是否近似相等的标准,应该与它们自身的_大小_有关。​

  1. ​核心公式:​

    abs(a - b) <= max(rel_tol * max(|a|, |b|), abs_tol)

    • rel_tol: ​​相对容差(relative tolerance)​​。这是一个很小的正数,表示你能接受的_最大相对误差_(例如 0.01 表示 1% 的相对误差,1e-5 表示 0.001% 的相对误差,1e-9 表示极小的相对误差)。这个值需要根据你的具体应用场景(你对精度的要求)来设定。
    • abs_tol: ​​绝对容差下限(absolute tolerance)​​。这也是一个很小的正数(例如 1e-12)。它的作用是确保当 a 和 b 都_非常非常接近零_时,公式仍然有效。
  2. ​公式解读:​

    • max(|a|, |b|): 取 a 和 b 的绝对值中较大的那个。这代表了参与比较的两个数在数值上的大致_量级(Scale)_。
    • rel_tol * max(|a|, |b|): ​​动态计算的相对容差​​。这个容差会根据 a 和 b 的当前量级自动调整:
      • 当 a 和 b 很大时,这个值会变大,可以容忍较大的绝对差值(只要相对误差小)。
      • 当 a 和 b 很小时(但还没有小到必须依赖 abs_tol),这个值会变小,要求更小的绝对差值才能被视为相等。
    • max( ..., abs_tol): 取 动态计算的相对容差 和 abs_tol 中​​较大的那个​​作为最终的容差阈值。
      • 当 a 和 b 远离零时,动态计算的相对容差 通常会远大于 abs_tol,所以max 的结果就是相对容差。
      • 当 a 和 b 非常接近零(或者其中一个为零)时,动态计算的相对容差 (rel_tol * max(|a|, |b|)) 会变得非常小(接近于零)。如果此时没有 abs_tol,即使两个非常接近零但彼此不同的数(比如 1e-20 和 2e-20,差值 1e-20),也会因为 1e-20 > 某个几乎为零的动态相对容差 (比如 1e-9 * 2e-20 = 2e-29) 而被错误地认为_不相等_(而实际上根据相对误差,它们差异很大)。更极端的是比较 0 和一个很小的数(比如 1e-30),动态计算的相对容差 会变成 0,没有 abs_tol 就无法进行有效比较。加入 abs_tol 提供了这个绝对下限。
      • abs_tol 就是为了确保在这种情况下,公式不会因为 动态计算的相对容差 太小而失效。它会提供一个最低限度的绝对容差保证(比如 1e-12)。如果 |a - b| <= abs_tol,即使 a 和 b 本身很小(导致 rel_tol * max(|a|, |b|) 更小),也能认为它们在绝对意义上足够接近零。
  3. ​举例说明:​

    • ​情况一:大数值(相对容差主导)​
      • a = 1e9b = 1e9 + 1000 (|a-b|=1000)
      • 设 rel_tol=1e-6abs_tol=1e-9
      • max(|a|,|b|) ≈ 1e9
      • 动态相对容差 = 1e-6 * 1e9 = 1000
      • max(1000, 1e-9) = 1000
      • 1000 (|a-b|) <= 1000 (阈值) → ​​相对相等(成立)​
    • ​情况二:中等数值(相对容差主导)​
      • a = 3.141592b = 3.141593 (|a-b|=0.000001)
      • 设 rel_tol=1e-6abs_tol=1e-9
      • max(|a|,|b|) ≈ 3.1416
      • 动态相对容差 ≈ 1e-6 * 3.1416 ≈ 3.1416e-6
      • max(3.1416e-6, 1e-9) ≈ 3.1416e-6
      • 0.000001 (1e-6) ≈ 1e-6 < 3.1416e-6 (阈值) → ​​相对相等(成立)​
      • 注意:如果只用 abs_tol=1e-91e-6 > 1e-9,会被判为不相等。相对容差(≈ 3e-6)更合理。
    • ​情况三:小数值(绝对容差主导)​
      • a = 1e-10b = 2e-10 (|a-b|=1e-10)
      • 设 rel_tol=1e-6abs_tol=1e-12
      • max(|a|,|b|) = 2e-10
      • 动态相对容差 = 1e-6 * 2e-10 = 2e-16 (非常小!)
      • max(2e-16, 1e-12) = 1e-12 (因为 1e-12 远大于 2e-16)
      • 1e-10 (|a-b|) <= 1e-12? ​​1e-10 > 1e-12​​ → ​​不相等(成立)​
        • 解释: 虽然它们很小(都在 1e-10 量级),但ba的2倍!相对误差极大。绝对容差 1e-12 无法容忍 1e-10 这么大的差,所以正确判定不相等。动态相对容差 2e-16 在这里太小而没起作用,abs_tol 1e-12 提供了合适的判断依据。
    • ​情况四:非常接近零(绝对容差主导)​
      • a = 0b = 1.5e-12
      • 设 rel_tol=1e-6abs_tol=1e-12
      • max(|a|,|b|) = 1.5e-12
      • 动态相对容差 = 1e-6 * 1.5e-12 = 1.5e-18 (极其小!)
      • max(1.5e-18, 1e-12) = 1e-12
      • 1.5e-12 (|a-b|) <= 1e-12? ​​1.5e-12 > 1e-12​​ → ​​不相等(成立)​
      • 想让它被判定为接近零(相等):
        • 可以设置更大的 abs_tol,比如 abs_tol=1.6e-12
        • 1.5e-12 <= 1.6e-12 → ​​相对相等(成立)​
    • ​情况五:相对容差和绝对容差都参与(通常发生在中等或较小数值)​
      • 公式选取两者中较大的作为最终容差,确保在两种标准中满足其一即可视为相等。

在代码中使用:​

在 Python 中,推荐使用标准库 math.isclose

1
2
3
4
import math

if math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12):
print("a and b are considered close")
  • rel_tol:相对容差,通常 1e-9 是一个较高的精度要求,1e-6 或 1e-3 可能用于精度要求较低的领域。根据你的需求设定。
  • abs_tol:绝对容差下限,通常设置为一个非常小的数(如 0.0, 1e-121e-15),或者当你知道要处理接近零的数时,设置成一个合适的、比“显著差异”要小的值(例如,如果你的数据精度极限是 1e-10abs_tol=1e-12 可能就太小了,需要设成 1e-9 或更大,但这时最好通过相对容差来覆盖非零区域)。 abs_tol 最主要的作用是确保 a 和 b 都接近零时比较逻辑仍然工作。

​总结:​

相对相等 (abs(a - b) <= max(rel_tol * max(|a|, |b|), abs_tol)) 是一种更​​稳健(Robust)​​的浮点数近似相等判断方法。它通过结合​​相对容差​​(rel_tol)来解决大数值比较的问题,以及​​绝对容差下限​​(abs_tol)来解决非常接近零的数值比较的问题,从而在各种不同的数值量级上提供更合理、更一致的比较结果。在复杂应用中,优先考虑使用相对相等而不是简单的绝对容差相等。

练习题

CSAPP-3rd P93 2.84

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
填写下列程序的返回值,这个程序测试它的第一个参数是否小于或者等于第二个参数。假定函数f2u返回一个无符号32位数字,其位表示与它的浮点参数相同。你可以假设两个参数都不是NaN。两种0,+0和一0被认为是相等的。
int float_1e(float x, float y){
unsigned ux=f2u(x);
unsigned uy=f2u(y);
/*Get the sign bits*/
unsigned sx=ux >>31;
unsigned sy=uy >>31;
/*Give an expression using only ux,uy,sx,and sy*/
return;
}
如果使用判断就比较简单,下面是不使用判断.
第一个参数是否小于等于第二个参数,将可能满足的情况(即会返回1的情况)进行分类处理。
情况一:两个参数相等且为0, 根据IEEE规则,0用Denormalized表示,且有+0(0x0)和-0(0x80000000)两种表示 所以通过左移一位来比较
==>ux << 1 == 0 && uy << 1 == 0
情况二:第一个参数为负(此时ux>>31为sx=0x1得到!sx=0) ,第二个参数为0或者正数(此时uy>>31为sy=0x0 !sy=1)
==>(sx && !sy)
反之,若第一个参数为正或者0, 第二个参数为负,这种情况肯定返回0,就不需要特殊处理。
情况三:两个参数都为正
==>(!sx && !sy && ux <= uy)
情况四:两个参数都为负,此时根据IEEE的定义 正数可以用无符号整数的升序进行排列(正数越大 无符号数越大) 负数可以用无符号整数的升序进行排列(负数越小,无符号数越大)
==>(sx && sy && ux >= uy)
四种情况或运算,满足一种就返回1

讨论题

下列哪些浮点数是符合标准的?1.e01.2e0.2e01231e02e4.21.2.e5
​​2e4.2:不符合​。指数部分 (e4.2) 包含了小数点 .。指数必须是一个​​整数​​(可正可负,如 4-2+10)。
.e5:​​不符合​。尾数部分 .e5 缺少有效的数字。前面只有小数点 . 而没有跟随任何数字。必须在小数点前后至少有一方包含数字(如 .11.1.21e5)。

两道诡异的题目

1
2
3
4
5
6
7
8
int main()
{
char a = 100;
char b = 200;
char c = a + b;
printf("%d %d\n", c, a+b);
}
//输出结果为44 44

a和b同为8位有符号整型类数据。直接加和,a+b等于300。a和b都是char型,最后得出的结果也是char型:因为溢出了8位的最大范围(0~255),所以需要模256,最终300转换后的结果等效于44。

44是存储在内存中的数据,最终显示给人类的还是44,因为44没有超过127,也就不用涉及到补码来表示负数。(反之的情况,如果模后的结果是128,则表示人类所看到的负数-128;如果模后的结果是255,则表示-1)

既然a和b都是char型,最后得出的结果也是char型,所以"c"和"a+b"两者代表的意义是一样的,最终都是char型下的44。于是输出结果为44。

上面的题目只是乐呵一下,下面的题目才诡异莫测,如果对计算机的底层运算法则、流程不详,那么是无法领会的。

1
2
3
4
5
6
7
8
int main()
{
unsigned char a = 100;
unsigned char b = 200;
char c = a + b;
printf("%d %d\n", c, a+b);
}
//输出结果为44 300

我们默认我们在x86体系架构下的、字长为32位的环境下运行。根据微机原理x86的描述,我们的通用寄存器有eax/ebx/ecx/edx。其中低16位叫做ax/bx/cx/dx。再分,低16位中的高8位叫做ah/bh/ch/dh、低8位叫做al/bl/cl/dl

经过测试,在VS2019编译器下,反汇编代码得出:a+b这个语句的运算首先要把a和b的值分别存放到寄存器eax/ecx中。注意:eax和ecx都是32位寄存器,如果把a和b的值分别存放到寄存器eax/ecx中,意味着存放了原来的8位有效数据外,前面的24位都需要补位,而无符号整型数据补前位时用0补位。
存数据之后,对两数的加操作是:add eax,ecx即加操作是在寄存器内累加的。那么即使300超过了255,本应溢出的数据仍然能有效保存在寄存器eax中(即关键的第9位——“1”)。

接下来:

对于char c = a + b;,对c的赋值是通过eax赋值的,因为声明了c是有符号char型,赋值时存在隐形类型转换,即要进行隐式的切片操作,将切除前24位,留下后8位。所以:这里编译器只把低八位即AL赋给了c。因此,上述的eax寄存器中关键的第9位——"1"失效了,只保留了后八位,最终打印c的结果是44。

image-20210818214251517

对于a + b;,a和b在eax寄存器中直接加和的结果是300,即使超过了255,本应溢出的数据仍然能有效保存在寄存器eax中(即关键的第9位——“1”)。而我们格式化控制的输出是%d,即我们要拿4个字节即32位有符号整型来识别内存的数据,并在最后转为十进制数,所以打印出来是300。(其实如果拿%u来打印更合适,即32位无符号整型来识别内存的数据,并在最后转为十进制数,最后打印出来也是300)

习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
char c = 128;
unsigned char uc = 128;
unsigned short us = 0;
us = c + uc;
printf("%x \n",us);//0

us = (unsigned char)c+uc;
printf("%x \n",us);//16:100->10:256

us = c+(char)uc;
printf("%x \n",us);//2:1111 1111 1000 0000+1111 1111 1000 0000=>(1) 1111 1111 0000 0000 -> 16:ff00

//同us = c + uc;原理一样,都是相当于把c存放到16位ax寄存器中,自然需要补位,而c原本是有符号数,则补符号位"1"。
//强转为(unsigned short)就表示存放到16位ax寄存器中。其实我们不用人为地显式写出"(unsigned short)",因为c+uc肯定需要达到统一类型,自然要把char c隐式转为unsigned short,即无符号16位数据。
us = (unsigned short)c+uc;
printf("%x \n",us);//1111 1111 1000 0000+0000 0000 1000 0000=>(1) 0000 0000 0000 0000 =>0 -> 16:0

return 0;
}//0 100 ff00 0

image-20210818224646093

image-20210818224658042

image-20210818224711482

image-20210818224728892

image-20210818224743542

高超的技艺

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//最拉跨的
int Get1Bit(int x)
{
int sum = 0;
while(x)
{
if(x & 0x01)
{
sum += 1;
}
x = x >> 1;
}
return sum;
}
//面试宝典中
//老师不讲,学生永远不知道
int Get1Bit(int x)
{
int sum = 0;
while(x)
{
x = x &(x-1);
sum+=1;
}
return sum;
}
//查表方案
int Get1Bit(int x)
{
int ar[] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int sum = 0;
for(int i = 0;i<sizeof(x)*2;++i)
{
sum = sum + ar[x & 0x0f];
x = x >> 4;
}
return sum;
}

这个减1就与原数按位与,每次都会少个1。

image-20210823024356781

计算一个4字节整型的二进制格式中1的个数。

有一位图论的学生,面试时通过此题进入了腾讯。

image-20210823021837556

image-20210823021900711

image-20210823021915500

断位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Node
{
char a:4;
unsigned char b:3;

unsigned char c:5;
};
int main()
{
struct Node x={};
x.a = 4;
x.b = 2;
x.c = 5;
}

刷题

image-20210921200013444

1
2
3
4
5
6
7
8
9
10
11
int getDecimalValue(struct ListNode* head)
{
int res = 0;
struct ListNode* p = head;
while (p != NULL)
{
res = res << 1 | p->val;
p = p->next;
}
return res;
}

C语言_结构体

本章内容

  1. 结构体类型的设计
  2. 结构体变量初始化
  3. 结构体成员访问
  4. 结构体与数组

结构体类型的设计

C 语言提供了基本数据类型,如 char, short, int, float 等类型,我们称之为内置类型。
程序开发人员可以使用结构体来封装一些属性,设计出新的类型,在 C 语言中称为结构体类型。
在 C 语言中,结构体是一种数据类型。(由程序开发者自己设计的类型)
可以使用结构体(struct)来存放一组不同类型的数据。结构体的定义形式为:

1
2
3
4
struct 结构体名
{
成员列表(可以是基本数据类型,指针,数组或其它结构类型)
};

我们自己设计一个学生类型

客观事物(实体)是复杂的,要描述它必须从多方面进行,也就是用不同的数据类型来描述不同的方面。如学生实体可以这样来描述:

  1. 学生学号(用字符串描述)
  2. 学生姓名(用字符串描述)
  3. 性别(用字符串描述)
  4. 年龄(用整型数描述)。
    这里用了2种不同数据类型,以及四个数据成员(data member)来描述学生实体。
    (数据成员,也可称之为属性,不能称之为函数中的变量概念)

image-20210815101308997

结构体变量的定义和初始化

既然结构体是一种数据类型,那么就可以用它来定义变量。结构体就像一个“模板”,定义出来的变量都具有相同的性质。
也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
结构体是一种数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要存储空间

结构体变量在内存中表示

思考下述代码结构体在内存中的分配是A情况还是B情况?

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Student
{
char s_id[8];
char s_name[8];
char s_sex[4];
int s_age;
};
int main()
{
int a = 10,b = 20;
struct Student s1 = {"09001","yhping","man",23};
return 0;
}

image-20210816155806100

答案是A,即结构体成员的内存分布顺序是从上到下依次排列。为什么不是像a,b那样的顺序?

我自己的解释:结构体的类型结构声明不像函数中变量的声明定义。我们要区分类型内部的成员和函数内部的变量,两者是截然不同的!结构体类型的抽象是一种类型,是由若干其他类型组成的一种新类型,那么类型内部的成员必然要按照我们在定义时的顺序从上到下分布内存空间,才符合程序设计的逻辑思路。按照如此规则如此分布,才能方便我们进行后续的给结构体变量初始化赋值,大括号内的值的顺序是按照成员顺序来的,而不是随意颠倒顺序,编译器是不会同意的。

示例

image-20210815102046756
如果把char数组改为指针:
image-20210815104639636

结构变量初始化

image-20210815102137170

大括号内的值的顺序是按照成员顺序来的,而不是随意颠倒顺序,编译器是不会同意的。

image-20210816162135598

如果把char数组改为指针:

这时按照上图的赋值方式,在VS2019中是不能通过的,因为,"09001"这种双引号引起来的字符串的类型是常量字符串型即const char*(要给s_id赋的值本质是字符串首字符'0'的指针,此指针只能读数据不能改数据,因此类型是const char*),与我们在结构体中声明的char*不匹配。

结构体嵌套结构体

image-20210815104658804

思考以下结构是否可行?

1
2
3
4
5
6
7
struct Student
{
char s_name[8];
int s_age;
float score;
struct Student studx;
}; //sizeof(Student)是计算不出来的,因为如此定义会导致无穷的递归下去。不能被sizeof计算的类型因此也叫做不完整的类型。

image-20210816181150361

结构体链接结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Student
{
char s_name[8];
int s_age;
float score;
struct Student* next;//如此定义才可行。
}; //sizeof(Student)==16
int main()
{
struct Node a,b,c;
struct Node* head = &a;
a.data = 10;
a.next = &b;
b.data = 20;
b.next = &c;
c.data = 30;
c.next = NULL;

return 0;
}

image-20210816181426848
如何使用循环打印?

1
2
3
4
5
6
7
8
9
void Print_List(const struct Node* head)
{
const struct Node* p = head;
while(p!=nullptr)
{
printf("%d ",p->data);
p = p->next;
}
}

结构体成员的访问

结构体变量的成员使用.访问。
获取和赋值结构体变量成员的一般格式为:结构体变量.成员名

结构体变量成员的访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>
#include<string.h>
struct Date
{
int year;
int month;
int day;
};
struct Student stud1
{
char s_name[20]; //姓名
struct Date birthday; //生日
float score; //成绩
};
int main()
{
struct Student stud1={"Yhp",2007,10,1,145.5};
struct Student stud2={"Liuwuyang",{2007,2,2},135.0};

int y = stud1.birthday.year;//用.访问结构体变量的成员
struct Student* sp = &stud1;
sp->s_name;//用->访问结构体变量指针对应的结构体变量的成员
(*sp).s_name;//用.访问结构体变量的成员
sp->birthday.year;//前sp是指针,后birthday是结构体,所以前用->后用.

return 0;
}

结构体变量(的成员)的赋值

对结构变量整体赋值有三种情况:

  1. 定义结构体变量(用{ }花括号初始化);
  2. 用已定义的结构变量初始化;
  3. 结构体类型相同的变量可以作为整体相互赋值

在其他情况的使用过程中只能对成员逐一赋值。
在 C 语言中不存在对结构体类型的强制转换(和内置类型的区别)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Student
{
char s_id[8];
char s_name[8];
char s_sex[4];
int s_age;
}Student;
int main
{
Student stda={"09001","xcg","M",23};
Student stdb=stda;//调用了memcpy(&stdb,&stda,sizeof(stda));
Student stdc;
Student stdx;
stdc = stda;
stdx.s_name = stda.s_name;//此语句是错误的。
strcpy_s(stdx.s_name,stda.s_name);//正确的做法!
}

关于两结构体变量整体赋值如何实现

首先,抓住两个结构体变量各自的地址,再依次同步迭代拷贝,调用的函数是memcpy();memcpy(&stdb,&stda,sizeof(stda));

关于上面提到的错误赋值

stdx.s_name = stda.s_name;此语句是错误的。`

为什么?

s_name是一个数组,数组是不可能给另一个数组直接赋值的。因为:s_name代表首元素指针,根据我们数组那一节的知识储备,这个指针是常量(即数组名所代表的指针),stdx.s_name = stda.s_name这个语句的意思是把stda.s_name这个指针赋给stdx.s_name这个指针,这显然是不可行的,数组首元素指针不可能改变!

同理,数组名不可以++,如已知int ar[100]={};ar数组,ar++这个语句是错误的。

应该对数组进行全部迭代拷贝,如调用strcpy_s(stdx.s_name,stda.s_name);

与数组{}花括号赋值的异同

不同点是:数组的花括号内的值类型必须一致,而结构体花括号内的值可能不一致。

共同点:如果{}花括号内的内容缺省,默认赋值为0。

结构体变量和函数

拿打印函数举例

image-20210816183332238

二个打印函数那个好 ? 原因是什么?

image-20210816183348520

二个打印函数那个好 ? 原因是什么?和指针比较的优势? 限制条件是什么?

肯定是Print_c好。优势在于如果不是用指针来传值,那么还要再次开辟空间且给形参复制源变量的信息,导致空间和时间的效率都大大降低。而用指针来传值,直接能操作源变量。而用const来读取信息更为谨慎,因为const能保证该指针只能读取变量信息而不能改变变量信息。

结构体的大小

1
2
3
4
5
6
7
8
9
10
11
struct Node_a
{
char ca;
int sum;
char cb;
}
int main()
{
struct Node_a ax;
printf("%d %d \n",sizeof(struct Node_a),sizeof(ax));//12 12
}

为什么要理解字节对齐问题

  1. 内存大小的基本单位是字节,理论上来讲,可以从任意地址访问变量,但是实际上,cpu并非逐字节读写内存,而是以 2, 4 或 8 的倍数的字节块来读写内存,因此就会对基本数据类型的地址作出一些限制,即它的地址必须是 2,4 或 8 的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。
  2. 有些平台每次读都是从偶地址开始,如果一个 int 型(假设为 32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这 32 bit,而如果存放在奇地址开始的地方,就需要 2 个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该 32 bit 数据。显然在读取效率上下降很多。
  3. 由于不同平台对齐方式可能不同,如此一来,同样的结构在不同的平台其大小可能不同,在无意识的情况下,互相发送的数据可能出现错乱,甚至引发严重的问题

计算规则

由于存储变量地址对齐的问题,计算结构体大小的 3 条规则:

  1. 结构体变量的首地址,必须是结构体变量中的“最大基本数据类型成员所占字节数”的整数倍。

  2. 结构体变量中,相对于结构体首地址每个成员偏移量,都是成员本身基本数据类型所占字节数的整数倍。

    1
    2
    3
    4
    5
    6
    struct Node
    {
    char ca;//偏移地址为0,占1字节。偏移地址1-7起地址对齐占位作用,内容无实际意义。
    double dx;//偏移地址为8,占8字节。因为要满足原则2即double的偏移量要相对于结构体首地址(视为0),是成员本身基本数据类型所占字节数的整数倍。只有在偏移地址8时,才满足(8-0)/8==1。
    char cb;
    };//24
  3. 结构体变量的总大小,为结构体变量中 “最大基本数据类型成员所占字节数”的整数倍。
    image-20210817015222060

实例/测验

1
2
3
4
5
6
struct node
{
char cha;
double da;
char chb;
};//24,因为da是从0x08开始的。cha和chb都占了8个
1
2
3
4
5
6
7
8
9
10
11
12
13
struct sdate
{
int year;
int ia;
int day;
};
struct Student
{
char s_id[10];//1-10
char s_name[8];//11-18
struct sdate birthday;//21-32
double grade;//33-40
};//40
1
2
3
4
5
6
7
8
struct Inventory
{
char description[15];//货物名称
char no[10];//货号
int quantity;//库存数量
double cost;//成本
double retail;//零售价格
};//48
1
2
3
4
5
6
7
8
struct Employee
{
char name[27];//1-27
char address[30];//28-57
long int zip;//61-64
long int telenum;//65-68
double salary;//72-80
};//80

如何巧妙计算偏移量

Employee类型的结构体,成员有name, address, zip, telenum, salary等,现要求:不要定义任何结构体变量计算zip相对结构体自身首地址的偏移量。
利用+无中生有法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define my_offset(type,exp) ( (int) & (( (type*)0 )->exp ))
struct Employee
{
char name[27];//1-27
char address[30];//28-57
long int zip;//61-64
long int telenum;//65-68
double salary;//72-80
};//80
int main()
{
int offset = 0;
//struct Employee x;
//offset = (char*)&x.salary - (char*)&x; //72-0==72
offset = (int) & ( (struct Employee*)0 )->salary;//无中生有 0x00->0x72
offset = my_offer(struct Employee,zip);//利用宏定义
printf("%d \n",offset);
}

#pragma pack指定对齐值

预处理指令#pragma pack(n)可以改变默认对齐数。n取值是 1, 2, 4, 8, 16
VS 中默认值 = 8,gcc 中默认值 = 4

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(1)
struct node
{
char cha;
double dx;
char chb;
};
//若( )内为1->size:10;
// 为2-> 12;
// 为4-> 16;
// 为8-> 24;
// 为16-> 24;
#pragma pack

终极总结

  1. 结构体变量的首地址,必须是MIN{"结构体 最大基本数据类型成员 所占字节数", 指定对齐方式}的整数倍。
  2. 结构体中,相对于结构体首地址,每个成员的偏移量,都是MIN{该基本数据类型成员, 指定对齐方式}的整数倍。
  3. 结构体的总大小,为MIN{结构体最大基本数据类型成员所占字节数, 指定对齐方式}的整数倍。

比较结构体变量

不要轻易地使用memcmp函数来对比两个结构体变量。因为结构体内存结构层面中,成员间的空隙填充的内容是不可控的,即结构体是一种非连续型内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Node
{
char cha;
int ix;
char chb;
};
//当调用主函数时,将对分配得到的栈帧的每个字节进行刷新,全部赋为'0xcc'
int main()
{
int ar[10]={};
int br[10]={};
int x = memcmp(ar,br,sizeof(ar));
printf("%d\n",x);

struct Node x = {'a',12,'b'};
struct Node y = {'a',12,'b'};
int tag1 = memcmp(&x,&y,sizeof(x));//不要轻易地使用memcmp比较结构体。因为结构体是一种非连续型内存空间。
struct Node z = {};//{}代表:把12个字节全赋成了0
z.cha = 'a';
z.chb = 'b';
z.ix = 12;
int tag2 = memcmp(&x,&z,sizeof(x));//返回值0代表相等,1代表大于,-1代表小于
printf("%d %d\n",tag1,tag2);//0 1
return 0;
}

image-20210817153745134

结构体与数组

所谓结构体数组,是指数组中的每个元素都是一个结构体类型。在实际应用中,C 语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个公司的员工等。

联合体

联合体(union)与结构体(struct)有一些相似之处。但两者有本质上的不同。在结构体中,各成员有各自的内存空间。而在联合体中,各成员共享同一段内存空间, 一个联合体变量的长度等于成员中最长的长度
应该说明的是, 这里所谓的共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值。
一个联合体类型必须经过定义之后, 才能使用它,才能把一个变量声明定义为该联合体类型。
联合体不仅可以节省内存空间,最本质、重要的用法是对同一段空间采取不同的类型格式去识别、读取数据。

image-20210817164050152

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union Node
{
short sx;
char cx[2];
};
int main()
{
union Node x;
x.sx = 0x6162;//0x62,0x61
printf("%c %c \n",x.cx[0],x.cx[1]);//b a 而非a b

x.cx[0]=1;
x.cx[1]=2;
printf("%d \n",x.sx); //0x01,0x02=>0x0201
return 0;
}

image-20210817162154866

声明和定义时的注意

设计有名的联合体,同时没有定义变量。

1
2
3
4
5
6
union UnData
{
short st;
char cs[2];
};
union UnData x;

设计有名的联合体,同时定义变量。与上述代码等效,节省了一行代码。

1
2
3
4
5
union UnData
{
short st;
char cs[2];
}x;

设计无名的联合体,同时定义变量。这样是可行的。

1
2
3
4
5
union
{
short st;
char cs[2];
}x;

但要注意的是,如下做法编译器是不认为x,y属于同一种类型的联合体的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
union
{
short st;
char cs[2];
}x;
union
{
short st;
char cs[2];
}y;
int main()
{
x = y;//不可编译通过。编译器不认为x,y属于同一种类型的联合体。
}

当然,我们可以用typedef关键字把无名的联合体定义出的变量名赋予其类型的性质。

1
2
3
4
5
typedef union
{
short sx;
char cx[2];
}x;

重要的面试笔试题目

IP地址本质上是一串32位二进制代码(对于ipv4是32位,ipv6是128位),可看作无符号int数。题目要求把32位二进制代码的每八位转换为一个无符号十进制数,并用“点”隔开,最终转为字符串。同时也要求把该格式的字符串能逆转换为32位二进制代码构成的无符号int数。

image-20210817114120334

要运用到的输入/输出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
char buff[20];
int a = 10,b = 20;

printf();//打印到屏幕上
int len = printf("a = %d b = %d \n",a,b);//将格式化字符串写入到标准输出设备中

sprintf(buff,"%d.%d.%d.%d",x.s4,x.s3,x.s2,x.s1);//将格式化字符串写入到buff中
int sprintf(char* buff,const char* fmt, ...);//返回的值:格式化字符串探测到'\0'时,返回有效字符的长度
int len = sprintf(buff,"a = %d b = %d \n",a,b);

fprintf(stdout,"a = %d b = %d \n",a,b);//将格式化字符串写入到文件指针对应的位置。如果该文件是标准输出设备则是打印到屏幕上。

//由此可知,printf()实际上内部调用了fprintf(),文件指针为stdout;而fprintf()实际上内部调用了sprintf(),buff指针指向文件指针。即sprintf才是根本所在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scanf();//从标准输入设备stdin中输入的数据读取值。

sscanf(char* buff,"%...", &...);//从buff(字符串)中读值。按格式化控制将%...对应的值写到&...中


int main()
{
int a,b;
int sum = scanf("%d %d",&a,&b);//scanf返回正确读取到的值的个数,此处若正确输入则返回2。若输入"12,23"则只能正确读取一个值
//如果输入时两数中间分开的不是空格,则无法正常输入。因此此处就体现了scanf的“格式控制”,那么同样的,我们若想要用户用.来隔开数据,则可以通过限制格式来达到控制效果。

char buff[]={"12,23,34,45"};
unsigned int s[4];
//sum = sscanf(buff,"%d.%d.%d.%d",&s[0],&s[1],&s[2],&s[3]);
//为了能检测到用户多输入了字符,则我们在格式化控制中多加一个哨兵位检测是否多输入了值。
char ch;
sum = sscanf(buff,"%d.%d.%d.%d%c",&s[0],&s[1],&s[2],&s[3],&ch);
//一旦用户多输入了字符如'.',则sum将大于4,则可以条件判断意外情况。
printf("%d \n",num);//4为正常,5为多输入了字符-意外情况
}

代码编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
union IPNode
{
unsigned int addr;
struct //没有名字,称为哑元结构(dummy),对应于实元。
{
unsigned char s1,s2,s3,s4;//或者声明一个数组也可以。unsigned char s[4];
};
//unsigned char s1,s2,s3,s4;不能简单地写成这样,因为这样写的结果是:四个变量都只共享第一个字节。
};
void int_to_str(unsigned int ip,char* buff)
{
assert(buff!=nullptr);
union IPNode x;
x.addr = ip;
sprintf_s(buff,20,"%d.%d.%d.%d",x.s4,x.s3,x.s2,x.s1);
}
int main()
{
unsigned int ip = 2394117684;
char buff[20]={};
int_to_str(ip,buff);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unsigned int str_to_int(const char* buff)
{
unsigned int ip=0;
if(buff==NULL)return ip;

union IPNode x;

unsigned int s[4]={};
char ch=0;
int sum=sscanf_s(buff,"%d.%d.%d.%d%c",&s[3],&s[2],&s[1],&s[0],&ch);
if(num>4)return ip;
for(int i=3;i>=0;--i)
{
if(s[i]>255)return ip;
x.s[i]=s[i];
}
ip=x.addr;
return ip;
}
int main()
{
unsigned int ip = 2394117684;
char buff[20];
printf("%u \n",ip);

int_to_str(ip,buff);
printf("%s \n",buff);

unsigned ipx = str_to_int(buff);
printf("%u \n",ipx);
}

作业

  1. 给结构体变量赋值和输出结构体变量的值。想尽办法做初始化。

    image-20210815114633078