1 源碼下載
gun官網(wǎng)鏈接
2 printf函數(shù)源碼
用vscode打開下載的源碼,找到printf函數(shù)。
( printf 函數(shù)路徑為:glibc-2.36\stdio-common\printf.c
)
源碼如下:
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stdout, format, arg, 0);
va_end (arg);
return done;
}
#undef _IO_printf
ldbl_strong_alias (__printf, printf);
ldbl_strong_alias (__printf, _IO_printf);
可以看見主要是四個(gè)東西:va_list
? va_start
? va_end
? __vfprintf_internal
前面三個(gè)先不看,后面重點(diǎn)介紹,先看看能不能看懂__vfprintf_internal
這個(gè)函數(shù)的實(shí)現(xiàn)。
在 glibc-2.36\stdio-common\vfprintf-internal.c
里可以看到這個(gè)函數(shù)實(shí)際上就是vfprintf
函數(shù), 也在相同的.c文件中
源碼如下:
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
/* The character used as thousands separator. */
THOUSANDS_SEP_T thousands_sep = 0;
/* The string describing the size of groups of digits. */
const char *grouping;
/* Place to accumulate the result. */
int done;
/* Current character in format string. */
const UCHAR_T *f;
/* End of leading constant string. */
const UCHAR_T *lead_str_end;
/* Points to next format specifier. */
const UCHAR_T *end_of_spec;
/* Buffer intermediate results. */
CHAR_T work_buffer[WORK_BUFFER_SIZE];
CHAR_T *workend;
/* We have to save the original argument pointer. */
va_list ap_save;
/* Count number of specifiers we already processed. */
int nspecs_done;
/* For the %m format we may need the current `errno' value. */
int save_errno = errno;
/* 1 if format is in read-only memory, -1 if it is in writable memory,
0 if unknown. */
int readonly_format = 0;
/* Orient the stream. */
#ifdef ORIENT
ORIENT;
#endif
/* Sanity check of arguments. */
ARGCHECK (s, format);
#ifdef ORIENT
/* Check for correct orientation. */
if (_IO_vtable_offset (s) == 0
&& _IO_fwide (s, sizeof (CHAR_T) == 1 ? -1 : 1)
!= (sizeof (CHAR_T) == 1 ? -1 : 1))
/* The stream is already oriented otherwise. */
return EOF;
#endif
if (UNBUFFERED_P (s))
/* Use a helper function which will allocate a local temporary buffer
for the stream and then call us again. */
return buffered_vfprintf (s, format, ap, mode_flags);
/* Initialize local variables. */
done = 0;
grouping = (const char *) -1;
#ifdef __va_copy
/* This macro will be available soon in gcc's <stdarg.h>. We need it
since on some systems `va_list' is not an integral type. */
__va_copy (ap_save, ap);
#else
ap_save = ap;
#endif
nspecs_done = 0;
#ifdef COMPILE_WPRINTF
/* Find the first format specifier. */
f = lead_str_end = __find_specwc ((const UCHAR_T *) format);
#else
/* Find the first format specifier. */
f = lead_str_end = __find_specmb ((const UCHAR_T *) format);
#endif
/* Lock stream. */
_IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
_IO_flockfile (s);
/* Write the literal text before the first format. */
outstring ((const UCHAR_T *) format,
lead_str_end - (const UCHAR_T *) format);
/* If we only have to print a simple string, return now. */
if (*f == L_('\0'))
goto all_done;
/* Use the slow path in case any printf handler is registered. */
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;
/* Process whole format string. */
do
{
STEP0_3_TABLE;
STEP4_TABLE;
int is_negative; /* Flag for negative number. */
union
{
unsigned long long int longlong;
unsigned long int word;
} number;
int base;
union printf_arg the_arg;
CHAR_T *string; /* Pointer to argument string. */
int alt = 0; /* Alternate format. */
int space = 0; /* Use space prefix if no sign is needed. */
int left = 0; /* Left-justify output. */
int showsign = 0; /* Always begin with plus or minus sign. */
int group = 0; /* Print numbers according grouping rules. */
/* Argument is long double/long long int. Only used if
double/long double or long int/long long int are distinct. */
int is_long_double __attribute__ ((unused)) = 0;
int is_short = 0; /* Argument is short int. */
int is_long = 0; /* Argument is long int. */
int is_char = 0; /* Argument is promoted (unsigned) char. */
int width = 0; /* Width of output; 0 means none specified. */
int prec = -1; /* Precision of output; -1 means none specified. */
/* This flag is set by the 'I' modifier and selects the use of the
`outdigits' as determined by the current locale. */
int use_outdigits = 0;
UCHAR_T pad = L_(' ');/* Padding character. */
CHAR_T spec;
workend = work_buffer + WORK_BUFFER_SIZE;
/* Get current character in format string. */
JUMP (*++f, step0_jumps);
/* ' ' flag. */
LABEL (flag_space):
space = 1;
JUMP (*++f, step0_jumps);
/* '+' flag. */
LABEL (flag_plus):
showsign = 1;
JUMP (*++f, step0_jumps);
/* The '-' flag. */
LABEL (flag_minus):
left = 1;
pad = L_(' ');
JUMP (*++f, step0_jumps);
/* The '#' flag. */
LABEL (flag_hash):
alt = 1;
JUMP (*++f, step0_jumps);
/* The '0' flag. */
LABEL (flag_zero):
if (!left)
pad = L_('0');
JUMP (*++f, step0_jumps);
/* The '\'' flag. */
LABEL (flag_quote):
group = 1;
if (grouping == (const char *) -1)
{
#ifdef COMPILE_WPRINTF
thousands_sep = _NL_CURRENT_WORD (LC_NUMERIC,
_NL_NUMERIC_THOUSANDS_SEP_WC);
#else
thousands_sep = _NL_CURRENT (LC_NUMERIC, THOUSANDS_SEP);
#endif
grouping = _NL_CURRENT (LC_NUMERIC, GROUPING);
if (*grouping == '\0' || *grouping == CHAR_MAX
#ifdef COMPILE_WPRINTF
|| thousands_sep == L'\0'
#else
|| *thousands_sep == '\0'
#endif
)
grouping = NULL;
}
JUMP (*++f, step0_jumps);
LABEL (flag_i18n):
use_outdigits = 1;
JUMP (*++f, step0_jumps);
/* Get width from argument. */
LABEL (width_asterics):
{
const UCHAR_T *tmp; /* Temporary value. */
tmp = ++f;
if (ISDIGIT (*tmp))
{
int pos = read_int (&tmp);
if (pos == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
if (pos && *tmp == L_('$'))
/* The width comes from a positional parameter. */
goto do_positional;
}
width = va_arg (ap, int);
/* Negative width means left justified. */
if (width < 0)
{
width = -width;
pad = L_(' ');
left = 1;
}
}
JUMP (*f, step1_jumps);
/* Given width in format string. */
LABEL (width):
width = read_int (&f);
if (__glibc_unlikely (width == -1))
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
if (*f == L_('$'))
/* Oh, oh. The argument comes from a positional parameter. */
goto do_positional;
JUMP (*f, step1_jumps);
LABEL (precision):
++f;
if (*f == L_('*'))
{
const UCHAR_T *tmp; /* Temporary value. */
tmp = ++f;
if (ISDIGIT (*tmp))
{
int pos = read_int (&tmp);
if (pos == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
if (pos && *tmp == L_('$'))
/* The precision comes from a positional parameter. */
goto do_positional;
}
prec = va_arg (ap, int);
/* If the precision is negative the precision is omitted. */
if (prec < 0)
prec = -1;
}
else if (ISDIGIT (*f))
{
prec = read_int (&f);
/* The precision was specified in this case as an extremely
large positive value. */
if (prec == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
}
else
prec = 0;
JUMP (*f, step2_jumps);
/* Process 'h' modifier. There might another 'h' following. */
LABEL (mod_half):
is_short = 1;
JUMP (*++f, step3a_jumps);
/* Process 'hh' modifier. */
LABEL (mod_halfhalf):
is_short = 0;
is_char = 1;
JUMP (*++f, step4_jumps);
/* Process 'l' modifier. There might another 'l' following. */
LABEL (mod_long):
is_long = 1;
JUMP (*++f, step3b_jumps);
/* Process 'L', 'q', or 'll' modifier. No other modifier is
allowed to follow. */
LABEL (mod_longlong):
is_long_double = 1;
is_long = 1;
JUMP (*++f, step4_jumps);
LABEL (mod_size_t):
is_long_double = sizeof (size_t) > sizeof (unsigned long int);
is_long = sizeof (size_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
LABEL (mod_ptrdiff_t):
is_long_double = sizeof (ptrdiff_t) > sizeof (unsigned long int);
is_long = sizeof (ptrdiff_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
LABEL (mod_intmax_t):
is_long_double = sizeof (intmax_t) > sizeof (unsigned long int);
is_long = sizeof (intmax_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
/* Process current format. */
while (1)
{
#define process_arg_int() va_arg (ap, int)
#define process_arg_long_int() va_arg (ap, long int)
#define process_arg_long_long_int() va_arg (ap, long long int)
#define process_arg_pointer() va_arg (ap, void *)
#define process_arg_string() va_arg (ap, const char *)
#define process_arg_unsigned_int() va_arg (ap, unsigned int)
#define process_arg_unsigned_long_int() va_arg (ap, unsigned long int)
#define process_arg_unsigned_long_long_int() va_arg (ap, unsigned long long int)
#define process_arg_wchar_t() va_arg (ap, wchar_t)
#define process_arg_wstring() va_arg (ap, const wchar_t *)
#include "vfprintf-process-arg.c"
#undef process_arg_int
#undef process_arg_long_int
#undef process_arg_long_long_int
#undef process_arg_pointer
#undef process_arg_string
#undef process_arg_unsigned_int
#undef process_arg_unsigned_long_int
#undef process_arg_unsigned_long_long_int
#undef process_arg_wchar_t
#undef process_arg_wstring
LABEL (form_float):
LABEL (form_floathex):
{
if (__glibc_unlikely ((mode_flags & PRINTF_LDBL_IS_DBL) != 0))
is_long_double = 0;
struct printf_info info =
{
.prec = prec,
.width = width,
.spec = spec,
.is_long_double = is_long_double,
.is_short = is_short,
.is_long = is_long,
.alt = alt,
.space = space,
.left = left,
.showsign = showsign,
.group = group,
.pad = pad,
.extra = 0,
.i18n = use_outdigits,
.wide = sizeof (CHAR_T) != 1,
.is_binary128 = 0
};
PARSE_FLOAT_VA_ARG_EXTENDED (info);
const void *ptr = &the_arg;
int function_done = __printf_fp_spec (s, &info, &ptr);
if (function_done < 0)
{
done = -1;
goto all_done;
}
done_add (function_done);
}
break;
LABEL (form_unknown):
if (spec == L_('\0'))
{
/* The format string ended before the specifier is complete. */
__set_errno (EINVAL);
done = -1;
goto all_done;
}
/* If we are in the fast loop force entering the complicated
one. */
goto do_positional;
}
/* The format is correctly handled. */
++nspecs_done;
/* Look for next format specifier. */
#ifdef COMPILE_WPRINTF
f = __find_specwc ((end_of_spec = ++f));
#else
f = __find_specmb ((end_of_spec = ++f));
#endif
/* Write the following constant string. */
outstring (end_of_spec, f - end_of_spec);
}
while (*f != L_('\0'));
/* Unlock stream and return. */
goto all_done;
/* Hand off processing for positional parameters. */
do_positional:
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep, mode_flags);
all_done:
/* Unlock the stream. */
_IO_funlockfile (s);
_IO_cleanup_region_end (0);
return done;
}
現(xiàn)在的水平看懂確實(shí)有一定的難度,于是就不看了。
3 幾個(gè)宏及解讀
前面提到了有幾個(gè)其他的東西,現(xiàn)在我們來看看是什么
va_list
在vs2017下的定義是這樣的:(\Community\VC\Tools\MSVC\14.16.27023\include\vadefs.h
)
#ifndef _VA_LIST_DEFINED
#define _VA_LIST_DEFINED
#ifdef _M_CEE_PURE
typedef System::ArgIterator va_list;
#else
typedef char* va_list;
#endif
#endif
可以看出大部分情況下他就是 char *
類型,再看看百度的解釋
va_start
這是 vs2017下x64的定義,看不了他的實(shí)現(xiàn),那么就總結(jié)一下其他資料的解釋。
這個(gè)宏的作用是:得到一個(gè)指向第一個(gè)可變參數(shù)的指針,也就是“”包裹的字符串后面的第一個(gè)參數(shù)。
看看這個(gè)宏的聲明:
void va_start(va_list ap, last_arg);
ap
: 這個(gè)參數(shù)的類型是之前提過的va_list
,也就是那個(gè)指向可變參數(shù)列表的指針。last_arg
: 是最后一個(gè)固定參數(shù),在printf
函數(shù)中就是那個(gè)“”包裹的字符串。
在介紹他的實(shí)現(xiàn)之前,我們需要先補(bǔ)充三個(gè)知識(shí):
- 函數(shù)傳參進(jìn)棧的順序
- 棧區(qū)在計(jì)算機(jī)內(nèi)部的地址情況
- _INTSIZEOF 宏
函數(shù)參數(shù)進(jìn)棧順序
先說第一點(diǎn),函數(shù)參數(shù)進(jìn)棧的順序
???? 從右往左,依次進(jìn)棧
func(int a, int b, int c, int d);
如果是這個(gè)函數(shù)的話,那么應(yīng)該是d-> c -> b -> a
這是要了解的第一點(diǎn)。
void func(char *fmt, ...);
fmt肯定是在這一系列參數(shù)中最低的地址部分。
棧區(qū)在計(jì)算機(jī)內(nèi)部的地址情況
來看這張經(jīng)典的圖:
棧底是高地址,棧頂是底地址,也就是說先進(jìn)棧的會(huì)是高地址。之前那個(gè)例子中,地址從高到低依次也是 d , c, b, a。
在可變參數(shù)的函數(shù)中,大概就是這種情況:
|——————————————————————————|
|最后一個(gè)可變參數(shù) | ----------------高內(nèi)存地址處
|——————————————————————————|
…
|——————————————————————————|
|第N個(gè)可變參數(shù) | -----------va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N個(gè)可變參數(shù)的地址。
|——————————————— |
………………………….
|——————————————————————————|
|第一個(gè)可變參數(shù) | ------------ va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一個(gè)可變參數(shù)的地址
|——————————————— |
|——————————————————————————|
| |
|最后一個(gè)固定參數(shù) | ------------- start的起始地址
|—————————————— —| …
|—————————————————————————— |
| |
|——————————————— |-> 低內(nèi)存地址處
文章來源:http://www.zghlxwxcb.cn/news/detail-528407.html
_INTSIZEOF 宏
宏的定義:
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
這個(gè)宏的作用是把sizeof(n)
向上取整作為 sizeof(int)
的整數(shù)倍,用以在內(nèi)存中對(duì)齊。
看過實(shí)現(xiàn)之后我覺得這個(gè)宏寫的當(dāng)精彩。
對(duì)于兩個(gè)正整數(shù) x, n 總存在整數(shù) q, r 使得
x = nq + r, 其中 0<= r <n ?? ? ??? //最小非負(fù)剩余
q, r 是唯一確定的。q = [x/n], r = x - n[x/n]. 這個(gè)是帶余除法的一個(gè)簡(jiǎn)單形式。在 c 語言中, q, r 容易計(jì)算出來: q = x/n, r = x % n.
所謂把 x 按 n 對(duì)齊指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 這也相當(dāng)于把 x 表示為:
x = nq + r’, 其中 -n < r’ <=0 ?????? ?? //最大非正剩余
nq 是我們所求。關(guān)鍵是如何用 c 語言計(jì)算它。由于我們能處理標(biāo)準(zhǔn)的帶余除法,所以可以把這個(gè)式子轉(zhuǎn)換成一個(gè)標(biāo)準(zhǔn)的帶余除法,然后加以處理:
x+n = qn + (n+r’),其中 0<n+r’<=n ??? ???? //最大正剩余
x+n-1 = qn + (n+r’-1), 其中 0<= n+r’-1 <n ?? ?? //最小非負(fù)剩余
所以 qn = [(x+n-1)/n]n. 用 c 語言計(jì)算就是:
((x+n-1)/n)*n
若 n 是 2 的方冪, 比如 2^m,則除為右移 m 位,乘為左移 m 位。所以把 x+n-1 的最低 m 個(gè)二進(jìn)制位清 0就可以了。得到:
(x+n-1) & (~(n-1))
先試著理解上面的,如果上面那些步驟沒看懂沒關(guān)系。我們這樣想,要把一個(gè)整數(shù)表達(dá)成另一個(gè)小的整數(shù)的整數(shù)倍,那么這個(gè)整數(shù)本身肯定只有兩種情況:1,他可以被這個(gè)小整數(shù)整除,也就是正好在小整數(shù)劃分區(qū)間的端點(diǎn)上。2,不可被整除,那就是落在某個(gè)區(qū)間上。我們要做的就是找到這個(gè)數(shù)的區(qū)間端點(diǎn)。
第一種情況: x正好是某個(gè)區(qū)間的端點(diǎn),那么x+n-1
落在他本身的區(qū)間,c語言中的/
是向下取整的,除以n剛好就是某個(gè)端點(diǎn)值。
第二種情況,x在某個(gè)區(qū)間中,x+n
必然在他下一個(gè)區(qū)間上。那么x+n-1
,又有兩種情況,一種是和x+n
在同一個(gè)區(qū)間(x的下一個(gè)區(qū)間
)上,第二種是在x下一個(gè)區(qū)間的左端點(diǎn)
上,不可能和x在同一個(gè)區(qū)間。那么不管是哪一種情況x+n-1
進(jìn)行c語言的向下整除操作后都會(huì)落在x的右端點(diǎn)
上,就達(dá)到了向上取整的目的。
最后解釋一下(x+n-1) & (~(n-1))
的含義, 要執(zhí)行x+n-1
除以n的操作, 相當(dāng)于向右移動(dòng)。那么我們有這個(gè)結(jié)論 若 n 是 2 的方冪, 比如 2^m,則除為右移 m 位,乘為左移 m 位。所以把 x+n-1 的最低 m 個(gè)二進(jìn)制位清 0就可以了
。 而n若為2^m
的話,二進(jìn)制下就是1后接m個(gè)0
,那么對(duì)n-1
取反就是得到了m個(gè)0,在進(jìn)行&
操作就行了。
回到va_start
宏:
#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v) ) // 得到第一個(gè)可變參數(shù)的地址
結(jié)合前面那張棧的圖,我們可以知道,最后一個(gè)固定參數(shù)的地址在第一個(gè)可變參數(shù)的地址下方,在給出固定參數(shù)的地址后,加上固定參數(shù)本身占用的內(nèi)存后,得到了第一個(gè)可變參數(shù)的起始地址。
注意:宏va_start是對(duì)參數(shù)的地址進(jìn)行操作的,要求參數(shù)地址必須是有效的。一些地址無效的類型不能當(dāng)作固定參數(shù)類型。比如:寄存器類型,它的地址不是有效的內(nèi)存地址值;數(shù)組和函數(shù)也不允許,他們的長(zhǎng)度是個(gè)問題。因此,這些類型時(shí)不能作為va函數(shù)的參數(shù)的。
va_arg
先看宏的定義:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
這個(gè)宏主要做了兩件事情:
1、 指針ap指向下一個(gè)參數(shù)的地址
2、 強(qiáng)制類型轉(zhuǎn)換后得到用戶所指定的值
我們可以拆開來看這句話
將( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) /* 指針ap指向下一個(gè)參數(shù)的地址 */
拆成:
1. ap += _INTSIZEOF(t); // 當(dāng)前,ap已經(jīng)指向下一個(gè)參數(shù)了
2. return *(t *)( ap - _INTSIZEOF(t))
/* ap減去當(dāng)前參數(shù)的大小得到當(dāng)前參數(shù)的地址,再強(qiáng)制類型轉(zhuǎn)換后返回它的值 */
用這個(gè)宏就進(jìn)行可變參數(shù)的遍歷操作,達(dá)到智能輸出的效果了。
va_end
#define va_end(ap) ( ap = (va_list)0 )
這個(gè)宏很簡(jiǎn)單,就是將指針置空,而這個(gè)空間也不是在堆上的,也不用free了。
x86平臺(tái)定義為ap=(char*)0;使ap不再> 指向堆棧,而是跟NULL一樣.有些直接定義為((void*)0),這樣編譯器不會(huì)為va_end產(chǎn)生代碼,例如gcc在linux的x86平臺(tái)就是這樣定義的.
4 自己實(shí)現(xiàn)的可變參數(shù)函數(shù)
#include<stdio.h>
#include<stdarg.h>
int k = -1, t = 0;
int it;
char *ct = NULL;
char cc = '0';
double dd = 0;
int it2 = 0;
int i = 0;
char tt[30];
void f(int it) {
while(it) {
t = it % 10;
tt[++k] = t + '0';
it /= 10;
}
}
void myprintf(char const* fmt, ...) {
char const *p;
va_list aq;
va_start(aq, fmt);
p = fmt;
while(*p != '\0') {
if(*p != '%') {
putchar(*p);
p++;
continue;
}
switch(*++p) {
case 'd':
it = va_arg(aq, int);
if(it < 0) {
putchar('-');
it = -it;
}
f(it);
for( ; k >= 0; k--) {
putchar(tt[k]);
}
break;
case 's':
ct = va_arg(aq, char *);
for( ; *ct; ct++) {
putchar(*ct);
}
break;
case 'c':
cc = va_arg(aq, int);
putchar(cc);
break;
case 'f':
dd = va_arg(aq, double);
if(dd < 0){
putchar('-');
dd = -dd;
}
it = (int)dd;
it2 = it;
dd = dd - it;
dd *= 1000000;
it = (int)dd;
for(i = k+1;i <= k+6 ;i++ ) {
tt[i] = '0';
}
f(it);
tt[k = i] = '.';
if(it2 == 0){
tt[++k] = '0';
}
f(it2);
for( ; k >= 0; k--) {
putchar(tt[k]);
}
break;
}
p++;
}
va_end(aq);
}
int main() {
myprintf("??%d, %s, >> %c, %d, %s , %f, %f", 10, "kkk", '&', 999, "aaaaa", 1234.15648777, 0.0012);
//myprintf("??%f", 0.0012);
return 0;
}
// ??10, kkk, >> &, 999, aaaaa , 1234.156487, 0.001200
算法還可以優(yōu)化,代碼也能更好看點(diǎn),但學(xué)習(xí)的目的已經(jīng)達(dá)到了
參考:文章來源地址http://www.zghlxwxcb.cn/news/detail-528407.html
- https://blog.csdn.net/blueskybluesoul/article/details/121969786
- https://www.cnblogs.com/saolv/p/7779364.html
- https://blog.csdn.net/zhyjunFov/article/details/12017697
- https://blog.csdn.net/weixin_45206746/article/details/117535332
- http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=814501
到了這里,關(guān)于C語言printf函數(shù)實(shí)現(xiàn)解讀的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!