前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++打怪升级(四)- 类和对象入门1

C++打怪升级(四)- 类和对象入门1

作者头像
怠惰的未禾
发布2023-04-27 21:45:07
4770
发布2023-04-27 21:45:07
举报
文章被收录于专栏:Linux之越战越勇

前言

类的引入,是C++面向对象特点的基础。那么类是什么呢?看完本篇相信你就会知道。


引子

C语言中的结构体

C语言是面向过程的结构化和模块化的编程语言,关注的是过程,通过具体的步骤,一步一步解决问题。 C++语言是基于面向对象的,关注的是对象,通过将一件事情拆分成不同的对象,靠对象之间的交互解决问题。 在C语言中,有者和类相似的概念 - 结构体。 我们可以在C语言中创建不同的结构体类型,通常是把一些变量封装在结构体中,抽象为一个新类型。 比如C语言实现栈(部分):

在C语言中结构体中只封装了数据成员(变量),具体的功能实现(函数)在结构体外部。数据成员和函数实现之间是分开的、相互独立的。 这样就会产生一些问题: 实现相同的功能,代码一般较长,即实现比较麻烦; 往往涉及大量的指针操作,这非常容易出现意料之外的错误,使得我们必须非常小心。 结构体没有对使用者做出任何限制,太自由了。我们很多时候是不希望直接操作结构体里的数据的,使用者可能会选择不调用对应的功能函数而直接操作结构体里的数据,极有可能使用者并没有注意到实现的细节就直接使用结构体变量中的数据,非常容易导致出错。 C++语言则引入了类的概念,改进C语言存在的问题,并用类实现了面向对象的操作。

C++中的结构体

C++从C而来,可以兼容C语言代码,C语言所写的结构体在C++中也支持,体现了C++语言的向前兼容。 同时,C++对C中的结构体struct进行了扩展和升级,struct结构体具有了和C++中类class基本相同的功能。

代码语言:javascript
复制
//升级的struct,与 类 class的功能相同
//C语言用法 - 结构体
struct ListNode {
	int val;
	struct ListNode* next;
};

//C++用法 - 类;C语言不能这么用(C的语法不支持)
struct ListNode {
	int val;
	ListNode* next;
};

于是,数据结构栈的写法可以变成下面的写法

C++中的结构体struct为了和C语言中的结构体struct兼容,在没有访问限定符时,默认是成员变量和成员函数公共的。 C++中的类class则没有这个包袱,在没有访问限定符时类的成员变量和成员函数是私有的。


类的定义

关键字class,后接一个类名chassName(标识符),接着是一对花括号{}括起来的类体,最后以分号;结束。 类体中的内容称为类的成员: 类体中的变量称为成员变量,也叫作做的属性; 类中的函数称为成员函数,也叫做类的方法。

代码语言:javascript
复制
class className{
//类体:成员函数 + 成员变量
};

类的定义方式

声明和定义全部放在类体中

在类中定义的成员函数编译器默认其为内联函数

代码语言:javascript
复制
class Stack {
public:
	void Init(size_t capacity = 4) {
		_array = (int*)malloc(sizeof(int) * capacity);
		if (_array == nullptr) {
			perror("Init file");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}

	void Destroy() {
		free(_array);
		_array = nullptr;
		_top = _capacity = 0;
	}

	void Push(int val) {
		if (_top == _capacity) {
			size_t newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_array, sizeof(int) * newcapacity);
			if (tmp == nullptr) {
				perror("Push flie");
				exit(-1);
			}
			_array = tmp;
			_capacity = newcapacity;
		}
		_array[_top] = val;
		++_top;
	}
	void Pop() {
		--_top;
	}
	int Top() {
		return _array[_top - 1];
	}
	bool Empty() {
		return _top == 0;
	}

private:
	int* _array;//指向数组
	size_t _top;//栈顶的下一个位置
	size_t _capacity;//栈容量
};

类的声明和定义都放在类中,这比较好理解,但是有一个问题:类中的成员函数比较少还可以这么整,但当类中成员函数较多时类就显得臃肿不堪了,也不方便去对类进行和调试,不能直观的分析类的功能。


声明和定义分离

类中的成员函数 statement.h

代码语言:javascript
复制
class Stack {
public:
	//缺省值能声明或定义一处给出
	void Init(size_t capacity = 4);
	inline void Destroy();
	bool Empty();
	
private:
	int* _array;
	size_t _top;
	size_t _capacity;
};

class Queue {
public:
	void Init();
	void Destroy();
	bool Empty();

private:
	typedef struct ListNode {
		int val;
		struct ListNode* next;
	}LTNode;
	LTNode* _head;
	LTNode* _tail;
	size_t _size;
};

define.cpp

代码语言:javascript
复制
#include "statement.h"
//这里需要 加域作用限定符 指定属于哪个类
void Stack::Init(size_t capacity) {
	_array = (int*)malloc(sizeof(int) * capacity);
	if (_array == nullptr) {
		perror("Init file");
		exit(-1);
	}
	_top = 0;
	_capacity = capacity;
}

void Stack::Destroy() {
	free(_array);
	_array = nullptr;
	_top = _capacity = 0;
}

bool Stack::Empty() {
	return _top == 0;
}


void Queue::Init() {
	_head = _tail = nullptr;
	_size = 0;
}

void Queue::Destroy() {
	while (!Empty()) {
		LTNode* del = _head;
		_head = _head->next;
		free(del);
	}
	_head = _tail = nullptr;
	_size = 0;
}
bool Queue::Empty() {
	return _head == nullptr;
}

类中成员函数声明和定义分离的好处: 类体中的代码大量减少,只保留了成员函数的声明,需要时可以不看成员函数具体实现,通过快速在类中浏览成员函数的声明就可以迅速了解类的大致功能,方便他人也方便自己。 定义与声明分离,也可以保护代码,防止函数实现被修改,避免源码的泄露。 定义静态库或动态库,只提供接口给使用者,从而隐藏具体的实现细节。 ** 类外成员函数实现的一个错误:** 原因是编译器不知道函数Init()到底是属于哪个类的。 没有指定查找的地方时,编译器默认首先在函数内部局部域查找,找不到再去全局域查找,再找不到就报错了。

需要注意的是: 类外的成员函数在具体实现时,在函数开始需要使用对应的类名className域作用限定符::对成员函数进行修饰限定,这是为了说明该成员函数是属于哪个类的,防止编译器发生混淆。

指定查找的地方时,编译器首先去函数内部局部域查找,再去指定的类作用域查找,找不到再去全局域查找,再找不到就报错。


关于C和C++声明和定义的一点说明

C语言要求变量和函数需要先声明或定义再使用,这是因为C语言中各个部分是相对独立的,程序又是顺序执行的,只能向上寻找,C++中的类class则与之不同。

类中的成员函数和成员变量定义和声明的先后位置是没有要求的,这是因为类是一个作用域,在类内的成员变量和成员函数是一个有机的整体,当需要使用类内的某个变量或函数时,会在类中所有地方寻找,而不是在使用的地方之前寻找。


类中成员变量的命名习惯

先来看一个例子:

代码语言:javascript
复制
class Date {
public:
	void Init(int year, int month, int day) {
		year = year;
		month = month;
		day = day;
	}
    void Print() {
		cout << year << "/" << month << "/" << day << endl;
	}
private:
	int year;
	int month;
	int day;
};

形参与类内成员函数存在同名的情况,这可能会导致一些问题。

解决方法1:类内成员变量前加上域作用限定符修饰

方法2:类内成员变量定义时,对变量名进行手动修饰,如:加上前缀、后缀、大小写等。目的是区分变量和传入的形参。

代码语言:javascript
复制
class Date {
public:
	void Init(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	//_day 或 day_ 或 m_day 或 mDay等
};
int main() {
	Date d;
	d.Init(2122, 10, 10);
	d.Print();
}

类的访问限定符和封装

访问限定符

分类

访问限定符分为

公有 :public 私有:private 保护:protected

代码语言:javascript
复制
class A {
public:
	int a;
private:
	int b;
protected:
	int c;
};

int main() {
	A A1;
	return 0;
}

public修饰的成员在类外可以直接访问;

protectedprivate修饰的成员在类外不能被直接访问;

访问权限作用域从该访问限定符开始直到下一个访问限定符出现为止;如果后面没有访问限定符,作用域就到}结束;也就是说,域作用限定符把类作用域分隔开了,形成一个个属性不同的小作用域

C++中struct和class的主要区别:

class的默认访问权限为private,而struct默认访问权限为publicstruct可以当作结构体使用,也可以作为类使用。 这是为了兼容C语言中的struct的使用,C语言中结构体的变量可以直接在结构体外使用,相当于是公共public的。

访问限定符只有在编译时起作用(所以挑战访问限定符时在编译期间产生的是编译错误,由编译器控制),当数据映射到内存后,没有任何访问限定符上的区别。


封装

首先我们直到面向对象的三大特征:封装、继承和多态。

封装的概念

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性实现细节,仅对外公开接口用以和对象进行交互。 封装本质上是一种对数据和方法的管理,使用者因此可以方便的使用类。 C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。


类的作用域

类定义了一个新的作用域类的所有成员都在类的作用域中。 在类体外定义成员时,需要使用作用域操作符::指明成员属于哪个类域。 这里有一个问题: 这里可以像命名空间域那样访问命名空间成员那样,使用域作用限定符::访问某个类域中的某个成员吗?

答案是不能。

命名空间中变量或函数等是已经定义的,有着储存空间,是一个实际对象;而类只是一种类型 - 类类型,不占任何储存空间,不是一个实际的对象,只有在类实例化 - 定义了类对象后,才能访问到类对象内部成员。


类的实例化

类的实例化:用类类型创建对象的过程

类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没 有分配实际的内存空间来存储它; 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量 ** 类实例化出对象就像现实中使用建筑设计图建造出房子**,类就像是设计图,类只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间


引子:

来看一个简单的日期类:

代码语言:javascript
复制
class A {
public:
	void Init(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {

	A a;
	a.Init(2122, 10, 3);
	a.Print();

	A b;
	b.Init(3122, 10, 3);
	b.Print();
}

我们思考一个问题:日期类Date的成员函数Init()和Print()都只有一个地址,不同的类对象调用Init()函数时,成员函数Init()怎么区分到底是哪一个对象调用的呢?它应该初始化那个对象呢? C++中引入了this指针解决了这个问题:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数this,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有对成员变量 的操作,都是通过该指针去访问操作的。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。


this指针特性

this指针类型

类类型* const 因此,this指针本身是不能被修改的,是指针常量,而可以修改this指针指向的对象,这也与成员变量的修改相呼应,即成员变量是通过this指针改变的。

this指针只能在成员函数的内部使用,这是因为this是以成员函数形参的形式接受实参类对象的地址的,在成员函数栈帧创建时保存在成员函数的栈帧中。

this指针本质上是成员函数的形参,当对象调用成员函数时,将类对象的地址作为实参传递给this形参,所以对象中不存储this指针 。

代码语言:javascript
复制
class A {
public:
	void Init(int year, int month, int day) {
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print() {
		cout << "this: " << this << endl;
		//cout << _year << "/" << _month << "/" << _day << endl;
		cout << this->_year << "/" 
			<< this->_month << "/" << this->_day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {

	A a;
	a.Init(2122, 10, 3);
	a.Print();
	cout << "&a: " << &a << endl;

	A b;
	b.Init(3122, 10, 3);
	b.Print();
	cout << "&b: " << &b << endl;
	return 0;
}

this指针成员函数的第一个隐含的指针形参,一般不需要由用户传递,而是由编译器通过ecx寄存器自动传递。


类对象模型

类对象储存方式的思考

首先需要明确: 不同的类对象每次调用相同的成员函数时,该成员函数的地址都是相同的,也就是说,程序运行期间,函数代码转换成的二进制指令只有一份,储存在某处供不同类对象调用。 类对象如果保存成员函数,实际上保存的是成员函数的地址。 而不同的类对象的相同的成员变量则是完全不同的,不同对象成员变量最多值是相等的,地址一定是不相同,因为不同的类对象有着有系统分配的独属于自己的储存空间,而对象的成员变量则分别在自己的储存空间中,这与成员函数不同。

    1. 对象中包含类的各个成员

思路1::每一个类对象即保存成员变量,也保存成员函数的地址。 相同的函数地址被多次保存,由此产生的空间浪费不可忽视。 这种思路被舍弃

    1. 类对象存放成员变量,成员函数共同存放在一个地方,形成函数表,而类对象只保存函数表的地址

思路2:每一个类对象除了都保存必要的成员变量之外,就只保存了类成员函数的函数表的地址,相比思路1,空间已经节约很多了。类函数表把类中的成员函数都放在且在内存中的某块空间而形成的。找到类函数表的地址,就可以找到对应的类成员函数了。

    1. 类对象只存放成员变量,成员函数共同存放在公共代码区

虽然思路2的空间已经节约很多了,但是还是存在着额外的空间占用,即类函数表地址的存放,所以还需要更完美的改进。 **思路3:**类对象中只存放成员变量的大小,类的总大小就是类对象所有成员变量的大小。而类对象的成员函数全部存放到了内存的公共代码区(常量区),这样当类对象调用类成员函数时,编译器直接去公共代码区去寻找待调用的成员函数即可。 在公共代码区存放的成员函数编译器直接就能够找到,不需要类对象自己保存类函数表地址然后自己寻找了。

所以结果显而易见,思路3被保留了下来:类对象中只存放成员变量的大小,类对象的成员函数全部存放到了内存的公共代码区(常量区)。


计算类对象的大小

普通类

一个类对象中可能存放着不同类型变量和许多成员函数。那么类的实例(对象)的大小是多少呢? 其实,类对象的大小的计算和C语言中计算结构体变量的大小是相同的,都需要考虑内存对齐。 在计算类对象大小时,注意到类与C语言中结构体不同的是类域中有成员函数,那么类域中成员函数占不占类对象的大小呢?

答案是不占,类域中成员函数被统一放在了公共代码区(常量区),所以类中只考虑所以成员变量所占空间的大小即可。


空类或空成员函数类
代码语言:javascript
复制
class A {
	void func2() {
		;
	}
};
//空类
class B{
};
//类储存方式的选择:
//类大小的计算:与C语言结构体相同
int main() {

	A A1;
	//不储存有效数据,占位,标识对象存在
	cout << sizeof(A1) << endl;
	B B1;
	//不储存有效数据,占位,标识对象存在
	cout << sizeof(B1) << endl;
	return 0;
}

空类和没有成员函数的类的对象大小是1byte,而不是0byte; 这一个字节大小不储存有效数据,而是占位,标识该对象存在,用以区分没有类的情况0byte


结构体内存对齐

  1. 第一个成员在与结构体偏移量为0的地址处;
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍;
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍;

this指针

引子:

来看一个简单的日期类:

代码语言:javascript
复制
class A {
public:
	void Init(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {

	A a;
	a.Init(2122, 10, 3);
	a.Print();

	A b;
	b.Init(3122, 10, 3);
	b.Print();
}

我们思考一个问题:日期类Date的成员函数Init()和Print()都只有一个地址,不同的类对象调用Init()函数时,成员函数Init()怎么区分到底是哪一个对象调用的呢?它应该初始化那个对象呢?

C++中引入了this指针解决了这个问题:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数this,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有对成员 变量的操作,都是通过该指针去访问操作的。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。


this指针特性

this指针类型

类类型* const 因此,this指针本身是不能被修改的,而可以修改this指针指向的对象,这也与成员变量的修改相呼应,即成员变量是通过this指针改变的。

this指针只能在成员函数的内部使用,这是因为this是以成员函数形参的形式接受实参类对象的地址的,在成员函数栈帧创建时保存在成员函数的栈帧中。

this指针本质上是成员函数的形参,当对象调用成员函数时,将类对象的地址作为实参传递给this形参,所以对象中不存储this指针 。

代码语言:javascript
复制
class A {
public:
	void Init(int year, int month, int day) {
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print() {
		cout << "this: " << this << endl;
		//cout << _year << "/" << _month << "/" << _day << endl;
		cout << this->_year << "/" 
			<< this->_month << "/" << this->_day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {

	A a;
	a.Init(2122, 10, 3);
	a.Print();
	cout << "&a: " << &a << endl;

	A b;
	b.Init(3122, 10, 3);
	b.Print();
	cout << "&b: " << &b << endl;
	return 0;
}

this指针成员函数的第一个隐含的指针形参,一般不需要由用户传递,而是由编译器通过ecx寄存器自动传递。

为什么要设计出this指针?

这还是要从C语言说起: C语言实现数据结构时,比如实现一个栈,首先需要创建一个栈的结构体类型

代码语言:javascript
复制
typedef struct Stack{ 
//...
}Stack;

在定义栈的功能函数时往往需要传入栈实例的地址

代码语言:javascript
复制
void StackInit(Stack* pst) { };
void StackPush(Stack* pst) { };

既然每个栈的函数都需要入栈实例的地址,前人在设计类时考虑了这一点,C++中在实现类成员函数时就把数据结构实例的地址默认传入了,该地址就被隐藏起来了,对该地址的使用也隐藏起来了。也就是说变成了编译器帮助使用者完成对象(实例)地址的传入,即编译器做的事增多了,使用者要做的事变少了。


C语言和C++实现数据结构比较

C++语言实现数据结构更有优势和方便。

C实现栈

代码语言:javascript
复制
typedef int STDataType;
struct Stack {
	STDataType* val;
	int top;
	int capacity;
};
//初始化
void StackInit(struct Stack* pst) {
	assert(pst);
	pst->val = NULL;
	pst->top = 0;
	pst->capacity = 0;
}
//压栈
void StackPush(struct Stack* pst, int val) {
	assert(pst);
	if (pst->top == pst->capacity) {
		int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(pst->val, sizeof(STDataType) * newcapacity);
		if (tmp == nullptr) {
			perror("realloc file");
			exit(-1);
		}
		pst->val = tmp;
		pst->capacity = newcapacity;
	}
	pst->val[pst->top] = val;
	++(pst->top);
}
//出栈
void StackPop(struct Stack* pst) {
	assert(pst);
	--(pst->top);
}
//取栈顶元素
int StackTop(struct Stack* pst) {
	assert(pst);
	return pst->val[pst->top - 1];
}
//是否为空
bool StackEmpty(struct Stack* pst) {
	assert(pst);
	return pst->top == 0;
}
//销毁栈
void StackDestroy(struct Stack* pst) {
	assert(pst);
	free(pst->val);
	pst->top = pst->capacity = 0;
}

对于数据结构栈,在用C语言实现时,Stack相关操作函数有以下共性:

  • 每个函数的第一个参数都是Stack*
  • 函数中必须要对第一个参数检测,因为该参数可能会为NULL
  • 函数中都是通过Stack*参数操作栈的;
  • 调用时必须传递Stack结构体变量的地址;
  • 结构体中只能定义存放数据的结构,操作数据的方法(函数)不能放在结构体中,即数据和操作数据 的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出 错 ;
  • 这也说明了this指针不是凭空而来,正是在C语言熟练的基础上,前人才引入了this指针,帮助我们更方便的使用C++来写数据结构的代码。

C++实现栈

代码语言:javascript
复制
class Stack {
public:
	//栈初始化
	void Init(int capacity = 4) {
		_val = (int*)malloc(sizeof(int) * capacity);
		if (_val == nullptr) {
			perror("Init file");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//压栈
	void Push(int val) {
		if (_top == _capacity) {
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			int* tmp = (int*)realloc(_val, sizeof(int) * newcapacity);
			if (tmp == nullptr) {
				perror("realloc file");
				exit(-1);
			}
			_val = tmp;
			_capacity = newcapacity;
		}
		_val[_top] = val;
		++(_top);
	}
	//出栈
	void Pop() {
		--(_top);
	}
	//取栈顶元素
	int Top() {
		return _val[_top - 1];
	}
	//栈是否为空
	bool Empty() {
		return _top == 0;
	}
	//销毁栈
	void Destroy() {
		free(_val);
		_val = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _val;
	int _top;
	int _capacity;
};

C++中通过类可以将数据 以及 操作数据的方法(函数)进行配合,通过访问权限可以控制那些方法在 类外可以被调用,即封装,在使用时就像使用自己的成员一样。 每个方法不需要传递Stack*的参数了,由编译器自动传递给隐式的this指针,编译器编译之后该参数会自动还原,即C++中 Stack *参数是编译器维护的,C语言中需用用户自己维护。``


结语

本节最主要介绍类的基本概念,并与结构体进行了比较。 下次再见!


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 引子
    • C语言中的结构体
      • C++中的结构体
      • 类的定义
        • 类的定义方式
          • 声明和定义全部放在类体中
          • 声明和定义分离
          • 关于C和C++声明和定义的一点说明
          • 类中成员变量的命名习惯
      • 类的访问限定符和封装
        • 访问限定符
          • 分类
          • C++中struct和class的主要区别:
        • 封装
          • 封装的概念
      • 类的作用域
      • 类的实例化
        • 引子:
          • this指针特性
            • this指针类型
        • 类对象模型
          • 类对象储存方式的思考
            • 计算类对象的大小
            • 结构体内存对齐
        • this指针
          • 引子:
            • this指针特性
              • this指针类型
            • 为什么要设计出this指针?
            • C语言和C++实现数据结构比较
            • 结语
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档