OC成长之路 <一> 位运算符、枚举、(原码,补码,反码)、以及NSInteger,NSUInteger,int的区别
引言:
咳咳,首先我是一枚资深小白(资深小白- -、也太衰),这个博客是自己用来记录迷糊犯二的东西。
最近发现难道是因为老了么(我明明18....),很多东西,脑子已经不记得了,迷迷糊糊,概念理论的东西脑子里面七零八落。
从一个知识点跳到另一个知识点,简直是连环事故. 😫sad.
傲娇的我不服,决心要认认真真做好笔记,记录期间遇见的种种。
生活要有仪式感,学习应该也是要这样,作为Chapter 1 ,应该要起个好名字。
想了几十分钟,还是随便一些吧。
那就叫:
OC成长之路
okok,来开始今天的学习。
小记:
本篇主要了解回顾了位运算符、枚举的使用、计算机原码,补码,反码的表示、32位和64位操作系统、以及NSInteger,NSUInteger,int的区别。
今天在看一些源码的时候发现一些位运算符操作,但是处理过程不是很清楚了,于是乎又重新查阅资料,手动写了一下加深理解。
位运算符
位运算符 包括:
运算 | C++ 符号 | 意义 |
---|---|---|
and |
& |
按位与 |
or |
` | ` |
not |
~ |
按位取反 |
xor |
^ |
按位异或 |
shl |
<< |
左移 |
shr |
>> |
右移 |
在使用的时候要注意运算符运算顺序优先级,优先级顺序如下表:
优先级 | 运算符 | 结合律 | 助记 |
---|---|---|---|
1 | :: |
从左至右 | 作用域 |
2 | a++ 、a-- 、 type() 、type{} 、 a() 、a[] 、 . 、-> |
从左至右 | 后缀自增减、 函数风格转型、 函数调用、下标、 成员访问 |
3 | ! 、~ 、 ++a 、--a 、+a 、-a 、 (type) 、sizeof 、&a 、 *a 、 new 、 new[] 、delete 、 delete[] |
从右至左 | 逻辑非、按位非、 前缀自增减、正负、 C 风格转型、取大小、取址、 指针访问、 动态内存分配 |
4 | .* 、->* |
从左至右 | 指向成员指针 |
5 | a*b 、a/b 、a%b |
从左至右 | 乘除、取模 |
6 | a+b 、a-b |
从左至右 | 加减 |
7 | << 、>> |
从左至右 | 按位左右移 |
8 | < 、<= 、> 、>= |
从左至右 | 大小比较 |
9 | == 、!= |
从左至右 | 等价比较 |
10 | a&b |
从左至右 | 按位与 |
11 | ^ |
从左至右 | 按位异或 |
12 | ` | ` | 从左至右 |
13 | && |
从左至右 | 逻辑与 |
14 | ` | ` | |
15 | a?b:c 、 = 、+= 、-= 、*= 、/= 、%= 、&= 、^= 、` |
=、 <<=、 >>=` |
从右至左 |
16 | , |
从左至右 | 逗号 |
接下来,来手动测试一下位运算符:
-
<< 左移运算符
-
int a = 1; //0001 int c = a << 1; //0010 -2 NSLog(@"左移运算符->%d",c); -->左移运算符->2
-
>> 右移运算符
-
int b = 2; //0010 int c = b >> 1; //0001 -1 NSLog(@"右移运算符->%d",c); -->右移运算符->1
-
| 按位或运算 有一个为1,就为1
-
int a = 1; //0001 int b = 2; //0010 int c = a | b; //0011 -3 NSLog(@"按位或运算->%d",c); -->按位或运算->3
-
&按位与运算 只有两个数都为1时,才是1
-
int a = 1; //0001 int b = 5; //0101 int c = a & b; //0001 -1 NSLog(@"按位与运算->%d",c); -->按位与运算->1
-
^按位异或运算,不相同时候为1,相同时候为0
-
int a = 1; //0001 int b = 5; //0101 int c = a^b ; //0100 -4 NSLog(@"按位异或运算->%d",c); -->按位异或运算->4
-
~按位取反
-
int a = 5; //0101 int c = ~a ;//1010 NSLog(@"按位取反运算->%d",c); -->按位取反运算->-6
好吧,你困惑么,为什么取反结果是-6? 0101 -->1010 ,按照正负数0正1负的概念,结果应该是-10才对啊?为什么会是-6呢,这个时候脑子里依稀记得学校里数字电路好像有提到负数的原码和补码表示。但是记不清了...无奈只能去查阅资料再次回顾一下,学过的都还给学校果然是如此😂。
今天要彻底搞懂这些关系。
首先要知道一点,计算机系统中是使用补码来表示数字存储。
先讲一下原码,反码,补码的概念:
原码
-
—用8位二进制表示一个数,那么+11 的原码就是:+11的原码为00001011,-11的原码就是10001011
-
[+11] = 0000 1011 原 [-11] = 1000 1011 原
-
第一位是符号位(0正 1负),因为第一位是符号位,所以,8位二进制的取值范围就是:
【1111 1111 ,0111 1111】--【-127,127】
反码
-
正数的反码是其本身
-
负数的反码是在原码的基础上,符号位不变,其余各位取反
-
[+11] = [0000 1011]原 = [0000 1011]反 [-11] = [1000 1011]原 = [1111 0100]反
-
如果用反码表示负数,是不能直观的看出它的数值的,所以通常会将其转成原码再计算。
补码
-
正数的补码就是其本身
-
负数的补码就是在原码的基础上,符号位不变,按位取反,最后+1.(反码+1)
-
[+11] = [0000 1011]原 = [0000 1011]反 = [0000 1011]补 [-11] = [1000 1011]原 = [1111 0100]反 = [1111 0101]补
-
同样,对于负数,用补码表示一样不能直观看出数值。
那么问题来了,就目前来看,不管是正数还是负数,原码才是最直观计算的方式,那为什么还要有反码和补码呢?
我们看一下这个计算: 1 - 1 = 1+(-1)= 0 ,00000001+10000001=10000010 ,按照原码计算的方式结果为-2,那我们要怎么计算才能让它变成0呢?对于正数原码的编码方式没有问题,0000 0000代表0,0000 0001代表1,这些符合我们的习惯,出错就在于后面的负数编码上,
所以,我们来想一下如何赋值,因为 1 + (-1)必须要等于0,所以,1 +x =0,x 这个二进制编码值要代替刚刚的-1的二进制编码。0000 0001 +x = 0000 0000, 我们看下面的式子:
0000 0001
+ 1111 1111
= 0000 0000
高位舍弃,所以结果为 0000 0000 ,这里的1111 1111是我们假定的一个中间值。
这时候,如果对原x = -1,我们按照负数的原码反码补码概念操作。
原码 1000 0001
反码 1111 1110
补码 1111 1111
-1补码的结果就是我们上面加法运算的假定值1111 1111。 有木有豁然开朗的感觉,原来他们定义补码的概念就是为了进行负数的加法运算。
所以,在CPU中,为了设计简单,通常只设计有加法器,而没有减法器。如果将减法(例如A-B)转换成加法(A+(-B)),那么在这个转换过程中就出现了负数(-B),而补码和反码的优势主要是用于对于负数的表示,所以就有了统一规定,还记得上面开头提到的那句话吗,计算机系统中是使用补码来表示数字存储,也就是为了统一使用补码进行运算:
- 在表示正数的时候,补码和反码的形式就与原码相同,
- 表示负数的时候,则用与原码不同形式的补码和反码。
按位取反,再加一
前面,我们知道了补码的概念,为了保证负数的加法运算,所以定义了补码,但是为什么概念里要定义按位取反 加一呢?再来细细研究一下,对于刚刚的1 +x =0,x = 0 - 1 , x = 0000 0000 - 0000 0001,我们要怎么计算这个二进制减法呢?小数减去大数无法借位,这里,要提到一个进位丢失的概念。
1111 1111
+ 0000 0001
= 结果是 0000 0000也就是0,虽然结果按道理应该是1 0000 0000,但是最高位只能有8位,所以高位1丢失。
我们可以把前面的0000 0000 换成(1111 1111 +0000 0001)。也就是说,前面的运算1+x =0,我们需要计算x=-1的补码,只要计算0-1的编码就好,也就是 0000 0000 - 0000 0001 = (1111 1111 +0000 0001)- 0000 0001 =-1的补码 = 1111 1111
有没有发现结果就是-1的补码,那么对于"按位取反再加一",是怎么来的呢?
我们继续看,按照上面的法则,1+x =0, (x= -1) , x = 0-1,每次通过0减去一个数的补码的到另一个数的补码, 也就是说(1111 1111 +0000 0001)不变,就是0,求一个数的补码的话:
(11111111+00000001)- 一个数的补码=它相反数的补码
把括号去掉再换位加上括号,也就是(11111111 - 一个数的补码)+00000001=它相反数的补码,
那么,11111111 减去 一个数的补码 是什么?
我们举个例子就明白了,以上面的1为例,1的原码=反码=补码= 0000 0001
1111 1111
- 0000 0001
=1111 1110
有没有发现,结果就是将这个数按位取反, 所以(11111111 - 一个数的补码)= 这个数按位取反,代入上面的运算;
这个数按位取反 +0000 0001 = 它相反数的补码,也就是按位取反,再加一。
所以负数的补码 = 按位取反 +1.
计算机操作使用补码来进行计算
这里,我不知道你之前有没有一个疑问,前面讲了很多次,计算机操作使用补码来进行数字存储。这是为什么呢?
首先,我们知道原码计算是不可行的,会出错。比如:
十进制表达式: 1-1 =0
1-1 = 1+(-1) = [0000 0001]原 + [1000 0001]原 = [1000 0010]原 =-2
那么如果用反码呢?
1-1 = 1+(-1)= [0000 0001]原 + [1000 0001]原=[0000 0001]反 +[1111 1110]反=[1111 1111]反 = -0
问题来了,按照我们的理解-0和0应该是一样的,因为0带符号本身没有任何意义,而且这里出现了[0000 0000 ]和[1000 0000],两个编码表示0。
这时候补码的出现解决了这个问题。
1-1 = 1+(-1)= [0000 0001]原 + [1000 0001]原 =[0000 0001]补+[1111 1111]补=[0000 0000]补=[0000 0000]原
这样,0用[0000 0000]表示,而-0的问题也不存在,这时候,我们可以用[1000 0000]表示-128;
(-1)+(-127)=[1000 0001]原+[1111 1111]原=[1111 1111]补+[1000 0001]补=[1000 0000]补
-1-127结果是-128,在用补码运算的结果中就是-128.
使用补码,不仅仅修复了0的符号以及两个编码的问题,而且还能够多表示一个最低位-128.
所以,有一个结论:
8位二进制数,使用原码或反码表示的范围为[-127,127],补码表示的范围为[-128,127]。
到这里,对于计算机的计算运算操作,应该非常清楚了。至少我清楚了😄
再回过头来看前面的问题,我们的位运算操作:按位取反。
int a = 5; //0101
int c = ~a ;//1010
NSLog(@"按位取反运算->%d",c);
-->按位取反运算->-6
我们同样,假定 5 原码=补码=0000 0101,对其取反结果= 1111 1010,这里的二进制代表补码。
所以,现在的问题转化成,我们怎么对补码操作,求原码or 真值。
我们都知道负数的原码求补码 = 原码按位取反+1,那么反过来,对于负数而言,怎么通过补码求原码呢?
下面给出以下方法:
- 方式一: 先把符号位去掉, 把剩下的非符号位取反后得到一个无符号位的二进制序列, 将该二进制序列转换为十进制整数(注意:无符号位二进制序列代表的一定是正数), 将该整数加1得到的数便是原补码代表的整数的绝对值.
对于上面的1010(补码),010 -取反 101 -二进制 0101 = 5,|a|=5+1 =6,所以,a =-6。
- 方式二: 利用公式: (-1)*2^(n-1) + 非符号位代表整数 = 值.
1010(补码),n =4, 010代表0010 2,a = -1*2^3+2 = -6
- 方式三:负数的补码的补码就是原码,也就是说,对补码取反再加一就是原码
-1010(补码),取反=1 0101,再加1 =1 0110 原码=-6
那么,你对负数的补码的补码就是原码有疑问吗?我们再来理清一下:
对于5,我们 以8位二进制表示00000101,位运算取反后是11111010,在计算机中首位是0表示正数,是1表示负数,即11111010表示的是一个负数,即要由11111010求这个负数,而且我们都知道,计算机中是以补码来表示存储计算,所以这里就是要求该补码的逆,也就是原码,对于正常的求负数补码操作,是按位取反再加1,这里反着来就好:
1111 1010先减1得1111 1001(二进制减法运算,不够向高位借位,一位代表2),再取反,取反时符号位不变,也就是1000 0110 最后等于-6
先别急,这里进行的都是二进制运算,那么,我们可以尝试一下这样,对1111 1010先取反再加1会是怎么样?
1111 1010 --取反= 1000 0101 再加1 = 1000 0110 =-6
也就是说,二进制运算中,减一取反和取反加一实质是一样的操作,所以,我们对负数的补码求原码,就是通过补码的补码求原码,即对补码取反再加一就好。
上面,我们以实例的方式研究了原码,补码,反码的概念以及使用,这部分就到此结束。
7. 常用的位运算表达式
功能 | 示例 | 位运算 |
---|---|---|
去掉最后一位 | 101101->10110 | x >> 1 |
把最后一位变成1 | 101100->101101 | a | 1 |
把最后一位变成0 | 101101->101100 | (a | 1)-1 |
最后一位取反 | 101101->101100 | a ^ 1 |
把右数第n位变成1 | 101001->101011,n=2 | a | (1 < < (n-1)) |
把右数第n位变成0 | 101101->100101,n=4 | a & ~ (1 < < (n-1)) |
右数第n位取反 | 101001->100001,n=4 | a ^ (1 < < (n-1)) |
取末三位 | 1101101->101 | a & 7 |
取末n位 | 1101101->1101,n=5 | a & ((1 < < n)-1) |
取右数第n位 | 1101101->1,n=4 | a >> (n-1) & 1 |
把末n位变成1 | 101001->101111,n=4 | a | (1 < <n-1) |
末n位取反 | 101001->100110,n=4 | a ^ (1 < <n-1) |
把右起第一个0变成1 | 100101111->100111111 | a | (a +1) |
把右边连续的0变成1 | 11011000->11011111 | a | (a -1) |
判断奇数 | (a&1)==1 | |
判断偶数 | (a&1)==0 |
枚举类型
当你所声明的对象包含各种状态或者类型,这时候就可以使用枚举类型,这样是为了能更好的体现编码规范。通过枚举,定义的类型结构更为直观,代码更易读懂。编译器会为枚举分配一个独有的编号,默认从0开始,每个枚举递增1。
-
enum
-
enum nameType { Name1,// 编号从0开始记数 Name2, Name3, }; enum nameType type = 1; switch (type) { case Name1: NSLog(@"小J"); break; case Name2: NSLog(@"小L"); break; case Name3: NSLog(@"小S"); break; default: break; }
-
typedef enum
我们都知道typedef 可以为数据类型另外指派一个名称
// 定义名称Count和int等效。 typedef int Count; Count a,b; //等价 int a,b;
使用typedef来定义枚举类型,我们不需要在指定生成enum对象,直接调用别名即可(enum后面的枚举类型名可以省略)。
typedef enum nameType { Name1,// 编号从0开始技数 Name2, Name3, }type; type t = 2; switch (t) { case Name1: NSLog(@"小J"); break; case Name2: NSLog(@"小L"); break; case Name3: NSLog(@"小S"); break; default: break; }
或者是下面这样:
typedef enum State:NSInteger{ StateOne, StateTwo, StateThree }test;
-
NS_ENUM
Apple统一了枚举类型的定义使用,当表示单一属性状态的时候使用-->NS_ENUM 整型枚举:
-
typedef NS_ENUM(NSUInteger, Seasons) { spring = 1, summer = 5, autumn = 10, winter = 15 }; Seasons t = 5; switch (t) { case spring: NSLog(@"spring"); break; case summer: NSLog(@"summer"); break; case autumn: NSLog(@"autumn"); break; case winter: NSLog(@"winter"); default: break; }
-
NS_OPTIONS
当一个枚举变量可能代表多个属性,也就是说需要同时满足多个枚举条件时,可以使用按位或操作来进行组合-->NS_OPTIONS 位移型枚举
先看一个很好的例子,关于枚举变量拥有多个属性时如何使用-->enum枚举表达。
-
//当变量同时享有Melee和Fire的属性时的处理过程。 typedef enum : NSUInteger { // 十进制 // 二进制 None = 0 , // 0000 0000 Melee = 1 , // 0000 0001 Fire = 2 , // 0000 0010 Ice = 4 , // 0000 0100 Posion = 8 // 0000 1000 } AttackType; AttackType attackType = Melee | Fire | Ice; // 0000 0111 //通过|运算,将结果分别与对应属性做与运算来判断匹配对应类型。 if (attackType & Melee) { // 位运算符:异或(^),表示去掉某个枚举值。 NSLog(@"Melee %li", attackType); attackType ^= Melee; NSLog(@"Melee %li", attackType); } if (attackType & Fire) { NSLog(@"Fire %li", attackType & Fire); } if (attackType & Ice) { NSLog(@"Ice %li", attackType & Ice); } if (attackType & Posion) { NSLog(@"Posion %li", attackType & Posion); }
再看使用新方式-->NS_OPTIONS
-
typedef NS_OPTIONS (NSUInteger, AttackType) { None = 0, Melee = 1 << 0, Fire = 1 << 1, Ice = 1 << 2, Posion = 1 << 3 }; // 赋值 AttackType attackType = Posion; .... ....
总结:
和上面的enum用法一样,但区别就在于使用NS_OPTIONS相对更直观明朗,官方也推荐使用。
- 使用 按位或 可以给一个参数同时设置多个 "类型"。在执行的时候,可以使用按位与判断具体的 "类型"。
typedef NS_OPTIONS(_type, _name) new; -> 位移的,可以使用按位或设置数值
typedef NS_ENUM(_type, _name) new; -> 数字的,直接使用枚举设置数值
当然推荐使用这个新枚举用法有一个最主要的原因就是(源自网上的总结):
根据是否要将代码按C++模式编译,NS_OPTIONS宏的定义方式有所不同。如果不按C++编译,那么其展开方式就和NS_ENUM相同。若按C++编译,则展开后的代码略有不同。原因在于,用按位或运算来操作两个枚举值时,C++编译模式的处理办法与非C++模式不一样。作为选项的枚举值经常需要用按位或运算来组合,在用或运算操作两个枚举值时,C++认为运算结果的数据类型应该是枚举的底层数据类型,也就是NSUInteger,而且C++不允许将这个底层类型“隐式转换”(implicit cast)为枚举类型本身
-->简言之就是:
因为编译器的编译方式不同,当你使用enum来定义枚举类型的时候,如果是c++编译模式,那么就会产生报错。所以在使用枚举来定义对象的时候,就统一按照官方标准,使用NS_ENUM和NS_OPTIONS即可。
如果对此有兴趣研究的话,可以参考源码:两个宏的定义在Foundation.framework的NSObjCRuntime.h中
好了,上面就是枚举类型的用法,到现在是不是很熟悉了,没错,反正我是很清楚了😄。不过,接下来我又产生了一个疑惑,迷糊它来的总是很快,而且是一环套一环。关于int和NSInteger有什么区别? 为什么枚举里的类型都使用NSInteger,NSUInteger,不是使用int?
带着疑问,又继续查了资料(脑子里有飘来学校里的知识碎片,好像是关于有无符号整型)。
NSUInteger和NSInteger和int
首先,我们应该都知道,int和NSInteger其实是差不多的,但更推荐使用NSInteger,因为NSInteger是一个封装,它会识别当前操作系统的位数,自动返回最大的类型
#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
这里,你对32位和64位操作系统还有概念么? 没关系,我也忘记了,继续mark 记录下来。
这里简短的说一下区别。
首先我们遇到32位和64位的情况有两种,第一是下载系统的时候会分X64和X86,第二是安装程序的时候会提示下载64还是32的。
1. 从系统方面来说:X86是32位版本的系统,而X64是64位版本的系统。我们知道CPU一次处理数据的能力是32位还是64位,关系着系统需要安装32位还是64位的系统。
32 位和 64 位中的"位",也叫字长,是 CPU 通用寄存器的数据宽度,是数据传递和处理的基本单位。字长是 CPU 的主要技术指标之一,指的是 CPU 一次能并行处理的二进制位数,字长总是8的整数倍。
2.内存寻址能力区别
32位系统寻址能力是4G容量,不过需要保留一些给硬件使用,因此留给用户的可用内存一般是3.25g-3.5G容量左右,即使你插上8G内存,也无法识别那么大容量,而64位系统可以支持128GB大内存,甚至更大。
1.32位系统的最大寻址空间是2的32次方=4294967296(bit)= 4(GB)左右;
2.64位系统的最大寻址空间为2的64次方=4294967296(bit)的32次方,数值大于1亿GB;
3.运算速度区别
安装64位系统,需要CPU必须支持64位,而64位CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度为64位,64位指令集可以运行64位数据指令,也就是说处理器一次可提取64位数据(只要两个指令,一次提取8个字节的数据),比32位(需要四个指令,一次提取4个字节的数据)提高了一倍,理论上性能会相应提升一倍。
一般情况下,我们很大部分的软件都是在32位架构环境下开发的,这就是为啥64位系统的兼容性不如32位
不过需要了解的是,在32位系统下是无法运行64位软件的,而64位系统却支持安装大部分的32位软件。
常用数据类型对应字节数
32位编译器:
char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节
64位编译器:
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节
不管是32位系统还是64位系统,int型都是4字节,只有指针类型不同(因为地址位数不同,造成寻址不同,造成指针大小不同)
ok,就说到这里,回到整型的部分。
NSUInteger是无符号整型,即没有负数而言,NSInteger是有符号整型,所以NSUInteger类型不能给它赋负值。
NSUInteger一般用于定义index 之类的正整数概念里。
如果你给一个NSUInteger赋值的话,你会掉进一个坑里去。
NSUInteger a = -2;
NSLog(@"NSUInteger value -> %lu",(unsigned long)a);
-->NSUInteger value -> 18446744073709551614
你会发现这个数字是真的想不到的大。。。。如果必须要用的话,那么一定要做好处理:
NSUInteger a = -2;
NSLog(@"NSUInteger value -> %ld",(NSInteger)a);//通过NSInteger 强转
NSLog(@"NSUInteger value -> %ld",(long)a);
NSArray *items = @[@1, @2, @3];//下面的循环不会执行
for (int i = -1; i <= (NSInteger)items.count; i++) {
NSLog(@"%d", i);
}
//原因是array count定义是NSUInteger类型,-1 和一个NSUinteger类型的数作比较的时候 -1被转换成了一个非常大的数。可以通过强转来执行循环操作
NSLog(@"NSUInteger value -> %ld",(NSInteger)items.count);
for (int i = -1; i <= (NSInteger)items.count; i++) {
NSLog(@"%d", i);
}
NSUInteger和NSInteger和int都是基础类型,是不能放入NSArray中,这时候我们可以使用NSNumber.
NSNumber可以将基本数据类型包装起来,形成一个对象,这样就可以给其发送消息,装入NSArray中。
- (NSNumber *) numberWithChar: (char) value;
- (NSNumber *) numberWithInt: (int) value;
- (NSNumber *) numberWithFloat: (float) value;
- (NSNumber *) numberWithBool: (BOOL) value;
手动试一下,果然会报错,所以对于基础类型的部分,还是要通过转化再装入array中
NSArray *a = [NSArray arrayWithObjects:@"JJJJ",@"LLLL", nil];
int bc = 3;
a = [a arrayByAddingObject:[NSNumber numberWithInt:bc]];
NSLog(@"the content is %@",[a objectAtIndex:2]);
上面所写的东西都是基于自己在学习中遇见的问题总结,大部分都是从网上看到的相关资料,自己顺着意思理解并整理出来作为学习使用,如果有侵犯的地方还请说明喔。今天到此结束啦,希望大家都有一个happy day _ 。