大家好,我是KookNut39,在CSDN寫文,分享一些自己認為在學習程序中比較重要的東西,致力于幫助初學者入門,希望可以幫助你進步,最近在更新C/C++方面的知識,感興趣的歡迎關注博主,可以去專欄查看之前的文章,希望未來能和大家共同探討技術,
對于學習C語言或者C++的同學,如果對字串的操作不熟悉,那就無法在學習的程序中更進一步,因為這是非常基礎,也是非常重要的一個環節,希望博主這篇文章中的例子,可以教你去搞定字串操作!對于初學者來說,一下要記住這么多東西是很難的,所以【建議收藏】,方便以后用到的時候查找,
新學習了制作動圖,以前結果演示都是干巴巴的一張截圖,今天新插了動圖,感覺很爽,hhhhhhhhhhhh,喜歡文章的話,一鍵三連支持博主歐🤞🤞🤞!!!
文章目錄
- 1. 記憶體初始化memset
- 2. 字串長度計算strlen
- 3. 字串復制strcpy
- 4. 字串復制memcpy
- 5. 字串復制memmove
- 6. 字串比較strcmp
- 7. 字串比較memcmp
- 8. 字串拼接strcat
- 9. 字串中搜索字符strchr
- 10. 字串中搜索子串strstr
廢話不多說,進入正題,字串操作幾乎是我們在C或C++學習的程序中都會遇到困惑的地方,我們得不到自己想要的輸出結果, 或者得到了,但沒完全得到,就比如我們得到了我們想要的字串,但是緊接著也會輸出一些亂碼,類似如圖所示的情況:
那么我們應該如何去搞定這些字串操作的bug呢?接下來咱們來細細討論:
對于一個字串陣列或者char型指標,我們一定應該給它初始化,因為不初始化的話,里面都是一些默認初始化的值,可以認為里面是一些“垃圾”資料,那么我們首先來介紹我們的第一個函式:
1. 記憶體初始化memset
我一般用memset這個函式來給動態申請的記憶體進行初始化,因為如果是堆疊區記憶體,我們一般就在變數宣告的同時就進行初始化,所以在堆區申請的記憶體,我們需要手動來給它初始化,我們來看一下函式的宣告:
void *
memset (
void *Dest,
int Value,
ACPI_SIZE Count)
從函式的宣告來看,總共有三個引數,我們來解釋一下這三個引數:
void *Dest:這是我們要進行初始化的指標,在這里被默認為void*指標
int Value:一個int型變數,也就是我們需要給這片記憶體初始化的值,一般傳0
ACPI_SIZE Count:這是我們需要初始化的記憶體長度,一般是和申請記憶體的大小相同
我們一般把一塊申請到的記憶體,使用之前,初始化為0,以便后續使用,特別說明:memset 不止針對字串指標,其它的指標也可以使用,我們來看一個簡單的使用舉例:
//對于堆疊區記憶體,我們在申請到的時候,就對其初始化
char Des[15] = { 0 };
//堆區記憶體,我們申請到之后,使用memset初始化為0
char* ptr1 = (char*)malloc(15);
memset(ptr1, 0, 15);//將我們申請到的15個位元組初始化為0
對于堆疊區記憶體初始化,我們看一下這個圖:

對于堆區記憶體初始化,我們來看一下初始化執行的效果:

2. 字串長度計算strlen
一個字串長度的計算會被用來干什么呢?也許拷貝字串的時候會用到的吧,或者說來計算一下輸入了多少字符?反正不管怎么說,這都是非常通用的字串操作函式,來看一下它的宣告:
ACPI_SIZE
strlen (
const char *String)
我們可以看到回傳值是一個數值型別,而這個函式的引數只有一個,多少有點敷衍了,,,但是流程還是要走的哈,所以我們再來看一下引數代表的意義:
const char *String:特別注意是一個const char*,有關這個的概念,
在我之前講C語言指標的時候講過,意思就是字串內容不允許修改,這是肯定的,我們傳入字串指標是為了讓它計算長度,肯定不能被修改,
其實strlen的實作非常簡單,在內部申請了一個區域變數用來計數,然后用一個while回圈來判斷,如果這個字串不為 ‘\0’ ,那就指標后移,計數器++,否則,字串長度計算結束,回傳這個計數器變數的值,
現在我們來用它計算一個字串的長度來看一下:
//初始化為HelloWorld
char Des[] = "HelloWorld";//注意這是11個位元組,因為還有 \0 結尾
//計算字串的長度
size_t StringLength = strlen(Des);
return 0;
計算這個字串的結果:

從上圖中我們應該能看到兩個問題,首先我們默認初始化的Des字串,里面有11個字符,但是最后我們發現計算出來的StringLength只算出了10,這個原因就是因為在strlen實作的時候,沒有把 \0 的長度計算進去,所以需要注意:strlen計算出來的字串長度不包含 \0 ,這個要切記,以后在寫代碼程序中會經常用到的,
3. 字串復制strcpy
當我們想要將一個源字串拷貝一個備份出來的時候,我們依然有可以使用的字串操作函式,我們也可以預想的到,如果想要實作一個復制操作,那最起碼得有源字串和目標字串,這樣才能說是拷貝吧,我們在這里先說strcpy函式,我們還是慣例先來看看它的宣告:
char *
strcpy (
char *DstString,
const char *SrcString)
我們看到這個函式的回傳值是一個char型指標,但是我們一般不取它的回傳值,因為在傳參時候的DstString指向的就是我們要傳回的字串值,來看一下這兩個引數分別代表什么:
char *DstString:目標字串指標,需要拷貝到的地方
const char *SrcString:源字串指標,被拷貝的物件
看一下使用舉例:
//對于堆疊區記憶體,我們在申請到的時候,就對其初始化
char SrcString[] = "HelloWorld";
//堆區記憶體,15個位元組的長度
char* DstString = (char*)malloc(15);
memset(DstString, 0, 15);//將我們申請到的15個位元組初始化為0
strcpy(DstString, SrcString);
再來看一下拷貝字串的效果:

在使用strcpy的程序中需要注意:那就是對于目標記憶體長度的申請,因為在之前strlen計算長度的時候,并沒有把字串的結尾null給計算進去,所以申請記憶體時候要多申請一個位元組,用來存放字串結尾字符,這是因為strcpy在內部實作的時候,就會在字串復制結束之后,在DstString的最后不上\0,也就是null字符來結尾,這個需要占用一個位元組,
4. 字串復制memcpy
在使用strcpy的程序中,我們發現我們只能按照字串的null字符結尾來控制復制操作的停止,這顯得有點被動了,那我們能不能變得主動出擊呢?當然是可以的,我們接下來介紹的memcpy就是可以控制我們對于復制長度的控制:
void *
memcpy (
void *Dest,
const void *Src,
ACPI_SIZE Count)
我們還是來看一下各個引數的詳細意思吧:
void *Dest:需要拷貝到的目標指標首地址
const void *Src:被拷貝的指標首地址
ACPI_SIZE Count:當前指標型別被拷貝的個數
比如說我們指向從源字串里面拷貝5個字符過來,那我們只需要將第三引數傳個5就OK了,看一下下面這個例子:
//對于堆疊區記憶體,我們在申請到的時候,就對其初始化
char SrcString[] = "HelloWorld";
//堆區記憶體,15個位元組的長度
char* DstString = (char*)malloc(15);
memset(DstString, 0, 15);//將我們申請到的15個位元組初始化為0
//我們只想復制前五個位元組 Hello,所以這樣寫
memcpy(DstString, SrcString,5);
我們可以使用memcpy只復制Hello,但是不能使用strcpy來達到這個效果:

但是這個函式就不會像strcpy一樣,在復制完成后給補上 \0 來作為結尾,因為它不止針對字串操作,所以不會默認給添加\0結尾,
那對于這個函式一定要注意,它不僅僅針對的是字串系列的操作,我們可以從引數型別看出來,它并不是和str系列的函式一樣,str系列都是char型引數,這個函式的引數都是void型引數,那就意味著什么型別,它都可以接受,其實不止當前函式,所有的mem系列函式都不是僅僅針對字串操作,
5. 字串復制memmove
好的,在這里我們來提出新的問題,因為有的同學喜歡玩,現在各個領域內卷態勢嚴重,那字串拷貝當然也要來幫幫場子,如果我們想實作一個自己拷貝自己,那我們使用什么函式呢?如果使用memcpy會有什么樣的效果呢?我記得在很久之前一直被大家津津樂道的記憶體重疊問題,memcpy就是典型的例子,但是因為我很久沒測驗了,今天測驗時候,居然發現memcpy記憶體重疊問題被解決掉了??
//對于堆疊區記憶體,我們在申請到的時候,就對其初始化
char SrcString[] = "HelloWorld";
//我希望拷貝HelloWorld 中的前8個位元組給字串本身
//也就是我希望拷貝之后字串變為HeHelloWor
//但是我預計它會出現HeHeHeHeHe的情況
memcpy(SrcString + 2, SrcString, 8);
本來這里我是預計自己搞自己,然后會發生記憶體重疊,輸出HeHeHeHeHe,然后再引出memmove來解決這個問題,沒想到,,,它居然行了??也就是說正確輸出了結果:

我都無語了,沒想到微軟居然把這個問題解決了,而我渾然不知!!!但是,但是,我肯定不甘心,我得告訴大家它之前是怎么實作的:
void *
my_memcpy(
void *Dest,
const void *Src,
size_t Count)
{
char *New = (char *)Dest;
char *Old = (char *)Src;
while (Count)
{
*New = *Old;
New++;
Old++;
Count--;
}
return (Dest);
}
這就是它原本的實作邏輯,而按照這個函式的實作邏輯,我們再來測驗一番:

OK!!nice!達到我要的錯誤示范的效果了,而且,它確實之前就是這么實作的!

接下來我們來“順理成章”的引出我們的主角memmove吧!先來看看宣告:
void *
memmove (
void *Dest,
const void *Src,
ACPI_SIZE Count)
對于這個函式的三個引數我們就不解釋了,因為這三個引數和memcpy的三個引數是一樣的,所以我們直接來看使用舉例以及效果圖吧:

這里也給大家看一下這個memmove的實作邏輯,來幫助大家更好的理解為什么能夠避免記憶體重疊:
void *
memmove (
void *Dest,
const void *Src,
ACPI_SIZE Count)
{
char *New = (char *) Dest;
char *Old = (char *) Src;
//在這里對于兩個地址的大小進行了比較,用來確定該如何去復制
if (Old > New)
{
/*從頭部復制*/
while (Count)
{
*New = *Old;
New++;
Old++;
Count--;
}
}
else if (Old < New)
{
/*從尾部開始復制*/
New = New + Count - 1;
Old = Old + Count - 1;
while (Count)
{
*New = *Old;
New--;
Old--;
Count--;
}
}
return (Dest);
}
6. 字串比較strcmp
在兩個字串出現的時候,有時候我們需要比較兩個字串是否相同,或者看一下從哪兒開始不同的,那我們就用到了strcmp函式:
int
strcmp (
const char *String1,
const char *String2)
這個函式的回傳值是我們需要注意的地方,它的兩個引數倒也容易理解:
回傳值:如果兩個字串相等,回傳0,若String1>String2則回傳 1,若String1<String2,則回傳 -1
const char *String1:第一個需要比較的字串
const char *String2:第二個需要比較的字串
這個函式相對來說比較好理解,讓我們來看一個小例子:
//初始化為HelloWorld
char String1[] = "HelloWorld";
//初始化為HelloChina
char String2[] = "HelloChina";
//預期回傳1
int index = strcmp(String1, String2);
比較的辦法,就是從兩個傳入的指標第一個字符開始比較,然后比較標準就是按照ASCII值的大小,比如’W’就大于’C’,這個就根據ASCII表比較了,如上的代碼我們預期index值是1:

7. 字串比較memcmp
我們使用strcmp比較的時候也是有個不好的地方,那就是無法控制比較字串的長短,字串長短還是自己能夠控制比較舒服,畢竟有時候不需要比較太長,那現在提供給了大家機會,那還愣著干什么,趕緊來看看memcmp:
int __cdecl
memcmp(
const void *s1,
const void *s2,
size_t n)
mem系列的函式我們之前就說過了不止針對字串,從它的引數就可以看出,那這三個引數又是什么意思呢?其實也和memcpy差不多:
回傳值:如果兩個字串相等,回傳0,若String1>String2則回傳 1,若String1<String2,則回傳 -1
void *s1:第一個需要比較的指標地址
const void *s2:第二個需要比較的指標地址
size_t n:比較的個數
它的回傳值和strcmp是一樣的,0,-1或者1,對于我們來說,我們就想比較兩個字串的前5個位元組,因為前5個字符是一樣的,我們來看看效果:
//初始化為HelloWorld
char String1[] = "HelloWorld";
//初始化為HelloChina
char String2[] = "HelloChina";
//預期輸出0
int index = memcmp(String1, String2, 5);

對于以上兩種字串比較函式,這個需要我們去衡量,在什么時候用哪個,如果說我們需要比較兩個完整的字串,可以選擇使用strcmp,如果我們比較兩個字串的一部分,那我們可以使用memcmp,
8. 字串拼接strcat
我們總希望能有改變自己的機會,字串又何必不是這樣呢??它也許也想變得強大,變的無堅不摧,但是苦于沒有辦法,一直在沉淀自己,終于,等到了strcat函式的到來:
char *
strcat (
char *DstString,
const char *SrcString)
我們可以看到這個函式的兩個引數和strcpy的引數的一樣的,第一引數是個char型指標,第二引數是個const char*指標,也就是說第二個引數指向的字串不允許被修改:
char *DstString:目標字串指標,需要拼接到的目標字串地址
const char *SrcString:源字串指標,拼接在DstString之后
回傳值是一個指標,指向目標字串的首地址,我們來看看經過拼接之后,能不能順利的讓字串完成變身,強大自我呢?
//初始化為Hello
char String1[] = "Hello";
//初始化為KookNut39
char String2[] = "KookNut39";
//預期最終String1變為HelloKookNut39
strcat(String1, String2);

但是!!!必須要說的是,這樣做是不對的,這是一個堆疊區記憶體,我們本身只有6個位元組的空間,現在加上這個字串之后,很明顯堆疊區記憶體越界了!!!這是肯定不被允許的,
想要強大自己,這沒錯,但是得看有沒有這個能力,所以我們現在需要讓它首先具備強大自己的能力,那就是具有足夠的記憶體空間!!
//動態申請20個位元組,并且初始化為0
char* String1 = (char*)malloc(20);
memset(String1, 0, 20);
//修改String1為Hello
strcpy(String1, "Hello");
//初始化為KookNut39
char String2[] = "KookNut39";
//預期最終String1變為HelloKookNut39
strcat(String1, String2);
稍作修改,動態申請記憶體,讓我們擁有足夠的記憶體空間來存放加上來的字串,結果如下:

9. 字串中搜索字符strchr
字串中搜索特定字符也是我們也許會用到的小功能,其實這個函式相比前面的那些函式來說,也簡單很多,我們先來看一下這個函式的宣告:
char *
strchr (
const char *String,
int ch)
對于其中的兩個引數和回傳值,我們這么理解它:
回傳值,當字符ch與源串String第一次匹配的時候的地址,如果串中所有字符都不匹配,那么回傳NULL地址
const char *String:源串,匹配的來源
int ch:目標字符,用在再源串中匹配的
我們寫一個簡單的小例子來結束這個函式的講解,我們涉及到了匹配成功和不成功兩種情況:
char String1[] = "KookNut39";
//字串中沒有 Y 預期回傳NULL
char* ptr1 = strchr(String1, 'Y');
//預期回傳 N 第一次出現的地址
char* ptr2 = strchr(String1, 'N');
看一下運行結果:

10. 字串中搜索子串strstr
字串中可以搜索匹配指定字符?那如果我要搜索一個字串,有沒有什么辦法來實作呢?當然有啦,直接來一個strstr就可以找到字串中子串出現的位置啦:
char *
strstr (
const char *String1,
const char *String2)
我們來看一下這個字串中這些引數的具體意思:
回傳值:如果String2是String1的子串,那么回傳第一次匹配的地址,否則回傳NULL
const char *String1:目標字串
const char *String2:需要在目標串中尋找的子串
我們還是一樣,在這里進行能匹配的子串和不能匹配的子串進行兩次測驗,看一下回傳值是不是符合我們的預期:
//初始化為HelloKookNut39
char String1[] = "HelloKookNut39";
//尋找我的名字KookNut39
char String2[] = "KookNut39";
//預期回傳第一次匹配到的地址
char* ptr1 = strstr(String1, String2);
//預期回傳NULL
ptr1 = strstr(String1, "Kt39");
return 0;

行文至此,終于結束了有關常用字串操作函式的介紹,授人以魚不如授人以漁,這句話我覺得始終是沒問題的,在這里我希望通過給大家介紹簡單的使用例子,能夠讓大家在以后的學習日子里,對于字串操作可以游刃有余!!如果這篇博文能夠給您的學習帶來幫助,麻煩您賞博主一個點贊+評論+收藏,給博主繼續創作提供動力,謝謝!
今日份與君共勉:“待到秋來九月八,我花開后百花殺”

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/286441.html
標籤:其他
下一篇:【編譯原理】部分題目+知識點
