《C++ Primer 5th》笔记(3 / 19):字符串、向量、迭代器和数组
文章目錄
- 命名空間的using聲明
- 標(biāo)準(zhǔn)庫類型string
- 定義和初始化string對象
- 直接初始化和拷貝初始化
- string對象上的操作
- 讀寫string對象
- 讀取未知數(shù)量的string對象
- 使用getline讀取一整行
- string的empty和size操作
- string::size_type類型
- 比較string對象
- 為string對象賦值
- 兩個string對象相加
- 字面值和string對象相加
- 處理string對象中的字符
- 建議:使用C++版本的C標(biāo)準(zhǔn)庫頭文件
- 處理每個字符,就使用基于范圍的for語句
- 使用范圍for語句改變字符串中的字符
- 只處理一部分字符
- 使用下標(biāo)執(zhí)行迭代
- 提示:注意檢查下標(biāo)的合法性
- 使用下標(biāo)執(zhí)行隨機(jī)訪問
- 標(biāo)準(zhǔn)庫類型vector
- 定義和初始化vector對象
- 列表初始化vector對象
- 創(chuàng)建指定數(shù)量的元素
- 值初始化
- 列表初始值還是元素數(shù)量
- 向vector對象中添加元素
- 關(guān)鍵概念:vector對象能高效增長
- 向vector對象添加元素蘊(yùn)含的編程假定
- 其他vector操作
- 計算vector內(nèi)對象的索引
- 不能用下標(biāo)形式添加元素
- 提示:只能對確知已存在的元素執(zhí)行下標(biāo)操作!
- 迭代器介紹
- 使用迭代器
- 迭代器運(yùn)算符
- 將迭代器從一個元素移動到另外一個元素
- 關(guān)鍵概念:泛型編程
- 迭代器類型
- 術(shù)語:迭代器和迭代器類型
- begin和end運(yùn)算符
- 結(jié)合解引用和成員訪問操作
- 某些對vector對象的操作會使迭代器失效
- 迭代器運(yùn)算
- 迭代器的算術(shù)運(yùn)算
- 使用迭代器運(yùn)算
- 數(shù)組
- 定義和初始化內(nèi)置數(shù)組
- 顯式初始化數(shù)組元素
- 字符數(shù)組的特殊性
- 不允許拷貝和賦值
- 理解復(fù)雜的數(shù)組聲明
- 訪問數(shù)組元素
- 檢查下標(biāo)的值
- 指針和數(shù)組
- 指針也是迭代器
- 標(biāo)準(zhǔn)庫函數(shù)begin和end
- 指針運(yùn)算
- 解引用和指針運(yùn)算的交互
- 下標(biāo)和指針
- C風(fēng)格字符串
- C標(biāo)準(zhǔn)庫String函數(shù)
- 比較字符串
- 目標(biāo)字符串的大小由調(diào)用者指定
- 與舊代碼的接口
- 混用string對象和C風(fēng)格字符串
- 使用數(shù)組初始化vector對象
- 建議:盡量使用標(biāo)準(zhǔn)庫類型而非數(shù)組
- 多維數(shù)組
- 多維數(shù)組的初始化
- 多維數(shù)組的下標(biāo)引用
- 使用范圍for語句處理多維數(shù)組
- 指針和多維數(shù)組
- 類別別名簡化多維數(shù)組的指針
- 一些術(shù)語
命名空間的using聲明
目前為止,我們用到的庫函數(shù)基本上都屬于命名空間std,而程序也顯式地將這一點(diǎn)標(biāo)示了出來。
例如,std::cin表示從標(biāo)準(zhǔn)輸入中讀取內(nèi)容。此處使用作用域操作符(::)的含義是:編譯器應(yīng)從操作符左側(cè)名字所示的作用域中尋找右側(cè)那個名字。因此,std::cin 的意思就是要使用命名空間std中的名字cin。
上面的方法顯得比較煩瑣,然而幸運(yùn)的是,通過更簡單的途徑也能使用到命名空間中的成員,也就是使用using聲明(using declaration),還會有其他途徑,敬請期待。
有了using聲明就無須專門的前綴(形如命名空間::)也能使用所需的名字了。using聲明具有如下的形式:
using namespace::name;一旦聲明了上述語句,就可以直接訪問命名空間中的名字:
#include <iostream> // using declaration; when we use the name cin, we get the one from the namespace std using std::cin; int main() {int i;cin >> i; // ok: cin is a synonym for std::cincout << i; // error: no using declaration; we must use the full namestd::cout << i; // ok: explicitly use cout from namepsace stdreturn 0; }每個名字都需要獨(dú)立的using聲明
#include <iostream> // using declarations for names from the standard library using std::cin; using std::cout; using std::endl;int main() {cout << "Enter two numbers:" << endl;int v1, v2;cin >> v1 >> v2;cout << "The sum of " << v1 << " and " << v2<< " is " << v1 + v2 << endl;return 0; }頭文件不應(yīng)包含using 聲明
位于頭文件的代碼一般來說不應(yīng)該使用using聲明。這是因?yàn)轭^文件的內(nèi)容會拷貝到所有引用它的文件中去,如果頭文件里有某個using聲明,那么每個使用了該頭文件的文件就都會有這個聲明。對于某些程序來說,由于不經(jīng)意間包含了一些名字,反而可能產(chǎn)生始料未及的名字沖突。
一點(diǎn)注意事項
之后的所有例子將假設(shè),但凡用到的標(biāo)準(zhǔn)庫中的名字都已經(jīng)使用using語句聲明過了。例如,我們將在代碼中直接使用cin,而不再使用std::cin。
為了讓書中的代碼盡量簡潔,今后將不會再把所有using聲明和#include指令一一標(biāo)出。
標(biāo)準(zhǔn)庫類型string
標(biāo)準(zhǔn)庫類型string表示可變長的字符序列,使用string類型必須首先包含string頭文件。作為標(biāo)準(zhǔn)庫的一部分,string定義在命名空間std中。接下來的示例都假定已包含了下述代碼:
#include <string> using std::string;本節(jié)描述最常用的string操作,將來還將介紹另外一些。
C++標(biāo)準(zhǔn)一方面對庫類型所提供的操作做了詳細(xì)規(guī)定,另一方面也對庫的實(shí)現(xiàn)者做出一些性能上的需求。因此,標(biāo)準(zhǔn)庫類型對于一般應(yīng)用場合來說有足夠的效率。(也就是說,你放心用標(biāo)準(zhǔn)庫類型吧!)
定義和初始化string對象
副本的本義是指同一書抄出的復(fù)本。link
string s1; // default initialization; s1 is the empty string string s2 = s1; // s2 is a copy of s1 string s3 = "hiya"; // s3 is a copy of the string literal string s4(10, 'c'); // s4 is cccccccccc初始化string對象的方式
| string s1 | 默認(rèn)初始化,s1是一個空串 |
| string s2(s1) | s2是s1的副本 |
| string s2 = s1 | 等價于s2(s1),s2是s1的副本 |
| string s3(“value”) | s3是字面值"value"的副本,除了字面值最后的那個空字符外 |
| string s3 = “value” | 等價于s3(“value”),s3是字面值"value"的副本 |
| string s4(n, ‘c’) | 把s4初始化為由連續(xù)n個字符c組成的串 |
直接初始化和拷貝初始化
C++語言有幾種不同的初始化方式,通過string我們可以清楚地看到在這些初始化方式之間到底有什么區(qū)別和聯(lián)系。
- 如果使用等號(=)初始化一個變量,實(shí)際上執(zhí)行的是拷貝初始化(copy initialization),編譯器把等號右側(cè)的初始值拷貝到新創(chuàng)建的對象中去。
- 與之相反,如果不使用等號,則執(zhí)行的是直接初始化(direct initialization)。
當(dāng)初始值只有一個時,使用直接初始化或拷貝初始化都行:
string s5 = "hiya"; // copy initialization string s6("hiya"); // direct initialization string s7(10, 'c'); // direct initialization; s7 is cccccccccc對于用多個值進(jìn)行初始化的情況,非要用拷貝初始化的方式來處理也不是不可以,不過需要顯式地創(chuàng)建一個(臨時)對象用于拷貝:
string s8 = string(10, 'c');//拷貝初始化,s8的內(nèi)容是cccccccccc相當(dāng)于
string temp(10, 'c'); // temp is cccccccccc string s8 = temp; // copy temp into s8其實(shí)我們可以看到,盡管初始化s8的語句合法,但和初始化s7的方式比較起來可讀性較差,也沒有任何補(bǔ)償優(yōu)勢。(沒什么卵用)
string對象上的操作
| os<<s | 將s寫到輸出流os當(dāng)中,返回os |
| is>>s | 從is中讀取字符串賦給s,字符串以空白分隔,返回is |
| getline(is, s) | 從is中讀取一行賦給s,返回is |
| s.empty() | s為空返回true,否則返回false |
| s.size() | 返回s中字符的個數(shù) |
| s[n] | 返回s中第n個字符的引用,位置n從0計起 |
| s1+s2 | 返回s1和s2連接后的結(jié)果 |
| s1=s2 | 用s2的副本代替s1中原來的字符 |
| s1==s2 | 如果s1和s2中所含的字符完全一樣,則它們相等;string對象的相等性判斷對字母的大小寫敏感 |
| s1!=s2 | 判斷s1和s2中所含的字符是否完全一樣 |
| <, <=, >, >= | 利用字符在字典中的順序進(jìn)行比較,且對字母的大小寫敏感 |
讀寫string對象
// Note: #include and using declarations must be added to compile this code int main() {string s; // empty stringcin >> s; // read a whitespace-separated string into scout << s << endl; // write s to the outputreturn 0; }string對象會自動忽略開頭的空白(即空格符、換行符、制表符等)并從第一個真正的字符開始讀起,直到遇見下一處空白為止。
如上所述,如果程序的輸入是“ Hello World! ”(注意開頭和結(jié)尾處的空格),則輸出將是“Hello”,輸出結(jié)果中沒有任何空格。
和內(nèi)置類型的輸入輸出操作一樣,string對象的此類操作也是返回運(yùn)算符左側(cè)的運(yùn)算對象作為其結(jié)果。因此,多個輸入或者多個輸出可以連寫在一起:
string s1, s2; cin >> s1 >> s2; // read first input into s1, second into s2 cout << s1 << s2 << endl; // write both strings假設(shè)給上面這段程序輸入與之前一樣的內(nèi)容“ Hello World! ”,輸出將是“HelloWorld!”。
讀取未知數(shù)量的string對象
int main() {string word;while (cin >> word) // read until end-of-file (Ctrl + Z)cout << word << endl; // write each word followed by a new linereturn 0; }使用getline讀取一整行
有時我們希望能在最終得到的字符串中保留輸入時的空白符,這時應(yīng)該用getline函數(shù)代替原來的>>運(yùn)算符。getline函數(shù)的參數(shù)是一個輸入流和一個string對象,函數(shù)從給定的輸入流中讀入內(nèi)容,直到遇到換行符為止(注意換行符也被讀進(jìn)來了),然后把所讀的內(nèi)容存入到那個string對象中去(注意不存換行符)。
getline只要一遇到換行符就結(jié)束讀取操作并返回流參數(shù),哪怕輸入的一開始就是換行符也是如此。如果輸入真的一開始就是換行符,那么所得的結(jié)果是個空string。
int main() {string line;// read input a line at a time until end-of-filewhile (getline(cin, line))cout << line << endl;return 0; }因?yàn)閘ine中不包含換行符,所以我們手動地加上換行操作符。和往常一樣,使用endl結(jié)束當(dāng)前行并刷新顯示緩沖區(qū)。
觸發(fā)getline函數(shù)返回的那個換行符實(shí)際上被丟棄掉了,得到的string對象中并不包含該換行符。
string的empty和size操作
empty函數(shù)根據(jù)string對象是否為空返回一個對應(yīng)的布爾值。
// read input a line at a time and discard blank lines while (getline(cin, line))if (!line.empty())cout << line << endl;size函數(shù)返回string對象的長度(即string對象中字符的個數(shù)),可以使用size函數(shù)只輸出長度超過80個字符的行:
string line; // read input a line at a time and print lines that are longer than 80 characters while (getline(cin, line))if (line.size() > 80)cout << line << endl;string::size_type類型
對于size函數(shù)來說,返回一個int或者一個unsigned似乎都是合情合理的。但其實(shí) size函數(shù)返回的是一個string::size_type類型的值。
string類及其他大多數(shù)標(biāo)準(zhǔn)庫類型都定義了幾種配套的類型。這些配套類型體現(xiàn)了標(biāo)準(zhǔn)庫類型與機(jī)器無關(guān)的特性,類型size_type即是其中的一種。在具體使用的時候,通過作用域操作符來表明名字size_type是在類string中定義的。
盡管我們不太清楚string::size_type類型的細(xì)節(jié),但有一點(diǎn)是肯定的:它是一個無符號類型的值而且能足夠存放下任何string對象的大小。所有用于存放string類的size函數(shù)返回值的變量,都應(yīng)該是string::size_type類型的。
過去,string::size_type這種類型有點(diǎn)兒神秘,不太容易理解和使用。在C++11新標(biāo)準(zhǔn)中,允許編譯器通過auto或者decltype來推斷變量的類型:
auto len = line.size (); //len的類型是string::size_type由于size函數(shù)返回的是一個無符號整型數(shù),因此切記,如果在表達(dá)式中混用了帶符號數(shù)和無符號數(shù)將可能產(chǎn)生意想不到的結(jié)果。例如,假設(shè)n 是一個具有負(fù)值的int,則表達(dá)式s.size()<n的判斷結(jié)果幾乎肯定是true。
這是因?yàn)?strong>負(fù)值n會自動地轉(zhuǎn)換成一個比較大的無符號值。
如果一條表達(dá)式中已經(jīng)有了size()函數(shù)就不要再使用int了,這樣可以避免混用int和unsigned可能帶來的問題。
比較string對象
string類定義了幾種用于比較字符串的運(yùn)算符。這些比較運(yùn)算符逐一比較string對象中的字符,并且對大小寫敏感,也就是說,在比較時同一個字母的大寫形式和小寫形式是不同的。
相等性運(yùn)算符(==和!=)分別檢驗(yàn)兩個string對象相等或不相等,string對象相等意味著它們的長度相同而且所包含的字符也全都相同。關(guān)系運(yùn)算符<、<=、>、>=分別檢驗(yàn)一個string對象是否小于、小于等于、大于、大于等于另外一個string對象。上述這些運(yùn)算符都依照(大小寫敏感的)字典順序:
根據(jù)規(guī)則1可判斷,對象s1小于對象s2;根據(jù)規(guī)則2可判斷,對象s3既大于s1也大于s2。
為string對象賦值
一般來說,在設(shè)計標(biāo)準(zhǔn)庫類型時都力求在易用性上向內(nèi)置類型看齊,因此大多數(shù)庫類型都支持賦值操作。對于string類而言,允許把一個對象的值賦給另外一個對象:
string st1(10, 'c'), st2; // st1 is cccccccccc; st2 is an empty string st1 = st2; // assignment: replace contents of st1 with a copy of st2 // both st1 and st2 are now the empty string兩個string對象相加
兩個string對象相加得到一個新的string對象,其內(nèi)容是把左側(cè)的運(yùn)算對象與右側(cè)的運(yùn)算對象串接而成。
也就是說,對string對象使用加法運(yùn)算符(+)的結(jié)果是一個新的string對象,它所包含的字符由兩部分組成:前半部分是加號左側(cè)string對象所含的字符、后半部分是加號右側(cè)string對象所含的字符。
另外,復(fù)合賦值運(yùn)算符(+=)負(fù)責(zé)把右側(cè)string對象的內(nèi)容追加到左側(cè)string對象的后面:
string s1 = "hello, ", s2 = "world\n"; string s3 = s1 + s2; // s3 is hello, world\n s1 += s2; // equivalent to s1 = s1 + s2字面值和string對象相加
上一章節(jié)說過所講的,即使一種類型并非所需,我們也可以使用它,不過前提是該種類型可以自動轉(zhuǎn)換成所需的類型。因?yàn)闃?biāo)準(zhǔn)庫允許把字符字面值和字符串字面值轉(zhuǎn)換成string對象,所以在需要string對象的地方就可以使用這兩種字面值來替代。利用這一點(diǎn)將之前的程序改寫為如下形式:
string s1 = "hello", s2 = "world"; // no punctuation in s1 or s2 string s3 = s1 + ", " + s2 + '\n';當(dāng)把string對象和字符字面值及字符串字面值混在一條語句中使用時,必須確保每個加法運(yùn)算符(+)的兩側(cè)的運(yùn)算對象至少有一個是string:
string s4 = s1 + ", "; // ok: adding a string and a literal string s5 = "hello" + ", "; // error: no string operand string s6 = s1 + ", " + "world"; // ok: each + has a string operand//字符串字面值不能直接相加 string s7 = "hello" + ", " + s2; // error: can't add string literalss4和s5初始化時只用到了一個加法運(yùn)算符,因此很容易判斷是否合法。s6的初始化形式之前沒有出現(xiàn)過,但其實(shí)它的工作機(jī)理和連續(xù)輸入連續(xù)輸出是一樣的,可以用如下的形式分組:
string s6 = (s1 + ", ") + "world";其中子表達(dá)式sl +", "的結(jié)果是一個string對象,它同時作為第二個加法運(yùn)算符的左側(cè)運(yùn)算對象,因此上述語句和下面的兩個語句是等價的:
string tmp = s1 + ", "; // ok: + has a string operand s6 = tmp + "world"; // ok: + has a string operand另一方面,s7的初始化是非法的,根據(jù)其語義加上括號后就成了下面的形式:
string s7 = ("hello" + ", ") + s2; // error: can't add string literals很容易看到,括號內(nèi)的子表達(dá)式試圖把兩個字符串字面值加在一起,而編譯器根本沒法做到這一點(diǎn),所以這條語句是錯誤的。
因?yàn)槟承v史原因,也為了與C兼容,所以C++語言中的字符串字面值并不是標(biāo)準(zhǔn)庫類型string的對象。切記,C++中字符串字面值與string是不同的類型。
處理string對象中的字符
cctype頭文件中的函數(shù)
| isalnum? | 當(dāng)c是字母或數(shù)字時為真 |
| isalpha? | 當(dāng)c是字母時為真 |
| iscntrl? | 當(dāng)c是控制字符時為真 |
| isdigit? | 當(dāng)c是數(shù)字時為真 |
| isgraph? | 當(dāng)c不是空格但可打印時為真 |
| islower? | 當(dāng)c是小寫字母時為真 |
| isprint? | 當(dāng)c是可打印字符時為真(即c是空格或c具有可視形式) |
| ispunct? | 當(dāng)c是標(biāo)點(diǎn)符號時為真(即c不是控制字符、數(shù)字、字母、可打印空白中的一種) |
| isspace? | 當(dāng)c是空白時為真(即c是空格、橫向制表符、縱向制表符、回車符、換行符、進(jìn)紙符中的一種) |
| isupper? | 當(dāng)c是大寫字母時為真 |
| isxdigit? | 當(dāng)c是十六進(jìn)制數(shù)字時為真 |
| tolower? | 如果c是大寫字母,輸出對應(yīng)的小寫字母;否則原樣輸出c |
| toupper? | 如果c是小寫字母,輸出對應(yīng)的大寫字母;否則原樣輸出c |
建議:使用C++版本的C標(biāo)準(zhǔn)庫頭文件
C++標(biāo)準(zhǔn)庫中除了定義C++語言特有的功能外,也兼容了C語言的標(biāo)準(zhǔn)庫。C語言的頭文件形如 name.h,C++則將這些文件命名為cname。也就是去掉了.h后綴,而在文件名name之前添加了字母c,這里的c表示這是一個屬于C語言標(biāo)準(zhǔn)庫的頭文件。
因此,cctype頭文件和 ctype.h頭文件的內(nèi)容是一樣的,只不過從命名規(guī)范上來講更符合C++語言的要求。特別的,在名為cname的頭文件中定義的名字從屬于命名空間std,而定義在名為.h的頭文件中的則不然。
一般來說,C++程序應(yīng)該使用名為cname的頭文件而不使用name.h的形式,標(biāo)準(zhǔn)庫中的名字總能在命名空間std中找到。如果使用.h形式的頭文件,程序員就不得不時刻牢記哪些是從C語言那兒繼承過來的,哪些又是C++語言所獨(dú)有的。
處理每個字符,就使用基于范圍的for語句
C++11新標(biāo)準(zhǔn):
范圍for (range for)語句。這種語句遍歷給定序列中的每個元素并對序列中的每個值執(zhí)行某種操作,其語法形式是:
for (declaration : expression)statement其中,expression部分是一個對象,用于表示一個序列。declaration部分負(fù)責(zé)定義一個變量,該變量將被用于訪問序列中的基礎(chǔ)元素。每次迭代,declaration部分的變量會被初始化為expression部分的下一個元素值。
(Note:與Java的forEach循環(huán)類似)
一個string對象表示一個字符的序列,因此 string對象可以作為范圍for語句中的expression部分。
舉一個簡單的例子,我們可以使用范圍for語句把string對象中的字符每行一個輸出出來:
string str("some string"); // print the characters in str one character to a line for (auto c : str) // for every char in strcout << c << endl; // print the current character followed by a newline再舉一點(diǎn)的例子,使用范圍for語句和 ispunct函數(shù)來統(tǒng)計string對象中標(biāo)點(diǎn)符號的個數(shù):
string s("Hello World!!!"); // punct_cnt has the same type that s.size returns;//decltype具體查看上一章 decltype(s.size()) punct_cnt = 0; // count the number of punctuation characters in s for (auto c : s) // for every char in sif (ispunct(c)) // if the character is punctuation++punct_cnt; // increment the punctuation counter cout << punct_cnt<< " punctuation characters in " << s << endl;輸出結(jié)果:
3 punctuation characters in Hello wor1d!!!使用范圍for語句改變字符串中的字符
如果想要改變string對象中字符的值,必須把循環(huán)變量定義成引用類型。
記住,所謂引用只是給定對象的一個別名,因此當(dāng)使用引用作為循環(huán)控制變量時,這個變量實(shí)際上被依次綁定到了序列的每個元素上。使用這個引用,我們就能改變它綁定的字符。
舉一個簡單的例子,假設(shè)我們想要把字符串改寫為大寫字母的形式。
string s("Hello World!!!"); // convert s to uppercase for (auto &c : s) // for every char in s (note: c is a reference)c = toupper(c); // c is a reference, so the assignment changes the char in s cout << s << endl;輸出結(jié)果:
HELLO WORLD!!!只處理一部分字符
如果要處理string對象中的每一個字符,使用范圍for語句是個好主意。然而,有時我們需要訪問的只是其中一個字符,或者訪問多個字符但遇到某個條件就要停下來。例如,同樣是將字符改為大寫形式,不過新的要求不再是對整個字符串都這樣做,而僅僅把string對象中的第一個字母或第一個單詞大寫化。
要想訪問string對象中的單個字符有兩種方式:
先介紹下標(biāo)運(yùn)算符(The subscript operator) ([])接收的輸入?yún)?shù)是string::size_type類型的值,這個參數(shù)表示要訪問的字符的位置;返回值是該位置上字符的引用。
string對象的下標(biāo)從0計起。如果string對象s至少包含兩個字符,則s[0]是第1個字符、s[1]是第2個字符、s[s.size()-1]是最后一個字符。
string對象的下標(biāo)必須大于等于0而小于s.size()。
使用超出此范圍的下標(biāo)將引發(fā)不可預(yù)知的結(jié)果,以此推斷,使用下標(biāo)訪問空string也會引發(fā)不可預(yù)知的結(jié)果。
下標(biāo)的值稱作“下標(biāo)(a subscript)”或“索引(an index )”,任何表達(dá)式只要它的值是一個整型值就能作為索引。不過,如果某個索引是帶符號類型的值將自動轉(zhuǎn)換成由string::size_type表達(dá)的無符號類型。
舉一個例子,使用下標(biāo)運(yùn)算符輸出string對象中的第一個字符:
if (!s.empty()) // make sure there's a character to printcout << s[0] << endl; // print the first character in s再舉一個例子,將字符串首字母變?yōu)榇髮?#xff1a;(只要字符串不是常量,就能為下標(biāo)運(yùn)算符返回的字符賦新值。)
string s("some string"); if (!s.empty()) // make sure there's a character in s[0]s[0] = toupper(s[0]); // assign a new value to the first character in s輸出s:
Some string使用下標(biāo)執(zhí)行迭代
另一個例子是把s的第一個詞改成大寫形式:
// process characters in s until we run out of characters or we hit a whitespace for (decltype(s.size()) index = 0;index != s.size() && !isspace(s[index]); ++index)s[index] = toupper(s[index]); // capitalize the current character輸出s:
SOME string提示:注意檢查下標(biāo)的合法性
使用下標(biāo)時必須確保其在合理范圍之內(nèi),也就是說,下標(biāo)必須大于等于0而小于字符串的size()的值。一種簡便易行的方法是,總是設(shè)下標(biāo)的類型為string::size_type,因?yàn)榇祟愋褪菬o符號數(shù),可以確保下標(biāo)不會小于0。此時,代碼只需保證下標(biāo)小于size()的值就可以了。
C++標(biāo)準(zhǔn)并不要求標(biāo)準(zhǔn)庫檢測下標(biāo)是否合法。一旦使用了一個超出范圍的下標(biāo),就會產(chǎn)生不可預(yù)知的結(jié)果。
使用下標(biāo)執(zhí)行隨機(jī)訪問
在之前的示例中,我們讓字符串的下標(biāo)每次加1從而按順序把所有字符改寫成了大寫形式。其實(shí)也能通過計算得到某個下標(biāo)值,然后直接獲取對應(yīng)位置的字符,并不是每次都得從前往后依次訪問。
例如,想要編寫一個程序把0到15之間的十進(jìn)制數(shù)轉(zhuǎn)換成對應(yīng)的十六進(jìn)制形式,只需初始化一個字符串令其存放16個十六進(jìn)制“數(shù)字”:
const string hexdigits = "0123456789ABCDEF"; // possible hex digits cout << "Enter a series of numbers between 0 and 15"<< " separated by spaces. Hit ENTER when finished: "<< endl; string result; // will hold the resulting hexify'd string string::size_type n; // hold numbers from the input while (cin >> n)if (n < hexdigits.size()) // ignore invalid inputresult += hexdigits[n]; // fetch the indicated hex digit cout << "Your hex number is: " << result << endl;假設(shè)輸入的內(nèi)容如下:
12 0 5 15 8 15程序的輸出結(jié)果將是:
Your hex number is: C05F8F標(biāo)準(zhǔn)庫類型vector
vector
英 [?vekt??] 美 [?vekt?r]
n. 矢量; 向量; (傳染疾病的)介體,載體; (航空器的)航線
標(biāo)準(zhǔn)庫類型vector表示對象的集合,其中所有對象的類型都相同。集合中的每個對象都有一個與之對應(yīng)的索引,索引用于訪問對象。因?yàn)関ector“容納著”其他對象,所以它也常被稱作容器(container)。
要想使用vector,必須包含適當(dāng)?shù)念^文件。在后續(xù)的例子中,都將假定做了如下using聲明:
#include <vector> using std::vector;C++語言既有類模板(class template),也有函數(shù)模板,其中vector是一個類模板。只有對C++有了相當(dāng)深入的理解才能寫出模板,事實(shí)上,我們直到最后階段的章節(jié)才會學(xué)習(xí)如何自定義模板。幸運(yùn)的是,即使還不會創(chuàng)建模板,我們也可以先試著用用它。
模板本身不是類或函數(shù),相反可以將模板看作為編譯器生成類或函數(shù)編寫的指令。(Templates are not themselves functions or classes. Instead, they can be thought of as instructions to the compiler for generating classes or functions. )編譯器根據(jù)模板創(chuàng)建類或函數(shù)的過程稱為實(shí)例化( instantiation),當(dāng)使用模板時,需要指出編譯器應(yīng)把類或函數(shù)實(shí)例化成何種類型。
對于類模板來說,我們通過提供一些額外信息來指定模板到底實(shí)例化成什么樣的類,需要提供哪些信息由模板決定。提供信息的方式總是這樣:即在模板名字后面跟一對尖括號,在括號內(nèi)放上信息。
以vector為例,提供的額外信息是vector內(nèi)所存放對象的類型:
vector<int> ivec; // ivec holds objects of type int vector<Sales_item> Sales_vec; // holds Sales_items vector<vector<string>> file; // vector whose elements are vectors在上面的例子中,編譯器根據(jù)模板vector生成了三種不同的類型:
- vector<int>
- vector<Sales_item>
- vector<vector<string>>
vector是模板而非類型,由vector生成的類型必須包含vector中元素的類型,例如vector<int>。
vector能容納絕大多數(shù)類型的對象作為其元素,但是因?yàn)橐貌皇菍ο?#xff0c;所以不存在包含引用的vector。除此之外,其他大多數(shù)(非引用)內(nèi)置類型和類類型都可以構(gòu)成vector對象,甚至組成vector的元素也可以是vector。
(與Java中的List類似)
關(guān)于舊版本的vector
需要指出的是,在早期版本的C++標(biāo)準(zhǔn)中如果vector的元素還是vector(或者其他模板類型),則其定義的形式與現(xiàn)在的C++11新標(biāo)準(zhǔn)略有不同。過去,必須在外層vector對象的右尖括號和其元素類型之間添加一個空格,如應(yīng)該寫成vector<vector<int> >而非vector<vector<int>>。
某些編譯器可能仍需以老式的聲明語句來處理元素為 vector的vector對象,如 vector<vector<int> >。
定義和初始化vector對象
初始化vector對象的方法:
| vector<T> v1 | v1是一個空vector,它潛在的元素是T類型的,執(zhí)行默認(rèn)初始化 |
| vector<T> v2(v1) | v2中包含有v1所有元素的副本 |
| vector<T> v2 = v1 | 等價于v2(v1),v2中包含有v1所有元素的副本 |
| vector<T> v3 (n, val) | v3包含了n個重復(fù)的元素,每個元素的值都是val |
| vector<T> v4 (n) | v4包含了n個重復(fù)地執(zhí)行了值初始化的對象 |
| vector<T> v5{a, b, c…} | v5包含了初始值個數(shù)的元素,每個元素被賦予相應(yīng)的初始值 |
| vector<T> v5={a, b, c…} | 等價于v5{a,b,c…} |
可以默認(rèn)初始化 vector對象,從而創(chuàng)建一個指定類型的空vector:
vector<string> svec; //默認(rèn)初始化,svec不含任何元素看起來空vector好像沒什么用,但是很快我們就會知道程序在運(yùn)行時可以很高效地往vector對象中添加元素。事實(shí)上,最常見的方式就是先定義一個空vector,然后當(dāng)運(yùn)行時獲取到元素的值后再逐一添加。
當(dāng)然也可以在定義vector對象時指定元素的初始值。
例如,允許把一個vector對象的元素拷貝給另外一個vector對象。此時,新vector對象的元素就是原vector對象對應(yīng)元素的副本。注意兩個vector對象的類型必須相同:
vector<int> ivec; // initially empty // give ivec some values vector<int> ivec2(ivec); // copy elements of ivec into ivec2 vector<int> ivec3 = ivec; // copy elements of ivec into ivec3 vector<string> svec(ivec2); // error: svec holds strings, not ints列表初始化vector對象
C++11新標(biāo)準(zhǔn)還提供了另外一種為vector對象的元素賦初值的方法,即列表初始化。此時,用花括號括起來的0個或多個初始元素值被賦給vector對象:
vector<string> articles = {"a", "an", "the"};之前已經(jīng)講過,C++語言提供了幾種不同的初始化方式。在大多數(shù)情況下這些初始化方式可以相互等價地使用,不過也并非一直如此。目前已經(jīng)介紹過的兩種例外情況是:
第三種特殊的要求是,如果提供的是初始元素值的列表,則只能把初始值都放在花括號里進(jìn)行列表初始化,而不能放在圓括號里:
vector<string> v1{"a", "an", "the"}; // list initialization vector<string> v2("a", "an", "the"); // error創(chuàng)建指定數(shù)量的元素
還可以用vector對象容納的元素數(shù)量和所有元素的統(tǒng)一初始值來初始化vector對象:
vector<int> ivec(10, -1); // ten int elements, each initialized to -1 vector<string> svec(10, "hi!"); // ten strings; each element is "hi!"值初始化
通常情況下,可以只提供vector對象容納的元素數(shù)量而不用略去初始值。此時庫會創(chuàng)建一個值初始化的(value-initialized)元素初值,并把它賦給容器中的所有元素。這個初值由vector對象中元素的類型決定。
如果vector對象的元素是內(nèi)置類型,比如 int,則元素初始值自動設(shè)為0。如果元素是某種類類型,比如 string,則元素由類默認(rèn)初始化:
vector<int> ivec(10); // ten elements, each initialized to 0 vector<string> svec(10); // ten elements, each an empty string對這種初始化的方式有兩個特殊限制:其一,有些類要求必須明確地提供初始值,如果vector對象中元素的類型不支持默認(rèn)初始化,我們就必須提供初始的元素值。對這種類型的對象來說,只提供元素的數(shù)量而不設(shè)定初始值無法完成初始化工作。
其二,如果只提供了元素的數(shù)量而沒有設(shè)定初始值,只能使用直接初始化:
vector<int> vi = 10; // error: must use direct initialization to supply a size這里的10是用來說明如何初始化vector對象的,我們用它的本意是想創(chuàng)建含有10個值初始化了的元素的vector對象,而非把數(shù)字10“拷貝”到vector中。因此,此時不宜使用拷貝初始化。
列表初始值還是元素數(shù)量
在某些情況下,初始化的真實(shí)含義依賴于傳遞初始值時用的是花括號還是圓括號。
例如,用一個整數(shù)來初始化vector<int>時,整數(shù)的含義可能是vector對象的容量也可能是元素的值。類似的,用兩個整數(shù)來初始化vector<int>時,這兩個整數(shù)可能一個是vector對象的容量,另一個是元素的初值,也可能它們是容量為2的vector對象中兩個元素的初值。通過使用花括號或圓括號可以區(qū)分上述這些含義:
vector<int> v1(10); // v1 has ten elements with value 0 vector<int> v2{10}; // v2 has one element with value 10 vector<int> v3(10, 1); // v3 has ten elements with value 1 vector<int> v4{10, 1}; // v4 has two elements with values 10 and 1如果用的是圓括號,可以說提供的值是用來構(gòu)造(construct)vector對象的。例如,v1的初始值說明了vector對象的容量; v3的兩個初始值則分別說明了vector對象的容量和元素的初值。
如果用的是花括號,可以表述成我們想列表初始化(list initialize)該vector對象。也就是說,初始化過程會盡可能地把花括號內(nèi)的值當(dāng)成是元素初始值的列表來處理,只有在無法執(zhí)行列表初始化時才會考慮其他初始化方式。
在上例中,給v2和v4提供的初始值都能作為元素的值,所以它們都會執(zhí)行列表初始化,vector對象v2包含一個元素而vector對象v4包含兩個元素。
另一方面,如果初始化時使用了花括號的形式但是提供的值又不能用來列表初始化,就要考慮用這樣的值來構(gòu)造vector對象了。例如,要想列表初始化一個含有string對象的vector對象,應(yīng)該提供能賦給string對象的初值。此時不難區(qū)分到底是要列表初始化vector對象的元素還是用給定的容量值來構(gòu)造vector對象:(Note:勉強(qiáng)初始化)
vector<string> v5{"hi"}; // list initialization: v5 has one element vector<string> v6("hi"); // error: can't construct a vector from a string literal vector<string> v7{10}; // v7 has ten default-initialized elements vector<string> v8{10, "hi"}; // v8 has ten elements with value "hi"盡管在上面的例子中除了第二條語句之外都用了花括號,但其實(shí)只有v5是列表初始化。要想列表初始化 vector對象,花括號里的值必須與元素類型相同。顯然不能用int初始化 string對象,所以v7和v8提供的值不能作為元素的初始值。確認(rèn)無法執(zhí)行列表初始化后,編譯器會嘗試用默認(rèn)值初始化vector對象。
向vector對象中添加元素
對vector對象來說,直接初始化的方式適用于三種情況:
然而更常見的情況是:創(chuàng)建一個vector對象時并不清楚實(shí)際所需的元素個數(shù),元素的值也經(jīng)常無法確定。還有些時候即使元素的初值已知,但如果這些值總量較大而各不相同,那么在創(chuàng)建vector對象的時候執(zhí)行初始化操作也會顯得過于煩瑣。
舉個例子,如果想創(chuàng)建一個vector對象令其包含從0到9共10個元素,使用列表初始化的方法很容易做到這一點(diǎn),但如果vector對象包含的元素是從0到99或者從0到999呢?這時通過列表初始化把所有元素都一一羅列出來就不太合適了。
對于此例來說,更好的處理方法是先創(chuàng)建一個空vector,然后在運(yùn)行時再利用vector的成員函數(shù)push_back向其中添加元素。push_back 負(fù)責(zé)把一個值當(dāng)成vector對象的尾元素“壓到(push)” vector對象的“尾端(back)”。例如:
vector<int> v2; // empty vector for (int i = 0; i != 100; ++i)v2.push_back(i); // append sequential integers to v2 // at end of loop v2 has 100 elements, values 0 . . . 99同樣的,如果直到運(yùn)行時才能知道vector對象中元素的確切個數(shù),也應(yīng)該使用剛剛這種方法創(chuàng)建vector對象并為其賦值。例如,有時需要實(shí)時讀入數(shù)據(jù)然后將其賦予vector對象:
// read words from the standard input and store them as elements in a vector string word; vector<string> text; // empty vector while (cin >> word) {text.push_back(word); // append word to text }關(guān)鍵概念:vector對象能高效增長
C++標(biāo)準(zhǔn)要求vector應(yīng)該能在運(yùn)行時高效快速地添加元素。因此既然 vector對象能高效地增長,那么在定義vector對象的時候設(shè)定其大小也就沒什么必要了,事實(shí)上如果這么做性能可能更差。只有一種例外情況,就是所有(all)元素的值都一樣。一旦元素的值有所不同,更有效的辦法是先定義一個空的vector對象,再在運(yùn)行時向其中添加具體值。此外,在未來將介紹vector還提供了方法,允許我們進(jìn)一步提升動態(tài)添加元素的性能。
開始的時候創(chuàng)建空的vector對象,在運(yùn)行時再動態(tài)添加元素,這一做法與C語言及其他大多數(shù)語言中內(nèi)置數(shù)組類型的用法不同。特別是如果用慣了C或者Java,可以預(yù)計在創(chuàng)建vector對象時順便指定其容量是最好的。然而事實(shí)上,通常的情況是恰恰相反。
(在Java中,如果你一開始知道容器的容量,就設(shè)定容器的容量,這樣可以減少容器擴(kuò)容時所帶來的消耗)
向vector對象添加元素蘊(yùn)含的編程假定
由于能高效便捷地向vector對象中添加元素,很多編程工作被極大簡化了。然而,這種簡便性也伴隨著一些對編寫程序更高的要求:其中一條就是必須要確保所寫的循環(huán)正確無誤,特別是在循環(huán)有可能改變vector對象容量的時候。
隨著對vector的更多使用,我們還會逐漸了解到其他一些隱含的要求,其中一條是現(xiàn)在就要指出的:如果循環(huán)體內(nèi)部包含有向vector對象添加元素的語句,則不能使用范圍for循環(huán),具體原因在日后詳細(xì)解釋。
范圍for語句體內(nèi)不應(yīng)改變其所遍歷序列的大小。
(在Java中,如果for循環(huán)遍歷一個List時,增刪元素,后臺會拋出ConcurrentModificationException)
其他vector操作
| v.empty() | 如果v不含有任何元素,返回真;否則返回假 |
| v.size() | 返回v中元素的個數(shù) |
| v.push_back(t) | 向v的尾端添加一個值為t的元素 |
| v[n] | 返回v中第n個位置上元素的引用 |
| v1 = v2 | 用v2中元素的拷貝替換v1中的元素 |
| v1 ={a,b,c…} | 用列表中元素的拷貝替換v1中的元素 |
| vl == v2 | v1和v2相等當(dāng)且僅當(dāng)它們的元素數(shù)量相同且對應(yīng)位置的元素值都相同 |
| v1 != v2 | . |
| <, <=, >, >= | 顧名思義,以字典順序進(jìn)行比較 |
訪問vector對象中元素的方法和訪問string對象中字符的方法差不多,也是通過元素在vector對象中的位置。例如,可以使用范圍for語句處理vector對象中的所有元素:
vector<int> v{1,2,3,4,5,6,7,8,9}; for (auto &i : v) // for each element in v (note: i is a reference)i *= i; // square the element value for (auto i : v) // for each element in vcout << i << " "; // print the element cout << endl;vector的empty和size兩個成員與string 的同名成員功能完全一致:
-
empty檢查vector對象是否包含元素然后返回一個布爾值;
-
size則返回vector對象中元素的個數(shù),返回值的類型是由vector定義的size_type類型。
要使用size type,需首先指定它是由哪種類型定義的。vector對象的類型總是包含著元素的類型:
vector<int>::size_type //正確 vector::size_type//錯誤vector比較
各個相等性運(yùn)算符和關(guān)系運(yùn)算符也與string 的相應(yīng)運(yùn)算符功能一致。
兩個vector對象相等當(dāng)且僅當(dāng)它們所含的元素個數(shù)相同,而且對應(yīng)位置的元素值也相同。
關(guān)系運(yùn)算符依照字典順序進(jìn)行比較:
-
如果兩個vector對象的容量不同,但是在相同位置上的元素值都一樣,則元素較少的vector對象小于元素較多的vector對象;
-
若元素的值有區(qū)別,則vector對象的大小關(guān)系由第一對相異的元素值的大小關(guān)系決定。
只有當(dāng)元素的值可比較時,vector對象才能被比較。
- 一些類,如 string等,確實(shí)定義了自己的相等性運(yùn)算符和關(guān)系運(yùn)算符;
- 另外一些,如 Sales_item類支持的運(yùn)算已經(jīng)全都羅列在第一章中了,顯然并不支持相等性判斷和關(guān)系運(yùn)算等操作。因此,不能比較兩個vector<Sales_item>對象。
計算vector內(nèi)對象的索引
使用下標(biāo)運(yùn)算符能獲取到指定的元素。和 string一樣,vector對象的下標(biāo)也是從О開始計起,下標(biāo)的類型是相應(yīng)的 size_type類型。只要vector對象不是一個常量,就能向下標(biāo)運(yùn)算符返回的元素賦值。此外,也能通過計算得到 vector 內(nèi)對象的索引,然后直接獲取索引位置上的元素。
舉個例子,假設(shè)有一組成績的集合,其中成績的取值是從0到100。以10分為一個分?jǐn)?shù)段,要求統(tǒng)計各個分?jǐn)?shù)段各有多少個成績。顯然,從0到100總共有101種可能的成績?nèi)≈?#xff0c;這些成績分布在11個分?jǐn)?shù)段上:每10個分?jǐn)?shù)構(gòu)成一個分?jǐn)?shù)段,這樣的分?jǐn)?shù)段有10個,額外還有一個分?jǐn)?shù)段表示滿分100分。這樣第一個分?jǐn)?shù)段將統(tǒng)計成績在0到9之間的數(shù)量;第二個分?jǐn)?shù)段將統(tǒng)計成績在10到19之間的數(shù)量,以此類推。最后一個分?jǐn)?shù)段統(tǒng)計滿分100分的數(shù)量。
// count the number of grades by clusters of ten: 0--9, 10--19, . .. 90--99, 100 vector<unsigned> scores(11, 0); // 11 buckets, all initially 0 unsigned grade; while (cin >> grade) { // read the gradesif (grade <= 100) // handle only valid grades++scores[grade/10]; // increment the counter for the current cluster }輸入:
42 65 95 100 39 67 95 76 88 76 83 92 76 93如果輸出scores,則為:
0 0 0 1 1 0 2 3 2 4 1不能用下標(biāo)形式添加元素
剛接觸C++語言的程序員也許會認(rèn)為可以通過vector對象的下標(biāo)形式來添加元素,事實(shí)并非如此。下面的代碼試圖為vector對象ivec添加10個元素:
vector<int> ivec; // empty vector for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)ivec[ix] = ix; // disaster: ivec has no elements然而,這段代碼是錯誤的:ivec是一個空 vector,根本不包含任何元素,當(dāng)然也就不能通過下標(biāo)去訪問任何元素!如前所述,正確的方法是使用push_back:
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)ivec.push_back(ix); // ok: adds a new element with value ixvector對象(以及string對象)的下標(biāo)運(yùn)算符可用于訪問已存在的元素,而不能用于添加元素。
提示:只能對確知已存在的元素執(zhí)行下標(biāo)操作!
關(guān)于下標(biāo)必須明確的一點(diǎn)是:只能對確知已存在的元素執(zhí)行下標(biāo)操作。例如,
vector<int> ivec; // empty vector cout << ivec[0]; // error: ivec has no elements!vector<int> ivec2(10); // vector with ten elements cout << ivec2[10]; // error: ivec2 has elements 0 . . . 9試圖用下標(biāo)的形式去訪問一個不存在的元素將引發(fā)錯誤,不過這種錯誤不會被編譯器發(fā)現(xiàn),而是在運(yùn)行時產(chǎn)生一個不可預(yù)知的值。
不幸的是,這種通過下標(biāo)訪問不存在的元素的行為非常常見,而且會產(chǎn)生很嚴(yán)重的后果。所謂的緩沖區(qū)溢出(buffer overflow)指的就是這類錯誤,這也是導(dǎo)致PC及其他設(shè)備上應(yīng)用程序出現(xiàn)安全問題的一個重要原因。
確保下標(biāo)合法的一種有效手段就是盡可能使用范圍for語句。
迭代器介紹
已經(jīng)知道可以使用下標(biāo)運(yùn)算符來訪問string 對象的字符或vector對象的元素,還有另外一種更通用的機(jī)制也可以實(shí)現(xiàn)同樣的目的,這就是迭代器( iterator)。
除了vector之外,標(biāo)準(zhǔn)庫還定義了其他幾種容器。所有標(biāo)準(zhǔn)庫容器都可以使用迭代器,但是其中只有少數(shù)幾種才同時支持下標(biāo)運(yùn)算符。(Note:日后就用迭代器迭代)
嚴(yán)格來說,string對象不屬于容器類型,但是string支持很多與容器類型類似的操作。vector支持下標(biāo)運(yùn)算符,這點(diǎn)和 string一樣;string支持迭代器,這也和 vector是一樣的。
類似于指針類型,迭代器也提供了對對象的間接訪問。
- 就迭代器而言,其對象是容器中的元素或者string對象中的字符。
- 使用迭代器可以訪問某個元素,迭代器也能從一個元素移動到另外一個元素。
- 迭代器有有效和無效之分,這一點(diǎn)和指針差不多。有效的迭代器或者指向某個元素,或者指向容器中尾元素的下一位置;其他所有情況都屬于無效。
使用迭代器
和指針不一樣的是,獲取迭代器不是使用取地址符,有迭代器的類型同時擁有返回迭代器的成員。比如,這些類型都擁有名為begin和end的成員,其中 begin 成員負(fù)責(zé)返回指向第一個元素(或第一個字符)的迭代器。如有下述語句:
// the compiler determines the type of b and e; see § 2.5.2 (p. 68) // b denotes the first element and e denotes one past the last element in v auto b = v.begin(), e = v.end(); // b and e have the same typeend成員則負(fù)責(zé)返回指向容器(或string對象)“尾元素的下一位置(one past the end)"的迭代器,也就是說,該迭代器指示的是容器的一個本不存在的“尾后(off the end)”元素。這樣的迭代器沒什么實(shí)際含義,僅是個標(biāo)記而已,表示我們已經(jīng)處理完了容器中的所有元素。end成員返回的迭代器常被稱作尾后迭代器(off-the-end iterator)或者簡稱為尾迭代器(end iterator)。特殊情況下如果容器為空,則 begin和 end返回的是同一個迭代器。
如果容器為空,則begin和end返回的是同一個迭代器,都是尾后迭代器。
一般來說,我們不清楚(不在意)迭代器準(zhǔn)確的類型到底是什么。在上面的例子中,使用auto關(guān)鍵字定義變量b和e ,這兩個變量的類型也就是begin和 end的返回值類型,稍后做更詳細(xì)的介紹。
迭代器運(yùn)算符
下表列舉了迭代器支持的一些運(yùn)算。使用==和!=來比較兩個合法的迭代器是否相等,如果兩個迭代器指向的元素相同或者都是同一個容器的尾后迭代器,則它們相等;否則就說這兩個迭代器不相等。
| *iter | 返回迭代器iter所指元素的引用 |
| iter->mem | 解引用iter并獲取該元素的名為mem的成員,等價于(*iter).mem |
| ++iter | 令iter指示容器中的下一個元素 |
| –iter | 令iter指示容器中的上一個元素 |
| iter1 == iter2 | 判斷兩個迭代器是否相等,如果兩個迭代器指示的是同一個元素或者它們是同一個容器的尾后迭代器,則相等;反之,不相等 |
| iter1 != iter2 | 判斷兩個迭代器是否不相等 |
和指針類似,也能通過解引用迭代器來獲取它所指示的元素,執(zhí)行解引用的迭代器必須合法并確實(shí)指示著某個元素。試圖解引用一個非法迭代器或者尾后迭代器都是未被定義的行為。
舉個例子,用迭代器把string對象的第一個字母改為了大寫形式:
string s("some string"); if (s.begin() != s.end()) { // make sure s is not empty auto it = s.begin(); // it denotes the first character in s*it = toupper(*it); // make that character uppercase }若輸出s,結(jié)果將是:
Some string將迭代器從一個元素移動到另外一個元素
迭代器使用遞增(++)運(yùn)算符來從一個元素移動到下一個元素。從邏輯上來說,迭代器的遞增和整數(shù)的遞增類似,整數(shù)的遞增是在整數(shù)值上“加1“,迭代器的遞增則是將迭代器“向前移動一個位置”。
因?yàn)閑nd返回的迭代器并不實(shí)際指示某個元素,所以不能對其進(jìn)行遞增或解引用的操作。
之前有一個程序把string對象中第一個單詞改寫為大寫形式,現(xiàn)在利用迭代器及其遞增運(yùn)算符可以實(shí)現(xiàn)相同的功能:
// process characters in s until we run out of characters or we hit a whitespace for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)*it = toupper(*it); // capitalize the current character關(guān)鍵概念:泛型編程
原來使用C或Java的程序員在轉(zhuǎn)而使用C++語言之后,會對for循環(huán)中使用!=而非<進(jìn)行判斷有點(diǎn)兒奇怪。C++程序員習(xí)慣性地使用!=,其原因和他們更愿意使用迭代器而非下標(biāo)的原因一樣(支持下標(biāo)的容器只有少數(shù));因?yàn)?strong>這種編程風(fēng)格在標(biāo)準(zhǔn)庫提供的所有容器上都有效。
之前已經(jīng)說過,只有string和 vector等一些標(biāo)準(zhǔn)庫類型有下標(biāo)運(yùn)算符,而并非全都如此。
與之類似,所有標(biāo)準(zhǔn)庫容器的迭代器都定義了==和!=,但是它們中的大多數(shù)都沒有定義<運(yùn)算符。因此,只要我們養(yǎng)成使用迭代器和!=的習(xí)慣,就不用太在意用的到底是哪種容器類型。
迭代器類型
就像不知道string 和 vector的size_type成員到底是什么類型一樣,一般來說我們也不知道(其實(shí)是無須知道)迭代器的精確類型。而實(shí)際上,那些擁有迭代器的標(biāo)準(zhǔn)庫類型使用iterator和const_iterator來表示迭代器的類型:
vector<int>::iterator it; // it can read and write vector<int> elements string::iterator it2; // it2 can read and write characters in a string vector<int>::const_iterator it3; // it3 can read but not write elements string::const_iterator it4; // it4 can read but not write charactersconst_iterator和常量指針差不多,能讀取但不能修改它所指的元素值。相反,iterator的對象可讀可寫。
- 如果vector對象或string對象是一個常量,只能使用const_iterator;
- 如果vector對象或string對象不是常量,那么既能使用iterator也能使用const_iterator。
術(shù)語:迭代器和迭代器類型
迭代器這個名詞有三種不同的含義:
重點(diǎn)是理解存在一組概念上相關(guān)的類型,我們認(rèn)定某個類型是迭代器當(dāng)且僅當(dāng)它支持一套操作,這套操作使得我們能訪問容器的元素或者從某個元素移動到另外一個元素。
每個容器類定義了一個名為 iterator的類型,該類型支持迭代器概念所規(guī)定的一套操作。
begin和end運(yùn)算符
begin和end返回的具體類型由對象是否是常量決定,
- 如果對象是常量,begin和end返回const_iterator;
- 如果對象不是常量,返回iterator:
有時候這種默認(rèn)的行為并非我們所要。
如果對象只需讀操作而無須寫操作的話最好使用常量類型(比如 const_iterator)。為了便于專門得到const_iterator類型的返回值,C++11新標(biāo)準(zhǔn)引入了兩個新函數(shù),分別是cbegin和cend:
auto it3 = v.cbegin();//it3的類型是vector<int>::const_iterator類似于begin和 end,上述兩個新函數(shù)也分別返回指示容器第一個元素或最后元素下一位置的迭代器。有所不同的是,不論vector對象(或string對象)本身是否是常量,返回值都是const_iterator。
結(jié)合解引用和成員訪問操作
解引用迭代器可獲得迭代器所指的對象,如果該對象的類型恰好是類,就有可能希望進(jìn)一步訪問它的成員。例如,對于一個由字符串組成的vector對象來說,要想檢查其元素是否為空,令it是該vector對象的迭代器,只需檢查it所指字符串是否為空就可以了,其代碼如下所示:
(*it).empty()注意,(*it). empty()中的圓括號必不可少,具體原因稍后介紹,該表達(dá)式的含義是先對it解引用,然后解引用的結(jié)果再執(zhí)行點(diǎn)運(yùn)算符。如果不加圓括號,點(diǎn)運(yùn)算符將由it來執(zhí)行,而非it解引用的結(jié)果:
(*it).empty() // dereferences it and calls the member empty on the resulting object *it.empty() // error: attempts to fetch the member named empty from it// but it is an iterator and has no member named empty上面第二個表達(dá)式的含義是從名為it的對象中尋找其 empty成員,顯然it是一個迭代器,它沒有哪個成員是叫empty的,所以第二個表達(dá)式將發(fā)生錯誤。
為了簡化上述表達(dá)式,C++語言定義了箭頭運(yùn)算符(->)。箭頭運(yùn)算符把解引用和成員訪問兩個操作結(jié)合在一起,也就是說,it->mem和(*it).mem表達(dá)的意思相同。
例如,假設(shè)用一個名為text的字符串向量存放文本文件中的數(shù)據(jù),其中的元素或者是一句話或者是一個用于表示段落分隔的空字符串。如果要輸出text中第一段的內(nèi)容,可以利用迭代器寫一個循環(huán)令其遍歷text,直到遇到空字符串的元素為止:
// print each line in text up to the first blank line for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)cout << *it << endl;某些對vector對象的操作會使迭代器失效
曾經(jīng)介紹過,雖然vector對象可以動態(tài)地增長,但是也會有一些副作用。已知的一個限制是不能在范圍for循環(huán)中向vector對象添加元素。另外一個限制是任何一種可能改變vector對象容量的操作,比如 push_back,都會使該vector對象的迭代器失效。日后將詳細(xì)解釋迭代器是如何失效的。
謹(jǐn)記,但凡是使用了迭代器的循環(huán)體,都不要向迭代器所屬的容器添加元素。
迭代器運(yùn)算
迭代器的遞增運(yùn)算令迭代器每次移動一個元素,所有的標(biāo)準(zhǔn)庫容器都有支持遞增運(yùn)算的迭代器。類似的,也能用==和!=對任意標(biāo)準(zhǔn)庫類型的兩個有效迭代器進(jìn)行比較。
string和 vector的迭代器提供了更多額外的運(yùn)算符,一方面可使得迭代器的每次移動跨過多個元素,另外也支持迭代器進(jìn)行關(guān)系運(yùn)算。所有這些運(yùn)算被稱作迭代器運(yùn)算(iterator arithmetic),其細(xì)節(jié)如表列出:
| iter + n | 迭代器加上一個整數(shù)值仍得一個迭代器,迭代器指示的新位置與原來相比向前移動了若干個元素。結(jié)果迭代器或者指示容器內(nèi)的一個元素,或者指示容器尾元素的下一位置 |
| iter - n | 迭代器減去一個整數(shù)值仍得一個迭代器,迭代器指示的新位置與原來相比向后移動了若干個元素。結(jié)果迭代器或者指示容器內(nèi)的一個元素,或者指示容器尾元素的下一位置 |
| iter1 += n | 迭代器加法的復(fù)合賦值語句,將iter1加n的結(jié)果賦給iter1 |
| iter1 -= n | 迭代器減法的復(fù)合賦值語句,將iter1減n的結(jié)果賦給iter1 |
| iter1 - iter2 | 兩個迭代器相減的結(jié)果是它們之間的距離,也就是說,將運(yùn)算符右側(cè)的迭代器向前移動差值個元素后將得到左側(cè)的迭代器。參與運(yùn)算的兩個迭代器必須指向的是同一個容器中的元素或者尾元素的下一位置 |
| >、>=、<、<= | 迭代器的關(guān)系運(yùn)算符,如果某迭代器指向的容器位置在另一個迭代器所指位置之前,則說前者小于后者。參與運(yùn)算的兩個迭代器必須指向的是同一個容器中的元素或者尾元素的下一位置 |
迭代器的算術(shù)運(yùn)算
可以令迭代器和一個整數(shù)值相加(或相減),其返回值是向前(或向后)移動了若干個位置的迭代器。執(zhí)行這樣的操作時,結(jié)果迭代器或者指示原vector對象(或string對象)內(nèi)的一個元素,或者指示原vector對象(或string對象)尾元素的下一位置。
舉個例子,下面的代碼得到一個迭代器,它指向某vector對象中間位置的元素:
// compute an iterator to the element closest to the midpoint of vi auto mid = vi.begin() + vi.size() / 2;對于string或vector的迭代器來說,除了判斷是否相等,還能使用關(guān)系運(yùn)算符(<、<=、>、>=)對其進(jìn)行比較。參與比較的兩個迭代器必須合法而且指向的是同一個容器的元素(或者尾元素的下一位置)。例如,假設(shè)it和mid是同一個vector對象的兩個迭代器,可以用下面的代碼來比較它們所指的位置孰前孰后:
if (it < mid)// process elements in the first half of vi只要兩個迭代器指向的是同一個容器中的元素或者尾元素的下一位置,就能將其相減,所得結(jié)果是兩個迭代器的距離。所謂距離指的是右側(cè)的迭代器向前移動多少位置就能追上左側(cè)的迭代器,其類型是名為 difference_type 的帶符號整型數(shù)。string和vector都定義了difference_type,因?yàn)檫@個距離可正可負(fù),所以difference_type是帶符號類型的。
使用迭代器運(yùn)算
使用迭代器運(yùn)算的一個經(jīng)典算法是二分搜索。
二分搜索從有序序列中尋找某個給定的值。二分搜索從序列中間的位置開始搜索,
- 如果中間位置的元素正好就是要找的元素,搜索完成;
- 如果不是,
- 假如該元素小于要找的元素,則在序列的后半部分繼續(xù)搜素;
- 假如該元素大于要找的元素,則在序列的前半部分繼續(xù)搜索。
在縮小的范圍中計算一個新的中間元素并重復(fù)之前的過程,直至最終找到目標(biāo)或者沒有元素可供繼續(xù)搜索。
// text must be sorted // beg and end will denote the range we're searching auto beg = text.begin(), end = text.end(); auto mid = text.begin() + (end - beg)/2; // original midpoint // while there are still elements to look at and we haven't yet found sought while (mid != end && *mid != sought) {if (sought < *mid) // is the element we want in the first half?end = mid; // if so, adjust the range to ignore the second halfelse // the element we want is in the second halfbeg = mid + 1; // start looking with the element just after midmid = beg + (end - beg)/2; // new midpoint }數(shù)組
數(shù)組是一種類似于標(biāo)準(zhǔn)庫類型vector的數(shù)據(jù)結(jié)構(gòu),但是在性能和靈活性的權(quán)衡上又與vector有所不同。
與vector相似的地方是,數(shù)組也是存放類型相同的對象的容器,這些對象本身沒有名字,需要通過其所在位置訪問。
與vector不同的地方是,數(shù)組的大小確定不變,不能隨意向數(shù)組中增加元素。因?yàn)閿?shù)組的大小固定,因此對某些特殊的應(yīng)用來說程序的運(yùn)行時性能較好,但是相應(yīng)地也損失了一些靈活性。
如果不清楚元素的確切個數(shù),請使用vector。
定義和初始化內(nèi)置數(shù)組
數(shù)組是一種復(fù)合類型。數(shù)組的聲明形如 a[d],其中 a是數(shù)組的名字,d是數(shù)組的維度。維度說明了數(shù)組中元素的個數(shù),因此必須大于0。數(shù)組中元素的個數(shù)也屬于數(shù)組類型的一部分,編譯的時候維度應(yīng)該是已知的。也就是說,維度必須是一個常量表達(dá)式:
unsigned cnt = 42; // not a constant expression constexpr unsigned sz = 42; // constant expression// constexpr see § 2.4.4 (p. 66) int arr[10]; // array of ten ints int *parr[sz]; // array of 42 pointers to int string bad[cnt]; // error: cnt is not a constant expression string strs[get_size()]; // ok if get_size is constexpr, error otherwise默認(rèn)情況下,數(shù)組的元素被默認(rèn)初始化。
和內(nèi)置類型的變量一樣,如果在函數(shù)內(nèi)部定義了某種內(nèi)置類型的數(shù)組,那么默認(rèn)初始化會令數(shù)組含有未定義的值。
定義數(shù)組的時候必須指定數(shù)組的類型,不允許用auto關(guān)鍵字由初始值的列表推斷類型。另外和 vector一樣,數(shù)組的元素應(yīng)為對象,因此不存在引用的數(shù)組。
顯式初始化數(shù)組元素
可以對數(shù)組的元素進(jìn)行列表初始化,此時允許忽略數(shù)組的維度。
- 如果在聲明時沒有指明維度,編譯器會根據(jù)初始值的數(shù)量計算并推測出來;
- 相反,如果指明了維度,那么初始值的總數(shù)量不應(yīng)該超出指定的大小。
- 如果維度比提供的初始值數(shù)量大,則用提供的初始值初始化靠前的元素,剩下的元素被初始化成默認(rèn)值:
字符數(shù)組的特殊性
字符數(shù)組有一種額外的初始化形式,我們可以用字符串字面值對此類數(shù)組初始化。當(dāng)使用這種方式時,一定要注意字符串字面值的結(jié)尾處還有一個空字符,這個空字符也會像字符串的其他字符一樣被拷貝到字符數(shù)組中去:
char a1[] = {'C', '+', '+'}; // list initialization, no null char a2[] = {'C', '+', '+', '\0'}; // list initialization, explicit null char a3[] = "C++"; // null terminator added automatically const char a4[6] = "Daniel"; // error: no space for the null!不允許拷貝和賦值
不能將數(shù)組的內(nèi)容拷貝給其他數(shù)組作為其初始值,也不能用數(shù)組為其他數(shù)組賦值:
int a[] = {0, 1, 2}; // array of three ints int a2[] = a; // error: cannot initialize one array with another a2 = a; // error: cannot assign one array to another一些編譯器支持?jǐn)?shù)組的賦值,這就是所謂的編譯器擴(kuò)展(compiler extension)。但一般來說,最好避免使用非標(biāo)準(zhǔn)特性,因?yàn)楹蟹菢?biāo)準(zhǔn)特性的程序很可能在其他編譯器上無法正常工作。
理解復(fù)雜的數(shù)組聲明
和 vector一樣,數(shù)組能存放大多數(shù)類型的對象。例如,可以定義一個存放指針的數(shù)組。又因?yàn)閿?shù)組本身就是對象,所以允許定義數(shù)組的指針及數(shù)組的引用。在這幾種情況中,定義存放指針的數(shù)組比較簡單和直接,但是定義數(shù)組的指針或數(shù)組的引用就稍微復(fù)雜一點(diǎn)了:
int *ptrs[10]; // ptrs is an array of ten pointers to int int &refs[10] = /* ? */; // error: no arrays of references int (*Parray)[10] = &arr; // Parray points to an array of ten ints int (&arrRef)[10] = arr; // arrRef refers to an array of ten ints默認(rèn)情況下,類型修飾符從右向左依次綁定。
對于ptrs來說,從右向左理解其含義比較簡單:首先知道我們定義的是一個大小為10的數(shù)組,它的名字是ptrs,然后知道數(shù)組中存放的是指向int的指針。
但是對于Parray來說,從右向左理解就不太合理了。因?yàn)閿?shù)組的維度是緊跟著被聲明的名字的,所以就數(shù)組而言,由內(nèi)向外閱讀要比從右向左好多了。
由內(nèi)向外的順序可幫助我們更好地理解 Parray 的含義:
-
首先是圓括號括起來的部分,*Parray意味著Parray是個指針,
-
接下來觀察右邊,可知道Parray是個指向大小為10的數(shù)組的指針,
-
最后觀察左邊,知道數(shù)組中的元素是int。
這樣最終的含義就明白無誤了,Parray是一個指針,它指向一個int數(shù)組,數(shù)組中包含10個元素。
同理,(&arrRef)表示arrRef是一個引用,它引用的對象是一個大小為10的數(shù)組,數(shù)組中元素的類型是int。
當(dāng)然,對修飾符的數(shù)量并沒有特殊限制:
int *(&arry)[10] = ptrs; // arry is a reference to an array of ten pointers按照由內(nèi)向外的順序閱讀上述語句,
- 首先知道arry是一個引用,
- 然后觀察右邊知道, arry引用的對象是一個大小為10的數(shù)組,
- 最后觀察左邊知道,數(shù)組的元素類型是指向int的指針。
這樣,arry就是一個含有10個int型指針的數(shù)組的引用。
要想理解數(shù)組聲明的含義,最好的辦法是從數(shù)組的名字開始按照由內(nèi)向外的順序閱讀。
訪問數(shù)組元素
與標(biāo)準(zhǔn)庫類型vector和 string一樣,數(shù)組的元素也能使用范圍for語句或下標(biāo)運(yùn)算符來訪問。數(shù)組的索引從О開始,以一個包含10個元素的數(shù)組為例,它的索引從0到9,而非從1到10。
在使用數(shù)組下標(biāo)的時候,通常將其定義為size_t類型。size_t是一種機(jī)器相關(guān)的無符號類型,它被設(shè)計得足夠大以便能表示內(nèi)存中任意對象的大小。在 cstddef頭文件中定義了size_t類型,這個文件是C標(biāo)準(zhǔn)庫stddef.h頭文件的C++語言版本。
數(shù)組除了大小固定這一特點(diǎn)外,其他用法與vector基本類似。例如,可以用數(shù)組來記錄各分?jǐn)?shù)段的成績個數(shù):
// count the number of grades by clusters of ten: 0--9, 10--19, ... 90--99, 100 unsigned scores[11] = {}; // 11 buckets, all value initialized to 0 unsigned grade; while (cin >> grade) {if (grade <= 100)++scores[grade/10]; // increment the counter for the current cluster }數(shù)組下標(biāo)運(yùn)算符與vector下標(biāo)運(yùn)算符不太明顯的區(qū)別是,
-
數(shù)組的下標(biāo)運(yùn)算符是由C++語言直接定義的,這個運(yùn)算符能用在數(shù)組類型的運(yùn)算對象上。
-
vector所用的下標(biāo)運(yùn)算符是庫模板vector定義的,只能用于vector類型的運(yùn)算對象。
與vector和 string一樣,當(dāng)需要遍歷數(shù)組的所有元素時,最好的辦法也是使用范圍for語句。例如,下面的程序輸出所有的scores:
for (auto i : scores) // for each counter in scorescout << i << " "; // print the value of that counter cout << endl;因?yàn)榫S度是數(shù)組類型的一部分,所以系統(tǒng)知道數(shù)組scores 中有多少個元素,使用范圍for語句可以減輕人為控制遍歷過程的負(fù)擔(dān)。
檢查下標(biāo)的值
與vector和 string一樣,數(shù)組的下標(biāo)是否在合理范圍之內(nèi)由程序員負(fù)責(zé)檢查,所謂合理就是說下標(biāo)應(yīng)該大于等于0而且小于數(shù)組的大小。要想防止數(shù)組下標(biāo)越界,除了小心謹(jǐn)慎注意細(xì)節(jié)以及對代碼進(jìn)行徹底的測試之外,沒有其他好辦法。對于一個程序來說,即使順利通過編譯并執(zhí)行,也不能肯定它不包含此類致命的錯誤。
大多數(shù)常見的安全問題都源于緩沖區(qū)溢出錯誤。當(dāng)數(shù)組或其他類似數(shù)據(jù)結(jié)構(gòu)的下標(biāo)越界并試圖訪問非法內(nèi)存區(qū)域時,就會產(chǎn)生此類錯誤。
指針和數(shù)組
在C++語言中,指針和數(shù)組有非常緊密的聯(lián)系。就如即將介紹的,使用數(shù)組的時候編譯器一般會把它轉(zhuǎn)換成指針。
通常情況下,使用取地址符來獲取指向某個對象的指針,取地址符可以用于任何對象。數(shù)組的元素也是對象,對數(shù)組使用下標(biāo)運(yùn)算符得到該數(shù)組指定位置的元素。因此像其他對象一樣,對數(shù)組的元素使用取地址符就能得到指向該元素的指針:
string nums[] = {"one", "two", "three"}; // array of strings string *p = &nums[0]; // p points to the first element in nums然而,數(shù)組還有一個特性:在很多用到數(shù)組名字的地方,編譯器都會自動地將其替換為一個指向數(shù)組首元素的指針:
string *p2 = nums; // equivalent to p2 = &nums[0]在大多數(shù)表達(dá)式中,使用數(shù)組類型的對象其實(shí)是使用一個指向該數(shù)組首元素的指針。
由上可知,在一些情況下數(shù)組的操作實(shí)際上是指針的操作,這一結(jié)論有很多隱含的意思。其中一層意思是當(dāng)使用數(shù)組作為一個auto變量的初始值時,推斷得到的類型是指針而非數(shù)組:
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints auto ia2(ia); // ia2 is an int* that points to the first element in ia ia2 = 42; // error: ia2 is a pointer, and we can't assign an int to a pointer盡管ia是由10個整數(shù)構(gòu)成的數(shù)組,但當(dāng)使用ia作為初始值時,編譯器實(shí)際執(zhí)行的初始化過程類似于下面的形式:
auto ia2(&ia[0]); // now it's clear that ia2 has type int*必須指出的是,當(dāng)使用decltype關(guān)鍵字時上述轉(zhuǎn)換不會發(fā)生,decltype(ia)返回的類型是由10個整數(shù)構(gòu)成的數(shù)組:
// ia3 is an array of ten ints decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; ia3 = p; // error: can't assign an int* to an array ia3[4] = i; // ok: assigns the value of i to an element in ia3指針也是迭代器
與介紹的內(nèi)容相比,指向數(shù)組元素的指針擁有更多功能。vector和 string 的迭代器支持的運(yùn)算,數(shù)組的指針全都支持。例如,允許使用遞增運(yùn)算符將指向數(shù)組元素的指針向前移動到下一個位置上:
int arr[] = {0,1,2,3,4,5,6,7,8,9}; int *p = arr; // p points to the first element in arr ++p; // p points to arr[1]就像使用迭代器遍歷vector對象中的元素一樣,使用指針也能遍歷數(shù)組中的元素。
當(dāng)然,這樣做的前提是先得獲取到指向數(shù)組第一個元素的指針和指向數(shù)組尾元素的下一位置的指針。
之前已經(jīng)介紹過,通過數(shù)組名字或者數(shù)組中首元素的地址都能得到指向首元素的指針;不過獲取尾后指針就要用到數(shù)組的另外一個特殊性質(zhì)了。我們可以設(shè)法獲取數(shù)組尾元素之后的那個并不存在的元素的地址:
int *e = &arr[10]; // pointer just past the last element in arr這里顯然使用下標(biāo)運(yùn)算符索引了一個不存在的元素,arr有10個元素,尾元素所在位置的索引是9,接下來那個不存在的元素唯一的用處就是提供其地址用于初始化 e。就像尾后迭代器一樣,尾后指針也不指向具體的元素。因此,不能對尾后指針執(zhí)行解引用或遞增的操作。
利用上面得到的指針能重寫之前的循環(huán),令其輸出arr的全部元素:
for (int *b = arr; b != e; ++b)cout << *b << endl; // print the elements in arr標(biāo)準(zhǔn)庫函數(shù)begin和end
盡管能計算得到尾后指針,但這種用法極易出錯。為了讓指針的使用更簡單、更安全,C++11新標(biāo)準(zhǔn)引入了兩個名為 begin和 end的函數(shù)。這兩個函數(shù)與容器中的兩個同名成員功能類似,不過數(shù)組畢竟不是類類型,因此這兩個函數(shù)不是成員函數(shù)。正確的使用形式是將數(shù)組作為它們的參數(shù):
#include <iterator>int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints int *beg = begin(ia); // pointer to the first element in ia int *last = end(ia); // pointer one past the last element in iabegin函數(shù)返回指向ia首元素的指針,end函數(shù)返回指向ia尾元素下一位置的指針,這兩個函數(shù)定義在iterator頭文件中。
使用begin和 end可以很容易地寫出一個循環(huán)并處理數(shù)組中的元素。例如,假設(shè)arr是一個整型數(shù)組,下面的程序負(fù)責(zé)找到arr中的第一個負(fù)數(shù):
// pbeg points to the first and pend points just past the last element in arr int *pbeg = begin(arr), *pend = end(arr); // find the first negative element, stopping if we've seen all the elements while (pbeg != pend && *pbeg >= 0)++pbeg;一個指針如果指向了某種內(nèi)置類型數(shù)組的尾元素的“下一位置”,則其具備與vector的end函數(shù)返回的與迭代器類似的功能。特別要注意,尾后指針不能執(zhí)行解引用和遞增操作。
指針運(yùn)算
指向數(shù)組元素的指針可以執(zhí)行所有迭代器運(yùn)算。這些運(yùn)算,包括
- 解引用、
- 遞增、
- 比較、
- 與整數(shù)相加、
- 兩個指針相減
- …
用在指針和用在迭代器上意義完全一致。
與整數(shù)相加
給(從)一個指針加上(減去)某整數(shù)值,結(jié)果仍是指針。新指針指向的元素與原來
的指針相比前進(jìn)了(后退了)該整數(shù)值個位置:
ip加上4所得的結(jié)果仍是一個指針,該指針?biāo)傅脑嘏cip原來所指的元素相比前進(jìn)了4個位置。
給指針加上一個整數(shù),得到的新指針仍需指向同一數(shù)組的其他元素,或者指向同一數(shù)組的尾元素的下一位置(如果計算所得的指針超出了上述范圍就將產(chǎn)生錯誤,而且這種錯誤編譯器一般發(fā)現(xiàn)不了。):
// ok: arr is converted to a pointer to its first element; p points one past the end of arr int *p = arr + sz; // use caution -- do not dereference!//指向同一數(shù)組的尾元素的下一位置 int *p2 = arr + 10; // error: arr has only 5 elements; p2 has undefined value當(dāng)給arr加上sz時,編譯器自動地將arr轉(zhuǎn)換成指向數(shù)組 arr中首元素的指針。執(zhí)行加法后,指針從首元素開始向前移動了sz(這里是5)個位置,指向新位置的元素。也就是說,它指向了數(shù)組arr尾元素的下一位置。
兩個指針相減
和迭代器一樣,兩個指針相減的結(jié)果是它們之間的距離。參與運(yùn)算的兩個指針必須指向同一個數(shù)組當(dāng)中的元素:
auto n = end(arr) - begin(arr); // n is 5, the number of elements in arr兩個指針相減的結(jié)果的類型是一種名為ptrdiff_t 的標(biāo)準(zhǔn)庫類型,和 size_t一樣,ptrdiff_t 也是一種定義在 cstddef頭文件中的機(jī)器相關(guān)的類型。因?yàn)椴钪悼赡転樨?fù)值,所以ptrdiff_t是一種帶符號類型。
只要兩個指針指向同一個數(shù)組的元素,或者指向該數(shù)組的尾元素的下一位置,就能利用關(guān)系運(yùn)算符對其進(jìn)行比較。例如,可以按照如下的方式遍歷數(shù)組中的元素:
int *b = arr, *e = arr + sz; while (b < e) {// use *b++b; }如果兩個指針分別指向不相關(guān)的對象,則不能比較它們:
int i = 0, sz = 42; int *p = &i, *e = &sz; // undefined: p and e are unrelated; comparison is meaningless! while (p < e)盡管作用可能不是特別明顯,但必須說明的是,上述指針運(yùn)算同樣適用于空指針和所指對象并非數(shù)組的指針。在后一種情況下,兩個指針必須指向同一個對象或該對象的下一位置。如果p 是空指針,允許給 p加上或減去一個值為o的整型常量表達(dá)式。兩個空指針也允許彼此相減,結(jié)果當(dāng)然是0。
解引用和指針運(yùn)算的交互
指針加上一個整數(shù)所得的結(jié)果還是一個指針。假設(shè)結(jié)果指針指向了一個元素,則允許解引用該結(jié)果指針:
int ia[] = {0,2,4,6,8}; // array with 5 elements of type int int last = *(ia + 4); // ok: initializes last to 8, the value of ia[4]表達(dá)式* (ia+4)計算ia前進(jìn)4個元素后的新地址,解引用該結(jié)果指針的效果等價于表達(dá)式ia[ 4 ]。
回憶一下先前介紹過如果表達(dá)式含有解引用運(yùn)算符和點(diǎn)運(yùn)算符,最好在必要的地方加上圓括號。類似的,此例中指針加法的圓括號也不可缺少。如果寫成下面的形式:
last = *ia + 4; // ok: last = 4, equivalent to ia[0] + 4含義就與之前完全不同了,此時先解引用ia,然后給解引用的結(jié)果再加上4。
下標(biāo)和指針
如前所述,在很多情況下使用數(shù)組的名字其實(shí)用的是一個指向數(shù)組首元素的指針。一個典型的例子是當(dāng)對數(shù)組使用下標(biāo)運(yùn)算符時,編譯器會自動執(zhí)行上述轉(zhuǎn)換操作。給定
int ia[] = {0,2,4,6,8}; // array with 5 elements of type int此時,ia[0]是一個使用了數(shù)組名字的表達(dá)式,對數(shù)組執(zhí)行下標(biāo)運(yùn)算其實(shí)是對指向數(shù)組元素的指針執(zhí)行下標(biāo)運(yùn)算:
int i = ia[2]; // ia is converted to a pointer to the first element in ia // ia[2] fetches the element to which (ia + 2) points int *p = ia; // p points to the first element in ia i = *(p + 2); // equivalent to i = ia[2]只要指針指向的是數(shù)組中的元素(或者數(shù)組中尾元素的下一位置),都可以執(zhí)行下標(biāo)運(yùn)算:
int *p = &ia[2]; // p points to the element indexed by 2 int j = p[1]; // p[1] is equivalent to *(p + 1), // p[1] is the same element as ia[3] int k = p[-2]; // p[-2] is the same element as ia[0]雖然標(biāo)準(zhǔn)庫類型string和vector也能執(zhí)行下標(biāo)運(yùn)算,但是數(shù)組與它們相比還是有所不同。
-
標(biāo)準(zhǔn)庫類型限定使用的下標(biāo)必須是無符號類型,而內(nèi)置的下標(biāo)運(yùn)算無此要求,上面的最后一個例子很好地說明了這一點(diǎn)。
-
內(nèi)置的下標(biāo)運(yùn)算符可以處理負(fù)值,當(dāng)然,結(jié)果地址必須指向原來的指針?biāo)竿粩?shù)組中的元素(或是同一數(shù)組尾元素的下一位置)。
內(nèi)置的下標(biāo)運(yùn)算符所用的索引值不是無符號類型,這一點(diǎn)與vector和string不一樣。
C風(fēng)格字符串
盡管C++支持C風(fēng)格字符串,但在C++程序中最好還是不要使用它們。這是因?yàn)镃風(fēng)格字符串不僅使用起來不太方便,而且極易引發(fā)程序漏洞,是諸多安全問題的根本原因。
字符串字面值是一種通用結(jié)構(gòu)的實(shí)例,這種結(jié)構(gòu)即是C++由C繼承而來的C風(fēng)格字符串(C-style character string)。C風(fēng)格字符串不是一種類型,而是為了表達(dá)和使用字符串而形成的一種約定俗成的寫法。按此習(xí)慣書寫的字符串存放在字符數(shù)組中并以空字符結(jié)束(null terminated)。以空字符結(jié)束的意思是在字符串最后一個字符后面跟著一個空字符(’\0‘)。一般利用指針來操作這些字符串。
C標(biāo)準(zhǔn)庫String函數(shù)
下表列舉了C語言標(biāo)準(zhǔn)庫提供的一組函數(shù),這些函數(shù)可用于操作C風(fēng)格字符串,它們定義在cstring頭文件中,cstring是C語言頭文件string.h的C++版本。
| strlen§ | 返回p的長度,空字符不計算在內(nèi) |
| strcmp(pi, p2) | 比較p1和p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一個正值;如果pl<p2,返回一個負(fù)值 |
| strcat(pl, p2) | 將p2附加到p1之后,返回p1 |
| strcpy(p1, p2) | 將p2拷貝給p1,返回p1 |
所列的函數(shù)不負(fù)責(zé)驗(yàn)證其字符串參數(shù)。
傳入此類函數(shù)的指針必須指向以空字符作為結(jié)束的數(shù)組:
char ca[] = {'C', '+', '+'}; // not null terminated cout << strlen(ca) << endl; // disaster: ca isn't null terminated此例中,ca雖然也是一個字符數(shù)組但它不是以空字符作為結(jié)束的,因此上述程序?qū)a(chǎn)生未定義的結(jié)果。strlen函數(shù)將有可能沿著ca在內(nèi)存中的位置不斷向前尋找,直到遇到空字符才停下來。
比較字符串
比較兩個C風(fēng)格字符串的方法和之前學(xué)習(xí)過的比較標(biāo)準(zhǔn)庫string對象的方法大相徑庭。比較標(biāo)準(zhǔn)庫string對象的時候,用的是普通的關(guān)系運(yùn)算符和相等性運(yùn)算符:
string s1 = "A string example"; string s2 = "A different string"; if (s1 < s2) // false: s2 is less than s1如果把這些運(yùn)算符用在兩個C風(fēng)格字符串上,實(shí)際比較的將是指針而非字符串本身:
const char ca1[] = "A string example"; const char ca2[] = "A different string"; if (ca1 < ca2) // undefined: compares two unrelated addresses謹(jǐn)記之前介紹過的,當(dāng)使用數(shù)組的時候其實(shí)真正用的是指向數(shù)組首元素的指針。因此,上面的if條件實(shí)際上比較的是兩個const char*的值。這兩個指針指向的并非同一對象,所以將得到未定義的結(jié)果。
要想比較兩個C風(fēng)格字符串需要調(diào)用strcmp函數(shù),此時比較的就不再是指針了。如果兩個字符串相等,strcmp返回0;如果前面的字符串較大,返回正值;如果后面的字符串較大,返回負(fù)值:
if (strcmp(ca1, ca2) < 0) // same effect as string comparison s1 < s2目標(biāo)字符串的大小由調(diào)用者指定
連接或拷貝C風(fēng)格字符串也與標(biāo)準(zhǔn)庫string對象的同類操作差別很大。例如,要想把剛剛定義的那兩個string對象s1和 s2連接起來,可以直接寫成下面的形式:
// initialize largeStr as a concatenation of s1, a space, and s2 string largeStr = s1 + " " + s2;同樣的操作如果放到cal和 ca2這兩個數(shù)組身上就會產(chǎn)生錯誤了。表達(dá)式cal + ca2試圖將兩個指針相加,顯然這樣的操作沒什么意義,也肯定是非法的。
正確的方法是使用strcat函數(shù)和strcpy函數(shù)。不過要想使用這兩個函數(shù),還必須提供一個用于存放結(jié)果字符串的數(shù)組,該數(shù)組必須足夠大以便容納下結(jié)果字符串及末尾的空字符。下面的代碼雖然很常見,但是充滿了安全風(fēng)險,極易引發(fā)嚴(yán)重錯誤:
// disastrous if we miscalculated the size of largeStr strcpy(largeStr, ca1); // copies ca1 into largeStr strcat(largeStr, " "); // adds a space at the end of largeStr strcat(largeStr, ca2); // concatenates ca2 onto largeStr一個潛在的問題是,我們在估算 largestr所需的空間時不容易估準(zhǔn),而且 largestr所存的內(nèi)容一旦改變,就必須重新檢查其空間是否足夠。不幸的是,這樣的代碼到處都是,程序員根本沒法照顧周全。這類代碼充滿了風(fēng)險而且經(jīng)常導(dǎo)致嚴(yán)重的安全泄漏。
對大多數(shù)應(yīng)用來說,使用標(biāo)準(zhǔn)庫string要比使用C風(fēng)格字符串更安全、更高效。
(Note:放心去用標(biāo)準(zhǔn)庫string)
與舊代碼的接口
很多C++程序在標(biāo)準(zhǔn)庫出現(xiàn)之前就已經(jīng)寫成了,它們肯定沒用到string和 vector類型。而且,有一些C+程序?qū)嶋H上是與C語言或其他語言的接口程序,當(dāng)然也無法使用C++標(biāo)準(zhǔn)庫。因此,現(xiàn)代的C++程序不得不與那些充滿了數(shù)組和/或C風(fēng)格字符串的代碼銜接,為了使這一工作簡單易行,C++專門提供了一組功能。
混用string對象和C風(fēng)格字符串
前文介紹過允許使用字符串字面值來初始化string對象:
string s("Hello World"); // s holds Hello World更一般的情況是,任何出現(xiàn)字符串字面值的地方都可以用以空字符結(jié)束的字符數(shù)組來替代:
-
允許使用以空字符結(jié)束的字符數(shù)組來初始化string對象或?yàn)?string對象賦值。
-
在string 對象的加法運(yùn)算中允許使用以空字符結(jié)束的字符數(shù)組作為其中一個運(yùn)算對象(不能兩個運(yùn)算對象都是);
-
在string對象的復(fù)合賦值運(yùn)算中允許使用以空字符結(jié)束的字符數(shù)組作為右側(cè)的運(yùn)算對象。
上述性質(zhì)反過來就不成立了:如果程序的某處需要一個C風(fēng)格字符串,無法直接用string對象來代替它。例如,不能用string對象直接初始化指向字符的指針。
為了完成該功能,string專門提供了一個名為c_str的成員函數(shù):
char *str = s; // error: can't initialize a char* from a string const char *str = s.c_str(); // ok顧名思義,c_str函數(shù)的返回值是一個C風(fēng)格的字符串。也就是說,函數(shù)的返回結(jié)果是一個指針,該指針指向一個以空字符結(jié)束的字符數(shù)組,而這個數(shù)組所存的數(shù)據(jù)恰好與那個string對象的一樣。結(jié)果指針的類型是const char*,從而確保我們不會改變字符數(shù)組的內(nèi)容。
我們無法保證c_str函數(shù)返回的數(shù)組一直有效,事實(shí)上,如果后續(xù)的操作改變了s的值就可能讓之前返回的數(shù)組失去效用。
如果執(zhí)行完c_str()函數(shù)后程序想一直都能使用其返回的數(shù)組,最好將該數(shù)組重新拷貝一份。
使用數(shù)組初始化vector對象
前文介紹過不允許使用一個數(shù)組為另一個內(nèi)置類型的數(shù)組賦初值,也不允許使用vector對象初始化數(shù)組。相反的,允許使用數(shù)組來初始化vector對象。要實(shí)現(xiàn)這一目的,只需指明要拷貝區(qū)域的首元素地址和尾后地址就可以了:
int int_arr[] = {0, 1, 2, 3, 4, 5}; // ivec has six elements; each is a copy of the corresponding element in int_arr vector<int> ivec(begin(int_arr), end(int_arr));在上述代碼中,用于創(chuàng)建ivec的兩個指針實(shí)際上指明了用來初始化的值在數(shù)組int_arr中的位置,其中第二個指針應(yīng)指向待拷貝區(qū)域尾元素的下一位置。此例中,使用標(biāo)準(zhǔn)庫函數(shù)begin和 end來分別計算int_arr 的首指針和尾后指針。在最終的結(jié)果中,ivec將包含6個元素,它們的次序和值都與數(shù)組int_arr完全一樣。
用于初始化 vector對象的值也可能僅是數(shù)組的一部分:
// copies three elements: int_arr[1], int_arr[2], int_arr[3] vector<int> subVec(int_arr + 1, int_arr + 4);這條初始化語句用3個元素創(chuàng)建了對象subvec,3個元素的值分別來自int_arr[1]、int_arr[2]和 int_arr[3]。
建議:盡量使用標(biāo)準(zhǔn)庫類型而非數(shù)組
使用指針和數(shù)組很容易出錯。一部分原因是概念上的問題:指針常用于底層操作,因此容易引發(fā)一些與煩瑣細(xì)節(jié)有關(guān)的錯誤。其他問題則源于語法錯誤,特別是聲明指針時的語法錯誤。
現(xiàn)代的C++程序應(yīng)當(dāng)盡量使用vector和迭代器,避免使用內(nèi)置數(shù)組和指針;應(yīng)該盡量使用string,避免使用C風(fēng)格的基于數(shù)組的字符串。
多維數(shù)組
嚴(yán)格來說,C++語言中沒有多維數(shù)組,通常所說的多維數(shù)組其實(shí)是數(shù)組的數(shù)組。謹(jǐn)記這一點(diǎn),對今后理解和使用多維數(shù)組大有益處。
當(dāng)一個數(shù)組的元素仍然是數(shù)組時,通常使用兩個維度來定義它:一個維度表示數(shù)組本身大小,另外一個維度表示其元素(也是數(shù)組)大小:
int ia[3][4]; // array of size 3; each element is an array of ints of size 4 // array of size 10; each element is a 20-element array whose elements are arrays of 30 ints int arr[10][20][30] = {0}; // initialize all elements to 0如前文所介紹的,按照由內(nèi)而外的順序閱讀此類定義有助于更好地理解其真實(shí)含義。
在第一條語句中,我們定義的名字是ia,顯然ia是一個含有3個元素的數(shù)組。接著觀察右邊發(fā)現(xiàn),ia的元素也有自己的維度,所以ia的元素本身又都是含有4個元素的數(shù)組。再觀察左邊知道,真正存儲的元素是整數(shù)。因此最后可以明確第一條語句的含義:它定義了一個大小為3的數(shù)組,該數(shù)組的每個元素都是含有4個整數(shù)的數(shù)組。
使用同樣的方式理解arr的定義。首先arr是一個大小為10的數(shù)組,它的每個元素都是大小為20的數(shù)組,這些數(shù)組的元素又都是含有30個整數(shù)的數(shù)組。
實(shí)際上,定義數(shù)組時對下標(biāo)運(yùn)算符的數(shù)量并沒有限制,因此只要愿意就可以定義這樣一個數(shù)組:它的元素還是數(shù)組,下一級數(shù)組的元素還是數(shù)組,再下一級數(shù)組的元素還是數(shù)組,以此類推。
對于二維數(shù)組來說,常把第一個維度稱作行,第二個維度稱作列。
多維數(shù)組的初始化
允許使用花括號括起來的一組值初始化多維數(shù)組,這點(diǎn)和普通的數(shù)組一樣。下面的初始化形式中,多維數(shù)組的每一行分別用花括號括了起來:
int ia[3][4] = { // three elements; each element is an array of size 4{0, 1, 2, 3}, // initializers for the row indexed by 0{4, 5, 6, 7}, // initializers for the row indexed by 1{8, 9, 10, 11} // initializers for the row indexed by 2 };其中內(nèi)層嵌套著的花括號并非必需的,例如下面的初始化語句,形式上更為簡潔,完成的功能和上面這段代碼完全一樣:
// equivalent initialization without the optional nested braces for each row int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};類似于一維數(shù)組,在初始化多維數(shù)組時也并非所有元素的值都必須包含在初始化列表之內(nèi)。如果僅僅想初始化每一行的第一個元素,通過如下的語句即可:
// explicitly initialize only element 0 in each row int ia[3][4] = {{ 0 }, { 4 }, { 8 }};其他未列出的元素執(zhí)行默認(rèn)值初始化,這個過程和一維數(shù)組一樣。在這種情況下如果再省略掉內(nèi)層的花括號,結(jié)果就大不一樣了。下面的代碼
// explicitly initialize row 0; the remaining elements are value initialized int ix[3][4] = {0, 3, 6, 9};含義發(fā)生了變化,它初始化的是第一行的4個元素,其他元素被初始化為0。
多維數(shù)組的下標(biāo)引用
可以使用下標(biāo)運(yùn)算符來訪問多維數(shù)組的元素,此時數(shù)組的每個維度對應(yīng)一個下標(biāo)運(yùn)算符。
如果表達(dá)式含有的下標(biāo)運(yùn)算符數(shù)量和數(shù)組的維度一樣多,該表達(dá)式的結(jié)果將是給定類型的元素;反之,如果表達(dá)式含有的下標(biāo)運(yùn)算符數(shù)量比數(shù)組的維度小,則表達(dá)式的結(jié)果將是給定索引處的一個內(nèi)層數(shù)組:
// assigns the first element of arr to the last element in the last row of ia ia[2][3] = arr[0][0][0];//int arr[10][20][30] = {0}; int (&row)[4] = ia[1]; // binds row to the second four-element array in ia在第一個例子中,對于用到的兩個數(shù)組來說,表達(dá)式提供的下標(biāo)運(yùn)算符數(shù)量都和它們各自的維度相同。在等號左側(cè),ia[2]得到數(shù)組ia的最后一行,此時返回的是表示ia最后一行的那個一維數(shù)組而非任何實(shí)際元素;對這個一維數(shù)組再取下標(biāo),得到編號為[3]的元素,也就是這一行的最后一個元素。
類似的,等號右側(cè)的運(yùn)算對象包含3個維度。首先通過索引0得到最外層的數(shù)組,它是一個大小為20的(多維)數(shù)組;接著獲取這20個元素數(shù)組的第一個元素,得到一個大小為30的一維數(shù)組;最后再取出其中的第一個元素。
在第二個例子中,把 row定義成一個含有4個整數(shù)的數(shù)組的引用,然后將其綁定到ia的第2行。
再舉一個例子,程序中經(jīng)常會用到兩層嵌套的for循環(huán)來處理多維數(shù)組的元素:
constexpr size_t rowCnt = 3, colCnt = 4; int ia[rowCnt][colCnt]; // 12 uninitialized elements // for each row for (size_t i = 0; i != rowCnt; ++i) {// for each column within the rowfor (size_t j = 0; j != colCnt; ++j) {// assign the element's positional index as its valueia[i][j] = i * colCnt + j;} }外層的for循環(huán)遍歷ia的所有元素,注意這里的元素是一維數(shù)組;內(nèi)層的for循環(huán)則遍歷那些一維數(shù)組的整數(shù)元素。此例中,我們將元素的值設(shè)為該元素在整個數(shù)組中的序號。
使用范圍for語句處理多維數(shù)組
由于在C++11新標(biāo)準(zhǔn)中新增了范圍for語句,所以前一個程序可以簡化為如下形式:
size_t cnt = 0; for (auto &row : ia) // for every element in the outer arrayfor (auto &col : row) { // for every element in the inner arraycol = cnt; // give this element the next value++cnt; // increment cnt}這個循環(huán)賦給ia元素的值和之前那個循環(huán)是完全相同的,區(qū)別之處是通過使用范圍for語句把管理數(shù)組索引的任務(wù)交給了系統(tǒng)來完成。因?yàn)橐淖冊氐闹?#xff0c;所以得把控制變量row和 col聲明成引用類型。
-
第一個for循環(huán)遍歷ia的所有元素,這些元素是大小為4的數(shù)組,因此row的類型就應(yīng)該是含有4個整數(shù)的數(shù)組的引用。
-
第二個for循環(huán)遍歷那些4元素數(shù)組中的某一個,因此 col的類型是整數(shù)的引用。每次迭代把cnt的值賦給ia的當(dāng)前元素,然后將cnt加1。
在上面的例子中,因?yàn)橐淖償?shù)組元素的值,所以我們選用引用類型作為循環(huán)控制變量,但其實(shí)還有一個深層次的原因促使我們這么做。
舉一個例子,考慮如下的循環(huán):
for (const auto &row : ia) // for every element in the outer arrayfor (auto col : row) // for every element in the inner arraycout << col << endl;這個循環(huán)中并沒有任何寫操作,可是我們還是將外層循環(huán)的控制變量聲明成了引用類型,這是為了避免數(shù)組被自動轉(zhuǎn)成指針。
假設(shè)不用引用類型,則循環(huán)如下述形式:
for (auto row : ia)for (auto col : row)程序?qū)o法通過編譯。這是因?yàn)?#xff0c;像之前一樣第一個循環(huán)遍歷ia的所有元素,注意這些元素實(shí)際上是大小為4的數(shù)組。因?yàn)閞ow不是引用類型,所以編譯器初始化row時會自動將這些數(shù)組形式的元素(和其他類型的數(shù)組一樣)轉(zhuǎn)換成指向該數(shù)組內(nèi)首元素的指針。這樣得到的row 的類型就是 int*,顯然內(nèi)層的循環(huán)就不合法了,編譯器將試圖在一個int*內(nèi)遍歷,這顯然和程序的初衷相去甚遠(yuǎn)。
要使用范圍 for語句處理多維數(shù)組,除了最內(nèi)層的循環(huán)外,其他所有循環(huán)的控制變量都應(yīng)該是引用類型。
指針和多維數(shù)組
當(dāng)程序使用多維數(shù)組的名字時,也會自動將其轉(zhuǎn)換成指向數(shù)組首元素的指針。
定義指向多維數(shù)組的指針時,千萬別忘了這個多維數(shù)組實(shí)際上是數(shù)組的數(shù)組。
因?yàn)槎嗑S數(shù)組實(shí)際上是數(shù)組的數(shù)組,所以由多維數(shù)組名轉(zhuǎn)換得來的指針實(shí)際上是指向第一個內(nèi)層數(shù)組的指針:
int ia[3][4]; // array of size 3; each element is an array of ints of size 4 int (*p)[4] = ia; // p points to an array of four ints//ia的第一行 p = &ia[2]; // p now points to the last element in ia//ia的第三行我們首先明確(*p)意味著p是一個指針。接著觀察右邊發(fā)現(xiàn),指針p所指的是一個維度為4的數(shù)組;再觀察左邊知道,數(shù)組中的元素是整數(shù)。因此,p就是指向含有4個整數(shù)的數(shù)組的指針。
在上述聲明中,圓括號必不可少:
int *ip[4]; // array of pointers to int int (*ip)[4]; // pointer to an array of four ints隨著C++11新標(biāo)準(zhǔn)的提出,通過使用auto或者decltype就能盡可能地避免在數(shù)組前面加上一個指針類型了:
// print the value of each element in ia, with each inner array on its own line // p points to an array of four ints for (auto p = ia; p != ia + 3; ++p) { // q points to the first element of an array of four ints; that is, q points to an intfor (auto q = *p; q != *p + 4; ++q)cout << *q << ' ';cout << endl; }使用標(biāo)準(zhǔn)庫函數(shù)begin和end也能實(shí)現(xiàn)同樣的功能,而且看起來更簡潔一些:
#include <iterator>// p points to the first array in ia for (auto p = begin(ia); p != end(ia); ++p) {// q points to the first element in an inner arrayfor (auto q = begin(*p); q != end(*p); ++q)cout << *q << ' '; // prints the int value to which q pointscout << endl; }類別別名簡化多維數(shù)組的指針
讀、寫和理解一個指向多維數(shù)組的指針是一個讓人不勝其煩的工作,使用類型別名能讓這項工作變得簡單一點(diǎn)兒,例如:
using int_array = int[4]; // new style type alias declaration; typedef int int_array[4]; // equivalent typedef declaration; // print the value of each element in ia, with each inner array on its own line for (int_array *p = ia; p != ia + 3; ++p) {for (int *q = *p; q != *p + 4; ++q)cout << *q << ' ';cout << endl; }程序?qū)㈩愋汀?個整數(shù)組成的數(shù)組”命名為 int_array,用類型名int_array 定義外層循環(huán)的控制變量讓程序顯得簡潔明了。
一些術(shù)語
緩沖區(qū)溢出(buffer overflow)一種嚴(yán)重的程序故障,主要的原因是試圖通過一個越界的索引訪問容器內(nèi)容,容器類型包括string、vector和數(shù)組等。
類模板(class template)用于創(chuàng)建具體類類型的模板。要想使用類模板,必須提供關(guān)于類型的輔助信息。例如,要定義一個vector對象需要指定元素的類型:vector<int>包含int類型的元素。
拷貝初始化 (copy initialization) 使用賦值號(=)的初始化形式。新創(chuàng)建的對象是初始值的一個副本。
直接初始化(direct initialization)不使用賦值號(=)的初始化形式。
實(shí)例化(instantiation)編譯器生成一個指定的模板類或函數(shù)的過程。
值初始化(value initialization)是一種初始化過程。內(nèi)置類型初始化為0,類類型由類的默認(rèn)構(gòu)造函數(shù)初始化。只有當(dāng)類包含默認(rèn)構(gòu)造函數(shù)時,該類的對象才會被值初始化。對于容器的初始化來說,如果只說明了容器的大小而沒有指定初始值的話,就會執(zhí)行值初始化。此時編譯器會生成一個值,而容器的元素被初始化為該值。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(3 / 19):字符串、向量、迭代器和数组的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Ultra Librarian 生成
- 下一篇: 《C++ Primer 5th》笔记(4