《C++程式設計原理與實踐》讀書筆記(五)

來源:互聯網
上載者:User

標籤:《c++程式設計原理與實踐》   讀書 筆記


拷貝


我們的vector類型具有如下形式:

class vector{    private:    int sz;    double * elem;public:    vector(int s):sz(s),elem(new double[s]){}    ~vector() {delete [] elem;}};


讓我們試圖拷貝其中的一個向量:

void f(int n){    vector v(3);    v.set(2, 2.2);    vector v2 = v;    //...}

對於一種類型而言,拷貝的預設含義是“拷貝所有的資料成員”。對於對象v與v2而言,由於指標elem指向同一塊記憶體,因此重複兩次釋放這一塊記憶體將會造成災難性的後果。那麼,我們應該怎麼做呢?我們需要顯示地進行拷貝操作:當用一個vector對象初始化另一個vector對象時,應拷貝所有的向量元素並且保證這一拷貝操作確實被調用了。某一類型的對象的初始化是由該類型的建構函式實現的。所以,為實現拷貝操作,我們需要實現一種特定類型的建構函式。這種類型的建構函式稱為 拷貝建構函式。C++定義拷貝建構函式的參數應該為一個對被拷貝對象的引用。因此,對於類型vector而言,它的拷貝建構函式如下形式:vector(const vector&);這一拷貝建構函式將在我們試圖使用一個vector對象初始化另一個vector對象時被調用。拷貝建構函式使用對象引用作為參數的原因在於我們不希望在傳遞函數參數時又發生參數的拷貝,而使用const引用的原因在於我們不希望函數對參數進行修改。因此,我們按如下形式重新定義vector類型:

class vector{         int sz;         double *elem;         void copy(const vector& arg){ for(int i = 0; i < arg.sz; ++i) elem[i] = arg.elem[i];}    public:         vector(const vector&):sz(arg.sz),elem(new double[arg.sz]) { copy(arg);}        //...};

拷貝賦值


我們可以通過建構函式拷貝(初始化)對象,但我們也可以通過賦值的方式進行vector對象的拷貝。與拷貝初始化類似,拷貝賦值預設只進行對象成員的拷貝。因此,對於我們目前定義的vector類型而言,拷貝賦值會造成資料重複刪除以及記憶體流失等問題。例如:

void f2(int n){     vector v(3);     v.set(2, 2.2);     vector v2(4);     v2 = v;     // ...}

我們希望對象v2成為對象v的副本(標準庫的vector類型按這種方式實現,但由於我們並未定義vector類型的拷貝賦值操作,因此將執行預設的拷貝賦值操作,即賦值操作將進行成員拷貝)。我們應按如下方式定義拷貝賦值操作:

class vector{     int sz;     double * elem;     void copy(const vector& arg);public:     vector& operator=(const vector&);}
vector& vector::operator=(const vector& a){     double * p = new double[a.sz];     for(int i = 0;i < a.sz; ++i) p[i] = a.elem[i];     delete [] elem;     elem = p;     sz = a.sz;     return * this;}

由於拷貝賦值操作需要考慮對一個對象原有元素的處理,因此拷貝賦值操作比拷貝構造操作稍微複雜一些。


在實現拷貝賦值操作時,我們可以在棄置站台之前首先釋放原有元素所佔用的記憶體以簡化代碼,但這不是一個好的做法。更好的做法是,我們應一直保留原有元素直到我們確信原有元素能夠被安全釋放。如果我們不這麼做,那麼將一個對象賦值給它自身時將有可能產生奇怪的結果。


拷貝術語


對於大多數的程式以及程式設計語言而言,拷貝帶來了很多的問題。一個基本問題是你應該拷貝一個指標(或引用)還是應該拷貝指標指向(或引用)的資料:

     (1)淺拷貝只拷貝指標,因此兩個指標可能指向同一個對象。

     (2)深拷貝將拷貝指標指向的資料,因此兩個指標將指向兩個不同的對象。當我們需要為某一類型的對象實現深拷貝時,我們需要顯式地為該類型定義自己的拷貝建構函式與拷貝賦值函數。

實現了淺拷貝的類型(如指標與引用)稱為具有指標語義或引用語義(它們拷貝地址)。實現了深拷貝的類型稱為具有值語義(它們拷貝指向的值)。從使用者角度看來,具有值語義類型的拷貝操作像沒有涉及指標一樣--僅僅只有值被拷貝了。也可以說,在進行拷貝時,具有值語義的類型表現得就好像它自己是整數類型一樣。


必要的操作


一個類型應該選擇哪些建構函式、該類型是否應定義解構函式、類型是否應定義拷貝賦值函數。

     (1)具有一個或多個參數的建構函式;

     (2)預設建構函式;

     (3)拷貝建構函式(拷貝同一類型的對象);

     (4)拷貝賦值函數(拷貝同一類型的對象);

     (5)解構函式

通常,我們需要一個或多個建構函式以採用不同的參數實現對象實始化。實始值的含義/用途完全取決於建構函式。通常我們使用建構函式來建立不變式。如果我們不能定義一個類的建構函式能夠建立的好的不變式,那麼很可能我們使用了一個糟糕的類設計或者簡單的資料結構。具有參數的建構函式的形式隨它們所在類型的不同而不同。而與之相比,其他的操作則具有更加規則的形式。我們如何知道一個類型是否需要預設建構函式呢?如果我們希望在不指定初始值的前提下構造該類型的對象,那麼該類型就需要預設建構函式。如果一個類型需要擷取系統資源,則該類型需要解構函式。類型需要解構函式的另一個特徵是該類型具有指標成員或引用成員。如果一個類型具有指標成員或引用成員,則該類型通常需要實現解構函式以及拷貝操作。通常,一個實現了解構函式的類型同時也需要實現拷貝建構函式與拷貝賦值函數。其原因很簡單,如果該類型的一個對象擷取了資源(或者具有指向資源的指標成員),那麼只進行預設拷貝(淺拷貝)幾乎肯定會帶來錯誤。另外,對於一個基類而言,如果它的衍生類別具有解構函式,則該基類的解構函式應為虛函數。


顯示建構函式:只具有一個參數的建構函式定義了一個從其參數類型向該函數所秘史類型的轉換。這種轉換是十分重要的。例如:

class complex{     public:         complex(double);         complex(double, double);         //...};complex z1 = 3.14;complex z2 = complex(1.2, 3.4);


儘管如此,我們應謹慎地使用隱式轉換,因為隱式轉換可能會造成不可預料的後果。幸運的是,我們能夠通過一種簡單的方式禁止將建構函式用於類型的隱式轉換。由關鍵字explicit修飾的建構函式(即顯式建構函式)只能用於對象的構造而不能用於隱式轉換。例如:

class vector{     // ...     explicit vector(int);     // ...};vector v = 10;  // errorv = 20;   //errorvector v0(10);   //ok


調試建構函式與解構函式


在程式的執行過程中,建構函式與解構函式都將在明確的、可預計的時間點上被調用。儘管如此,我們並不是總是需要採用顯式的方式來調用這些函數。我們在做某些事的時候也會調用這些函數。建構函式與解構函式的調用可能會造成人們對文法的混淆。下面是常見的建構函式與解構函式被調用的場合:

     (1)每當類型X的一個對象構建時,類型X的一個建構函式將被調用。

     (2)每當類型X的一個對象被銷毀時,類型X的解構函式將被調用。

每當類型的一個對象被銷毀時,該類型的解構函式將被調用;這種情況可能發生在變數的範圍結束時、程式結束時或者delete作用於一個指向對象的指標時。每當類型的一個對象被構建時,該類型的建構函式將被調用;這種情況可能發生在變數被初始化時,通過new構建對象(除了內建類型)時以及拷貝對象時。為了對這個問題進行體會,我們在建構函式,賦值函數以及解構函式內加入了列印語句。

struct X{     int val; void out(const string&s, int nv) { cerr << this << "->" << s << ":" << "(" << nv << ")\n";} X() { out("X()", 0); val = 0;} X(int v) {out("X(int)", v); val = v;} X(const X& x) {out("X(X&)", x.val); val = x.val;} X& operator(const X& a) {out("X::operator=(), a.val); val = a.val; return * this;} ~X() {out("~X()", 0);}};X glob(2);X copy(X a) {return a;}X copy2(X a) { X aa = a; return aa;}X& ref_to(X& a) {return a;}X* make(int i) {X a(i); return new X(a);}X* make(int i) {X a(i); return new X(a);}struct XX {X a; X b;};int main(){     X loc(4);     X loc2 = loc;     loc = X(5);     loc2 = copy(loc);     loc2 = copy2(loc);     X loc3(6);     X& r =ref_to(loc);     delete make(7);     delete make(8);     vector <X> v(4);     XX loc4;     X * p =new X(9);     delete p;     X* p = new X(9);     delete p;     X* pp = new X[5];     delete [] pp;}


訪問向量元素

class vector{     double& operator[](int n) { return elem[n];}     double operator[](int n) const; //對const對象重載運算子};

上述實現使得對象vector的下標操作符具有與常規下標操作符相似的含義:v[i]被解釋為函數調用v.operator[](i),且調用返回對象v的編號為i的元素的引用。


數組


我們已經通過使用數組來引用在自由儲存區中順序排列的對象。與命名變數一樣,我們也可以在其他的地方分配數組。實際上,數組可以作為:

     (1)全域變數(但定義全域變數通常是一個糟糕的主意)

     (2)局部變數(但數組作為局部變數時會受到嚴格的限制)

     (3)函數成員(但一個數組不知道其自身大小)

     (4)類的成員(但數群組成員難於初始化)

現在,你可能會發覺我們更贊成使用vector類型而不是數組。我們應當儘可能地用vector類型取代數組。儘管如此,數組在vector對象出現之前就已經存在了很長的時間,並且它與其他程式設計語言中(如C語言)的數組提供的功能大致相同,因此我們必須學會如何使用數組,以便我們能夠處理那些很久以前編寫的代碼,或者那些由不能使用vector類型的人編寫的代碼。


數組是在記憶體空間中順序排列的同類型對象的集合;也就是說,數組的所有元素都具有相同的類型,並且各元素之間不存在記憶體空隙。數組中的元素從0開始順序編號的。數組可以用“方括弧”表示:

const int max = 100;int gai[max];void f(int n){    char lac[20];    int lai[60];    double lad[n];    // ...}

注意,數組的使用存在一個限制:對於一個命名數組而言,在程式編譯時間必須知道該數組包含元素的數目。如果你希望元素的數目是一個變數,那麼你必須在自由儲存中分配數組,並通過指標對數組進行訪問。vector類型就是這麼做的。像在自由儲存區存放的數組一樣,我們通過下標與解引用操作符([]和*)訪問命名數組。


指向數組元素的指標

double ad[10];double * p = &ad[5];


我們可以使用正數或負數作為指標的下標運算元。只要元素位於數組的範圍之內,那麼這樣的操作就是正確的。然而,通過指標訪問位於數組之外的資料是非法的。通常,編譯器不能監測對數組範圍之外資料的訪問,並且這樣的訪問很可能是災難性的。當指標指向一個數組時,加操作與下標操作能夠改變指標,使得指標指向數組中的其他元素。例如:

p+=2; p-=5;

通過操作符+、-、+=、-=移動指標只能在數組的範圍內進行移動。不幸的是,由指標運算所造成的錯誤有時很難被發現。通常最好的策略是盡量避免使用指標運算。指標運算最常見的操作是對指標進行自增操作(使用++)以使指標指向下一個元素,以及對指標進行自減操作(使用--)以使指標指向上一個元素。例如,我們可以通過如下方式列印ad元素取值:

for(double * p = &ad[0]; p < &ad[10]; ++p) cout << *p << ‘\n‘;

或者反向列印:

for(double * p = &ad[9]; p >= &ad[0]; --p) cout << *p << ‘\n‘;

注意,指標元素另一種常用的方式是將指標作為函數的參數進行傳遞。C++允許指標運算主要是因為曆史原因。還有部分原因在於,在一些低層次的應用中,使用指標運算更為便利。


數組的名字代表了數組的所有元素。例如: char ch[100]; ch的大小sizeof(ch)為100。然而,數組的名字可以轉化(退化)為指標。例如:char * p = ch;


迴文


使用string實現迴文。使用標準庫的string類型以及int類型的索引跟蹤字元比較的進度:

bool is_palindrome(const string& s){     int first = 0;     int last = s.length() -1;     while (first < last)    {         if (s[fisrt] != s[last] return false;         ++ first;         --last;     }     return true;}


使用數組實現迴文

bool is_palindrome(const char s[], int n){    int first = 0;    int last = n-1;    while (first < last)    {       if (s[fist] != s[last] return false;       ++first;       --last;    }    return true;}


使用指標實現迴文

bool is_palindrome(const char * first, const char * last){    while (first < last)   {        if (*first != *last) return false;        ++first;        --last;   }   return true;}


注意,實際上我們可以對指標進行自增操作或自減操作。自增操作使指標指向數組中的下一個元素,而自減操作使指標指向上一個元素。如果指標指向的地區超出了數組的實際範圍,那麼將會產生嚴重的越界錯誤。這是使用指標可能會產生的問題。

《C++程式設計原理與實踐》讀書筆記(五)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.