【原創】自制string類型(已完成)


這篇文章首發於360doc http://www.360doc.com/content/21/0526/17/73755266_979099504.shtml ,其實360doc里面的那個也是我的帳號,發在那里沒什么人看,就發到這里來了。

文章原創,轉載需注明原作者。

前后總計兩個多月,總算是寫完了。完成日期:2021.8.1.

若本文有錯誤,請大家在評論區中指出,我會盡我所能加以改正。

 

 

目錄

第一章 前言和准備工作

1.1.前言

1.2.准備工作

第二章 string類函數——簡單版

2.1.最簡單的string

2.2.string類型的輸入和輸出

2.3.查找子串,插入,刪除和替換

2.4.at函數和size函數

第三章:運算符重載和構造函數

3.1.賦值運算符

3.2.“+”和“+=”的重載

3.3.比較運算符的重載

3.4.下標運算符的重載

3.5.構造函數

第四章:string類函數(續)

4.1.內存動態分配

4.2.append函數

4.3.begin和end迭代器

4.4.getline函數

第五章:拾遺

5.1.string.h和string

5.2.字符數組

5.3.string類的本質

 

 

第一章 前言和准備工作

1.1.前言

1.1.1.前言

C++語言相比C語言,比較大和好用的變化就是類(class)這一功能。這次,我們嘗試來根據類這一新功能,構造一些比較簡單的數據結構。這次,我們挑戰一下string這一類型。

1.1.2.string是什么

string是C++STL容器中自帶的一個庫,是關於字符串的。字符串的概念什么的這里就不細講了,比較簡單。有了string,在C++中,就可以更加方便的使用字符串了,相比較於C語言而言,不需要使用strcpy,strcmp等函數,直接使用運算符即可。它的讀入和輸出和cin,cout結合,也有專門的函數getline等。同時,string也可以和C語言的老版本字符串進行轉換,兼容一些老的函數。

1.2.准備工作

1.2.1.工具

一台電腦,一個C++編譯器(筆者這里使用的是Dev C++,各位讀者可以根據自己的喜好,例如Visual C++,CodeBlocks等)

1.2.2.學習資源

如果有不懂的地方,提供一些參考的在線學習資源。實際筆者也是從這里參考了很多用法。

http://c.biancheng.net/cplus/

https://www.runoob.com/cplusplus/cpp-tutorial.html

http://www.weixueyuan.net/cpp/rumen/

第二章:string類函數——簡單版

2.1.最簡單的string

string是一個類型,我們構造string類型,也就需要定義一個string類型。定義類型,我們一般在C語言會使用typedef struct的方式,我們這里既然用上了C++,就用一下C++的新功能——類。

類其實是一個類似於結構體的東西,把很多東西打包在一起。類中除了可以包含變量,同時也可以包含成員函數,構造函數,析構函數以及運算符重載等。這里,我們暫且先把這個字符串本身和他的大小信息包含在整個類里面。話不多說,我們看代碼。

class String{

public:

    char *str;

    int size;

};

 

這里string的s是大寫為了避免和標准庫的string重名。可以看到,類的定義和結構體的定義類似。public說明這些成員是公共訪問的。

接下來,我們需要寫成員函數。我們先寫最簡單的也是最重要的init函數。事實上,string是沒有init的,但是在寫構造函數之前,我們先用最簡單的成員函數的形式寫一個init,用於初始化,給指針分配空間。(str是指針,為了使得str長度可變)

class String{

    char *str;

    int size;

    void init(void){

        str=(char*)malloc(100);

    }

};

 

首先是成員函數的位置,寫在class里面。第二是寫法,無需指定所屬類,直接寫str即可。

如果比較了解C++特性的,可能會問為什么用malloc不用new。這里用malloc是為了方便擴大空間(擴大要用realloc),這樣數組就是可變的了。

本文之后為了方便和節省空間,就不把整個代碼貼出來了,只寫本次要寫的函數,大家注意一下函數的位置。

接下來,為了讓標准函數可以訪問(例如printf),需要一個函數用於把這個字符串轉換為C風格字符串,也就是c_str函數。

const char *c_str(void){

    return str;

}

 

這樣,以后用printf就方便了。

2.2.string類型的輸入和輸出

2.2.1.輸入

string自帶的是使用cin進行輸入,但是我們這里也沒法用cin,只能簡單的寫一個get函數來實現輸入。為了方便,get可以包含多種版本。各位讀者也可以根據自己的喜好來設計更多版本。

版本1 get()輸入字符串,空格停止

版本2 get(字符個數)輸入字符串,到達字符個數停止

版本3 get(停止字符)輸入字符串,輸入時遇到停止字符停止

C++有函數重載功能,多個函數只要參數不同,就會當作不同函數處理。C++編譯器會根據類型來判斷調用哪一個函數。

void get(void){

    scanf("%s",str);

}

void get(int n){

    fgets(str,n,stdin);

}

void get(char c){

    int i=0;

    for(;;){

        str[i]=getchar();

        if(str[i]==c)break;

        ++i;

    }

}

 

fgets函數是從文件讀入的函數,第二個參數用於指定字符總數。

多說一句,新版本的C++標准中是不支持gets函數的,因為不安全(容易數組越界),對於VC編譯器新增了gets_s函數,部分編譯器還是保留gets的,但是為了兼容性,建議把gets全部寫成fgets的形式。

2.2.2.輸出

字符串的輸出也有很多種方式。這里列出比較常見的兩種。

版本1 put()輸出字符串,遇到'\0’結束

版本2 put(字符數量)輸出字符串,到達指定數量結束

實現起來其實也很簡單。

void put(void){

    for(int i=0;str[i]!='\0';i++)

        putchar(str[i]);

}

void put(int n){

    for(int i=0;i<n;i++)

        putchar(str[i]);

}

 

很簡單。兩個都是使用for循環來輸出,只不過循環條件不同,一個是不等於“\0”,另一個是小於n。

多說幾句,實際上,'\0’實際上就是0,所以也可以寫成str[i]!=0。但是根據習慣原因,我們一般寫作'\0’,表示這是一個字符串。但是,有人是這樣寫的:

str[i]!=NULL;

 

這樣寫就有點問題了。NULL其實也是0,但是一般用於指針較多。很多機器上面其實NULL在stdio.h是這樣定義的:

#define NULL ((void*)0)

 

這樣的話,0是void*類型的,而不是char類型的。所以,上面的代碼可能會報錯。C++對於類型轉換是很嚴格的。

2.3.查找子串,插入,刪除和替換

2.3.1.find函數

我們這一節來討論查找,插入,刪除和替換。想要閱讀這一節的話,最好有一點線性表的功底。

先說查找。標准庫的find,我們只用兩種。

find(String subs)

find(int n,String subs)

 

當然,其實參數還可以是char*類型的c風格字符串。這里是一定要區分開來的,因為參數為c風格字符串的話,就不是sub.str而是直接寫成subs了。看到這里,我們其實知道,string和c風格字符串完全不是一個東西,(一個是類,一個是數組或者說是指針)必須寫兩種形式。

為了節約篇幅。這里只列出參數為String的東西了。至於參數為char*的話...把后面的.str刪掉即可。

int find(String subs){

for(int i=0;i<strlen(str)-strlen(subs.str);i++)

if(strcmp(str+i,subs.str)==0)return i;

return -1;

}

 

反復查找,直到最后一個。避免比較時候數組越界,最終位置在str長度減去subs長度。

如果一直沒有,返回-1。實際上應該是string::npos,很多環境里面都定義為-1。

2.3.2.insert函數

insert函數用於往字符串里面插入一個字符串。函數的形式為:insert(插入位置,插入字串)。它將會在原字符串的插入位置后面插入字符串。例如。”abef”在第2個位置插入”cd”,結果為”abcdef”。

我們按照上面這個樣例,來分析一下算法。

首先,我們根據cd的長度,來把插入點后面的東西后移。假設插入點為ins,插入字符串為subs。

for(i=strlen(str)-1;i>ins;i--){

str[i+strlen(subs)]=str[i];

}

 

我們注意插入的操作是從后往前的移動。

然后,需要在最后放入’\0’這一結束符。應該是在往str[strlen(s)-1+t_size+1]這個位置。

然后,我們需要把字符串subs拷貝到這個位置去。但是,不可以用一般的strcpy,這樣會在最后放入一個’\0’,就結束了這個字符串,所以,我們不可以用strcpy。但是為了復制字符串,我們只能自制一個strcpy。因為沒有'\0’,就叫做nz_strcpy(no zero的縮寫)

void nz_strcpy(char *dest,const char *src){

while(*src!=0){

*dest=*src;++dest;++src;

}

}

(中略)

void insert(int ins,char *subs){

int i;

ins--;//數組下標從0開始

int subs_size=strlen(subs);//取得subs長度,方便s元素后移

for(i=strlen(str)-1;i>ins;i--){

str[i+subs_size]=str[i];//移動元素

}

str[strlen(str)-1+subs_size+1]='\0';//最終的結束符

nz_strcpy(str+ins+1,subs);//復制字符串

}

 

很簡單吧。這就是線性表插入的基本操作。

2.3.3.erase操作

erase(int p,int n)

刪除從p開始的n個字符。

void erase(int p,int n){

int front=p+1,rear=p+n;

while(str[rear]!=’\0’){

str[front]=str[rear];

++front;++rear;

}

str[front]=’\0’;

}

 

我們使用覆蓋的方法,設置一頭一尾兩個指針,每次把尾指針的內容復制到頭指針,直到尾指針指向的字符為0。如果不為0,那么就繼續下一個字符。例如,把abcdefg的第三個字符到第五個字符刪除。我們用列表的方式來看一下。

1 2 3 4 5 6 7

a b c d e f g

a b F d e F g

a b f G e f G

a b f g 0 f g

其中,大寫字母表示頭指針和尾指針所在的位置。可以看到,把后面的字符逐個放到前面,最后添上0即可。因為添上了0,最后不用刪除,字符串自動結束。

2.3.4.replace操作

其實我感覺replace和insert非常類似。replace(int start,int end,char *str);把start至end的區間全部替換成str。相當於先刪除start-end的區間,然后再插入str。所以,偷懶的辦法如下。

void replace(int st,int en,char *str){

erase(st,en);

insert(st,str);

}

 

這樣做即可。

2.3.5.拾遺

事實上,類似於find,erase,insert,replace等函數的實現,實際上都有很多類型。就例如insert,這里就有大約七八種。

basic_string& insert (size_type p0 , const E * s); //在p0前面插入s

basic_string& insert (size_type p0 , const E * s, size_type n); //將s的前n個字符插入p0位置

basic_string& insert (size_type p0, const basic_string& str);

basic_string& insert (size_type p0, const basic_string& str,size_type pos, size_type n); //選取 str

的子串 basic_string& insert (size_type p0, size_type n, E c); //在下標 p0 位置插入 n 個字符 c

iterator insert (iterator it, E c); //在 it 位置插入字符 c

void insert (iterator it, const_iterator first, const_iterator last); //在字符串前插入字符

void insert (iterator it, size_type n, E c) ; //在 it 位置重復插入 n 個字符 c

 

(參考自http://c.biancheng.net/view/1449.html)

事實上,這些都使用了函數重載功能。如果要全部寫起來,比較麻煩,而且很多函數可能我們平時不會用到,本文只挑選了部分出來。下面講述一下對函數轉換的方法。

例如,

basic_string& insert (size_type p0 , const E * s, size_type n); //將s的前n個字符插入p0位置

 

這一個。

首先,對這類函數的編寫的步驟。第一步,對已知條件進行轉化,根據后面的參數得出要操作的字符串。例如,這里,根據s和n,我們需要對s取前n個字符,把結果存放入s。第二步,使用標准函數。我們把標准函數的代碼搬過來。

在例如,

basic_string& insert (size_type p0, size_type n, E c); //在下標 p0 位置插入 n 個字符 c

 

我們只需要先根據n和c,構建出要插入的字符串s,然后執行標准函數即可。

char s[n];

for(int i=0;i<n;i++)s[i]=c;

 

兩句話即可轉換。

關於其他的函數,我們可以參考筆者一開始給出的幾個網站進行了解,嘗試編寫出更多的函數。

2.4.at函數和size函數

2.4.1.at函數

聽前面的各種數組操作,有的人應該已經厭煩了吧。如果筆者這里繼續寫insert,erase,replace的各種新方法的話,估計各位又要犯困了。(笑)所以,這一節換換口味,講幾個簡單的函數:at和size。

at函數類似於取字符串的一個字符。一般我們更加常用的方法是用下標,但是下標涉及到運算符重載,比較復雜。所以,這里我們先進行at函數的制作。

char at(int i){

    assert(i<=size);

    return str[i];

}

 

我們一般只會用到函數的第二句語句,第一句assert可能不太常用,這里我們就來講解一下。

assert(表達式);

如果表達式為真,不做任何操作。否則,如果表達式為0,那么就輸出異常。如果想看看assert效果的話,可以在程序里寫一個assert(0),看程序的反應。程序應該會輸出”asseration failed”一句話,然后直接終止運行。輸出的文字,根據環境不同,可能結果也會不同。

這里,為了不讓數組越界,這里就用了一個assert檢驗下標i是否小於等於size。

事實上,標准庫的at就有這個功能,筆者的dev c++環境會輸出這個。

terminate called after throwing an instance of 'std::out_of_range'

  what():  basic_string::at: __n (which is 100) >= this->size() (which is 3)

This application has requested the Runtime to terminate it in an unusual way.

Please contact the application's support team for more information.

2.4.2.size函數

其實寫到這里,筆者感覺之前的東西有問題。size之前做了成員變量,不可以做函數,所以這里必須先把size成員變量改掉。名字就叫做len吧。

class String{

    int len;//這里!

    char *str;

    ...

};

 

size很簡單,只需要調用strlen即可。

int size(){

    return strlen(str);

}

 

同樣,還有一個功能完全相同的函數length。

int length(){

    return strlen(str);

}

 

很簡單吧。

第三章:運算符重載和構造函數

3.1.賦值運算符

3.1.1.運算符重載

到這里,我們函數就做的差不多了。

我們一般使用的運算符,都是用自己的功能了。例如,+運算符是做加法。但是,對於字符串,+的作用完全不同,是字符串連接。所以,這就涉及到一個知識點,運算符重載。

為了方便,用成員函數很麻煩,所以,我們完全可以用運算符來完成這一功能,改變運算符自己的功能,叫做運算符重載。

運算符重載,在系統內部實際上是調用函數。例如,s=a+b,實際上就是s=a.operator+(b)。

operator+就是一個函數。

3.1.2.=的重載

與其啰啰嗦嗦說一大堆,不如自己動手去做做。

String operator=( String s){

    strcpy(str,s.str);

    return *this;

}

String operator=(const char *s){

    strcpy(str,s);

    return *this;

}

 

運算符重載的一般格式如下:

類名 operator運算符(參數){

    操作;

    return *this;

}

 

這是二元運算符的一般重載方式。

如果一個運算符R有X個參數,那么就稱R為X元運算符。例如*,/,=都是二元運算符。而+,-即可以做一元運算符(正負號),也可以做二元運算符(一般的加減法)。

例如,一個二元運算符R的參數為A,B,那么這個運算記作A.operatorR(B),當作一個成員函數使用。其實等號運算符重載的寫法為s.operator=(c)。而為了簡便,s.operator=(c)可以簡便寫做s=c。這是不是非常類似於真正的string類型了?

而return *this返回的是什么?this是一個指針,指向當前在操作的類對象。所以,我們需要執行return *this,返回當前對象,這樣才能正確執行我們的操作。

這樣,我們的函數就全部完成了。

3.1.3.運算符重載的廣泛運用

C++的標准輸入和輸出流中,都是用運算符重載來做的。

cin>>a;

cout<<p[i]<<endl;

 

中,<<和>>都是重載的運算符。此時,cin和cout肯定不是函數(因為函數不可以做計算),其實是一個變量。

<<符和>>符是左移位運算符和右移位運算符。至於cin和cout使用它的原因不知道,大概是為了看上去方便吧。

所以,上面的語句還可以寫成;

cout.operator<<(p[i]).operator<<(endl);

 

順便提一句,cout是ostream類的對象,cin是istream的對象。當然,本文不是講cin和cout的,是講string類型的,所以這里就不詳細描述了。

 

3.2.“+”和“+=”的重載

3.2.1.strcat函數

strcat是C語言的用於字符串連接的標准函數。

strcat(字符串1,字符串2)把字符串2連接到字符串1后面。

3.2.2.正題

String operator+(String s){

strcat(str,s.str);

return *this;

}

String operator+=(String s){

strcat(str,s.str);

return *this;

}

 

加號運算符和“+=”運算符都可以起到字符串連接的作用。非常簡單。調用strcat即可。

3.3.比較運算符的重載

3.3.1.strcmp

慣例,我們先介紹c庫標准函數。

strcmp比較兩個字符串大小。

strcmp(a,b)

a>b 返回值>0

a<b 返回值<0

a=b 返回值=0

3.3.2.自制strcmp

這里先偏離正題,說說c庫函數strcmp的自制。

int strcmp(char *s1;char *s2){

  while(*s1==*s2){

    ++s1;++s2;//如果相等,就下一個字符

  }

  return *s1-*s2;//這里一定是不相等的字符,相減即可

}

 

一開始的while循環不斷比較s1和s2,如果相等,指針++,指向下一個字符。

最后退出循環時,一定是不相等的,兩個數相減,用作差法比較大小。

但是有一個問題,如果字符串相等,那就會比較到數組后面去,使得數組越界。我們必須保證第一個循環條件為字符不等於'\0',也就是不結束。

int strcmp(char *s1,char *s2){

  while(*s1==*s2 && *(s1+1)!=0 && *(s2+1)!=0){

    ++s1;++s2;

  }

  return *s1-*s2;

}

 

保證下一個字符不等於0,這樣做就可以了。

 

 

3.3.3.比較運算符的重載

比較運算符主要有大於,小於,等於三個。我們同樣按照strcmp來進行重載,單手注意返回值變成了真和假。

bool operator>(String s){

if(strcmp(str,s.str)>0))return 1;

else return 0;

}

bool operator<(String s){

if(strcmp(str,s.str)<0))return 1;

else return 0;

}

bool operator==(String s){

if(strcmp(str,s.str)==0))return 1;

else return 0;

}

 

當然,我們還需要考慮參數為char*的字符串的情況,這里就略了。

制作起來非常簡單,只需要調用strcmp即可。多說一下,如果是C語言用多的人可能不知道bool是什么,其實bool是一種特殊的1字節變量,只能存放1和0,表示真和假。邏輯運算符的返回就是真假。如果給bool類型賦值為任意一個非0值,那么視作賦值為1。所有不等於0的值都看作為真,這就是可以把if(a!=0)寫作if(a)的原因。

3.3.4.compare函數

compare也是一個string的成員函數,也用於比較字符串大小。我們只看代碼,猜一猜它的功能。

bool compare(String s){

if(strcmp(str,s.str)==0)return 1;

else return -1;

}

 

3.4.下標運算符的重載

3.4.1.什么是下標

我們之前在學習數組的時候,應該看到過這樣的描述:

數組中每一個數都有一個編號,這個編號就是下標。

我們在引用數組元素時,經常用到s[i]這種寫法,實際上i就是一個下標,而[]就是下標運算符。

3.4.2.下標運算符的重載

首先我們要搞清楚一點,下標是可以作為一個左值來進行賦值的。也就是說,我們是可以這樣寫的:s[i]='a’

而一般的函數是不能這樣寫的:s.at(i)='a’

所以,如何讓這個下標運算符可以被賦值是一個問題。我們先拋開這個問題,來寫程序。

char  operator[] (int i){

return str[i];

}

 

好像很簡單...等等!如果寫一個類似於s[i]='a’的語句,會報錯嗎?

error: lvalue required as left operand of assignment

也就是說,這里是不可以作為一個左值來賦值的。因為這樣的語句,函數返回的是一個常量a,所以這個語句也是表示’a’='a’,是非法的。所以,我們需要在函數聲明的地方動點手腳。

char  &operator[] (int i){

return str[i];

}

 

在前面加上&符號即可。&符號這里不是取地址的意思,而是表示引用。例如,交換兩個變量的值,C++語言可以用引用的方法,這樣寫。

void swap(int &a,int &b);

這樣,如果把a和b傳遞過去,形參就是對實參的引用,就實現了交換。同理,用引用的方式,就可以賦值了。

 

 

3.5.構造函數

3.5.1.構造函數

構造函數是在定義的時候執行的函數。例如,我們初始化string類型,一定看見過這樣的語句:

string s("abcd")

在定義的時候初始化。

3.5.2.寫法

構造函數沒有返回值,且構造函數的名稱與類名一致。

String(const char *s){
        strcpy(str,s);
}

 

但是注意構造函數只要定義了,我們就需要在初始化時寫上構造函數,不然會出錯。
String s;//錯誤
String s("");//正確

為了方便,我們寫一個空構造函數。
String(void){
}
這樣,在不寫括號的情況下,c++會自動執行空函數,也就是說,初始化時,不加括號默認執行空構造函數。
趁熱打鐵,還有幾個構造函數,我們一起實現一下。
String(int n,char c){
        for(int i=0;i<n;i++)
            str[i]=c;
}
 
        

初始化為若干相同字符。
不過突然想到一個問題,在初始化調用構造函數時,還沒有init,所以str是一個未初始化的指針,會出錯,構造函數也沒有意義了。我們需要把init的過程寫進構造函數里面,至於size先定義為100吧,如果不夠再realloc...等等!用new分配的空間無法realloc,我們還是用stdlib.h庫中的malloc分配,這樣還能realloc。這樣一來,init函數就沒用了,可以把它刪掉了,而且也更像真正的string類型了。
操作非常簡單,在每一個構造函數加上一句str=(char*)malloc(100);即可。運行結果一樣,真不錯。

 

第四章:string類函數(續)

4.1.內存動態分配

之前運算符重載的所有內容都講完了。我們換個口味,繼續說函數。

首先我們要知道,str=(int*)malloc(100)指給str分配100個空間。一開始,我們沒有寫成在類中直接寫char str[100]而是特意在構造函數這樣寫,就是為了以后可以重新擴大內存。現在,我們需要在內存不夠時重新擴大內存。
擴大內存,我們不能用new,必須使用realloc函數用於分配過的指針再次分配。所以,我們需要在string中新增一個成員變量size,說明現在字符串有幾個空間,以及變量free,就是size-strlen(str),說明剩余多少內存可用。

class String {
public:
    char *str;
    int size;
    ...
    ...
    void insert(int ins,String t){
        int tmp=strlen(str)+strlen(t.str);
        if(tmp>size){
            str=(char*)realloc(str,tmp);
            size=tmp;
        }
        ...
    }
    String operator +=(String s){
        if(strlen(s.str)+strlen(str)>size){
            str=realloc(str,strlen(s.str)+strlen(str));
            size=strlen(s.str)+strlen(str);
        }
        ...
    }
    String operator =(String s){
        if(strlen(s.str)>size){
            str=realloc(str,strlen(s.str));
            size=strlen(s.str);
        }
        ...
    }
    
};

 

這里在每一個函數之前都加上了字符長度判斷,至於構造函數只需要加上size的賦值即可,不用多說。

 

4.2.append函數

4.2.1.append函數簡介

在字符串后面追加字符串。

本文在這里只簡單地寫兩種函數重載。第一種:直接在string的后面加上string(或char*字符數組)

第二種:在string的后面加上string的前n個字符。

 

4.2.2.編寫函數

介紹完了,我們開始正式編寫。首先我們找到函數的結束位置,然后在結束位置后面把需要的字符串拷貝過來即可。

 

 上圖是一張簡圖,表示函數的執行方式。

如果需要指定所加上的字符總數,那么我們使用strncpy就可以了。

void append(String s){
  int pos;
  char *t=str;
  for(pos=0;t[pos]!='\0';++pos){}
  strcpy(t+pos,s.str);
}
void append(String s,int n){
  int pos;
  char *t=str;
  for(pos=0;t[pos]!='\0';++pos){}
  strncpy(t+pos,s.str,n);
}

 

 

 

4.3.begin和end迭代器

begin指向第一個字符,end指向最后一個字符。於是本節就這樣寫完了。

char begin(void){
  return str[0];
}
char end(void){
  int i;
  for(i=0;str[i]!='\0';i++){}
  return str[i-1];
}

 

4.4.getline函數

getline讀入一整行。

void getline(istream cin,string s){
  int i;
  for(i=0;s[i]!='\n';i++)s[i]=getchar();//讀入到行末尾
}

其實函數參數中的那個cin根本沒有用到,只是裝裝樣子而已。(cin定義在iostream中,是istream類型的變量)

 

第五章:拾遺

5.1.string.h和string

有些人根本不知道下面三個頭文件的區別,經常在使用的時候搞混。

#include<string.h>
#include<cstring>
#include<string>

解釋:

string.h是老版本C語言的頭文件,cstring是新版本C++新增加的頭文件,功能等同於string.h,包含的是字符數組(char*)相關的函數(例如strcpy,strcmp,strlen等)。

string是C++的頭文件,包含的是string類(即string,本文寫的那些函數)只有使用這個函數,才可以寫那些有關string的代碼。例如:

#include<iostream>
#include<string>
string s;
int main(){
  cin>>s;
  cout<<s.size();
}

其中定義的string類型和size函數都是這個頭文件里面的。

到現在我還見到很多人在這種時候#include<cstring>,在部分編譯器會編譯錯誤!

 

5.2.字符數組

5.2.1.字符數組常識

上一節我們說了string.h,那么老版本的字符數組如何使用?

字符串在這種情況下是一個數組。

char s[100];

如果需要對這個數組做輸入和輸出,可以使用scanf或printf的%s參數。cout可以輸出字符數組,但是cin不能輸入字符數組。

原因:

cout<<"hello";

用雙引號括起來的都是字符數組。雖然在使用string的時候,新的string可以和字符數組混用,但是這是兩個東西。(所以本文中,所有函數都要寫兩遍,一遍是針對string的,一遍是針對字符數組的)

5.2.2.進階——字符數組和指針

請看兩種數組的聲明,有什么區別?

char *s="abc";
char s[10]="abc";

 

不妨嘗試執行下面的代碼:

int main(){
  s[0]='1';
  cout<<s;
}

運行結果:

上面的聲明:segmentation fault(即runtime error,運行時崩潰錯誤)

下面的聲明:輸出1bc

 

可以看到,使用指針進行初始化,得到的是一個字符串常量。但是使用數組進行初始化,效果相當於拷貝字符串。(只有在這種情況下'='可以做拷貝字符串的作用)

(2021.8.8補充:

使用指針形式初始化是使得這個指針指向那個abc,也就是指向字符串常量。而對於字符串常量,內存是受到特殊保護的,只能讀取不能寫入,因此會發生錯誤。

而使用數組的形式,是對字符串進行拷貝操作,數組所在的內存區域沒有受到特殊保護,因此可以寫入。)

 

並且,使用指針形勢初始化時,會出現一個warning。

warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]

 

5.3.string類的本質

終於寫到最后一節了,最后我們來討論string類的本質。

本質非常簡單,這就是一個普通的類,和其他的stack,queue沒有什么區別,就是一個數組+一堆成員函數。

因此,也是5.2中的結論,雙引號括起來的不是string,而是const char*字符串。因此,string中,成員函數的參數,定義了兩種形式,一種string,一種字符數組。使用上去,就像是string和字符數組完全通用。string的本質,還是一個普通的類。

當然,如果C++允許重載雙引號的話(雙引號也不是運算符),string估計就可以完全和字符數組通用了。就像這樣:

String operator ""(const char *s){
  String Str=s;
  return Str;
}

雙引號也不是運算符,上面的只是瞎寫而已,如果真的要完全通用的話...就算到了宇宙毀滅,這也是不可能的吧。

 

(全文完,總字數7608字)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM