1. 程式人生 > 其它 >C-風格字串與C++中的string類

C-風格字串與C++中的string類

技術標籤:C/C++c++指標字串c語言string

C-風格字串與C++中的string類


在編寫C語言或者C++程式時,我們可能會經常對字串進行操作。嚴格的來說,C++中提供了兩種型別的字串,一種繼承於C語言,我們在這裡將其稱為”C-風格字串“
;另一種是在C++中採用面向物件(OOP)程式設計思想定義的string類。那麼,這兩者之間有什麼關係呢?這是我們接下來要討論的話題。

1 什麼是C-風格字串?

C風格字串,簡單來說,就是遵循C語言規範,在C語言中可以使用的字串,常見的包括:字串常量,可視為字串的字元型陣列(這裡要注意:字元型陣列不一定就是字串,這一點至關重要,下面會對其進行專門性的說明),以及指向字串的指標(簡記為char*型別)。如下所示,

	//1-列印一個字串常量
	printf("Hello,C!");	//C語言中的列印操作
	std::cout<<"Hello,C++"
<<std::endl; //C++中的列印操作 //2-分別定義一個簡單的字元型陣列、一個可以用於表示字串的字元型陣列 char cArr1[5]="Hello"; //×,不是字串,只是簡單的字元型陣列 char cArr2[6]="Hello"; //√,是字串,可以用於表示字串 //3-定義一個指向字串常量的指標 const char* pStr="Hello";

1.1 字元型陣列不一定是字串!

在進行正深入的探討之前,我認為十分有必要先說一下標題中提到的內容,”字元型陣列不一定是字串!“。這很容器理解,就比如我們在1中定義的字元型陣列和用於表示字串的字元型陣列,

	//2-分別定義一個簡單的字元型陣列、一個可以用於表示字串的字元型陣列
	char cArr1[5]="Hello";	//×,不是字串,只是簡單的字元型陣列
	char cArr2[6]="Hello";	//√,是字串,可以用於表示字串

C-風格字串有一種特殊的性質,即:C-風格的字串必須以空字元結尾,其中,空字元被寫為:’\0’,其ASCII碼值為0,用於表示一個字串的結尾。我們知道,在C語言和C++中,char-字元型別可以看作是整型的一種變體,因為兩者是可以在同一個表示式中,進行運算的。例如,

	char c='a';	
	std::cout << int(c) << std::endl;	//字元'a'對應的ASCII碼值為:97
	int b=1;
	std::cout<<c+b<<std::endl;	//98

暫不考慮型別轉換的因素,我們很容易知道,該段程式碼的執行結果為“98”,這至少證明了:char型變數和int整型變數是可以一起運算的。
現在,我們考慮另一個事情,“計算機底層對於整形數值是以二進位制形式進行運算的”。其實,在計算機底層,字元型變數的儲存方式和整型變數的儲存方式完全一致,都是以二進位制形式儲存的。只不過,對於字元型變數,需要先將該字元變數對應的字元值轉換為ASCII碼值,再將ASCII碼值轉換為二進位制進行儲存;而整型變數是直接將其對應的二進位制值儲存到計算機中。為了進一步對其進行說明,我們再舉一個例子,

	char a = 0;
	char a1 = '\0';
	int b = 1;
	std::cout << a+b << std::endl;	//輸出結果:1
	std::cout << a + b << std::endl;	//輸出結果:1

好了,到這裡你可能對字元和ASCII碼值之間的關係有了一個清晰的認識:整型的0和’\0’是對應的,或者說,它們本質上就是一個東西,在記憶體中以二進位制形式表示的話都是一串0。現在言歸正傳,我們先定義一個長度為5的空字元型陣列,並列印其陣列元素。

	char CArray[5] = {};	//空陣列
	for (int i = 0; i < sizeof(CArray) / sizeof(char); i++) {
		std::cout <<"CArray["<<i<<"]="<< CArray[i] << "\t";
	}

你會發現列印結果如下,即:所有元素的值都預設為空。
在這裡插入圖片描述
我們換一種列印形式,先將陣列元素轉換為整型,再執行列印操作,可以發現現在陣列元素均為0。這說明:char陣列的元素預設值為’\0’,即上面我們提到的字串結束符。

char CArray[5] = {};
	for (int i = 0; i < sizeof(CArray) / sizeof(char); i++) {
		std::cout <<"CArray["<<i<<"]="<< int(CArray[i]) << "\t";
	}

在這裡插入圖片描述
我們使用之前定義好的陣列,直接按照字串來使用,並列印它們的值,(由於將下列程式碼直接放置在*c.cpp中會報錯,因此,我們將其放在*.c檔案中進行編譯連結和執行)。

	char cArr1[5] = "Hello";	//×,不是字串,只是簡單的字元型陣列
	char cArr2[6] = "Hello";	//√,是字串,可以用於表示字串
	printf("cArr1=%s\n", cArr1);
	printf("cArr2=%s\n", cArr2);

在這裡插入圖片描述
可以發現,在第一個輸出結果中出現了一堆亂碼;而第二個就很正常的輸出了其對應的字串。這裡是為什麼呢?我們先按照之前的方法,將兩個字元型陣列的元素轉換為整型值打印出來,在分析原因。

	char cArr1[5] = "Hello";	//×,不是字串,只是簡單的字元型陣列
	char cArr2[6] = "Hello";	//√,是字串,可以用於表示字串
	
	printf("cArr1:");
	for (int i = 0; i < sizeof(cArr1) / sizeof(char); i++) {
		printf("cArr1[%d]=%d\t",i,cArr1[i]);
	}
	printf("\ncArr2:");//換行
	for (int i = 0; i < sizeof(cArr2) / sizeof(char); i++) {
		printf("cArr1[%d]=%d\t", i, cArr2[i]);
	}

在這裡插入圖片描述
可以看到,兩個字元型陣列的前個元素值都一樣,只是第一個陣列中的最後一個元素對應的Int值為,即:我們上面所提到的字串結束符。總結來說,字元型陣列是否可以被視為字串,完全取決於該字元陣列的末尾是否有字串結束符’\0’。

1.2 C&C++中的:字串常量

字串常量,簡單來說:字串常量就是被雙引號”“括起來的一串字元。在編寫程式碼時,我們可能會使用到Dev++、VSCode,或者是Visual Studio。這裡以我比較熟悉的Visual Studio為例來說明字串常量這個問題。
字串常量在C語言和C++中的使用稍有不同。一個常識是:當原始碼檔案以*.c結尾時,在對原始碼進行編譯處理時,被調起的是C編譯器;而當原始碼檔案以*.cpp結尾時,在對原始碼進行編譯處理時,被調起的是C++編譯器。舉個例子,在C語言中,你可能會這樣寫,

	char* pStr="Hello";

這完全是合法的,因為:字串本身本質上表示的就是該字串在記憶體中的首地址,或者說是該字串中第一個字元的地址。我們也可以舉個例子,

	printf("the address of Hello is %x\n",("Hello"));
	printf("the address of H in Hello is %x\n", &("Hello")[0]);

在這裡插入圖片描述
這個結果就證明了我們的上述說法。但是在C++中,使用的方式稍有不同,如何想把按照在C語言中的方式將”Hello“這個字串常量傳遞給一個指標,如果依舊按照上述方式去寫程式碼,C++編譯器可能會提醒你,


在這裡插入圖片描述

所以,你需要這樣寫。因為C++編譯器自動將用引號引起來的字串常量視為是const型別的char*,即:一個常量指標——指向“常量”的指標,其值是不能修改的。

	const char* pStr = "Hello";

1.3 C++中不太一樣的char *與動態記憶體分配

在1.2中,已經指出了,在C++中,直接讓一個char*指標指向一個字串常量是不可取的,因為C++編譯器會提示你:const char*的值不能用於初始化char *型別的實體。這個問題在C++中就很容易解釋了,因為C++編譯器認為const型別的指標所指向的內容是不可更改的,而僅僅只是char*的話,它的值是可以更改的,兩個不是同一種類型,相互之間的賦值就變為非法的了。那麼,如果我們非得想讓一個char*指標,指向一個字串常量,這完全是合理的,也是可以實現的,只不過會稍微麻煩一些。
既然無法直接將字串常量的地址傳遞給一個非const型別的char型指標,那麼我們就採用C++中的動態記憶體分配的方式,重新在記憶體中(準確來說,是在堆區/自由儲存區)開闢一塊記憶體,專門用於存放這個字串常量的副本,並讓這個char*指標指向這塊記憶體區域。
這十分容易實現,因為C++中提供了更為方便的new關鍵字,它可以用來在堆區根據資料型別自動開闢一塊大小適宜的記憶體空間,並返回這塊記憶體空間的首地址。如下所示,

	char* p = new char[6];	//在堆區開闢記憶體空間
	strcpy(p, "Hello");	//將字串常量拷貝給字元型指標
	std::cout << "p:" << p << std::endl;	//列印字串
	delete[] p;	//釋放堆區記憶體空間

在這裡插入圖片描述

2 C++中的字串:基於char*的string類

嚴格來說,C++中提供的專門的字串string,本質上就是一個類,它被定義在標頭檔案中。所以,我們在C++中要使用string類時,常常需要用到如下的編譯預處理命令,

#include <iostring>	//包含最基本輸出輸出操作的標頭檔案
#include <string>  //包含string的定義的標頭檔案
using namespace std;	//標準名稱空間

2.1 char*與string類的關係

那麼,我們這裡所說的C-風格字串和上面提到的string字串有什麼關係呢?我們知道,C++是C語言的升級版本,因為它原本就叫”C With Class“,即:帶類的C(面向物件OOP是C++的重要機制之一)。其實,string類內部本質上是維護了一個char*即字元型指標(現在,結合1.3中的分析,讓一個字元型指標指向一個字串常量,這十分容易實現),而這裡的char*,即:C-風格的字串之一(暫不考慮字元型陣列和字串常量/字串字面值)。

2.2 如何使用char* 自定義MyString字串類

string將(char*)字元型指標作為成員屬性,同時提供了另一個專門用於記錄“這個字元型指標所指向的字串長度”的int型別的成員屬性length。這使得我們在編寫C++程式中,對於字串的操作十分簡便,因為該類將對於這個C-風格字串,即:char*的操作封裝成為了一個個的成員函式,比如:字串賦值、子串查詢(字串匹配問題)、字串替換等常用操作。因此,暫且不考慮更加複雜的操作,我們可以自己製作一個自定義的String類,僅僅包含建構函式、解構函式、拷貝建構函式、賦值運算子過載函式、左移運算子過載函式,如下所示為自定義string類的宣告。

#pragma once
#include <iostream>
#include <string>
using namespace std;

class MyString
{
	//友元運算子過載函式
	//左移運算子過載函式-物件的輸出
	friend ostream& operator<<(ostream& out, const MyString& str) {
		out << str.m_str;
		return out;
	}

public:
	//建構函式
	MyString();
	MyString(const char* const s);
	MyString(int n, char c);
	//拷貝建構函式
	MyString(const MyString& str);
	//解構函式
	~MyString();
	//Getter
	int getLength() {
		return this->m_length;
	}
	//運算子過載函式
	MyString& operator=(const MyString & str);//賦值運算子過載函式:MyString
	MyString& operator=(const char* str);//賦值運算子過載函式:char*
	MyString& operator=(char c);//將char字元賦值給當前物件
	char operator[](const int index);//下標運算子過載函式
	MyString& operator+=(const char* const str);
	MyString& operator+=(const char c);
	MyString& operator+=(const MyString& str);
	
protected:
	MyString& copyCharArrToString( MyString& myStr,const char* pArr);//將char*字串拷貝到MyString型別的物件中
	MyString& strCat_Str(MyString& myStr,const char* s);//將s連線到myStr後邊
private:
	char* m_str;
	int m_length;
};

類的實現如下,

#include "MyString.h"

//建構函式
MyString::MyString()
{
	this->m_str = new char[1];
	this->m_str[0] = '\0';//將首元素設定為字串結束符
	this->m_length = 0;
}

MyString::MyString(const char* const s)
{
	copyCharArrToString(*this, s);
}

//拷貝建構函式
MyString::MyString(const MyString & str)
{
	this->copyCharArrToString(*this, str.m_str);
}

MyString::MyString(int n, char c)
{
	this->m_length = n;
	this->m_str = new char[n + 1];
	memset(this->m_str, c, n);
	this->m_str[n] = '\0';
}

//解構函式
MyString::~MyString()
{
	if (this->m_str != NULL) {
		delete[] this->m_str;
		this->m_str = NULL;
		this->m_length = 0;
	}
}

//賦值運算子過載函式
MyString & MyString::operator=(const MyString & str)
{
	// TODO: 在此處插入 return 語句
	delete[] this->m_str;
	return this->copyCharArrToString(*this, str.m_str);
}

MyString & MyString::operator=(const char * str)
{
	// TODO: 在此處插入 return 語句
	return	copyCharArrToString(*this, str);
}

MyString & MyString::operator=(char c)
{
	delete[] this->m_str;
	//判斷當前字元是否為字串結束符
	if (c == '\0') {
		this->m_length = 0;
		this->m_str = new char[1];
		this->m_str[0] = '\0';
	}
	else
	{
		this->m_length = 1;
		this->m_str = new char[2];
		this->m_str[0] = c;
		this->m_str[1] = '\0';
	}
	return *this;
}

char MyString::operator[](const int index)
{
	if (this->m_length <= index)
		return '\0';
	else
		return this->m_str[index];
}

MyString & MyString::operator+=(const char * const str)
{
	// TODO: 在此處插入 return 語句
	return this->strCat_Str(*this, str);
}

MyString & MyString::operator+=(const char c)
{
	//設定字串長度
	this->m_length += 1;
	this->m_str = (char*)realloc(this->m_str, this->m_length + 1);
	//字元拼接
	strcat(this->m_str, &c);
	return *this;
}

MyString & MyString::operator+=(const MyString & str)
{
	return this->strCat_Str(*this, str.m_str);
}

MyString& MyString::copyCharArrToString(MyString & myStr, const char * str)
{
	if (str) {
		myStr.m_length = strlen(str);
		myStr.m_str = new char[myStr.m_length + 1];
		strcpy(myStr.m_str, str);
	}
	else
	{
		MyString();
	}
	return myStr;
}

MyString & MyString::strCat_Str(MyString & myStr, const char * str)
{
	// TODO: 在此處插入 return 語句
	if (!str)
		return *this;
	//設定字串長度
	myStr.m_length += strlen(str);
	//重新開闢新的記憶體空間
	myStr.m_str = (char*)realloc(myStr.m_str, myStr.m_length + 1);
	//字串拼接
	strcat(myStr.m_str, str);
	//返回當前物件的引用
	return myStr;
}

可以看到,在上述自定義類中,我們使用到了諸如運算子過載、友元函式、函式過載等基本語法規範。

2.3 字串基本操作的實現

下面對字串的基本操作,例如:字串拼接、子串查詢、字串替換、字串插入、去除空白字串等進行實現。

2.3.1 字串拼接

C++中字串拼接的實現方式有兩種,第一種是通過+=運算子過載實現;第二種是通過成員函式實現。第一種方式在2.2中已經實現,下面介紹第二種方式。首先在類中宣告如下函式,

//1-字串賦值操作
	MyString& assign(const char* s);
	MyString& assign(const char* s,const int n);
	MyString& assign(const MyString& s);
	MyString& assign(int n,const char c);

其次在原始檔中進行實現,程式碼如下,

MyString & MyString::assign(const char * s)
{
	return this->copyCharArrToString(*this, s);
}

MyString & MyString::assign(const char * s, const int n)
{
	//	void *memcpy(void *dest, const void *src, size_t n)	從 src 複製 n 個字元到 dest
	this->m_length = n;
	//釋放原始記憶體空間
	delete[] this->m_str;
	//分配新的記憶體空間
	this->m_str = new char[n + 1];
	//將目標字串賦值到當前字串中
	memcpy(this->m_str, s, n);
	//將第n個位置設定為字串結束標誌
	this->m_str[n] = '\0';
	//返回當前物件
	return *this;
}

MyString & MyString::assign(const MyString & s)
{
	return this->copyCharArrToString(*this, s.m_str);
}

MyString & MyString::assign(int n, const char c)
{
	//釋放原始記憶體空間
	delete[] this->m_str;
	//開闢新的記憶體空間
	this->m_str = new char[n + 1];
	//重新設定字串長度
	this->m_length = n;
	//用字元c填充字串
	memset(this->m_str, c, n);
	//將最後一個字元設定為結束符
	this->m_str[n] = '\0';
	return *this;
}

2.3.2 子串查詢

C++中子串查詢可以通過字串匹配演算法來實現,這裡使用樸素的字串匹配演算法加以實現,實現程式碼如下,

//myStr:用於匹配的字串;s:子串;pos:開始匹配的位置
int MyString::strMacth_BF_Str(MyString & myStr, const char * s,int pos)
{
	//B-F演算法:樸素字串匹配演算法
	int index = -1, i, j;
	for (int i = pos; i < myStr.m_length; i++) {
		//從pos位置處開始遍歷模板字串
		for (j = 0; j < strlen(s); j++) {
			//從起始位置開始遍歷s
			if (myStr.m_str[i + j] != s[j])
				//如果模板字串中存在一個和s不匹配的字元,就終止此次匹配
				break;
		}
		//判斷是否匹配成功
		if (j == strlen(s))
		{
			index = i;
			break;//終止子串查詢
		}
	}
	return index;
}

2.3.3 字串替換

實現程式碼如下,

MyString & MyString::replace(int pos, int n, const char * s)
{
	// TODO: 在此處插入 return 語句
	int s_len = strlen(s);//字串長度
	//①n>=strlen(s)->:若n==strlen(s),則直接替換;若n>strlen(s),則:執行字串空白字元的去除
	if (n == s_len) {
		for (int i = pos, index = 0; i < pos + n; i++, index++) {
			this->m_str[i] = s[index];
		}
	}
	else if(n>s_len){
		for (int i = pos, index = 0; i < pos + n; i++, index++) {
			if (index >= s_len)
				this->m_str[i] = ' ';
			else
				this->m_str[i] = s[index];
		}
		if (n > s_len) {
			//去除空白字元
			this->trim();
		}
	}
	//②n<strlen(s)
	else if (n < strlen(s)) {
		//記錄原始字串
		char* pOldStr = new char[this->m_length + 1];
		strcpy(pOldStr, this->m_str);
		//重新設定字串長度
		this->m_length += (s_len - n);
		//釋放原始記憶體空間
		delete[] this->m_str;
		//重新申請記憶體空間
		this->m_str = new char[this->m_length + 1];
		int s_count = 0,p_count=0;
		//字串替換操作
		for (int i = 0; i < this->m_length; i++) {
			if (i >= pos && i < pos + n) {
				this->m_str[i] = s[s_count++];
			}
			else
			{
				this->m_str[i] = pOldStr[p_count++];
			}
		}
		//設定字串結束標誌
		this->m_str[this->m_length] = '\0';
	}
	return *this;
}

拼接的實現方式有兩種,第一種是通過+=運算子過載實現;第二種是通過成員函式實現。第一種方式在2.2中已經實現,下面介紹第二種方式。首先在類中宣告如下函式,

2.3.4 字串插入

實現程式碼如下,

MyString & MyString::insert(int pos, const char * s)
{
	// TODO: 在此處插入 return 語句
	if (pos<0 || pos>this->m_length)
		return *this;
	int s_len = strlen(s), //字串長度
		s_index = 0, c_index=0;
	char* pStr = this->m_str;//保留原始字串內容
	//重新設定當前字串長度
	this->m_length += s_len;
	//重新分配記憶體空間
	this->m_str = new char[this->m_length + 1];
	//字串插入操作
	for (int i = 0; i < this->m_length; i++) {
		if (i >= pos && i < pos + s_len) {
			this->m_str[i] = s[s_index++];
		}
		else
		{
			this->m_str[i] = pStr[c_index++];
		}
	}
	//將最後一個字元設定為結束符
	this->m_str[this->m_length] = '\0';
	//釋放原始記憶體空間 
	delete[] pStr;
	//返回對當前物件的引用
	return *this;
}

2.3.5 去除空白字串

實現程式碼如下,但是這裡僅僅只對空白字元進行了考慮,沒有考慮tab等其他空白符。

MyString& MyString::trim()
{
	char* pStr = new char[this->m_length + 1];
	int count = 0;//記錄非空白字元的數量
	for (int i = 0; i < this->m_length; i++) {
		if (this->m_str[i] != ' ') {
			pStr[count++] = this->m_str[i];
		}
	}
	//將下一個設定為'\0'
	pStr[count] = '\0';
	//重新設定當前字串的長度
	this->m_length = strlen(pStr);
	//釋放原始字串記憶體空間
	delete[] this->m_str;
	//重新分配記憶體
	this->m_str = new char[count + 1];
	//字串拷貝
	strcpy(this->m_str, pStr);
	//釋放指標
	delete[] pStr;
	return *this;
}
關於C++中的C-風格字串和string字串的探討到這裡就結束了,後面2.2、2.3部分沒有做過多的解釋,因為程式碼中的註釋已經將其解釋得十分清晰了,另外,為了降低冗餘程式碼,部分地方直接對一些操作封裝為成員函式。其他不足之處希望大家多多指正!