CPP全面總結(涵蓋C++11標准)


OOP之類和對象

1. this指針的引入

每個成員函數都有一個額外的隱含的形參,這個參數就是this指針,它指向調用對象的地址。默認情況下,this的類型是指向類類型非常量版本的常量指針。可以表示成如下偽代碼形式:

/* 假設現在有一個類Sales_data,以及其非常量Sales_data類型對象,則該隱式的this指針可以寫成如下偽代碼形式 */
Sales_data *const this = &total;

this指針一般用於解決重名問題和返回自身的值或者引用。例如:

struct A{
    int a;

    void test(int a){
        this->a = a;
    }
};

test函數的形參a和類成員a成名,根據就近原則,直接使用a,調用的是形參a,那么如何使用被屏蔽的成員a呢,這里就是采用this指針。

2. const成員函數

緊隨參數列表之后的const關鍵字作用為:修改隱式this指針所指向的對象的類型,如下:

/* 假設現在有一個類Sales_data,以及Sales_data類型對象,則在const成員函數中隱式的this指針可以寫成如下偽代碼形式 */
const Sales_data *const this = &total;

這里加const的含義是,這個函數不能修改本對象,其實就是函數體內不得對類的成員進行修改。const主要起到保護的作用。

注意以下幾點:

a)非const對象可以調用const成員函數,也可以調用非const成員函數,但是const對象只能調用const成員函數。並且,非const對象優先調用非const成員函數。

b)const成員函數只可以返回本對象的常量引用,如下寫法會報錯:

Student &print(ostream &os) const
{
    os << id_ << " " << name_ << " " << age_ << endl;
    return *this;
}

報錯提示:

clang下:error: binding of reference to type 'Student' to a value of type 'const Student' drops qualifiers
return *this;

g++下:error: invalid initialization of reference of type ‘Student&’ from e
return *this;

最后記住:構造函數不能為const。如果為const,怎么完成初始化工作?!

3. const成員函數和非const成員函數可以構成重載。

到此為止,構成函數重載的要素有:類的名稱、函數名、函數形參表以及成員函數的const屬性。事實上,函數簽名就是由這幾個部分構成。

在這里我們解釋一個問題: 為什么C語言里面沒有函數重載? 因為在編譯器編譯C程序時會維護一張符號表,C語言在記載函數的時候就是簡單的記錄函數的名字,所以函數名就是C函數的唯一標識。當我們試圖定義兩個名字相同的函數時,就發生了重定義。

C++是怎么做的呢? 很顯然,對於普通函數,它的符號(唯一標識)是根據函數名和參數列表生成的,對於類的成員函數,還要加上類名和const屬性,所以我們進行函數重載的時候,這些函數在符號表中的標識是不相同的。 C++正是通過這種機制實現了函數的重載

注意:C++編譯器生成函數符號的時候沒有考慮返回值,這也是函數重載和返回值無關的原因。

4. 構造函數之構造函數初始值列表(constructor initialize list)

構造函數有一個特殊的地方,就是它可以包含一個構造函數初始化列表,如下:

Person(int id, const string &name, int age)
         :_id(id), _name(name), _age(age){
}

雖然以下形式,也完全可以達到目的:

Person(int id, const string &name, int age){
        _id = id;
        _name = name;
        _age = age;
}

但兩者是不同的。第一種形式帶構造函數初始值列表,執行的是真正的初始化工作;而第二種形式,進行的是賦值操作。

注意,即使構造函數沒有構造函數初始值列表(更確切的說是構造函數初始值列表為空),那么類中的成員變量將會執行默認初始化。因此在以下情況我們必須使用構造函數默認初始化列表:

a)const內置類型變量以及沒有顯示定義默認構造函數的const類類型變量(可以參考該博文合成的默認構造函數定義為delete的一種情況

b)引用類型成員

c)沒有默認構造函數的類類型變量

其本質是因為,const內置類型變量和引用類型必須初始化;而對於類類型對象,可以通過默認構造函數進行默認初始化(非const類類型對象只要有默認構造函數就可以默認初始化,而const類類型對象必須有顯示定義的默認構造函數才可以執行默認初始化)

5. 類成員初始化的順序是它們在類中聲明的順序,而不是初始化列表中列出的順序

考慮下面的類:

class X {
    int i;
    int j;
public:
    X(int val) :
    j(val), i(j) {
    }
};

我們的設想是這樣的,用val初始化j,用j的值初始化i,然而這里初始化的次序是先i然后j。

記住:類成員初始化的順序是它們在類中聲明的順序,而不是初始化列表中列出的順序!

6. 析構函數

與構造函數一樣,析構函數也是一種特殊的函數。構造函數在對象被創建時調用,析構函數則是在對象被銷毀時被調用。構造函數與構造函數一樣,同樣沒有返回值,並且析構函數沒有任何參數。如下:

~Person(){
        
}

需要引起注意的是:

a)對於類類型對象foo的析構函數只是在它生命期的最后一刻的回調罷了,管不了foo自己所占的內存,就像自己沒法給自己收屍一樣。

b)對於堆上的類類型對象:free 干的事情是釋放內存。delete 干的事情是調用析構函數,然后釋放內存,注意是delete釋放的內存空間,而不是析構函數釋放的。對於棧上的類類型對象,退出作用域時會自動調用析構函數,然后釋放內存。

總結:對於棧上的類類型對象其實和內置類型變量一樣,退出作用域后都是由系統自動釋放內存的。實際上無論是棧空間,還是堆空間,內置類型對象和類類型對象銷毀時的區別,在於類對象會在銷毀前調用析構函數。

7. static成員

不用於普通的數據成員,static 數據成員獨立於該類的任何一個對象而存在,每個static數據成員是與類關聯,並不與該類的對象相關聯。

正如類可以定義共享的 static 數據成員一樣,類也可以定義 static 成員函數。static 成員函數沒有 this 形參(因為static成員不屬於任何一個對象),它可以直接訪問所屬類的 static 成員,但不能直接使用非 static 成員(因為沒有this指針)。當我們在類的外部定義 static 成員時,無須重復指定 static 保留字,該保留字只出現在類定義體內部的聲明處即可。

小結:

a)static 成員是類的組成部分但不是任何對象的組成部分,因此,static 成員函數沒有 this 指針

b)因為 static 成員不是任何對象的組成部分,所以 static 成員函數不能是const成員函數。因為,將成員函數聲明為 const 就是承諾不會修改該函數所屬的對象,而 static 成員不是任何對象的組成部分。

c)static 函數只能使用 static 成員,而不能直接調用普通成員(方法+數據成員),當然如果這樣寫,static void print(Test &t) 誰也擋不住其調用對象t的普通成員。

d)static 成員一般在類內聲明,類外定義。注意,當我們在類的外部定義 static 成員時,無須重復指定 static 保留字,該保留字只出現在類定義體內部的聲明處即可。

8. 友元

1. 必須先定義包含成員函數的類,才能將這個類的成員函數設置為另外一個類的友元。

2. 不必預先聲明類和非成員函數來將它們設為友元。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test
{
    public:
        friend class Other;                //聲明某個類是Test的朋友
        friend void bar(const Test &t);     //聲明某個函數是Test的朋友
    private:
        int x_;
        int y_;
};

class Other
{
    public:
        void foo(Test &t)
        {
            t.x_ = 10;
            t.y_ = 20;
        }
};

void bar(const Test &t)
{
    cout << t.x_ << endl;
}

int main(int argc, const char *argv[])
{
    Test t;
    return 0;
}

注意:友元關系是單向的,以上例子中Test並不是Other的朋友,因此Test不能訪問Other的private成員。(tmd,這不就是在告訴我們,你的是我的,我的還是我的)。順便黑一下C++:

C++ is a modern language where your parent can't touch your privates but your friends can.

多么痛的領悟。

STL之順序容器

1. 順序容器的初始化

順序容器主要是vector和list,他們的初始化方式有以下五種:

1. 直接初始化一個空的容器

2. 用一個容器去初始化另一個容器

3. 指定容器的初始大小

4. 指定容器的初始大小和初始值

5. 用一對迭代器范圍去初始化容器

第2種和第5種初始化方式的區別在於:第2種不僅要求容器類型相同,還要求容器元素類型完全一致,而第5種不要求容器相同,對於容器元素,要求能相互兼容即可。

指針可以當做迭代器,所以可以這樣做:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(int argc, char **argv) {
    
    const size_t MAX_SIZE = 3;
    string arr[MAX_SIZE] = { "hello", "world", "foobar" };

    vector<string> vec(arr, arr + MAX_SIZE);

    return 0;
}

注意,凡是傳入迭代器作為指定范圍的參數,可以使用指針代替。

2. 容器元素的類型約束

凡是放入vector中的元素,必須具備復制和賦值的能力,因為放入vector中的元素只是一份拷貝。下例會報錯。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

//Test不支持復制和賦值。所以不能放入vector
class Test
{
    public:
        Test() {}

    private:
        //設為私有,禁用了Test的復制和賦值能力 
        Test(const Test &);           //用於復制(拷貝構造函數)
        void operator=(const Test &); //用於賦值(賦值運算符)
};

int main(int argc, const char *argv[])
{
    vector<Test> vec;
    Test t;
    vec.push_back(t);
    return 0;
}

3. 特殊的迭代器成員 begin和end

有四個特殊的迭代器:

c.begin() //指向容器C的第一個元素

C.end() //指向最后一個元素的下一個位置

C.rbegin() //返回一個逆序迭代器,指向容器c的最后一個元素

C.rend() //返回一個逆序迭代器,指向容器c的第一個元素的前面的位置

分別去順序迭代和逆序迭代容器,例如:

#include <iostream>
#include <string>
#include <vector>
#include <list>

using namespace std;

int main(int argc, char **argv) {

    vector<string> vec;
    vec.push_back("beijing");
    vec.push_back("shanghai");
    vec.push_back("guangzhou");
    vec.push_back("shenzhen");

    for (vector<string>::iterator iter = vec.begin(); iter != vec.end();
            ++iter) {
        cout << *iter << endl;
    }

    for (vector<string>::reverse_iterator iter = vec.rbegin();
            iter != vec.rend(); ++iter) {
        cout << *iter << endl;
    }

    return 0;
}

/*
output:
beijing
shanghai
guangzhou
shenzhen
shenzhen
guangzhou
shanghai
beijing
*/

4. 順序容器的插入操作

1. vector沒有push_front(vectoe內部實現是數組)。list有push_front。

2. 針對List

a)可以使用insert(p, t) 在指定位置元素之前添加元素,其中p是迭代器,t時元素的值

b)Insert(p, n, t) 在迭代器p指向的位置之前插入n個元素,初始值為t

c)Insert(p, b, e) 在迭代器p指向的位置之前插入迭代器b和迭代器e之間的元素

d)可是使用push_front 頭插

5. 順序容器的刪除操作

1. 刪第一個或最后一個元素

類似與插入元素,pop_front或者pop_back可以刪除第一個或者最后一個元素

2. 刪除容器的一個元素

與insert對應,刪除采用的是erase操作,該操作有兩個版本:刪除由一個迭代器指向的元素,或者刪除由一對迭代器標記的一段元素。刪除元素需要接收返回值,防止迭代器失效,最好使用while循環。

6. 容器大小的操作

vector與容量有關的函數:

a)size 元素數目,類似於會議室中人的數目

b)resize 調整元素數目,類似於調整函數

c)capacity 可容納數目,類似於會議室中的座位數量

d)reserve 告訴vector容器應該預留多少個元素的存儲空間

7. 迭代器的失效問題

任何insert或者push操作都可能導致迭代器失效。當編寫循環將元素插入到vector或list容器中時,程序必須確保迭代器在每次循環后都得到更新。

vector迭代器持續有效,除非:

1. 使用者在較小的索引位置插入或者刪除元素。

2. 由於容量的變化引起的內存重新分配。

list迭代器持續有效,除非:

將it指向的元素刪除,那么it則失效(list內部實現是鏈表,it指向的元素刪了就是沒有了,再用it訪問直接段錯誤。vector也有可能失效,只不過后面的元素會往前移,再用it訪問可能不會產生段錯誤)。

刪除元素需要接收返回值,最好使用while循環。例如刪除下例刪除偶數:

vector<int>::iterator it = vec.begin();
while(it != vec.end())
{
    if(*it % 2 == 0)
        //vec.erase(it);
        it = vec.erase(it);
    else
        ++it;
}

8. vector的for_each方法

遍歷vector方法:

1. 下標

2. 迭代器

3. for_each

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

void print(int i)
{
    cout << i << endl;
}

int main(int argc, const char *argv[])
{
    vector<int> vec;
    vec.push_back(12);
    vec.push_back(23);
    vec.push_back(45);
    vec.push_back(56);
    vec.push_back(221);
    vec.push_back(35);
    vec.push_back(129);

    for_each(vec.begin(), vec.end(), print);

    return 0;
}

/*
output:
12
23
45
56
221
35
129
*/

9. vector和list的區別

a) vector采用數組實現,list采用鏈表。

b) vector支持隨機訪問,list不提供下標。

c) 大量增加刪除的操作適合使用list。

10. string之截取子串substr

例子如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(int argc, const char *argv[])
{
    string s = "helloworldfoo";


    string s2 = s.substr(1, 4); //ello
    cout << s2 << endl;

    return 0;
}

注意,迭代器一般是取基地址到尾后地址的一段范圍。而下標操作,通常是基地址+長度。

11. stack

#include <iostream>
#include <string>
#include <vector>
#include <stack>
using namespace std;

int main(int argc, const char *argv[])
{
    stack<int> s;

    s.push(10);
    s.push(22);
    s.push(23);
    s.push(1);
    s.push(8);
    s.push(99);
    s.push(14);

    while(!s.empty())
    {
        cout << s.top() << endl;
        s.pop();
    }

    return 0;
}

/* 
輸出如下:
14
99
8
1
23
22
10
*/

12. queue

#include <iostream>
#include <string>
#include <vector>
#include <queue>
using namespace std;

int main(int argc, const char *argv[])
{
    queue<int> q;

    q.push(12);
    q.push(23);
    q.push(4);
    q.push(5);
    q.push(7);


    while(!q.empty())
    {
    
        cout << q.front() << endl;
        q.pop();
    }


    return 0;
}

/*
輸出:

12
23
4
5
7

*/

13. 優先級隊列(用堆實現)

例1:

#include <iostream>
#include <string>
#include <vector>
#include <queue>
using namespace std;

int main(int argc, const char *argv[])
{
    priority_queue<int> q;
    q.push(12);
    q.push(99);
    q.push(23);
    q.push(123);

    while(!q.empty())
    {
        cout << q.top() << endl;        
        q.pop();
    }

    return 0;
}

/*

output:
123
99
23
12

*/

例2:

#include <iostream>
#include <string>
#include <vector>
#include <queue>
using namespace std;


int main(int argc, const char *argv[])
{
    priority_queue<int, vector<int>, greater<int> > q;
    
    q.push(12);
    q.push(99);
    q.push(23);
    q.push(123);

    while(!q.empty())
    {
        cout << q.top() << endl;        
        q.pop();
    }

    return 0;
}

/*
output:
12
23
99
123
*/
#include <iostream>
#include <string>
#include <vector>
#include <queue>
using namespace std;


int main(int argc, const char *argv[])
{
    priority_queue<int, vector<int>, less<int> > q;
    q.push(12);
    q.push(99);
    q.push(23);
    q.push(123);

    while(!q.empty())
    {
        cout << q.top() << endl;        
        q.pop();
    }

    return 0;
}

/*
output:
123
99
23
12
*/

例3:傳入函數對象

#include <iostream>
#include <string>
#include <vector>
#include <queue>
using namespace std;


struct Score
{
    int score_;
    string name_;

    Score(int score, const string name)
        :score_(score), name_(name)
    { }
};


class Cmp
{
    public:
        bool operator() (const Score &s1, const Score &s2)
        {
            return s1.score_ < s2.score_;
        }
};

// Cmp p;
// p(s1, s2)


int main(int argc, const char *argv[])
{
    priority_queue<Score, vector<Score>, Cmp> q;
    
    q.push(Score(67, "zhangsan"));
    q.push(Score(88, "lisi"));
    q.push(Score(34, "wangwu"));
    q.push(Score(99, "foo"));
    q.push(Score(0, "bar"));

    while(!q.empty())
    {
        cout << q.top().name_ << " : " << q.top().score_ << endl;
        q.pop();
    }

    return 0;
}

/*
output:
foo : 99
lisi : 88
zhangsan : 67
wangwu : 34
bar : 0
*/

14. reverse迭代器

反向迭代器邏輯上指向的元素是物理上指向元素的下一個元素。

在實際(物理)實現上,rbegin()指向最后一個元素的下一個位置,rend()指向第一個元素。但是在邏輯上,rbegin()指向最后一個元素,rend()指向第一個元素的前一個位置。

注意:reverse迭代器不能用於erase函數。刪除的正確方式是:it = string::reverse_iterator(s.erase((++it).base()));(見示例3)

示例1:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


int main(int argc, const char *argv[])
{
    vector<int> coll;

    for (int i = 0; i <= 9 ; i++)            // 0 1 2 3 4 5 6 7 8 9
    {
        coll.push_back(i);
    }

    vector<int>::iterator pos;
    pos = find(coll.begin(), coll.end(), 5); // 此時pos物理指向的元素就是5

    cout << "pos: " << *pos << endl;         // 輸出5

    vector<int>::reverse_iterator rpos(pos); // 反向迭代器物理上指向的元素確實是5 
    cout << "rpos: " << *rpos << endl;       // 但是邏輯上指向的元素是它的下一個元素,在此處即為4    
}

/*
output:
pos: 5
rpos: 4
*/

示例2:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int i)
{
    cout << i << " ";
}

int main(int argc, const char *argv[])
{
    vector<int> coll;

    for (int i = 0; i <= 9 ; i++)               // 0 1 2 3 4 5 6 7 8 9
    {
        coll.push_back(i);
    }

    vector<int>::iterator pos1;
    pos1 = find(coll.begin(), coll.end(), 2);   // pos1指向2

    vector<int>::iterator pos2;
    pos2 = find(coll.begin(), coll.end(), 7);   // pos2指向7


    for_each(pos1, pos2, print);                // 輸出2 3 4 5 6
    cout << endl;


    vector<int>::reverse_iterator rpos1(pos1);  // rpos1物理指向2,邏輯指向1
    vector<int>::reverse_iterator rpos2(pos2);  // rpos2物理指向7,邏輯指向6


    for_each(rpos2, rpos1, print);              // 輸出6 5 4 3 2
    cout << endl;
}

/*
output:
2 3 4 5 6
6 5 4 3 2
*/

示例3:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(int argc, const char *argv[])
{
    string s = "helloworld";

    string::reverse_iterator it = s.rbegin();     // s.rbegin物理指向的元素最后一個元素之后的位置,邏輯指向的是最后一個元素
    while(it != s.rend())
    {
        if(*it == 'r')
        {
            string::iterator tmp = (++it).base(); // 由於earse()參數不能是刪除反向迭代器,因此需要將其轉換為正向迭代器
            tmp = s.erase(tmp);                   // 而it此時物理指向的元素並不是'r',++it后才物理指向‘r’,此時經base()轉換為正向迭代器后刪除
            it = string::reverse_iterator(tmp);   // 之后將正向迭代器轉換成反向迭代器
            //it = string::reverse_iterator(s.erase((++it).base()));
        }
        else
            ++it;
    }

    cout << s << endl;
}
/*
output:
hellowold
*/

STL之關聯容器

1. Pair類型

Pair是一種簡單的關聯類型。注意:pair不是容器,而是代表一個key-value鍵值對。

示例1:

#include <iostream>
#include <string>
#include <utility>
using namespace std;

int main(int argc, const char *argv[])
{
    pair<int, int> p1;
    p1.first = 10;
    p1.second = 12;
    
    pair<int, string> p2;
    p2.first = 12;
    p2.second = "hello";

    pair<string, string> p3;
}
示例2:
#include <iostream>
#include <string>
#include <vector>
using namespace std;

//生成pair對象的三種方法
int main(int argc, const char *argv[])
{
    vector<pair<string, int> > vec;

    pair<string, int> word;
    word.first = "hello";
    word.second = 12;
    vec.push_back(word);

    pair<string, int> word2("world", 12);
    vec.push_back(word2);
    
    vec.push_back(make_pair("foo", 3));
}

示例3:vector中裝入pair,實現統計詞頻:

#include <iostream>
#include <string>
#include <vector>
#include <utility>
using namespace std;

typedef vector<pair<string, int> > Dict;

void makeDict(Dict &dict, const vector<string> &words);
void addWordToDict(Dict &dict, const string &word);

int main(int argc, const char *argv[])
{
    vector<string> words;
    string word;

    while(cin >> word)
        words.push_back(word);

    Dict dict;
    makeDict(dict, words);
    
    for(const pair<string, int> &p : dict)
    {
        cout << p.first << " : " << p.second << endl;
    }

    return 0;
}

void makeDict(Dict &dict, const vector<string> &words)
{
    dict.clear();
    for(vector<string>::const_iterator it = words.begin();
        it != words.end();
        ++it)
    {
        addWordToDict(dict, *it);
    }
}

void addWordToDict(Dict &dict, const string &word)
{
    Dict::iterator it;
    for(it = dict.begin();
            it != dict.end();
            ++it)
    {
        if(it->first == word)
        {
            ++it->second;
            break;
        }
    }
    
    if(it == dict.end())
        dict.push_back(make_pair(word, 1));
}

2. map

map可以看做是一種存儲pair類型的容器,內部采用二叉樹實現(編譯器實現為紅黑樹)。

1. pair不是容器,而是代表一個key-value鍵值對;而map則是一個容器,里面存儲了pair對象,只是存儲的方式與vector<pair>這種連續存儲,有所不同,map采用的是二叉排序樹存儲pair,一般而言是紅黑樹,因此內部是有序的

2. 當map使用下標訪問時,如果key不存在,那么會在map中添加一個新的pair,value為默認值

示例1:

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc, const char *argv[])
{
    map<string, int> m;

    m["beijing"] = 2000;
    m["shenzhen"] = 1000;
    m["shanghai"] = 1500;
    m["hongkong"] = 500;
    m["hangzhou"] = 880;

    for(map<string, int>::const_iterator it = m.begin();
        it != m.end();
        ++it)
    {
        //*it pair
        cout << it->first << " : " << it->second << endl;
    }

    return 0;
}

/*
output:
beijing : 2000
hangzhou : 880
hongkong : 500
shanghai : 1500
shenzhen : 1000
*/
// 由於key是string類型,因此輸出按字典序。

示例2:

#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;

int main(int argc, const char *argv[])
{
    map<string, int> m;

    m["beijing"] = 40;
    m["shenzhen"] = 30;
    m["guangzhou"] = 37;

    cout << m.size() << endl; //3
    cout << m["shanghai"] << endl;
    cout << m.size() << endl;

    return 0;
}

/*
output:
3
0
4
*/
3. map 的 key 必須具有小於操作符 operator <

以下為錯誤代碼:

#include <iostream>
#include <map>
using namespace std;

struct Test
{
    int a;
};

int main(int argc, const char *argv[])
{
    map<Test, int> m;  
    Test t;
    m[t] = 1;
}

/* 編譯報錯,因為Test對象在次數為key-value對中的key,但其並沒有定義 operator< 運算符,紅黑樹無法進行排序 */

4. map查找元素的效率是lgn,因為樹的高度不超過O(lgN)

示例:使用map,實現統計詞頻,如下:

#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;


int main(int argc, const char *argv[])
{
    map<string, int> words;

    string word;
    
    /* 如果key(word)存在,則value++; 如果word不存在,此處會在map(words)中添加一個新的pair,value為默認值(此處為0),然后value++ */
    while(cin >> word)
        words[word]++;

    for(const pair<string, int> &p : words)
        cout << p.first << " : " << p.second << endl;

    return 0;
}

5. 在map中添加元素

剛才我們看到,采用下標的方式,可以給map添加元素,但更好的做法時采用insert插入一個pair對象。

這里值得注意的是insert的返回值,其返回了一個pair對象,第一個元素是指向該key所在的那個pair對象的的迭代器,第二個則表示插入是否成功。使用insert插入map元素時,如果失敗,則不會更新原來的值。看下面例子:

#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;

int main(int argc, const char *argv[])
{
    map<string, int> m;

    m.insert(make_pair("hello", 1));
    m.insert(make_pair("foo", 1));
    m.insert(make_pair("bar", 1));
    m.insert(make_pair("hello", 1));

    cout << "size : " << m.size() << endl;

    /* insert的返回值:指向key所在pair的迭代器,以及表示插入是否成功的布爾值 */
    pair<map<string, int>::iterator, bool> ret;

        // 之前沒有這個key,插入成功
    ret = m.insert(make_pair("fwfgwfg", 23));
    cout << "ret = " << ret.second << endl;
    
    // 之前已有的key,插入失敗。插入失敗的話,不會更新原來的value值
    ret = m.insert(make_pair("hello", 25425));
    cout << "ret = " << ret.second << endl;
    cout << ret.first->second << endl;

    return 0;
}

/*
output:
size : 3 
ret = 1       
ret = 0
1
*/

下面的程序仍然是實現統計詞頻:

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main(int argc, const char *argv[])
{
    map<string, int> words;

    string word;
    pair<map<string, int>::iterator, bool> ret;
    while(cin >> word)
    {
        ret = words.insert(make_pair(word, 1));
        if(ret.second == false) //word已經存在
            ++ret.first->second;
    }

    for(const pair<string, int> &p : words)
        cout << p.first << " : " << p.second << endl;

    return 0;
}

綜上,在本章中我們已經使用三種方式,去統計詞頻了,分別是:vector中使用pair, map的下標訪問方式以及map的insert方式。

6. 在map中查找元素

剛才看到可以利用下標獲取value的值,但是這樣存在一個弊端,如果下標訪問的是不存在的元素,那么會自動給map增加一個鍵值對,這顯然不是我們所預期的。

我們可以采用 count 和 find 來解決問題,其中 count 僅僅能得出該元素是否存在,而 find 能夠返回該元素的迭代器。

示例1:

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc, const char *argv[])
{
    map<string, string> m;
    m["beijing"] = "bad";
    m["shanghai"] = "just soso";
    m["shenzhen"] = "well";
    m["hangzhou"] = "good";


    cout << m.count("hangzhou") << endl;
    cout << m.count("HK") << endl;

    return 0;
}

/*
output:
1
0
*/

示例2:

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc, const char *argv[])
{
    map<string, string> m;
    m["beijing"] = "bad";
    m["shanghai"] = "just soso";
    m["shenzhen"] = "well";
    m["hangzhou"] = "good";

        // find的返回值
    map<string, string>::iterator it = m.find("HK");
    
    if(it == m.end())
        cout << "不存在" << endl;
    else
        cout << it->first << " : " << it->second << endl;
    
    return 0;
}

/*
output:
不存在
*/

3. set

Set類似於數學上的集合,僅僅表示某個元素在集合中是否存在,而不必關心它的具體位置。同樣,set中的元素互異,也就是無法兩次插入相同的元素。set 底層采用紅黑樹實現,按照值進行排序,map則按照key進行排序。使用方式和map類似,但是簡單很多。

示例1:

#include <iostream>
#include <string>
#include <set>
using namespace std;

int main(int argc, const char *argv[])
{
    set<int> s;

        // set不會插入重復的元素
    for(int i = 0; i < 20 ; ++i)
    {
        s.insert(i);
        s.insert(i);
    }

    cout << "size : " << s.size() << endl;

    return 0;
}

/*
output:
size : 20
*/

示例2:

#include <iostream>
#include <string>
#include <vector>
#include <set>
#include <stdlib.h>
using namespace std;


int main(int argc, const char *argv[])
{
    srand(10000);

    set<int> s;
    
    for(int i = 0; i < 40; ++i)
    {
        s.insert(rand() % 100);
    }
    
    // 注意是有序的
    for(int i : s)
    {
        cout << i << " ";
    }
    cout << endl;

    return 0;
}

/*
output:
4 5 8 12 13 15 16 20 21 22 24 25 27 32 38 39 42 43 46 50 54 57 59 63 66 72 78 82 85 93 94 96 98
*/

4. 小結

map 中key 的值是唯一的,set 中的元素都是唯一的。

1. map和set比較:

a) 二者均使用紅黑樹實現

b) key需要支持<操作

c) map側重於key-value的快速查找

d) set側重於查看元素是否存在

2. 用戶無法對map和set中的元素進行排序,不然會干擾map和set本身的實現。

最后注意:map 的 key 值是不可更改的。而 set 中的 value 雖然可以更改,但不建議這樣做,真有需求,直接刪除即可。

5. 哈希

c++11標准添加了 std::unordered_map 與 std::unordered_map。

map采用二叉樹實現,hash_map采用hash表,那么二者的使用上:

a) 當key本身需要排序時,使用map

b) 其他情況采用hash_map更佳(hash_map無序),但是采用map效率也是足夠的。

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;


int main(int argc, const char *argv[])
{
    unordered_map<string, int> m;

    m["beijing"] = 1;
    m["shanghai"] = 2;
    m["shenzhen"] = 3;

    for(unordered_map<string, int>::const_iterator it = m.begin();
        it != m.end();
        ++it)
    {
        cout << it->first << " : " << it->second << endl;
    }
}

/*
output:
shenzhen : 3
shanghai : 2
beijing : 1
*/

STL 算法

以下只是對小部分常用算法進行了介紹,但具體用法大同小異。

1. for_each

template <class InputIterator, class Function>
   Function for_each (InputIterator first, InputIterator last, Function fn);

Apply function to range

Applies function fn to each of the elements in the range [first,last).

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <algorithm>
#include <ctype.h>
using namespace std;

void toUpper(string &s)
{
    for(string::iterator it = s.begin();
            it != s.end();
            ++it)
    {
        if(islower(*it))
            *it = toupper(*it);
    }
}

void print(const string &s)
{
    cout << s << " ";
}

int main(int argc, const char *argv[])
{
    vector<string> vec;
    vec.push_back("beijing");
    vec.push_back("changchun");
    vec.push_back("shijiahzuang");
    vec.push_back("shenyang");
    vec.push_back("dalian");
    vec.push_back("jinan");
    vec.push_back("nanjing");


    for_each(vec.begin(), vec.end(), toUpper);
    
    for_each(vec.begin(), vec.end(), print);
}

/*
output:
BEIJING CHANGCHUN SHIJIAHZUANG SHENYANG DALIAN JINAN NANJING
*/

2. count && count_if

count

template <class InputIterator, class T>
  typename iterator_traits<InputIterator>::difference_type
    count (InputIterator first, InputIterator last, const T& val);

Count appearances of value in range

Returns the number of elements in the range [first,last) that compare equal to val.
The function uses operator== to compare the individual elements to val.

示例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main(int argc, const char *argv[])
{
    int myints[] = {10,20,30,30,20,10,10,20};
    int mycount = count(myints, myints+8, 10);
    cout << "10 appears " << mycount << " times." << endl;

    vector<int> myvector(myints, myints+8);
    mycount = count(myvector.begin(), myvector.end(), 20);
    cout << "20 appears " << mycount << " times." << endl;
}

/*
output:
10 appears 3 times.
20 appears 3 times.
*/

count_if

template <class InputIterator, class Predicate>
  typename iterator_traits<InputIterator>::difference_type
    count_if (InputIterator first, InputIterator last, UnaryPredicate pred);

Return number of elements in range satisfying condition

Returns the number of elements in the range [first,last) for which pred is true.

示例:

/* count_if example */

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool IsOdd(int i)
{
    return i % 2 == 1;
}

int main(int argc, const char *argv[])
{
    vector<int> vec;
    for(int i = 1; i < 10; ++i)
    {
        vec.push_back(i); //vec: 1 2 3 4 5 6 7 8 9
    }
    
    int mycount = count_if(vec.begin(), vec.end(), IsOdd);
    cout << "vec contains " << mycount << " odd values." << endl;
}

/*
output:
vec contains 5 odd values.
*/

3. min_element

default (1)

template <class ForwardIterator>
  ForwardIterator min_element (ForwardIterator first, ForwardIterator last);

custom (2)

template <class ForwardIterator, class Compare>
  ForwardIterator min_element (ForwardIterator first, ForwardIterator last,
                               Compare comp);

Return smallest element in range

Returns an iterator pointing to the element with the smallest value in the range [first,last).

The comparisons are performed using either operator< for the first version, or comp for the second; An element is the smallest if no other element compares less than it. If more than one element fulfills this condition, the iterator returned points to the first of such elements.

示例:

// min_element/max_element example

#include <iostream>     // std::cout
#include <algorithm>    // std::min_element, std::max_element

bool myfn(int i, int j) 
{ 
    return i<j; 
}

struct myclass {
  bool operator() (int i,int j) { return i<j; }
} myobj;

int main () {
  int myints[] = {3,7,2,5,6,4,9};

  // using default comparison:
  std::cout << "The smallest element is " << *std::min_element(myints,myints+7) << '\n';
  std::cout << "The largest element is "  << *std::max_element(myints,myints+7) << '\n';

  // using function myfn as comp:
  std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myfn) << '\n';
  std::cout << "The largest element is "  << *std::max_element(myints,myints+7,myfn) << '\n';

  // using object myobj as comp:
  std::cout << "The smallest element is " << *std::min_element(myints,myints+7,myobj) << '\n';
  std::cout << "The largest element is "  << *std::max_element(myints,myints+7,myobj) << '\n';

  return 0;
}

/*
output:
The smallest element is 2
The largest element is 9
The smallest element is 2
The largest element is 9
The smallest element is 2
The largest element is 9
*/

4. find && find_if

find

template <class InputIterator, class T>
   InputIterator find (InputIterator first, InputIterator last, const T& val);

Find value in range

Returns an iterator to the first element in the range [first,last) that compares equal to val. If no such element is found, the function returns last.

The function uses operator== to compare the individual elements to val.

find_if

template <class InputIterator, class UnaryPredicate>
   InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred);

Find element in range

Returns an iterator to the first element in the range [first,last) for which pred returns true. If no such element is found, the function returns last.

示例:

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <algorithm>
#include <ctype.h>
using namespace std;

void print(const string &s)
{
    cout << s << " ";
}

bool isShorter(const string &s)
{
    return s.size() < 6;
}

int main(int argc, const char *argv[])
{
    vector<string> vec;
    vec.push_back("beijing");
    vec.push_back("changchun");
    vec.push_back("shijiahzuang");
    vec.push_back("shenyang");
    vec.push_back("dalian");
    vec.push_back("jinan");
    vec.push_back("nanjing");

    vector<string>::iterator it = 
        std::find(vec.begin(), vec.end(), "dalian");
    cout << *it << endl;
    
    //find_if
    it = std::find_if(vec.begin(), vec.end(), isShorter);
    cout << *it << endl;
}

/*
output:
dalian
jinan
*/

5. copy

template <class InputIterator, class OutputIterator>
  OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result);

Copy range of elements

Copies the elements in the range [first,last) into the range beginning at result.

The function returns an iterator to the end of the destination range (which points to the element following the last element copied).

The ranges shall not overlap in such a way that result points to an element in the range [first,last). For such cases, see copy_backward.

示例(注意插入迭代器的用法):

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <algorithm>
#include <ctype.h>
using namespace std;


void print(const string &s)
{
    cout << s << " ";
}

int main(int argc, const char *argv[])
{
    vector<string> vec;
    vec.push_back("beijing");
    vec.push_back("changchun");
    vec.push_back("shijiahzuang");
    vec.push_back("shenyang");
    vec.push_back("dalian");
    vec.push_back("jinan");
    vec.push_back("nanjing");


    list<string> lst;

    std::copy(vec.begin(), vec.end(), back_inserter(lst));  //執行的是push_back。如果填寫lst.begin(),需要list<string> lst(7);

    for_each(lst.begin(), lst.end(), print); 
    cout << endl;

    lst.clear();

    std::copy(vec.begin(), vec.end(), front_inserter(lst)); //執行的是push_front。如果填寫lst.rbegin(),需要list<string> lst(7);

    for_each(lst.begin(), lst.end(), print); 
    cout << endl;
    return 0;
}

/*
output:
beijing changchun shijiahzuang shenyang dalian jinan nanjing
nanjing jinan dalian shenyang shijiahzuang changchun beijing
*/

6. lambda 表達式

c++11中新增了lambda表達式。

簡單來說,編程中提到的 lambda 表達式,通常是在需要一個函數,但是又不想費神去命名一個函數的場合下使用,也就是指匿名函數

示例:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <ctype.h>
using namespace std;


void toUpper(string &s)
{
    for(string::iterator it = s.begin();
            it != s.end();
            ++it)
    {
        if(islower(*it))
            *it = toupper(*it);
    }
}

void print(const string &s)
{
    cout << s << " ";
}



int main(int argc, const char *argv[])
{
    vector<string> vec;
    vec.push_back("beijing");
    vec.push_back("changchun");
    vec.push_back("shijiahzuang");
    vec.push_back("shenyang");
    vec.push_back("dalian");
    vec.push_back("jinan");
    vec.push_back("nanjing");


    for_each(vec.begin(), vec.end(), toUpper);

    for_each(vec.begin(), vec.end(), [](const string &s) { cout << s << " "; } );
}

/*
output:
BEIJING CHANGCHUN SHIJIAHZUANG SHENYANG DALIAN JINAN NANJING 
*/

OOP之復制控制

1. 對象復制的時機:

a)根據一個類去顯式或者隱式初始化一個對象

b)復制一個對象,將它作為實參傳給一個函數

c)從函數返回時復制一個對象

那么如何完成對象復制的工作?這里需要的就是拷貝構造函數。

2. 拷貝構造函數(也叫復制構造函數)

只有單個形參,而且該形參是本類類型對象的引用(常用const修飾),這樣的構造函數成為復制控制函數。

復制構造函數調用的時機就是在對象復制的時候。

如果什么也不做,編譯器會自動幫我們合成一個默認的復制構造函數。

那么如果我們自己來定義復制構造函數,應該怎么寫?示例如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;


class Student
{
    public:
        Student() {}
        Student(int id, const string &name, int age)
            :id_(id), name_(name), age_(age)
        {  }
        Student(const Student &other)
            :id_(other.id_),
             name_(other.name_),
             age_(other.age_)
        {
        }

        void print() const
        {
            cout << id_ << " : " << name_ << " : " << age_;
        }

    private:
        int id_;
        string name_;
        int age_;
};


int main(int argc, const char *argv[])
{
    Student s(11, "zhangsan", 23);
    s.print();
    cout << endl;

    Student s2(s); // 調用拷貝構造函數
    s2.print();
    cout << endl;
}

/*
output:
11 : zhangsan : 23
11 : zhangsan : 23
*/

現在來思考一個問題,既然編譯器生成的拷貝構造函數工作正常,那么什么時候需要我們自己來編寫拷貝構造函數呢?這就是下面的深拷貝和淺拷貝的問題。

3. 深拷貝和淺拷貝

我們通過自己定義的string類來解釋深拷貝與淺拷貝的問題。先來看以下這個錯誤版本的string類:

_string.h

#ifndef _STRING_H_
#define _STRING_H_ 

#include <stddef.h>

namespace __str
{
    class string
    {
        public:
            string();
            string(const char*);

            void debug() const;
            size_t size() const;

            ~string();

        private:
            char *_str;

    };
}       /* namespace __str */  

#endif  /*_STRING_H_*/
 
_string.cpp
 
#include "_string.h"
#include <iostream>
#include <string.h>
using namespace std;

namespace __str
{
    string::string()
        :_str(new char[1])
    { _str[0] = 0; }

    string::string(const char *s)
        :_str(new char[strlen(s) + 1])
    { strcpy(_str, s); }

    size_t string::size() const
    { return strlen(_str); }

    void string::debug() const
    { cout << _str << endl; }

    string::~string()
    { delete []_str; }
} /* namespace __str */

main.cpp

#include "_string.h"
using namespace __str;

int main(int argc, const char *argv[])
{
    string s("hello"); // 調用一個參數的構造函數
     s.debug();

    string s2(s);      // 調用系統合成的拷貝構造函數
     s2.debug();
}

程序運行后,輸出兩次 hello,直接直接掛掉。為什么會這樣子呢?

因為系統合成的拷貝構造函數,在復制String對象時,只是簡單的復制其中的_str的值,這樣復制完畢后,就有兩個String中的_str指向同一個內存區域,當對象析構時,發生兩次delete,導致程序錯誤

如何解決?

方案很簡單,就是我們在復制String時,不去復制str的值,而是復制其指向的內存區域。

我們自定義拷貝構造函數如下:

string::string(const String &s)
    :str_(new char[strlen(s.str_) + 1])
{ strcpy(str_, s.str_); }

如此程序就可以正常運行,輸出兩次hello。

含有指針成員變量的類在復制時,有兩種選擇:

a) 復制指針的值,這樣復制完畢后,兩個對象指向同一塊資源,這叫做淺拷貝 shallow copy

b) 復制指針所指向的資源,復制完畢后,兩個對象各自擁有自己的資源,這叫做深拷貝 deep copy

注意:編譯器默認的是淺拷貝,此時如果需要深拷貝,需要自己編寫拷貝構造函數。

4. 賦值運算符

前面的復制構造函數說的是對象的復制,對象的賦值調用的則是對象的賦值運算符。

對於我們自定義的類(例如Student),我們是無法進行比較操作的,因為我們自定義的類沒有內置比較運算符(<= < > >= == !=),此時我們就可以通過運算符重載的規則給這些類加上運算符,這里我們需要重載的就是賦值運算符。

當然,如果我們什么也不做,系統也會自動合成一個賦值運算符,但是什么時候需要我們自己來重載賦值運算符呢,仍然是考慮深拷貝和淺拷貝的問題。

對於剛剛我們自定義的string類這個例子而言,如果我們使用系統自動合成的賦值運算符,那么同樣會引起錯誤。因為當發生賦值時,兩個string對象的_str仍然會指向同一片內存空間,那么當程序退出時,會析構兩次,發生錯誤。

因此,我們應該自己來定義string類的賦值運算符,如下:

string& string::operator=(const string &s)// 自定義賦值運算符
{
    // 防止自賦值,這樣執行delete的時候,會沖掉原有內容
    if(this == &s)
    {
        return *this;
    }
    
    // 釋放原來指向的內存空間
    delete []_str;
    
    _str = new char[strlen(s._str) + 1];
    strcpy(_str, s._str);

    return *this;
}

注意:賦值操作符,需要先釋放掉以前持有的資源,同時必須處理自賦值的問題。

5. 禁止類的復制和賦值

如果想禁止復制一個類,應該怎么辦?

顯然需要把類的復制構造函數設為private,但是這樣以來類的friend仍然可以復制該類,於是我們只聲明這個函數,而不去實現。另外,如果你不需要復制該類的對象,最好把賦值運算也一並禁用掉。

所以這里的做法是:把復制構造函數和賦值運算符的聲明設為private而不去實現。

注意:如果一個類,不需要復制和賦值,那就禁用這種能力,這可以幫助避免大量潛在的bug。

示例:

class Test
{
    public:
        Test() {}
        ~Test() {}
    private:
        Test(const Test &t);
        void operator=(const Test &t);
};

實際上,更通用的做法是寫一個類noncopyable,凡是繼承該類的任何類都無法復制和賦值。

而google開源項目風格指南建議的做法是使用 DISALLOW_COPY_AND_ASSIGN 宏:

// 禁止使用拷貝構造函數和 operator= 賦值操作的宏
// 應該類的 private: 中使用

#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
            TypeName(const TypeName&); \
            void operator=(const TypeName&)
class foo 中使用方式如下:
class Foo {
    public:
        Foo(int f);
        ~Foo();

    private:
        DISALLOW_COPY_AND_ASSIGN(Foo);
};

絕大多數情況下都應使用 DISALLOW_COPY_AND_ASSIGN 宏。如果類確實需要可拷貝,應在該類的頭文件中說明原由,並合理的定義拷貝構造函數和賦值操作。注意在 operator= 中檢測自我賦值的情況。為了能作為 STL 容器的值,你可能有使類可拷貝的沖動。在大多數類似的情況下,真正該做的是把對象的指針放到 STL 容器中。可以考慮使用智能指針。

6. 小結

1. 復制構造函數、賦值運算符以及析構函數,稱為三法則,一旦提供了其中一個,務必提供其余兩個。以我們之前自定義的string類為例:

a) 涉及到深拷貝、淺拷貝問題,所以需要提供拷貝構造函數

b) 然后,為了保持一致,賦值運算符也應該實現深拷貝

c) 既然實現深拷貝,那么必定申請了資源(例如內存),所以必然需要析構函數來手工釋放。

2. 一個空類,編譯器提供默認無參數構造函數、拷貝構造函數、賦值運算符以及析構函數,一共四個函數(針對03標准,c++11中還有移動構造函數和移動賦值運算符)。

3. 對於復制和賦值,請務必保證在程序的語義上具有一致性。

4. 如果一個類,實現了像value一樣的復制和賦值能力(意味着復制和賦值后,兩個對象沒有任何關聯,或者邏輯上看起來無任何關聯),那么就稱這個類的對象為值語義(value semantics)。如果類不能復制,或者復制后對象之間的資源歸屬糾纏不清,那么稱為對象語義(object semantics),或者引用語義(reference semantics)。

運算符重載

1. 通過自定義運算符,程序員可以自定義類的操作

運算符的重載有成員函數和友元兩種形式。有的運算符可以選擇任意一種實現,有的則必須使用友元函數的形式。

2. 重載運算符函數的參數數量與該運算符作用的運算對象數量一樣多

一員運算符有一個參數,二元運算符有兩個。對於二元運算符來說,左側運算對象傳遞給第一個參數,而右側對象傳遞給第二個參數。如果一個運算符函數是成員函數,則它的第一個(左側)運算對象綁定到隱式的this指針上,因此,成員運算符函數的(顯式)參數數量比運算符的運算對象總數少一個。

3. 對於一個運算符函數來說,它或者是類的成員,或者至少含有一個類類型的參數

4. 准則

下面的准則有助於我們在將運算符定義為成員函數還是右元函數做出抉擇:

1. 賦值(=)、下標([ ])、調用(( ))和成員訪問箭頭(->)運算符必須是成員。

2. 復合賦值運算符一般來說應該是成員,但並非必須,這一點與賦值運算符略有不同。

3. 改變對象狀態的運算符或者與給定類型密切相關的運算符,如遞增、遞減和解引用運算符,通常應該是成員。

4. 具有對稱性的運算符可能轉換成任意一端的運算對象,例如算術、相等性、關系和位運算符等,因此它們通常應該是友元函數。

程序員希望能在含有混合類型的表達式中使用對稱性運算符。例如,我們能求一個int和一個double的和,因為它們中的任意一個都可以是左側運算對象或右側運算對象,所以加法是對稱的。如果我們想提供含有類對象的混合類型表達式,則運算符必須定義成非成員函數(通常為右元形式)。

當我們把運算符定義成成員函數時,它的左側運算對象必須是運算符所屬類的一個對象。例如:

string s = "world";
string t = s + "!";   // 正確:我們能把一個const char* 加到一個string對象中
string u = "hi" + s;  // 如果+是string的成員函數,則編譯報錯

如果 operator+ 是 string 類的成員,則上面的第一個加法等價於 s.operator(“!”) 。同樣的,“hi”+ s 等價於 “hi”.operator+(s)。顯然“hi”的類型是const char*,這是一種內置類型,根本沒有成員函數。

因為標准庫的 string 類將+定義成了普通的非成員函數,所以 “hi”+ s 等價於operator+(“hi”,s)。和任何其他函數一樣,每個實參都能被轉換成形參類型。唯一的要求是至少有一個運算對象是類類型,並且兩個運算對象都能准確無誤地抓換成string。

5. 兩個注意點

1. 區分前置和后置運算符

要想同時定義前置和后置運算符,必須首先解決一個問題,即普通的重載形式無法區分這兩種情況。前置和后置版本使用的是同一個符號,意味着其重載版本所用的名字將是相同的,並且運算對象的數量和類型也相同。

為了解決這個問題,后置版本使用一個額外的(不被使用)int類型的形參。當我們使用后置運算符時,編譯器為這個形參提供一個值為0的實參。盡管從語法上來說后置函數可以使用這個額外的形參,但是在實際過程中通常不會這么做。這個形參的唯一作用就是區分前置版本和后置版本的函數,而不是真正要在實現后置版本時參與運算。

示例:

class A
{
    public:
       A operator++(int);          //后置運算符
        A operator--(int);
};


// 注意前置運算符返回的是引用(左值)
// 后置運算符返回的是臨時變量(右值)
A A::operator++(int)
{

    A ret = *this;        // 記錄當前的值
    ++*this;              // 假設前置++已定義
    return ret;           // 返回之前記錄的狀態
}

A A::operator--(int)
{
    A ret = *this;
    --*this;
    return ret;
}

// 如果想通過函數調用的方式調用后置版本,必須為它的整型參數傳遞一個值。
// 盡管傳入的值通常會被運算符函數忽略,但卻必不可少,因為編譯器只有通過它才能知道應該使用后置版本
A p;
p.operator++(0);   //調用后置版本的operator++
p.operator++();    //調用前置版本的operator++
 

這里提一下與本塊內容不是很相關的兩個易錯點:

a)static成員在類的定義體中聲明為static即可,類外定義無需再加上static。

b)指定默認形參的值只需在聲明中即可,定義中無需再指明。

2. 對箭頭運算符返回值的限定

對於其他運算符,我們可以指定它做任何事情。但是對於箭頭運算符,它永遠不能丟掉成員訪問這個最基本的含義。

當我們重載箭頭運算符時,可以改變的是從哪個對象當中獲取成員,而箭頭獲取成員這一事實則永遠不變。

箭頭運算符最后返回的永遠是指針!看如下代碼:

point -> men // 等價於point.operator->()->men;
我們假設point對象時定義了operator->的類的一個對象,則我們使用point.operator->()的結果來獲取men。其中,如果該結果是一個指針,則直接解引用獲取men的值。如果該結果本身含有重載的 operator->(),則重復調用當前步驟。

綜上:重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象。

6. 源碼

這里通過String類的編寫,講述運算符的重載的實際運用。此處代碼過長,請讀者去我的github上閱讀,地址為https://github.com/jianxinzhou/classHub/tree/master/string

OOP之泛型編程

1. 函數模板

1. 函數模板可以看做一種代碼產生器,往里面放入具體的類型,得到具體化的函數。

2. 模板的編譯分為兩步:

a) 實例化之前,先檢查模板本身語法是否正確。

b) 根據函數調用,去實例化代碼,產生具體的函數。

3. 沒有函數調用,就不會實例化模板代碼,在目標文件 obj 中找不到模板的痕跡。

4. 一個非模板函數可以和一個同名的函數模板同時存在,構成,同樣,同名的兩個模板函數之間也可以因為參數不同構成重載。

5. 模板函數重載時,選擇函數版本的一些特點:

a) 當條件相同時,優先選擇非模板函數。

b) 在強制類型轉化,與實例化模板可行之間,優先選擇實例化模板。

c) 實例化版本不可行,則去嘗試普通函數的轉化。

d) 參數是指針時,優先選擇指針版本。

e) 總之,盡可能采用最匹配的版本。

6. 在模板函數重載中,不要混合使用傳值和傳引用。盡可能使用傳引用。

7. 傳值和傳引用對於參數來說,本質區別在於是否產生了局部變量。

8. 對於返回值而言,傳值和傳引用的區別在於,返回時是否產生了臨時變量。

9. 函數的所有重載版本的聲明都應該位於該函數被調用的位置之前。

示例代碼參看:https://github.com/jianxinzhou/classHub/tree/master/S_Template/fun_template

2. 類模板

1. 模板類類似於代碼產生器,根據用戶輸入的類型不同,產生不同的class。

2. 標准庫中的vector就是一個典型的模板類,vector<int> 和 vector<string>是兩個完全不同的類。同樣,vector不是一個完整的類名。

3. 在模板類的內部,可以直接使用模板類名作為完整類名,而不必指定抽象類型T,例如 vector 內部可以直接使用 vector,而不必使用 vector<T>。

4. 模板類的編譯也分為兩步:

a) 檢查模板class的自身語法

b) 根據用戶的指定類型vector<string>,去實例化一個模板類。注意,不是實例化所有的代碼,而是僅僅實例化用戶調用的部分。

5. 模板類看做函數,輸入的是類型,輸出的是具體的class代碼,所以模板類是一個代碼產生器。

6. 模板的缺點是代碼膨脹,編譯速度慢。帶來的好處是運行速度快。

7. 在類的外面實現函數時,注意類名要寫完整,例如Stack<T>

8. 將Stack拆分成h和cpp文件,構建時產生了鏈接錯誤,原因在於:

a) 模板的調用時機和代碼的實例化必須放在同一時期。

b) 編譯Stack.cpp時,編譯器找不到任何用戶調用的代碼,所以得到的 Stack.o文件為空,可以使用nm -A查看。

c) 編譯main.cpp時,編譯器獲取用戶的調用,了解應該去實例化哪些代碼(pop push),但是這些代碼存在於另一模塊,編譯器無法實例化(#include進來的.h文件只有聲明)。

d) 鏈接期間,因為以上的原因,需要鏈接的代碼並沒有產生,找不到pop、 push等函數的代碼,報錯。

因此比較簡單的做法是:我們要把類的定義和實現全部放到同一個文件中,可以為 .h 文件,但最好使用 .hpp 文件。

示例代碼參看:https://github.com/jianxinzhou/classHub/tree/master/S_Template/class_template

3. 缺省模板實參

1. 對於類模板,你還可以為模板參數定義缺省值,這些值就被稱為缺省模板實參。而且,它們還可以引用之前的模板參數。

2. 實際上,STL中的stack、queue、priority_queue等並不是容器,而是根據其他容器適配而來的適配器(adapter),例如 stack:

template <class T, class Container = deque<T> > class stack;

stack 默認采用 deque 作為底層實現,但是用戶可以自行制定容器。

3. STL中的容器大多使用了缺省模板參數,例如 map:

template < class Key,                                     // map::key_type
           class T,                                       // map::mapped_type
           class Compare = less<Key>,                     // map::key_compare
           class Alloc = allocator<pair<const Key,T> >    // map::allocator_type
           > class map;
4. 示例代碼參看:
https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E7%BC%BA%E7%9C%81%E6%A8%A1%E6%9D%BF%E5%AE%9E%E5%8F%82

4. 模板參數不僅可以為類型,還可以為數值

1. 值得注意的是:數值也是類名的一部分,例如 Stack<int, 5> 和 Stack<int, 10> 不是同一個類,二者的對象也無法相互賦值。

2. 為了解決上述的問題,可以使用成員模板,實現 Stack<int, 5> 和 Stack<int, 10>,甚至是和 Stack<double, 12> 之間的賦值,方法就是在 Stack 模板內部編寫:

template <typename T2, int MAXSIZE2>
Stack<T, MAXSIZE> &operator=(const Stack<T2, MAXSIZE2> &other);

注意這個函數的存在,並不影響編譯器為我們提供默認的賦值運算符(默認的賦值運算符是同類型的)。

3. 具體代碼參看:

https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E6%A8%A1%E6%9D%BF%E5%8F%82%E6%95%B0%E4%B8%BA%E6%95%B0%E5%80%BC

https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E6%88%90%E5%91%98%E6%A8%A1%E6%9D%BF

5. 在模板定義內部指定類型

除了定義數據成員或函數成員之外,類還可以定義類型成員。例如,標准庫的容器類定義了不同的類型,如 size_type,使我們能夠以獨立於機器的方式使用容器。如果要在函數模板內部使用這樣的類型,必須告訴編譯器我們正在使用的名字指的是一個類型。必須顯式地這樣做,因為編譯器(以及程序的讀者)不能通過檢查得知,由類型形參定義的名字何時是一個類型何時是一個值。例如,在模板中編寫:

T::value_type * p;

1. 編譯器可能將其解釋為乘法,為了顯式告訴編譯器這是定義一個變量,需要加上typename

2. typename T::value_type * p;

3. 示例代碼如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

template <class Parm, class U>
Parm fcn(Parm *array, U value)
{
    typename Parm::size_type * p;
    return *array;
}

int main(int argc, const char *argv[])
{
    vector<int> vec;
    fcn(&vec, 12); 
    return 0;
}

6. 注意點

1. 對於非引用類型的參數,在實參演繹的過程中,會出現從數組到指針的類型轉換,也稱為衰退(decay)。

2. C中只有傳值,所以C語言中把數組當做函數的參數,總是引發decay問題,丟失數組的長度信息。

3. 引用類型不會引發衰退(decay)。

4. 具體代碼參看:

https://github.com/jianxinzhou/classHub/tree/master/S_Template/%E8%A1%B0%E9%80%80

oop之內存分配

1. new & delete

C++中的 new 運算符,具體工作流程如下:

1. 調用 operator new 申請原始內存

2. 調用 place new 表達式,執行類的構造函數

3. 返回內存地址

而 delete 操作符的工作是:

1. 調用對象的析構函數

2. 調用 operator delete 釋放內存

注意:free 干的事情是釋放內存,delete 干的事情是調用析構函數,然后釋放內存。

示例代碼如下:

#include <iostream>
using namespace std;

class Test
{
public:
    Test() { cout << "Test" << endl; }
    ~Test() { cout << "~Test" << endl; }
};

int main(int argc, char const *argv[])
{

    //這里的pt指向的是原始內存
    Test *pt = static_cast<Test*>(operator new[] (5 * sizeof(Test)));
    
    for(int ix = 0; ix != 5; ++ix)
    {
        new (pt+ix)Test(); //調用定位new運算式 執行構造函數
    }

    for(int ix = 0; ix != 5; ++ix)
    {
        pt[ix].~Test(); //調用析構函數,但是並未釋放內存
    }
    operator delete[] (pt); //釋放內存

}

2. 標准庫Allocate

c++中 new 運算符涉及到的工作無非以下兩步:

1. 申請原始內存

2. 執行構造函數

delete 涉及到了兩個工作:

1. 執行析構函數

2. 釋放原始內存

實際上,標准庫提供了一種更加高級的手段實現內存的分配和構造,下面我們介紹 std::allocator<T>。

對應於上面的 new 和 delete , allocator 提供了如下四個操作:

a.allocate(num)              為 num 個元素分配原始內存

a.construct(p)               將 p 所指的元素初始化(執行構造函數)

destroy(p)                    銷毀 p 指向的元素 (執行析構函數)

deallocate(p, num)         回收 p 指向的“可容納 num  個元素”的內存空間

來看如下示例:

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

class Test
{
    public:
        Test()  { cout << "Test"  << endl; }
        ~Test() { cout << "~Test" << endl; }

        Test(const Test &t)
        {
            cout << "Copy..." << endl;
        }
};


int main(int argc, const char *argv[])
{
    allocator<Test> alloc;
    // 此時pt指向的是原始內存
    Test *pt = alloc.allocate(3); // 申請3個單位的Test內存
    
    {
        // 構建一個對象,使用默認值
         // 注意調用的是拷貝構造函數
         alloc.construct(pt, Test()); 
        alloc.construct(pt+1, Test());
        alloc.construct(pt+2, Test());
    }
    
    // 執行指針所指對象的析構函數
     alloc.destroy(pt);
    alloc.destroy(pt+1);
    alloc.destroy(pt+2);
    
    // 釋放原始內存
    alloc.deallocate(pt, 3);
    return 0;
}

/*
output:

Test         注意Test與~Test是臨時對象執行構造函數和析構函數的結果       
Copy...
~Test

Test
Copy...
~Test

Test
Copy...
~Test

~Test
~Test
~Test
*/

這里注意,allocator提供的 allocate 函數與 operator new 函數區別在於返回值,前者返回的是指向要分配對象的指針,而后者返回的是 void *,所以前者更加安全。

還有一點,construct一次只能構造一個對象,而且調用的是拷貝構造函數。實際上,標准庫提供了三個算法用於批量構造對象(前提是已經分配內存),如下:

uninitialized_fill(beg, end, val)               // 以val初始化[beg, end]

uninitialized_fill_n(beg, num, val)          // 以val初始化beg開始的num個元素

uninitialized_copy(beg, end, mem)        // 以[beg, end)的各個元素也初始化mem開始的各個元素

以上三個函數操控的對象都是原始內存,示例如下:

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <stdlib.h>
using namespace std;

class Test
{
    public:
        Test(int val)
            :val_(val) 
        { cout << "Test ..." << endl; }

        ~Test()
        {
            cout << "~Test ..." << endl;
        }

        Test(const Test &t)
            :val_(t.val_)
        {
            cout << "Copy ... " << endl;
        }
        
        // just for test, so do as a public member
        int val_;
};

int main(int argc, const char *argv[])
{
    // 利用malloc申請原始內存
    Test *pt = (Test*)malloc(3 * sizeof(Test));

    Test t(12);

    uninitialized_fill(pt, pt+3, t);
    cout << pt[0].val_ << endl;

    Test *pt2 = (Test*)malloc(2 * sizeof(Test));
    uninitialized_copy(pt, pt+2, pt2);


    free(pt);
    free(pt2);
    
    return 0;
}

/*
output:
Test ...
Copy ...
Copy ...
Copy ...
12
Copy ...
Copy ...
~Test ...
*/

注意,以上示例中,free 只會釋放空間,並不會執行析構函數。

這里注意標准庫的 copy、fill 函數與 uninitialized_ 系列函數的區別:

copy、fill 等操作的是已經初始化對象的內存,因此調用的是賦值運算符

而uninitialized_針對的是原始內存,調用的是拷貝構造函數

至此,我們可以總結出分配原始內存的三種手段:

1. 使用malloc

2. 使用operator new

3. allocator的allocate函數

這三者從上到下,是一個由低級到高級的過程。

那么執行構造函數,有兩種手段:

1. 使用placement new運算符

2. 使用allocator的construct函數

3. 小結

1. POD 數據僅僅申請內存就可以直接使用,不需要執行特殊的構造工作(指執行構造函數),可以直接使用malloc。因此,對於POD數據,可以通過memcpy系列函數,直接操控內存達到目的。 注意,C語言中的數據都是POD類型。

2. C++中的非POD變量經過兩個步驟生成:

a) 申請原始內存(字節數組)。

b) 在內存上執行構造函數。

因此,對於非POD數據通常使用 new 一步到位,而不使用malloc。

3. 切記:allocator 執行 construct 時調用的是拷貝構造函數

4. 總之,C語言中的數據都是 POD 類型,使用原始內存即可,但是C++中的大部分都是POD類型,需要執行相應的初始化函數,所以,在C++中應該盡可能避免使用memcpy之類的直接操控原始內存的函數

5. POD指的是原生數據,包括int、double等基本數據,以及包含基本數據的結構體(struct或class),但是 class 或者 strut 不能包含自定義的構造函數,不能含有虛函數、更不能包含非POD數據。這個定義並不精確,但是夠用。可以參看我的這篇博文,Aggregate類型以及值初始化

oop之繼承

之所以把繼承放在模板之后來說,是因為模板與泛型編程這塊博大精深,真正要精通的話,還得去看 C++ Templates 這本經典的傳世之作,不過相信我,在你沒有接觸過函數式編程語言之前,你是絕對看不懂此書的,不過好在,我們實際工作中所用到的模板的知識非常有限,本文之前介紹的已然夠用了。因此,在繼承這塊,我不會再講模板相關的東西。

1. 類的派生

我們寫程序時,經常會遇到具有類似屬性,但是細節或者行為存在差異的組件。在這種情形下,一種解決方案是將每個組件聲明為一個類,並在每個類中實現所有的屬性,這將導致大量重復的代碼。另一種解決方案是使用繼承,從同一個基類派生出類似的類,在基類中實現所有通用的功能,並在派生類中覆蓋基本的功能,以實現讓每個類都獨一無二的行為。

C++派生語法如下:

// 基類

class Base {

// Base class members

};

// 派生類

class Derived: public Base {

// derived class members

};

2. protected 關鍵字

1. protected 僅限於本類和派生類可以訪問。

2. 經過 public 繼承,父類中的 private、protected、public 在子類中的訪問權限為:不可訪問的、protected、public。

3. 繼承體系下的函數調用

通過子類對象去調用函數,遵循以下規則:

a) 父類中的非 private 函數,可以由子類去調用。

b) 子類額外編寫的函數,也可以正常調用。

c) 子類中含有和父類同名的函數,無論參數列表是否相同,調用的始終是子類的版本。(如果想執行父類的版本,必須顯式指定父類的名稱)

注意:

父類和子類含有同名函數,那么通過子類對象調用函數,總是調用的子類的版本。這叫做子類的函數隱藏(hidden)了父類的函數。只有顯式指定父類類名,才可以調用被隱藏的函數。

只要我們在派生類中寫了一個函數,和基類的函數重名(無論參數表是否相同),那么通過派生類對象調用的總是派生類重寫的函數。

4. 繼承時的對象布局

派生類內部含有一個無名的基類對象,之后才是派生類自己的成員,所以構造派生類時會先構造基類。

1. 子類對象中含有一個父類的無名對象。

2. 構造子類對象時,首先需要調用父類的構造函數,其次是子類的構造函數,析構的順序與之相反。

3. 子類的對象可以賦值給父類的對象,其中子類對象多余的部分被切除,這叫做對象的切除問題。但是,父類對象賦值給子類對象是非法的。

小結

1. 派生類的構造順序:

a) 構建基類對象(執行基類對象構造函數)

b) 構造成員對象(執行成員對象構造函數)

c) 執行派生類構造函數函數體

實際上以上三個部分,也都是屬於派生類構造函數的,a 和 b 實際上應該在派生類構造函數的初始化列表中完成,如果沒有在其初始化列表中顯式初始化,則會執行默認初始化。

示例:

// 此處,Student類 public 繼承 Person 類
Student(int id, const string &name, int age, const string &school)
         :Person(id, name, age), school_(school)
{ }

2. 派生類的析構順序

與派生類的構造順序相反,如下:

a) 執行派生類析構函數函數體

b) 銷毀成員對象(執行成員對象的析構函數)

c)銷毀基類對象(執行基類對象的析構函數)

5. 繼承與復制控制

這里涉及到兩個函數,拷貝構造函數和賦值運算符。如果自己實現這兩者,都必須顯式調用基類的版本。示例代碼如下:

// 此處,Student類 public 繼承 Person 類
Student(const Student &s)

    :Person(s), school_(s.school_)

{ }


Student &operator=(const Student &s)
{

    if(this != &s)

    {

        //先對基類對象賦值

        //再對自身變量賦值

        Person::operator=(s);

        school_ = s.school_;

    }
    return *this;
}

總而言之:子類在構造對象時通過初始化列表,指定如何初始化父類的無名對象。而拷貝構造函數用子類去初始化父類對象,賦值運算符中則是顯式調用父類的賦值運算符。

6. 禁止復制

之前講復制控制時,我們已經提過禁止一個類復制的做法是將其拷貝構造函數和賦值運算符設為私有,而且只有聲明,沒有實現(你們是否還記得谷歌開源風格的那個寫法)。

如果我們這里有10個類都需要禁止復制,那么可以每個類都進行上面的操作,但這樣導致大量的重復代碼,好的解決方案是采用繼承,如下:

class NonCopyable
 
{
    public:
 
        NonCopyable() {}
 
        ~NonCopyable() {}
 
    private:
 
        NonCopyable(const NonCopyable &);
        void operator=(const NonCopyable &);
};
 

// private 可以省略,默認就是 private 繼承
class Test : private NonCopyable
{ };

這樣凡是繼承了 NonCopyable 的類均失去了復制和賦值的能力。

注意:NonCopyable 要采用私有繼承。

7. 總結

1. OOP的第二個性質稱為繼承。

2. public繼承,塑造的是一種“is-a”的關系(子類是父類)。在繼承體系中,從上到下是一種具體化的過程,而從下到上則是抽象、泛化的過程。

3.一個類包含另一個類,叫做“has-a”關系,也稱為類的組合。

OOP之動態綁定

這是這篇文章的最后一個部分。當我們講面向對象編程的時候,常會提到其第三個特征為動態綁定。事實上,動態綁定屬於運行期多態。前面我們講過的函數重載屬於編譯期多態。

這里必須注意,傳統的說法,OOP的三大特征封裝、繼承、多態中的多態僅包含運行期多態。編譯期多態並不是面向對象編程特征的一部分

1. 大前提

基類的指針或者引用指向派生類對象。

2. 靜態綁定

靜態綁定:編譯器在編譯期間根據函數的名字和參數,決定調用哪一份代碼,這叫做靜態綁定,或者早綁定。

在靜態綁定期間,通過通過基類指針調用函數,有以下幾種情況:

a) 基類中存在的函數,可以調用

b) 子類額外添加的函數,不可以

c) 父子類同名的函數,調用的是父類的版本。

也就是說靜態綁定期間,通過基類指針只能調用基類自身的函數。

以上的原因在於:通過基類指針調用函數,編譯器把基類指針指向的對象視為基類對象。(實際上更深層次的原因就是因為派生類中含有基類的一個無名對象,這樣將派生類的指針賦值給基類指針,實際上基類指針指向的恰好就是派生類中基類的那個對象(基類類型的字節數))

注意:派生類指針可以轉化為基類指針,這叫做“向上塑形”,這是絕對安全的,因為繼承體系保證了“is-a”的關系,然而,基類指針轉化為派生類指針則需要強制轉化,而且需要人為的保證安全性,“向下塑形”本質上是不安全的(讀者可以自己想想怎么從內存上來解釋)

3. 動態綁定

動態綁定:編譯器在編譯期間不確定具體的函數調用,而是把這一時機推遲到運行期間,叫做動態綁定,或者晚綁定。

1. C++中觸發動態綁定的條件:

a) virtual虛函數

b) 基類的指針或者引用指向了派生類的對象

2. 觸發多態綁定后,virtual 數的調用不再是編譯期間確定,而是到了運行期,根據基類指針指向的對象的實際類型,來確定調用哪一個函數。

以ps->print();為例:

a) 靜態綁定,根據的是 ps 指針本身的類型 (基類 *ps = &派生類對象,基類* 就是 ps 本身的類型)

b) 動態綁定,根據的是 ps 指向實際對象的真實類型。(派生類是 ps 實際指向的類型)

3. 動態綁定的執行流程:

運行期間,因為觸發了動態綁定,所以先去尋找對象的vptr(虛指針),根據 vptr 找到虛函數表(vtable),里面存儲着虛函數的代碼地址,根據 vtable 找到要執行的函數。(注意派生類會從基類繼承 vptr)

虛函數具有繼承性,如果子類的同名函數,名字和參數與父類的虛函數相同,且返回值相互兼容,那么子類中的該函數也是虛函數。

子類在繼承父類虛函數的時候,如果對函數體進行了改寫,那么子類的虛函數版本會在 vtable 中覆蓋掉父類的版本,這叫做函數的覆蓋。

4. 函數的重載、隱藏和覆蓋

三者必定是針對同名函數。

1. 重載

構成函數重載的要素有:函數形參表以及成員函數的const屬性。

2. 隱藏

實際上,凡是不符合函數覆蓋的情形,都屬於函數隱藏。

每個類都保持着自己的作用域,在該作用域中定義了成員的名字。在繼承情況下,派生類的作用域嵌套在基類作用域中。如果不能在派生類作用域中確定名字,就在外圍基類作用域中查找該名字的定義。(派生類作用域位於基類作用域之內

以下情形屬於隱藏:

i. 父類中的非虛函數,子類名字參數與其一致

ii. 父類中的非虛函數,子類對其參數或返回值做了改動

iii. 父類中的虛函數,但是子類中對其參數做了改動,或者返回值不兼容。

總結一下就以下兩點:

a)對於基類的非虛函數,派生類中只要有同名的函數,就屬於隱藏。

b)對於基類的虛函數,派生類中有同名函數,且該函數參數類型與基類不一致,或者返回值不兼容,就屬於隱藏。

3. 覆蓋

覆蓋必定是觸發多態的情形。

父類中的虛函數,子類的名字、參數與其完全相同,返回值兼容

5. 注意

1. 不要改動從父類繼承而來的非 virtual 函數(即不要觸發函數的隱藏)。why?因為這樣做根本沒有意義。

2. 如果父類中的某函數為虛函數,那么有以下兩個選擇:

a) 不做任何改動,采用其默認實現。

b) 覆蓋父類的實現,提供自己的行為。

3.  virtual void run() = 0; 聲明了一個純虛函數,此函數只有聲明,沒有實現。包含了純虛函數的類,成為了抽象類。子類在繼承抽象類后,必須將其中所有的純虛函數全部實現,否則仍然是一個抽象類。

4. 在繼承體系中,應該把基類的析構函數設為virtual。

5. 動態綁定是運行期的多態,是面向對象的第三個特征,也成為動多態。靜多態一般指的是函數重載與模板,但是,這個多態特性不屬於面向對象的特性。

(全文完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM