1. 程式人生 > >C++ Primer學習筆記(2)

C++ Primer學習筆記(2)

容器和演算法

順序容器的操作

容器元素的初始化:

C<T> c;   //建立一個名為c的空容器。C是容器型別名,T是元素型別,適用於所有容器

C<T> c2(c);//建立容器c的副本,c2和c必須具有相同的容器型別,並存放相同型別的元素。適用於所有容器

C<T> c(b,e);//建立c,其元素是迭代器b和e表示的範圍內元素的副本。適用於所有容器

C<T> c(n,t);//用n個值為t的元素建立容器c,其中t必須是容器型別c的元素型別的值,或者可轉換為該型別的值。只適用於順序容器

C<T> c(n); //建立有n個值初始化元素的容器c。只適用於順序容器

vector和deque型別迭代器支援的操作:

iter+n; iter-n; iter+=iter;  iter-=iter;  iter-iter; <=, <, >=, >

關係操作符只適用於vector和deque容器,為其提供隨即快速的訪問元素。

list容器的迭代器既不支援算術運算(加法或減法),也不支援關係運算(<=,<,>=,>)它只提供前置和後置的自增自減及相等(不等)運算。

在順序容器中新增元素操作:

c.push_back(t);在容器尾部新增值為t的元素,返回void型別。

c.push_front(t);在容器前端新增值為t的元素,返回void型別。只適用於list,deque

c.insert(p,t);    在迭代器p所指向的元素前面插入t元素,返回指向新元素的迭代器。等效於push_front函式。

c.insert(p,n,t);  在迭代器p所指向的元素前面插入n個值為t的元素,返回void型別。

c.insert(p,b,e);  在迭代器p所指向的元素前面插入由迭代器b和e表決範圍內的元素。

容器元素都是副本。在新增元素時,系統是將元素值賦值到容器中,容器元素的改變不影響被複制的原值。

避免儲存end操作返回的迭代器。新增或刪除deque或vector容器內的元素都會導致儲存的迭代器失效。

訪問順序容器內元素的操作:

c.back()   返回容器c的最後一個元素的引用。如果c為空,則該操作未定義。

c.front()  返回容器c的第一個元素的引用。如果c為空,則該操作未定義。

c[n]        返回下標為n的元素的引用。如果n<0或n>c.size(),則該操作未定義,只適用於deque和vector容器。

c.at(n)     返回下標為n的元素的引用,如果小標越界,則該操作未定義,只適用於deque和vector容器。

刪除順序容器內元素的操作:

c.erase(p)              刪除迭代器p所指向的元素。返回一個迭代器,指向被刪除元素後面的元素。如果p本身是指向超出末端的下一個位置的迭代器,則該函式未定義

c.erase(b,e)     刪除迭代器b和e所標記範圍內的所有元素。

c.clear()         刪除c內的所有元素,返回void

c.pop_back()   刪除c的最後一個元素,返回void。如果c為空,則該函式未定義

c. pop_back() 刪除c的第一個元素,返回void。如果c為空,則該函式未定義。只適用於list或deque容器

erase,pop_back,pop_back函式是指向被刪除元素的所有迭代器失效。對於vector容器,指向刪除點後面的元素的迭代器通常也會失效。而對於deque容器,如果刪除時不包含第一個元素或最後一個元素,那麼該deque容器相關的所有迭代器都會失效。

賦值與swap

c1 = c2    型別必須相同。

c1 =swap(c2) 交換的速度比賦值快。

c.assign(b,e)   重新設定c的元素:迭代器b,e必須不是指向c中元素的迭代器。可用於不同(或相同)型別的容器,但元素型別不同但相互相容(或相同)中

c.assign(n,t)    將容器c重新設定為儲存n個值為t的元素。

與複製相關的操作都作用於整個容器。除swap操作外,其他操作都可以用erase和insert操作實現。複製後,左右兩邊的容器相等:儘管複製前後兩個容器的長度可能不相等。

通常來說,除非找到選擇使用其他容器的更好理由,否則vector容器都是最佳選擇。

選擇容器型別法則(考慮插入/刪除操作和訪問操作哪個使用得多?):

(1)程式要求隨機訪問元素,則使用vector或deque容器。

(2)程式必須在容器的中間位置插入或刪除元素,則採用list容器。

(3)程式在容器首部或尾部插入或刪除元素,則採用deque容器。

(4)如果只需在讀取輸入是在容器的中間位置插入元素,然後需要隨機訪問元素,則可考慮在輸入時將元素讀入到一個list容器,接著對此容器重新排序,使其適合順序訪問,然後將排序後的list容器複製到一個vector容器。

關聯容器(map,set,multimap,multiset)

pair型別:

multimap:一個鍵對應多個值,同一個鍵所關聯的元素必然相鄰存放,不可以通過下標訪問。multimap訪問元素方法:

(1)使用count和find操作。count計數,find返回指向第一個擁有正在查詢的鍵:

string search_item("C++primer");

typedefmultimap<string,string>::size_type sz_type;

sz_type entries=authors.count(search_item);

multimap<string,string>::iteratoriter=authors.find(search_item);

for(sz_typecnt=0;cnt!=entries;++cnt,++iter)

{

       cout<<iter->second<<endl;

}

(2)與眾不同的迭代器的解決方案

m.lower_bound(k);  返回一個迭代器,指向鍵不小於k的第一個元素

m.upper_bound(k); 返回一個迭代器,指向鍵大於k的第一個元素

m.equal_range(k);   返回一個迭代器的pair物件,他的first成員等價於m.lower_bound(k)。second成員等價於m. upper_bound(k)。

泛型演算法

演算法不直接修改容器的大小。如果需要新增或刪除元素,則必須使用容器操作。

在類內部定義的成員函式預設為inline,在類外部定義的成員函式需指明類的作用域。

設計類的介面時,設計者應該考慮的是如何方便類的使用;使用類的時候,設計者就不應該考慮類如何工作。

在宣告和定義出指定inline都是合法的。在類外部定義inline使類容易閱讀。

類的宣告和定義:類宣告是不完全型別,只能以有限方式使用,不能定義該型別的物件,只能用於定義指向該型別的指標及引用,或者宣告使用該型別作為形參型別或返回型別的函式。類定義,一旦類被定義,我們就可以知道所有類的成員,以及儲存該類的物件所需的儲存空間。在建立類的物件之前或者使用引用或指標訪問類的成員之前必須定義類。

類物件:定義一個類時,也就定義了一個型別。一旦定義了類,就可以定義該型別的物件。定義物件時,將為其分配足以容納該物件的記憶體。(定義類型別時不分配記憶體,定義類物件時分配記憶體)每個物件具有自己的型別資料成員的副本。

定義類型別的物件:

(1)類名字 類物件名;如:Sales_item item1;

(2)class(struct)類名字 類物件名;class Sales_item item2;

為什麼類的定義一分號結束?:分號是必需的,因為在類定義之後可以接一個物件定義列表(應避免這麼做)。

在普通的const成員函式中,this的型別是一個指向類型別物件的const指標。可以改變this所指向的值,但不能改變this所儲存的地址。在const成員函式中,this的型別是一個指向const類型別物件的const指標。既不能改變this所指向的物件,也不能改變this所儲存的地址。不能從const成員函式返回指向類物件的普通引用。const成員函式只能返回*this作為一個const引用。

基於成員函式是否為const,可以過載一個成員函式;同樣的,基於一個指標形參是否指向const,可以過載一個函式。const物件只能使用const成員。非cosnt物件可以使用任一成員,但最好也使用非cosnt成員。

可變資料成員:將資料成員宣告為mutable來實現,可修改類的資料成員(甚至在const成員函式內)。可變資料成員永遠都不能為const,甚至當它是const物件的成員時也如此。

建構函式不能宣告為const。const建構函式是不必要的。建立類型別的const物件時,執行一個普通建構函式來初始化該const物件。建構函式的工作是初始化物件。不管物件是否為const,都用一個建構函式來初始化該物件。

建構函式初始化

有些成員必須在初始化列表中進行初始化。對這樣的成員,在建構函式體中對它們賦值不起作用。沒有預設建構函式的類型別的成員了,以及const或引用型別的成員,都必須在建構函式初始化列表中進行初始化。對非類型別的資料成員進行賦值或使用初始化在結果和效能上都是等價的。可以初始化const物件或引用型別的物件,但不能對他們賦值,在開始執行建構函式的函式體之前要完成初始化。當一個類中自己定義了建構函式時,編譯器就不會自動合成一個預設構造函數了。

防止由建構函式定義的隱式轉換:顯示地宣告建構函式—explicit關鍵字。

string型別的物件可以隱式地轉換成臨時的類物件。

stringnull_isbn=”9-999-9999-9”;

Sales_itemnull(null_isbn);//使用string的物件null_isbn為實參,呼叫Sales_item類的建構函式建立Sales_item 物件 null1。

Sales_itemnull(”9-999-9999-9”);// 使用接受一個 C 風格字串形參的 string 類的建構函式,生成一個臨時string 物件,然後用這個臨時物件作為實參,呼叫Sales_item 類的建構函式來建立Sales_item 類的物件 null。

static類成員

sttic資料成員獨立於該類的任意物件:每個static資料成員是與類關聯的物件,而不與該類的物件相關聯

static成員函式沒有this形參,它可以直接訪問所屬類的static成員,但不能直接使用非static成員。

使用static成員而不是全域性物件的三個優點:

(1)static成員的名字是在作用域中,因此可以避免與其他類的成員或全域性物件名字的衝突。

(2)可以實施封裝。static成員可以是私有成員,而全域性物件不可以。

(3)通過閱讀程式容易看出static成員是與特定類關聯的。

static成員定義

在類內部宣告時加static關鍵字,在類外定義時不用static。static成員遵循正常的公有/私有訪問規則。

static成員不是任何物件的組成部分,所以static成員函式不能宣告為const。static成員函式也不能被宣告為虛擬函式。static資料成員可以宣告為任意型別,包括常量、引用、陣列等。

static資料成員必須在類定義體的外部定義。static成員不是通過類建構函式進行初始化,而是應該在定義時進行初始化。

//base.h

classBase

{

public:

       static double rate(){return interestRate;}

       static void rate(double);

private:

       std::stringowner;

       static doubleinterestRate;

       static doubleinitRate();

};

//base.cpp

doubleBase::interestRate=initRate();

voidBase::rate(double newRate)

{

       interestRate= newRate;

}

static資料成員的型別可以是該成員所屬的類型別。非static成員被限定宣告為其自身類物件的指標或引用。static資料成員可用作預設實參傳遞,而非static資料成員不行,因為它的值不能脫離物件而使用。

classBase

{

public:

       Base():val(ival){}

private:

       static Base base1;//ok

       Base*Base2;//ok

       //Base base3;//error

       static const int ival=0;

       int val;

};

複製控制(複製建構函式、賦值操作符、解構函式)

兩種情況下必須定義複製建構函式:類中有一個經常使用得資料成員指標;有成員表示在建構函式中分配的其他資源。而另一些類在建立新物件時必須做一些特定的工作。

禁止複製:顯示宣告其複製建構函式為private。然後類的友元和成員仍可以進行復制。如果想要禁止友元和成員的複製,可以宣告一個private複製建構函式但不對其定義。

賦值操作符

在需要定義複製建構函式時,也需要定義賦值操作符,即如果一個類(1)類中包含指標型資料成員,(2)或者在進行賦值操作時需要做一些特定工作,則該類需要定義賦值操作符。

如果一個類需要解構函式,則它也需要賦值操作符和複製建構函式。--三法則

解構函式用於類的物件超出作用域時釋放物件所獲取的資源,或刪除指向動態分配物件的指標。

合成解構函式的作用:(1)按物件建立時的逆序撤銷每個非 static 成員,(2)對於類型別的成員,合成解構函式呼叫該成員的解構函式來撤銷物件。

編譯器總會為每個類合成一個解構函式。當(1)需要釋放指標成員的資源時,(2)需要執行某些特定工作時,必須自己定義解構函式。

指標成員的管理:

(1)指標成員採取常規指標型行為。這樣的類具有指標的所有缺陷但無需特殊的複製控制。

(2)類可以實現所謂的“智慧指標”行為。指標所指向的物件是共享的,但類能夠防止懸垂指標。

(3)類採取值型行為。指標所指向的物件是唯一的,有每個類物件獨立管理。

與複製建構函式和賦值操作符不同,無論類是否定義了自己的解構函式,都會建立和執行合成解構函式。如果類定義了解構函式,則在類定義的解構函式結束之後執行合成解構函式。

過載操作符與轉換

不能過載的操作符:

(1)::    作用域操作符。

(2).    成員訪問運算子。因為‘.’在類中對任何成員都有意義,已經成為標準用法。

(3).*   成員函式呼叫運算子。

(4)?: 條件運算子。因為這個運算子對於類物件來說沒有實際意義,相反還會引起歧義。

過載操作符必須具有一個類型別運算元。過載操作符不能重新定義用於內建型別物件的操作符的含義。

過載操作符不保證運算元的求值順序,尤其是不會保證內建邏輯AND、邏輯OR和逗號表示式的運算元求值。因此,儘量避免過載&&、||和逗號運算子。

可以把非成員操作符宣告為友元,允許訪問類的私有成員。如operator>>、operator<<。

將操作符設定為類成員還是普通非成員函式的原則:

(1)賦值(=)、下標([])、呼叫(())和成員訪問箭頭(->)等操作符必須定義為成員。定義為非成員函式將在編譯時出錯。

(2)與賦值一樣,複合賦值操作符通常應定義為類的成員。與賦值不同的是,它也可以定義為非成員函式(如果編譯不會出錯的話)。

(3)對改變物件狀態或與給定型別緊密聯絡的其他操作符,如自增、自減和解引用,通常定義為類成員。

(4)對稱的操作符,如算術操作符、相等操作符、關係操作符和位操作符,最好定義為普通非成員函式。

過載操作符儘量避免輸出換行符。

IO操作符必須為非成員函式。否則,左運算元只能是該類型別的物件。如果想要支援正常用法,則左運算元必須為ostream型別。因此,類通常將IO操作符設為友元。

ostream& operator<<(ostream&out,const className& s) {out<<s.data;return out;}

istream& operator>>(istream&in,className& s) {in>>s.data;returnin;}

className operator+(const className& lhs,constclassName& rhs){className ret(lhs); ret += rhs;returnret;}

className operator+=(className&lhs,const className& rhs) {lhs=lhs+rhs;return lhs;}

className& operator+=(const className& rhs){return*this;}

inline booloperator==(constclassName& lhs,const className& rhs) {return lhs==rhs;}

inline booloperator!=(constclassName& lhs,const className& rhs) {return !(lhs==rhs);}

賦值必須返回對*this的引用。

類定義下標操作符時,一般需要定義兩個版本:一個為const成員並返回普通引用,另一個為const成員並返回const引用。

int&operator[] (constsize_t index){return data[index];}

const int &operator[] (const size_t index)const{return data[index];}

自增操作符和自減操作符

classBase

{

public:

       Base(int *b,int*e):beg(b),end(e),curr(b){}

       Base&operator++()       //字首表示式

       {

              if(curr==end)

                     throw out_of_range("incrementpast the end of Base");

              ++curr;

              return *this;

       }

//為了區分字首字尾,字尾表示式增加一個額外的int型引數,通常傳遞0

       Base&operator++(int)   //字尾表示式

       {

              Baseret(*this);

              ++*this;

              return ret;

       }

       Base&operator--();

       Base&operator--(int);

private:

       int *beg;

       int* end;

       int* curr;

};

//Base類過載下標操作符

int& Base::operator[]( const size_t index )

{

if ( beg + index>= end || beg + index < beg )

throw out_ot_range( “invalid index“ );

return *( beg + index);
}

const int & Base::operator[](const size_tindex ) const

{

if ( beg + index>= end || beg + index < beg )

throw out_ot_range( “invalid index“ );

return *( beg + index);

}

轉換操作符:對任何可作為函式返回型別的型別(void除外)都可以定義轉換函式。一般來說,不允許轉換為陣列或函式型別,轉換為指標型別以及引用型別是可以的。一種內建型別應該只有一種轉換。

classSmallInt        //定義從Smallint的轉換

{

public:

       SmallInt(int i=0):val(i){}

       operator int() const{return val;}//operatortype();type表示內建型別名、類型別名或有類型別名所定義的名字

private:

       std::size_tval;

};

int calc(int );   SmallIntsi;     int i=calc(si);  //將si 轉換為 int 再呼叫 calc 函式

面向物件程式設計和泛型程式設計

在C++中,通過基類的引用(或指標)呼叫虛擬函式時,發生動態繫結。除建構函式外,任意非static成員函式都可以是虛擬函式。基類通常將派生類需要重定義的任意函式定義為虛擬函式。

一旦函式在基類宣告為虛擬函式,他就一直為虛擬函式,派生類無法改變該函式為虛擬函式這一事實。

成員函式的過載、覆蓋、隱藏:

成員函式被過載的特徵:

(1)相同的範圍(在同一個類中);

(2)函式名字相同;

(3)引數不同;

(4)virtual 關鍵字可有可無。

覆蓋是指派生類函式覆蓋基類函式,特徵是:

(1)不同的範圍(分別位於派生類與基類);

(2)函式名字相同;

(3)引數相同;

(4)基類函式必須有virtual 關鍵字。

隱藏是指派生類的函式遮蔽了與其同名的基類函式,規則如下:

(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。

(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。

設計類時,派生類應反映與基類的“Is A”(是一種)的關係;型別之間另一種關係為“Has A”(有一個)

如果派生類protected/private繼承基類,使得派生類物件不能夠訪問基類的public成員,可以通過在派生類中的public訪問控制下使用using宣告基類的成員,如:

public: using Base::size; //其中size為基類成員資料

預設繼承:如果在繼承是沒有宣告繼承級別,則class宣告的派生類預設私有繼承基類,struct宣告的派生類預設公有繼承基類。

友元關係不能繼承,因為友元不是類的成員函式。

派生類的建構函式只能初始化直接基類(派生列表中指定的類)。因為每個類都定義了自己的介面。

定義派生類複製建構函式:Derived(const Derived& d):Base(d) {/*………*/}先呼叫基類的賦值建構函式,在呼叫自己的建構函式。如果省略基類初始化函式Derived(const Derived& d) {/*………*/},效果是執行Base預設建構函式初始化物件的基類部分。假定Derived成員的初始化從d複製對應成員,則新建構函式將具有奇怪配置:它的Base部分將儲存預設值,而Derived成員是另一物件的副本。

派生類賦值操作符(必須對基類部分顯示賦值):

Derived &Derived::operator=(const Derived&rhs) {

if(this != &rhs) {Base:: operator=(rhs); /*對派生類成員賦值*/ }return *this;}

為什麼建構函式不能定義為虛擬函式?因為建構函式是在物件完全構造之前執行的,在建構函式執行的時候,物件的動態型別還不完整。

如果派生類需要僅僅重定義一個過載集中某些版本的行為,並繼承其他版本的含義。此時派生類可以用using宣告其他過載版本,只需重定義確實必須定義的那些函式。

使用指標或引用會加重類使用者的負擔。C++中一個通用技術是定義包裝類控制代碼類。控制代碼類儲存和管理基類指標。指標所指物件的型別可以變化,它既可以指向基類型別物件又可以指向派生型別物件。

模板與泛型程式設計

模板宣告:像其他任意函式或類一樣,對於模板可以只宣告而不定義。宣告必須指出函式或類是一個模板。

typename與class的區別

在函式形參表中,這兩個關鍵字具有相同含義,可以互換使用,可同時出現在同一模板形參表中使用。使用typename更加直觀。typename關鍵字是作為標準C++的組成部分加入到C++中的,因此舊的程式更有可能只用關鍵字class。

typename可以在模板定義內部指定型別。如,標準庫的容器類定義了不同的型別,如size_type。如果要在函式模板內部使用這樣的型別,必須告訴編譯器我們正在使用的名字指的是一個型別成員的名字,而不是資料成員的名字。預設情況下編譯器假定這樣的名字指定資料成員,而不是型別。如:typename size_type *p;//宣告p是指向size_type型別的指標。

智慧指標auto_ptr

auto_ptr<int> p = new int(42);或auto_ptr<int> p1 =(new int(42));//p指向42這個int型物件。auto_ptr<int>p2;//預設情況下auto_ptr的內佈置在置為0.對未繫結的auto_ptr物件解引用程式會出錯。

if(p2) *p2=1024;//error

智慧指標和普通指標的區別:auto_ptr和內建指標對待複製和賦值有非常關鍵的重要區別。

auto_ptr<string> p1 = new string (“abc”);

auto_ptr<string> p2 = new string (“abc”);

p2=p1;或(p2(p1))//

1刪除p2指向的物件;

2將p2置為指向p1所指的物件;

3解除p1所繫結的物件,將p1置為未繫結狀態。

而普通指標在複製或賦值之後,兩個指標指向同一物件(即“相等”)。

auto_ptr類模板使用注意:1、不要使用auto_ptr物件儲存指向動態分配陣列的指標。當auto_ptr物件被刪除的時候,它只釋放一個物件—它使用普通delete操作符,而不是delete []操作符。2、不要將auto_ptr物件儲存在容器中。容器要求所儲存的型別定義複製和賦值操作符,複製和賦值前後的物件應相等。3、永遠不要使用兩個auto_ptr物件指向同一物件。4、不要使用auto_ptr物件儲存指向靜態分配物件的指標,當auto_ptr物件撤銷的時候,他將試圖刪除指向非動態分配物件的指標,導致未定義的行為。

異常

class explicit out_of_stock : public runtime_time{

public:

explicit explicit out_of_stock(const string&s) : runtime_time(s){}

virtual ~ out_of_stock() throw(){}

};

void no_problem() throw();//異常說明是函式介面的一部分,函式定義以及該函式的任意宣告必須具有相同的異常說明。如果一個函式宣告沒有指定異常說明,則該函式可以丟擲任意型別的異常。

名稱空間

名稱空間的名字在定義該名稱空間的作用域中必須是唯一的。名稱空間可以在全域性作用域或其他作用域內部定義,但不能在函式或類內部定義。名稱空間作用域不能以分號結束。

虛繼承

使用虛基類的多重繼承比沒有虛基類可以減少二義性問題。可以無二異性的直接訪問共享虛基類中的成員。

class istream : public virtual ios {};//虛繼承ios類

class ostream : virtual public ios {};//虛繼承ios類

class istream : public istream, public ostream{};//只繼承一個共享基類(ios稱為虛基類)。

虛基類的初始化

通常,每個類只初始化自己的直接基類。但在用於虛基類的時候,這個初始化策略會失敗。如果使用常規規則,就可能會多次初始化虛基類。為解決重複初始化問題,在虛派生中,由最低派生類的建構函式初始化虛基類。當建立istream物件的時候,首先使用建構函式初始化列表中指定的初始式是構造ios 部分;然後構造istream 部分,忽略istream 的用於ios建構函式初始化列表的初始化式;接下來構造ostream 部分,再次忽略ios 初始化式;最後構造istream 部分。如果istream 建構函式不顯示初始化ios 基類,就是用ios 預設建構函式;如果ios沒有建構函式,則程式碼出錯。

虛基類的建構函式與解構函式

無論虛基類出現在繼承層次中任何地方,總是在構造非虛基類之前構造虛基類,然後按照宣告呼叫非虛基類的建構函式。如:

虛繼承TeddyBear 層次(左):         呼叫建構函式次序(右):