原帖地址: LeoRanbom的博客
博主:LeoRanbom
只在原帖地址的博客上發布,其他地方看到均為爬取。
如果覺得不錯希望來點個贊。
前言
結束了前2天水了一個基礎的小程序,現在開始深入學習。本節我將從字符串入手開始復習,將涉及到C語言風格的字符串、C++的字符串、還有C++17在字符串方面提出的新標准新特性。
1.動態字符串
在接觸了java、Python等面向對象的語言,它們的字符串有很多方便的特性:可拓展至任意大小,提取或替換子字符串。但轉過頭來看c語言就會感覺特別地蛋疼。它連邊界檢測都沒有。
1.1.C風格的字符串
眾所周知,C語言的字符串是char類型的數組。通常我們會以char*的形式來給它動態分配內存。
缺點:
- C字符串不會記錄自己的長度信息,獲取字符串長度時,會遍歷字節數組,直到遇到null字符,復雜度O(n)。(官方將null字符('\0')定義為NUL,只有一個L)
- C字符串不記錄長度,修改時也不會判斷內存空間是否足夠。若不夠,會造成緩沖區溢出。
雖然用的是C++,但是經常會調用一些用C編寫的接口。因此我們也要去了解C風格的字符串
1.1.1.易錯點
C字符串經常會被遺漏掉'\0'。舉個例子,"hello"是5個字母,但是卻由'h','e','l','l','o','\0'這6個字符組成,所以在內存空間中其也是6個字節。而C語言對字符串的操作strcpy()和strcat()函數是極其不安全的,他們不會對字符串的長度進行檢測。
1.1.2.strcpy()
這是對字符串進行復制的函數。它有2個字符串參數,將第二個字符串復制到第一個字符串,但是卻不會考慮空間是否足夠。
這里用函數包裝改良一下來解決原有的問題:
char* copyString(const char* str){
char* newStr = new char[strlen(str) + 1];//strlen()函數可以獲得字符串的長度,但是不算\0
strcpy(newStr,str);
return newStr;
}//記得使用完后釋放copyString()分配的堆內存
1.1.3.strcat()
這里是對字符串進行拼接的函數。它也是2個字符串參數,將第二個字符串賦值到第一個后面,並儲存在第一個。同樣不會判斷內存空間。
這里也用函數來包裝一下,讓它能拼接3個字符串,並且自動擴充空間。
char* addStrings(const char* str1,const char* str2,const char* str3){
char* newStr = new char[strlen(str1) + strlen(str2) + strlen(str3) + 1];
strcpy(newStr,str1);
strcat(newStr,str2);
strcat(newStr,str3);
return newStr;
}
1.1.4.對字符串用sizeof()關鍵字或者strlen()函數的區別
在上面的1.1.2與1.1.3中能看到,我是用strlen()來獲取字符串的長度。sizeof是獲取字節數,按道理也可以表示長度啊。
這里我來排一下坑(實驗室組織的某次考核中,我因為概念不清,稀里糊塗地用混了,但王學長私下告訴我那次是第一,並且讓我繼承了他的電路書,這電路書是我疫情期間接觸過的唯一一本課本,感激涕零)
在C風格的字符串里,sizeof()會根據字符串的存儲方式來返回不同的大小。如果是char[],則返回實際存儲的類型,包括'\0';而如果是char*,則sizeof()會根據平台的不同,返回你的系統下指針的內存大小(32位是4,64位是8)
1.1.5.安全C庫
VS里用C風格的字符串函數時會出現警告,說是已經廢棄了。使用strcpy_s()和strcat_s()這些“安全C庫”即可避免。不過還是最好切換到C++的std::string類。
1.2.字符串字面量
1.2.1.字面量
cout<<"hello"<<endl;
這樣包含字符串本身,而不是包含字符串變量。它本身是一個字符串字面量(string literal),以值的形式寫出,而不是以變量的形式。而與字面量相關聯的內存位於內存只讀區。通過這種機制,編譯器可以對相同的字符串字面量進行復用,從而優化內存使用。e.g.一個程序用了幾百次"hello"字符串字面量,但其實只創建了一個hello實例,這就是字面量池(literal pooling)
C++標准指出字符串字面量是const char的數組。它可以被賦值給變量,但在用指針指向的時候會產生風險。如:
char* ptr = "hello";//聲明一個字符串變量(指針指向字符串字面量)
ptr[1] = 'a';//Undefined Behaviour
本身字面量是const不允許修改,但這里硬是修改的話很明顯就是一個ub。它根據編譯器的不同,可能會導致程序崩潰,可能會繼續運行;可能會產生改變,可能會忽視改變。
更安全一點的寫法習慣則是:
const char* ptr = "hello";
ptr[1] = 'a';//ERROR,attempt to write to read-only memory
這樣子寫的話,編譯器會因為“嘗試向只讀區域寫入”而報錯。
注:如果用的是字符數組char[]的形式,開辟了對應大小的內存。那么這個字面量不會放在只讀的內存區,也不會使用字面量池。當然也就可以自由改變。
1.2.2.原始字符串字面量
原始字符串字面量是不對轉義字符進行轉換的處理方式。它以R"(開頭,以)"結尾。
cout<<R"(hello "world"!\n)";
控制台會輸出
hello "world"!\n
而如果想要換行的話,在原始字符串字面量里直接打回車就可以了。
但因為結尾是)",所以其中不能有)"存在,否則會報錯。解決方法是使用拓展的原始字符串字面量語法——可選的分隔符序列。
R"d-char-sequence(lalala)d-char-sequence"
分隔符序列最多能有16個字符。
1.3.C++ std::string類
C++中,std::string是一個類(實際上是basic_string模板類的一個實例),這個類支持
1.3.1.有C的字符串,為什么還有C++的字符串?
C風格字符串的優勢和劣勢
優勢:
- 簡單,只用到基本數據類型(char)和數組結構
- 輕量,由於結構簡單,可以只占用所需的內存。
- 低級,可以按照操作原始內存的方式來對字符串進行操作。
- 能夠被C程序員理解
劣勢:
- 為了模擬字符串,要花很多努力。
- 使用難度大,容易產生bug,不安全
- 沒有用到C++的oop思想
- 要求程序員理解底層
1.3.2.使用string類
盡管string是一個類,但我更喜歡把它看做一個簡單的數據類型。通過運算符重載,讓對string的操作更為直觀和簡單。
string A("12");
string B("34");
string C;
C = A + B;//C is "1234"
+=,==,!=,<,[]等都被重載了。
而C語言中字符串的不安全(如果一個是char[],一個是char*,則會返回false,因為它比較的是指針的值,而非字符串內容。需要用strcmp(a,b)0來比較)
為了達到兼容的目的,可以用string類的c_str()方法來獲得一個C風格的const字符指針。不過一旦string進行內存重分配,或者對象被銷毀,這個指針也會跟着失效。
注意,不要從函數中返回在基於堆棧的string上調用c_str()方法得來的結果。
在C++17中,data()方法在非const字符調用時,會返回char(而14或更早的版本始終返回const char)
1.3.3.std::string字面量
源代碼中的字符串仍是const char*,但如果"xxx"s,那么可以把字面量變成std::string。
但需要std::string_literals或者std命名空間。
1.3.4.高級數值轉換
std名稱空間中有很多輔助函數,以便完成數值和字符串的轉換
- string to_string(int val);
- string to_string(unsigned val);
- string to_string(long val);
- string to_string(unsigned long val);
- string to_string(long long val);
- string to_string(unsigned long long val);
- string to_string(float val);
- string to_string(double val);
- string to_string(long double val);
還有將字符串轉換為數值,其中str是要轉換的字符串,idx是一個指針,接收第一個未轉換字符的索引,base表示轉換過程中使用的進制數。指針可以是空指針,如果是空指針,則被忽略。如果不能執行任何轉換,則會跑出invalid_argument異常,如果超出返回類型的范圍,則拋出out_of_range異常
- int stoi(const string& str, size_t *idx=0,int base=0);
- long stol(const string& str, size_t *idx=0,int base=0);
- unsigned long stoul(const string& str, size_t *idx=0,int base=0);
- longlong stoll(const string& str, size_t *idx=0,int base=0);
- unsigned long long stoull(const string& str, size_t *idx=0,int base=0);
- float stof(const string& str, size_t *idx=0);
- double stod(const string& str, size_t *idx=0);
- long double stold(const string& str, size_t *idx=0);
1.3.5.低級轉換(C++17)
C++17中提供了許多低級數值轉換函數,在
若要將整數轉換為字符,可以使用下面這組函數:to_char_result to_chars(char* first, char* last, IntegerT value, int base = 10);
。這里IntegerT可以是任何整數類型或字符類型。返回結果是to_char_result類型,看看定義:
struct to_char_result{
char* ptr;
errc ec;
};
如果成功轉換,ptr成員將等於所寫入字符的下一個位置(one-past-the-end)的指針,如果轉換失敗(即ec == errc.value_to_large),則它等於last。
下面舉個示例:
類似地,浮點數也可以被轉換:
to_chars_result to_chars(char* first,char* last, FloatT value);
to_chars_result to_chars(char* first,char* last, FloatT value, chars_format format);
to_chars_result to_chars(char* first,char* last, FloatT value, chars_format format, int precision);
可以通過修改chars_format來修改浮點數的數據類型
enum class chars_float{
scientific, //Style:(-)d.ddde±dd
fixed, //Style:(-)ddd.ddd
hex, //Style:(-)h.hhhp±d (NoteL no 0x)
general = fixed|scientific
}
默認格式是chars_format::general,將導致to_chars()將浮點值轉換為fixed的十進制表示形式或者scientific的十進制表示形式,得到最短的表示形式,小數點前至少有以為存在。如果指定了格式,但是沒有指定精度,那么會自動確定最簡短的表示形式。最大精度是6個數字。
而對於相反的轉換(即數->字符串),則有一下一組函數:
from_chars_result from_chars(const char* first, const char* last, IntegerT& value, int base = 10);
from_chars_result from_chars(const char* first, const char* last, FloatT& value, chars_format format = chars_format::general);
Here ,from_chars_result is a type defined as follows:
struct from_chars_result{
const char* ptr;
errc ec;
};
from_chars不會忽略任何前導空白。ptr指向未轉換的第一個字符。如果全部轉換,那么指向last,如果全沒轉換,指向first,ec將為errc::invalid_argument。如果值過大,則為errc::result_out_of_range。
1.4.std::string_view類(C++17)
在之前,為了接受只讀字符串的函數選擇形參類型一直讓人很猶豫。不知道是用const string&還是const char*。C++17中引入了std::string_view類來解決這類問題。
它是std::basic_string_view類模板的實例化,在<string_view>頭文件中定義。是const string&的簡易替代品。不會額外復制字符串,所以不會額外產生內存開銷,它支持和string類似的接口,但是缺少c_str()。並且添加了remove_prefix(size_t)和remove_suffix(size_t)方法。前者將起始指針 前移一定偏移量來收縮字符串,后者則將結尾指針倒退一定的偏移量來收縮。
注意:無法連接一個string和string_view,無法編譯(解決方法:使用string_view.data()這一方法)
當形參是string_view時,你就可以傳入string、 char*、字符串字面量(常量).
而如果以const string&為參數,則不能傳入字符串字面量常量和 char*。只能用string。(string_view轉換為string類方法:
1.xxx.data();
2.string(xxx)//explicit ctor。
1.4.1.string_view字面量
可使用"xxxxx"sv來讓字面量解釋為std::string_view。需要命名空間std::string_view_literals或者直接std。
1.5.非標准字符串
很多人不喜歡用C++風格的字符串,有些是因為不知道,有些則是不合口味。但無論是用MFC、QT內置的字符串,還是用自己開發的字符串,都應為項目或團隊確立一個標准。