寫在前面

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

你如果是從中間插過來看的,請仔細閱讀(一)羽夏看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:“私有”的全域性變數