程序按功能劃分可分為數(shù)值運(yùn)算、符號(hào)處理和I/O操作三類,符號(hào)處理程序占相當(dāng)大的比例,符號(hào)處理程序無處不在,編譯器、瀏覽器、Office套件等程序的主要功能都是符號(hào)處理。無論多復(fù)雜的符號(hào)處理都是由各種基本的字符串操作組成的,本節(jié)介紹如何用C語言的庫函數(shù)做字符串初始化、取長度、拷貝、連接、比較、搜索等基本操作。
#include <string.h>
void *memset(void *s, int c, size_t n);
返回值:s指向哪,返回的指針就指向哪
memset
函數(shù)把s
所指的內(nèi)存地址開始的n
個(gè)字節(jié)都填充為c
的值。通常c
的值為0,把一塊內(nèi)存區(qū)清零。例如定義char buf[10];
,如果它是全局變量或靜態(tài)變量,則自動(dòng)初始化為0(位于.bss
段),如果它是函數(shù)的局部變量,則初值不確定,可以用memset(buf, 0, 10)
清零,由malloc
分配的內(nèi)存初值也是不確定的,也可以用memset
清零。
#include <string.h>
size_t strlen(const char *s);
返回值:字符串的長度
strlen
函數(shù)返回s
所指的字符串的長度。該函數(shù)從s
所指的第一個(gè)字符開始找'\0'
字符,一旦找到就返回,返回的長度不包括'\0'
字符在內(nèi)。例如定義char buf[] = "hello";
,則strlen(buf)
的值是5,但要注意,如果定義char buf[5] = "hello";
,則調(diào)用strlen(buf)
是危險(xiǎn)的,會(huì)造成數(shù)組訪問越界。
在第 1 節(jié) “本章的預(yù)備知識(shí)”中介紹了strcpy
和strncpy
函數(shù),拷貝以'\0'
結(jié)尾的字符串,strncpy
還帶一個(gè)參數(shù)指定最多拷貝多少個(gè)字節(jié),此外,strncpy
并不保證緩沖區(qū)以'\0'
結(jié)尾。現(xiàn)在介紹memcpy
和memmove
函數(shù)。
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
返回值:dest指向哪,返回的指針就指向哪
memcpy
函數(shù)從src
所指的內(nèi)存地址拷貝n
個(gè)字節(jié)到dest
所指的內(nèi)存地址,和strncpy
不同,memcpy
并不是遇到'\0'
就結(jié)束,而是一定會(huì)拷貝完n
個(gè)字節(jié)。這里的命名規(guī)律是,以str
開頭的函數(shù)處理以'\0'
結(jié)尾的字符串,而以mem
開頭的函數(shù)則不關(guān)心'\0'
字符,或者說這些函數(shù)并不把參數(shù)當(dāng)字符串看待,因此參數(shù)的指針類型是void *
而非char *
。
memmove
也是從src
所指的內(nèi)存地址拷貝n
個(gè)字節(jié)到dest
所指的內(nèi)存地址,雖然叫move但其實(shí)也是拷貝而非移動(dòng)。但是和memcpy
有一點(diǎn)不同,memcpy
的兩個(gè)參數(shù)src
和dest
所指的內(nèi)存區(qū)間如果重疊則無法保證正確拷貝,而memmove
卻可以正確拷貝。假設(shè)定義了一個(gè)數(shù)組char buf[20] = "hello world\n";
,如果想把其中的字符串往后移動(dòng)一個(gè)字節(jié)(變成"hhello world\n"
),調(diào)用memcpy(buf + 1, buf, 13)
是無法保證正確拷貝的:
例 25.1. 錯(cuò)誤的memcpy調(diào)用
#include <stdio.h>
#include <string.h>
int main(void)
{
char buf[20] = "hello world\n";
memcpy(buf + 1, buf, 13);
printf("%s", buf);
return 0;
}
在我的機(jī)器上運(yùn)行的結(jié)果是hhhllooworrd
。如果把代碼中的memcpy
改成memmove
則可以保證正確拷貝。memmove
可以這樣實(shí)現(xiàn):
void *memmove(void *dest, const void *src, size_t n)
{
char temp[n];
int i;
char *d = dest;
const char *s = src;
for (i = 0; i < n; i++)
temp[i] = s[i];
for (i = 0; i < n; i++)
d[i] = temp[i];
return dest;
}
借助于一個(gè)臨時(shí)緩沖區(qū)temp
,即使src
和dest
所指的內(nèi)存區(qū)間有重疊也能正確拷貝。思考一下,如果不借助于臨時(shí)緩沖區(qū)能不能正確處理重疊內(nèi)存區(qū)間的拷貝?
用memcpy
如果得到的結(jié)果是hhhhhhhhhhhhhh
倒不奇怪,可為什么會(huì)得到hhhllooworrd
這個(gè)奇怪的結(jié)果呢?根據(jù)這個(gè)結(jié)果猜測(cè)的一種可能的實(shí)現(xiàn)是:
void *memcpy(void *dest, const void *src, size_t n)
{
char *d = dest;
const char *s = src;
int *di;
const int *si;
int r = n % 4;
while (r--)
*d++ = *s++;
di = (int *)d;
si = (const int *)s;
n /= 4;
while (n--)
*di++ = *si++;
return dest;
}
在32位的x86平臺(tái)上,每次拷貝1個(gè)字節(jié)需要一條指令,每次拷貝4個(gè)字節(jié)也只需要一條指令,為了提高拷貝的效率,我們先處理完零頭然后4個(gè)字節(jié)4個(gè)字節(jié)地拷貝。注意這個(gè)實(shí)現(xiàn)并不正確,把void *
指針轉(zhuǎn)成int *
指針來訪問應(yīng)該考慮對(duì)齊的問題,請(qǐng)讀者自己實(shí)現(xiàn)一個(gè)更完善的版本。
我們來看一個(gè)跟memcpy
/memmove
類似的問題。下面的函數(shù)將兩個(gè)數(shù)組中對(duì)應(yīng)的元素相加,結(jié)果保存在第三個(gè)數(shù)組中。
void vector_add(const double *x, const double *y, double *result)
{
int i;
for (i = 0; i < 64; ++i)
result[i] = x[i] + y[i];
}
如果這個(gè)函數(shù)要在多處理器的計(jì)算機(jī)上執(zhí)行,編譯器可以做這樣的優(yōu)化:把這一個(gè)循環(huán)拆成兩個(gè)循環(huán),一個(gè)處理器計(jì)算i值從0到31的循環(huán),另一個(gè)處理器計(jì)算i值從32到63的循環(huán),這樣兩個(gè)處理器可以同時(shí)工作,使計(jì)算時(shí)間縮短一半。但是這樣的編譯優(yōu)化能保證得出正確結(jié)果嗎?假如result
和x
所指的內(nèi)存區(qū)間是重疊的,result[0]
其實(shí)是x[1]
,result[i]
其實(shí)是x[i+1]
,這兩個(gè)處理器就不能各干各的事情了,因?yàn)榈诙€(gè)處理器的工作依賴于第一個(gè)處理器的最終計(jì)算結(jié)果,這種情況下編譯優(yōu)化的結(jié)果是錯(cuò)的。這樣看來編譯器是不敢隨便做優(yōu)化了,那么多處理器提供的并行性就無法利用,豈不可惜?為此,C99引入restrict
關(guān)鍵字,如果程序員把上面的函數(shù)聲明為void vector_add(const double *restrict x, const double *restrict y, double *restrict result)
,就是告訴編譯器可以放心地對(duì)這個(gè)函數(shù)做優(yōu)化,程序員自己會(huì)保證這些指針?biāo)傅膬?nèi)存區(qū)間互不重疊。
由于restrict
是C99引入的新關(guān)鍵字,目前Linux的Man Page還沒有更新,所以都沒有restrict
關(guān)鍵字,本書的函數(shù)原型都取自Man Page,所以也都沒有restrict
關(guān)鍵字。但在C99標(biāo)準(zhǔn)中庫函數(shù)的原型都在必要的地方加了restrict
關(guān)鍵字,在C99中memcpy
的原型是void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
,就是告訴調(diào)用者,這個(gè)函數(shù)的實(shí)現(xiàn)可能會(huì)做些優(yōu)化,編譯器也可能會(huì)做些優(yōu)化,傳進(jìn)來的指針不允許指向重疊的內(nèi)存區(qū)間,否則結(jié)果可能是錯(cuò)的,而memmove
的原型是void *memmove(void *s1, const void *s2, size_t n);
,沒有restrict
關(guān)鍵字,說明傳給這個(gè)函數(shù)的指針允許指向重疊的內(nèi)存區(qū)間。在restrict
關(guān)鍵字出現(xiàn)之前都是用自然語言描述哪些函數(shù)的參數(shù)不允許指向重疊的內(nèi)存區(qū)間,例如在C89標(biāo)準(zhǔn)的庫函數(shù)一章開頭提到,本章描述的所有函數(shù),除非特別說明,都不應(yīng)該接收兩個(gè)指針參數(shù)指向重疊的內(nèi)存區(qū)間,例如調(diào)用sprintf
時(shí)傳進(jìn)來的格式化字符串和結(jié)果字符串的首地址相同,諸如此類的調(diào)用都是非法的。本書也遵循這一慣例,除非像memmove
這樣特別說明之外,都表示“不允許”。
關(guān)于restrict
關(guān)鍵字更詳細(xì)的解釋可以參考[BeganFORTRAN]。
字符串的拷貝也可以用strdup(3)
函數(shù),這個(gè)函數(shù)不屬于C標(biāo)準(zhǔn)庫,是POSIX標(biāo)準(zhǔn)中定義的,POSIX標(biāo)準(zhǔn)定義了UNIX系統(tǒng)的各種接口,包含C標(biāo)準(zhǔn)庫的所有函數(shù)和很多其它的系統(tǒng)函數(shù),在第 2 節(jié) “C標(biāo)準(zhǔn)I/O庫函數(shù)與Unbuffered I/O函數(shù)”將詳細(xì)介紹POSIX標(biāo)準(zhǔn)。
#include <string.h>
char *strdup(const char *s);
返回值:指向新分配的字符串
這個(gè)函數(shù)調(diào)用malloc
動(dòng)態(tài)分配內(nèi)存,把字符串s
拷貝到新分配的內(nèi)存中然后返回。用這個(gè)函數(shù)省去了事先為新字符串分配內(nèi)存的麻煩,但是用完之后要記得調(diào)用free
釋放新字符串的內(nèi)存。
#include <string.h>
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
返回值:dest指向哪,返回的指針就指向哪
strcat
把src
所指的字符串連接到dest
所指的字符串后面,例如:
char d[10] = "foo";
char s[10] = "bar";
strcat(d, s);
printf("%s %s\n", d, s);
調(diào)用strcat
函數(shù)后,緩沖區(qū)s
的內(nèi)容沒變,緩沖區(qū)d
中保存著字符串"foobar"
,注意原來"foo"
后面的'\0'
被連接上來的字符串"bar"
覆蓋掉了,"bar"
后面的'\0'
仍保留。
strcat
和strcpy
有同樣的問題,調(diào)用者必須確保dest
緩沖區(qū)足夠大,否則會(huì)導(dǎo)致緩沖區(qū)溢出錯(cuò)誤。strncat
函數(shù)通過參數(shù)n
指定一個(gè)長度,就可以避免緩沖區(qū)溢出錯(cuò)誤。注意這個(gè)參數(shù)n
的含義和strncpy
的參數(shù)n
不同,它并不是緩沖區(qū)dest
的長度,而是表示最多從src
緩沖區(qū)中取n
個(gè)字符(不包括結(jié)尾的'\0'
)連接到dest
后面。如果src
中前n
個(gè)字符沒有出現(xiàn)'\0'
,則取前n
個(gè)字符再加一個(gè)'\0'
連接到dest
后面,所以strncat
總是保證dest
緩沖區(qū)以'\0'
結(jié)尾,這一點(diǎn)又和strncpy
不同,strncpy
并不保證dest
緩沖區(qū)以'\0'
結(jié)尾。所以,提供給strncat
函數(shù)的dest
緩沖區(qū)的大小至少應(yīng)該是strlen(dest)+n+1
個(gè)字節(jié),才能保證不溢出。
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
返回值:負(fù)值表示s1小于s2,0表示s1等于s2,正值表示s1大于s2
memcmp
從前到后逐個(gè)比較緩沖區(qū)s1
和s2
的前n
個(gè)字節(jié)(不管里面有沒有'\0'
),如果s1
和s2
的前n
個(gè)字節(jié)全都一樣就返回0,如果遇到不一樣的字節(jié),s1
的字節(jié)比s2
小就返回負(fù)值,s1
的字節(jié)比s2
大就返回正值。
strcmp
把s1
和s2
當(dāng)字符串比較,在其中一個(gè)字符串中遇到'\0'
時(shí)結(jié)束,按照上面的比較準(zhǔn)則,"ABC"
比"abc"
小,"ABCD"
比"ABC"
大,"123A9"
比"123B2"
小。
strncmp
的比較結(jié)束條件是:要么在其中一個(gè)字符串中遇到'\0'
結(jié)束(類似于strcmp
),要么比較完n
個(gè)字符結(jié)束(類似于memcmp
)。例如,strncmp("ABCD", "ABC", 3)
的返回值是0,strncmp("ABCD", "ABC", 4)
的返回值是正值。
#include <strings.h>
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
返回值:負(fù)值表示s1小于s2,0表示s1等于s2,正值表示s1大于s2
這兩個(gè)函數(shù)和strcmp
/strncmp
類似,但在比較過程中忽略大小寫,大寫字母A和小寫字母a認(rèn)為是相等的。這兩個(gè)函數(shù)不屬于C標(biāo)準(zhǔn)庫,是POSIX標(biāo)準(zhǔn)中定義的。
#include <string.h>
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
返回值:如果找到字符c,返回字符串s中指向字符c的指針,如果找不到就返回NULL
strchr
在字符串s
中從前到后查找字符c
,找到字符c
第一次出現(xiàn)的位置時(shí)就返回,返回值指向這個(gè)位置,如果找不到字符c
就返回NULL
。strrchr
和strchr
類似,但是從右向左找字符c
,找到字符c
第一次出現(xiàn)的位置就返回,函數(shù)名中間多了一個(gè)字母r可以理解為Reverse。
#include <string.h>
char *strstr(const char *haystack, const char *needle);
返回值:如果找到子串,返回值指向子串的開頭,如果找不到就返回NULL
strstr
在一個(gè)長字符串中從前到后找一個(gè)子串(Substring),找到子串第一次出現(xiàn)的位置就返回,返回值指向子串的開頭,如果找不到就返回NULL。這兩個(gè)參數(shù)名很形象,在干草堆haystack
中找一根針needle
,按中文的說法叫大海撈針,顯然haystack
是長字符串,needle
是要找的子串。
搜索子串有一個(gè)顯而易見的算法,可以用兩層的循環(huán),外層循環(huán)把haystack
中的每一個(gè)字符的位置依次假定為子串的開頭,內(nèi)層循環(huán)從這個(gè)位置開始逐個(gè)比較haystack
和needle
的每個(gè)字符是否相同。想想這個(gè)算法最多需要做多少次比較?其實(shí)有比這個(gè)算法高效得多的算法,有興趣的讀者可以參考[算法導(dǎo)論]。
很多文件格式或協(xié)議格式中會(huì)規(guī)定一些分隔符或者叫界定符(Delimiter),例如/etc/passwd
文件中保存著系統(tǒng)的賬號(hào)信息:
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
...
每條記錄占一行,也就是說記錄之間的分隔符是換行符,每條記錄又由若干個(gè)字段組成,這些字段包括用戶名、密碼、用戶id、組id、個(gè)人信息、主目錄、登錄Shell,字段之間的分隔符是:號(hào)。解析這樣的字符串需要根據(jù)分隔符把字符串分割成幾段,C標(biāo)準(zhǔn)庫提供的strtok
函數(shù)可以很方便地完成分割字符串的操作。tok是Token的縮寫,分割出來的每一段字符串稱為一個(gè)Token。
#include <string.h>
char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);
返回值:返回指向下一個(gè)Token的指針,如果沒有下一個(gè)Token了就返回NULL
參數(shù)str
是待分割的字符串,delim
是分隔符,可以指定一個(gè)或多個(gè)分隔符,strtok
遇到其中任何一個(gè)分隔符就會(huì)分割字符串??聪旅娴睦?。
例 25.2. strtok
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[] = "root:x::0:root:/root:/bin/bash:";
char *token;
token = strtok(str, ":");
printf("%s\n", token);
while ( (token = strtok(NULL, ":")) != NULL)
printf("%s\n", token);
return 0;
}
$ ./a.out
root
x
0
root
/root
/bin/bash
結(jié)合這個(gè)例子,strtok
的行為可以這樣理解:冒號(hào)是分隔符,把"root:x::0:root:/root:/bin/bash:"
這個(gè)字符串分隔成"root"
、"x"
、""
、"0"
、"root"
、"/root"
、"/bin/bash"
、""
等幾個(gè)Token,但空字符串的Token被忽略。第一次調(diào)用要把字符串首地址傳給strtok
的第一個(gè)參數(shù),以后每次調(diào)用第一個(gè)參數(shù)只要傳NULL
就可以了,strtok
函數(shù)自己會(huì)記住上次處理到字符串的什么位置(顯然這是通過strtok
函數(shù)中的一個(gè)靜態(tài)指針變量記住的)。
用gdb
跟蹤這個(gè)程序,會(huì)發(fā)現(xiàn)str
字符串被strtok
不斷修改,每次調(diào)用strtok
把str
中的一個(gè)分隔符改成'\0'
,分割出一個(gè)小字符串,并返回這個(gè)小字符串的首地址。
(gdb) start
Breakpoint 1 at 0x8048415: file main.c, line 5.
Starting program: /home/akaedu/a.out
main () at main.c:5
5 {
(gdb) n
6 char str[] = "root:x::0:root:/root:/bin/bash:";
(gdb)
9 token = strtok(str, ":");
(gdb) display str
1: str = "root:x::0:root:/root:/bin/bash:"
(gdb) n
10 printf("%s\n", token);
1: str = "root\000x::0:root:/root:/bin/bash:"
(gdb)
root
11 while ( (token = strtok(NULL, ":")) != NULL)
1: str = "root\000x::0:root:/root:/bin/bash:"
(gdb)
12 printf("%s\n", token);
1: str = "root\000x\000:0:root:/root:/bin/bash:"
(gdb)
x
11 while ( (token = strtok(NULL, ":")) != NULL)
1: str = "root\000x\000:0:root:/root:/bin/bash:"
剛才提到在strtok
函數(shù)中應(yīng)該有一個(gè)靜態(tài)指針變量記住上次處理到字符串中的什么位置,所以不需要每次調(diào)用時(shí)都把字符串中的當(dāng)前處理位置傳給strtok
,但是在函數(shù)中使用靜態(tài)變量是不好的,以后會(huì)講到這樣的函數(shù)是不可重入的。strtok_r
函數(shù)則不存在這個(gè)問題,它的內(nèi)部沒有靜態(tài)變量,調(diào)用者需要自己分配一個(gè)指針變量來維護(hù)字符串中的當(dāng)前處理位置,每次調(diào)用時(shí)把這個(gè)指針變量的地址傳給strtok_r
的第三個(gè)參數(shù),告訴strtok_r
從哪里開始處理,strtok_r
返回時(shí)再把新的處理位置寫回到這個(gè)指針變量中(這是一個(gè)Value-result參數(shù))。strtok_r
末尾的r就表示可重入(Reentrant),這個(gè)函數(shù)不屬于C標(biāo)準(zhǔn)庫,是在POSIX標(biāo)準(zhǔn)中定義的。關(guān)于strtok_r
的用法Man Page上有一個(gè)很好的例子:
例 25.3. strtok_r
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
char *str1, *str2, *token, *subtoken;
char *saveptr1, *saveptr2;
int j;
if (argc != 4) {
fprintf(stderr, "Usage: %s string delim subdelim\n",
argv[0]);
exit(EXIT_FAILURE);
}
for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {
token = strtok_r(str1, argv[2], &saveptr1);
if (token == NULL)
break;
printf("%d: %s\n", j, token);
for (str2 = token; ; str2 = NULL) {
subtoken = strtok_r(str2, argv[3], &saveptr2);
if (subtoken == NULL)
break;
printf(" --> %s\n", subtoken);
}
}
exit(EXIT_SUCCESS);
}
$ ./a.out 'a/bbb///cc;xxx:yyy:' ':;' '/'
1: a/bbb///cc
--> a
--> bbb
--> cc
2: xxx
--> xxx
3: yyy
--> yyy
a/bbb///cc;xxx:yyy:
這個(gè)字符串有兩級(jí)分隔符,一級(jí)分隔符是:號(hào)或;號(hào),把這個(gè)字符串分割成a/bbb///cc
、xxx
、yyy
三個(gè)子串,二級(jí)分隔符是/,只有第一個(gè)子串中有二級(jí)分隔符,它被進(jìn)一步分割成a
、bbb
、cc
三個(gè)子串。由于strtok_r
不使用靜態(tài)變量,而是要求調(diào)用者自己保存字符串的當(dāng)前處理位置,所以這個(gè)例子可以在按一級(jí)分隔符分割整個(gè)字符串的過程中穿插著用二級(jí)分隔符分割其中的每個(gè)子串。建議讀者用gdb
的display
命令跟蹤argv[1]
、saveptr1
和saveptr2
,以理解strtok_r
函數(shù)的工作方式。
Man Page的BUGS部分指出了用strtok
和strtok_r
函數(shù)需要注意的問題:
-
這兩個(gè)函數(shù)要改寫字符串以達(dá)到分割的效果
-
這兩個(gè)函數(shù)不能用于常量字符串,因?yàn)樵噲D改寫.rodata
段會(huì)產(chǎn)生段錯(cuò)誤
-
在做了分割之后,字符串中的分隔符就被'\0'
覆蓋了
-
strtok
函數(shù)使用了靜態(tài)變量,它不是線程安全的,必要時(shí)應(yīng)該用可重入的strtok_r
函數(shù),以后再詳細(xì)介紹“可重入”和“線程安全”這兩個(gè)概念
1、出于練習(xí)的目的,strtok
和strtok_r
函數(shù)非常值得自己動(dòng)手實(shí)現(xiàn)一遍,在這個(gè)過程中不僅可以更深刻地理解這兩個(gè)函數(shù)的工作原理,也為以后理解“可重入”和“線程安全”這兩個(gè)重要概念打下基礎(chǔ)。
2、解析URL中的路徑和查詢字符串。動(dòng)態(tài)網(wǎng)頁的URL末尾通常帶有查詢,例如:
http://www.google.cn/search?complete=1&hl=zh-CN&ie=GB2312&q=linux&meta=
http://www.baidu.com/s?wd=linux&cl=3
比如上面第一個(gè)例子,http://www.google.cn/search
是路徑部分,?號(hào)后面的complete=1&hl=zh-CN&ie=GB2312&q=linux&meta=
是查詢字符串,由五個(gè)“key=value”形式的鍵值對(duì)(Key-value Pair)組成,以&隔開,有些鍵對(duì)應(yīng)的值可能是空字符串,比如這個(gè)例子中的鍵meta
。
現(xiàn)在要求實(shí)現(xiàn)一個(gè)函數(shù),傳入一個(gè)帶查詢字符串的URL,首先檢查輸入格式的合法性,然后對(duì)URL進(jìn)行切分,將路徑部分和各鍵值對(duì)分別傳出,請(qǐng)仔細(xì)設(shè)計(jì)函數(shù)接口以便傳出這些字符串。如果函數(shù)中有動(dòng)態(tài)分配內(nèi)存的操作,還要另外實(shí)現(xiàn)一個(gè)釋放內(nèi)存的函數(shù)。完成之后,為自己設(shè)計(jì)的函數(shù)寫一個(gè)Man Page