【C++】 C++知識點總結

李春港發表於2021-04-11

作者:李春港
出處:https://www.cnblogs.com/lcgbk/p/14643010.html

目錄

前言

這篇文章是對C++的知識點做了一些簡單的總結,基本包含了所有的C++基礎知識點。以下提到的知識點並非深入講解,只是大概講解了各個知識點的基本使用。如需要深入瞭解,可以針對某個知識點去深入學習。

一、C++常用字尾

cpp, .h, cc, cxx, hpp

二、標頭檔案

1、C++輸入輸出

  • 標頭檔案#include
  • 標準輸入(standard input)與預定義的 istream 物件 cin 對應
  • 標準輸出(standard output) 與預定義的 ostream 物件 cout 對應
  • 標準出錯(standard error)與預定義的的 ostream 物件 cerr 對應

例子:用c++寫一個簡單計算器

    #include <iostream>
    int main(void)
    {
            int a=0, b=0;
            char c=0;
            std::cout<<"please input type: a+b" <<std::endl;
            std::cin>>a>>c>>b;
            switch(c)
            {
            case '+':
                    std::cout<<a<<c<<b<<"="<<a+b<<std::endl; break;
            case '-':
                    std::cout<<a<<c<<b<<"="<<a-b<<std::endl; break;
            case '*':
                    std::cout<<a<<c<<b<<"="<<a*b<<std::endl; break;
            case '/':
                    std::cout<<a<<c<<b<<"="<<a/b<<std::endl; break;
            }
            return 0;
    }

2、在C++中使用C的庫函式

extern "C"
{
    #include <stdlib.h>
    #include <string.h>
}

三、 指標與動態記憶體分配

靜態記憶體分配(全域性變數, 區域性變數), 動態記憶體分配(在 c 中用 malloc 分配的堆空間 free 來釋放)c++中用 new 分配堆空間 delete 釋放。

1、C

char  *name  =(char*) malloc(100);
free(name);

2、C++

  • 整形數: int *p = new int(10) ; 分配空間並且初始化為 10 釋放 delete p
  • 整形陣列:int *arr = new int[10] ; 分配十個連續整數空間 釋放 delete []arr
  • 字元型:char *p = new char('a'); 釋放 delete p;
  • 字串:char *arr = new char[100];分配 100 個字元空間 釋放 delete []arr;

四、名稱空間

為了確保程式中的全域性實體的名字不會與某些庫中宣告的全域性實體名衝突,引入了名稱空間。

1、作用

  • 避免名稱衝突;
  • 模組化應用程式;
  • 匿名的名稱空間可避免產生全域性靜態變數,建立的 “匿名” 名稱空間只能在建立它的檔案中訪問。

2、定義

除main函式外所有函式, 變數, 型別。

namespace  空間名{
    函式,變數, 型別
}

例子:
namespace class01
{
    std::string name="jack";
    int age=19;
    int number = 123;
}

3、使用空間成員

1、 直接通過空間名::成員名   --標準使用--提倡使用的方法
     class01::name   -- ::所屬符號
2、using指示符指引
     using namespace calss01; // 把class01空間了裡面的內容暴露在當前位置,當檔案有變數與名稱空間的成員一樣時,則後期使用該成員或變數時,程式執行時會報錯;但能編譯通過
3、 using宣告
    using class01::number;
  // 當檔案也有number變數時,則編譯的時候就報錯,相當於定義了兩次該變數。

所屬符號::

4、名稱空間巢狀

namespace  AAA{
    namespace BBB
    {
        int number=0;
    }
}

使用:

  • AAA::BBB::number;
  • using namespace AAA::BBB;   number;
  • using AAA::BBB::number;

5、匿名空間

相當於全域性變數直接使用(只能在本文中使用)
static。
定義:

namespace {
    int data;
}

匿名空間與static的異同:
static 無法修飾自定義型別;static 產生當前 符號只在當前原始檔有效,是因為它修改了符號的Bind屬性,使之變為區域性的;而匿名空間雖然可以產生相同效果,但是符號還是具有外部連結屬性。匿名名稱空間內的變數與函式,只在當前原始檔內有效;不同原始檔的匿名名稱空間,可以存在同名符合。static需要在每個變數加上

六、引用

引用:就是某一變數(目標)的一個別名,對引用的操作與對變數直接操作完全一樣。
引用的宣告方法:型別識別符號 &引用名=目標變數名;(別名)

int a = 10;
int &ra = a; (ra 就是 a 的引用 ,也稱 a 的別名)

1、引用特點:

  • &在此不是求地址運算子,而是起標識作用。
  • 型別識別符號是指目標變數的型別。
  • 宣告引用時,必須同時對其進行初始化。
  • 引用宣告完畢後,相當於目標變數有兩個名稱即該目標原名稱和引用名,且不能再把該引用名作為其他變數名的別名。
  • 宣告一個引用,不是新定義了一個變數,它只表示該引用名是目標變數名的一個別名,它本身不是一種資料型別,因此引用本身不佔儲存單元,系統也不給引用分配儲存單元,但引用本身是有大小的,一個指標的大小,在64位系統:sizeof(&ra) = 8,32位為4位元組,sizeof(ra)=sizeof(a)被引用物件的大小。故:對引用求地址,就是對目標變數求地址。 &ra 與&a 相等。
  • 不能建立陣列的引用。因為陣列是一個由若干個元素所組成的集合,所以無法建立一個

2、引用的應用:

1.引用作為引數
引用的一個重要作用就是作為函式的引數。以前的 C 語言中函式引數傳遞是值傳遞,如果有大塊資料作為引數傳遞的時候,採用的方案往往是指標,因為這樣可以避免將整塊資料全部壓棧,可以提高程式的效率。但是現在(C++中)又增加了一種同樣有效率的選擇
2.常引用
常引用宣告方式:const 型別識別符號 &引用名 = 目標變數名;
用這種方式宣告的引用,不能通過引用對目標變數的值進行修改,從而使引用的目標成為 const,達到了引用的安全性。
3.引用作為函式返回值
要以引用返回函式值,則函式定義時要按以下格式:
型別識別符號 &函式名 (形參列表及型別說明){ 函式體 }
特點:

  • 以引用返回函式值,定義函式時需要在函式名前加&
  • 用引用返回一個函式值的最大好處是,在記憶體中不產生被返回值的副本。
  • 不能返回區域性變數的引用

七、函式過載

函式過載只能在同一個類中
int open();
int open(const char* filename);
int open(const char *filename , int flag);
c++中編譯程式的是檢測函式,通過函式名和引數列表

如果在一個檔案中出現同名的函式但引數列表不同,那麼這些函式屬於過載

函式過載的依據:

  1. 函式名相同,

  2. 引數列表不同,(個數, 型別) add(int, doube) add(double, int)

  3. 如果引數是指標, 或引用,那麼const修飾也可以作為過載依據

  4. 函式的返回不能作為函式過載的依據

  5. 引數是否為預設引數不能作為函式過載的依據

八、函式預設引數(預設引數)

int open(const char *filename, int flag=10)
int open(const char *filename="c++", int flag=10)
int open(const char *filename="c++", int flag) 錯誤

注意: 若給某一引數設定了預設值,那麼在參數列中其後(也就是右邊)所有的引數都必須也設定預設值

九、類與物件

1、類

(1)定義:

 class 類名{
    類的特徵(屬性) 成員變數
    類的行為(功能) 成員方法, 函式
};
注意:當類裡面的成員引數函式有預設值時,若需要在外部定義該函式時,不能寫預設值,預設值只能在類裡面宣告的時候寫預設值。
例子:
 class Tdate
{
    public:
        void Set(int m, int d, int y )
        {
        month = m ;
        day = d ;
        year = y ;
        }  
    private:
        int month;
        int day;
        int year;
};

struct和class在C++都能定義類,其區別:

  • struct作為資料結構的實現體,它預設的資料訪問控制是public的,而class作為物件的實現體,它預設的成員變數訪問控制是private的。
  • 預設的繼承訪問許可權。struct是public的,class是private的。

(2)類成員許可權控制

  • public 公有 公有段的成員是提供給外部的介面,在外部可以訪問
  • protected 保護 保護段成員在該類和它的派生類中可見,在類外不能訪問(也就是在外部建立物件時不能訪問)
  • private 私有 私有段成員僅在類中可見(友元函式或者友元類可以訪問),在類外不能訪問(也就是在外部建立物件時不能訪問)

(3)類內/外訪問

  • 類內訪問:在類的成員函式中訪問成員(沒有任何限制)
  • 類外訪問: 在類的外部通過類的物件訪問類的成員

2、物件

定義與成員訪問:

 class Tdate
{
    public:
        int num;
        void set(int m, int d, int y )
        {
        month = m ;
        day = d ;
        year = y ;
        }  
    private:
        int month;
        int day;
        int year;
};

//定義物件
Tdate A;
Tdate *B = new Tdate( );
//物件成員訪問
A.set(1,1,1);
A.num = 2;
B->set(1,1,1);
B->num = 2;

十、構造和解構函式

1、建構函式

建構函式是成員函式,函式名與類名相同,函式沒有返回值, 函式不需要使用者呼叫,在建立物件的時候自動呼叫。
(1)如果建立一個類你沒有寫任何建構函式,則系統會自動生成預設的無參建構函式,函式為空,什麼都不做。
(2)只要你寫了一個下面的某一種建構函式,系統就不會再自動生成這樣一個預設的建構函式,如果希望有一個這樣的無參建構函式,則需要自己顯示地寫出來
(3)引數列表初始化:只有建構函式才有引數列表初始化。若要在類內宣告,類外定義建構函式,且使用引數列表初始化引數時,則在類內宣告的時候不允許引數列表初始化,只能類外定義的時候進行引數列表初始化
(4)函式預設引數:無論是成員函式還是建構函式,若需要類內宣告和類外定義的時候,預設引數值在宣告或者定義的時候都可賦值,但宣告和定義的引數不能有預設值

<1>建構函式定義及過載

class Complex
{
public:
    Complex()//建構函式定義
    {
        m_real = 0.0;
        m_imag = 0.0;
    }
    
    Complex(int a,int b)//過載建構函式定義
    {
        m_real = a;
        m_imag = b;
    }
private :
    double m_real;
    double m_imag;
};

//建立物件
Complex A;//這個時候不能有()
Complex A(1,1);
Complex *A = new Complex( );//可以有()也可以沒有
Complex *A = new Complex(1,1);

<2>建構函式引數列表初始化

Student(string n="name", int num=1000)
        :name(n),number(num){
        //name = n;
        //number = num;
}
注意:
* name、number:是本類裡面的成員;
* n、num:是對成員賦的值或者變數;
* 不能在類裡面宣告的時候用引數列初始化,宣告的時候可以加預設值;

對物件成員進行列表初始化:
class A
{
public:
    A(int a,int b){}
}

class B:public A
{
    A a;
public:
    B(int c,int d ):a(c,d){}
}

使用原因及用處:

  • 建構函式是成員函式,必須建立物件後才能呼叫
  • 引數列表初始化是在申請空間的同時就初始化
  • 如果成員是const修飾的成員、引用成員、繼承時候呼叫父類建構函式,這幾種情況就必須用引數列表初始化。

<3>拷貝建構函式

(1)如果沒有自定義拷貝建構函式,系統會預設生成一個拷貝建構函式(淺拷貝建構函式,不會拷貝堆空間)

    Student Jack; //建構函式
    Student Rose = Jack; //拷貝建構函式
    Student Tom (Jack); //拷貝建構函式
   
   後面兩種拷貝建構函式不會再次呼叫建構函式

(2)深拷貝建構函式

class Student
{
public:
    Student(int age, const char *n){
        this->age = age;
        this->name = new char[32];
        strcpy(this->name, n);
        cout<<"Student()"<<endl;
    }//this指標就是函式呼叫者
    ~Student(){
        delete []this->name;
        cout<<"~Student()"<<endl;
    }
    //深拷貝建構函式
    Student(Student& s)
    {
        cout<<"Student(Student&)"<<endl;
        this->age = s.age;
        this->name = new char[32];
        strcpy(this->name, s.name);
        //this->name = s.name;
    }
private:
    int age;
    char *name;
};

2、解構函式

  • 函式名有類一樣在函式名前面新增~符號
  • 解構函式沒有返回值, 也沒有引數
  • 解構函式在物件銷燬的時候自動呼叫(如果new一個物件,建構函式會自動執行,只有在delete的時候才呼叫解構函式)

例子:

class Complex
{
public:
    Complex( )//建構函式定義
    {
       cout << "complex" << endl;
    }
   ~ Complex( )//解構函式定義
    {
       cout << "~complex" << endl;
    }
};

int main( )
{
    Complex a; 
    Complex *p = new Complex();
    delete p;
    return 0;
}

結果:
complex
complex
~complex
~complex

十一、類的記憶體空間

類本身是一個資料型別,在沒有定義物件前是不佔用記憶體空間的,定義物件的時候才會分配空間。

  • 計算一個類的物件佔用多少空間用sizeof(類名或物件)
  • 類的物件大小是其資料成員(非靜態資料段),和虛擬函式表指標(一個類裡最多隻能有兩個指標,一個是虛擬函式的指標,一個是虛繼承的指標)大小和。普通方法(普通函式)不佔用記憶體,但用virtual修飾的虛擬函式佔用一個指標大小的記憶體。注:一個指標的大小、記憶體的對齊方式和編譯器有關;64位的話,大小為8;32位的話,大小為4。
  • 如果一個類中沒有資料成員,也沒有虛表那麼這個類的大小規定為 1 個位元組。

十二、類繼承

繼承:

  • 新的類從已知的類中得到已有的特徵的過程
  • 新類叫派生類/子類
  • 已知的類叫基類/父類
  • 如果直接將派生類的物件賦值給基類物件,派生類自身的成員就會被丟棄,只保留基類繼承來的成員。
  • 將基類指標指向派生類物件是安全的,因為派生類物件“是”它的基類的物件。但是要注意的是,這個指標只能用來呼叫基類的成員函式。

作用:
繼承可以減少重複的程式碼。比如父類已經提供的方法,子類可以直接使用,不必再去實現。

類的繼承格式:

class 子類名 :繼承方式 父類
{
    子類成員
};

例如:

class Base
{
public:
    Base() {}
    int b;
};

class Child: public Base
{
};

1、繼承方式

繼承方式: 公有繼承, 保護繼承, 私有繼承

  • 公有繼承(public):繼承時保持基類中各成員屬性不變,並且基類中的private成員被隱藏。派生類的成員只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的物件只能訪問基類中的public成員。
  • 保護繼承(protected):繼承時基類中各成員屬性均變為protected,並且基類中的private成員被隱藏。派生類的成員只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的物件不能訪問基類中的任何的成員。
  • 私有繼承(private):繼承時基類中各成員屬性均變為private,並且基類中private成員被隱藏。派生類的成員也只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的物件不能訪問基類中的任何的成員。

注意1: 私有繼承在下一次被繼承時,所有從父類繼承而來的都會別隱藏,而保護繼承在下次被繼承時根據繼承的屬性其中的資料可能被從新利用,所以私有繼承的保護性更加強。

注意2: 無論那種繼承子類的大小為子類+父類(所有成員都要加起來,包括私有成員)

2、繼承

(1)繼承構造方法和析構方法的定義和呼叫

因為有父類才有子類,所以呼叫順序如下:
建構函式的呼叫順序父類建構函式—物件成員建構函式—子類建構函式。
解構函式則相反。

注意:

  • 當派生類的建構函式為B(){cout << "Afather\n";}時,建立一個派生類預設會呼叫沒有引數的父類建構函式A()。
  • 如果父類建構函式帶無預設值引數,派生類建構函式怎麼寫?
    如下:
例子一:
父類建構函式
public:
    Person(string name, string sex, int age):name(name),sex(sex),age(age) {
        cout<<"Person()"<<endl;
    }
子類建構函式
public:
    Student( ):Person("jack","man",19){
        cout<<"Student()"<<endl;
    }  //==>Person();
    
例子二:
class Animal
{
public:
    Animal(int w, const char *color, int age){
    this->weight = w;
    this->age = age;
    strcpy(this->color, color);
    }
protected:
    int weight;
    int age;
    char color[32];
};
class Cat:public Animal
{
public:
    Cat(int w, const char *color, int age, const char *type):Animal(w, color, age)
    {
        strcpy(this->type, type);
    }
    void show()
    {
        cout << "weight=" << weight << "\nage=" << age << "\ncolor=" << color <<endl;
    }
protected:
    char type[32];
};

(2)繼承後成員函式呼叫

  • 父子類成員函式名相同時,不是過載,這時父類的此函式會別隱藏
  • 子類呼叫成員函式時候會檢測子類是否存在,如果存在就呼叫自己的, 如果不存在就呼叫父類的(前提是父要有這個函式)
  • 如果子類和父同時存在這個函式,一定要呼叫父類函式,可以用(父類名::函式名( ))呼叫。

例如:

#include <iostream>
using namespace std;

class A{
public:
    A(){cout << "Afather\n";}
    ~A(){cout << "~Afather\n";}
    void fun( ){
        cout << "father fun\n";
    }
};

class B:public A{
public:
    B(){cout << "Bchildren\n";}
    ~B(){cout << "~~Bchildren\n";}
    void fun(){
        cout << "children fun\n";
    }
};

int main( )
{
    B x;
    x.A::fun( );//呼叫父類的fun
    return 0;
}

輸出結果:
Afather
Bchildren
father fun
~~Bchildren
~Afather

3、多繼承

(1)語法:

class <派生類名>:<繼承方式1> <基類名1>,<繼承方式2><基類名2>,…
{
    <派生類類體>
};

(2)例子1:

class A{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
protected:
    int dataA;
};

class B{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
protected:
    int dataB;
};

class C:public A, public B
{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
protected:
    int dataC;
};

注意:建立子類物件構造順序 A->B->C
如果改為:class C:public B, public A,建立子類物件構造順序 B->A->C

(3)例子2: 如果父類建構函式帶引數

繼承關係class C:public A, public B

父類帶引數
A(int a):dataA(a){cout<<"A()"<<endl;}
B(int b):dataB(b){cout<<"B()"<<endl;}

C(int a, int b, int c):A(a), B(b),dataC(c){cout<<"C()"<<endl;}

(4)多個父類有同名的成員
多個父類有同名的成員, 在子類中訪問會出現歧義

  1. 顯示呼叫對應父類中的成員
    c.A::info();
    c.B::info();
  2. 在子類中新增父類的同名成員
    這個時候系統會將父類的同名成員隱藏,呼叫子類的成員

4、虛擬繼承

語法:

class D :virtual public B{  //虛擬繼承
 ...
};

多繼承中多級繼承時候多個父類同時繼承同一個基類出現二義性問題--用虛擬繼承解決。

例如:

class A{ 
public:
    void fun(){}
};

class B:virtual public A{ };
class C:virtual public A{ };

class D:public B,public C{ };
int main(void)
{
    D x;
    x.fun();//如果不是虛繼承,會出現二異性,因為在D類繼承了兩次A類
    return 0;
}

十三、虛擬函式、虛表

定義: 在類的成員函式宣告前面新增virtual

virtual void show(){cout<<data<<endl;}
  • 如果一個類中包含虛擬函式, 那麼這個類的物件中會包含一個虛表指標vptr
  • 虛表指標儲存在物件空間的最前面
  • 虛表中儲存的是類中的虛擬函式地址
  • 物件呼叫類中虛擬函式,會查詢虛表指標再執行函式
  • 一個類裡最多隻有兩個虛表指標(一個是虛擬函式的指標,一個是虛繼承的指標)
  • 用virtual修飾的虛擬函式佔用一個指標大小的記憶體。64位的話,大小為8;32位的話,大小為4。
  • 同一個類的不同例項共用同一份虛擬函式表, 它們都通過一個所謂的虛擬函式表指標__vfptr(定義為void**型別)指向該虛擬函式表.

例子1:觀察輸出的最後結果是什麼(一定要看)

#include <iostream>
using namespace std;
class Base
{
public:
    Base(){}
    virtual ~Base(){}
public:
    virtual void show(int a=123){
        cout<<"Base::show()"<<a<<endl;
    }
};
class Child:public Base
{
public:
    Child(){}
    ~Child(){}
    virtual void show(int a=321){
        cout<<"Child::show()"<<a<<endl;
    }
    virtual void info()
    {
        cout<<"Child::info()"<<endl;
    }
};
int main()
{
    Child c;
    Base *p = &c;
    p->show();
    return 0;
}

結果:
Child::show()123
注意:
(1)當show函式不是虛繼承時,輸出結果為Base::show()123,因為父類的指標只能呼叫自己的成員,如果有虛繼承,則虛表裡面父類的show函式的地址會被子類的show函式地址覆蓋,被覆蓋的前提是:兩個函式的名稱和引數型別、個數和返回值型別一樣。

例子2:通過指標呼叫虛表中的虛擬函式(在ubuntu下執行,虛表地址通過qt除錯檢視)

#include <iostream>
using namespace std;

class Base
{
public:
    Base(){}
    virtual ~Base(){}
protected:
    virtual void show(int a= 0){
        cout<<"Base::show()"<<endl;
    }
};
class Child:public Base
{
public:
    Child(){}
    ~Child(){}
    virtual void show(){
        cout<<"Child::show()"<<endl;
    }
    virtual void info()
    {
        cout<<"Child::info()"<<endl;
    }
};
int main()
{
    Child c;
    typedef void (*Fun)();
    c.show();
    Fun f = (Fun)(((long*)(*((long*)(&c))))[2]);
    f();
    return 0;
}

結果:
Child::show()
Base::show()

十四、純虛擬函式(抽象函式)、抽象類

(1)純虛擬函式--虛擬函式不需要實現直接賦值為0,純虛擬函式有時稱為抽象函式。
定義:

virtual void run()=0;

(2)抽象類

  • 如果一個類中包含純虛擬函式,那麼這個就是抽象類,抽象類是不能建立物件。
  • 抽象類可以派生出子類, 如果在子類中沒有把父類中的純虛擬函式全部實現,那麼子類照樣是抽象類。

例子:執行緒獲取時間

#include <iostream>
#include <pthread.h>
#include <windows.h>
#include <time.h>
using namespace  std;
class Thread{
public:
    Thread(){}
    ~Thread(){}
    void start();
    virtual void run()=0;
protected:
    pthread_t id;
};

void *handle(void *arg)
{
    Thread* th = (Thread*)arg;
    th->run();
}

void Thread::start()
{
    int ret = pthread_create(&id, NULL, handle, (void*)this);
    if(ret < 0)
    {
        cout<<"create fail"<<endl;
    }
}

//派生一個執行緒子類--獲取系統時間
class TimeThread: public Thread{
public:
    virtual void run()
    {
        while(1)
        {
            cout<<"TimeThread::run()"<<endl;
            Sleep(1000);
            time_t t;
            time(&t);
            cout<<pthread_self()<<"-----------"<<ctime(&t)<<endl;
        }
    }
};

int main()
{
    TimeThread tth;
    tth.start();
    TimeThread tt;
    tt.start();
    while(1){}
    return 0;
}

十五、多型、虛析構

(1)多型

<1>概念
C++中,多型性是指具有不同功能的函式可以用同一個函式名,這樣就可以用一個函式名呼叫不同內容的函式。

在物件導向方法中一般是這樣表述多型性的:向不同的物件傳送同一訊息(呼叫函式),不同的物件在接收時會產生不同的行為(即方法,不同的實現,即執行不同的函式)。可以說多型性是“一個介面,多種方法”。

多型性分為兩類:
(1)靜態多型性:在程式編譯時系統就能決定呼叫的是哪個函式,因此又稱為編譯時的多型性,通過函式的過載實現(運算子過載實際上也是函式過載);
(2)動態多型性:在程式執行過程中才動態地確定操作所針對的物件,又稱為執行時多型性,通過虛擬函式實現。 

區別:函式過載是同一層次上的同名函式(首部不同,即引數個數或型別不同),虛擬函式是不同層次上的同名函式(首部相同)。

<2>動態多型性和虛擬函式
父類引用(指標變數)指向子類物件時,呼叫的方法仍然是父類中的方法。如果將父類中的該方法定義為virtual,則呼叫的方法就是子類中的方法了。  

說明:本來,父類指標是用來指向父類物件的,如果指向子類物件,則進行型別轉換,將子類物件的指標轉為父類的指標,所以父類指標指向的是子類物件中的父類部分,也就無法通過父類指標去呼叫子類物件中的成員函式。但是,虛擬函式可以突破這一限制!如果不使用虛擬函式,企圖通過父類指標呼叫子類的非虛擬函式是絕對不行的!  

注意:父類中非虛擬函式被子類重寫後,父類指標呼叫的是父類的成員函式,子類指標呼叫的是子類中的成員函式,這並不是多型!因為沒有用到虛擬函式!

例如:

#include <iostream>
using namespace std;
class Person{
public:
    Person(){}
    ~Person(){}
    virtual void work(){cout<<"Person::work()"<<endl;}
protected:
    int data;
};

class ChildPerson: public Person{
public:
    ChildPerson(){}
    ~ChildPerson(){}
    virtual void show(){cout<<"ChlidPerson::show()"<<endl;}
    virtual void work(){cout<<"ChildPerson::work()"<<endl;}
    virtual void info(){}
protected:
    int number;
};

class A: public Person{
public:
    A(){}
    ~A(){}
    virtual void show(){cout<<"A::show()"<<endl;}
    virtual void work(){cout<<"A::work()"<<endl;}
    virtual void info(){}
protected:
    int num;
};

int main()
{
    ChildPerson cp;
    A a;
    Person* p = &a;
    Person* pson = &cp;
    pson->work(); //ChildPerson::work();
    p->work();//A:work();
    return 0;
}

(2)虛析構

多型的時候,用父類指標指向子類物件, 在delete 父類指標的時候預設只會呼叫父類解構函式,子類解構函式沒有執行(可能會導致子類的記憶體洩漏)--通過設定父類解構函式為虛擬函式類解決,執行子類解構函式後,自動執行父類解構函式。

例如:

#include<iostream>
using namespace std;
class Base
{
public:
    Base(){cout<<"create Base"<<endl;}
    virtual ~Base(){cout<<"delete Base"<<endl;}
};
class Der : public Base
{
public:
    Der(){cout<<"create Der"<<endl;}
    ~Der(){cout<<"Delete Der"<<endl;}
};
int main(int argc, char const* argv[])
{
    Base *b = new Der;
    delete b;
    return 0;
}

十六、友元

友元:是c++裡面一個特性,為了解決在函式中可以訪問類的私有,或保護成員。
友元函式是可以直接訪問類的私有成員的非成員函式。它是定義在類外的普通函式,它不屬於任何類。

  • 友元優點: 可以在函式中直接訪問成員資料, 可以適當提高程式效率
  • 友元缺點:在函式類的許可權失效, 破壞了類的封裝性
    friend關鍵宣告友元函式,或類。

第一種定義情況:類外定義:例如

class Data
{
public:
    Data() {}
    void setA(int a)
    {
        this->a = a;
    }
protected:
    int a;
private:
    int b;
    //在Data類中宣告函式fun為友元函式
    friend void fun();
};
void fun()
{
    Data data;
    //data.a = 120;
    data.setA(120);
    data.b = 220;
}
友元宣告只放類內部宣告, 可以放在類內任意位置

第二種定義情況:類內定義例如:

class Data
{
public:
    Data() {}
    void setA(int a)
    {
        this->a = a;
    }
protected:
    int a;
private:
    int b;
    //在Data類中宣告函式fun為友元函式
    friend void fun();
    //宣告show為友元函式,show不是成員函式
    friend void show()
    {
        Data data;
        data.a = 130;
        cout<<data.a<<" "<<data.b<<endl;
    }
};
void show(); //在外面宣告函式

十七、友元類

在一個類中的成員函式可以訪問另外一個類中的所有成員比如在 A 類中的成員函式可以訪問 B 類中的所有成員。有兩種方法如下:
(1)在 B 類中設定 A 類為友元類。
(2)A::fun 要訪問B類中的所有成員, 把A::fun函式宣告為B類的友元函式。

(1)例如:在 B 類中設定 A 類為友元類,A類成員函式可以訪問B類的protected、private成員,B類不能訪問A類,如果要雙向訪問則要在兩個類中宣告對方為友元類。友元關係不能被繼承。

class B
{
public:
    B(){}
    friend class A;//在 B 類中宣告 A 類為友元類
private:
    int bdata;
};
class A
{
public:
    A(){}
    void showB(B &b)
    {
    b.bdata = 100;//在 A 類中成員函式使用 B 類的私有資料
    }
private:
    int adata;
};

(2)A::fun 要訪問B類中的所有成員, 把A::fun函式宣告為B類的友元函式

#include <iostream>
using namespace std;
//前向宣告----只能用於函式形參, 定義指標, 引用,不能使用類具體成員
class B;
class A{
public:
    void fun(B& b);
};
class B{
public:
    B(){}
protected:
    int mb;
private:
    int nb;
    friend void A::fun(B& b);//在B類中宣告fun為友元函式
};
void A::fun(B& b){
    cout<<b.mb<<b.nb<<endl;
}
int main()
{
    cout << "Hello World!" << endl;
    return 0;
}

十八、運算子過載

運算子過載 關鍵子函式operator

  1. 根據實際應用需求來過載運算子, 過載的時候必須保持不能改變運算子本來的特性
  2. 只能過載c++已有的運算子,不能自己新建立運算子

1、那些運算能過載

  • 雙面運算子  (+,-,*,/, %)
  • 關係運算子 (==, !=, <,  >, <=, >=)
  • 邏輯運算子 (||,   &&,  !)
  • 單目運算子 (*, &, ++, --)
  • 位運算子   (|,  &,  ~, ^,  <<, >>)
  • 賦值運算子 (=, +=, -=,   .....)
  • 空間申請運算子 (new , delete)
  • 其他運算子 ((),  ->,  [])

2、那些運算子不能過載

  • .(成員訪問運算子)
  • .*(成員指標訪問運算子)
  • ::(域運算子)
  • sizeof(資料型別長度運算子)
  • ?:(條件運算子, 三目運算子)

注意:

3、格式:

返回型別說明符 operator 運算子符號(<參數列>)
{
   函式體
}

4、過載方式:
1.過載方式---成員函式過載

Complex C = A+B;    --》A.operator+(B);
規定:左值是函式呼叫者, 右值函式的引數

2.過載方式--友元過載(普通函式過載)(可以在類裡面定義,也可以在類外定義類內宣告)

Complex C = A-B;     -->operator-(A, B);
規定:左值為第一個引數, 右值為第二個引數

1、雙目運算子過載

(1)+ -過載

#include <iostream>
using namespace std;
class Complex
{
public:
    Complex(int r, int i):real(r), image(i) {}
    //成員過載實現過載加法+
    Complex  operator+ (Complex &B)
    {
        cout<<"operator"<<endl;
        Complex C(0,0);
        C.real = this->real +  B.real;
        C.image = this->image + B.image;
        return C;
    }
    //成員過載實現 物件加一個整型數
    int operator+(const int &a)
    {
        this->real += a;
        this->image += a;
        return this->real;
    }
private:
    int real;
    int image;
    friend Complex operator- (Complex &A, Complex &B);
};
//友元過載實現過載減法
Complex operator- (Complex &A, Complex &B)
{
    Complex C(0,0);
    C.real = A.real -  B.real;
    C.image = A.image - B.image;
    return C;
}
int main()
{
    Complex A(2,2);
    Complex B(1,1);
    Complex C = A+B; //==>A.operator+(B)
    int c  =  A+100; //==>A.operator+(100)
    Complex D = A-B; //operator-(A, B)
    return 0;
}

(2)輸出、輸入, 運算子過載

#include <iostream>
using namespace std;
class Point
{
public:
    Point (int x=0, int y=0):x(x),y(y){}
    void show()
    {
        cout<<"("<<x<<","<<y<<")"<<endl;
    }
private:
    int x, y;
    //宣告友元函式
    friend  ostream& operator<<(ostream &out, Point& p);
    friend istream&  operator>>(istream &in, Point& p);
};
//過載輸出
ostream&  operator<<(ostream &out, Point& p)
{
    out<<"("<<p.x<<","<<p.y<<")"<<endl;
    return out;
}
//輸入過載
istream&  operator>>(istream &in, Point& p)
{
    in>>p.x>>p.y;
    return in;
}
int main()
{
    Point p(10,20);
    p.show();
    cout<<p<<endl;   //==> ostream& operator<<(cout, p)
    Point A(0,0);
    cin>>A;
    cout<<A;
    return 0;
}

3、單目運算子過載

(1)++A、A++過載

成員函式過載

#include <iostream>
using namespace std;
class Data
{
public:
    Data(int d=0):data(d) {}
    //過載A++
    Data operator++(int)
    {
        Data old(*this);//儲存原先的數捍
        this->data += 1;//對原數進行自劍
        return old;//返回未加之前的數捍
    }
    //過載++A
    Data& operator++()
    {
        this->data += 1;//對原數進行自劍
        return *this;
    }
private:
    int data;
    friend ostream &operator<<(ostream& out, Data &d);
};
ostream &operator<<(ostream& out, Data &d)
{
    out<<d.data<<endl;
    return out;
}
int main()
{
    Data A;
    Data d = A++; //==>A.operator++(int)
    cout<<d<<A<<endl;
    Data &c = ++A;
    cout<<c<<A<<endl;
    return 0;
}

友元函式過載

#include <iostream>
using namespace std;

class A
{
    int data;
public:
    A(int d = 0):data(d) {}
    void show()
    {
        cout << this->data << endl;
    }
    //友元函式過載++A
    friend A& operator++ (A &a);
    //友元函式過載A++
    friend A operator++ (A &b,int);
    //友元函式過載<<
    friend ostream& operator<< (ostream &out,A &a);
};

//友元函式過載++A
A& operator++ (A &a)
{
    a.data += 1;
    return a;
}
//友元函式過載A++
A operator++ (A &b,int)
{
    A old(b);
    b.data += 1;
    return old;
}
//友元函式過載<<
 ostream& operator<< (ostream &out,A &a)
 {
     out << a.data;
     return  out;
 }

int main(int argc,char **argv)
{
    A a(5);
    A b = ++a;
    cout << a << " " << b << endl;

    A c(5);
    A d = c++;
    cout << c << " " << d << endl;

    return 0;
}

(2)過載中括號[ ]

#include <iostream>
using namespace std;
class Array
{
public:
    Array(int n):length(n) {
        this->ptr = new int[this->length];
    }
    ~Array(){
        delete []this->ptr;
    }
    //拷貝建構函式---深拷貝(類的成員有指標指向堆空間)
    Array(Array& array)
    {
        this->length = array.length;
        this->ptr = new int[this->length];
        memcpy(this->ptr, array.ptr , this->length);
    }
    //過載[]
    int& operator[](int i)
    {
        cout<<i<<endl;
        return this->ptr[i];//返回第i個物件
    }
private:
    int length;
    int *ptr;
};

int main()
{
    Array mArr(10);
    mArr[0] = 100;  //
    return 0;
}

4、用運算子過載實現資料型別轉換

(1)轉換建構函式

轉換建構函式的作用:是將一個其他型別的資料轉換成一個類的物件。 當一個建構函式只有一個引數,而且該引數又不是本類的const引用時,這種建構函式稱為轉換建構函式。 轉換建構函式是對建構函式的過載。
例如:

#include <iostream>
using namespace std;
class Complex
{
public:
    Complex():real(0),imag(0){cout << "test1\n";}
    
    Complex(double r, double i):real(r),imag(i){cout << "test2\n";}
    
    // 定義轉換建構函式
    Complex(double r):real(r),imag(0){cout << "test3\n";}  
    
    /*
    // 拷貝建構函式
    Complex(Complex &a){  cout << "test4\n";  }//當此函式存在時,Complex c = 1;Complex c2 = c1 + 3.1;編譯時都會報錯
    */
    
    void Print(){
        cout<<"real = " << real <<" image = "<<imag<<endl;
    }
    
    Complex operator+(Complex c){
        Complex ret(this->real + c.real, this->imag + c.imag);
        return  ret;
    }
private:
    double real;
    double imag;
};
int main()
{
    Complex c;
    c = 4;  // 呼叫轉換建構函式將1.2轉換為Complex型別,此時會呼叫轉換建構函式
    c.Print();
    Complex c1(2.9, 4.2);
    Complex c2 = c1 + 3.1; // 呼叫轉換建構函式將3.1轉換為Complex型別
    c2.Print();
    return 0;
}

輸出結果:
test1
test3
real = 4 image = 0
test2
test3
test2
real = 6 image = 4.2

注意:

  • 1、用轉換建構函式可以將一個指定型別的資料轉換為類的物件。但是不能反過來將一個類的物件轉換為一個其他型別的資料(例如將一個Complex類物件轉換成double型別資料)。
  • 2、如果不想讓轉換建構函式生效,也就是拒絕其它型別通過轉換建構函式轉換為本型別,可以在轉換建構函式前面加上explicit
(2)用運算子過載實現資料型別轉換

用轉換建構函式可以將一個指定型別的資料轉換為類的物件。但是不能反過來將一個類的物件轉換為一個其他型別的資料(例如將一個Complex類物件轉換成double型別資料)。而型別轉換函式就是專門用來解決這個問題的!
型別轉換函式的作用是將一個類的物件轉換成另一型別的資料。

#include <iostream>
using namespace std;
class Person
{
public:
    Person(int age=0):age(age) {}
    operator int() //通過運算子過載來實現資料型別轉換
    {
        cout<<"int"<<endl;
        return age;
    }
    operator long() //通過運算子過載來實現資料型別轉換
    {
        cout<<"long"<<endl;
        return num;
    }
    int getAge(){return age;}
private:
    int age;
    long num;
};
int main()
{
    Person Jack(19);
    int age = Jack;
    int a = Jack.getAge();
    long b = Jack;
    return 0;
}

注意:

  • 1、在函式名前面不能指定函式型別,函式沒有引數。
  • 2、其返回值的型別是由函式名中指定的型別名來確定的。
  • 3、型別轉換函式只能作為成員函式,因為轉換的主體是本類的物件,不能作為友元函式或普通函式。
  • 4、從函式形式可以看到,它與運算子過載函式相似,都是用關鍵字operator開頭,只是被過載的是型別名。double型別經過過載後,除了原有的含義外,還獲得新的含義(將一個Complex類物件轉換為double型別資料,並指定了轉換方法)。這樣,編譯系統不僅能識別原有的double型資料,而且還會把Complex類物件作為double型資料處理。

十九、模板函式

1、概念: 如果一個函式實現的功能類似,但是函式引數個數相同型別不同,這樣就可以把實在該功能的函式設計為模板函式。

2、格式:

template <typename T>  //T為型別名
資料型別 函式名(引數列表){
    函式體
}

3、注意:

  • (1)在編譯時,根據變數生成例項。
  • (2)template   T只對其下面的函式模板有效。如果要定義第二個模板函式時,則要再寫template
  • (3)typename也可以用class。
  • (4)T名字可以隨便取。
  • (5)當引數不一樣時,可以這樣定義引數列表template <class T,class Tp>
  • (6)引數列表可以帶預設型別,template <class T,class Tp = int>。
  • (7)模板函式只有在使用(不是執行)的時候才會檢測錯誤。

例子1:

//設計一個模板函式實現兩個物件交換
template <typename T>
void mswap(T &a, T &b)
{
    T  c = a;
    a = b;
    b = c;
}
int main()
{
    int a = 10;
    int b = 20;
    cout<<a<<" "<<b<<endl;
    mswap(a, b);
    cout<<a<<" "<<b<<endl;
    return 0;

例子2:

//錯誤,因為不能夠確定返回值的型別
template <class T, class Tp>
Tp fun(T &a)
{
    return a;
}

//修改,但返回值定死了
template <class T, class Tp = int>
Tp fun(T &a)
{
    return a;
}

//呼叫函式時指定型別
template <class T, class Tp = int>
Tp fun(T &a)
{
    return a;
}
int main(void)
{
    int a = 2;
    double ret = fun<int, double>(a)
}

4、模板函式與函式普通同時存在該如何呼叫

template <typename T>
void mswap(T &a, T &b)
{
    cout<<"template"<<endl;
    T  c = a;
    a = b;
    b = c;
}

//普通函式
void mswap(int &a, int &b)
{
    cout<<"std"<<endl;
    int c = a;
    a = b;
    b = c;
}

呼叫(1)
    int a = 10;
    int b = 20;
    cout<<a<<" "<<b<<endl;
    mswap(a, b);//---普通函式
    cout<<a<<" "<<b<<endl;
呼叫(2)
    double a = 10;
    double b = 20;
    cout<<a<<" "<<b<<endl;
    mswap(a, b);//---模板函式
    cout<<a<<" "<<b<<endl;

如果模板函式和普通函式同時存在, 呼叫的時候會根據引數選擇最優函式

二十、模板類

1、模板類的定義

//設計一個模板類  -模板類的類名 A<T>
//template< class  T ,  class Ty> //A<T, Ty>

template< class T > //A<T>
class A
{
public:
    A() {}
protected:
    T dataA;
};

int main()
{
    A<int> a;//定義模板類物件
    return 0;
}

注意:

  • (1)如果是浮點型或者其他普通型別, 是指標或者是引用 template <double &N,class T=int>
     class array{...}
    定義物件: array<N,int> a ;這裡的 N 必須是全域性變數。
  • (2)引數列表可以帶預設型別,template <class T,class Tp = int> , 如果是預設型別, 與函式的預設引數類似, 必須是如果從那個一個開始預設, 那麼後面的所有模板型別多必須有預設型別。
  • (3)如果使用數值為整型( char, short, int, long) 時候。template <int N,class T=int> class array{...},這裡的N只能是常量不能是變數,例如 array<10,int> a或者const int a = 5; array<a,int>。

2、模板類友元過載輸出

例如: 用模板類設計一個順序表(陣列)

#include <iostream>

using namespace std;

template< class T >
class MVector{
public:
    MVector(){
        this->size = 1024;
        this->count = 0;
        this->ptr = new T[this->size];
    }
    ~MVector(){
        delete []this->ptr;
    }

    //拷貝建構函式
    MVector(MVector& mv){
        this->size = mv.size;
        this->ptr = new T[this->size];
        memcpy(this->ptr, mv.ptr, this->size*sizeof(T));
    }
    //新增資料
    void append(const T &data){
        this->ptr[this->count] = data;
        this->count++;
    }
    //過載<<追加資料
    void operator<<(int data)
    {
        this->ptr[this->count] = data;
        this->count++;
    }
#if 1//在類裡面定義過載函式
    //宣告友元過載輸出<<
    friend ostream& operator<<(ostream& out, MVector &mv)
    {
        for(int i=0; i<mv.count; i++)
        {
            out<<mv.ptr[i]<<" ";
        }
        out<<endl;
        return out;
    }
#endif
    //template<class Ty>
    //friend ostream& operator<<(ostream& out, MVector<Ty> &mv);
protected:
    int count;
    int size;
    T* ptr;
};
#if 0 //在類內宣告,在類外定義過載函式
//過載輸出<<運算子
template< class Ty >
ostream& operator<<(ostream& out, MVector<Ty> &mv)
{
    for(int i=0; i<mv.count; i++)
    {
        out<<mv.ptr[i]<<" ";
    }
    out<<endl;
    return out;
}
#endif
//模板函式在使用(不是執行)該函式的時候才會檢查語法

int main()
{
    MVector<int> mvs;
    mvs.append(100);
    mvs<<200;
    cout<<mvs;
    return 0;
}

3、模板類繼承

如果在派生子類的時候父類類沒有確定class B: public A,那麼子類也是模板類。
例如:

#include <iostream>

using namespace std;

//設計一個模板類A<T>
template< class T >
class A
{
public:
    A(T a) {}
protected:
    T data;
};

//設計一個子類B 繼承模板類A<T> --B類也是模板類
template< class T >
class B: public A<T>
{
public:
    B(T a):A<T>(a){}

protected:
    int datab;
};

//設計一個子類C 繼承模板類A<int> --C類就是一個具體類
class C: public A<int>
{
public:
    C():A<int>(10){}
};

int main()
{
    A<char> a(10); //模板類建立物件
    B<string> b("hello"); //模板類子類建立物件
    C c;
    return 0;
}

4、模板類中的靜態成員

編譯時根據模板生成的不同類的靜態成員是不同記憶體空間的;在同一個類中建立的物件的靜態成員是共用一個記憶體空間的。
如下:

#include <iostream>

using namespace std;

template<class T>
class Data
{
public:
    Data() {}
    void show(T msg)
    {
        data = msg;
        cout<<data<<endl;
    }
public:
    static  T  data;
};

//類外初始化靜態成員
template<class T>
T Data<T>::data ;

int main()
{
    //建立一個物件
    Data<int> mydata;   //編譯的時候會生成一個 T為int的類
    mydata.show(100);

    Data<string>::data = "hello"; //編譯的時候會生成一個T 為string的類
    cout<<Data<string>::data<<endl;

    Data<string> mystr;
    cout<<mystr.data<<endl;
    return 0;
}

二十一、強制型別轉換const_cast、static_cast、reinterpert_cast、dynamic_cast

注意:以上,如果轉換失敗的時候會返回空

1、const_cast把常量轉為變數

#include <iostream>

using namespace std;

int main()
{
    const int a = 10;
    const int *p = &a;

    int *ptr = (int*)(&a);//c語言轉換(在c語言可以這樣寫:int *ptr=&a,只是會警告,一樣可以操作,c++不允許)
    *ptr = 1;
    cout<<a<<endl;
    cout<<*ptr<<endl;

    int &ra = const_cast<int&>(a);
    ra = 2;
    cout<<a<<endl;
    cout<<ra<<endl;

    int *x = const_cast<int*>(p);
    *x = 3;
    cout<<a<<endl;
    cout<<*x<<endl;

    return 0;
}

輸出結果:
10
1
10
2
10
3
解釋:因為a是const修飾的,此時a的值會存在符號表中,也就是改變a地址所指向的值,也不會改變a的值,當呼叫a的時候,編譯器回到符號表中取值,而不是從a的地址取值。

(1)為何要去除const限定
原因(1)是,我們可能呼叫了一個引數不是const的函式,而我們要傳進去的實際引數確實const的,但是我們知道這個函式是不會對引數做修改的。於是我們就需要使用const_cast去除const限定,以便函式能夠接受這個實際引數。
例如:

#include <iostream>
using namespace std;

void Printer (int* val,string seperator = "\n")
{
        cout << val<< seperator;
}

int main(void) 
{       
        const int consatant = 20;
        //Printer(consatant);//Error: invalid conversion from 'int' to 'int*'
        Printer(const_cast<int *>(&consatant));
        
        return 0;
}

原因(2):
還有一種我能想到的原因,是出現在const物件想呼叫自身的非const方法的時候,因為在類定義中,const也可以作為函式過載的一個標示符。

2、static_cast靜態轉化

static_cast < type-id > ( expression )該運算子把expression轉換為type-id型別,但沒有執行時型別檢查來保證轉換的安全性。它主要有如下幾種用法:

  • ①用於類層次結構中基類(父類)和派生類(子類)之間指標或引用的轉換,不允許不相關的類進行轉換。
    進行上行轉換(把派生類的指標或引用轉換成基類表示)是安全的;
    進行下行轉換(把基類指標或引用轉換成派生類表示)時,由於沒有動態型別檢查,所以是不安全的。
  • ②用於基本資料型別之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
  • ③把空指標轉換成目標型別的空指標。
  • ④把任何型別的表示式轉換成void型別。

注意: static_cast不能轉換掉expression的const、volatile、或者__unaligned屬性

例如

#include <iostream>
using namespace std;
int main()
{
    char a = 'a';
    int b = (int)a;
    double g = static_cast<int>(a);

    //為什麼不能轉換普通型別指標,卻能轉換物件指標和void指標(規定的)
    void *pp;
    double *pp1 = static_cast <double*>(pp);
    int *xx;
    void *xx1 = static_cast <void*>(xx);
    //double *xx2 = static_cast <double*>(xx);//錯誤寫法
    return 0;
}

3、reinterpret_cast強制型別轉換符

reinterpret_cast (expression)
type-id 必須是一個指標、引用、算術型別、函式指標或者成員指標。它可以把一個指標轉換成一個整數,也可以把一個整數轉換成一個指標(先把一個指標轉換成一個整數,再把該整數轉換成原型別的指標,還可以得到原先的指標值)。

  • reinterpret_cast可以轉換任意一個32bit整數,包括所有的指標和整數。可以把任何整數轉成指標,也可以把任何指標轉成整數,以及把指標轉化為任意型別的指標。但不能將非32bit的例項轉成指標。總之,只要是32bit的東東,怎麼轉都行!
  • 因為任何指標可以被轉換到void,而void可以被向後轉換到任何指標(對於static_cast<> 和 reinterpret_cast<>轉換都可以這樣做),如果沒有小心處理的話錯誤可能發生。

例如1:

#include <iostream>
using namespace std;
class A {
public:
    int m_a;
};
class B {
public:
    int m_b;
};
class C : public A, public B {};
int main()
{
    int n= 1231651 ;
    double *d;
    cout << d << endl;
    d=reinterpret_cast<double*> (&n);
    //為什麼d和n的地址一樣但為什麼地址裡面的值不一樣?
    //是因為double型別資料儲存的方式不一樣,用*d訪問時,
    //系統會以讀取double型別資料來讀取。
    cout << d << "  " << &n << endl;
    cout << *d << "  " << n << endl;
    cout << "---------------------------\n";

    //將一個32位的整數轉換成一個指標
    char *n_p = reinterpret_cast<char*>(10);
    
    //reinterpret_cast和static_cast的主要區別在於多繼承
    C c;
    printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));//前兩個的輸出值是相同的,最後一個則會在原基礎上偏移4個位元組,這是因為static_cast計算了父子類指標轉換的偏移量,並將之轉換到正確的地址(c裡面有m_a,m_b,轉換為B*指標後指到m_b處),而reinterpret_cast卻不會做這一層轉換。

    return 0;
}

結果:

例如2:

 //強制型別轉換//編寫一個程式跳轉到地址0x12345678執行
    typedef void(*Fun)(void);//定義一個函式指標資料型別
    Fun fun = reinterpret_cast<Fun>( 0x12345678 );
    fun();

4、dynamic_cast類轉換

dynamic_cast < type-id > ( expression )
說明: 該運算子把expression轉換成type-id型別的物件。Type-id必須是類的指標、類的引用或者void ;如果type-id是類指標型別,那麼expression也必須是一個指標,如果type-id是一個引用,那麼expression也必須是一個引用。
使用場景: dynamic_cast主要用於類層次間的上行轉換和下行轉換,還可以用於類之間的交叉轉換。在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的;在進行下行轉換時,dynamic_cast具有型別檢查的功能,比static_cast更安全。
注意:
① dynamic_cast是動態轉換,只有在基類指標轉換為子類指標時才有意義。
② dynamic_cast<>需要類成為多型,即包括“虛”函式,並因此而不能成為void*。 
③ static_cast和dynamic_cast可以執行指標到指標的轉換,或例項本身到例項本身的轉換,但不能在例項和指標之間轉換。static_cast只能提供編譯時的型別安全,而dynamic_cast可以提供執行時型別安全。
例如:

//dynamic---用於繼承過程中把父類指標轉換為子類指標
#include <iostream>

using namespace std;
class A
{
public:
    A() {}
    virtual ~A(){}
};
class B:public A
{
public:
    B() {}
    ~B(){}
};
//呼叫
int main()
{
    A *p = new B(); //使用者子類指標初始化父類指標
    A *a = new A();//建立一個A類物件
    //以下兩句必須在基類有虛析構的情況下才正確,否則編譯的時候報錯
    B *bptr = dynamic_cast<B*>(a);//把新的父類指標賦值子類指標(nullptr)
    B *bptr1 = dynamic_cast<B*>(p);//p是A型別指向B物件空間,把p轉回B類
    if(bptr1 == nullptr)//為nullptr時轉換不成功
    {
        cout<<"fail"<<endl;
    }
    else {
        cout<<"success"<<endl;
    }
    return 0;
}

二十二、異常捕捉和處理

在閱讀別人開發的專案中,也許你會經常看到了多處使用異常的程式碼,也許你也很少遇見使用異常處理的程式碼。那在什麼時候該使用異常,又在什麼時候不該使用異常呢?在學習完異常基本概念和語法之後,後面會有講解。

(1)異常丟擲和捕捉語句

//1.丟擲異常
throw  異常物件

//2.異常捕捉
try{
    可能會發生異常的程式碼
}catch(異常物件){
    異常處理程式碼
}
  • throw子句:throw 子句用於丟擲異常,被丟擲的異常可以是C++的內建型別(例如: throw int(1);),也可以是自定義型別。
  • try區段:這個區段中包含了可能發生異常的程式碼,在發生了異常之後,需要通過throw丟擲。
  • catch子句:每個catch子句都代表著一種異常的處理。catch子句用於處理特定型別的異常。catch塊的引數推薦採用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用物件的多型性。

(2)異常的處理規則

  • throw丟擲的異常型別與catch抓取的異常型別要一致;
  • throw丟擲的異常型別可以是子類物件,catch可以是父類物件;
  • catch塊的引數推薦採用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用物件的多型性。另外,派生類的異常捕獲要放到父類異常撲獲的前面,否則,派生類的異常無法被撲獲;
  • 如果使用catch引數中,使用基類捕獲派生類物件,一定要使用傳遞引用的方式,例如catch (exception &e);
  • 異常是通過丟擲物件而引發的,該物件的型別決定了應該啟用哪個處理程式碼;
  • 被選中的處理程式碼是呼叫鏈中與該物件型別匹配且離丟擲異常位置最近的那一個;
  • 在try的語句塊內宣告的變數在外部是不可以訪問的,即使是在catch子句內也不可以訪問;
  • 棧展開會沿著巢狀函式的呼叫鏈不斷查詢,直到找到了已丟擲的異常匹配的catch子句。如果丟擲的異常一直沒有函式捕獲(catch),則會一直上傳到c++執行系統那裡,導致整個程式的終止。

(3)例項

  • 例項1:丟擲自定義型別異常
class Data
{
public:
    Data() {}
};

void fun(int n)
{
    if(n==0)
        throw 0;//拋異常 int異常
    if(n==1)
        throw "error"; //拋字串異常
    if(n==2)
    {
        Data data;
        throw data;
    }
    if(n>3)
    {
        throw  1.0;
    }
}

int main()
{
    try {
        fun(6);//當異常發生fun裡面,fun以下程式碼就不會再執行,調到catch處執行異常處理程式碼,後繼續執行catch以外的程式碼。當throw丟擲異常後,沒有catch捕捉,則整個程式會退出,不會執行整個程式的以下程式碼
        cout<<"*************"<<endl;
    }catch (int  i) {
        cout<<i<<endl;
    }catch (const char *ptr)
    {
        cout<<ptr<<endl;
    }catch(Data &d)
    {
        cout<<"data"<<endl;
    }catch(...)//抓取 前面異常以外的所有其他異常
    {
        cout<<"all"<<endl;
    }
    return 0;
}
  • 例項2:標準出錯類丟擲和捕捉異常
#include <iostream>
using namespace std;

int main()
{
    try {
        char* p = new char[0x7fffffff];  //丟擲異常
    }
    catch (exception &e){
        cout << e.what() << endl;   //捕獲異常,然後程式結束
    }
    return 0;
}

輸出結果:
當使用new進行開空間時,申請記憶體失敗,系統就會丟擲異常,不用使用者自定義異常型別,此時捕獲到異常時,就可告訴使用者是哪裡的錯誤,便於修改。

  • 例項3:繼承標準出錯類的派生類的異常丟擲和捕捉
#include <iostream>
#include <exception>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
class FileException :public exception
{
public:
    FileException(string msg) {
        this->exStr = msg;
    }
    virtual const char*what()  const noexcept//宣告這個函式不能再拋異常
    {
         return this->exStr.c_str();
    }
protected:
    string exStr;
};

void fun()
{
    int fd = ::open("./open.txt",O_RDWR);

    if(fd<0)
    {
        FileException openFail("open fail"); //建立異常物件
        throw  openFail;//拋異常
    }
}

int main( )
{
    try {
        fun();
    } catch (exception &e) {//一般需要使用引用
        cout<<e.what()<<endl;
    }
    cout<<"end"<<endl;
    return 0;
}

當檔案不存在時,輸出結果:

如果在Linux上執行,上述程式碼需要根據環境修改:
98標準寫法

~FileException()throw(){}//必須要
virtual const char*what()  const throw()//宣告這個函式不能再拋異常
{
     return this->exStr.c_str();
 }
 //編譯
g++ main.cpp 

2011標準寫法

~FileException()noexcept{}//必須要
virtual const char*what()  const noexcept//宣告這個函式不能再拋異常
{
     return this->exStr.c_str();
}
 //編譯
g++ main.cpp -std=c++11   指定用c++11標準編譯

(4)總結

1. 使用異常處理的優點:

  • 傳統錯誤處理技術,檢查到一個錯誤,只會返回退出碼或者終止程式等等,我們只知道有錯誤,但不能更清楚知道是哪種錯誤。使用異常,把錯誤和處理分開來,由庫函式丟擲異常,由呼叫者捕獲這個異常,呼叫者就可以知道程式函式庫呼叫出現的錯誤是什麼錯誤,並去處理,而是否終止程式就把握在呼叫者手裡了。

2. 使用異常的缺點:

  • 如果使用異常,光憑檢視程式碼是很難評估程式的控制流:函式返回點可能在你意料之外,這就導致了程式碼管理和除錯的困難。啟動異常使得生成的二進位制檔案體積變大,延長了編譯時間,還可能會增加地址空間的壓力。
  • C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致記憶體洩漏、死鎖等異常安全問題。 這個需要使用RAII來處理資源的管理問題。學習成本較高。
  • C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。

3. 什麼時候使用異常?

  • 建議:除非已有的專案或底層庫中使用了異常,要不然儘量不要使用異常,雖然提供了方便,但是開銷也大。

4. 程式所有的異常都可以catch到嗎?

  • 並非如此,只有發生異常,並且又丟擲異常的情況才能被catch到。例如,陣列下標訪問越界的情況,系統是不會自身丟擲異常的,所以我們無論怎麼catch都是無效的;在這種情況,我們需要自定義丟擲型別,判斷陣列下標是否越界,然後再根據自身需要throw自定義異常物件,這樣才可以catch到異常,並進行進一步處理。

二十三、STL標準模板庫

容器:

  • vector---順序儲存---順序表 (訪問遍歷查詢)
  • list ------鏈式儲存 ----連結串列  (適合資料長度不確定, 經常改變)
  • map ----鍵值對儲存 (key:value)       (適合資料成對儲存)
  • set ------容器-----------------------(儲存資料是唯一的)

1、vector(順序表)

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

int main()
{
    //建立vector物件
    //大小變化:1024  2048  4096(開始會以2倍數增加,後面慢慢以1/3、1/5等的形式增加)
    vector<string> names;
    //賦值,3個jack
    names.assign(3,"Jack"); //Jack, Jack, Jack

    //插入資料
    //建立一個迭代器
    vector<string>::iterator it = names.begin();
    //insert之後迭代器it已經改變,返回值為插入值的位置
    it = names.insert(++it, "Rose"); //結果:Jack Rose Jack Jack
    it = names.insert(++it,"Jim");//結果:Jack Rose Jim Jack Jack
    it = names.insert(++it, "lcg"); //結果:Jack Rose Jim lcg Jack Jack
    it = names.insert(names.end(), "Jack"); //結果:Jack Rose Jim lcg Jack Jack Jack

    //查詢資料/刪除資料---迭代器遍歷使用vector, list, map, set
    //names.end()為順序表最後一個元素的下一個地址
    for(it = names.begin(); it != names.end(); ++it)
    {
        if(*it == "Jack")
        {
            cout<<*it<<" ";

            //擦除,返回擦出元素的下一個位置
            //例如a b c,刪除b後,返回迭代器指向c
            it=names.erase(it);
            --it;
        }
    }
    cout<<endl;

    //遍歷---順序表
    for(int i=0; i<names.size(); i++)
    {
        cout<<names[i]<<" ";
    }
    cout<<endl;

    return 0;
}

相關文章