【C语言进阶】C语言程序设计:动态通讯录(顺序表实现)

简介: 目录动态通讯录的实现一、实验的目的和意义二、实验内容描述三、功能描述四、数据结构1、三大模块2、结构设计3、动态开辟4、文件操作 5、主要函数五、流程图及模块算法1.Contacct 程序运行流程图 2、AddContct(增加)函数流程图 3、DelContct(删除)函数流程图 4、SearchContct(查找)函数流程图5、ModifyContct(修改)函数流程图6、SortContct(排序)函数流程图六、实验测试结果七、实验总结八、源代码1、DynamicContact.h2、DynamicC

目录

动态通讯录的实现

一、实验的目的和意义

二、实验内容描述

三、功能描述

四、数据结构

1、三大模块

2、结构设计

3、动态开辟

4、文件操作

5、主要函数

五、流程图及模块算法

1.Contacct 程序运行流程图

2、AddContct(增加)函数流程图

3、DelContct(删除)函数流程图

4、SearchContct(查找)函数流程图

5、ModifyContct(修改)函数流程图

6、SortContct(排序)函数流程图

六、实验测试结果

七、实验总结

八、源代码

1、DynamicContact.h

2、DynamicContact.c

3、test.c

最后

动态通讯录的实现

一、实验的目的和意义

1、巩固和加深对C语言知识的理解

2、学会使用编译器的各种调试

3、提高解决实际问题的能力

二、实验内容描述

通讯录,是用来存放联系人的信息,它在如今电子信息发展的社会中是不可或缺的。联系人的信息主要包括名字、性别、年龄、地址等。

本次课程设计使用C语言来实现通讯录,正常的通讯录应具备以下主要功能:

增:增加联系人

删:删除联系人

查:查找联系人

改:修改联系人的某项信息

显示:显示通讯录所有联系人的信息

排序:对通讯录中的联系人进行排序

三、功能描述

通讯录的主要功能为增加联系人、删除联系人、查找联系人、修改联系人的信息等等

下面具体描述各功能的作用:

1、增加:可以添加联系人,其信息主要包括名字、性别、年龄、电话、地址

2、删除:输入想要删除联系人的名字进行删除,联系人查找存在且删除后则提示删除成功,联系人查找不存在则提示要删除的联系人不存在,或者通讯录为空则输出通讯录已空,无法删除

3、查找:输入想要查找的联系人的名字,联系人存在则打印出联系人的全部信息,否则提示联系人不存在

4、修改:输入想要修改联系人信息的名字,先对联系人查找,联系人存在则对需要修改的信息进行修改,联系人不存在则显示联系人不存在

5、显示:显示通讯录所有联系人的信息

6、排序:对通讯录中的联系人按名字、年龄进行排序,并且显示排序后通讯录中的联系人

四、数据结构

文件名 作用
DynamicContact.h 通讯录的函数声明、头文件声明及各种声明
DynamicContact.c 通讯录函数接口的实现
test.c 通讯录函数功能测试

1、三大模块

通讯录大致分为三大模块,详情如下:


2、结构设计

定义一个结构体 PeoInfo ,结构体包括名字、性别、年龄、电话、地址,另一个结构体 Contact 嵌套着 PeoInfo 这个结构体,用于创建通讯录,类似于顺序表。

3、动态开辟


typedefstructPeoInfo{
charname[NAME_MAX];
charsex[SEX_MAX];
intage;
chartele[TELE_MAX];
charaddr[ADDR_MAX];
}PeoInfo;
typedefstructContact{
PeoInfo*data;//存放联系人的信息intcount;//通讯录中已经保存的信息个数intcapacity;//记录通讯录当前的最大容量}Contact;

3、动态开辟

1、通讯录的联系人可以用数组储存,但是考虑到空间浪费的问题,就要对需要空间的多少进行动态开辟。

1、通讯录的联系人可以用数组储存,但是考虑到空间浪费的问题,就要对需要空间的多少进行动态开辟。

1、通讯录的联系人可以用数组储存,但是考虑到空间浪费的问题,就要对需要空间的多少进行动态开辟。

比如用数组进行储存,开辟了1000个空间,但实际上只用了10个空间,剩余的空间就会造成很大大的浪费。

但是使用动态开辟空间,需要多大的空间就开辟出多大的空间,对开辟的空间不会造成很大的浪费。

这时候就要动态内存函数,通讯录中使用 malloc 函数和 realloc 函数。

2、两个内存函数的区别,想了解详细的可以去 C++ 官网查阅。

C++ 官网:

http://www.cplusplus.com/


4、文件操作

通讯录结束运行后,存储好的联系人的信息也随之销毁了。

我们想要在程序结束运行后,联系人的信息依旧保存,这时候就要运用C语言文件操作相关的知识,在程序结束运行之前把联系人保存在一个文件中,这样联系人的信息就可以存储不被销毁。

通讯录数据以二进制的方式储存。

通讯录代码中使用如下两个:

image.png

如图:contact.data 就是通讯录退出程序前保留的数据

image.png


5、主要函数

//初始化通讯录
void InitContact(Contact* p);
//销毁通讯录
void DestroyContact(Contact* p);
//添加联系人
void AddContact(Contact* p);
//删除联系人
void DelContact(Contact* p);
//查找联系人
void SearchContact(const Contact* p);
//修改联系人信息
void ModifyContact(Contact* p);
//打印联系人
void PrintContact(const Contact* p);
//排序
void SortContact(const Contact* p);
//保存通讯录的信息到文件
void SaveContact(const Contact* pc);
//加载文件信息到通讯录中
void LoadContact(Contact* p);

五、流程图及模块算法

1.Contacct 程序运行流程图

image.png

2、AddContct(增加)函数流程图

image.png

3、DelContct(删除)函数流程图

image.png

4、SearchContct(查找)函数流程图

image.png

5、ModifyContct(修改)函数流程图

image.png

6、SortContct(排序)函数流程图

image.png

画流程图使用的工具

https://www.draw.io/index.html

六、实验测试结果

声明一点,请输入字符,不要输入中文


1、通讯录代码正常运行

image.png

2、进行添加联系人,正常运行

image.png

3、对联系人进行删除,正常运行

image.png

4、对联系人进行修改,正常运行

image.png

image.png

5、对联系人进行排序,正常运行

image.png

七、实验总结

本次课程设计我选择的是设计通讯录管理系统。

总的来说,这次的课程设计使我体会较大的是应用比理论学习难得多,它涉及到各种实际问题,但是也加深了我对知识的理解和运用,也深知只有多写代码、练习等才能写出一个好的程序。


1、在代码方面,所有代码运行都没有问题,就排序函数无法运行,进行排序程序就会崩掉,进行一个下午的调试才找到代码误

进行排序就崩掉,显示 0xC0000005: 读取位置 xxx时发生访问冲突

看到错误提示,我已经想到指针发生越界了,调试一下午才找到错误,如图

image.png

2、

这次课程设计也是一次很好的对自我检查,让我知道哪一个方面存在不足,对知识的了解还不够深入,对一些学过的知识没有很好的掌握。

3、

在编写代码时需要小心谨慎,否则就会写出很多的 bug。在调试上,这次课程设计加强了我对代码的调试能力。

八、源代码

源代码 gitee 上也有,需要的可以自行查看

链接

https://gitee.com/Maple_fylqh/code/tree/master/code_c/DynamicContact/DynamicContact


1、DynamicContact.h

#pragma once#define _CRT_SECURE_NO_WARNINGS //vs 编译器需要,其他编译器不需要,可自行删去//动态版通讯录#include <stdio.h>#include <assert.h>#include <string.h>#include <stdlib.h>#include <Windows.h>//类型声明//PeoInit结构体所用#define NAME_MAX 20#define SEX_MAX 5#define TELE_MAX 12#define ADDR_MAX 30//通讯录初始状态的容量大小#define DEFAULT_SZ 3//枚举选项enumOption//test函数所用的枚举{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
PRINT};
enumModify//修改联系人所用的枚举{
EXIT0,
NAME,
SEX,
AGE,
TELE,
ADDR};
//结构体声明typedefstructPeoInfo{
charname[NAME_MAX];
charsex[SEX_MAX];
intage;
chartele[TELE_MAX];
charaddr[ADDR_MAX];
}PeoInfo;
typedefstructContact{
PeoInfo*data;//存放联系人的信息intcount;//通讯录中已经保存的信息个数intcapacity;//记录通讯录当前的最大容量}Contact;
//函数声明//初始化通讯录voidInitContact(Contact*p);
//销毁通讯录voidDestroyContact(Contact*p);
//添加联系人voidAddContact(Contact*p);
//删除联系人voidDelContact(Contact*p);
//查找联系人voidSearchContact(constContact*p);
//修改联系人信息voidModifyContact(Contact*p);
//打印联系人voidPrintContact(constContact*p);
//排序voidSortContact(constContact*p);
//保存通讯录的信息到文件voidSaveContact(constContact*pc);
//加载文件信息到通讯录中voidLoadContact(Contact*p);

2、DynamicContact.c

#include "DynamicContact.h"//排序所用菜单voidmenu2()
{
printf("********************************\n");
printf("******  1.name    2.age   ******\n");
printf("******  0.exit            ******\n");
printf("********************************\n");
}
//修改联系人所用的菜单voidmenu1()
{
printf("********************************\n");
printf("******  1.name    2.sex   ******\n");
printf("******  3.age     4.tele  ******\n");
printf("******  5.addr    0.exit  ******\n");
printf("********************************\n");
}
//检测通讯录容量voidCheckCapacity(Contact*p)
{
assert(p);
if (p->capacity==p->count)
    {
PeoInfo*tmp= (PeoInfo*)realloc(p->data, (p->capacity+2) *sizeof(PeoInfo));
if (p->data!=NULL)
        {
p->data=tmp;
        }
else        {
perror("CheckCapacity::realloc");
return;
        }
p->capacity+=2;
printf("增容成功\n");
    }
}
//初始化通讯录voidInitContact(Contact*p)
{
assert(p);
p->count=0;
p->capacity=DEFAULT_SZ;
p->data= (PeoInfo*)malloc(p->capacity*sizeof(PeoInfo));
if (p->data==NULL)
    {
perror("InitContact::malloc");
return;
    }
memset(p->data, 0, p->capacity*sizeof(PeoInfo));//把PeoInit全部初始化为0//加载文件信息到通讯录中LoadContact(p);
}
//销毁通讯录voidDestroyContact(Contact*p)
{
free(p->data);
p->data=NULL;
p->capacity=0;
p->count=0;
printf("销毁成功\n");
}
//添加联系人voidAddContact(Contact*p)
{
//检查容量CheckCapacity(p);
//录入信息printf("请输入名字:>");
scanf("%s", p->data[p->count].name);
printf("请输入性别:>");
scanf("%s", p->data[p->count].sex);
printf("请输入年龄:>");
scanf("%d", &(p->data[p->count].age));
printf("请输入电话:>");
scanf("%s", p->data[p->count].tele);
printf("请输入地址:>");
scanf("%s", p->data[p->count].addr);
p->count++;
printf("添加成功\n\n");
}
//查找,找到了返回下标,找不到返回 -1intFindName(constContact*p, charname[])
{
assert(p);
inti=0;
for (i=0; i<p->count; i++)
    {
if (0==strcmp(p->data[i].name, name))
        {
returni;
        }
    }
return-1;
}
//删除联系人voidDelContact(Contact*p)
{
assert(p);
if (0==p->count)
    {
printf("通讯录已空,无法删除\n");
return;
    }
charname[NAME_MAX];
printf("请输入要查找的名字:>");
scanf("%s", name);
intposition=FindName(p, name);//查找if (-1==position)
    {
printf("要删除的联系人不存在\n\n");
return;
    }
//删除inti=0;
for (i=position; i<p->count-1; i++)
    {
p->data[i] =p->data[i+1];
    }
p->count--;
printf("删除成功\n\n");
}
//查找联系人voidSearchContact(constContact*p)
{
assert(p);
charname[NAME_MAX];
printf("请输入要查找的名字:>");
scanf("%s", name);
intposition=FindName(p, name);//查找if (-1==position)
    {
printf("要查找的联系人不存在\n\n");
return;
    }
printf("\n-----------------------------------------------\n");
printf("%-10s %-5s %-5s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
printf("%-10s %-5s %-5d %-12s %-30s\n", p->data[position].name, 
p->data[position].sex, p->data[position].age, 
p->data[position].tele, p->data[position].addr);
printf("\n-----------------------------------------------\n\n");
}
//修改联系人信息voidModifyContact(Contact*p)
{
assert(p);
intintput=0;
charname[NAME_MAX];
printf("请输入要修改联系人的名字:>");
scanf("%s", name);
intposition=FindName(p, name);//查找if (-1!=position)
    {
printf("\n-----------------------------------------------\n");
printf("%-10s %-5s %-5s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
printf("%-10s %-5s %-5d %-12s %-30s\n", p->data[position].name, p->data[position].sex,
p->data[position].age, p->data[position].tele, p->data[position].addr);
printf("\n-----------------------------------------------\n\n");
do        {
menu1();
printf("请输入要修改的选项:>");
scanf("%d", &intput);
switch (intput)
            {
caseNAME:
printf("请修改名字:>");
scanf("%s", p->data[position].name);
printf("修改成功\n\n");
break;
caseSEX:
printf("请修改性别:>");
scanf("%s", p->data[position].sex);
printf("修改成功\n\n");
break;
caseAGE:
printf("请修改年龄:>");
scanf("%d", &(p->data[position].age));
printf("修改成功\n\n");
break;
caseTELE:
printf("请修改电话号码:>");
scanf("%s", p->data[position].tele);
printf("修改成功\n\n");
break;
caseADDR:
printf("请修改地址:>");
scanf("%s", p->data[position].addr);
printf("修改成功\n\n");
break;
caseEXIT0:
printf("退出修改\n\n");
break;
default:
printf("选择错误,请重新选择\n\n");
break;
            }
        } while (intput);
    }
else    {
printf("所要修改的联系人不存在\n\n");
return;
    }
}
//打印联系人voidPrintContact(constContact*p)
{
assert(p);
inti=0;
printf("\n-----------------------------------------------\n");
printf("%-10s %-5s %-5s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
for (i=0; i<p->count; i++)
    {
printf("%-10s %-5s %-5d %-12s %-30s\n", p->data[i].name, p->data[i].sex,
p->data[i].age, p->data[i].tele, p->data[i].addr);
    }
printf("-----------------------------------------------\n\n");
}
intcmp_name(constvoid*e1, constvoid*e2)
{
returnstrcmp(((structPeoInfo*)e1)->name, ((structPeoInfo*)e2)->name);
}
intcmp_age(constvoid*e1, constvoid*e2)
{
return (((structPeoInfo*)e1)->age- ((structPeoInfo*)e2)->age);
}
//排序voidSortContact(constContact*p)
{
assert(p);
intintput=0;
do    {
menu2();
printf("请选择需要排序的选项:>");
scanf("%d", &intput);
switch (intput)
        {
case1:
qsort(p->data, p->count, sizeof(structPeoInfo), cmp_name);
printf("按名字排序成功\n\n");
break;
case2:
qsort(p->data, p->count, sizeof(structPeoInfo), cmp_age);
printf("按年龄排序成功\n\n");
break;
case0:
printf("退出排序\n\n");
break;
default:
printf("选择错误,请重新选择\n\n");
break;
        }
    } while (intput);
}
//保存通讯录的信息到文件voidSaveContact(constContact*p)
{
//打开并创建文件FILE*pf=fopen("contact.data.txt", "w");//w:只写,if (pf==NULL)
    {
perror("SaveContact::fopen");
return;
    }
//写文件inti=0;
for (i=0; i<p->count; i++)
    {
fwrite(p->data+i, sizeof(PeoInfo), 1, pf);
    }
//关闭文件fclose(pf);
pf=NULL;
}
//加载文件信息到通讯录中voidLoadContact(Contact*p)
{
//打开文件FILE*pf=fopen("contact.data.txt", "r");//r:只读if (pf==NULL)
    {
perror("LoadContact::fopen");
return;
    }
//读文件PeoInfotmp= { 0 };
while (fread(&tmp, sizeof(PeoInfo), 1, pf))
    {
CheckCapacity(p);
p->data[p->count] =tmp;
p->count++;
    }
//关闭文件fclose(pf);
pf=NULL;
}

3、test.c

#include "DynamicContact.h"voidmenu()
{
printf("================================\n");
printf("*********** Contact ************\n");
printf("================================\n");
printf("***     1.add     2.del      ***\n");
printf("***     3.search  4.modify   ***\n");
printf("***     5.sort    6.print    ***\n");
printf("***     0.exit               ***\n");
printf("================================\n");
}
voidtest()
{
intintput=0;
Contactcon;//创建通讯录InitContact(&con);//初始化通讯录do    {
menu();
printf("请选择:>");
scanf("%d", &intput);
switch (intput)
        {
caseADD:
AddContact(&con);
break;
caseDEL:
DelContact(&con);
break;
caseSEARCH:
SearchContact(&con);
break;
caseMODIFY:
ModifyContact(&con);
break;
caseSORT:
SortContact(&con);
break;
casePRINT:
PrintContact(&con);
break;
caseEXIT:
SaveContact(&con);//销毁通讯录之前把数据存入文件中DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("输入错误,请重新输入\n\n");
break;
        }
    } while (intput);
}
intmain()
 {
test();
return0;
}

最后

文章到这就结束了,希望对你有帮助,觉得文章不错就点个赞吧。

文章有什么问题可以留言,感谢支持!!

相关文章
|
7天前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
46 16
|
11天前
|
算法 C语言
【C语言程序设计——循环程序设计】求解最大公约数(头歌实践教学平台习题)【合集】
采用欧几里得算法(EuclideanAlgorithm)求解两个正整数的最大公约数。的最大公约数,然后检查最大公约数是否大于1。如果是,就返回1,表示。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。作为新的参数传递进去。这个递归过程会不断进行,直到。有除1以外的公约数;变为0,此时就找到了最大公约数。开始你的任务吧,祝你成功!是否为0,如果是,那么。就是最大公约数,直接返回。
65 18
|
11天前
|
Serverless C语言
【C语言程序设计——循环程序设计】利用循环求数值 x 的平方根(头歌实践教学平台习题)【合集】
根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码,求解出数值x的平方根;运用迭代公式,编写一个循环程序,求解出数值x的平方根。注意:不能直接用平方根公式/函数求解本题!开始你的任务吧,祝你成功!​ 相关知识 求平方根的迭代公式 绝对值函数fabs() 循环语句 一、求平方根的迭代公式 1.原理 在C语言中,求一个数的平方根可以使用牛顿迭代法。对于方程(为要求平方根的数),设是的第n次近似值,牛顿迭代公式为。 其基本思想是从一个初始近似值开始,通过不断迭代这个公式,使得越来越接近。
42 18
|
11天前
|
C语言
【C语言程序设计——循环程序设计】统计海军鸣放礼炮声数量(头歌实践教学平台习题)【合集】
有A、B、C三艘军舰同时开始鸣放礼炮各21响。已知A舰每隔5秒1次,B舰每隔6秒放1次,C舰每隔7秒放1次。编程计算观众总共听到几次礼炮声。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。开始你的任务吧,祝你成功!
43 13
|
7天前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
19 3
|
11天前
|
存储 安全 C语言
【C语言程序设计——选择结构程序设计】预测你的身高(头歌实践教学平台习题)【合集】
分支的语句,这可能不是预期的行为,这种现象被称为“case穿透”,在某些特定情况下可以利用这一特性来简化代码,但在大多数情况下,需要谨慎使用。编写一个程序,该程序需输入个人数据,进而预测其成年后的身高。根据提示,在右侧编辑器补充代码,计算并输出最终预测的身高。分支下的语句,提示用户输入无效。常量的值必须是唯一的,且在同一个。语句的作用至关重要,如果遗漏。开始你的任务吧,祝你成功!,程序将会继续执行下一个。常量都不匹配,就会执行。来确保程序的正确性。
36 10
|
11天前
|
小程序 C语言
【C语言程序设计——基础】顺序结构程序设计(头歌实践教学平台习题)【合集】
目录 任务描述 相关知识 编程要求 测试说明 我的通关代码: 测试结果: 任务描述 相关知识 编程编写一个程序,从键盘输入3个变量的值,例如a=5,b=6,c=7,然后将3个变量的值进行交换,使得a=6,b=7,c=5。面积=sqrt(s(s−a)(s−b)(s−c)),s=(a+b+c)/2。使用输入函数获取半径,格式指示符与数据类型一致,实验一下,不一致会如何。根据提示,在右侧编辑器补充代码,计算并输出圆的周长和面积。
32 10
|
7天前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
11 2
|
11天前
|
存储 C语言
【C语言程序设计——循环程序设计】利用数列的累加和求 sinx(头歌实践教学平台习题)【合集】
项的累加和,一般会使用循环结构,在每次循环中计算出当前项的值(可能基于通项公式或者递推关系),然后累加到一个用于存储累加和的变量中。在C语言中推导数列中的某一项,通常需要依据数列给定的通项公式或者前后项之间的递推关系来实现。例如,对于一个简单的等差数列,其通项公式为。的级数,其每一项之间存在特定的递推关系(后项的分子是其前项的分子乘上。,计算sinx的值,直到最后一项的绝对值小于。为项数),就可以通过代码来计算出指定项的值。对于更复杂的数列,像题目中涉及的用于近似计算。开始你的任务吧,祝你成功!
29 6
|
11天前
|
C语言
【C语言程序设计——循环程序设计】鸡兔同笼问题(头歌实践教学平台习题)【合集】
本教程介绍了循环控制和跳转语句的使用,包括 `for`、`while` 和 `do-while` 循环,以及 `break` 和 `continue` 语句。通过示例代码详细讲解了这些语句的应用场景,并展示了如何使用循环嵌套解决复杂问题,如计算最大公因数和模拟游戏关卡选择。最后,通过鸡兔同笼问题演示了穷举法编程的实际应用。文中还提供了编程要求、测试说明及通关代码,帮助读者掌握相关知识并完成任务。 任务描述:根据给定条件,编写程序计算鸡和兔的数量。鸡有1个头2只脚,兔子有1个头4只脚。
36 5

热门文章

最新文章