1.面向過程與面向對象
1.1面向過程
我們之前學習的C語言就是一種面向過程的語言,面向過程的語言強調的是具體實現的過程,一般用函數來具體實現。我們用面向過程的思想,就可以把炒菜分為以下幾個步驟:
1.2面向對象
而對于面向對象的語言而言,它將問題分為了多個對象,問題的解決由對象之間的交互完成,而不再注重對象完成事情的具體過程,譬如上述炒菜的過程我們即可抽象為人、菜、鍋的交互。
C++即是一門面向對象的語言,我們更強調對象之間的交互,而并不在意對象該如何解決問題。?
1.3對比
面向過程的語言和面向對象的語言各有其優缺點:
面向過程的語言流程化分工明確,效率高,但可維護性差,拓展能力差。
面向對象的語言結構化更清晰,易維護,但開銷大,性能低。
2.類的引入
在C++中,我們在結構體的基礎上引入了類(class)的概念,并對struct在CPP的基礎上重新進行了解釋。
由于我們的面向對象不注重過程的實現,但是對象本身還是需要實現過程的,因此我們要給對象加上一定的過程,比如我們的人鍋交互可以炒菜,我們就要在類中聲明一個作用是炒菜的函數。
也就是說,CPP的類和結構體中可以定義成員函數!
2.1類的聲明方式
在C++中,有一個關鍵字為class,我們可以使用這個關鍵字來聲明一個類,類內可以聲明成員函數和成員變量,其語法結構為:
class 類名
{//類內的成員函數和成員變量
};
現在我們聲明一個名為duanku的類:
class duanku
{int a;//成員變量int Add(int a, int b);//成員函數
};
2.2類的成員的兩種定義方式
在定義類時,我們可以將類的定義與聲明在同一文件書寫,也可以在不同文件內書寫。
2.2.1單文件定義
定義一個類,自然需要實現類體內的函數,我們可以將函數在類內定義,如下所示:
class duanku
{int a;//成員變量int Add(int a, int b)//成員函數{return a + b;}
};
這里需要大家注意的是:成員函數若在類內定義,則可能會被編譯器當作內聯函數處理。
2.2.2多文件定義
一個類也可以分為兩個文件來實現,我們可以將成員函數的定義放在cpp文件內部。
但這兒需要大家注意的一點是:你定義的函數是誰的函數呢?是只在這個文件內部用的,還是實現了哪個地方的聲明呢?因此,我們需要加域作用限定符,讓編譯器知道我們實現的是哪個函數。
//.h文件
class duanku
{int a;//成員變量int Add(int a, int b);//成員函數
};
//.cpp文件
int duanku::Add(int a, int b)
{return a + b;
}
?PS:在日常使用時,我們可以將定義和聲明都放在類中。但是在實際的工程中,更傾向于將聲明和定義分離。
3.類的訪問限定符與封裝
3.1訪問限定符
在C++類中有三種訪問限定符:public、private、protect
它們的作用如下
- public修飾的成員可以在類外直接訪問
- private和protect修飾的成員在類外不可直接被訪問(后續文章會詳細解釋private和protect)
- 一個類中可以有多個訪問限定符,一個限定訪問符的作用域是該限定訪問符出現的位置到下一個限定訪問符出現的位置,如果后面沒有限定訪問符,則到類結束的位置為止。
- class的默認訪問權限為private,struct的默認訪問權限為public(下篇文章中會給出class和struct的詳解)
3.2封裝
封裝:用類將對象的屬性(數據)與操作數據的方法結合在一塊,讓對象更加完善,通過訪問權限選擇性的將其接口提供給外部的用戶使用。
通俗的說,封裝其實是一種管理,是為了方便讓用戶更方便的使用類的一種方法。
比如說:對于一臺電腦而言,我們提供給用戶的是一系列的USB接口,我們可以通過它們和計算機進行一系列的交互,也可以自由更改每個USB接口的接什么,但是我們無法更改電腦的硬件元件。
這里,我們可以這樣理解:
每個USB接口都是一個函數,我們可以自由更改它們,它們的訪問限定為public。
而硬件元件則是類中的私有成員,不讓外界訪問,它們的訪問限定為private。
4.類對象
4.1類對象的實例化
在類中定義的成員變量實際上是一種聲明,而我們利用類名字定義的對象就是類對象的實例化。
即,用類的類型創造對象的過程,稱為類的實例化。
1.類是對對象進行描述的,是一個模型一樣的東西,限定了類中有哪些成員,定義出一個類并不會分配實際的內存空間來存儲它。
2.一個類可以實例化出多個對象,實例化出的對象才占用實際的內存空間,存儲的是類的成員變量。
我們不可以直接利用類來進行定義,如下圖所示
?正確的定義方法如下:
Date a;a._year = 3;
4.2類對象的存儲
我們已經知道了類對象的創建方式,那么具體類中的成員變量與成員函數又是如何存儲的呢?
4.2.1 存儲方式1
每次創建對象時,都開辟一個空間存儲類中的成員變量與成員函數。
但是這樣存儲就會出現一個問題,成員函數都是用的一個函數,我們每次都創建一個豈不是會有很大的開銷嗎?一個就能行的事兒為什么要用很多個呢?
于是,我們的計算機中,大多數都不是這么存儲對象的。
4.2.2 存儲方式2
?我們首先將成員函數放在外面,之后我們每次創建對象時,都開辟一個空間存儲類成員變量與成員函數的地址,需要成員函數的時候直接按照地址去找。
4.2.3存儲方式3
每次創建對象時,都開辟一個空間存儲類成員變量。而成員函數提前單獨存儲在另外一個區域(公共代碼段)。
之后我們沒去用到成員函數時,都去那個區域里面拿出來用。
一般的編譯器采用的都是存儲模式2或存儲模式3,具體使用的是哪種存儲模式,我們可以通過計算類大小來進行判斷。?
4.3類對象的大小
4.3.1普通類對象的大小
類型對象的大小也遵循結構體的內存對齊規則:
1.類的第一個成員對齊到結構體變量起始位置偏移量為0的地址處
2.其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
? ?對齊數=編譯器默認的一個對齊數與該成員變量大小的較小值。
? ?--VS2022中的默認對齊數是8
? ?--Liunx中gcc沒有默認對齊數,對齊數就是成員自身的大小
3.類總大小為所有成員對齊數中的最大對齊數的整數倍
4.如果嵌套了結構體,嵌套的結構體成員對齊到自己的成員的最大對齊數處,結構體的整體大小就是所有成員的最大對齊數(含嵌套結構體成員)的整數倍。
當然,類大小的計算我們可以也借助sizeof計算。
#include<iostream>
using namespace std;
class Date
{
//可以被直接訪問
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
//不能被直接訪問
private:int _year;int _month;int _day;
};
int main()
{Date d;cout << sizeof(d) << endl;return 0;
}
?
由此可見,我們這里算出來的12是三個int類型的總和,而我們的成員函數則被放到了公共代碼段中。 若是使用的存儲模式2的話,則應該是12+成員函數地址的大小(32位系統是4,64位系統是8)。
?4.3.2空類的計算
當類中只有成員函數,或者什么都沒有時,類的大小是多少呢?
class A
{
public:void func(){;}
};
class B
{
};
int main()
{A a;B b;cout << sizeof(a)<<endl << sizeof(b);return 0;
}
?
這里我們發現空類的大小為1,現在我們可以思考這么一個問題,為什么空類的大小是1而不是0呢??這是因為我們在進行空類的實例化時必須要有空間表示我們在這兒存儲了一個對象。這時編譯器就會默認給一個字節大小來標記這個對象。
5.this指針
5.1this指針的由來
#include<iostream>
using namespace std;
class Date
{
public://初始化void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year; // 年int _month; // 月int _day; // 日
};
int main()
{Date d1, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}
在這里我先給大家拋出這么一個問題:為什么d1調用Init函數時,該函數知道是給d1對象調用Init,而不是給d2對象調用Init呢??
在C++中通過引入this指針解決該問題,即:C++編譯器給每個“非靜態的成員函數“增加了一個隱藏的指針參數。通過不同的對象地址來分辨不同的對象,只不過所有的操作對用戶是透明的的,即用戶不需要來傳遞,編譯器自動完成。實際代碼如下:
void Init(Date* this, int year, int month, int day)
{this->_year = year;this->_month = month;this->_day = day;
}
d1.Init(&d1,2022, 1, 11);//實際傳參
5.2this指針的特點
- this指針額類型為const,即我們無法給this指針賦值
- this指針的本質是“成員函數”的形參,當對象調用成員函數時,會將對象的地址當作實參傳遞給形參。
- this指針只能在成員函數中使用
- this指針是“成員函數”第一個隱藏起來的指針形參,一般由編譯器放在通過eax寄存器自動傳遞,不需要用戶傳遞。
5.3兩個問題
5.3.1問題1
// 1.下面程序編譯運行結果是? A、編譯報錯 B、運行崩潰 C、正常運行
class duanku
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{duanku* p = nullptr;p->Print();(*p).Print();return 0;
}
這段代碼是可以正常運行的,雖然p是一個空指針,但是由于我們的成員函數放在公共代碼段中,因此我們還是可以找到這個函數的,不會因為對空指針解引用而找不到它。
5.3.2問題2
// 1.下面程序編譯運行結果是? A、編譯報錯 B、運行崩潰 C、正常運行
class duanku
{
public:void Print(){cout << _a << endl;}
private:int _a;
};
int main()
{duanku* p = nullptr;p->Print();(*p).Print();return 0;
}
這里就會讓程序崩潰,因為函數中訪問了_a?,nullptr->_a程序就崩潰了。
?6.成員變量的命名
大家在使用類時,可能會出現以下這么一個問題:
這時為了解決問題這類問題,我們在定義成員變量時會對其進行特定地修飾。比如說_變量名
或者變量名_
。不同公司,不同的程序員可能都有一套自己的命名規則,這里我們采用第一種。
由此我們可以將上述代碼改寫為這樣:
class Date
{void Init(int year, int month, int day){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};