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

OOP之類和對象

1. this指針的引入

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

/* 假設如今有一個類Sales_data,以及其很是量Sales_data類型對象,則該隱式的this指針能夠寫成以下僞代碼形式 */
Sales_data *const this = &total;

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

struct A{
    int a;

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

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

2. const成員函數

緊隨參數列表以後的const關鍵字做用爲:修改隱式this指針所指向的對象的類型,以下:git

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

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

注意如下幾點: github

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. 動態綁定是運行期的多態,是面向對象的第三個特徵,也成爲動多態。靜多態通常指的是函數重載與模板,可是,這個多態特性不屬於面向對象的特性。

(全文完)

相關文章
相關標籤/搜索