|
EDA365欢迎您!
您需要 登录 才可以下载或查看,没有帐号?注册
x
对于嵌入式系统来讲,嵌入式软件相当于嵌入式系统的灵魂,整个嵌入式系统如何工作,都是由嵌入式软件来控制的。如何编写高质量,高效率的嵌入式软件在实际项目开发过程中变的越来越重要。3 @% n2 G1 X. @. w
. V' P) v$ ]- F; t& C( _
. D7 O! z- Q% {8 Y" n相信大家都有过这样的感受:看到不规范(杂乱差)的代码,瞬间就没有看下去的欲望了。 ; L- M# S1 y+ [+ m/ E
* f& ]0 I1 c$ `$ P; @
当我们在公司进行嵌入式项目开发的时候,并不是你一个人在单打独斗,通常是一个团队在一起战斗。很多人一起共同完成一个嵌入式项目,通常是每个成员,每个小组完成整个项目中的一个或几个模块。我们编写的代码首先是给人看的,其次才是给机器执行的,这就要求我们团队中的每个人在编写软件的时候,要遵循统一的编程规范和编码风格,提高代码的可读性和可维护性,方便团队成员之间的沟通和交流。在实际项目开发过程中,遵循统一的编程规范相当重要,同学们一定要引起足够的重视,下面我就从代码排版,代码注释,标识符命名,代码可读性和函数设计几个方面来讲解比较通用的嵌入式软件编程规范。0 ~3 t1 d( n! E0 |: {, e& ^
. N" v# U/ l" W( I3 C6 B& G* B% P. o- h; x1 G7 k* z1 j4 P
关于编程规范及原则Ⅱ
6 K8 C6 J0 U/ {* H编程规范也就是编写出简洁、可维护、可靠、可测试、高效、可移植的代码,提高产品代码的质量。: m ]6 z j+ z0 g- G- h9 k
6 E2 R* @3 V; x; P! ?( P6 P$ n本文针对嵌入式,主要结合C语言编程的规范给大家讲述。
1 n8 x& `8 V4 I( W- n
3 Z5 t' u7 Q$ A: x1.头文件
' [7 o1 |; d' u. s对于C语言来说,头文件的设计体现了大部分的系统设计,不合理的头文件布局是编译时间过长的原因。
: f1 c) }& t" k! B& ` 8 W6 d2 \2 H& G% w0 W) K
有很多人将工程中所有的头文件包含在一个include.h文件中,然后在每一个.c源代码文件中包含include.h头文件,这样做可以让代码看上去简洁,但实际忽视了编译效率问题,而且代码的可移植性也不好。
4 h; ]+ T# A+ R4 E7 D ' M- {5 l* l" l; p5 e
原则:
& I# \! x2 e2 W% h0 @A.头文件中适合放置接口的声明,不适合放置实现;% [; ]+ q3 k7 v( R0 T
B.头文件应当职责单一;
$ M6 @, b$ z7 y5 \, wC.头文件应向稳定的方向包含。3 B3 _5 `* \" x5 b3 R+ B7 Q+ N+ |
- d2 q* V* {) n. x
规则:
+ z# x* M' y# ~; EA.每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口;
- b! f0 o0 C5 \4 s1 f5 O1 i2 GB.禁止头文件循环依赖;$ k; u, N& H& \. f: B* v+ |
C..c/.h文件禁止包含用不到的头文件;
! A2 j0 F7 `+ R" e5 t4 x% K# rD.头文件应当自包含;
9 O' _0 p, y1 h! n, t7 `2 hE.总是编写内部#include保护符( #define 保护);
% Z5 Y1 o v* Y; }0 jF.禁止在头文件中定义变量;3 |$ b }) x5 V$ e) F) S3 t$ e
G.只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量;; I$ I) q: R4 ?/ x- u
H.禁止在extern "C"中包含头文件。
% J# B; p n: C; L5 o# @( e6 b : P: z$ u: h# i; K' \* ]: [# r7 A
建议:
' e+ E& E9 m- \( X" UA.一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名;% i# t6 B$ m5 f- k1 C) i8 t& e
B.如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名(降低接口使用者的编写难度);7 ]1 i7 L% F& W. o/ Y( x6 D2 j& u
C.头文件不要使用非习惯用法的扩展名,如.inc;' v- s+ k9 B5 U3 N
D.同一产品统一包含头文件排列方式。
6 Q, |- l. k$ p/ P) v8 z # c- i9 J2 `+ k p* j! k
2.函数
' O+ m2 a4 \3 s0 [5 {2 _( v1 ~函数设计的要点:编写整洁的函数,同时把代码有效组织起来。
% o3 R b1 H* F: K; C
1 v6 W7 @. i4 y/ E/ n
& r% w1 X% p/ M# @; L- N函数整洁的要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。2 v4 F$ b+ u2 i% T+ m) z
* X8 d: q1 J' Z v7 }' q原则:9 Q- m& D" w7 U) d# S7 ~
A.一个函数仅完成一件功能;+ i% b6 n" |3 b. t$ u
B.重复代码应该尽可能提炼成函数.3 f, o3 j$ t+ Z8 y# }
3 ~+ C: n/ [& ]- _' q h: X! l6 G4 }规则:3 {; U# V/ u( @$ y
A.避免函数过长,新增函数不超过100行(非空非注释行);
, U% T. p3 S; r% T" JB.避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层;
/ w# e v8 F: D) sC.可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护;1 c$ ]6 ]# ~4 u! F6 q
D.对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定;
, {( z( R% `7 h0 l2 M! eE.对函数的错误返回码要全面处理;8 z5 i+ J3 b5 p H" z/ y
F.设计高扇入,合理扇出(小于7)的函数;
2 z [- r! P1 j. ]; o. f: C; \5 IG.废弃代码(没有被调用的函数和变量)要及时清除。
# Z8 b# B9 m# Y5 j% o3 q- u$ N % V! V7 q8 b3 a7 t/ u' u; s
建议:; [8 _" v+ }9 E
A.函数不变参数使用const;/ B, h$ O, e$ L0 r5 _2 z4 ^
B.函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用;
) E, h6 V* C$ ?: [6 g6 MC.检查函数所有非参数输入的有效性,如数据文件、公共变量等;
) K4 K4 J! i3 K. g1 g) r/ sD.函数的参数个数不超过5个;
: d+ V4 R9 q4 N0 G& i) mE.除打印类函数外,不要使用可变长参函数;$ [/ c+ S" l; C0 P" Y7 ~7 H
F.在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字。1 m4 ~7 {, w+ g
7 S4 P5 N+ D& l7 o
3.标识符命名与定义3 Z: w4 A# W9 e# y" w
程序命名是一个关键,如果命名不规范,自己写的代码,时间长了恐怕连自己都不知道是什么意思了。
1 {6 c2 B5 s! Q) V$ [ 8 N3 X1 k0 T5 e) L* m# Z- ^1 l& o
3.1通用命名规则8 n' I: C4 u. n8 P' M
常见命名风格:4 a% S6 U2 a5 \- G! {6 L2 m: L
A.用下划线?_?分割,如text_mutex;1 n `7 l; y) j: l9 g; b3 f, I+ L E6 f! j' a
B.大小写字母混用,如ReadRFCText。: H9 x1 P! M. Q8 r2 Y' U5 ?/ D6 `
$ E8 ~6 d, v$ M, u) k9 k r规则:
- l3 X+ B) y. HA.标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解;! G/ o/ B# g6 _6 N5 h- a. e* [
B.除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音;
1 A! Y( Y! C: vC.产品/项目组内部应保持统一的命名风格.
& T! V7 v9 d1 t
l0 o( H" e7 T& Q建议:
+ P9 r* {* m {3 p) WA.用正确的反义词组命名具有互斥意义的变量或相反动作的函数等;+ _ x9 Z! h* G _$ ?* o# K) d
B.尽量避免名字中出现数字编号,除非逻辑上的确需要编号;
; B' V# {) C# S# s( C5 T& S5 `8 LC.标识符前不应添加模块、项目、产品、部门的名称作为前缀;! f8 m$ `& g* u7 b! H
D.平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致;
, I g2 E! L. G6 t3 @E.重构/修改部分代码时,应保持和原有代码的命名风格一致。% y+ q# i' P4 T" B& R! F9 A6 }
8 Y8 w9 f4 O, a/ v o: B; q$ o4 C3.2 文件命名规则
2 t8 ^# m" m4 @7 U+ I: B& {7 @因为不同系统对文件名大小写处理会不同,建议文件命名统一采用小写字符。
. {& w% [8 G. \8 ?0 Y0 D) N& ] 8 g# O9 O! z) i, j3 R
3.3 变量命名规则* O7 S; ~. n( j6 |
首先,全局变量十分危险,通过前缀使得全局变量更加醒目, 促使开发人员对这些变量的使用更加小心。
7 W9 g& A1 q. C/ d
5 Z4 t' R' y$ `# z5 N; \# f7 X6 O2 e$ l8 m x
其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。4 L2 S; I9 P ~0 e8 c# Q4 \% G
( X. I$ ~; f# B
规则:& a' _, b1 X( P3 R
A.全局变量增加“g_”前缀,静态变量增加“s_”前缀;
4 P) x J8 d( B; R4 ?; nB.禁止使用单字节命名变量,但允许定义i、 j、 k作为局部循环变量;% N/ M! ^3 Y/ s6 a6 u
C.使用名词或者形容词+名词方式命名变量。
1 E8 y- y8 I3 s4 ~5 l/ o 3 |, V7 e6 C9 _" ^9 k; d; D
3.4 函数命名规则
6 ^1 R7 w9 p# j- O' R- t9 `A.函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构;
) J5 v Y! G3 gB.函数指针除了前缀,其他按照函数的命名规则命名。
4 [0 S+ K' u' @. g4 |- B% D
. C, J6 F) t# Z# A3.5 宏的命名规则4 S" l8 H7 H! y: a4 x6 y
A.对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线?_?的方式命名(枚举同样建议使用此方式定义);
- w5 p, d2 ]. E" Q2 fB.除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线?_?开头和结尾。
7 L* M+ s, q! K4 N/ o# P
- T6 @. m5 ~$ ~" l' n5 f' ~4.变量
! B# k! ^$ Y' |原则:
6 i% ?; W. X( W) w2 h+ RA.一个变量只有一个功能,不能把一个变量用作多种用途;2 r) ~/ j+ J. x+ ]) @
B.结构功能单一;不要设计面面俱到的数据结构;
& p' S# m5 V8 M$ s: jC.不用或者少用全局变量。& G/ U9 u1 X Z$ C+ C9 O
1 O# q7 }. S- q
规则:
. J- r+ p. x5 S$ j4 N6 y7 WA.防止局部变量与全局变量同名;; M: f0 x1 _; G8 M; k
B.通讯过程中使用的结构,必须注意字节序;
, F7 l O# f. QC.严禁使用未经初始化的变量作为右值;
' o6 @2 `' B* o0 H7 e; ?
. \" Q' [0 I) u/ i建议:
4 k+ g( K% D% @+ mA.构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象;
3 ?4 j0 M4 J- U, f0 pB.使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥;5 o! g4 X. G# V8 H/ o0 U2 l
C.在首次使用前初始化变量,初始化的地方离使用的地方越近越好;
9 P! k. \% H! C5 y7 r9 H* bD.明确全局变量的初始化顺序,避免跨模块的初始化依赖;# K/ |: C+ j7 N. _, i/ b& P
E.尽量减少没有必要的数据类型默认转换与强制转换。7 S3 v7 \/ N/ B6 [* l {; b e
! K6 v( q+ k# f" X5 H
5.宏、常量
* \: O+ P, _" ~- K0 z因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。
. o; c0 A0 |' ^
: F+ c& Z: }: W( w9 K9 r! X规则:
. ? s. X4 D$ T3 zA.用宏定义表达式时,要使用完备的括号;* o( E% a1 d! a6 T
不规范:#define RECTANGLE_AREA(a, b) a * b! c1 }! @: `! _' k4 B" L& ~7 v" O5 U
规范:#define RECTANGLE_AREA(a, b) ((a) * (b))/ \) |- T+ R9 \& I5 p
2 g, f7 O( D( A% y' c7 L( [, {
B.将宏所定义的多条表达式放在大括号中;) J3 {5 w# h/ q4 ?3 ]" `
C.使用宏时,不允许参数发生变化;
1 `# p V. ?: [+ |: @$ p#define SQUARE(a) ((a) * (a))
) y: T- p/ I, c* s) L( J0 g6 Lint a = 5;
& X. O/ [, p' }int b;
+ C) K }- P% ]- T9 ~) F不规范:% S+ v# P* n; J! r1 n6 ^6 R
b = SQUARE(a++);
1 h* L& ?3 t4 U4 B3 k5 X8 u* q
6 ^6 R$ ]/ D `: S2 w规范:. ~9 _) W0 d) M2 F
b = SQUARE(a);
8 A1 Z/ G# S# K! y7 _/ a+ R za++;) z3 L g: d# W7 A, o. M$ G0 a* ]
5 V7 {3 M* S, q' D: Z
建议:
5 u, Y! q9 G5 i0 WA.除非必要,应尽可能使用函数代替宏;
/ l7 k- a9 j* {) kB.常量建议使用const定义代替宏;% q* J& I9 l+ a* \% |9 b' D
C.宏定义中尽量不使用return、 goto、 continue、 break等改变程序流程的语句。, |% K a0 C. ~! @. t3 y$ A% ?- P$ ~
2 ] F! }! d! Z% B6.注释
( j8 g& p" A9 P! e a原则:) }( `) E4 E: c# Q" Z2 a
A.优秀的代码可以自我解释,不通过注释即可轻易读懂;' n* H; _. o5 ~3 c( v+ O' R
B.注释的内容要清楚、明了,含义准确,防止注释二义性;
9 u% |# X! f @; G" E7 Q& xC.在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码。
) H% _3 B6 m4 }
! b, n8 y. Z4 W) u" g, N规则:
3 o# P# |5 h; q, i* [+ D! OA.修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删;9 K$ ^. V' ~% m8 v, t2 H
B.文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明;7 p8 H& P n, _" |* Q0 h
C.函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等;8 k8 A& y4 ]: T
D.全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明;
( f+ m2 @7 }# m! C. f7 EE.注释应放在其代码上方相邻位置或右方,不可放在下面。 如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同;" t" J7 ?, | Q
F.避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写;: e6 B- z6 |/ V+ _8 n1 |: H: I/ Z
G.同一产品或项目组统一注释风格。
4 a" C5 y# S" W2 L7 v! h+ Y $ y6 R# ~/ b2 j8 T
建议:% c! ]$ b( W6 X0 {8 B
A.避免在一行代码或表达式的中间插入注释;8 l$ A2 P7 K+ J) g$ Z& r) v6 |
B.文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式。& N- Z6 B! R3 c7 W. o( u
( y& f; P! _0 X/ T0 i
7.排版与格式
1 m7 l8 G! b) a0 D( n规则:
& S, J. U2 r e- n8 vA.程序块采用缩进风格编写, 每级缩进为4个空格;
c" v5 X8 n8 lB.相对独立的程序块之间、变量说明之后必须加空行;
* W$ b) V5 B' @/ kC.一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定;
2 Z8 d/ X( r- `D.多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句;
0 K* d7 U3 ]# S. o$ k# m# yE.if、 for、 do、 while、 case、 switch、 default等语句独占一行;/ ?7 z0 T2 r3 s( c
F.在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格; 进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格;
! l3 A J( w+ o- C$ D/ O; rG.注释符(包括?/*??//??*/?)与注释内容之间要用一个空格进行分隔。+ |; _9 P0 R _9 V# |, R3 P
0 n: Q9 t4 ]% C- V v/ ~# ^嵌入式编程中的注意事项2 w. A& Q( S X
嵌入式软件开发和普通软件编程相比,有一些自己的特点,下面从嵌入式软件架构,中断编程,寄存器配置,浮点运算等几个方面来讲解嵌入式编程中的注意事项.
" G( m! u+ s2 }) C
4 h5 E6 X) S$ A* }
6 p2 J' X' H4 x9 i* c1. 嵌入式系统的软件架构9 N" Y* C, A3 b! R
+ K" |( @$ |( }/ V4 `) v5 q' f$ j+ l$ C, k3 w
/ N& e" {+ ]2 q# M4 f一个大型的嵌入式软件往往需要根据功能的不同划分成多个软件功能模块。" m3 y; ?0 `; M
1) 模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;5 `) G9 ^5 H/ G7 d l9 }; {( R5 I# N
2) 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明;6 W: S% X1 C& F4 C
3) 模块内的函数和全局变量需在.c文件开头冠以static关键字声明;6 c: ?7 F3 H. l( ?
+ T5 @/ v- V8 ^- r+ B* r4) 永远不要在.h文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量
2 n# e7 ?( J$ q# u; r/ c8 P
! @; d' h& o! k* N* b5 }, K" b$ j7 B. X
2. 中断编程4 J& f& R$ o; G: l. @: c) g
中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。 许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序. 类似于__interrupt、#program interrupt等。当一个函数被定义为中断服务处理程序的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
( P, B4 j1 j( I4 s! Z Y" C* C7 Q4 k$ t
( Z' L7 E! w% q+ S" z# Q
, M; d* d/ V9 ~% `
中断服务程序需要满足如下要求:
+ x6 ]; Z4 V7 }5 p' |( T3 x- L% I% W
1) 不能有返回值;- F/ |4 E4 \- V. q7 @
2) 不能向中断服务处理程序传递参数;
; i, J! y v/ u3) 中断服务处理程序应该尽可能的短小精悍,不要包含耗时的代码5 E m7 @: V3 e7 w5 z4 h5 n
; r$ e2 a2 C1 F# h! k l
8 ? x8 ?9 u6 I3 G5 x) o3 z4 R! r1 @, R1 n; I$ U8 O S% I. U
3. 寄存器配置+ p! ?# j5 I3 ] o
4 H* c+ l( F( c) j, N
, X6 [; x1 o z; d8 y; C
; `' E' [( ?- {
嵌入式软件是面向硬件底层的软件,我们在对硬件进行编程时,通常是通过配置硬件相关的寄存器来实现的。在配置寄存器时,通常我们只需要配置寄存器的1位或几位,对于其他不需要配置的位,我们要保持不变,不要更改我们不需要配置的位。5 K: K+ M' h9 M; j4 S9 ?
* E$ u6 t8 a1 J" x, s
5 {9 m1 s' T# Q) s
例如:我们希望配置寄存器的 GPIOADAT 的第 1位 为 14 A# @. d' H) g8 n' h. w
我们不能这样写成这样:
) w3 ~: e6 z4 Y/ Y$ }& J5 t2 M" _3 s- l7 x
GPIOADAT = 0x02; //将其他位设置为 0) [+ h8 @! m. k" d$ b5 H5 N$ s
6 L- z: M# ^9 Z3 E8 F$ D. @而应该写成这样:/ H5 V2 q/ S4 D5 v
8 I7 b8 r" | U! n% _+ U* f
GPIOADAT |= 0x02; //保证其他位不变% r% H- c4 H7 t# |; m/ _! N3 n
# q4 `/ ?( I4 X4 x
$ K8 K8 Z1 a) l# @0 f& h7 f$ M8 Z+ t0 M' _) X2 w3 U1 Y
4. 浮点运算) C2 h% s9 ?- f
大多数低档次的单片机都是不支持浮点运算的,因此在实际使用过程中也很少用到,因此为了降低成本,一般都去掉了浮点运算模块,这就带来了一个问题,如果万一要用到浮点运算怎么办?我们可以采用的是“定点”的方法来解决这个问题,就是直接放大10的N次方倍进行整数的计算,可以得出近似值,因此为了不增加不必要的麻烦,应该总是尽量避免使用浮点运算,一般情况也是可以避免的。$ m, l4 f5 t" N+ [2 `% P: }$ I
5 l& t" P: b; |# }0 c5 ~: H: q
3 v9 G1 p/ k; c( Q( I' Z, z: ]
5. volatile 关键字的使用0 Q6 s/ I8 H, C5 [! S
( F, S9 T7 l) I ?2 O' |嵌入式开发过程中,在定义硬件寄存器的时候,需要使用volatile关键字。 volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。 如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值。
# r- m/ a: ?0 @/ X6 B7 G
' W' B! J, z1 \' H4 U9 d- y) l. W
_, ^/ _* y1 J6 f: P
例如: #define GPIO_DATA (*(volatile unsigned int *)0x90002000)9 }( { V* X/ K6 `
3 V; f" ?5 C7 n: G* Q2 z! p: H
. w0 F- s* ~! `1 o5 |" L+ z T+ L小结:$ H( W1 M1 p5 @# i# T) P- u! `! X( D
8 o& X' h2 O: J5 a: t f8 v
良好的编程习惯是需要日积月累的,如果你正处于学习阶段,请你时刻要注意这些细节问题。
- n1 C6 ?) y# V" G+ k
- Z- O: f' q0 X' A本文转自网络,版权归原作者,如果您觉得不好,请联系我们删除!
) t5 Y2 c! m5 G: s+ y' d1 j0 h* m |
|