C語言中字符串詳解

C語言中字符串詳解

字符串時是C語言中很是重要的部分,咱們從字符串的性質和字符串的建立、程序中字符串的輸入輸出和字符串的操做來對字符串進行詳細的解析。程序員

什麼是字符串?

C語言自己沒有內置的字符串類型,字符串本質上是一種特殊類型的數組,它的組成元素類型爲char,除此以外不受制與數組長度的限制,以'\0'做爲結束標誌,做爲字符串結束的標誌。(\0做爲一個特殊字符,它的ASCII值爲0,可是它不是'0'字符,'0'字符的ASCII值爲48。)express

定義字符串

1. 字符串字面量(字符串常量)

字符串字面量形如"string",也被稱爲字符串常量,編譯器會將它末尾自動添加上字符串結尾標誌\0。它做爲一種靜態存儲類型, 在程序開始運行時被分配地址,一直存在到程序結束,引號括起來的部分將表示它儲存的首地址,很相似於數組,數組名做爲數組首元素儲存的地址。數組

 #include <stdio.h>
 
 int main() {
 printf("%s %p   %c", "Hello", "Hello", *"Hello");
 return 0;
 }
 /**
  * Hello 00405044   H
  * **/

上面說明了字符串常量的儲存形式,並且它自己只表明首元素的地址。安全

2. 字符串數組形式的初始化

字符串以一種特殊的字符串數組的形式存在,區別於通常數組,進行通常初始化時:函數

char a[] = {'h', 'e', 'l', 'l', 'o', '!', '\0'};測試

而不能是:ui

char a[] = {'h', 'e', 'l', 'l', 'o', '!'};spa

後者仍然是一個普通的字符串數組,不是字符串,這樣的初始化顯然是麻煩的,咱們能夠這樣:設計

char a[] = "hello!";指針

或者

char *a = "hello!";

怎麼理解這兩種行爲呢,他們都使用a儲存了字符串hello!的地址,可是它們也是有所不一樣的,下面詳細討論下他們的區別所在。

3. 字符串數組和指針

  • 字符串數組形式:咱們知道字符串常量以靜態形式儲存在程序中,使用字符串數組來對它進行存儲時須要將其拷貝到新的儲存空間,而後將新的儲存空間地址賦值到a上。

  • 指針形式:這時候就是一個常規意義上的賦值,咱們把在靜態儲存區的常量地址直接賦值到a上。

這樣本質的區別有什麼在應用上的區別呢?其一,使用字符串數組a做爲常量指針來儲存地址,使用指針形式是一種變量來儲存地址;其二,由於字符串數組將是一種對原字符常量的一種拷貝,因此咱們支持和容許對這樣字符串的修改,可是指針只是對原常量地址的一種儲存,咱們不容許對常量進行修改,因此經過這個指針對原字符進行修改是未定義的惡劣行爲,咱們看下面的程序:

 #include <stdio.h>
 
 int main() {
     char *a = "hello!";
     a[0] = 'w';
     printf("%s", "hello!");
 }

這樣的程序看起來沒問題,咱們但願將hello!修改成wello!,而後咱們但願打印hello!,可是這樣的程序可能輸出wello!,由於咱們修改了源地址上的數據,固然編譯器也有可能崩潰。

因此通常狀況下,咱們只但願同過使用常量指針來儲存字符串

const char *a = "hello!";,這樣能夠避免在程序中出現異常的修改常量的錯誤。

因此咱們能夠總結,咱們但願修改字符串時使用字符串數組,只但願讀取字符串時咱們使用指針,並且應該是常量指針。

還有一些關於它們值得討論的部分,假如咱們想要使用咱們有一個字符串數組(本質上做爲一個字符數組的數組),有下面兩種形式:

char a[3][20] = {"I love you.", "Do you love me?", "Please."};

char *a[3] = {"I love you.", "Do you love me?", "Please."};

這樣又有什麼區別呢?第一個字符串數組佔用3*20*1 = 60byte,第二個佔用3個指針爲3*4=12byte。在程序非靜態部分無疑是後者更爲儉省。並且前者由於固定格式的緣由,字符良莠不齊可是它們建立的空間卻都必須知足容納最長的字符串,形成必定空間的浪費。

因此想要使用一系列待顯示的字符串時可使用指針數組,想要修改字符串在以後則使用通常形式的字符串的數組。

還有對字符串的拷貝,由於字符串變量所存在的形式都是字符串首元素的地址,因此咱們下意識對於字符串的拷貝每每是不起做用的:

 #include <stdio.h>
 
 int main() {
  char *a = "Hello!";
  char *pa = a;
  printf("a = %s   %s = pa\n", a, pa);
  printf("a -> %p\n", a);
  printf("pa -> %p\n", pa);
  printf("a = & %p\n", &a);
  printf("pa = & %p\n", &pa);
 }
 
 /**
  a = Hello!   Hello! = pa
  a -> 00405044
  pa -> 00405044
  a = & 0061FF1C
  pa = & 0061FF18
  * **/

在這裏a和pa做爲字符串打印時,內容時徹底相同的,可是仔細看咱們發現他們起始指向了相同的地址,也就是所咱們並無完成對字符串內容的拷貝,而只是對地址值的拷貝,並且a和pa做爲指針儲存在相鄰的兩個單元,相隔4個字節。這樣的拷貝在某些意義上不大,咱們將在下面再討論如何對於字符串進行拷貝。

字符串I/O

首先,在瞭解了字符串性質的狀況下,咱們來了解字符串I/O,由於字符串須要在建立時得到一段連續的數組空間,因此嘗試將輸入的字符串加載進入程序時,咱們須要先 分配空間

這樣作是必要的,由於對於未分配內存的字符指針,咱們並不知道它的初始狀態,它可能指向任意位置,咱們在進行輸入的時候頗有可能所以抹除了先前儲存位置上的數據,一般這是不被編譯器容許的,每每會形成程序崩潰。

 #include <stdio.h>
 
 int main() {
  char *a;
  scanf("%s", a);// 這個程序可能會崩潰
  puts(a);
  return 0;
 }
 

因此在處理字符串I/O以前,首先要考慮的就是爲輸入的字符串分配空間,並且保證輸入的字符串不超過咱們申請的空間。

下面咱們來看一些I/O函數來深刻理解這樣的理念。

1. gets()被廢棄的選項

gets(),gets須要一個參數,一個字符串指針,它從I/O設備上讀取一行信息(等到遇到一個換行符中止),而後在末尾添加空字符,最後的換行符也會被讀取並丟棄。

看起來這是一個很不錯的I\O函數,可是在C99標準中它被建議不要使用,在C11標準中被徹底廢棄,這是由於它存在着嚴重的隱患,看下面這段程序:

 #include <stdio.h>
 
 int main() {
  char b[5] = "hhhh";
  char a[5];
  gets(a);
  puts(a);
  puts(b);
  return 0;
 }
 
 /**
  abcd
  abcd
  hhhh
  * **/
 
 /**
     abcdefghijklmn
     abcdefghijklmn
     fghijklmn
  **/

這段程序,咱們輸入了兩段內容進行測試,第一次abcd恰好長度爲5,gets函數正常接受將它放到a分配的地址中,沒有出現問題;可是在第二次咱們輸入了超過了既定分配長度的字符,咱們發現原字符出現了異常的變化,超過了既定長度5,容納下了全部的輸入字符,可是隨之咱們原有的字符串b也被徹底修改,原數據被徹底抹除。

這是由於它們的地址恰好相鄰,gets函數並不會對字符長度進行檢查,它只會將一整行的數據放入指針指向區域上,即便超過申請空間的邊界,他也會繼續寫入,抹去相鄰區域的數據也在情理之中了。

這給咱們程序帶來了巨大的危險,若是溢出的部分佔用了未使用的空間問題並不大,可是它輕易抹除以使用空間中內容極可能致使程序崩潰,因此咱們不要使用gets函數,應該嘗試更多的根據建議使用fgets()或則gets_s來避免這樣的問題。

2. fgets()和gets_s()

爲了解決gets函數中存在的問題,有兩個能夠函數做爲替代。

首先是基於gets的升級版gets_s他須要另外的一個參數指定最大讀取長度,並根據這個長度來作出相對應的操做:

  • 正常狀況下,gets_s從標準輸入流中讀取信息,相似於gets在未達到最大長度並且讀取到換行符時,它將從緩衝區讀取該換行符並將其丟棄並在末尾補充上空字符。

  • 在讀取出現問題時,gets_s讀取到最大讀取長度數目的字符可是仍然未讀取到換行符時,gets_s將會將對應指針(數組首字符)指向數據設定爲空字符,而後繼續讀取知道讀取到文件末尾或者換行符,而後返回空指針,以後調用依賴實現的函數的處理函數,可能停止或者退出程序。

在這裏咱們給出一段處理函數使用的實例:

 #include <stdio.h>
 #include <stdlib.h>
 #include <crtdbg.h>  // For _CrtSetReportMode
 
 void myInvalidParameterHandler(const wchar_t* expression,
                                const wchar_t* function,
                                const wchar_t* file,
                                unsigned int line,
                                uintptr_t pReserved)
 {
     wprintf(L"Invalid parameter detected in function %s."
             L" File: %s Line: %d\n", function, file, line);
     wprintf(L"Expression: %s\n", expression);
     printf("Error!");
 }
 
 int main() {
     char a[5];
 
     _invalid_parameter_handler oldHandler, newHandler;
     newHandler = myInvalidParameterHandler;
     oldHandler = _set_invalid_parameter_handler(newHandler);
     _CrtSetReportMode(_CRT_ASSERT, 0);
     gets_s(a, 5);
     puts(a);
     return 0;
 }

在這裏及時咱們輸入超過5位字符,程序也不會呈現崩潰退出。

詳細信息參照

咱們發現gets_s函數中使用並不特別方便,還有一個函數能夠做爲替代fgets函數,它相較於前二者,還須要另一個參數,讀入文件名稱,若是從鍵盤中讀取,那麼即爲標準輸入流stdinfgets函數的通常行爲:

  • 正常讀取到換行符或則文件末尾時,讀取中止,將換行符讀入字符串中而後在字符串末尾上填入空字符。這時候函數會返回指向讀取函數儲存位置的指針,若是到達文件末尾將返回空指針,當讀取發生某些其餘錯誤時也會返回空指針,在C語言中它被定義爲宏NULL

  • 在讀取超過字符串最大長度的字符時,將要達到最大長度時中止讀取而後在末尾補充上空字符。在讀取到文件末尾時函數會返回空指針。

 #include <stdio.h>
 
 int main() {
     char a[5];
     char *status = fgets(a, 5, stdin);
     puts(a);
     printf("a = &%p\tstatus = &%p\n", a, status);
     return 0;
 }
 
 
 /**
     123456
     123
     a = &0135FA10   status = &0135FA10
  * **/
 
 /**
     123
     123
 
     a = &0061FF17   status = &0061FF17
  **/

對於最大長度參數n,代表函數最多讀取n-1個數據(包括換行符),因此輸入123會將換行符正常讀取而後puts函數又輸出了一個換行符,因此輸出了兩個換行符;可是輸入更多時,函數讀取到四個數據後中止讀取補充上空字符。

不一樣於gets_s函數,讀取不到換行符時,函數也不會對緩衝區中其餘數據作出任何操做,對於前者會清空緩衝區中全部下一個換行符前的全部內容,可是fgets並不會,咱們能夠自由的選擇對這些緩衝區的內容進行處理。

因爲fgets()函數的安全性和可擴展性更佳,因此咱們推薦更多的去使用fgets()函數。它每每是最佳選擇。

3. scanf()不甚理想的選擇

scanf做爲泛用性很強的函數,也有它讀取字符串的模式:

scanf("%s", a);

可是使用它來讀取字符串並非最理想的選擇,由於scanf函數讀取字符時開始與一個非空字符,終止於第一個空字符。這樣下來他可能只能夠讀取到一個簡單的單詞,而不是咱們指望的包含空格等完整內容的字符串,因此通常狀況下咱們不使用scanf讀取整句字符串,而將它用於單詞和具備特定格式的字符的讀取。

咱們能夠經過轉換說明修飾符來讀取規則的字符串:

scanf("%5s", a);這樣就能夠讀取長度爲5的單詞(中間讀取到換行符依舊會中止讀取,其中不包括空字符),功能能夠比擬fgets(a, 6, stdin);,可是後者可能包括特殊的換行符之類,因此它們也算是各有用武之地。

4. 輸出函數

系統的說明了幾個C語言輸入函數,咱們如今來相似的梳理輸出函數,它們與輸入函數是相對應的,也是各有特點的。

  • puts——gets,輸出字符串直到空字符,而且會在最後輸出一個換行符,這樣的存在也可能訪問到未被分配的內存,這樣的行爲是未定義的,可是這樣很不靠譜。

  • fputs——fgets,輸出字符直到碰到空字符,可是與fgets匹配,它不會在輸出最後輸出換行符,並且須要額外的參數指示輸出位置,若是是屏幕則爲stdout

  • printf——scanfscanf相較於前二者較爲多才多藝,不會輸出換行符,能夠根據本身對格式的要求進行自由控制,並且在同時輸出多個字符時用起來十分方便。

字符串處理函數

討論完字符串性質和I\O後咱們來繼續討論和字符串息息相關的一些C語言自帶的字符串處理函數(其中大部分都是咱們能夠實現的),熟悉他們方便咱們更好的處理字符串。通常狀況下他們定義在頭文件string.h中。

  1. strcatstrncat

這兩個函數被用來字符串合併。

對於strcat接受兩個字符串指針做爲參數,將第二個字符串接到第一個字符串上,而後返回第一個字符串的指針,可是它也存在相似gets的缺陷,當第一個指針所指向被分配的空間並不足夠大時,額外從第二個字符讀取的字符將會可能覆寫掉其餘已經分配空間上的數據。可是基於C語言制定時相信程序員的準則它仍然能夠繼續使用,不一樣於getsgets產生的錯誤可能由用戶製造,可是strcat製造的問題卻能夠由程序員來避免,因此它仍然可使用。

strncat須要額外的一個指定拷貝後的字符的最大長度(包含空字符),以此來保證拷貝後的數組不會超過以分配的儲存空間,其餘內容同strcat一致。

  1. strcmpstrncmp

這兩個函數用於字符串比較。

對於strcmp,接受兩個字符串指針比較它們指向的字符串(而不是它們所指向的地址)若是相同則返回0,不然返回非零的數字,具體狀況根據編譯器的實現有所不一樣。

strcmp也能夠經過指定從指定的起始位置開始比較字符串,只須要在傳遞指針時進行加減運算:

strcmp(a+5, b+4);這樣使得字符串的比較更加靈活。

strncmp使得字符串的比較更加靈活,經過第三個參數n來指定比較的長度,咱們能夠進行前綴匹配。

  1. strcpystrncpy

這兩個函數用於字符串的拷貝。

strcpy拷貝第二個字符串指針的字符到第一個字符串指針所指向的空間中去,可是咱們也須要注意第一個參數所指向的空間也必須足夠大容納第二個字符串。咱們也大可沒必要從字符開始部分開始拷貝,咱們能夠吧參數指針移動到任何咱們想要它拷貝到的位置:

strcpy(a+4, "hello!");

strncpy彌補了strcpy的缺點,能夠在第三個參數中指定拷貝的最大長度(這個大小不包含空字符,由於函數設計就預想到可能碰不到空字符就要中止,因此拷貝完這個最大長度後,函數會向原字符後自動添加上空字符),可是n的大小最大爲第一個字符數組空間大小減去1。

  1. sprintf

sprintf聲明在stdio.h中,相似於printf它能夠將字符串進行格式化並輸出到一個字符串中,使用時一樣須要考慮字符串分配空間的問題,這個問題在全部涉及字符串的使用時都要考慮!下面看一段用例:

 #include <stdio.h>
 
 int main() {
     char *s = "Today is ";
     int year = 2021, month = 2, day = 2;
     char data[30];
     sprintf(data, "%s%d/%d/%d.", s, year, month, day);
     puts(data);
     return 0;
 }
 
 
 /**
    Today is 2021/2/2.
  * **/

總結

總的來講字符串使用時,不管在什麼時候務必用注意分配空間的使用,不要訪問到未分配的空間,這樣會給程序帶來沒法預料的結果。

相關文章
相關標籤/搜索