Linux下的C编程实战_linux下的c编程实战

其他范文 时间:2020-02-27 10:50:56 收藏本文下载本文
【www.daodoc.com - 其他范文】

Linux下的C编程实战由刀豆文库小编整理,希望给你工作、学习、生活带来方便,猜你可能喜欢“linux下的c编程实战”。

Linux下的C编程实战(可编辑)

Linux 下的C 编程实战 ――开发平台搭建

引言

Linux 操作系统在服务器领域的应用和普及已经有较长的历史 这源于它的开源特点以及其超越 Windows 的安全性和稳定性而近年 来 操作系统在嵌入式系统领域的延伸也可谓是如日中天许多

版本的嵌入式Linux 系统被开发出来如ucLinuxRTLinuxARM-Linux 等 等

在嵌入式操作系统方面 Linux 的地位是不容怀疑的它开源它包 含TCPIP 协议栈它易集成GUI 鉴于 Linux 操作系统在服务器和嵌入式系统领域愈来愈广泛的 应用社会上越来越需要基于Linux 操作系统进行编程的开发人员 浏览许多论坛经常碰到这样的提问现在是不是很流行unixlinux 下的c 编程所以想学习一下但是不知道该从何学起如何下手有什 么好的建议吗各位高手哪些书籍比较合适初学者在深入浅出的 过程中应该看哪些不同层次的书比如好的网站论坛请大家赐教不慎 感激

鉴于读者的需求在本文中笔者将对Linux 平台下C 编程的几个方 面进行实例讲解并力求回答读者们关心的问题以与读者朋友们进行 交流

共同提高在本文的连载过程中有任何问题或建议您可以给笔者 发送email21cnbao21cncom 您也可以进入笔者的博客参与讨论 cnbao 笔者建议在PC 内存足够大的情况下不要直接安装 Linux 操作系 统最好把它安装在运行VMWare 虚拟机软件的Windows 平台上如下图

在 Linux 平台下可用任意一个文本编辑工具编辑源代码但笔者 建议使用emacs 软件它具备语法高亮版本控制等附带功能如下图

GCC 编译器

GCC 是Linux 平台下最重要的开发工具它是 GNU 的 C 和 C 编译

器其基本用法为 [options] [filenames] 为编译选项

GCC 总http://www.daodoc.com/共提供的编译选项超过100 个但只有 少数几个会被频繁使用我们仅对几个常用选项进行介绍 假设我们编译一输出Hello World 的程序 Filenamehelloworldc

printf “Hello World”

最简单的编译方法是不指定任何编译选项 helloworldc 它会为目标程序生成默认的文件名aout 我们可用-o 编译选项来

为将产生的可执行文件指定一个文件名来代替aout 例如将上述名为 的 C 程序编译为名叫 helloworld 的可执行文件需

要输入如下命令 –o helloworld helloworldc c 选项告诉 GCC 仅把源代码编译为目标代码而跳过汇编和连接 的步骤

S 编译选项告诉 GCC 在为 C 代码产生了汇编语言文件后停止 编译 GCC 产生的汇编语言文件的缺省扩展名是 s 上述程序运行如下 命令 –S helloworldc 将生成helloworldc 的汇编代码使用的是ATT 汇编用emacs 打开 汇编代码如下图

E 选项指示编译器仅对输入文件进行预处理当这个选项被使用 时预处理器的输出被送到标准输出默认为屏幕而不是储存在文件里 O 选项告诉 GCC 对源代码进行基本优化从而使得程序执行地更 快而-O2 选项告诉 GCC 产生尽可能小和尽可能快的代码使用-O2 选项 编译的速度

比使用-O 时慢但产生的代码执行速度会更快

g 选项告诉 GCC 产生能被 GNU 调试器使用的调试信息以便调试 你的程序可喜的是在GCC 里我们能联用-g 和-O 产生优化代码

pg 选项告诉 GCC 在你的程序里加入额外的代码执行时产生用的剖析信息以显示你的程序的耗时情况 GDB 调试器

GCC 用于编译程序而 Linux 的另一个 GNU 工具 gdb 则用于调试://www.daodoc.com/par程序gdb 是一个用来调试C 和C 程序的强力调试器我们能通过它进行 一

系列调试工作包括设置断点观查变量单步等

其最常用的命令如下 装入想要调试的可执行文件 终止正在调试的程序 列表显示源代码 执行一行源代码但不进入函数内部 执行一行源代码而且进入函数内部 执行当前被调试的程序 终止gdb 监视一个变量的值 在代码里设置断点程序执行到这里时挂起 不退出gdb 而重新产生可执行文件 不离开gdb 而执行shell 下面我们来演示怎样用GDB 来调试一个求012399 的程序

Filenamesumc

int i sum sum 0 for i 0 i 100 i sum i

printf “the sum of 12 is d” sum

执行如下命令编译sumc 加-g 选项产生debug 信息 –g –o sum sumc 在命令行上键入gdb sum 并按回车键就可以开始调试sum 了再运 行run 命令执行sum 屏幕上将看到如下内容

命令 命令用于列出源代码对上述程序两次运行list 将出现如下 画面源代码被标行号

根据列出的源程序如果我们将断点设置在第 5 行只需在 gdb 命 令行提示符下键入如下命令设置断点 gdb break 5 执行情况如下图 这个时候我们再run 程序会停止在第5 行如下图

设置断点的另一种语法是 break 它在进入指定函数 function 时停住

相反的clear 用于清除所有的已定义的断点clear 清除设置在函数上的断点 clear 则清除设置在指定行上的断点 ://www.daodoc.com/par 命令 命令用于观查变量或表达式的值我们观查sum 变量只需要 运行watch sum 为表达式变量 expr 设置一个观察点一量表达式值有变化时 程序会停止执行

要观查当前设置的watch 可以使用info watchpoints 命令 命令 用于单步执行在执行的过程中被 watch 变量的变化情

况将实时呈现 分别显示Old value 和New value 如下图 命令的区别在于step 遇到函数调用会跳转到到该函数

定义的开始行去执行而 next 则不进入到函数内部它把函数调用语句 当作

一条普通语句执行

Make 是所有想在 Linux 系统上编程的用户必须掌握的工具对于 任何稍具规模的程序我们都会使用到make 几乎可以说不使用make 的

程序不具 备任何实用价值

在此我们有必要解释编译和连接的区别编译器使用源码文件来 产生某种形式的目标文件 object files 在编译过程中外部的符号 参考

并没有被解释或替换即外部全局变量和函数并没有被找到因此

在编译阶段所报的错误一般都是语法错误而连接器则用于连接目标 文

件和程序包生成一个可执行程序在连接阶段一个目标文件中对 别的文件中的符号的参考被解释如果有符号不能找到会报告连接错 误

编译和连接的一般步骤是第一阶段把源文件一个一个的编译成 目标文件第二阶段把所有的目标文件加上需要的程序包连接成一个 可执行文

件这样的过程很痛苦我们需要使用大量的gcc 命令

而 make 则使我们从大量源文件的编译和连接工作中解放出来综 合为一步完成 GNU Make 的主要工作是读进一个文本文件称为这 个文件记录了哪些文件目的文件目的文件不一定是最后的可执 行程序它可以是任何一种文件由哪些文件依靠文件产生用什么命

令来产http://www.daodoc.com/生Make 依靠此makefile 中的信息检查磁盘上的文件如果

目的文件的创建或修改时间比它的一个依靠文件旧的话 make 就执行 相应的命令以便更新目的文件

假设我们写下如下的三个文件addh 用于声明add 函数addc 提供 两个整数相加的函数体而mainc 中调用add 函数 filenameaddh int add int i int j filenameaddc add int i int j

return i j

filenamemainc “addh” int a b a 2 b 3 printf “the sum of ab is d” add a b

怎样为上述三个文件产生makefile 呢如下

注意分割符为TAB 键

上述makefile 利com 行gcc-c addc-o com 码利用com 行gcc c mainc-o 码最后利用com 两个模块的目标代码执行gcc maino addo-o 命令产生可执行文件

我们可在makefile 中加入变量另外环境变量在make 过程中也被 解释成make 的变量这些变量是大小写敏感的一般使用大写字母Make 变

量可以做很多事情例如 存储一个文件名列表 存储可执行文件名 存储编译器选项 要定义一个变量只需要在一行的开始写下这个变量的名字后面 跟一个 号再跟变量的值引用变量的方法是写一个符号后面跟变量 名我们把前面的 makefile 利用变量重写一遍并假设使用-Wall O –g 编译选

maino

addo

gcc http://www.daodoc.com/-Wall-O-g OBJS CC OBJS-o test mainc addh CC CFLAGS-c mainc-o maino addc addh CC CFLAGS-c addc-o addo 中还可定义清除 clean 目标可用来清除编译过程中产 生的中间文件例如在上述makefile 文件中添加下列代码-f o 运行make clean 时将执行 rm-f o 命令删除所有编译过程中产 生的中间文件

不管怎么说自己动手编写 makefile 仍然是很复杂和烦琐的而且 很容易出错因此GNU 也为我们提供了Automake 和Autoconf 来辅助快 速自动

产生makefile 读者可以参阅相关资料 小结

本章主要阐述了Linux 程序的编写编译调试方法及 make 实际上 就是引导读者学习怎样在Linux 下编程为后续章节做好准备 下的C 编程实战二 ――文件系统编程

Linux 文件系统

Linux 支 持 多 种 文 件 系 统 如minixiso9660msdosfatvfatnfs 等在这些具体文件系统的上

层Linux 提供了虚拟

文件系统 VFS 来统一它们的行为虚拟文件系统为不同的文件系 统与内核的通信提供了一致的接口下图给出了 Linux 中文件系统的 关系

--[if vml]----[endif]--在Linux 平台下对文件编程可以使用两类函数1Linux 操作系统 文件API2C 语言IO 库函数 前者依赖于Linux 系统调用 后者实际上与操作系统是独立的因为在任何操作系统下使用 C 语言 IO 库函数操作文件的方法都是相同的本章将对这两种方法进行 实例讲 解

Linux http://www.daodoc.com/文件API 的文件操作API 涉及到创建打开读写和关闭文件

创建 creat const char filename mode_t mode 参数mode 指定新建文件的存取权限它同 umask 一起决定文件的 最终权限modeumask 其中umask 代表了文件在创建时需要去掉的一些 存取

权限umask 可通过系统调用umask 来改变 umask int newmask 该调用将umask 设置为newmask 然后返回旧的umask 它只影响读 写和执行权限

打开 open const char pathname int flags open const char pathname int flags mode_t

mode 函数有两个形式其中 pathname 是我们要打开的文件名 包 含路径名称缺省是认为在当前路径下面 flags 可以去下面的一个值 或者是几 个值的组合 标志 含义 _RDONLY 以只读的方式打开文件 _WRONLY 以只写的方式打开文件 _RDWR 以读写的方式打开文件

_APPEND 以追加的方式打开文件 _CREAT 创建一个文件 _EXEC 如果使用了O_CREAT 而且文件已经存在就会发生一个错误 _NOBLOCK 以非阻塞的方式打开一个文件 _TRUNC 如果文件已经存在则删除文件的内容

_RDONLYO_WRONLYO_RDWR 三个标志只能使用任意的一个

如果使用了O_CREATE 标志则使用的函数是int open const char flagsmode_t mode 这个时候我们还要指定 mode 标志

用 ://www.daodoc.com/r来表示文件的访问权限mode 可以是以下情况的组合 标志 含义 _IRUSR 用户可以读 _IWUSR 用户可以写 _IXUSR 用户可以执行 _IRWXU 用户可以读写执行 _IRGRP 组可以读 _IWGRP 组可以写 _IXGRP 组可以执行

_IRWXG 组可以读写执行 _IROTH 其他人可以读 _IWOTH 其他人可以写 _IXOTH 其他人可以执行 _IRWXO 其他人可以读写执行 _ISUID 设置用户执行ID _ISGID 设置组的执行ID

除了可以通过上述宏进行或逻辑产生标志以外我们也可以自己

用数字来表示Linux 总共用5 个数字来表示文件的各种权限第一位表 示

设置用户ID 第二位表示设置组ID 第三位表示用户自己的权限位 第四位表示组的权限最后一位表示其他人的权限每个数字可以取 1 执

行权限 2 写权限 4 读权限 0 无 或者是这些值的和例如要创 建一个用户可读可写可执行但是组没有权限其他人可以读 可以执行的文件并设置用户 ID 位那么我们应该使用的模式是 1 设置用户ID 0 不设置组ID 7 124 读写执行 0 没有权限 14 读执行 即10705 “test” O_CREAT 10705 上述语句等价于 “test” O_CREAT S_IRWXU S_IROTH S_IXOTH S_ISUID 如果文件打开成功 open 函数会返回一个文件描述符以后对该文 件的所有操作就可以通过对这个文件描述符进行操作来实现 读写

在文件打开http://www.daodoc.com/以后我们才可对文件进行读写了 Linux 中提供文件

读写的系统调用是readwrite 函数 read int fd const void buf size_t length write int fd const void buf size_t length 其中参数buf 为指向缓冲区的指针 length 为缓冲区的大小以字 节为单位函数 read 实现从文件描述符 fd 所指定的文件中读取 个字

节到 buf 所指向的缓冲区中返回值为实际读取的字节数函数实现将把 length 个字节从 buf 指向的缓冲区中写到文件描述 符fd 所指向的文

件中返回值为实际写入的字节数

以O_CREAT 为标志的open 实际上实现了文件创建的功能因此下

面的函数等同creat 函数 open pathname O_CREAT O_WRONLY O_TRUNC mode 定位

对于随机文件我们可以随机的指定位置读写使用如下函数进行

定位 lseek int fd offset_t offset int whence 将文件读写指针相对 whence 移动offset

个字节操作成功时返回文件指针相对于文件头的位置参数whence 可使用下述值 _SET 相对文件开头 _CUR 相对文件读写指针的当前位置 _END 相对文件末尾 可取负值例如下述调用可将文件指针相对当前位置向前

移动5 个字节 fd-5 SEEK_CUR 由于 lseek 函数的返回值为文件指针相对于文件头的位置因此 下列调用的返回值就是文件的长度 fd 0 SEEK_END 关闭

当我们操作完成以后我们要关闭文件了只要调用 close 就可以 了其中fd 是我们要关闭的文件描述符

http://www.daodoc.com/ 例程编写一个程序在当前目录下创建用户可读写文件 hellotxt 在其中写入Hello software weekly 关闭该文件再次打开该 文件读取其中的内容并输出在屏幕上 LENGTH 100

int fd len char str[LENGTH] fd open “hellotxt” O_CREAT O_RDWR S_IRUSR S_IWUSR 创建并打开文件 if fd

write fd “Hello Software Weekly” strlen “Hello software ” 写入Hello software weekly 字符串 close fd

fd open “hellotxt” O_RDWR len read fd str LENGTH 读取文件内容 str[len] printf “s” str close fd

close int fd

编译并运行执行

C 语言库函数 库函数的文件操作实际上是独立于具体的操作系统平台的不管 是在DOSWindowsLinux 还是在VxWorks 中都是这些函数

创建和打开 fopen const char path const char mode 实现打开指定文件 filename 其中的 mode 为打开模式 C 语言中支持的打开模式如下表 标志 含义

rb 以只读方式打开 wb 以只写方式打开如果文件不存在则创建该文件否则文件被截断 ab 以追加方式打开如果文件不存http://www.daodoc.com/在则创建该文件 rb rb 以读写方式打开 wb wh 以读写方式打开如果文件不存在时创建新文件否则文件被截断 ab ab 以读和追加方式打开如果文件不存在创建新文件

其中b 用于区分二进制文件和文本文件这一点在DOSWindows 系 统中是有区分的但Linux 不区分二进制文件和文本文件

读写 库函数支持以字符字符串等为单位支持按照某中格式进行文件 的读写这一组函数为 fgetc FILE stream fputc int c FILE stream fgets char s int n

FILE stream fputs const char s FILE stream fprintf FILE stream const char format fscanf FILE stream const char format _t fread void ptr size_t size size_t n FILE stream _t fwrite const void ptr size_t size size_t n FILE 实现从流stream 中读取加n 个字段每个字段为size 字

节并将读取的字段放入 ptr 所指的字符数组中返回实际已读取的字 段数在读

取的字段数小于 num 时可能是在函数调用时出现错误也可能是

读到文件的结尾所以要通过调用feof 和ferror 来判断 实现从缓冲区 ptr 所指的数组中把 n 个字段写到流中每个字段长为size 个字节返回实际写入的字段数

另外C 库函数还提供了读写过程中的定位能力这些函数包括 fgetpos FILE stream fpos_t pos fsetpos Fhttp://www.daodoc.com/ILE stream const fpos_t pos fseek FILE stream long offset int whence 等

关闭

利用C 库函数关闭文件依然是很简单的操作 fclose FILE stream 例程将第2 节中的例程用C 库函数来实现 LENGTH 100 FILE fd char str[LENGTH] fd fopen “hellotxt” “w” 创建并打开文件 if fd

fputs “Hello Software Weekly” fd 写入 Hello software 字符串 fclose fd

fd fopen “hellotxt” “r” fgets str LENGTH fd 读取文件内容 printf “s” str fclose fd 小结

Linux 提供的虚拟文件系统为多种文件系统提供了统一的接口的文件编程有两种途径基于Linux 系统调用基于C 库函数这两

种编程所涉及到文件操作有新建打开读写和关闭对随机文件还

可以定位本章对这两种编程方法都给出了具体的实例 下的C 编程实战三 ――进程控制与进程通信编程

Linux 进程

Linux 进程在内存中包含三部分数据代码段堆栈段和数据段代 码段存放了程序的代码代码段可以为机器中运行同一程序的数个 进程共享堆栈段存放的是子程序函数的返回地址子程序的参数 及程序的局部变量而数据段则存放程序的全局变量常数以及动态数 据分配的数据空间比如用 malloc 函数申请的内存与代码段不同 如果系统中同时运行多个相同的程序它们不能使用同一堆栈段和数 据

段 http://www.daodoc.com/进程主要有如下几种状态用户状态进程在用户状态下运行的状态内核状态进程在内核状态下运行的状态内存中就绪进程 没有执行但处于就绪状态只要内核调度它就可以执行内存中睡 眠进程正在睡眠并且处于内存中没有被交换到SWAP 设备就绪 且换出进程处于就绪状态但是必须把它换入内存内核才能再次

调度它进行运行睡眠且换出进程正在睡眠且被换出内存被抢 先进程从内核状态返回用户状态时内核抢先于它做了上下文切 换调度了另一个进程原先这个进程就处于被抢先状态创建状态 进程刚被创建该进程存在但既不是就绪状态也不是睡眠状态这

个状态是除了进程0 以外的所有进程的最初状态僵死状态进程调用 结束进程不再存在但在进程表项中仍有记录该记录可由父 进程收集

下面我们来以一个进程从创建到消亡的过程讲解 Linux 进程状

态转换的生死因果

进程被父进程通过系统调用fork 创建而处于创建态

fork 调用为子进程配置好内核数据结构和子进程私有数据结构 后子进程进入就绪态或者在内存中就绪或者因为内存不够而在 SWAP 设 备中就绪

若进程在内存中就绪进程可以被内核调度程序调度到CPU 运行 内核调度该进程进入内核状态再由内核状态返回用户状态执行 该进程在用户状态运行一定时间后又会被调度程序所调度而进入内 核

状态由此转入就绪态有时进程在用户状态运行时也会因为需要 内核服务使用系统调用而进入内核状态服务完毕会由内核状态转回 用户状态要注意的是进程在从内核状态向用户状态返回时可能 被抢占这是由于有优先级更高的进程急需使用 CPU 不能等到下一次

调度时 机从而造成抢占

进程执行exit 调用进入僵死状态最终结束 进程控制

进程控制中主要涉及到进程的创建睡眠和退出等在 Linux 中主 要提供了 forkexecclone 的进程创建方法 sleep 的进程睡眠和 exit 的进程

退出调用另外 Linux 还提供了父进程等http://www.daodoc.com/待子进程结束的系统调 用wait 对于没有接触过UnixLinux 操作系统的人来说fork 是最难理解 的概念之一它执行一次却返回两个值完全不可思议先看下面的程序 main int i if fork 0

for i 1 i 3 i printf “This is child proce” else

for i 1 i 3 i printf “This is parent proce”

执行结果为 is child proce is child proce is parent proce is parent proce 在英文中是分叉的意思这个名字取得很形象一个进程在运 行中如果使用了fork 就产生了另一个进程于是进程就分叉了

当前进程为父进程通过 fork 会产生一个子进程对于父进程函数返回子程序的进程号而对于子程序fork 函数则返回零这就 是

一个函数返回两次的本质可以说fork 函数是Unix 系统最杰出的 成就之一它是七十年代 Unix 早期的开发者经过理论和实践上的长期 艰苦探 索后取得的成果

如果我们把上述程序中的循环放的大一点 main

int i if fork 0

for i 1 i 10000 i printf “This is child proce” else

for i 1 i 10000 i printf “This is parent proce” ://www.daodoc.com/

则可以明显地看到父进程和子进程的并发执行交替地输出 This is proce 和This is parent proce 此时此刻我们还没有完全理解 fork 函数再来看下面的一段程 序看看究竟会产生多少个进程程序的输出是什么 main int i for i 0 i 2 i

if fork 0

printf “This is child proce” else

printf “This is parent proce”

在 Linux 中 可 使 用 exec 函 数 族 包 含 多 个 函 数和execvp 被用于启动一个指定路径

和文件名的进程

函数族的特点体现在某进程一旦调用了exec 类函数正在执

行的程序就被干掉了系统把代码段替换成新的程序由 exec 类函数执 行 的代码并且原有的数据段和堆栈段也被废弃新的数据段与堆栈 段被分配但是进程号却被保留也就是说exec 执行的结果为系统认为 正在执行的还是原先的进程但是进程对应的程序被替换了 函数可以创建一个子进程而当前进程不死如果我们在 fork 的子进程中调用 exec 函数族就可以实现既让父进程的代码执行又启 动一个新的指定进程这实在是很妙的fork 和exec 的搭配巧妙地解决了程序

启动另一程序的执行但自己仍继续运行的问题请看下面的例子 command[_CMD_LEN] main

int rtn 子进程的返回数值 while 1

从终端读取要执行的命令

printf “http://www.daodoc.com/ ” fgets command _CMD_LEN stdin command[strlen command1

printf “Error in fork” exit 1

执行子进程 if pid 0

printf “in the spawned child proce” 子进程向父进程写数据关闭管道的读端 close file_descriptors[INPUT] write file_descriptors[OUTPUT] “test data” strlen “test ” exit 0 else

执行父进程

printf “in the spawning parent proce” 父进程从管道读取子进程写的数据关闭管道的写端 close file_descriptors[OUTPUT] returned_count read file_descriptors[INPUT] buf buf printf “d bytes of data received from spawned proce s” returned_count buf

上述程序中无名管道以 pipe int filedis[2] 方式定义参数 filedis 返回两个文件描述符 filedes[0]为读而 打开filedes[1]为写而打开

filedes[1]的输出是

filedes[0]的输入 ://www.daodoc.com/在 Linux 系统下有名管道可由两种方式创建假设创建一个名为的有名管道 mkfifo “fifoexample”“rw”

mknod fifoexample p 是一个函数 mknod 是一个系统调用即我们可以在 shell 下输出上述命令

有名管道创建后我们可以像读写文件一样读写之 进程一读有名管道 main

FILE in_file int count 1 char buf[BUFFER_LEN] in_file fopen “pipeexample” “r” if in_file NULL

printf “Error in fdopen” exit 1

while count fread buf 1 BUFFER_LEN in_file 0 printf “received from pipe s” buf fclose in_file

进程二写有名管道 main

FILE out_file int count 1 char buf[BUFFER_LEN] out_file fopen “pipeexample” “w” if out_file NULL

printf “Error opening pipe” exit 1

sprintf buf “this is test data for the named pipe ”

fwrite buf 1 BUFFER_LEN out_file fclose out_file

消息队列用于运行于同一台机器上的进程间通信与管道http://www.daodoc.com/相似 共享内存通常由一个进程创建其余进程对这块内存区进行读写 得到共享内存有两种方式映射 devmem 设备和内存映像文件前一种方 式

不给系统带来额外的开销但在现实中并不常用因为它控制存取 的是实际的物理内存常用的方式是通过 shmXXX 函数族来实现共享内 存 shmget key_t key int size int flag 获得一个共享存 储标识符

该函数使得系统分配size 大小的内存用作共享内存 shmat int shmid void addr int flag 将共享内存连接

到自身地址空间中 为 shmget 函数返回的共享存储标识符 addr 和 flag 参数 决定了以什么方式来确定连接的地址函数的返回值即是该进程数据 段所连接的实

际地址此后进程可以对此地址进行读写操作访问共享内存

下载Linux下的C编程实战word格式文档
下载Linux下的C编程实战.doc
将本文档下载到自己电脑,方便修改和收藏。
点此处下载文档

文档为doc格式

    热门文章
      整站推荐
        点击下载本文