C++之模板類_容器_迭代器_算法_GCC_make


站在編譯器和C的角度剖析c++原理, 用代碼說話


類模板基礎

首先什麼是模板: 模板就是把要處理的函數或類的類型參數化,表現爲參數的多態性. 模板用來表現邏輯結構相同,但具體數據元素類型不同的對象的通用行爲.函數模板我們在上一篇中已經說過了,所以自行回看一下. 我們這裏討論類模板:
類模板用於實現類所需數據的類型參數化.
我們先定義一個模板類:

template<typename T>
class A
{
public:
    A(int a)
    {
        this->a = a;
    }
    ~A()
    {
    }
    T getA()
    {
        return a;
    }
    void setA(T a)
    {
        this->a = a;
    }
protected:
private:
    T a;
};

這裏需要強調一點的是類屬參數在類中至少要出現一次. 這樣的話,

void printA(A<int> *p)
 {
     cout<<"打印a:"<<p->getA()<<endl;
 }
 void printA2(A<char> *p)
 {
     cout<<"打印a:"<<p->getA()<<endl;
 }
 void main()
 {
     A<char> a1;
     A<int> b1(19);
     b1.setA(10);
     cout<<"打印a:"<<b1.getA()<<endl;
     return 0;
 }

我們再考慮一種情況就是如果存在繼承模板類的情況呢?

class C : public A<int>
{
public:
    C(int c, int a) :A<int>(a)
    {
        this->c = c;
    }
protected:
private:
    int c;
};
void printC(C *myc)
{
    cout<<myc->getA()<<endl;
}
int main()
{
    C  myc(1, 2);
    printC(&myc);
    return 0;
}

這裏注意繼承的時候也要public A<int>寫明屬性類型. 因爲編譯器就是根據這個類型來重新構造類的. 經過測試發現一個問題就是在繼承中,如果父類沒有帶參的構造方法的話,在構造子類的時候是不會去父類中的.
類模板派生普通類,在定義派生類時要對基類的抽象類參數實例化. 從普通類派生模板類,意味着派生類添加了抽象類數據成員.

當類模板遇上static

template<typename T>
class A
{
public:
    static int m_a;
protected:
private:
    T a1;

};
template<typename T>
int A<T>::m_a=0;
int main()
{
    A<int> a1;
    A<int> a2;
    A<int> a3;
    A<char> b1;
    A<char> b2;
    A<char> b3;
    a1.m_a ++;
    b3.m_a = 199;
    cout<<a3.m_a<<endl;//1
    cout<<b2.m_a<<endl;//199
    return 0;
}

首先我們知道靜態成員要在類外面初始化進行激活. 注意使用形式: 首先是template<typename T>也得指明,然後是涉及到類,就得跟上A<T>, 最後在寫上類型或者返回值就行.
經過程序的執行,我們會發現只要是一種數據類型是共用一個靜態區成員變量的,但是不同的數據類型間是獨立的. 由此更能看出編譯器根據類型進行重新創建類.

當模板類遇上友元函數

在模板類中能夠聲明各種友元關係:
1. 一個函數或函數模板可以是類或者類模板的成員
2. 一個類或類模板可以是類或類模板的友元類

template<class T>
class Complex
{
public:
    Complex(T  Real = 0,T Image=0 );
    Complex(T a);
    friend  Complex operator+(Complex &c1, Complex &c2 );//返回匿名對象
    void print();
protected:
private:
    T  Real,Image;
};
template<class T>
Complex<T>::Complex(T  Real, T Image)
{
    this->Real = Real;
    this->Image = Image;
}
template<class T>
Complex<T>::Complex(T a)
{
    this->Real = a; this->Image = 0;
}
template<class T>
void Complex<T>::print()
{
    cout<<this->Real<<" + "<<this->Image<<endl;
}
template<class T>
complex<T> operator+(Complex<T> &c1, Complex<T> &c2 )
{
    Complex<T> tmp(c1.Real+c2.Real, c1.Image + c2.Image);
    return tmp;
}

相比我們對友元類友元函數還沒陌生吧,如果有些模糊了返回去看看之前的文章吧. 這裏簡單提一下,友元函數是在類外面定義的函數然後爲了使用類中的參數所以要與類構成好朋友的關係.
當我們執行以上函數的時候:

int main0002()
{
    Complex<float> c1(1.0, 2.0);
    Complex<float> c2(3.0, 4.0);
    c1.print();
    Complex<float> c3 = c1 + c2;
    c3.print();
    return 0;
}

是會報錯的:

無法解析的外部符號 "
 class Complex<float> __cdecl operator+(class Complex<float> &,class Complex<float> &)
 " (??H@YA?AV?$Complex@M@@AAV0@0@Z),該符號在函數 _main 中被引用

出現這種錯誤的原因是當編譯器通過類型去構建出一個新的類的時候,沒法解析operator+這個符號. 所以這種情況下,我們只能將友元函數的實現放在模板類中了:

    friend Complex operator-(Complex &c1, Complex &c2 )
    {
        Complex tmp(c1.Real-c2.Real, c1.Image - c2.Image);
        return tmp;
    }

所以一般情況下都是要用到模板類的時候,都是把.h和.c存放在一起去實現的, 只是對友元函數特殊,出現錯誤就是編譯器兩次編譯按照友元模板的時候出錯了.

STL簡介

Standard Templete Library是c++的一種標準庫,是c++用template機制來表達.
STL使用泛型技術來設計完成的實例,就像MFC是用面向對象技術來設計完成的實例.
STL抽象出這些基本屬性成功的將算法和這些數據結構分離,在沒有效率損失的情況下,得到了極大的彈性.
那麼STL的組成是什麼呢?
1. 容器(container)
2. 算法(Algorithm)
3. 迭代器(Iterator)
4. 仿函數(Function Object)
5. 適配器(Adaptor)
6. 空間適配器(allocator)

容器

容器的分類:
1. 序列式容器(sequences containers)
每個元素都有固定的位置,取決於插入時間和地點,與元素值無關.
vector, deque, list
2. 關聯式容器(Associated containers)
元素位置取決於特定的排序準則,和插入順序無關
set, multiset, map, multimap

Vector

vector是一個模板類,在使用模板類的時候要指明具體的類型.

void main01()
{
    vector<int> v1(5);  //相當於 int v1[5];
    //vector<char> v1(5);  //相當於 char v1[5];
    for (int i=0; i<5; i++)
    {
        v1[i] = i+1;//重載了[]操作符的
    }
    for (int i=0; i<5; i++)
    {
        cout<<v1[i];
    }
    return 0;
}

我們能發現vector重載了一些運算符,那麼我們看看源碼中的定義:

 reference       operator[](size_type n);
 const_reference operator[](size_type n) const;

這裏值列舉單獨的一個,可自行查看.
我們來讓vector作爲函數參數傳遞:

void printfV(vector<int> &c)
{
    int size = c.size();
    for (int i=0; i<size; i++)
    {
        printf("%d ", c[i]);
    }
}
int main(void)
{
    vector<int> c2(20);
    c2 = c1;//重載了等號操作符
    printfV(v2);
    return 0;
}

這裏有人會問vector相當於數組,但是在函數傳遞的時候不是會退化成指針嗎,這樣的話不是需要進行數組長度的傳遞嗎?其實不然,因爲vector是模板類,是個類,在類中已經是封裝了屬性方法等的. 將類直接做函數參數傳遞是可以的.

    vector<int>v3(20);
    v3.push_back(100);
    v3.push_back(101);
    printfV(v3);

vector會將20個單元大小全部進行初始化,當我們push_back的時候,會有18個0打出來.
到這裏是基本數據類型的用法,我們傳個類用用:

struct Teacher
{
    char name[10];
    int age;
};
void main()
{
    Teacher t1, t2, t3;
    t1.age = 11;
    t2.age = 22;
    t3.age = 33;
    vector<Teacher> v1(5); 
    v1[0] = t1;
    v1[1] = t2;
    v1[2] = t3;
   printf("age:%d \n", v1[0].age);
    return 0;
}

我們再來看一下傳入類指針:

int main()
{
    Teacher t1, t2, t3;
    t1.age = 11;
    t2.age = 22;
    t3.age = 33;
    vector<Teacher *> v1(5);
    v1[0] = &t1;
    v1[1] = &t2;
    v1[2] = &t3;
    for (int i=0; i<v1.size(); i++)
    {
        Teacher *tmp = v1[i];
        if (tmp != NULL)
        {
            printf("age:%d", tmp->age);
        }
    }
    return 0;
}

是不是感覺很方便,什麼類型都能傳入,這就是用模板類實現泛型的強大之處.

Iterator

const int arraysize = 10;
int ai[arraysize] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *begin = ai;
int *end = ai + arraysize;
for(int *pi = begin; pi != end; pi++)
{
    count << *pi << " ";
}

上面一段的代碼就是利用指針來進行數組的遍歷,這也就是我們引出的迭代器的本質. 但是當我們使用鏈表的時候存儲空間並不是連續的,所以我們不能通過簡單的指針的累加進行查找, 那麼就會想到能不能通過一個類似於指針的類對非數組的類進行遍歷呢? 我們就能以同樣的方式遍歷所有的數據結構(容器).
迭代器是一個”可遍歷STL容器內全部或部分元素”的對象. 在迭代器中重載了一些操作符:
Operator *:
傳回當前位置上的元素值, 如果該元素擁有成員, 你可以透過迭代器,直接以operator->取用
Operator++:
將迭代器前進至下一個元素. 大多數迭代器還可以使用operator--退回到前一個元素.
Operater==Operator !=:
判斷兩個迭代器是否指向同一位置.
Operator=:
爲迭代器的賦值(將其指元素的位置指派過去)
有了這個基礎知識我們就能介紹list

List

首先我們在c語言中都接觸過鏈表,這也就是是c++爲我們封裝了些方法讓我們去很方便的操作鏈表. 因爲鏈表不像數組一樣那種連續的存儲空間,所以當遍歷的時候就需要我們用到迭代器.
我們先來簡單的用一下list:

void main()
{
    list<int> l;
    for (int i=0; i<5; i++)
    {
        l.push_back(i+1);
    }
    cout<<l.size()<<endl;
    list<int>::iterator current = l.begin();//迭代器就相當於是個指針在不斷指向

    while (current != l.end())
    {
        cout<<*current<<endl;
        current ++;
    }
    //獲取鏈表的開頭,賦值給迭代器
    current = l.begin(); //0
    current ++; //1
    current ++; //2
    current ++; //3
    l.insert(current, 100);
    printf("\n插入位置測試,請你判斷是從0位置開始,還是從1位置開始\n");
    for (list<int>::iterator p = l.begin(); p!= l.end(); p++)
    {
        cout<<*p<<" ";
    }
    return 0;
}

我們能夠看到容器中迭代器的使用方式,每個容器模板類中都有自己的iterator方法的. 從這裏也能夠看出鏈表就是指針. 然後l.end()是指向了鏈表的最後一個元素的後一位. 在list中在第三號位置插入就是在第三號前面加入.
我們用類來作爲鏈表類型:

struct Teacher
{
    char name[64];
    int age;
};
void main()
{
    list<Teacher> l;
    Teacher t1, t2, t3;
    t1.age = 11;
    t2.age = 12;
    t3.age = 13;
    l.push_back(t1);
    l.push_back(t2);
    l.push_back(t3);
    for (list<Teacher>::iterator p = l.begin(); p!=l.end();p++)
    {
        printf("tmp:%d ", p->age);
    }
    return 0;
}

相當於是鏈表中的值是Teacher結構體. 然後迭代器相當於是指針,隨意就相當於是指向了每個結構體的指針.

int main()
{
    list<Teacher *> l;

    Teacher t1, t2, t3;
    t1.age = 11;
    t2.age = 12;
    t3.age = 13;
    l.push_back(&t1);
    l.push_back(&t2);
    l.push_back(&t3);
    for (list<Teacher *>::iterator p = l.begin(); p!=l.end();p++)
    {
        Teacher * tmp  = *p;
        printf("tmp:%d ", tmp->age);
    }
    return 0;
}

相當於是將Teacher結構體的指針放入到了list的值域中了. 這樣的話迭代器再取地址,就相當於指向指針的指針了,也就是二級指針了.

Stack

棧是先進後出的結構.

void printStack(stack<int> &s)
{
    //遍歷棧的所有元素,必須要一個一個的彈出元素
    while(!s.empty())
    {
        //獲取棧頂元素
        int tmp = s.top();
        //彈出棧元素
        s.pop();
        printf("tmp:%d ", tmp);
    }
}
void main()
{
    //定義了容器類,具體類型int
    stack<int> s;//也是模板類
    for (int i=0; i<5; i++)
    {
        s.push(i+1);
    }
    //遍歷棧的所有元素,必須要一個一個的彈出元素
    while(!s.empty())
    {
        //獲取棧頂元素
        int tmp = s.top();

        //彈出棧元素
        s.pop();
        printf("tmp:%d ", tmp);
    }
    printf("\n");
    printStack(s);
    return 0;
}

同樣的我們用類作爲類型:


struct Teacher
{
    char name[100];
    int age;
};

void main()
{
    stack<Teacher> s;
    Teacher t1, t2, t3;
    t1.age = 31;
    t2.age = 32;
    t3.age = 33;
    s.push(t1);
    s.push(t2);
    s.push(t3);
    while(!s.empty())
    {
        Teacher tmp = s.top();
        s.pop();
        printf("tmp:%d ", tmp.age);
    }
    printf("\n");
    return 0;
}

我們再使用類指針作爲參數類型:

int main()
{
    stack<Teacher *> s;
    Teacher t1, t2, t3;
    t1.age = 31;
    t2.age = 32;
    t3.age = 33;
    s.push(&t1);
    s.push(&t2);
    s.push(&t3);
    while(!s.empty())
    {
        Teacher *tmp = s.top();
        s.pop();
        printf("tmp:%d ", tmp->age);
    }
    printf("\n");
    return 0;
}

在這裏我們將容器用圖畫出來更形象的比喻一下:
這裏寫圖片描述

大小端與內存中到底是怎麼樣存儲數組的

在UNP那本書中碰到一個大小端的問題,這裏又涉及到棧的問題,所以很想知道棧到底是怎麼樣分配內存地址的. 我們通常畫棧都是開口朝上的,但是其實並不是的.
這裏寫圖片描述
首先我們能夠看出的是棧底是高地址. 並且數組並不是a[0]先入棧的,它的過程是當一旦寫int[3]馬上爲其分配3個四字節的內存空間在壓入棧中,但是當a[0]賦值的時候,會從數組底部開始賦值的. 所以棧中是根據數據類型整體操作的. 內部分配又是另一種方式了. 這個圖在搞懂大端小端很有用.

Queue

隊列顧名思義就是現進先出的數據結構.

void main()
{
    queue<int> q;
    for (int i=0; i<5; i++)
    {
        q.push(i+1);
    }
    while (!q.empty())
    {
        //獲取隊列的第一個元素
        int tmp = q.front();
        printf("tmp:%d ", tmp);
        q.pop();
    }
    return 0;
}
struct Teacher
{
    char name[64];
    int age ;
};
void printQ(queue<Teacher *>  &myq)
{
    while (!myq.empty())
    {
        Teacher *tmp = myq.front();
        if (tmp != NULL)
        {
            printf("tmp:age : %d ", tmp->age);
        }
        myq.pop();
    }
}
int main()
{
    queue<Teacher *> q;
    Teacher t1, t2, t3;
    t1.age = 32;
    t2.age = 33;
    t3.age = 34;
    q.push(&t1);
    q.push(&t2);
    q.push(&t3);
    printQ(q);
    return 0;
}

與stack的思路是一樣的.

Algorithm

泛型算法通則:
1. 所有算法的前兩個參數都是一對iterators:[first, last), 用來指出容器內的一個範圍內的元素.
2. 每個算法的聲明中,都表現出它所需要的最低層的iterator類型.
但我們在使用STL中的算法的時候需要引入#include "algorithm".

void myPrintFunc(int &v)
{
    cout << v;
}
int mycompare(const int &a, const int &b)
{
    return a < b;
}
int main()
{
    vector<int> v(10);
    for (int i = 0; i < v.size(); i++) {
        v[i] = rand() % 10;
    }
    for (int i = 0; i < v.size(); i++) {
        printf("%d ", v[i]);
    }
    for_each(v.begin(), v.end(), myPrintFunc);
    sort(v.begin(), v.end(), mycompare);
    return 0;
}

STL對數組進行排序的時候,stl實現了排序算法,但是排序的標準可以由用戶自己來定義.

GCC

gcc(GNU C Compiler)編譯器的作者是Richard Stallman,也是GNU項目的奠基者,那麼什麼是gcc?
gcc是GNU Compiler Collection的縮寫。最初是作爲C語言的編譯器(GNU C Compiler),現在已經支持多種語言了,如C、C++、Java、Pascal、Ada、COBOL語言等。gcc支持多種硬件平臺,甚至對Don Knuth 設計的 MMIX 這類不常見的計算機都提供了完善的支持.
GCC的主要特徵:
1. gcc是一個可移植的編譯器,支持多種硬件平臺
2. gcc不僅僅是個本地編譯器,它還能跨平臺交叉編譯
3. gcc有多種語言前端,用於解析不同的語言。
4. gcc是按模塊化設計的,可以加入新語言和新CPU架構的支持
5. gcc是自由軟件
GCC的編譯過程:
預處理(Pre-Processing)
編譯(Compiling)
彙編(Assembling)
鏈接(Linking)
這裏寫圖片描述
Gcc *.c -o 1exe (總的編譯步驟)
Gcc -E 1.c -o 1.i
Gcc -S 1.i -o 1.s
Gcc -c 1.s -o 1.o
Gcc 1.o -o 1exe
動態庫和靜態庫
1. 一個與共享庫鏈接的可執行文件僅僅包含它用到的函數入口地址的一個表,而不是外部函數所在目標文件的整個機器碼
2. 在可執行文件開始運行以前,外部函數的機器碼由操作系統從磁盤上的該共享庫中複製到內存中,這個過程稱爲動態鏈接(dynamic linking)
3. 共享庫可以在多個程序間共享,所以動態鏈接使得可執行文件更小,節省了磁盤空間。操作系統採用虛擬內存機制允許物理內存中的一份共享庫被要用到該庫的所有進程共用,節省了內存和磁盤空間。

Make

利用 make 工具可以自動完成編譯工作。這些工作包括:如果僅修改了某幾個源文件,則只重新編譯這幾個源文件;如果某個頭文件被修改了,則重新編譯所有包含該頭文件的源文件。利用這種自動編譯可大大簡化開發工作,避免不必要的重新編譯。
make 工具通過一個稱爲 Makefile 的文件來完成並自動維護編譯工作。Makefile文件描述了整個工程的編譯、連接等規則。
Makefile的基本準則
TARGET … : DEPENDENCIES …
COMMAND

目標(TARGET)程序產生的文件,如可執行文件和目標文件;目標也可以是要執行的動作,如clean,也稱爲僞目標。
依賴(DEPENDENCIES)是用來產生目標的輸入文件列表,一個目標通常依賴於多個文件。
命令(COMMAND)是make執行的動作(命令是shell命令或是可在shell下執行的程序)。注意:每個命令行的起始字符必須爲TAB字符!
如果DEPENDENCIES中有一個或多個文件更新的話,COMMAND就要執行,這就是Makefile最核心的內容。
Make工具的最核心思想:如果DEPENDENCIES有所更新,需要重新生成TARGET;進而執行COMMAND命令

main:main.o add.o sub.o
    gcc main.o add.o sub.o -o main
main.o:main.c add.h sub.h
    gcc -c main.c -o main.o
add.o:add.c add.h
    gcc -c add.c -o add.o
sub.o:sub.c sub.h
    gcc -c sub.c -o sub.o
clean:
    rm -f main main.o add.o sub.o

也可以用通配符:

$@ 規則的目標文件名
$< 規則的第一個依賴文件名
$^ 規則的所有依賴文件列表

聯繫方式: [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章