请大家以回帖的方式将你在共读第一周的心得体会用你自己的话表达出来。
样例:
所在小组
第一组
组内昵称
张三
你的心得体会
可以基于不同知识点进行,有更新请在原回贴更新,每人每周只发一个帖子
一段自己的阐述
第二段自己的阐述
…
请大家以回帖的方式将你在共读第一周的心得体会用你自己的话表达出来。
样例:
第一组
张三
可以基于不同知识点进行,有更新请在原回贴更新,每人每周只发一个帖子
…
第七组
杨文
第3组
h0n9xu
本周任务是完成第二章;其中2.1信息存储本应该上周完成,其他内容包括2.2整数表达形式,2.3整数运算,2.4浮点数。这章蛮难的,以后还要多看。
debug_assert
)检查integer-overflow,Go不检查。文中讲了两个integer-overflow带来的安全问题(FreeBSD-SA-02:38.signed-error和CA-2002-25),CVE中由它导致的问题还蛮多的。第一组
sadame
1.逻辑运算与移位运算的区别
2.C语言中int在32/64位程序中都是32位,区别只是long的长度
3.java只支持有符号整数,并且区分逻辑右移和算术右移。因为C中的有符号到无符号的隐式转换会导致错误
4补码的除法中可以利用偏置量来决定答案是向上舍入还是向下舍入
5.阶码在32位数中为8位,在64位数中为11位,阶码中偏置值在32位中是127,64位数中是1023,
E=(e_ke_k-1…e_0)阶码表示的无符号数-(2^k-1)偏移量
只能表示有理数V=符号位×2^E×M
E和M的值在E为全0时会发生变化 ,E全1时为无穷或NaN
1.所在小组:第七组
2.组内昵称:吴奇驴
3.心得体会
1.所在小组:第六组
2.组内昵称:黄永平
3.心得体会:
整数运算
采用补码表示有符号数的优点,除了零值是唯一的,更重要的是,有符号数与无符号数的算术执行可以采用相同的位级实现。也就是说,位运算时可以不必区分有符号数或无符号数,执行相同的位级操作。只是运算操作之后,对同样的位模式分别采用不同的位级解析成有符号或无符号数值。有符号数的表示方式,除了补码外,还有反码、原码。但最终都选择补码是有原因的。
浮点数
浮点数的表示,分别由符号S、尾数M、阶码E三部分组成,其中符号S是首个bit,通过该bit标示有符号位,也就是说浮动数是采用原码表示。尾数M是二进制小数,随阶码E的取值,决定是否隐式包含1。而阶码E的取值,将浮点数分为三类:规格化、非规格化、无穷大或NaN。浮点数是经过精细设计的,从非规格化值能够平滑过渡到规格化值。
所在小组:第三组
组内昵称:hy
心得体会
如何使用二进制 0 和 1 表示整数?
从低位到高位,每一位表示 2^x
是否取值。
无符号(unsigned)整数都是正数,其表示比较简单,如果有 w 个位,每一位代表 2^x
是否取值,可以表示 0 ~ 2^w - 1
范围。
负数如何表示呢?
最常见的是补码形式,将字的最高有效为定义为负权(negative weight)。
对于 w 位的整数,最高位称为符号位,它的权重为 -2^(w-1)
,当符号位为 1 时,表示值为负(剩下的加起来也没这个大),例如:
如果 w 等于 4,此时:
[1011] = -1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 = -8 + 0 + 2 + 1 = -5
表示 -5。
最小值为:[10…0] -2^(w-1)
也就是 -8
最大值为:[01…1] 等于 w-1
的无符号表示,等于 4 + 2 + 1 = 7
。
根据上面的例子 -8 + 7 = -1
因此,-1 的二进制表示全是 1,对应 16 进制:0xFFFFFF
。
因为正数还包括一个 0,而 0 是非负数。
数字按照绝对值转换成二进制,如果是负数在最高位补 1。
例如:
5 的原码是 00000000 00000000 00000000 00000101
-5 的原码是 10000000 00000000 00000000 00000101
也就是用最高位表示符号:0 为正,1 为负。
如实使用 8 个 bit 表示无符号整数,就是 0 ~ 255
;如果是有符号,最高位表示符号,剩下的 7 个 bit 表示数字,就是 -127 ~ +127
。
但是原码有个弱点,有两个 0,对应的 10000000
和 00000000
表示都是 0,即 +0、-0,非常诡异。
进行异号相加或同号相减时,比较笨蛋,先要判断 2 个数的绝对值大小,然后进行加减操作,最后运算结果的符号还要与大的符号相同;
于是,反码产生了。
正数的反码:与原码相同;
负数的反码:除符号位以外全部取反。
-5 对应 10000101
的反码为 11111010
。
还是有 +0、-0
的问题,反码作为过渡产物,补码出现了。
我认为就是为了方便做加法,5 - 5 因为有了反码,直接就在二进制下得到结果 0000 0000
补码表示法:
正数的补码与原码相同;
负数的补码为原码的反码然后加 1。
例如 -5:
原码:10000101
反码:11111010
加 1 得到最终的编码:11111011
为什么要加 1?
这样就没有 +0、-0
的问题,因为:
+0
(正数): 补码与原码相同,就是 00000000
。
-0
(负数): 原码:10000000
,反码:11111111
,加 1 得到补码 00000000
和上面的 0 表示一样,统一了。
鉴于不需要两个 0,对于 -0
会特殊处理。
如果在 -0
取反码时会连符号位一起取反得到 01111111
补码得到 10000000
(与原码一样) 表示 -128
(最高位的 1 即表示符号位又表示数字位)。
这样两个 0 就只剩一个了,而且还多了一个坑位表示 -128
。
运算时,减法可以转加法,将被减的数取负数即可,转为加负数。
5 - 5 = 5 + (-5) = 00000000
-1 在计算机中如何表示:
原码:10000001
反码:11111110
补码(负数 +1):11111111
全是 1,对应 16 进制:0xFFFFFF
。
在补码表示下,负数会多一个,就是从 -0(10000000)
抠过来的一个位置,表示最小的负数。
负数最大值是特殊情况,转换成原码时最高位的 1 有两层含义,符号位和数值位。
对其取反码会将符号位也一起取反,得到反码 01111111
,加一得到补码 10000000
。
参考资料:
10000000 + 00000001 = 10000001
而 -127
就是 10000001
,对于补码的加法,直接二进制加就行了。
-127
的原码表示 11111111
反码 10000000
补码 10000001
。
无论是无符号数还是有符号数,一旦用来表示数值的最高位发生了进位,超出了表达形式或者改变了符号位,就会发生溢出。
对于无符号数加法,如果两个 w 位的数字相加,结果是 w+1 位的话,那么就会丢弃掉最高位,实际上是做了一个 mod 操作。
假设 w=3,那么能够表达的数字范围是 000~111(0~7)(括号内为二进制对应的十进制数值,后同),那么如果一个表达式是 110+111(6+7),原本应该等于 1101(13),但是由于 w=3,所以最终的结果是 101(5),也就是发生了溢出,两个无符号数相加,有可能反而变『小』。
对于有符号的加法(Two’s Complement Addition),操作过程和无符号加法一样,只是解释的时候会有不同,因此会得到正溢出(positive overflow)和负溢出(negative overflow)两种。正溢出就是数值太大把原来为 0 的符号位修改成了 1,反而成了负数;负溢出是数值太小,把原来为 1 的符号位修改成了 0,反而成了正数。
还是用刚才 w=3 作为例子,能够表达的数字范围是 100~011(-4~3)
,如果一个表达式是 011+010(3+2),理论上应该等于 5,但是相加之后变成了 101(-3)
,也就是发生了正溢出。如果一个表达式是 100+101(-4+(-3))
,理论上应该等于 -7,但是相加后进位截取变成了 001(1)
,也就是发生了负溢出。
对于乘法来说,值的范围会大很多,这里分情况讨论一下,假设两个乘数是 x,y 并且都是 w 位的:
无符号数:至多 2w 位
有符号数,最小的负数:至多 2w - 1 位
有符号数,最大的正数:至多 2w 位
如果需要保证精度,就需要用软件来实现了。
浮点数是对 V = x * 2^y
形式的有理数进行编码,计算机不像数学有各种符号可以表示任意的实数,只能表示有限精度的范围和精度,因此浮点数只能近似的表示实数运算。
浮点数的二进制表示法等于整数位 + 小数位,小数位的权重是 2^-1、2^-2...
依次相加,例如:
5.75 = 101.11
也就是整数位 5
+ 小数位 1/2 + 1/4
。
这种方法的问题在于,只能表示能够被写成 x * 2^y
的数,其他值只能近似地表示,比特位越多精度越高。
例如数字 0.2 能用十进制准备的表示,但确不能准备地表示为二进制,只能通过增加二进制的长度来提高表示的精度,试试如何通过增加小数位的长度接近 0.2:
| 二进制 | 值 | 十进制 |
| — | — | — |
| 0.0 | 0/2 | 0.0 |
| 0.01 | 1/4 | 0.25 |
| 0.010 | 2/8 | 0.250 |
| 0.0011 | 3/16 | 0.1875 |
| 0.00110 | 6/32 | 0.1875 |
| 0.001101 | 13/64 | 0.203125 |
| 0.0011010 | 26/128 | 0.203125 |
| 0.00110011 | 51/256 | 0.19921875 |
有 4 个小数位,浮点数就有 4 位小数的精度
1/16 = 0.0625
。
这种方法表示不了 1,只能无限接近 0.111111…
而且,二进制表示法不能有效地表示很大的数字,例如 5 * 2^100
使用 101.100个0
的位模式来表示,0 的数量太多了,如果能通过给定 x 和 y 的值来表示 x * 2^y
的数,就能提升到更高的精度。
因此,实际采用的是 IEEE 754 规范。
IEEE(eye-triple-ee)是电气和电子工程师协会的缩写,IEEE 标准 754 是 1985 年发布的浮点数国际标准,定义了浮点数的算术格式、交换格式、舍入规则、操作和异常处理。
IEEE 浮点标准使用 V = (-1)^s * M * 2^E
的形式来表示一个数:
s 符号 sign:表示正负;
M 尾数 significand:是一个二进制小数,取值范围大于 1 无限逼近 2;
E 阶码 exponent:对浮点数加权,权重是 2 的 E 次幂。
32 位的浮点数编码形式为:
0|01111100|01000000000000000000000
s exp frac(小数域)
计算 E 的值:
E = Exp - Bias
Exp:是 exp 编码区域的无符号整数值。
Bias 值为 2^(k-1) - 1
,k 是 E 编码的位数,之所以需要 Bias 这个偏移量,是为了保证 E 编码只需要以无符号数来处理:
单精度:127(Exp:1…254 E:-126…127)
双精度:1023(Exp:1…2046 E:-1022…1023)
计算 M 的值:
frac 编码尾数(0 <= f < 1),采用二进制表示,此时 M = 1 + f
是一个永远大于 1 的数,能增加额外的精度(更接近不能代表的值)。
如果 Exp 全是 0,即阶码域都为 0 时,表示的数是非规格化的值,这样可以表示 +0 和 -0。
当阶码域都为 1 时,能表示:
正无穷 s = 0
负无穷 s = 1
还要一个特殊值 NaN(Not a Number),表示一些运算的结果不是实数或无穷。
对于不能精确表示的数 x,只能尽量接近,找到最接近的匹配值(能用二进制浮点数表示)来代表 x,而这个匹配值肯定有两个,一个比 x 大一点,一个比 x 小一点,我们选择其中一个的过程就是舍入。
问题是,在两个值之间,我们如何确定舍入方向?
目前有 4 种舍入的方法:
向偶数舍入(Rount to Even)(默认的方式):将数字向上或向下舍入,保证结果的最低有效数字是偶数(也就是 0);
向零舍入:往 0 靠近的舍入方式,在两个数中选择接近 0 的那一个;
向下舍入:选择更小的值;
向上舍入:选择更大的值。
其他的比较简单,我们着重讨论向偶数舍入:
也被称为向最接近的值舍入(round to nearest),会寻找这两个数中最接近的一个,例如 1.40 接近 1 而 1.60 接近 2,唯一的特殊情况是 1.50,接近的距离是相等的,此时也是采用向偶数舍入的时机,
1.5、2.5 最近的偶数是 2 所以都舍入到 2。
向偶数舍入这个名字有歧义,其实应该是先向最接近的值舍入,如果接近的值一样,再向偶数舍入,是一个处理分歧的规则。
向偶数舍入的好处是:如果数字很多,得到的平均数更均匀,因为一半是向上一半是向下舍入的。
0.1 + 0.2 = 0.30000000000000004
?因为不能精确表示 0.1 和 0.2。
我们无法使用有限的二进制位数准确地表示十进制中的 0.1 和 0.2,这就造成了精度的损失,这些精度损失不断累加在最后就可能累积成较大的错误。
但是 0.25 和 0.5 两个十进制的小数都可以用二进制的浮点数准确表示,所以使用浮点数计算 0.25 + 0.5
的结果也一定是准确的。
为了解决浮点数的精度问题,一些编程语言引入了十进制的小数 Decimal
。
Decimal 在不同社区中都十分常见,如果编程语言没有原生支持 Decimal,我们在开源社区也一定能够找到使用特定语言实现的 Decimal 库。Java 通过 BigDecimal 提供了无限精度的小数,该类中包含三个关键的成员变量:
表示 1234.56
时:
intVal
中存储的是去掉小数点后的全部数字,即 123456;
scale
中存储的是小数的位数,即 2;
precision
中存储的是全部的有效位数,小数点前 4 位,小数点后 2 位,即 6;
intVal * 10^-scale
BigDecimal 这种使用多个整数的方法避开了二进制无法准确表示部分十进制小数的问题,因为 BigInteger 可以使用数组表示任意长度的整数,所以如果机器的内存资源是无限的,BigDecimal 在理论上也可以表示无限精度的小数。
虽然部分编程语言实现了理论上无限精度的 BigDecimal,但是在实际应用中我们大多不需要无限的精度保证,C# 等编程语言通过 16 字节的 Decimal 提供的 28 ~ 29 位的精度,而在金融系统中使用 16 字节的 Decimal 一般就可以保证数据计算的准确性了。
任何需要用到浮点数的地方,使用 Decimal 就错不了。
第一组
nigel
源码与补码的转换:
1)0和正数的补码与源码相同
2)负数,补码的符号位不变,其余位取反,最后+1
计算:50000*50000=2500000000
二进制补码计算结果:1001 0101 0000 0010 11111 0010 0000 0000
正数加符号位后:0|1001 0101 0000 0010 11111 0010 0000 0000(33位了)
超出32位后,去掉最高位0:1 0010 1010 0000 0101 1111 0010 0000 0000
符号位不变,其余取反后+1获得源码: 1101 0101 1111 1010 0000 1101 1111 1111+1=1101 0101 1111 1010 0000 0111 0000 0000
结果转化为10进制:-1794967296
提示:一般遇到这种情况,可以把变量定义成长整型或者浮点型。
浮点数:
1.同样占4个byte,为什么50000*50000=2500000000.000000,转化为浮点数,就能正确计算呢?
答:因为浮点数的存储形式与整数不同,取值范围比整数要大,包含了上面的结果值。
2.如何计算浮点数的取值范围呢?
根据IEEE浮点数的表示规则,可以表示为公式:
[image:B12E24E6-6A12-4B99-B341-52D60B65999D-56964-000C017D4A44836F/42E0B43A-B97E-4DD4-8E24-75DDF15AE515.png]
S:符号位,0正,1负
exp:阶码位,以移码形式存储,位数决定取值范围,用e表示,其阶码值为E
frac:尾数位,以原码形式存储,小数部分,位数决定小数的精度,用M表示
图中例子,
尾数位:1001.10012^ 0,存入计算机需要规格化后,=1.00110012^ 3
那么浮点数是如何转换用计算机表示的呢?看图讲解:
图1:
[image:987B155D-3D8A-48B3-83FD-56702276AE5A-56964-000C023D5E648967/155DE558-B1FC-4E0C-B5FF-54B68A5DEA1E.png]
[image:64839006-6B23-49E8-8151-96649BC9E917-56964-000C022CF1A83A53/2C1D7D75-3C5A-4BFC-8FB6-AFB723FB4921.png]
计算机中,浮点数表示共分三类,以单精度浮点数为例:
1.规格化:当阶码位在1和254之间时,阶码位不是0,也不是255
2.非规格化:阶码位全为0
3.无穷大:阶码位全为1,尾数位全为0
提示:阶码位全为1,尾数位不为0,认为不是一个数,c语言用NaN表示
图1,显示了计算Bias(偏置值)的计算规则,k表示阶码位的位数,单精度的阶码位是8位,所以Bias=2^ 7-1=127,又因为阶码位是移码的形式存储的,所以要得到阶码值,就必须要经过转换,而Bias就是转换的关键,所以这里要先求Bias
图2,求出Bias后,就得到图2下半部分的转换过程。
根据以上,得出,规格化的最大表示形式为:
S=0,e=1111110,M=1111111111111111111111
E=e-127=254-127=127
带入公式后:
[image:25154C8E-E056-4AA7-AC72-7FD9DE89944C-56964-000C030EC9C86C39/1EE1C3DA-6EB0-4046-859F-90EFC8E7C2F2.png]
所以,最大取值范围是: -3.410^ 38~3.410^ 38
这个范围,包含了50000*50000,所以,在转换为浮点数后,可以得到正确的结果。
3.那么计算机如何存储这个结果呢?
[image:9A1FF819-13F7-401A-AA07-91D2289956FB-56964-000C0351C82AD0A5/44804D18-4D26-4938-965C-CEB6F6E4B0BF.png]
第一步将结果转换成二进制并进行规格化处理,将小数点移到高两位之间得到
1.0010101000000101111100100000000*10^ 31(31就是阶码值E)
E = e - Bais 得到 e = E + Bais
S=0, e = (31+127)10进制= (10011110)2进制 ,M=001010100000001011111001
最后结果: 0 1001110 00101010000001011111001
可以打印其汇编值进行验证。
第五组
王传义
汇编指令回顾,下面都是具体例子,先了解,在验证(目前没有验证)
静默组
清风环佩
静默组
Han
无符号数的位表示就是二进制的值,很好理解。有符号数位表示也是二进制表示,只是最高位取负值,其它为取正值。
下面的程序打印出符号数的位表示。
#include <stdio.h>
void print_bits(char c) {
int l = sizeof(c) * 8;
printf("%d = ", c);
for(int i = l-1; i >= 0; i--) {
printf("%d", (c >> i) & 0x01);
}
printf("\n");
}
int main() {
char c = -1;
print_bits(c);
c = -2;
print_bits(c);
c = -128;
print_bits(c);
c = 127;
print_bits(c);
return 0;
}
Output:
-1 = 11111111
-2 = 11111110
-128 = 10000000
127 = 01111111
可以看到,-1
的位表示为 11111111
。
-1 = (10000000=-2^7) + (01111111=2^7-1)
有符号数和无符号数之间转换时,不同字长的整数之间转换,实际工作中常见遇到的情况是隐式的强制转化导致的bug,很难发现。
对于需要考虑跨平台的代码,最好使用明确了大小的数据类型来声明,如uint32_t,int16_t
等,而不是使用int,short
,代码的可读性更高,避免了产生不明显的错误的可能性。
整数运算中越界其实还挺容易遇到的,尤其是在使用unsigned char
或者short
时,发生之后,问题产生的现象会很奇怪,很难定位。尤其是随着项目的持续维护,实现之初,即使是有经验的开发,都会有预设这样写肯定没问题,运算的输入绝对不会导致越界,但随着时间推移,开发人员变更、上下文输入变化,甚至有人直接拷贝代码片段复用到其它场景。
浮点数的存储细节,开发工作中很少会涉及到,了解下原理性就好了。重点要关注运算过程中可溢出的可能性、各个类数据类型间的转换,很多安全漏洞,都是由这种事情导致的。
是否存在静态检查工具?帮助找到这种潜在的问题,找了几个工具,对于书中的示例扫描结果都没有警告提示。
第一组
郝立鹏
依旧和第一章一样,关于公式的部分我没有细看,就看了下结论,总体来说对第二章感觉没那么深刻,
起码第三章的开始做实验的章节。到时边看书边做实验,也许会好点?
学习第二章之前,我对整数的表示方式比较理解,但是对浮点数一直很困惑,这个如何来表示呢?
1、浮点数
浮点数的表示,分别由符号S、尾数M、阶码E三部分组成,其中符号S是首个bit,通过该bit标示有符号位,也就是说浮动数是采用原码表示。尾数M是二进制小数,随阶码E的取值,决定是否隐式包含1。而阶码E的取值,将浮点数分为三类:规格化、非规格化、无穷大或NaN。
2、溢出
32位的数值转为16位时可能会发生溢出;32位数值乘以32位数值的结果存储在32位数值中,可能无法存下
3、浮点数之间相等如何来判断
4、补码的运算
源码与补码的转换:
1)0和正数的补码与源码相同
2)负数,补码的符号位不变,其余位取反,最后+1
计算:50000*50000=2500000000
二进制补码计算结果:1001 0101 0000 0010 11111 0010 0000 0000
正数加符号位后:0|1001 0101 0000 0010 11111 0010 0000 0000(33位了)
超出32位后,去掉最高位0:1 0010 1010 0000 0101 1111 0010 0000 0000
符号位不变,其余取反后+1获得源码: 1101 0101 1111 1010 0000 1101 1111 1111+1=1101 0101 1111 1010 0000 0111 0000 0000
结果转化为10进制:-1794967296
由于有一个组员是写在其他笔记软件上的,帮他贴一下
张仁杰
week2:心得体会:
从使用二进制进行表示再到为了更好的位模式而使用16进制,这些都是信息展现在我们眼前最出的样子,但是在寻址与字节顺序时需要注意机器的大小端,这些都是信息的表示上面的坑。不管是浮点数还是整数,当初设计时便是如此的巧妙;最常见的负数的表示方式就是以补码的方式,而且Java中要求采用补码进行标识;但是对于程序中信息的处理时需要注意的,虽然是很微妙同时也容易被忽略,从考虑数据的溢出,再到数据隐士强制类型转换,这些都是可能造成漏洞,导致一些不可想象的后果,那么从开发上来讲,需要正确的考虑极限的种场景下,这种信息表示是否合理。浮点数的运算这个可以算是比较难理解的,规格化的值与非规格化值为什么要如此设计?它就是想要浮点数按照整数排序那样进行排序。浮点数计算中同样存在溢出、精度损失等问题,这些都是在使用浮点数计算需要注意的,它也不是遵守我们所认识的算数属性。
第六组
之昂
机器表示法
1. 无符号编码基于传统的二进制表示法:表示大于或者等于零的数字;
2. 补码编码表示有符号证书最常见的方式,有符号证书就是可以为正或为负;
3. 浮点数编码是表示实数的科学技术法的以2为技术的版本
溢出
计算机的表示法使用有限数量的位来对一个数字编码,结果太大不能表示就会溢出。
整数表示虽只能编码一个相对较小的数值范围,但是这种是精确的,浮点数虽然可以编码,但是近似的
字长
32位/64位程序
区别在于程序是如何编译,而不是其运行的机器类型。
c语言
1. 位级运算:
与、或、非、异或
2. 逻辑运算:
认为所有的非零的参数都表示true,参数0表示false,返回0或1,分别表示true或false
||、&& 和 !
3. 移位运算
和左移相应的右移运算x>>k,有点微妙。一般而言,机器支持两种形式的右移:逻辑右移和算术右移。
逻辑右移在左端补k个0,
算术右移是在左端补k个最高有效位的值
它对有符号整数数据的运算非常有用。
有符号数表示法
无符号与补码直接转换
第七组
高华
静默组
Bean
1.所在小组:第五组
2.组内昵称:肖思成
3.心得体会
因为自己一直做的PHP开发,对这方面了解的不多,所以这部分读起来的进度比较慢。
整个2.2阅读下来,我觉得最重要的一句话就是C语言在无符号数和有符号数之间的转换,其原则是底层的位表示保持不变。所有的知识点围绕这句话来进行讲解。
1.所在小组:静默组
2.组内昵称:Tang_D
3.心得体会
所在小组
静默组
组内昵称
franceshu
你的心得体会
本周主要学习第二章信息的表示和处理,数值处理部分相对比较枯燥.第一遍看到后面定理晕头转向的,后面看第二遍的时候将所有练习题都跟着动手完成,效果明显好了很多.
对于整数运算,表示数字的有限字长限制了可能值的取值范围,结果存在溢出可能.C语言中强制类型转换等一些规定可能会产生非直观的结果,需要注意.
浮点数计算只有有限的范围和精度,并且不遵守普遍的算数属性.
重新温顾的知识点:
所在小组:静默组
组内昵称:黄小黄
你的心得体会:
1.所在小组:静默组
2.组内昵称:维钢、
3.心得体会: