(九)羽夏看C語言——C++番外篇


寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 (一)羽夏看C語言——簡述 ,方便學習本教程。本篇是C++番外篇,會將零碎的東西重新集合起來介紹,可能會與前面有些重復或重合。

☀️ 封裝

將函數定義到結構體內部,就是封裝。

☀️ 類

帶有函數的結構體,稱為類。

☀️ 成員函數

結構體里面的函數,稱為成員函數。

☀️ 結構體傳參

1️⃣ 直接使用結構體傳參

#include <iostream>
using namespace std;

struct my_struct
{
    int a;
    int b;
    int c;
};

int mplus(my_struct &struct_)
{
    return  struct_.a + struct_.b + struct_.c;
}

int main()
{
    my_struct struct_ = { 1,2,3 };
    int res = mplus(struct_);
    printf_s("%d", res);
    system("pause");
    return 0;
}
    my_struct struct_ = { 1,2,3 };
 mov         dword ptr [struct_],1
 mov         dword ptr [ebp-10h],2
 mov         dword ptr [ebp-0Ch],3
    int res = mplus(struct_);
 sub         esp,0Ch
 mov         eax,esp
 mov         ecx,dword ptr [struct_]
 mov         dword ptr [eax],ecx
 mov         edx,dword ptr [ebp-10h]
 mov         dword ptr [eax+4],edx
 mov         ecx,dword ptr [ebp-0Ch]
 mov         dword ptr [eax+8],ecx
 call        mplus (0C912C0h)
 add         esp,0Ch
 mov         dword ptr [res],eax

2️⃣ 使用結構體指針/引用傳參

#include <iostream>
using namespace std;

struct my_struct
{
    int a;
    int b;
    int c;
};

int mplus(my_struct& struct_)
{
    return  struct_.a + struct_.b + struct_.c;
}

int main()
{
    my_struct struct_ = { 1,2,3 };
    int res = mplus(struct_);
    printf_s("%d", res);
    system("pause");
    return 0;
}
    my_struct struct_ = { 1,2,3 };
 mov         dword ptr [struct_],1
 mov         dword ptr [ebp-10h],2
 mov         dword ptr [ebp-0Ch],3
    int res = mplus(struct_);
 lea         eax,[struct_]
 push        eax
 call        mplus (01A12C0h)
 add         esp,4
 mov         dword ptr [res],eax

故盡量使用結構體指針傳參

3️⃣ 封裝使用

#include <iostream>
using namespace std;

struct my_struct
{
    int a;
    int b;
    int c;

    int mplus()
    {
        return  a + b + c;
    }
};

int main()
{
    my_struct struct_ = { 1,2,3 };
    int res = struct_.mplus();
    printf_s("%d", res);
    system("pause");
    return 0;
}

  • 生成的反匯編同 2️⃣
  • 函數並不屬於這個結構體,這樣做僅僅是為了使用方便,但虛函數會多占用4個字節(無論多少個)。

☀️ this指針

struct my_struct
{
    int a;
    int b;
    int c;

    void init(int a,int b,int c)
    {
        this -> a = a;
        this -> b = b;
        this -> c = c;
    }

    int mplus()
    {
        return  a + b + c;
    }
};
    my_struct struct_ ;
    struct_.init(1, 2, 3);
 push        3
 push        2
 push        1
 lea         ecx,[struct_]  //傳地址到ecx,即this指針
 call        my_struct::init (05A12C0h)
    int res = struct_.mplus();
 lea         ecx,[struct_]  //傳地址到ecx,即this指針
 call        my_struct::mplus (05A1320h)
 mov         dword ptr [res],eax
void init(int a,int b,int c)
    {
 push        ebp
 mov         ebp,esp
 sub         esp,0CCh
 push        ebx
 push        esi
 push        edi
 push        ecx
 lea         edi,[ebp-0CCh]
 mov         ecx,33h
 mov         eax,0CCCCCCCCh
 rep stos    dword ptr es:[edi]
 pop         ecx
 mov         dword ptr [this],ecx  //this指針
        this->a = a;
 mov         eax,dword ptr [this]
 mov         ecx,dword ptr [a]
 mov         dword ptr [eax],ecx
        this->b = b;
 mov         eax,dword ptr [this]
 mov         ecx,dword ptr [b]
 mov         dword ptr [eax+4],ecx
        this->c = c;
 mov         eax,dword ptr [this]
 mov         ecx,dword ptr [c]
 mov         dword ptr [eax+8],ecx
    }
 pop         edi
 pop         esi
 pop         ebx
 mov         esp,ebp
 pop         ebp
 ret         0Ch
  1. this指針是編譯器默認傳入的,通常都會使用ecx進行參數的傳遞。
  2. 成員函數都有this指針,無論是否使用。
  3. this指針不能做++--等運算,不能重新被賦值。
  4. this指針不占用結構體的寬度。

☀️ 構造函數

  1. 與類同名且沒有返回值
  2. 創建對象的時候執行/主要用於初始化
  3. 可以有多個(最好有一個無參的),稱為重載其他函數也可以重載
  4. 編譯器不要求必須提供

☀️ 析構函數

  1. 只能有一個析構函數,不能重載
  2. 不能帶任何參數
  3. 不能帶返回值
  4. 主要用於清理工作
  5. 編譯器不要求必須提供

☀️ 在堆中創建對象

new = malloc + 構造函數
delete = free + 析構函數

int* p = new int;
delete p;
int* p = new int[3];
delete[] p;

☀️ 引用

int main()
{
    int x = 2;
    int& ref = x;   //定義引用類型
    ref = 3;
    printf("%d",ref);   //輸出為3
    return 0;
}
  1. 引用必須賦初始值,且只能指向一個變量,“從一而終”。
  2. 對引用賦值,是對其指向的變量賦值,而並不是修改引用本身的值。
  3. 對引用做運算,就是對其指向的變量做運算,而不是對引用本身做運算。
  4. 引用類型就是一個“弱化了的指針”。

☀️ 常引用

void show(const int& content)   //函數內部無法修改content的值
{
    content = 5;    //編譯器檢查,編譯不通過
    printf("%d",content);
}

☀️ 面向對象程序設計之繼承與封裝

class Person
{
    int age;
    char sex;
public:
    Person(int age,char sex)
    {
        setAge(age);
        setSex(sex);
    }
    void setAge(int age)
    {
        age < 0 ? age = 0 : this->age = age;
    }

    void setSex(char sex)
    {
        this->sex = sex == 'b' || sex == 'g' ? sex : 'b';
    }
};

class Teacher:public Person //注意不能少了public
{
    int grade;
public:
    Teacher(int age,char sex,int grade):Person(age,sex)
    {
        setAge(age);
        setSex(sex);
        SetGrade(grade);
    }
    void SetGrade(int grade)
    {
        this ->grade = grade < 0 ?  0 :  grade;
    }
};

➡️ 分析
1️⃣ setAge/setSex/SetGrade系列函數的設計是為了輸入的數據更加可控,封裝性更好。
2️⃣ Teacher(int age,char sex,int grade):Person(age,sex)中若沒有:Person(age,sex),則默認調用Person()進行構造。如果Person沒有這個函數,編譯器就會報錯。

☀️ 面向對象程序設計之多態

class Person
{
    int age;
    char sex;
public:
    Person(int age,char sex)
    {
        setAge(age);
        setSex(sex);
    }
    void setAge(int age)
    {
        age < 0 ? age = 0 : this->age = age;
    }

    void setSex(char sex)
    {
        this->sex = sex == 'b' || sex == 'g' ? sex : 'b';
    }

    virtual void show()     //利用虛函數實現多態
    {
        printf("%d %d ",age,sex);
    }
};

class Teacher:public Person
{
    int grade;
public:
    Teacher(int age,char sex,int grade):Person(age,sex)
    {
        setAge(age);
        setSex(sex);
        SetGrade(grade);
    }
    void SetGrade(int grade)
    {
        this ->grade = grade < 0 ?  0 :  grade;
    }

    void show() //重寫實現,共用接口
    {
        Person::show();
        printf("%d",grade);
    }
};

//調用此函數,就能實現打印Person或Teacher里的數據
void Print(Person& per)
{
    per.show();
}

☀️ 純虛函數

  1. 虛函數目的是提供一個統一的接口,被繼承的子類重載,以多態的形式被調用。
  2. 如果基類中的函數沒有任何實現的意義,那么可以定義成純虛函數:
    virtual 返回類型 函數名(參數列表) = 0;
  3. 含有純虛函數的類被稱為抽象類,不能創建對象。
  4. 虛函數可以被直接使用,也可以被子類重載以后以多態的形式調用,而純虛函數必須在子類中實現該函數才可以使用。

☀️ 虛表

  • 以下是實現代碼
#include <iostream>
#include <Windows.h>
using namespace  std;

class Person
{
    int age;
    char sex;
public:
    Person(int age, char sex)
    {
        setAge(age);
        setSex(sex);
    }
    void setAge(int age)
    {
        age < 0 ? age = 0 : this->age = age;
    }

    void setSex(char sex)
    {
        this->sex = sex == 'b' || sex == 'g' ? sex : 'b';
    }

    virtual void show()     //利用虛函數實現多態
    {
        printf("%d %d ", age, sex);
    }
    virtual void whoami()
    {
        puts("Person");
    }
};

class Teacher :public Person
{
    int grade;
public:
    Teacher(int age, char sex, int grade) :Person(age, sex)
    {
        setAge(age);
        setSex(sex);
        SetGrade(grade);
    }
    void SetGrade(int grade)
    {
        this->grade = grade < 0 ? 0 : grade;
    }

    void show() //重寫實現,共用接口
    {
        Person::show();
        printf("%d", grade);
    }

    void whoami()
    {
        puts("Teacher");
    }
};

void Print(Person& per)
{
    per.show();
}

void whoami(Person& per)
{
    per.whoami();
}

int main()
{
    Teacher t(20, 'b', 20);
    Print(t);
    whoami(t);
    system("pause");
    return 0;
}
  • 通過下斷點,我們發現t變量不同之處:

  在tPerson里多了一個成員__vfptr,這個指向 虛表 的指針,我們看一下內存布局。

  經過檢驗,里面的值是函數地址,第一個是指向Teacher里面的show函數的地址,第二個是指向Teacher里面的whoami的地址,這就是所謂的虛表。看編譯器如何利用虛表實現多態,看下面的反匯編:

void whoami(Person& per)
{
 push        ebp
 mov         ebp,esp
 sub         esp,0C0h
 push        ebx
 push        esi
 push        edi
 lea         edi,[ebp-0C0h]
 mov         ecx,30h
 mov         eax,0CCCCCCCCh
 rep stos    dword ptr es:[edi]
    per.whoami();
 mov         eax,dword ptr [per]
 mov         edx,dword ptr [eax]
 mov         esi,esp
//獲取per地址的第一個成員,即為__vfptr。
 mov         ecx,dword ptr [per]
//由於whoami在虛表的第二個位置,故需要edx+4才是它的地址
 mov         eax,dword ptr [edx+4]
 call        eax

☀️ 模板

  • 示例
template<c1ass T>
void Sort(T* arr,int nLength)
{
    int i;
    int k;
    for(i=0;i<nLength-1;i++)
    {
        for(k=0;k<nLength-1-i;k++)
        {
            if(arr[k]>arr[k+1])
            {
                T temp =arr[k];
                arr[k]= arr[k+1];
                arr[k+1]=temp;
            }
        }
    }
}
  • 本質

編譯器幫我們生成函數,T有多少種,編譯器就生成多少個此函數。

☀️ 抽象類

作用:作為標准規范,方便對子類管理

  1. 含有純虛函數的類,稱為抽象類(Abstract Class)
  2. 抽象類也可以包含普通的函數
  3. 抽象類不能實例化

☀️ 拷貝構造函數

  拷貝構造函數由編譯器提供,不需編寫,他會把原對象數據原封不動的復制到目標對象,稱之為淺拷貝。

#include <iostream>
#include <Windows.h>
using namespace  std;

class Person
{
    int age;
    char sex;
public:
    Person(int age, char sex)
    {
        setAge(age);
        setSex(sex);
    }
    void setAge(int age)
    {
        age < 0 ? age = 0 : this->age = age;
    }

    void setSex(char sex)
    {
        this->sex = sex == 'b' || sex == 'g' ? sex : 'b';
    }

    void show()
    {
        printf("%d %d ", age, sex);
    }
};

int main()
{
    Person p0(20,'b');
    Person p(p0);
    p.show();
}
  • 如果類里面有指針,且賦值的數據是內容不是簡單的地址,需要自己重寫。
class Person
{
    int age;
    char sex;
public:
    Person(int age, char sex)
    {
        setAge(age);
        setSex(sex);
    }
};

class Teacher :public Person
{
    int grade;
public:
    Teacher(int age, char sex, int grade) :Person(age, sex)
    {
        setAge(age);
        setSex(sex);
        SetGrade(grade);
    }
    void SetGrade(int grade)
    {
        this->grade = grade < 0 ? 0 : grade;
    }
    /*下面是拷貝構造函數*/
    Teacher(const Teacher& t):Person(t)
    {
        //除了父類全部由自己實現
    }
    /*Person(t)如果沒有,父類的拷貝構造需要自己實現*/
};

☀️ 賦值重載

  • 如下是示例
CBase& operator=(const CBase& ref)
{
    m_nLength = ref.m_nLength;
    if(m_pBuFfer != NULL)
        delete[] m_pBuffer;
    m_pBuffer = new char[m_nLength];
    memcpy(m_pBufFer,ref.m_pBuffer,m_nLength);
    return *this;
}
CSub& operator= ( const CSub& ref)
{
    CBase::operator= (ref);
    m_nIndex = ref.m_nIndex;
    return *this;
}
  • 為什么拷貝構造函數不能這樣寫?
    子類能全盤繼承父類的在何東西,除了構造函數和析構函數,所以不能在函數體中顯式調用父類的拷貝構造。

☀️ 友元

友元破壞了C++面向對象的封裝特性,不推薦使用。

class CObject
{
    friend void Print0bject(cobject* pObject);
    //告訴編譯器Print0bject函數可以訪問我的私有成員
private:
    int x;
public:
    CObject(){}
    CObject(int x)
    {
        this -> x = x;
    }
};
void Printobject(cobject* pObject)
{
    printf("%d \n",pObject->x);
}

TestFriend類中的函數都可以直接訪問MyObject中的私有成員,但只是單向的。

class MyObject
{
    friend class TestFriend;
private:
    int x;
public:
    My0bject(){}
    MyObject(int x)
    {
        this -> x =x;
    }

};

class TestFriend
{
public:
void Fn(My0bject* pObject)
{
    printf("%d tn", pObject->x);
}

☀️ 內部類

  內部類和外部類之間的私有成員無法互通,如果一個類只在模塊內部使用,則可以實現類名隱藏。

☀️ 命名空間(namespace)

運用命名空間可以解決命名沖突問題

  1. 所有沒有明確命名的命名空間都在全局命名空間
#include <iostream>
using namespace x;

int Test()
{
    return 0;
}

int main()
{
    ::Test();   //如果x命名空間也有Test函數,可用此方式
    system("pause");
    return 0;
}

☀️ static 關鍵字

將變量和函數私有的全局化,聲明在類里不屬於此類的成員。

class CBase
{
public:
    CBase(int x,int y);
    static int GetSum();    //聲明靜態成員函數
private:
    int x,y;
    static int Sum;        //聲明靜態數據成員
}
int CBase::Sum = 10;       //定義並初始化靜態數據成員

☀️ 面向對象設計中的static之靜態數據成員

1️⃣ 靜態數據成員存儲在全局數據區,且必須初始化
2️⃣ 靜態數據成員和普通數據成員一樣遵從public,protected,private訪問規則
3️⃣ 類的靜態數據成員有兩種訪問形式:
類對象名.靜態數據成員名類類型名::靜態數據成員名
4️⃣ 同全局變量相比,使用靜態數據成員可以避免命名沖突實現信息隱藏
5️⃣ 出現在類體外的函數定義不能指定關鍵字static
6️⃣ 靜態成員之間可以相互訪問,包括靜態成員函數訪問靜態數據成員和訪問靜態成員函數
7️⃣ 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員
8️⃣ 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員

☀️ static 實現單實例模式

class CSingleton
{
public:
    static CSingieton* GetInstance()
    {
        if(m_pInstance == NULL)
            m_pInstance = new CSingleton();
        return m_pInstance;
    }

private:
    CSingleton(){}
    static CSingleton* m_pinstance; //定義靜態成員
};
CSingleton* CSingleton::m_pInstance = NULL;//初始化靜態成員

int main(int argc, char* agrv[])
{
    CSingleton* p1= CSingleton::GetInstance();
    CSingleton* p2= CSingleton::GetInstance();
    //p1和p2的值是一樣的
    return 0;
}

☀️ C++碎碎念

  1. 繼承:繼承就是數據的復制,減少重復代碼的編寫
  2. 繼承不僅僅局限於父類,它會把父類繼承到的東西全部拿來
  3. 如果子類(記為A)有和父類(記為B)相同的成員,以子類為准,如果非要使用父類的重名成員(記為C),則通過A.B::C引用
  4. structclass里的私有成員不是絕對不能訪問的,只是不能直接引用,需要用指針獲取。
  5. classstruct的區別:
      編譯器默認class中的成員為private,而struct中的成員為public。繼承也是如此,class繼承注意在繼承符號面的public不要漏下。
  6. 父類的指針可以指向子類的對象
  7. 操作符重載(在類里面,只是方便寫代碼而設計)
  8. 面向過程設計中的static:“私有”的全局變量


免責聲明!

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



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