C語(yǔ)言探索之旅 | 第二部分第四課:字符串

作者 謝恩銘,公眾號(hào)「程序員聯(lián)盟」(微信號(hào):coderhub)。
轉(zhuǎn)載請(qǐng)注明出處。
原文:http://www.itdecent.cn/p/2be7006765ec

《C語(yǔ)言探索之旅》全系列

內(nèi)容簡(jiǎn)介


  1. 前言
  2. 字符類(lèi)型
  3. 顯示字符
  4. 字符串其實(shí)就是字符的數(shù)組
  5. 字符串的創(chuàng)建和初始化
  6. 從 scanf 函數(shù)取得一個(gè)字符串
  7. 操縱字符串的一些常用函數(shù)
  8. 總結(jié)
  9. 第二部分第五課預(yù)告

1. 前言


上一課 C語(yǔ)言探索之旅 | 第二部分第三課:數(shù)組 ,我們結(jié)束了關(guān)于數(shù)組的旅程。

好了,這課我不說(shuō)“廢話”,直接進(jìn)入主題(但又好像不是我的風(fēng)格...)。這一課我們還是會(huì)涉及一些指針和數(shù)組的知識(shí)。

字符串,這是一個(gè)編程的術(shù)語(yǔ),用來(lái)描述“一段文字”。

一個(gè)字符串,就是我們可以在內(nèi)存中以變量的形式儲(chǔ)存的“一段文字”。

比如,用戶名是一個(gè)字符串,“程序員聯(lián)盟”是一個(gè)字符串。

但是我們之前的課說(shuō)過(guò),呆萌的電腦兄只認(rèn)得數(shù)字,“眾里尋他千百度,電腦卻只認(rèn)得數(shù)。”

所以實(shí)際上,電腦是不認(rèn)得字母的,但是“古靈精怪”的計(jì)算機(jī)先驅(qū)們是如何使電腦可以“識(shí)別”字母呢?

接下來(lái)我們會(huì)看到,他們還是很聰明的。

2. 字符類(lèi)型


在這個(gè)小部分,我們把注意力先集中在字符類(lèi)型上。

如果你還記得,之前的課程中我們說(shuō)過(guò):char(有符號(hào)字符類(lèi)型)是用來(lái)儲(chǔ)存范圍從 -128 到 127 的數(shù)的;unsigned char(無(wú)符號(hào)字符類(lèi)型)用來(lái)儲(chǔ)存范圍從 0 到 255 的數(shù)。

注意: 雖然 char 類(lèi)型可以用來(lái)儲(chǔ)存數(shù)值,但是在 C語(yǔ)言中卻鮮少用 char 來(lái)儲(chǔ)存一個(gè)數(shù)。
通常,即使我們要表示的數(shù)比較小,我們也會(huì)用 int 類(lèi)型來(lái)儲(chǔ)存。
當(dāng)然了,用 int 來(lái)儲(chǔ)存比用 char 來(lái)儲(chǔ)存在內(nèi)存上更占空間。但是今天的電腦基本上是不缺那點(diǎn)內(nèi)存的,“有內(nèi)存任性嘛”。

char 類(lèi)型一般用來(lái)儲(chǔ)存一個(gè)字符,注意,是 一個(gè) 字符。

前面的課程也提到了,因?yàn)殡娔X只認(rèn)得數(shù)字,所以計(jì)算機(jī)先驅(qū)們建立了一個(gè)表格(比較常見(jiàn)的有 ASCII 表, 更完整一些的有 Unicode 表),用來(lái)約定字符和數(shù)字之間的轉(zhuǎn)換關(guān)系,例如大寫(xiě)字母 A 對(duì)應(yīng)的數(shù)字是 65。

C語(yǔ)言可以很容易地轉(zhuǎn)換字符和其對(duì)應(yīng)的數(shù)值。為了獲取到某個(gè)字符對(duì)應(yīng)的數(shù)值(電腦底層其實(shí)都是數(shù)值),只需要把該字符用單引號(hào)括起來(lái),像這樣:

'A'

在編譯的時(shí)候,'A' 會(huì)被替換成實(shí)際的數(shù)值 65。

我們來(lái)測(cè)試一下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char letter = 'A';

    printf("%d\n", letter);

    return 0;
}

程序輸出:

65

所以,我們可以確信大寫(xiě)字母 A 的對(duì)應(yīng)數(shù)值是 65。類(lèi)似地,大寫(xiě)字母 B 對(duì)應(yīng) 66, C 對(duì)應(yīng) 67, 以此類(lèi)推。

如果我們測(cè)試小寫(xiě)字母,那你會(huì)看到 a 和 A 的數(shù)值是不一樣的,小寫(xiě)字母 a 的數(shù)值是 97。

實(shí)際上,在大寫(xiě)字母和小寫(xiě)字母之間有一個(gè)很簡(jiǎn)單的轉(zhuǎn)換公式,就是

小寫(xiě)字母的數(shù)值 = 大寫(xiě)字母的數(shù)值 + 32

所以電腦是區(qū)分大小寫(xiě)的,看似呆萌的電腦兄還是可以的么。

大部分所謂“基礎(chǔ)”的字符都被編碼成 0 到 127 之間的數(shù)值了。在 ASCII 表(發(fā)音 [aski])的官網(wǎng) http://www.asciitable.com 上,我們可以看到大部分常用的字符的對(duì)應(yīng)數(shù)值。

當(dāng)然這個(gè)表我們也可以在其他網(wǎng)站上找到,比如維基百科,百度百科,等等。

3. 顯示字符


要顯示一個(gè)字符,最常用的還是 printf 函數(shù)啦。這個(gè)函數(shù)真的很強(qiáng)大,我們會(huì)經(jīng)常用到。

上面的例子中,我們用 %d 格式,所以顯示的是字符對(duì)應(yīng)的數(shù)值(%d 是整型)。如果要顯示字符實(shí)際的樣子,需要用到 %c 格式(c 是英語(yǔ) character 的首字母,表示“字符”):

int main(int argc, char *argv[])
{
    char letter = 'A';

    printf("%c\n", letter);

    return 0;
}

程序輸出:

A

當(dāng)然我們也可以用常見(jiàn)的 scanf 函數(shù)來(lái)請(qǐng)求用戶輸入一個(gè)字符,而后用 printf 函數(shù)打?。?/p>

int main(int argc, char *argv[])
{
    char letter = 0;

    scanf("%c", &letter);

    printf("%c\n", letter);

    return 0;
}

如果我輸入 C,那我將看到:

C
C

第一個(gè)字母 C 是我輸入給 scanf 函數(shù)的,第二個(gè) C 是 printf 函數(shù)打印的。

以上就是對(duì)于字符類(lèi)型 char 我們大致需要知道的,請(qǐng)牢記以下幾點(diǎn):

  • signed char(有符號(hào)字符類(lèi)型)用來(lái)儲(chǔ)存范圍從 -128 到 127 的數(shù)。
  • unsigned char(無(wú)符號(hào)字符類(lèi)型)用來(lái)儲(chǔ)存范圍從 0 到 255 的數(shù)。
  • C語(yǔ)言中,如果你沒(méi)寫(xiě) signed 或 unsigned 關(guān)鍵字,默認(rèn)情況下是 signed(有符號(hào))。
  • 計(jì)算機(jī)先驅(qū)們給電腦規(guī)定了一個(gè)表,電腦可以遵照里面的轉(zhuǎn)換原則來(lái)轉(zhuǎn)換字符和數(shù)值,一般這個(gè)表是 ASCII 表。
  • char 類(lèi)型只能儲(chǔ)存一個(gè)字符。
  • 'A' 在編譯時(shí)會(huì)被替換成實(shí)際的數(shù)值:65 。因此,我們使用單引號(hào)來(lái)獲得一個(gè)字符的值。

4. 字符串其實(shí)就是字符的數(shù)組


這一部分的內(nèi)容,就如這個(gè)小標(biāo)題所言。

事實(shí)上:一個(gè)字符串就是一個(gè)“字符的數(shù)組”,僅此而已。

到這里,你是否對(duì)字符串有了更直觀的理解呢?

如果我們創(chuàng)建一個(gè)字符數(shù)組:

char string[5];

然后我們?cè)跀?shù)組的第一個(gè)成員上儲(chǔ)存 'H',就是 string[0] = 'H',第二個(gè)成員上儲(chǔ)存 'E'(string[1] = 'H'),第三個(gè)成員上儲(chǔ)存 'L'(string[2] = 'L'),第四個(gè)成員儲(chǔ)存 'L'(string[3] = 'L'),第五個(gè)成員儲(chǔ)存 'O'(string[4] = 'O'),那么我們就構(gòu)造了一個(gè)字符串。

下圖對(duì)于字符串在內(nèi)存中是怎么存儲(chǔ)的,可以給出一個(gè)比較直觀的印象(注意: 實(shí)際的情況比這個(gè)圖演示的要略微復(fù)雜一些,待會(huì)兒會(huì)解釋):

上圖中,我們可以看到一個(gè)數(shù)組,擁有 5 個(gè)成員,在內(nèi)存上連續(xù)存放,構(gòu)成一個(gè)字符串 "HELLO"(表示“喂,你好”)。

對(duì)于每一個(gè)儲(chǔ)存在內(nèi)存地址上的字符,我們用了單引號(hào)把它括起來(lái),是為了突出實(shí)際上儲(chǔ)存的是數(shù)值,而不是字符。在內(nèi)存上,儲(chǔ)存的就是此字符對(duì)應(yīng)的數(shù)值。

實(shí)際上,一個(gè)字符串可不是就這樣結(jié)束了,上面的圖示其實(shí)不完整。

一個(gè)字符串必須在最后包含一個(gè)特殊的字符,稱為“字符串結(jié)束符”,它是 '\0',對(duì)應(yīng)的數(shù)值是 0。

“為什么要在字符串結(jié)尾加這么一個(gè)多余的字符呢?”

問(wèn)得好!

那是為了讓電腦知道一個(gè)字符串到哪里結(jié)束。

'\0' 用于告訴電腦:“停止,字符串到此結(jié)束了,不要再讀取了,先退下吧”。

因此,為了在內(nèi)存中存儲(chǔ)字符串 "HELLO"(5 個(gè)字符),用 5 個(gè)成員的字符數(shù)組是不夠的,需要 6 個(gè)!

因此每次創(chuàng)建字符串時(shí),需要記得在字符數(shù)組的結(jié)尾留一個(gè)字符給 '\0'。

忘記字符串結(jié)束符是 C語(yǔ)言中一個(gè)常見(jiàn)的錯(cuò)誤。

因此,下面才是正確展示我們的字符串 "HELLO" 在內(nèi)存中實(shí)際存放情況的示意圖:

如上圖所見(jiàn),這個(gè)字符串包含 6 個(gè)字符,而不是 5 個(gè)。

也多虧了這個(gè)字符串結(jié)束符 '\0',我們就無(wú)需記得字符串的長(zhǎng)度了,因?yàn)樗鼤?huì)告訴電腦字符串在哪里結(jié)束。

因此,我們就可以將我們的字符數(shù)組作為參數(shù)傳遞給函數(shù),而不需要傳遞字符數(shù)組的大小了。

這個(gè)好處只針對(duì)字符數(shù)組,你可以在傳遞給函數(shù)時(shí)將其寫(xiě)為 char * 或者 char[] 類(lèi)型。

對(duì)于其他類(lèi)型的數(shù)組,我們總是要在某處記錄下它的長(zhǎng)度。

5. 字符串的創(chuàng)建和初始化


如果我們想要用 "Hello" 來(lái)初始化字符數(shù)組 string,我們可以用以下的方式來(lái)實(shí)現(xiàn)。當(dāng)然,有點(diǎn)沒(méi)效率:

char string[6];  // 六個(gè) char 構(gòu)成的數(shù)組,為了儲(chǔ)存:H-e-l-l-o + \0

string[0] = 'H';
string[1] = 'e';
string[2] = 'l';
string[3] = 'l';
string[4] = 'o';
string[5] = '\0';

雖然是笨辦法,但至少行得通。

我們用 printf 函數(shù)來(lái)測(cè)試一下。

要使 printf 函數(shù)能顯示字符串,我們需要用到 %s 這個(gè)符號(hào)(s 就是英語(yǔ) string 的首字母,表示“字符串”):

#include <stdio.h>

int main(int argc, char *argv[])
{

    char string[6];  // 六個(gè) char 構(gòu)成的數(shù)組,為了儲(chǔ)存:H-e-l-l-o + \0

    string[0] = 'H';
    string[1] = 'e';
    string[2] = 'l';
    string[3] = 'l';
    string[4] = 'o';
    string[5] = '\0';

    // 顯示字符串內(nèi)容
    printf("%s\n", string);

    return 0;
}

程序輸出:

Hello

如果我們的字符串內(nèi)容多起來(lái),上面的方法就更顯拙劣了。其實(shí)啊,初始化字符串還有更簡(jiǎn)單的一種方式(讀者:“你好‘奸詐’,不早講,害我寫(xiě)代碼這么辛苦...”):

int main(int argc, char *argv[])
{
    char string[] = "Hello";  // 字符數(shù)組的長(zhǎng)度會(huì)被自動(dòng)計(jì)算

    printf("%s\n", string);

    return 0;
}

以上程序的第一行,我們寫(xiě)了一個(gè)char [] 類(lèi)型的變量,其實(shí)也可以寫(xiě)成 char * ,同樣是可以運(yùn)行的:

char *string = "Hello";

這種方法就比之前一個(gè)字符一個(gè)字符初始化的方法高大上多了,因?yàn)橹恍枰陔p引號(hào)里輸入你想要?jiǎng)?chuàng)建的字符串,C語(yǔ)言的編譯器就很智能地為你計(jì)算好字符串的大小。

編譯器計(jì)算你輸入的字符的數(shù)目,然后再加上一個(gè) '\0' 的長(zhǎng)度(是 1),就把你的字符串里的字符一個(gè)接一個(gè)寫(xiě)到內(nèi)存某個(gè)地方,在最后加上 '\0' 這個(gè)字符串結(jié)束符,就像我們剛才用第一種方式自己一步步做的。

但是簡(jiǎn)便也有缺陷。我們會(huì)發(fā)現(xiàn),對(duì)于字符數(shù)組來(lái)說(shuō),這種方法只能用于初始化,你在之后的程序中就不能再用這種方式來(lái)給整個(gè)數(shù)組賦值了,比如你不能這樣:

char string[] = "Hello";
string = "nihao";   // --> 出錯(cuò)!

只能一個(gè)字符一個(gè)字符地改,例如:

string[0] = 'j';   // --> 可以!

但是問(wèn)題又來(lái)了,對(duì)于用 char * 來(lái)聲明的字符串,我們可以在之后整個(gè)重新賦值,但是不可以單獨(dú)修改某個(gè)字符:

char *string = "Hello";
string = "nihao";   // --> 可以!

這樣是可以的。但是如果修改其中的一個(gè)字符,就不可以:

string[1] = 'a'; // --> 出錯(cuò)!

很有意思吧。大家可以親自動(dòng)手試試。所以這里就引出了一個(gè)話題:

指針和數(shù)組根本就是兩碼事!

為什么會(huì)出現(xiàn)上述的情況呢?(請(qǐng)注意:下面的這塊內(nèi)容比較難,如果看不懂,也可以暫時(shí)跳過(guò)。不過(guò)建議測(cè)試一下給出的代碼)。

那是因?yàn)椋?/p>

char stringArray[] = "Hello";

這樣聲明的是一個(gè)字符數(shù)組,里面的字符串是儲(chǔ)存在內(nèi)存的變量區(qū),是在棧上,所以可以修改每個(gè)字符的內(nèi)容,但是不可以通過(guò)數(shù)組名整體修改:

stringArray = "nihao";  // --> 出錯(cuò)!

只能一個(gè)個(gè)單獨(dú)改:

stringArray[0] = 'a';  // --> 可以!

因?yàn)橹暗恼n程里說(shuō)過(guò),stringArray 這個(gè)數(shù)組的名字表示的是數(shù)組首元素的首地址。

char *stringPointer = "Hello";

這樣聲明的是一個(gè)指針,stringPointer 是指針的名字。指針變量在 32 位系統(tǒng)下,永遠(yuǎn)占 4 個(gè) byte(字節(jié));在 64 位系統(tǒng)下,永遠(yuǎn)占 8 個(gè) byte(字節(jié))。其值為某一個(gè)內(nèi)存的地址。

所以 stringPointer 里面只是存放了一個(gè)地址,這個(gè)地址上存放的字符串是常量字符串。這個(gè)常量字符串存放在內(nèi)存的靜態(tài)區(qū),不可以更改。

和上面的字符數(shù)組情況不一樣,上面的字符數(shù)組是本身存放了那一整個(gè)字符串。

stringPointer[0] = 'a';  // --> 出錯(cuò)!

但是可以改變 stringPointer 指針的指向:

stringPointer = "nihao";  // --> 可以?。ㄒ?yàn)榭梢孕薷闹羔樦赶蚰睦铮?

大家可以自己測(cè)試一下:

char *n1 = "it";
char *n2 = "it";

printf("%p\n%p\n", n1, n2);  //用 %p 查看地址

會(huì)發(fā)現(xiàn)二者的結(jié)果是一樣的,指向同一個(gè)地址!

再進(jìn)一步測(cè)試(生命在于折騰...):

char *n1 = "it";
char *n2 = "it";
 
printf("%p\n%p\n", n1, n2);
 
n1 = "haha";
 
printf("%p\n%p\n", n1, n2);

你會(huì)發(fā)現(xiàn)以上程序,指針 n2 所指向的地址一直沒(méi)變,而 n1 在經(jīng)過(guò)

n1 = "haha";

之后,它所指向的地址就改變了。

經(jīng)過(guò)上面地分析,可能很多朋友還是有點(diǎn)暈,特別是可能不太清楚內(nèi)存各個(gè)區(qū)域的區(qū)別。

如果有興趣深入探究,既可以自己去看相關(guān)的 C語(yǔ)言書(shū)籍。也可以參考下表和一些解釋,如果暫時(shí)不想把自己搞得更暈,可以跳過(guò),以后講到相關(guān)內(nèi)容時(shí)自然更好理解。

名稱 內(nèi)容
代碼段 可執(zhí)行代碼、字符串常量
數(shù)據(jù)段 已初始化全局變量、已初始化全局靜態(tài)變量、局部靜態(tài)變量、常量數(shù)據(jù)
BSS 段 未初始化全局變量,未初始化全局靜態(tài)變量
局部變量、函數(shù)參數(shù)
動(dòng)態(tài)內(nèi)存分配

一般情況下,一個(gè)可執(zhí)行二進(jìn)制程序(更確切的說(shuō),在 Linux 操作系統(tǒng)下為一個(gè)進(jìn)程單元)在存儲(chǔ)(沒(méi)有調(diào)入到內(nèi)存運(yùn)行)時(shí)擁有 3 個(gè)部分,分別是代碼段、數(shù)據(jù)段和 BSS 段。

這 3 個(gè)部分一起組成了該可執(zhí)行程序的文件。

(1) 代碼段(code segment / text segment):存放 CPU 執(zhí)行的機(jī)器指令。通常代碼段是可共享的,這使得需要頻繁被執(zhí)行的程序只需要在內(nèi)存中擁有一份拷貝即可。代碼段也通常是只讀的,這樣可以防止其他程序意外地修改其指令。另外,代碼段還規(guī)劃了局部數(shù)據(jù)所申請(qǐng)的內(nèi)存空間信息。
代碼段通常是指用來(lái)存放程序執(zhí)行代碼的一塊內(nèi)存區(qū)域。這部分區(qū)域的大小在程序運(yùn)行前就已經(jīng)確定,并且內(nèi)存區(qū)域通常屬于只讀,某些架構(gòu)也允許代碼段為可寫(xiě),即允許修改程序。在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。

(2) 數(shù)據(jù)段(data segment):或稱全局初始化數(shù)據(jù)段/靜態(tài)數(shù)據(jù)段(initialized data segment / data segment)。該段包含了在程序中明確被初始化的全局變量、靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)和常量數(shù)據(jù)。

(3) 未初始化數(shù)據(jù)段:也稱 BSS(Block Started by Symbol)。該段存入的是全局未初始化變量、靜態(tài)未初始化變量。

而當(dāng)程序被加載到內(nèi)存單元時(shí),則需要另外兩個(gè)域:棧和堆。

(4) 棧(stack):存放函數(shù)的參數(shù)值、局部變量的值,以及在進(jìn)行任務(wù)切換時(shí)存放當(dāng)前任務(wù)的上下文內(nèi)容。

(5) 堆(heap):用于動(dòng)態(tài)內(nèi)存分配(之后的課程馬上會(huì)講到),就是使用 malloc / free 系列函數(shù)來(lái)管理的內(nèi)存空間。

在將應(yīng)用程序加載到內(nèi)存空間執(zhí)行時(shí),操作系統(tǒng)負(fù)責(zé)代碼段、數(shù)據(jù)段和 BSS 段的加載,并將在內(nèi)存中為這些段分配空間。

棧也由操作系統(tǒng)分配和管理,而不需要程序員顯式地管理;堆由程序員自己管理,即顯式地申請(qǐng)和釋放空間。

很多 C語(yǔ)言的初學(xué)者搞不懂指針和數(shù)組到底有什么樣的關(guān)系。

現(xiàn)在就告訴大家:指針和數(shù)組之間沒(méi)有任何關(guān)系!它們是“清白”的...

  • 指針就是指針:指針變量在 32 位系統(tǒng)下,永遠(yuǎn)占 4 個(gè) byte(字節(jié));在 64 位系統(tǒng)下,永遠(yuǎn)占 8 個(gè) byte(字節(jié))。其值為某一個(gè)內(nèi)存的地址。指針可以指向任何地方,但不是任何地方你都能通過(guò)這個(gè)指針變量訪問(wèn)到。

  • 數(shù)組就是數(shù)組:其大小與元素的類(lèi)型和個(gè)數(shù)有關(guān)。定義數(shù)組時(shí)必須指定其元素的類(lèi)型和個(gè)數(shù)。數(shù)組可以存任何類(lèi)型的數(shù)據(jù),但不能存函數(shù)。

推薦大家去看《C語(yǔ)言深度解剖》這本只有 100 多頁(yè)的 PDF,是國(guó)人寫(xiě)的,里面對(duì)于指針和數(shù)組分析得很全面。

不禁感嘆,C語(yǔ)言果然是博(xiang)大(dang)精(ke)深(pa)。

6. 從 scanf 函數(shù)取得一個(gè)字符串


我們可以用 scanf 函數(shù)獲取用戶輸入的一個(gè)字符串,也要用到 %s 符號(hào)。

但是有一個(gè)問(wèn)題:就是你不能知道用戶究竟會(huì)輸入多少字符。

假如我們的程序是問(wèn)用戶他的名字是什么。那么他可能回答 Tom,只有三個(gè)字符,或者 Bruce LI,就有 8 個(gè)字符了。

所以我們只能用一個(gè)足夠大的數(shù)組來(lái)存儲(chǔ)名字,例如 char[100]。你會(huì)說(shuō)這樣太浪費(fèi)內(nèi)存了,但是前面我們也說(shuō)過(guò)了,目前的電腦一般不在乎這點(diǎn)內(nèi)存。

所以我們的程序會(huì)是這樣:

int main(int argc, char *argv[])
{
    char name[100];

    printf("請(qǐng)問(wèn)您叫什么名字 ? ");

    scanf("%s", name);

    printf("您好, %s, 很高興認(rèn)識(shí)您!\n", name);

    return 0;
}

運(yùn)行程序:

請(qǐng)問(wèn)您叫什么名字?Oscar
您好,Oscar,很高興認(rèn)識(shí)您!

7. 操縱字符串的一些常用函數(shù)


字符串在 C語(yǔ)言里是很常用的。事實(shí)上,此刻你在電腦或手機(jī)屏幕上看到的這些單詞、句子等,都是在電腦內(nèi)存里的字符數(shù)組。

為了方便我們操縱字符串,C語(yǔ)言的設(shè)計(jì)者們?cè)?string 這個(gè)標(biāo)準(zhǔn)庫(kù)中已經(jīng)寫(xiě)好了很多函數(shù),可供我們使用。

當(dāng)然在這以前,需要在你的 .c 源文件中引入這個(gè)頭文件:

#include <string.h>

下面我們就來(lái)介紹它們之中最常用的一些吧:

strlen:計(jì)算字符串的長(zhǎng)度


strlen 函數(shù)返回一個(gè)字符串的長(zhǎng)度(不包括 '\0')。

為什么名字是 strlen?其實(shí)很好記:

  • str 是 string(表示“字符串”)的前三個(gè)字母。
  • len 是 length(表示“長(zhǎng)度”)的前三個(gè)字母。

因此,strlen 就是“字符串長(zhǎng)度”。

函數(shù)原型是這樣:

size_t strlen(const char* string);

注意:size_t 是一個(gè)特殊的類(lèi)型,它意味著函數(shù)返回一個(gè)對(duì)應(yīng)大小的數(shù)目。
不是像 int,char,long,double 之類(lèi)的基本類(lèi)型,而是一個(gè)被“創(chuàng)造”出來(lái)的類(lèi)型。
在接下來(lái)的課程中我們就會(huì)學(xué)到如何創(chuàng)建自己的變量類(lèi)型。
暫時(shí)說(shuō)來(lái),我們先滿足于將 strlen 函數(shù)的返回值存到一個(gè) int 變量里(電腦會(huì)把 size_t 自動(dòng)轉(zhuǎn)換成 int)。當(dāng)然,嚴(yán)格來(lái)說(shuō)應(yīng)該用 size_t 類(lèi)型,但是我們這里暫時(shí)不深究了。

函數(shù)的參數(shù)是 const char * 類(lèi)型,之前的課程中我們學(xué)過(guò),const(只讀的變量)表明此類(lèi)型的變量是不能被改變的,所以函數(shù) strlen 并不會(huì)改變它的參數(shù)的值。

寫(xiě)個(gè)程序測(cè)試一下 strlen 函數(shù):

#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    char string[] = "Hello";
    int stringLength = 0;

    // 將字符串的長(zhǎng)度儲(chǔ)存到 stringLength 中
    stringLength = strlen(string);

    printf("字符串 %s 中有 %d 個(gè)字符\n", string, stringLength);

    return 0;
}

程序運(yùn)行,顯示:

字符串 Hello 中有 5 個(gè)字符

當(dāng)然了,這個(gè) strlen 函數(shù),其實(shí)我們自己也可以很容易地實(shí)現(xiàn)。只需要用一個(gè)循環(huán),從開(kāi)始一直讀入字符串中的字符,計(jì)算數(shù)目,一直讀到 '\0' 字符結(jié)束循環(huán)。

我們就來(lái)實(shí)現(xiàn)我們自己的 strlen 函數(shù)好了:

#include <string.h>
#include <stdio.h>

int stringLength(const char *string);

int main(int argc, char *argv[])
{
    char string[] = "Hello";
    int length = 0;

    length = stringLength(string);

    printf("字符串 %s 中有 %d 個(gè)字符\n", string, length);

    return 0;
}

int stringLength(const char *string)
{
    int charNumber = 0;
    char currentChar = 0;

    do
    {
        currentChar = string[charNumber];
        charNumber++;
    } while (currentChar != '\0');  // 我們做循環(huán),直到遇到 '\0',跳出循環(huán)

    charNumber--;  // 我們將 charNumber 減一,使其不包含 '\0' 的長(zhǎng)度

    return charNumber;
}

程序輸出:

字符串 Hello 中有 5 個(gè)字符

strcpy:把一個(gè)字符串的內(nèi)容復(fù)制到另一個(gè)字符串里


為什么名字是 strcpy?其實(shí)很好記:

  • str 是 string 的前三個(gè)字母。
  • cpy 是 copy(表示“拷貝,復(fù)制”)的縮寫(xiě)。

因此,strcpy 就是“字符串拷貝”。

函數(shù)原型:

char* strcpy(char* targetString, const char* stringToCopy);

這個(gè)函數(shù)有兩個(gè)參數(shù):

  • targetString:這是一個(gè)指向字符數(shù)組的指針,我們要復(fù)制字符串到這個(gè)字符數(shù)組里。

  • stringToCopy:這是一個(gè)指向要被復(fù)制的字符串的指針。

函數(shù)返回一個(gè)指向 targetString 的指針,通常我們不需要獲取這個(gè)返回值。

用以下程序測(cè)試此函數(shù):

#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    /* 我們創(chuàng)建了一個(gè)字符數(shù)組 string,里面包含了幾個(gè)字符。
    我們又創(chuàng)建了另一個(gè)字符數(shù)組 copy,包含 100 個(gè)字符,為了足夠容納拷貝過(guò)來(lái)的字符 */
    char string[] = "Hello", copy[100] = {0};

    strcpy(copy, string);  // 我們把 string 復(fù)制到 copy 中

    // 如果一切順利,copy 的值應(yīng)該和 string 是一樣的
    printf("string 是 %s\n", string);
    printf("copy 是 %s\n", copy);

    return 0;
}

程序輸出:

string 是 Hello
copy 是 Hello

如果我們的 copy 數(shù)組的長(zhǎng)度小于 6,那么程序會(huì)出錯(cuò),因?yàn)?string 的總長(zhǎng)度是 6(最后有一個(gè) '\0' 字符串結(jié)束符)。

strcpy 的原理圖解如下:

strcat:連接兩個(gè)字符串


為什么名字是 strcat?其實(shí)很好記:

  • str 是 string 的前三個(gè)字母。
  • cat 是 concatenate (表示“連結(jié),使連鎖”)的縮寫(xiě)。

因此,strcat 就是“字符串連結(jié)”。

strcat 函數(shù)的作用是連接兩個(gè)字符串,就是把一個(gè)字符串接到另一個(gè)的結(jié)尾。

函數(shù)原型:

char* strcat(char* string1, const char* string2);

因?yàn)?string2 是 const 類(lèi)型,所以我們就想到了,這個(gè)函數(shù)肯定是將 string2 的內(nèi)容接到 string1 的結(jié)尾,改變了 string1 所指向的字符指針,然后返回指向 string1 所指字符數(shù)組的指針。

略微有點(diǎn)拗口,但不難理解吧。

寫(xiě)個(gè)程序測(cè)試一下:

#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    /* 我們創(chuàng)建了兩個(gè)字符串,字符數(shù)組 string1 需要足夠長(zhǎng),因?yàn)槲覀円獙?string2 的內(nèi)容接到其后 */
    char string1[100] = "Hello ", string2[] = "Oscar!";

    strcat(string1, string2);  // 將 string2 接到 string1 后面

    // 如果一切順利,那么 string1 的值應(yīng)該會(huì)變?yōu)?"Hello Oscar!"
    printf("string1 是 %s\n", string1);

    // string2 沒(méi)有變
    printf("string2 始終是 %s\n", string2);

    return 0;
}

程序輸出:

string1 是 Hello Oscar!
string2 始終是 Oscar!

strcat 的原理如下:

當(dāng) strcat 函數(shù)將 string2 連接到 string1 的尾部時(shí),它需要先刪去 string1 字符串最后的 '\0'。

strcmp:比較兩個(gè)字符串


為什么名字是 strcmp?其實(shí)很好記:

  • str 是string 的前三個(gè)字母。
  • cmp 是 compare(英語(yǔ)“比較”)的縮寫(xiě)。

因此,strcmp 就是“字符串比較”。

函數(shù)原型:

int strcmp(const char* string1, const char* string2);

可以看到,strcmp 函數(shù)不能改變參數(shù) string1 和 string2,因?yàn)樗鼈兌际?const 類(lèi)型。

這次,函數(shù)的返回值有用了。strcmp 返回:

  • 0:當(dāng)兩個(gè)字符串相等時(shí)。
  • 非零的整數(shù)(負(fù)數(shù)或正數(shù)):當(dāng)兩個(gè)字符串不等時(shí)。

用以下程序測(cè)試 strcmp 函數(shù):

#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    char string1[] = "Text of test", string2[] = "Text of test";

    if (strcmp(string1, string2) == 0) // 如果兩個(gè)字符串相等
    {
        printf("兩個(gè)字符串相等\n");
    }
    else
    {
        printf("兩個(gè)字符串不相等\n");
    }

    return 0;
}

程序輸出:

兩個(gè)字符串相等

sprintf:向一個(gè)字符串寫(xiě)入


當(dāng)然,這個(gè)函數(shù)其實(shí)不是在 string.h 這個(gè)頭文件里,而是在 stdio.h 頭文件里。但是它也與字符串的操作有關(guān),所以我們也介紹一下,而且這個(gè)函數(shù)是很常用的。

看到 sprintf 函數(shù)的名字,大家是否想到了printf 函數(shù)呢?

printf 函數(shù)是向標(biāo)準(zhǔn)輸出(一般是屏幕)寫(xiě)入東西,而 sprintf 是向一個(gè)字符串寫(xiě)入東西。最前面的 s 就是英語(yǔ) string 的首字母。

寫(xiě)個(gè)程序測(cè)試一下此函數(shù):

#include <stdio.h>

int main(int argc, char *argv[])
{
    char string[100];
    int age = 18;

    // 我們向 string 里寫(xiě)入"你18歲了"
    sprintf(string, "你 %d 歲了", age);

    printf("%s\n", string);

    return 0;
}

程序輸出:

你 18 歲了

其他常用的還有一些函數(shù),如 strstr(在字符串中查找一個(gè)子串),strchr(在字符串里查找一個(gè)字符),等等,我們就不一一介紹了。

8. 總結(jié)


  1. 電腦不認(rèn)識(shí)字符,它只認(rèn)識(shí)數(shù)字(0 和 1)。為了解決這個(gè)問(wèn)題,計(jì)算機(jī)先驅(qū)們用一個(gè)表格規(guī)定了字符與數(shù)值的對(duì)應(yīng)關(guān)系,最常用的是 ASCII 表和 Unicode 表。

  2. 字符類(lèi)型 char 用來(lái)存儲(chǔ)一個(gè)字符,且只能存儲(chǔ)一個(gè)字符。實(shí)際上存儲(chǔ)的是一個(gè)數(shù)值,但是電腦會(huì)在顯示時(shí)將其轉(zhuǎn)換成對(duì)應(yīng)的字符。

  3. 為了創(chuàng)建一個(gè)詞或一句話,我們需要構(gòu)建一個(gè)字符串,我們用字符數(shù)組來(lái)實(shí)現(xiàn)。

  4. 所有的字符串都是以 '\0' 結(jié)尾,這個(gè)特殊的字符 '\0' 標(biāo)志著字符串的結(jié)束。

  5. 在 string 這個(gè) C語(yǔ)言標(biāo)準(zhǔn)庫(kù)中,有很多操縱字符串的函數(shù),只需要引入頭文件 string.h 即可。

9. 第二部分第五課預(yù)告

今天的課就到這里,一起加油咯。

下一課:C語(yǔ)言探索之旅 | 第二部分第五課:預(yù)處理


我是 謝恩銘,公眾號(hào)「程序員聯(lián)盟」(微信號(hào):coderhub)運(yùn)營(yíng)者,慕課網(wǎng)精英講師 Oscar 老師,終生學(xué)習(xí)者。
熱愛(ài)生活,喜歡游泳,略懂烹飪。
人生格言:「向著標(biāo)桿直跑」

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容