前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >sizeof 知多少? (上)

sizeof 知多少? (上)

原创
作者头像
serena
修改2021-08-03 14:56:10
8510
修改2021-08-03 14:56:10
举报
文章被收录于专栏:社区的朋友们社区的朋友们

作者:郁旭斌

稍熟悉C/C++的朋友,对于sizeof肯定不陌生,通过他我们可以知晓某个类型或者实例的内存大小(以字节计),但是如果深入一下sizeof计算的细节,想来大部分朋友就不会那么熟稔了,不过平心而论,平日的工作中其实也很少需要准确计算类型的内存大小(当然,定性的估算类型内存占用还是需要的),但是了解一下sizeof底层的计算方式并没有什么坏处,甚至于可能对我们的灵魂都有益处(Low-level programming is good for the programmer’s soul),网上关于这个话题的信息其实挺多的,但是大多比较零散,自己尝试写了一篇,算作是一个总结吧。

0. 基本类型

像 char, int 等基本类型的 sizeof 大小应该属于基本常识了,值得注意的一点是部分基本类型在32位系统和64位系统中具有不同大小(譬如 long 类型在32位系统中一般为4字节大小,而在64位系统中一般为8字节大小),简单起见,后面的示例我们尽量限制了基本类型的使用,并且有以下约定:

sizeof(char) = 1(字节)

sizeof(short) = 2(字节)

sizeof(int) = 4(字节)

sizeof(pointer) = 4(字节)(其中pointer指代任意的指针类型)

1. 结构类型

通过定义结构,我们可以组合使用基本类型,考虑以下代码:

代码语言:javascript
复制
struct s1
{
    int m_1;
};

sizeof(s1)的大小是多少呢?很简单,对于只有单个成员的结构,其sizeof的大小便是其成员的sizeof大小,拿s1来说:

sizeof(s1) = sizeof(s1.m_1) = sizeof(int) = 4(字节)

那么如果s1有多个成员呢?考虑以下代码:

代码语言:javascript
复制
struct s1
{
    int m_1;
    int m_2;
    int m_3;
};

如果你简单做个试验,你会得到 sizeof(s1) = 12(字节) 的结果,综合之前的讨论,我们似乎可以得出以下结论:

  • 结构体 S 的大小等于 S 中各成员的大小之和 *

写成公式可能更直观些(假设结构体 S 的各成员分别为 M1, M2, … Mn):

  • sizeof(S) = sizeof(M1) + sizeof(M2) + … + sizeof(Mn) *

OK,既然有了公式,计算结构体的大小便简单了,仍然考虑之前的s1,我们稍稍改动一下他的定义(m_2成员由int类型改为了short类型):

代码语言:javascript
复制
struct s1
{
    int m_1;
    short m_2;
    int m_3;
};

按照之前总结的公式,我们有:

  • sizeof(s1) = sizeof(s1.m_1) + sizeof(s1.m_2) + sizeof(s1.m_3) = sizeof(int) + sizeof(short) + sizeof(int) = 4 + 2 + 4 = 10(字节) *

但实际上,如果你再次试验一番,你会发现 sizeof(s1) 仍然是 12(字节)!,我们究竟遗漏了什么?

其实这里面有一个数据对齐问题

不同的计算机平台对于数据在内存中的存放位置是有限制的,一般要求数据的内存地址是其大小的整数倍(譬如 int 数据的大小是 4 字节,则其地址也应该是 4 的整数倍,例如0,4,8等等),如果违反了这个存放限制,一些平台需要多次访问内存才能完整读取数据(影响效率),一些平台则会直接产生错误。

由于存在上面的数据对齐问题,编译器在为结构体生成内存布局的时候,会在结构体的数据成员之间填充数据,以使结构体的各个成员都能满足内存对齐需求。

而关于编译器填充数据的规则,一般都要求填充的数据越少越好,核心的方法如下(假设结构体 S 的各成员分别为 M1, M2, … Mn):

首先假设结构体的起始地址为0(0对于任意的数据类型都是对齐地址)

依次考虑M1, M2, … Mn来计算S的大小,例如对于其中Mi,考虑之前所计算的结构大小是否满足Mi的对齐需求,不满足则填充数据

规则看上去比较抽象,我们拿之前的s1来试验一下:

代码语言:javascript
复制
struct s1
{
    int m_1;
    short m_2;
    int m_3;
};

假设s1的起始地址是0,依次考虑各个数据成员:

考虑 m_1,由于当前地址是0,满足对齐规则,我们不需要填充数据,s1的大小更新为 sizeof(s1.m_1) = 4

考虑 m_2,由于当前地址是4,而m_2的大小是2,地址4是2的整数倍,所以m_2满足对齐规则,不需要填充数据, s1的大小更新为 4 + sizeof(s1.m_2) = 4 + 2 = 6

考虑 m_3,由于当前地址是6,而m_3的大小是4,地址6不是4的整数倍,所以我们需要填充2字节来使地址增长为4的整数倍(即8),再加上m_3的大小, s1的大小更新为 6 + 2(填充) + sizeof(s1.m_3) = 6 + 2 + 4 = 12

一图胜千言,下面关于s1的内存布局示意图可能更直观一些(其中深色部分为填充数据):

图:s1内存布局

其实上述的计算过程是可以用公式来表述的(虽然公式不是封闭形式,而是递推形式,但仍然比文字描述来的清晰)

我们定义函数 sizeof’(S, i) 为结构体 S 考虑了第 i 个成员后计算所得的sizeof大小,则有 sizeof(S) = sizeof’(S, n) (假设S有n个成员)

sizeof’(S, 0) = 0

sizeof’(S, i) = ceil(sizeof’(S, i - 1) / sizeof(Mi)) \ sizeof(Mi) + sizeof(Mi) (其中ceil为向上取整函数) *

我们再来利用公式计算一下s1的大小:

sizeof’(s1, 0) = 0

sizeof’(s1, 1) = ceil(sizeof’(s1, 0) / sizeof(s1.m_1)) sizeof(s1.m_1) + sizeof(s1.m_1) = ceil(0 / 4) 4 + 4 = 4

sizeof’(s1, 2) = ceil(sizeof’(s1, 1) / sizeof(s1.m_2)) sizeof(s1.m_2) + sizeof(s1.m_2) = ceil(4 / 2) 2 + 2 = 6

sizeof’(s1, 3) = ceil(sizeof’(s1, 2) / sizeof(s1.m_3)) sizeof(s1.m_3) + sizeof(s1.m_3) = ceil(6 / 4) 4 + 4 = 12

sizeof’(s1, 3) = sizeof(s1) = 12

可以看到通过使用公式我们清晰化了之前的计算流程

我们继续考虑下面的s1定义(增加了成员m_4):

代码语言:javascript
复制
struct s1
{
    int m_1;
    short m_2;
    int m_3;
    short m_4;
};

此时sizeof(s1)的大小是多少呢?简单,我们套用之前的公式计算即可:

sizeof’(s1, 0) = 0

sizeof’(s1, 1) = ceil(sizeof’(s1, 0) / sizeof(s1.m_1)) sizeof(s1.m_1) + sizeof(s1.m_1) = ceil(0 / 4) 4 + 4 = 4

sizeof’(s1, 2) = ceil(sizeof’(s1, 1) / sizeof(s1.m_2)) sizeof(s1.m_2) + sizeof(s1.m_2) = ceil(4 / 2) 2 + 2 = 6

sizeof’(s1, 3) = ceil(sizeof’(s1, 2) / sizeof(s1.m_3)) sizeof(s1.m_3) + sizeof(s1.m_3) = ceil(6 / 4) 4 + 4 = 12

sizeof’(s1, 4) = ceil(sizeof’(s1, 3) / sizeof(s1.m_4)) sizeof(s1.m_4) + sizeof(s1.m_4) = ceil(12 / 2) 2 + 2 = 14

sizeof’(s1, 4) = sizeof(s1) = 14

公式计算的结果是14,但实际上,如果你试验一下,sizeof(s1)的大小应该是16,我们又遗漏了什么吗?

其实这里还有一个结构体连续存放的问题

考虑结构体数组定义 S s[2],并且我们假设s的起始地址是0,对于结构s[0]来说,其各个成员都在对齐的内存地址上,但是对于结构s[1]来讲,其各个成员就未必在对齐的内存地址上了.

就拿上面的s1举例,如果我们在各个成员对齐后不再做任何填充操作,那么他的大小便是我们刚才通过公式算出的14,那么对于s1的数组定义 s1 s2,

s[0]的各成员都是满足内存地址对齐的,但是由于结构s1的大小为14,这将导致s[1]的起始地址为14,遂而导致s[1]中的部分成员(譬如s[1].m_1)违反内存对齐原则…

怎么解决这个问题呢?方法就是在对齐结构体的各数据成员之后,再根据结构体中最大成员的大小来填充结构体(这里有个前提就是成员大小都必须是2的幂次,或者说最大成员大小一定要能够整除其余成员大小)

继续拿之前的s1举例,我们计算出了他的大小为14,但这只是s1各个成员经过内存对齐之后的结果,我们还需要对s1进行一次“整体”填充,考虑到s1中最大的成员大小为4(s1.m_1和s1.m_3),由于14并不能整除4,所以需要在结构体末尾再填充2个字节(到达16),所以s1的实际大小应为16

依然给张示意图(其中深色部分为填充的数据):

图:s1内存布局

综合以上因素,我们可以继续完善结构体大小的计算公式:

我们定义函数 sizeof’(S, i) 为结构体 S 考虑了第 i 个成员后计算所得的sizeof大小(假设S有n个成员))

sizeof’(S, 0) = 0

sizeof’(S, i) = ceil(sizeof’(S, i - 1) / sizeof(Mi)) \ sizeof(Mi) + sizeof(Mi) (其中ceil为向上取整函数) *

maxsizeof = max(sizeof(M1), sizeof(M2), …, sizeof(Mn)) (其中max为最大值函数)

sizeof(S) = ceil(sizeof’(S, n) / maxsizeof) \ maxsizeof *

2. 结构中的结构

有了上面的基础,我们接着来看下如果结构中嵌套结构,那么其sizeof的大小应该如何计算呢?考虑下面的代码:

代码语言:javascript
复制
struct s1
{
    int m_1;
    short m_2;
    int m_3;
    short m_4;
};

struct s2
{
    s1 m_1;
    short m_2;
};

这次我们要来尝试计算s2的大小,一开始的尝试自然是套用之前总结的公式:

首先对齐各个成员:

sizeof’(s2, 0) = 0

sizeof’(s2, 1) = ceil(sizeof’(s2, 0) / sizeof(s2.m_1)) sizeof(s2.m_1) + sizeof(s2.m_1) = ceil(0 / 16) 16 + 16 = 16

sizeof’(s2, 2) = ceil(sizeof’(s2, 1) / sizeof(s2.m_2)) sizeof(s2.m_2) + sizeof(s2.m_2) = ceil(16 / 2) 2 + 2 = 18

然后做一次整体填充:

maxsizeof = max(sizeof(s2.m_1), sizeof(s2.m_2)) = max(16, 2) = 16

sizeof(s2) = ceil(sizeof’(s2, 2) / maxsizeof) maxsizeof = ceil(18 / 16) 16 = 32

套用之前的公式,我们得到s2的大小为32,但实际上,试验后的s2大小为20,看来我们又再次遗漏了什么东西…

这次的问题是出在我们之前总结的公式上,由于之前我们仅考虑了基本类型,所以公式中大量使用了sizeof(Mi)这种形式, 但实际上,我们真正需要的是alignof(Mi)这种表达(即某个成员变量的对齐值),而之前的运算之所以正确,完全是因为对于基本类型来说,sizeof(Mi)和alignof(Mi)是相等的! 而现在我们引入了结构体形式的成员变量,sizeof(Mi) = alignof(Mi)这个前提便不再成立了,套用之前的公式自然也无法获得正确答案…

不过我们依然可以通过alignof来修正之前总结的计算公式(注意sizeof和alignof的使用):

我们定义函数 sizeof’(S, i) 为结构体 S 考虑了第 i 个成员后计算所得的内存大小(假设S有n个成员)

sizeof’(S, 0) = 0

sizeof’(S, i) = ceil(sizeof’(S, i - 1) / alignof(Mi)) \ alignof(Mi) + sizeof(Mi) (其中ceil为向上取整函数) *

maxalignof = max(alignof(M1), alignof(M2), …, alignof(Mn)) (其中max为最大值函数)

sizeof(S) = ceil(sizeof’(S, n) / maxalignof) \ maxalignof *

借助这个修正后的公式,我们再来计算一遍s2的大小:

首先对齐各个成员(结构体的对齐值是其成员的最大对齐值):

sizeof’(s2, 0) = 0

sizeof’(s2, 1) = ceil(sizeof’(s2, 0) / alignof(s2.m_1)) alignof(s2.m_1) + sizeof(s2.m_1) = ceil(0 / 4) 4 + 16 = 16

sizeof’(s2, 2) = ceil(sizeof’(s2, 1) / alignof(s2.m_2)) alignof(s2.m_2) + sizeof(s2.m_2) = ceil(16 / 2) 2 + 2 = 18

然后做一次整体填充:

maxalignof = max(alignof(s2.m_1), alignof(s2.m_2)) = max(4, 2) = 4

sizeof(s2) = ceil(sizeof’(s2, 2) / maxalignof) maxalignof = ceil(18 / 4) 4 = 20

这次我们得到了s2的正确大小

继续给张s2的内存示意图:

图: s2内存布局

我们接着来考虑以下的结构定义:

代码语言:javascript
复制
struct s3
{
    short m_1;
    int m_2[33];
};

这里结构体中出现了数组的定义,我们应该如何计算其sizeof大小呢?其实我们只要将数组看做一种特殊的结构即可(大小为 数组元素大小 \ 数组元素个数, 对齐值为 数组元素对齐值*).

据此我们依然可以套用之前的公式来计算s3的大小:

首先对齐各个成员:

sizeof’(s3, 0) = 0

sizeof’(s3, 1) = ceil(sizeof’(s3, 0) / alignof(s3.m_1)) alignof(s3.m_1) + sizeof(s3.m_1) = ceil(0 / 2) 2 + 2 = 2

sizeof’(s3, 2) = ceil(sizeof’(s3, 1) / alignof(s3.m_2)) alignof(s3.m_2) + sizeof(s3.m_2) = ceil(2 / 4) 4 + 4 * 33 = 136

然后做一次整体填充:

maxalignof = max(alignof(s3.m_1), alignof(s3.m_2)) = max(2, 4) = 4

sizeof(s3) = ceil(sizeof’(s3, 2) / maxalignof) maxalignof = ceil(136 / 4) 4 = 136

如果成员是更复杂一些的结构体数组,仍然可以使用相同的方法进行处理,只是这时候的数组元素是结构体罢了.

3. union

除了struct/class之外,C/C++中我们还可以定义union,考虑下面的定义:

代码语言:javascript
复制
union u1
{
    int m_1;
    short m_2;
    char m_3;
};

sizeof(u1)的大小为多少呢?由于联合体需要共用内存,所以其大小是其成员的最大大小,再加上根据联合体对齐值进行填充的数据大小(需要填充的原因和结构体一致),而联合体的对齐值则跟结构体一样,为其成员的最大对齐值

使用公式依然会更清晰一些(假设联合体 U 的各成员分别为 M1, M2, … Mn):

alignof(U) = max(alignof(M1), alignof(M2), …, alignof(Mn))

sizeof(U) = ceil(max(sizeof(M1), sizeof(M2), …, sizeof(Mn)) / alignof(U)) * alignof(U)

套用这个公式,我们来计算下u1的内存大小:

alignof(u1) = max(alignof(u1.m_1), alignof(u1.m_2), alignof(u1.m_3)) = max(4, 2, 1) = 4

sizeof(u1) = ceil(max(sizeof(u1.m_1), sizeof(u1.m_2), sizeof(u1.m_3)) / alignof(u1))alignof(u1) = ceil(max(4, 2, 1) / 4) 4 = 4

继续考虑下面的union定义(其中包含了数组和结构):

代码语言:javascript
复制
struct s1
{
    int m_1;
    short m_2;
    int m_3;
    short m_4;
};

union u1
{
    int m_1;
    char m_2[33];
    s1 m_3[2];
};

初看上去有些吓人,该联合中甚至包含了结构数组!但是套用公式的话,我们依然可以简单的计算出联合的大小(我们已知s1的大小为16,对齐值为4):

alignof(u1) = max(alignof(u1.m_1), alignof(u1.m_2), alignof(u1.m_3)) = max(4, 1, 4) = 4

sizeof(u1) = ceil(max(sizeof(u1.m_1), sizeof(u1.m_2), sizeof(u1.m_3)) / alignof(u1))alignof(u1) = ceil(max(4, 1 33, 16 2) / 4) 4 = 36

4. bit field(位域)

除了union,在C/C++中还可以通过设置位域来实现数据的紧凑存储,考虑下面的定义:

代码语言:javascript
复制
struct s4
{
    int m_1 : 16;
    int m_2 : 8;
    int m_3 : 8;
    short m_4 : 4;
};

那么sizeof(s4)应该如何计算呢?一般来讲,相邻并且类型相同的位域数据成员会被打包在一起存储,直到成员的位宽之和超过类型大小,或者遇到不同类型的数据成员(包括非位域数据成员),其中也会进行成员的内存对齐和最后的结构填充.

现在让我们根据上述的规则来计算一下s4的大小(以下计算的单位为位(bit)):

首先假设s4的起始地址为0(对齐地址)

考虑 m_1,由于其类型为int(大小为4(字节) = 32(位)),而 m_1 的位宽为 16, 累计位宽为 0 + 16 <= 32, 可以打包进一个int单元

考虑 m_2,类型为int,与上一个成员类型相同, m_2 的位宽为 8, 累计位宽为 16 + 8 = 24 <= 32,可以打包进一个int单元

考虑 m_3,类型为int,与上一个成员类型相同, m_3 的位宽为 8, 累计位宽为 24 + 8 = 32 <= 32,可以打包进一个int单元

考虑 m_4,类型为short,与上一个成员类型不同,需要重新分配short单元,先考虑对齐,由于上一个数据类型是int,地址变更为4(字节),4(字节)可以整除2(字节)(sizeof(short)),所以不用填充对齐数据,之前的累计位宽被清0

继续考虑 m_4,m_4 的位宽为4,累计位宽为 0 + 4 <= 16,可以打包进一个short单元

尝试填充整个结构,这里可以使用之前总结的公式 sizeof(s4) = ceil(6(字节) / 4(字节)) * 4(字节) = 8(字节)(其中6(字节) = sizeof(int) + sizeof(short),4(字节)是s4的对齐值(即int的对齐值))

实际上位域的相关细节还有很多,不同编译器之间的实现也有不少差异,有兴趣的朋友可以继续了解一下.

5. alignas 和 alignof

c++11引入了对齐相关的 alignas 说明符和 alignof 运算符,其中alignof的作用其实和我们之前自己定义的alignof函数相似,用以获取类型的对齐值,而alignas则提供了一种让我们自行设置类型对齐值的方法.

考虑下面的结构定义:

代码语言:javascript
复制
struct alignas(8) s5
{
    char m_1;
    alignas(1) int m_2;
    char m_3;
    alignas(4) short m_4;
};

结构体开头的 alignas 尝试设置了s5的对齐值,之前我们讲过结构体的对齐值为其成员的最大对齐值,即(假设结构体 S 的各成员分别为 M1, M2, … Mn):

alignof(S) = max(alignof(M1), alignof(M2), …, alignof(Mn))

引入了alignas之后,结构体的对齐值变为:

alignof(S) = max(alignas, alignof(M1), alignof(M2), …, alignof(Mn))

注意这里max函数的运用,因为结构体的对齐值取得是alignas及各成员对齐值中的最大对齐值,所以alignas设置的数值不一定就是结构体的对齐值,同样的,对于结构体成员的对齐值我们也可以使用alignas设置,也依然遵循选取最大对齐值的规则.

根据这个规则,我们来计算一下s5的大小(依然可以套用之前的公式,需要注意alignof的计算):

首先对齐各个成员:

sizeof’(s5, 0) = 0

sizeof’(s5, 1) = ceil(sizeof’(s5, 0) / alignof(s5.m_1)) alignof(s5.m_1) + sizeof(s5.m_1) = ceil(0 / 1) 1 + 1 = 1

sizeof’(s5, 2) = ceil(sizeof’(s5, 1) / alignof(s5.m_2)) alignof(s5.m_2) + sizeof(s5.m_2) = ceil(1 / 4) 4 + 4 = 8 (alignof(s5.m_2) = max(1, 4))

sizeof’(s5, 3) = ceil(sizeof’(s5, 2) / alignof(s3.m_3)) alignof(s3.m_3) + sizeof(s3.m_3) = ceil(8 / 1) 1 + 1 = 9

sizeof’(s5, 4) = ceil(sizeof’(s5, 3) / alignof(s3.m_4)) alignof(s3.m_4) + sizeof(s3.m_4) = ceil(9 / 4) 4 + 2 = 14 (alignof(s5.m_4) = max(4, 2))

然后做一次整体填充:

maxalignof = max(8, alignof(s5.m_1), alignof(s5.m_2), alignof(s5.m_3), alignof(s5.m_4)) = max(8, 1, 4, 1, 4) = 8

sizeof(s5) = ceil(sizeof’(s5, 4) / maxalignof) maxalignof = ceil(14 / 8) 8 = 16

s5的内存布局如下:

图: s5内存布局

6. pragma pack etc. ?

很多网上的文章一上来都会提到 pragma pack(4),declspec(align(32)) 或者 attribute((aligned(16))) 之类的设置,其实是不恰当的,一是因为这些都是非标准的编辑器扩展;二是因为这些设置现在基本都能使用标准的alignas进行替代.

值得一提的是 pragma pack 这个VC扩展,他同alignas类似,可以设置结构体的对齐值,但是使用的是最小值规则,考虑以下定义:

代码语言:javascript
复制
#pragma pack(1)
struct s6
{
    int m_1;
    short m_2;
};
#pragma pack()

我们使用#pragma pack(1)尝试设置了s6(包括其各个成员)的对齐值为1,那么有:

alignof(s6.m_1) = min(1, 4) = 1

alignof(s6.m_2) = min(1, 2) = 1

alignof(s6) = min(1, alignof(s6.m_1), alignof(s6.m_2)) = 1

进而我们可以使用公式计算得到s6的大小为6

这种类似压缩的效果是使用标准的alignas无法实现的(因为alignas使用了max规则),所以在某些场景下可能还需要使用 #pragma pack

7. virtual method table (vtable)

C++为了实现多态使用了虚拟函数表,每一个包含至少一个虚函数的类型都会有一张虚函数表,每一个对应类型的实例都存有一个虚函数表指针指向该类型的虚函数表,一般来讲虚函数表指针都放在类型实例内存布局的首部.

考虑以下代码:

代码语言:javascript
复制
struct cv1
{
    int m_1;
    virtual ~cv1() {};
};

由于有虚拟析构函数,所以cv1的实例会包含一个虚拟函数表指针(简称为vptr),按照之前的约定,指针类型的sizeof大小为4字节,alignof大小也为4字节,在计算cv1的内存大小时,我们仍然可以沿用之前的计算公式:

首先对齐各个成员:

sizeof’(cv1, 0) = 0

sizeof’(cv1, 1) = ceil(sizeof’(cv1, 0) / alignof(cv1.vptr)) alignof(cv1.vptr) + sizeof(cv1.vptr) = ceil(0 / 4) 4 + 4 = 4

sizeof’(cv1, 2) = ceil(sizeof’(cv1, 1) / alignof(cv1.m_1)) alignof(cv1.m_1) + sizeof(cv1.m_1) = ceil(4 / 4) 4 + 4 = 8

然后做一次整体填充:

maxalignof = max(alignof(cv1.vptr), alignof(cv1.m_1)) = max(4, 4) = 4

sizeof(cv1) = ceil(sizeof’(cv1, 2) / maxalignof) maxalignof = ceil(8 / 4) 4 = 8

cv1的内存布局如下:

图:cv1内存布局

《sizeof 知多少? (下)》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 基本类型
  • 1. 结构类型
  • 2. 结构中的结构
  • 3. union
  • 4. bit field(位域)
  • 5. alignas 和 alignof
  • 6. pragma pack etc. ?
  • 7. virtual method table (vtable)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com