第四部分(進階主題)
1).标準庫的附加特性,
- 求解大規模問題很有用
- 或者适用于特殊問題
2).我們使用的标準庫的名字,實際上都是使用名字為std的命名空間中的名字。
第十七章(标準庫特殊設施)
/1.tuple類型
1).
tuple
類似于
pair
成員。不同
tuple
類型的成員類型也不同,但一個
tuple
可以由任意數量的成員。每一個确定的
tuple
類型的成員數量也是固定的。
- 可以應用在一些資料組合成單一的對象,但是有不必建構一個類。
-
類型和它的伴随類,函數都定義在頭檔案tuple
中。tuple
- 相關的操作見p636。
//1.定義和初始化tuple
1).
{
// 當我們定義一個tuple時,需要指出每一個成員的類型
tuple<size_t, size_t, size_t> threeD;//三個成員都為0
tuple<string, vector<double>, int, list<int>> someval("contents", {1, 3, 4}, 34, {1, 2, 3});
// 建構一個tuple的對象可以使用它的預設構造函數,進行 值 初始化
// 或者為每一個成員提供一個初始值。
// 注意這個構造函數時explicit的
// 必須使用直接初始化
tuple<size_t, size_t, size_t> threeD = {1, 2, 3};//錯誤
tuple<size_t, size_t, size_t> threeD{1, 2, 3};//注意這裡也可以使用{}
// 類似于,make_pair函數,标準庫定義了make_tuple函數。
auto item = make_tuple("isbn", 2, 23.00);
// make_tuple就是一個函數模闆
//并且它通過推斷實參類型,得到tuple的類型
// tuple<const char *, int, double>
}
2).通路tuple的成員
-
的成員都是未命名的,使用名為tuple
的标準庫 函數模闆。get
{
// 使用get
auto book = get<0>(item);//傳回item的第一個成員
auto = cnt = get<1>(item);//傳回item的第二個成員
// 我們必須指定一個顯式模闆實參,指定我們要通路第幾個成員,
// 必須是一個整型常量表達式,從0開始計數。
// 并且傳遞一個tuple對象。
// 函數傳回的是指定成員的引用。
get<2>(item) *= .8;//打八折
// 如果不知道一個tuple準确的類型細節資訊,可以使用兩個輔助類模闆來查詢tuple的成員的數量和類型
typedef decltype(item) trans;//trans 是item的類型
size_t sz = tuple_size<trans>::value;//傳回的是tuple的成員的數量
tuple_element<1, trans>::type cnt = get<1>(item);//cnt是一個int類型
// 使用之前需要知道一個tuple對象的類型,
// 然後對兩個類模闆進行執行個體化。
// 取它的value和type成員即可。
// 其中,value是一個public static
// type是一個public成員,
// 類似于get,tuple_element的索引下标也是從0開始。
}
3).關系和相等運算符
-
的比較運算符,比較左右兩個tuple
裡的成員,這一點和容器對應;但是隻有兩個tuple中的成員數量一樣時,我們才可以比較它們。并且比較時,我們需要保證對于每一個成員相應的比較運算是有定義的。tuple
{
tuple<string, string> dou("1", "2");
tuple<size_t, size_t> twoD(1, 2);
bool b = (dou == twoD);//錯誤,不能比較size_t和string
tuple<size_t, size_t, size_t> threeD(1, 2, 3);
b = (twoD < twoD);//錯誤,成員數量不一緻
tuple<size_t, size_t> origin(0, 0);
b = (origin < twoD);//正确,b為true
// 由于在tuple中定義了==和<運算,是以可以将tuple傳遞給排序算法,可以在無序容器中作為關鍵字類型。
}
//2.使用tuple傳回多個值
1).
{
// 每一個元素代表一家店的銷售記錄
vector<vector<Sales_data>> files;
// 對于一本書,我們在整個files中搜尋出售過這本書的書店,對于每一個加與比對的銷售記錄的書店,我們将建立一個tuple
// 來儲存這加書店的索引和兩個疊代器。
// 索引指出書店在files中的位置,疊代器标記給定書籍在此書店vector<Sales_data>的起止位置。
// 傳回tuple的函數
typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator, vector<Sales_data>::const_iterator
> matches;
// findBook傳回一個vector,每一個銷售了給定書籍的店都有一項
vector<matches>
findBook(const vector<vector<Sales_data>> &files,
const string &book) {
vector<matches> ret;//初始化為空的vector
// const疊代器,是底層的const
for (auto it = files.cbegin(); it != files.cend(); ++it) {
auto found = equal_range(it->cbegin(), it->cend(),
book, compareIsbn);
if (found.first != found.second) //此書店銷售了給定的書籍
ret.push_back(make_tuple(it - files.cbegin(), found.first, found.secod);
}
return ret;
}
// 預設情況下,equal_range是使用<進行比較,我們使用compareIsbn可調用對象。
}
3).對傳回的結果進行處理
{
void reportResult(istream &is, ostream &os, const vector<vector<Sales_data>> &files) {
string s;//需要查找的書籍
while (is >> s) {
auto trans = findBook(files, s);
if (trans.empty()) {
cont << s << "not found in ant stores" << endl;
continue;
}
for (const auto &store : trans) {
os << "store: " << getr<0>(store) << "sales: " << accumulate(get<1>(store), get<2>(store), Sales_data(s)) << endl;
}
}
}
}
/2.bitset類型
1).
bitset
類可以處理超過最長整型類型大小的位集合。而且位運算也變得更加肉容易。
bitset
類定義在頭檔案
bitset
中。
//1.定義和初始化bitset
1).
bitset
,是一個類模闆,它類似
array
類,具有固定的大小。當我們定義一個
bitset
時,需要聲明它包含多少個二進制位。
- 它的大小必須是一個常量表達式。
{
bitset<32> bitvec(1U);//32位,最低位為1,其他位為0;
// 這句語句定義bitset為一個包含32位
// 的bitset。
// 就像vector中的包含未命名元素一樣
// bitset包含的二進制位也是命名的。我們使用位置來進行通路
// 二進制的位也是從0開始編号的
// 是以,編号是從0-31。
// 編号從0開始的二進制位被稱為低位
// 編号到31号結束的二進制位被稱為高位。
}
2).初始化方式表見p641表格。
- 使用
值初始化unsigned
bitset
{
// 當我們使用一個整型值來初始化bitset時,此值被轉換為unsigned long long類型,并且被當作位模式來處理
// bitset的二進制位将是此模式的一個副本
// 如果bitset的大小等于一個unsigned long long 中的二進制位數,則剩餘的高位被置為零
// 如果bitset的大小小于一個unsigned long long中的二進制位數。
// 則隻是用給定值中的低位,超出的部分就被丢棄。
bitset<13> bitvec1(0xbeef);
bitset<20> bitvec2(0xbeff);
bitset<128> bitvec3(~0ULL);//0-63位為1,其餘的為0;
// 在64位的及其中,long long 0ULL是64個bit。
}
- 用一個
來初始化string
bitset
{
// 我們可以使用一個string或者一個字元數組指針來初始化bitset
// 兩種情況下,字元都直接表示位模式
// 當我們使用字元串表示數時,字元串中下标最小的字元對應最高位
bitset<32> bitvec4("1100");//表示的二進制位為,0011,其餘的位置是0
// 這是因為如果string中包含的字元數比bitset少,則bitset的高位置為0
// 使用字串來初始化一個bitset
string str("10010010010010010101");
bitset<32> bitvec5(str, 5, 4);//從str[5]開始的4位二進制位,1100。
// 結果同上
bitset<32> bitvec6(str, str.size() - 4);//使用最後4個字元
// 初始化和以上的方式一樣。
// 低位指派給高位,
// 是平移不是翻轉。
}
練習,
- 17.9,如果輸入的不是二進制數,會抛出,
的異常。invalid_argument
//2.bitset操作
1).有多種檢測或者設定位的方法。也支援位運算,并且含義也是與我們将這些運算符作用于
unsigned
的含義是相同的。。
- 操作表,見p643。
{
// count, size, all, none,等不接受參數,傳回的是整個bitset的狀态。
// set, reset, flip, 改變bitset的狀态。
// 改變bitset的狀态的成員函數都是重載的
// 對于每一個函數,不接受參數的版本是對整個集合執行給定的操作,
// 接受一個位置參數的版本則對指定位置進行操作
bitset<32> bitvec(1U);//低位為1,其他為0
bool is_set = bitvec.any();//傳回true,因為有一位置位
boll is_not_set = bitvec.none();//false
bool all_set = bitvec.all();//false
size_t onBits = bitvec.count();//1
size_t sz = bitvec.size();//32
bitvec.flip();//翻轉所有的位
bitvec.reset();//所有位複位
bitvec.set();//所有位置位
// 置位就是等于1。
// size操作時一個constexpr操作,允許我們使用在任何要求常量表達式的地方。
// 可以接受參數的重載版本
bitvec.flip(0);//翻轉第一位
bitvec.set(bitvec.size() - 1);//将最後一位置位
b.set(0, 0);//複位最後一位
b.reset(i);//複位第i位
b.test(0);//傳回false,因為第一位時複位的。
// 下标運算對const屬性進行了重載
// const版本在指定位置位時傳回true,
// 否則傳回false
// 非const版本,傳回bitset定義的一個特殊類型,允許我們操作指定位的值
b[0] = 0;//将第一位複位
b[31] = b[0];
b[0].flip();//反轉第一位
~b[0];//翻轉第一位
bool a = b[0];//轉為bool值。
}
2).提取
bitset
的值
{
// to_ulong, to_ullong操作都傳回一個值,儲存了于bitset對象相同的位模式
//隻有當bitset的大小等于對應的大小時,才能使用這兩個操作
unsigned long ulong = bitvec3.to_ulong();
// 如果bitset中的值不能放入給定的類型時,兩個兩個操作會抛出一個overflow_error的異常。
}
3).
bitset
的IO運算符
{
// 輸入運算從一個輸入流中讀取字元,儲存在一個臨時的string對象中。
// 直到讀取的字元數達到對應的bitset大小時或者遇到不是1或者0的字元時,或者遇到檔案尾或者遇到錯誤時,讀取程式才會結束
// 随即用臨時的string對象來初始化bitset。
// 此時的規則就和用string來初始化bitset是一樣的
bitset<16> bits;
cin >> bits;
cout << "bits: " << bits << endl; //這裡輸出的是string,也就是剛剛讀入的string
}
4).使用
bitset
{
// 實作評分程式
bool status;
unsigned long quizA = 0;//此值将被當作位集合來使用
quizA |= 1UL << 27; //指出第27個學生通過了測驗
status = quizA & (1UL << 27);//檢查第二十七位學生是否通過了測驗
quizA &= ~(1UL << 27);//第二十七位學生沒有通過測驗
// 使用标準庫bitset進行等價的操作
bitset<30> quizB;
quizB.set(27);
status = quizB[27];
quizB.reset(27);
}
練習,
- 17.13,好題。鍛煉模闆的使用,類外定義成員函數的注意事項。
/3.正規表達式
1).重點介紹如何使用。RE庫(正規表達式庫)定義在頭檔案
regex
中。包含的元件見p645。
2).
regex
可以做一些什麼。
-
類表示一個正規表達式。除了指派和初始化之外,還支援一些其他的操作。見p647。regex
- 函數
确定一個給定的字元序列與一個給定的regex_match,regex_search
是否比對。如果整個輸入序列和表達式比對,則regex
傳回regex_match
,如果輸入序列中的一個字串與表達式比對,則true
函數傳回regex_search
。true
3).p646列出了,
regex
的函數的參數。這些函數都是傳回
bool
,。而且被重載了。其中一個版本接受類型為
smatch
的附加參數。如果比對成功,這些函數将成功比對的相關資訊儲存在給定的
smatch
對象中。
//1.使用正規表達式庫
1).從簡單的例子開始。
{
// 查找違反拼寫規則“i除非在c之後,否則必須在e之前”的單詞。
// 查找不在字元c之後的字元串ei
string pattern("[^c]ei");
// 我們需要包含pattern的整個單詞
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern);//構造一個用于查找模式的regex
smatch results;//定義一個對象儲存搜尋的結果
string test_str = "receipt freind theif receive";
// 用r在test_str中查找與pattern比對的子串
if (regex_search(test_str, results, r)) //如果有比對的子串
cout << results.str() << endl;//列印比對的單詞
//[^c]表示比對任意非c的字元,
// [^c]ei,表示三個字元,第一個不是c後面兩個是ei
// 如果我們想要得到完整的單詞,
// 由于regex使用的正規表達式語言是ECMAScript。在ECMAScript中,模式[[:alpha:]]表示任意的字母
// 符号+表示1個或者多個字元
// 符号*表示0個或者多個字元。
// 我們将正規表達式存入在string中後,
// 用來初始化一個名字為r的regex對象。
// 如果regex_search函數比對到字串,就會傳回true。使用smatch對象results中的str成員來列印,模式比對的部分。
// 由于函數regex_search在輸入序列中隻要找到一個比對的字串就會傳回,停止查找,是以
// 輸出結果就是freind
}
2).指定
regex
對象的選項見表格p647
- 當我們定義一個
或者對一個regex
使用regex
賦新值的時候,可以指定一些标志來影響assign
如何操作。這些标志控制regex
對象的處理過程。詳見p647。regex
- 對于指出編寫正規表達式所用的語言的6個标志,我們必須設定其中一個,而且隻能設定一個。預設情況下,
标志被設定。進而,ECMAScript
會使用ECMA-262規範。這也是很多regex
浏覽器所使用的正規表達式語言。Web
- 其他的三個标志允許我們指定正規表達式處理過程中與語言無關的方面。例如,我們可以指定希望正規表達式比對過程中以大小寫無關的方式進行。
{
// 使用icase标志查找具有特定擴充名稱的檔案
// 大多數系統都是以大小寫無關的方式來識别擴充名的
// 例如c++程式的擴充名可以是cc, Cc, cC, CC等。效果是一樣的
// 一個或者多個字母或者數字後面加上一個.
// 再接上cpp或者cxx或者cc
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results;
string filename;
while (cin >> filename) {
if (regex_search(filename, results, r)) {
cout << results.str() << endl;//列印比對的結果。
}
}
// 此時正規表達式會比對指定檔案的擴充名而不會理會大小寫
// 在正規表達式中,.表示比對任意字元
// 在其前面加上一個\去掉其特殊含義
// 由于反斜杠也是一個特殊字元,是以
// 得到一個.需要這樣做\\.
}
3).我們可以将正規表達式本身看作是,用一種簡單程式設計語言編寫的“程式”。這種語言不是由c++編譯器解釋的。正規表達式是在運作時,當一個
regex
對象被初始化或者被賦予一個新模式時,才被“編譯”的。與任何其他程式設計語言一樣,我們用這種語言編寫的正規表達式也可能會有錯誤。需要意識到的是,一個正規表達式的文法是否正确是在運作時才解析的。
- 如果我們編寫的正規表達式存在錯誤,則在運作時會抛出一個
的異常。regex_error
- 類似于标準異常類型,
有一個regex_error
操作描述發生了什麼錯誤;還有一個名為what
的成員,傳回某一個錯誤類型對應的數值編碼;它傳回的值是由具體實作定義的。RE庫能抛出的标準錯誤,見表17.7(p649)code
{
try {
// 漏掉一個方括号的錯誤
regex r("[[:alnum:]+\\.(cpp|cxx|cc)&", regex::icase);
} catch (regex_error e) {
cout << e.what() << "\ncode :" << e.code << endl;
}
// 生成結果就是,
regex_error(error_brack)
The expression contained mismatched [ and ].
code: 4
// 我們的編譯器定義了code成員,傳回的是表17.7的錯誤類型的編号,
// 一樣的編号從0開始
// 例如以上的錯誤error_brack
// 就是第五個錯誤類型,編号為4.
// 正規表達式的編譯時一個非常慢的操作,特别時你是喲共了擴充的正規表達式文法
// 或者是複雜的正規表達式時
// 是以,構造一個regex對象以及對一個已存在的regex賦予一個新的正規表達式可能是非常耗時間的
// 為了最小化這種開銷,避免建立不必要的regex
// 例如,當我們需要在循環裡面使用正規表達式時,應該在循環體外面建立它
// 而不是在循環體内建立
}
4).正規表達式類型和輸入序列類型
- 輸入的序列可以是普通的
資料,也可以是char
資料wchar_t
- 字元可以是儲存在
,也可以是在string
數組中。(寬字元版本,char
,wstring
數組中。)wchar_t
- RE庫,為這些不同的輸入序列都定義了對應的類型。
-
類儲存類型regex
的正規表達式;char
類儲存類型wregex
的正規表達式,它的操作和wchar_t
完全相同。唯一的差别是regex
的初始值必須是喲共wregex
而不是wchar_t
。char
-
表示smatch
類型的輸入序列;string
表示字元數組的輸入序列;cmatch
表示寬字元串wsmatch
的輸入序列;wstring
表示寬字元數組的輸入序列。wcmatch
- 我們使用的RE庫類型,必須和輸入序列類型比對。詳見p650。
{
// 如果不比對
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results; //比對的是string的序列
if (regex_reach("myfile.cc", results, r));//錯誤,待比對的序列是一個const char *
//将上述的smatch 改為cmatch即可。
}
//2.比對和Regex疊代器類型
1).我們可以使用
sregex_iterator
疊代器類擷取所有的比對。
-
疊代器是一種疊代器擴充卡,被綁定到一個輸入序列和一個regex
對象上。疊代器的操作見表17.9(p651)regex
- 當我們将一個
綁定到一個sregex_iterator
序列和一個string
對象上時,疊代器自動定位到regex
中的第一個比對的位置。即,string
的構造函數自動對給定的sregex_iterator
和string
調用regex
。當我們解引用疊代器時,會得到一個對應一次所有結果的regex_search
對象,當我們遞增疊代器時,samtch
會輸入序列regex_search
中查找下一個比對。string
2).使用
sregex_iterator
。
{
string pattern("[^c]ei");
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern, regex::icase);
for (sregex_iterator it(file.begin(), file.end(), r), end_it;
it != end_it; ++it) {
cout << it->str() << endl;//輸出比對的單詞。
}
// 注意end_it是一個空的sregex_iterator;起到尾後疊代器的作用。
// 如果我們還想要比對結果的上下文資訊
// 見表17.10,17.11
// smatch和ssub_match類型允許我們獲得比對的上下文資訊。
// 比對類型,有兩個名為prefix和suffix的成員。調用後分别傳回表示輸入序列中目前比對之前和之後的部分的ssub_match對象
// 一個ssub_match對象有兩個名為str和length的成員,分别 傳回 比對的string和該string的大小
for (sregex_iterator it(file.begin(), file.end(), r), end_it);
it != end_it; ++it) {
auto pos = it->prefix().length(); //字首的大小
pos = pos > 40 ? pos - 40 : 0; //最多隻要40個字元
// 從0開始的位置。
cout << it->prefix().str().substr(pos)
<< "\n\t\t>>> " << it->str() << " <<<\n"
<< it->suffix().str().substr(0, 40)
<< endl;
}
}
//3.使用子表達式
1).正規表達式中的模式通常包含一個或者多個子表達式。一個子表達式是模式的一部分,本身也具有意義。正規表達式文法通常用括号表示子表達式。
- 例如,比對檔案字尾時,就是用括号類分組可能的檔案擴充名。每當我們使用括号分組多個可行選項時,同時也就聲明了這些選項形成子表達式。
{
// 模式中點之前的檔案名也形成子表達式
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$",regex::icase);
// 有兩個子表達式,
// 改寫程式使之輸出語句值列印檔案名
if (regex_search(filename, results, r))
cout << results.str(1) << endl; //列印第一個子表達式
// 比對對象不僅僅提供比對整體的資訊之外,還提供通路 模式中子表達式的能力
// 子比對是按照位置來比對的。第一個子比對的位置為0,表示整個模式對應的比對;
// 随後是每一個子表達式對應的比對。
// 預設情況下就是0。
}
2).子表達式用于,資料驗證
- 驗證必須比對特定格式的資料
{
// 美國的電話号碼驗證
// 首先将用一個正規表達式找到可能是電話号碼的序列
// 再調用一個函數完成資料的驗證
// 首先了解ECMAScript正規表達式語言的一些特性
//1. \{d}表示單一個數字,\{d}{n}表示一個n個數字的序列(\{d}{3}表示比對三個數字的序列)
//2. 在方括号中的字元集合表示比對這些字元中的任意一個。([-. ]比對一個短橫或者一個點
// , 或者一個空格 注,.在方括号裡面沒有特殊含義)
//3. 後接'?'的元件時可選的。
// \{d}{3}[-. ]?\{d}{4}
//可以比對444-9088/.0989/ 0989/9069。/表示或者,不是語言特性,筆者偷懶而已。
//4. 類似于c++,ECMAScript使用反斜杠\表示沒有特殊含義。
// \(\)才表示括号,而不是特殊的字元
// 由于反斜線是c++的特殊字元,在模式中使用\的地方,我們都必須用一個額外的\來告訴c++,我們使用的是一個\而不是特殊符号
// \\{d}{3},來表示\{d}{3}這一正規表達式
// 驗證手機号碼,我們需要得到模式的組成部分。
// 因為我們不希望的到隻有一個括号的情況
// 為了獲得比對的組成部分,我們需要定義正規表達式時使用子表達式。
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
// 其中\\d{3}和\\{d}{3}含義相同
// ([-. ]?)和([-. ])?含義也是一樣的
string phone = ...;
regex r(phone);
smatch results;
string s;
while (getline(cin, s)) {
for (sregex_iterator it(s.begin(), s.end(), r), end_it;
it != end_it; ++it) {
if (valid(*it))
cout << "valid: " << it->str() << endl;
else
cout << "not valid: " << it->str() << endl;
}
}
// 使用子比對的操作來編寫valid函數
// pattern有7個子表達式
// smatch對象會有8個ssub_match元素
// smatch[0]表示整個比對的表達式,
// 當調用valid時,我們知道一定有一個完整的比對。
// 如果子表達式是完整比對的一部分,則其對應的ssub_match對象的matched成員是true
// 主要檢查是否是完整的括号或者是沒有括号
bool valid(const smatch &m) {
// 如果區号左邊的括号是存在的
if (m[1].matched) {
// 則區号後面必須有一個右括号
// 之後緊跟剩餘的号碼或者一個空格
return m[3].matched &&
(m[4].matched == 0 || m[4].str() == " ")
else {
// 區号後面的不能有括号
// 另外兩個組成部分間的分隔符必須比對,即使用的分隔符要相同。
return !m[3].matched &&
m[4].str() == m[6].str();
}
}
}
}
練習
- 17.22,任意多個空白字元。
還是(\s)
(\s)*
//4.使用regex_replace
1).用來查找并且替換一個序列的時候。詳見p657(表17.12)。它接受一個輸入字元序列和一個
regex
對象,以及我們想要的輸出形式的字元串。
- 替換字元串由我們想要的字元組合和比對的字元串中對應的子表達式組成。
{
// 使用第二個,第五個,第七個子表達式
// 而忽略其他的子表達式
// 我們使用$後跟子表達式的索引号來表示一個特定的子表達式。
string fmt = "$2.$5.$7";//将号碼格式改為ddd.ddd.dddd
// 使用
regex r(phone);//用來尋找模式的regex對象
string number = "(908) 555-1800";
cout << regex_replace(number, r, fmt) << endl;
// 輸出結果是
908.555.1800
}
2).隻是替換輸入序列中的一部分
{
// 可以用在一個很大的文本中
// 處理電話号碼的格式修改
int main() {
string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\d{3})([-. ])?(\d{4})";
regex r(phone);
string s;
smatch m;
string fmt = "$2.$5.$7";//用來修改格式
while (getline(cin, s)) {
cout << regex_replace(s, r, fmt) << endl;
}
return 0;
}
}
3).用來控制比對和格式的标志
- 與控制
對象比對過程的标志一樣。替換過程中有相似的控制比對和格式的控制。(這些都是标準庫定義的)。詳見表格17.13。regex
- 這些标志可以傳遞給函數
regex_search,regex_match或者類smatch的format成員
- 比對和格式化标志的類型為
。這些值均定義在match_flag_type
命名空間中。與regex_constants
的bind
,placeholders
也是在命名空間regex_constants
中的命名空間。std
-
using std::regex_constants::format_no_copy;
{
// 使用格式标志
// 預設情況下regex_replace會将輸入序列全部輸出
// 沒有比對的會原樣輸出,
// 比對的按格式字元串指定的格式輸出
string fmt2 = "$2.$5.$7 ";//電話号碼後面放置一個空格符來作為分隔符
cout << regex_replace(s, r, fmt2, format_no_copy) << endl;
// 隻輸出它所改變的文本
}
/4.随機數
1).在新标準之前,c和c++都依賴于一個簡單的c庫函數
rand
。此函數生成均勻分布的僞随機數,範圍在0-32767之間(系統相關的最大值)。
- 問題
- 需要随機的浮點數,需要非均勻分布的數。為了轉換生成的随機數,類型或者分布,常常會引入非随機性。
- 解決,定義在頭檔案中
的随機數庫通過一組協作的類來解決這些問題,random
- 随機數引擎類,生成随機的
整數序列,範圍内的每一個數被生成的機率是相同的。unsigned
- 随機數分布類,使用引擎傳回指定類型,給定範圍的,服從特定機率分布的随機數。
- c++程式不應該使用庫函數
,應該使用rand
類和恰當的分布類對象。default_random_engine
//1.随機數引擎和分布
1).随機數 引擎 是一個函數對象類。定義了一個調用運算符函數。
- 該運算符函數不接受參數
- 傳回一個随機的
整數。unsigned
{
default_random_engine e;
for (size_t i = 0; i < 10; ++i)
cout << e() << " ";
// 标準庫定義了多個引擎類,差別在于性能和随機性品質不同。
// 每一個編譯器都會指定其中一個作為default_random_engine類型
// 該類型一般具有最常用的特性。
// 标準庫定義的引擎類見p783
// 引擎類的操作見p660
// 我們把以上稱為原始随機數
// 因為大多數情況下,以上的輸出不能直接使用
// 問題在于範圍與我們的所需要的是不符合的,而且轉換是困難的
}
2).分布類型和引擎
{
// 使用分布類型對象得到指定範圍的随機數
// 生成0-9之間的(包含)均勻分布的随機數
uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e;
for (size_t i = 0; i < 10; ++i) {
cout << u(e) << " ";
}
// uniform_int_distribution<unsigned>類型生成 均勻分布 的unsigned值。
// 當我們定義該類型的對象時。
// 可以提供想要的最大和最小值(包含)
}
- 分布類型也是函數對象類。它接受一個随機數引擎類作為參數,分布對象使用它的引擎參數生成随機數,并将其映射到指定的分布中。
{
// 注意我們傳遞的是一個引擎對象,而不是它的一個随機數值
u(e);//正确
u(e());//編譯錯誤
// 原因在于,某一些分布可能需要調用引擎多次才可以得到一個值
}
- 随機數發生器,指的是分布對象和引擎對象的組合。
3).比較随機數引擎和
rand
函數
比較類型 | rand | 引擎對象 |
---|---|---|
生成數的範圍 | 在0-RAND_MAX之間 | 它生成的unsigned在一個系統定義的範圍内,可以調用該類型對象的 , 傳回值來得到。依賴于系統。 |
4).引擎生成一個數值序列
{
// 對于一個給定的發生器,每一次運作它都會傳回相同的數值序列
// 序列不變這一事實可以用來調式
// 使用時也需要注意這一點
vector<unsigned> bad_randVec() {
default_random_engine e;
uniform_int_distribution<unsigned> u(0, 9);
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i) {
ret.push_back(u(e));
}
return ret;
}
//每一次調用這個函數都會傳回相同的vector
// 編寫此函數的正确方法是,将引擎和關聯的分布對象定義為static
// 因為我們希望引擎和分布對象保留狀态,是以我們需要把它們定義為static
// 進而每一次調用都生成新的數
// 這樣第一次調用生成前100個随機數,
// 第二次調用哦生成接下來的100個數
// 以此類推
}
5).設定随機數發生器種子
- 提供種子,
。種子就是一個數值, 引擎 利用它從序列中的一個新位置重新開始生成随機數。seed
- 兩種方式提供
seed
- 建立時設定種子
- 調用引擎的
成員seed
{
default_random_engine e1;//使用預設的種子
default_random_engine e2(234235242);//使用給定的種子值
default_random_engine e3; //使用預設的種子值
e3.seed(32767); //設定一個新的種子值
default_random_engine e4(32767);
//d3和e4将會生成相同的随機數值
// 因為它們的種子是一樣的
// 選擇一個好的種子是及其困難的。
// 最常用方式就是調用系統函數time
// 該函數定義在頭檔案ctime中
// 傳回從一個特定時刻到目前經過了多少秒
// 函數接受單個指針參數
// 它指向用于寫入時間的資料結構
// 指針為空,函數簡單地傳回時間
default_random_engine e(time(0));
// 由于time是以秒計時的,這種方式生成的種子适合間隔以秒為級别的或更長的應用。
// 如果程式是需要反複進行,time作為種子的方式可能導緻多次使用的都是同一個種子
}
//3.其他随機分布
1).解決不同分布和不同類型的問題(随機引擎隻是生成均勻分布的
unsigned
)。标準庫定義了不同的随機數分布類來滿足這個需求。
2).生成随機實數
- 解決0-1之間的随機數。最常用但不正确的從
獲得一個随機浮點數的方法是使用rand
。不正确的原因是随機整數的精度通常低于随機浮點數,這樣一些浮點值就永遠不會生成。rand()/RAND_MAX
- 使用新标準庫設施,支援的操作見p664。
{
// 定義一個uniform_real_distribution 對象
// 讓标準庫從随機整數到随機浮點數的映射。
// 我們同樣可以指定範圍。
default_random_engine e;
uniform_real_distribution<double> d(0, 1);
// 使用
u(e);
// 分布類型都是模闆。具有單一的模闆類型參數,表示分布生成的随機數的類型
// 這些分布類型要麼生成整型要麼生成浮點類型
// 每一個分布模闆都有一個預設模闆實參,生成浮點型的分布類型預設生成的是double;
// 生成整型值的分布預設是int。
// 由于分布類型隻有一個模闆參數,是以當我們希望使用預設随機數類型時,要在後面加上<>
uniform_real_distribution<> u(0, 1);//預設生成的是double
// ------生成非均勻的随機數----------
// 除了可以指定範圍,類型,還可以指定分布。
// 20種的分布類型,見p781。
// 生成正态分布的值的序列,并畫出值得分布
default_random_engine e;
normal_distribution<> n(4, 1.5);//均值4,标準差1.5
vector<unsigned> vals(9);//均為0
for (size_t i = 0; i != 200; ++i) {
unsigned v = lround(n(e));//舍入到最接近的整數
if (v < val.size())
++vals[v]; //統計出現的次數
}
for (size_t i = 0; i != vals.size(); ++i) {
cout << i << ": " << string(vals[i], "*") << endl;
}
//normal_distribution生成浮點值
// 頭檔案的cmath中的lround函數。得到最接近的整數
// 以4為均值,表示以4為中心
// 由于是正态分布,我們希望99%的數都在0-8之間。
// 先進行統計次數。
// 列印一個星号組成的圖,來表示随機分布
// -----------bernoulli_distribution(伯努利随機分布)
// 該類是一個普通類。此分布總是傳回一個bool,它傳回true的機率是一個常數,預設是0.5
// 編寫誰先行的程式
// 1.可以使用uniform_int_distribution來選擇誰先行,範圍為0-1即可
//2. 使用bernoulli随機分布。
string resp;
default_random_engine e; //需要保持狀态,在函數外圍定義。
bernoulli_distribution b; //預設是50/50的機率
// 分布對象也需要保持狀态
// bernoulli_distribution b(.55);//表示有.55的機率得到true
do {
bool first = b(e);
cout << (first ? "We go first" : "You get to go first") << endl;
// 傳遞誰先進行遊戲
cout << ((play(first)) ? "sorry you lost" : "congrats, you won") << endl;
cout << "play again ? Enter 'yes' or 'no'" << endl;
} while (cin >> resp && resp[0] == 'y');
// 如果随機數引擎和分布定義在裡面
// 每一次得到的随機數一樣的,遊戲的先行者是固定的
}
/5.IO庫再探
//1.格式化輸入與輸出
1).除了條件狀态之外,每一個
iostream
對象還維護一個格式狀态來控制IO如何格式化的細節。格式狀态控制格式化的某一些方面,
- 整數的進制
- 浮點值的精度
- 輸出元素的寬度
2).标準庫定義了一組操作符,來修改流的格式狀态。見表17.17(p670)、17.8。一個操作符是一個函數或者一個對象。它們可以用作輸入或輸出運算符的運算對象,也傳回流的對像。是以我們可以在一條語句中組合操縱符和資料。
3).操縱符用于兩大類輸出控制。
- 控制數值的輸出形式
- 控制補白的數量和位置
- 大多數的改變格式狀态的操縱符都是設定和複原成對的。當操縱符改變流的格式狀态時,通常改變後的狀态對所有後續的IO都生效。
- 通常在不需要特殊格式時盡快将流恢複到預設的狀态下。
4).應用
- 控制布爾值的格式
{
cout << "default bool value: " << true << " " << false
<< "\nalpha bool value: " <<
boolalpha <<
true << " " << false
<< endl;
// 輸出結果就是1 0 true false
// 使用了boolalpha來覆寫預設的格式
// 将它複原
cout << boolalpha << true << noboolalpha ;
}
- 控制整型的進制
{
// 預設情況下就是十進制
cout << "default:" << 20 << " " << 1024 << endl;
cout << "octal(8):" << otc << 20 << 1024 << endl;
cout << "hex(16):" << hex << 20 << 1024 << endl;
cout << "decimal(10):" << dec << 20 << 1024 << ednl;
// 輸出
20 1024
24 2000
14 400
20 1024
// 注意可以進行覆寫。otc之後hex或者dec
// 以上的操縱符隻對整型影響,對于浮點值的表示形式沒有影響
// --------在輸出中指出進制
// 解決我們不知道是幾進制的問題
// 顯式進制
// 顯式的規範和我們在整型常量中指定進制的規範一樣
cout << showbase; //列印整型值時顯式進制
...
cout << noshowbase;
// 輸出
20 1024
024 02000
0x14 0x400
20 1024
// 預設情況下,十六進制值會以小寫列印,0x也是小寫的,
// 我們可以使用uppercase操作符号來輸出大寫的X并将十六進制數字也将大寫的放hi輸出
cout << uppercase << showbase << hex << .....
<< nouppercase << noshowcase << dec << endl;
// 輸出
0X14 0X400
}
- 控制浮點數格式
{
// -----------指定列印精度
// 預設情況下列印的是6位數字
// -----------指定是否列印小數點
// 如果浮點值沒有小數部分,不列印小數點
// -----------指定格式(十六進制如何?)
// 根據浮點數的值選擇列印成定點十進制或者科學計數法的形式。
// 标準庫會選擇一種可讀性好的格式;非常大或者非常小的值列印為科學計數法的形式,其他列印成十進制定點的形式
// -----------指定列印的精度
// 預設情況下,精度會控制列印的數字總數;列印時,浮點值按目前精度舍入而不是截斷
// 例如精度為4,則3.14159将列印為3.142
// 如果精度為3,将列印為3.14
// 使用IO對象的precision成員或者
// setprecision操縱符來改變精度
// precision成員是重載的,一個版本接受int,将精度設為該值,并傳回舊精度
// 另一個版本不接受參數,傳回當親啊的精度
// setprecision操縱符接受一個參數,用來設定精度。
// 操縱符setprecision和其他接收參數的操縱符都定義在頭檔案iomanip中
cout << "Precision: " << cout.precision() << ",value:" << sqrt(2.0) << endl;
// 将精度設定為12
cout.precision(12);
cout << "..." << ....
cout << setprecision(3);
.....
// 輸出
6,1.41421
12, 1.41421356237
3, 1.41
// sqrt标準庫函數,定義在頭檔案cmath中。
// sqrt是重載的,分别接受一個float,double,long double參數。傳回實參的平法根。
//--------------指定浮點數記數法
// 使用操縱符scientific改變流的狀态使用科學計數法
// 使用操縱符fixed改變流的狀态使用定點十進制
// 新标準庫中,允許使用hexfloat強制浮點數使用十六進制格式
// 新标準中,還提供一種名為defaultfloat的操縱符,它将流恢複到預設狀态--根據要列印的值選擇計數法
// 這些操縱符也會改變精度的預設含義
// 在執行scientific,fixed或者hexfloat時
// 精度值控制的是小數點後面的數字位數
cout << 100 * sqrt(2.0) << '\n'
<< scientific
<< fixed
<< hexfloat
<< defaultfloat
<< endl;
// 輸出
141.421
1.414214e+002
141.421356
0x1.ad7bcp+7
141.421
// 同理我們可以使用uppercase将小寫改成大寫
//--------------列印小數點
cout << 10.0 << endl;//10
cout << showpoint << 10.0
<< noshowpoint << endl;/10.000000
}
- 控制補白
{
// 1、setw指定下一個數字或字元串值得最小空間
// 2、left表示左對齊輸出
// 3、right表示右對齊輸出,這是預設格式
// 4、internal控制負數的符号的位置,它是左對齊符号,右對齊值,控制填滿之間的空間
// 5、setfill允許指定一個字元來代替預設的空客來補白輸出
// setw不改變流的内部狀态,它隻是改變下一個輸出的大小。
int i = -16;
double d = 3.14159;
....
cout << internal << left << right ....
cout << sefill('#') << setw(12) << d << endl;
cout << setw(12) << i << endl;
cout << setfill(' ');
// 輸出
// 右對齊
#########-16
#####3.14159
// 左對齊
-16#########
3.14159#####
// internal
-#########16
#####3.14159
// 注意,小數點也是一個位
// 操縱符号也是隻對特定的類型起作用,對其他類型沒有影響
}
- 控制輸入格式
{
// 預設情況下輸入運算符會忽略空白,制表,換行,換紙,回車符号
char ch;
while (cin >> ch) {
cout << ch;
}
// 輸入
a b c
d
// 輸出
abcd
// 循環一共執行了四次
// 操縱符noskipws會零輸入運算讀物空白符,而不是跳過
cin >> noskipws;
while (cin >> ch)
cout << ch;
cin >> skipws;//恢複預設的狀态
// 此循環會執行7次。将普通字元和空白字元均讀取
// 輸出
a b c
d
}
//2.未格式化的輸入和輸出操作
1).到目前為止我們隻使用過格式化IO操作。
- 輸入運算符忽略空白符(包含空格符…)
- 輸出運算符應用補白,精度規則。
- 輸入和輸出運算符根據讀取的資料類型來格式化它們。
2).标準庫還提供了一組底層操作,支援未格式化IO。這些操作允許我們将一個流當作一個無解釋的位元組序列來處理。
3).單位元組操作
- 有幾個未格式化操作每次一個位元組的處理流。見表格17.19(p673)。它們會處理而不是忽略空白符。
{
char ch;
while (cin.get(ch))
cout,put(ch);
// 該程式保留輸入中的空白,輸出和輸入完全一緻,它的執行過程和前一個使用noskipws完全相同
}
4).将字元放回輸入流
- 有時候,我們需要讀取一個字元才知道還沒有準備好處理它。此時我們希望可以退回到流中。标準庫提供了三種退回字元的方式。有細微的差别
-
傳回輸入流中下一個字元的副本,但是不會将他從流中删除。peek
-
使得輸入流向後移動,進而最後讀取的值又回到流中。即使我們不知道最後從流中讀取什麼值,仍然可以調用unget
unget
-
,是更加特殊的putback
,它退回流中讀取的最後一個值,但它必須接受一個參數,此參數必須和最後讀取的值相同。unget
- 一般情況下,在讀取下一個值之前,标準庫保證我們可以退回最多一個值,即,标準庫 不保證 中間 不進行 讀取操作的情況下, 能 連續調用
或者putback
。unget
5).從輸入操作傳回的int值
- 函數
和無參數的peek
版本都以get
從輸入流傳回一個字元。int
- 原因,可以傳回檔案尾标記,我們使用
範圍中的每一個值來表示一個真實字元,是以,取值範圍中沒有額外的值可以标志檔案尾。char
- 傳回int的函數将它們要傳回的字元先轉換為
,然後再将結果提升到unsigned char
。是以,即使字元集中有字元映射到負值,這些操作傳回的int也是正值。而标準庫使用負值表示檔案尾,這樣就可以保證與任何合法字元的值都不一樣。頭檔案int
定義了一個名為cstdio
的EOF
,我們可以使用它來确定傳回的是否是檔案尾,而不必記憶檔案尾的實際數值。const
{
int ch;//使用一個int而不是char來儲存get()的傳回值
while ((ch = cin.get() != EOF))
cout.put(ch);
// 接受參數的版本,可以自動的檢測到EOF
// 使用while檢測流的狀态
}
6).多位元組操作
- 一次處理大的資料塊。速度快,但是類似于其他底層操作,這些操作容易出錯。這些操作要求我們自己配置設定并管理儲存和提取資料的字元數組。表17.20,(p674)列出了這些操作。
-
和get
函數接受相同的參數,它們的行為類似但是不相同。getline
都是一個sink
數組,用來儲存資料,兩個函數都一直讀取資料,知道以下條件之一發生。char
- 數組已經滿了
- 遇到檔案的末尾
- 遇到分隔符(由我們指定)
- 兩個函數的差别在于對于分隔符的處理,
-
将分割符留作get
的下一個字元istream
-
則将其讀取并且丢棄。getline
- 無論哪一個函數都不會将分隔符儲存再
中。sink
- 一個常見的錯誤是本想着從流中删除分隔符,但是沒有做。
7).确定讀入了多少個字元
- 調用
來确定最後一個未格式輸入操作讀取了多少個字元。gcount
- 應該再任何後續未格式化輸入操作之前調用
。(明确讀取的字元數目)gcount
- 将字元退回流的單字元操作也是屬于未格式化操作。如果在調用
之前調用了gcount
,則peek,unget,getback
傳回值為0。gcount
8).底層函數容易出錯。
- 例如,将
的傳回值賦予一個get,peek
而不是char
。這樣做是錯誤的,但是編譯器不會發現這個錯誤。具體發生什麼錯誤,取決于機器和輸入資料。int
- 在一個
被實作為char
的機器上,unsigned
永遠都不會停止循環。while ((ch = cin.get() != EOF))
- 在一個視
為char
上,會發生什麼?有的不能确定循環的行為;有的遇到變量的值越界了,依賴編譯器如何處理這種越界;有的恰好可以正常工作,除非遇到和signed char
相等的情況,雖然在普通資料中共這種字元不太可能,但是底層的IO通常用于讀取二進制值,二進制值不能直接映射到普通字元和數值。…EOF
- 總之,讀寫有類型的值,格式化的數值,這樣的錯誤就不會發生。可以使用标準庫更加安全,更加高層的操作。就使用标準庫的操作。
練習
- 了解未格式化版本的
,暫時略過。getline
//3.流随機通路操作
1).各種流類型通常都支援對流中資料的随機通路。**我們可以重定位流,使之跳過一些資料,首先讀取最後一行,然後讀取第一行,以此類推。**标準庫提供了一對函數(成員函數。),
-
定位到流中的給定位置seek
-
傳回我們目前的位置。tell
- 随機IO本質上是依賴于系統的,為了了解如何使用這些特性,必須查詢系統文檔。
2).雖然标準庫為所有的流類型都定義了
seek
和
tell
函數,但他們是否會做有意義的事情,依賴于流綁定的裝置。綁定到
cin,cout,cerr,clog
的流裝置不支援随機通路——當我們
cout
直接輸出資料時,類似向回跳十個位置這樣的操作是沒有意義的…對這些流我們可以調用
seek,tell
函數,但是在運作時會出錯,将流置為無效狀态。
- 由于
類型通常不支援随機通路,以下内容隻是用于istream,ostream
類型。fstream,和sstream
3).
seek,tell
函數,詳見表格17.21(p676)
- 為了支援随機通路,IO類型維護一個标記來确定下一個讀寫操作要在哪裡進行。還提供了以上兩個函數。實際上,标準庫定義類兩對的
分别用于輸入流,輸出流。差别在于字尾一個是g(get)讀取,輸入;一個是p(put)寫入,輸出。seek,tell
- 對于iostream,fstream,stringstream既可以讀又可以寫的關聯的流,可以對這些類型使用g和p。
4).标準庫雖然區分讀寫的版本,但是在流中隻有單一的标記–不存在獨立的讀标記和寫标記。
- 當我們處理一個隻讀或者隻寫的流時,這種特性沒有展現出來;因為我們試圖對輸出流使用g,或者對輸入流使用p将會報錯。
- 而對于
。它們有單一的緩沖區,用來儲存讀寫的資料,标記隻有一個,表示緩沖區目前的位置。标準庫将g和p版本的讀寫都映射到這個單一的标記。由于隻有單一的标記,我們要在讀寫操作間進行切換時,就必須使用fstream,stringstream
操作來重定位标記。seek
5).重定位标記
{
// 将标記移動到一個固定的位置
seekg(new_position);//将讀标記移動到一個指定的pos_type類型的位置上
seekp(new_position);//将寫标記移動到指定的pos_type位置上
// 移動到給的那個起止點之前或之後指定的偏移位置
seekg(offset, from);//将讀标記移動到距from偏移量為offset的位置
seekp(offset, from);
// from可能值見表17.21(p676)
// new_positon類型為pos_type
// offset類型為off_type
// 以上兩個類型都是機器相關的
// 定義在頭檔案istream和ostream中
// pos_type表示一個檔案位置
// off_set的值可以是整的也可以為負
// 通過正負确定是向前還是向後移動
// -----------通路标記
// tell函數傳回一個類型為pos_type的值,表示目前的流的位置
// tell函數通常是為了記住一個位置,以便稍後回來改位置
ostringstream writeStr;
ostringstream::pos_type mark = writeStr.tellp();
// ...
if (cancelEntry) {
// 回到剛才的位置
writeStr.seekp(mark);
}
// ---------讀寫同一個檔案
// 給定一個檔案,在檔案末尾寫入新的一行,包含檔案中每一行的相對起始位置。
abcd
efg
hi
j
5 9 12 14
// 注意偏移量包括行尾的不可見的換行符
int main() {
// 以讀寫的方式打開檔案并且定位到檔案尾
fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out);
if (!inOut) {
cerr << "..." << endl;
return EXIT_FALLURE;//這是一個預處理變量,定義在頭檔案cstdlib中
}
// inOut以ate的方式打開,是以一開始就是定位在檔案的末尾
auto end_mark = inOut.tellg(); //記住 讀取 的末尾位置
inOut.seekg(0, fstream::beg);//重定位到檔案開始的位置
size_t cnt = 0; //位元組數計數器
string line;//儲存每一行
// 如果讀取沒有失敗
// 并且還沒有 讀取 到達檔案end_mark
//
while (inOut && inOut.tellg() != end_mark
&& getline(inOut, line)) {
cnt += line.size() + 1;//加1表示換行符
auto mark = inOut.tellg();//記住目前 讀取 的位置
intOut.seekp(0, fstream::end); //将 寫 的标記移動到檔案的末尾
inOut << cnt ;//寫入資料
if (mark != end_mark) inOut << " ";//如果不是最後一行,列印一個分割符
inOut.seekg(mark);//恢複到上一次 讀 的位置
}
inOut.seekp(0, fstream::end);//定位到檔案的末尾
inOut << '\n';//寫入換行符号。
return 0;
}
// 絕對位置是沒有from的三個特殊值得
// 使用重載的版本得到檔案的起始和結束位置
}
/6.小結
1).
match
對象是
sub_match
對象的容器(反映的是整個正規表達式(0号元素)/子表達式的比對結果。),而且它還有成員
prefix(),suffix()
傳回比對之前和之後的資訊。也是一個
sub_match
對象。