類作為C++中重要的概念之一,有著眾多的特性,也是最迷人的部分!
類是一個加工廠,開發者使用C++提供的各種材料組裝這個工廠,使得它可以生產出符合自己要求的資料,通過對工廠的改造,可以精細控制物件從出生到死亡的各種行為,真正達到我的代碼我做主的境界,
類
我們經常說的面向物件三大特征:封裝,繼承和多型,其實說的是一種抽象維度,最簡單的就是具體類,它將資料打包在一起,提供操作資料的函式,使得開發者不再需要通過傳參的形式傳遞資料,它實作了事物的抽象,也就是所謂的封裝,第二層是在一堆資料中提取出共性的部分作為基類,然后將特性作為子類,充分利用繼承的優點,實作代碼復用,它不僅追求資料抽象,也追求行為上的相似性,而更進一步,一套演算法不關心實際的資料,只關心它可以用來完成什么作業,甚至相互都不知道對方的存在,唯一的共同點就是都繼承自某個類,都能完成那個類指定的操作,至于細節都不關心,這就是多型,類只是一種規范流程,從第一層到第三層,抽象的事物從具體轉向抽象,重心也從資料轉向行為,只是為了更好的可維護性和解耦性,三者的關系可能是下圖這樣的:

為了能將跟高級的繼承和多型講明白,本篇我們將著重介紹他們的第一形態:封裝,也就是具體類,
類的基本組成
類是一種自定義型別,主要由兩部分組成:成員變數保存類管理的資料,成員函式操作資料,
和普通變數相比,類中的成員變數最大的不同是其生命周期,成員變數在類實體化后才占用空間,建構式完成其初始化作業,在構造完成后,成員函式就可以無限制地使用成員變數,直到解構式被呼叫,
成員函式和普通函式的不同之處是成員函式有個隱含的this指標,這個指標指向成員變數的存盤位置,也就是可以很方便地完成成員變數的訪問,
由此可見,具體類研究的主體是資料,接下來我將圍繞著資料的生命周期完成對類特性的決議,
物件的創建和銷毀
類的第一大作用就是控制類怎么生成和銷毀,和Java不同,不需要用new也會涉及到建構式的呼叫,哪怕只是個普通的區域變數,出了變數的作用范圍,物件就會被銷毀,記憶體就會被釋放,
class Sample{
public:
Sample(){
std::cout<<"Creating a Sample object"<<std::endl;
}
~Sample(){
std::cout<<"Destoring a Sample object"<<std::endl;
}
};
int main(){
// Sample的建構式被呼叫
Sample a;
{
// 大括號創建了一個區域作用域,物件b只存在大括號范圍內,出了大括號后,b就會被銷毀,呼叫Sample的解構式
Sample b;
}
// 此時只有物件a還存活
}
// 輸出
// Creating a Sample object
// Creating a Sample object
// Destoring a Sample object
// Destoring a Sample object
上面的Sample是最簡單的類定義,我們只創建了類的建構式和解構式,在main函式中,創建了兩個變數,通過檢查輸出,我們可以確定類的建構式和解構式都被呼叫了,
上面那個類從功能上毫無用處,我們只能創建一個它的物件,然后看著它死去,什么也干不了,接下來,我們來改造下Sample類,讓它能在構造的時候告訴我們,哪一個物件在構造,
class Sample{
Sample(const std::string name){
std::cout<<"Creating a Sample object:name = "<<name<<std::endl;
}
//其余不變
};
int main(){
// 由于創建物件a時,用到了string物件,所以要先創建一個string物件
std::string str{"a"};
// 此時構造類需要一個名字了,我們已經控制了類的初始化狀態
Sample a{str};
{
// Sample唯一給建構式需要一個string的物件,但是編譯器推測出傳遞給Sample建構式的引數型別是字串常量
// 引數不匹配,但這還沒達到編譯失敗的條件,因為編譯器還沒檢查是否存在一種從字串常量生成字串物件的建構式,
// 答案是有的,string類提供了這樣的建構式
// 接下來編譯器用字串常量構造出了string物件,自動完成了string物件的創建
// 并傳遞給Sample的建構式,條件滿足,編譯順利完成
Sample b{"b"};
}
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Destoring a Sample object
// Destoring a Sample object
上面的例子有一個值得注意的地方,那就是物件b直接從字串常量創建出來了,省略了中間字串物件,其實這一步是編譯器為我們完成了,它的創建程序和a是完全一樣的,這種行為稱為隱式轉換,
這時的Sample類還是什么也做不了,甚至連哪一個物件被銷毀了我們都不知道,解構式是函式,那么給解構式添加引數行不行呢?答案是不行,因為解構式是編譯器自動幫我們呼叫的,它不知道呼叫時需要什么引數,所以就只能是無參,那么有什么辦法能正確標記出是哪個物件被銷毀了呢,答案是成員變數,
成員變數和物件是同生共死的,它和物件使用同一塊記憶體,物件創建就為成員變數也分配了空間,但是沒有初始化,需要開發者在建構式或者其他函式使用前初始化,在解構式呼叫時,記憶體尚未被回收,這時候是使用成員變數的最后時機,成員變數還有另一個重要的特點,在類中定義的所有非static函式都能使用它,不需要通過函式引數傳遞,這也是類設計的初衷之一,用類管理資料,
所以,接下來的解構式可以這樣寫
class Sample {
private:
// 第一步,創建一個成員變數
std::string name;
public:
// 第二步,在建構式中初始化成員變數
Sample(const std::string name) :name{ name } {
std::cout << "Creating a Sample object:name = " << name << std::endl;
}
~Sample() {
//第三步,使用成員變數
std::cout << "Destoring a Sample object:name = " << name << std::endl;
}
};
int main() {
std::string str{ "a" };
Sample a{ str };
{
Sample b{ "b" };
}
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Destoring a Sample object:name = b
// Destoring a Sample object:name = a
可以看到,創建成員變數也很簡單,關鍵在于第二步,這和Java又不一樣,第二步中,初始化成員變數使用了特殊的語法,在建構式小括號后面添加了:,然后普通變數初始化的語法,稱之為成員變數初始化,這樣寫的關鍵原因在于,物件創建需要先申請記憶體,記憶體申請后使用:后面的初始化方式初始化成員變數,最后才呼叫建構式完成物件的創建,每一步都有它對應的位置和作用,假如像Java一樣寫在建構式里面,就相當于將第二步放到了第三步,打亂了它本來的順序,
為了說明成員函式確實在物件的整個生命周期都可以使用,我們再個它添加一個成員函式吧,
class Sample{
void print() {
std::cout << "Invoke print name = " << name << std::endl;
}
//其余不變
}
int main() {
std::string str{ "a" };
Sample a{ str };
{
Sample b{ "b" };
b.print();
}
a.print();
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Invoke print name = b
// Destoring a Sample object:name = b
// Invoke print name = a
// Destoring a Sample object:name = a
我們添加了一個成員函式print它沒有引數,但是它的函式體使用了成員變數name,可以看到,它也能正常作業,
至此,物件的創建和銷毀就說得差不多了,還沒說到的是建構式可以有很多個,在創建物件的時候可以選擇使用哪種方式創建,編譯器會根據傳遞的引數來推匯出實際使用的建構式,開發者需要考慮的是提供的建構式都能完成成員函式的正確初始化,以便在呼叫成員函式時,成員函式都能按預期作業,如Sample,我們還可以提供一個無參的建構式,然后將name初始化為空字串,這樣print和解構式也能正常作業,
總結一下,類是管理資料的容器,它的資料隨著物件的創建而創建,并在物件存在的整個生命周期都可用,建構式需要保證資料的初始化,并可以控制它構造的方式,成員函式可以隨時使用,解構式是物件銷毀時最后一個呼叫,它需要保證資料到此都被清理,
資料的轉移和共享
資料拷貝
資料創建之后,不僅可以供成員函式使用,還可能被轉移到其他物件中去,或者和其他物件共享,復制建構式可以控制資料以怎樣的方式和其他物件共享,
class Sample {
private:
int value;
public:
Sample(const int value) :value{ value } {
std::cout << "Create value = "https://www.cnblogs.com/honguilee/archive/2023/06/21/<< value << std::endl;
}
// 以Sample命名,是建構式,函式引數是自己的型別,說明是復制建構式
// 這個復制建構式選擇用賦值的形式共享value資料
Sample(const Sample& sample) :value{ sample.value } {
std::cout <<"Copy create object" << std::endl;
}
};
void use(Sample sample) {
//函式回傳,sample物件被銷毀
}
int main() {
Sample a{ 1 };
// a的資料被分享給一個臨時物件了,此時出現了兩個物件,它們的value都是1
use(a);
}
// 輸出
// Create value = https://www.cnblogs.com/honguilee/archive/2023/06/21/1
// Copy create object
復制建構式有以下幾個特征
- 會出現至少兩個同型別的物件,因為復制需要先有一個存在的物件,再用這個存在的物件資料初始化另一個正在創建的物件的成員變數,這也是復制建構式引數是自己的原因,
- 存在變數從無到有初始化的情況都會呼叫復制建構式,函式呼叫,形參需要初始化為實參,引數本來不存在,呼叫函式會傳遞一個已存在的物件,就會呼叫到復制建構式,這也是為什么復制建構式引數是參考的型別,假如是普通變數,呼叫復制構造的時候需要產生臨時變數,臨時變數又需要呼叫復制建構式,程式就會陷入無限遞回中,
- 除了函式呼叫,函式回傳值,用物件初始化新變數的情況也會呼叫到復制建構式,函式回傳后,函式體中所有的區域變數都會被銷毀,回傳值也屬于一種區域變數肯定也要被銷毀,但是回傳后的值卻需要被 外部使用,它們的生命周期是不一樣的,由此我們就知道肯定創建了一個新的物件,這個物件被區域回傳值初始化,但是有著和外部一樣的生命周期,用物件初始化變數就更直觀了,初始化的物件是從無到有創建的,符合建構式出現的特點,
我們可以來驗證一下
//其余不變
Sample returnSample() {
// 用普通建構式初始化的
Sample sample{ 2 };
return sample;
}
int main() {
Sample a{ 1 };
std::cout << "init local variable" << std::endl;
// b是新物件,用a初始化的,所以呼叫了復制建構式
Sample b = a;
// use的形參被用來初始化
std::cout << "Use Sample as parameter" << std::endl;
use(a);
//回傳的sample被用來初始化c
std::cout << "return sample" << std::endl;
Sample c = returnSample();
}
// 輸出
// Create value = https://www.cnblogs.com/honguilee/archive/2023/06/21/1
// init local variable
// Copy create object
// Use Sample as parameter
// Copy create object
// return sample
// Create value = 2
// Copy create object
可以看到,這三種情況都會造成復制建構式的呼叫,
資料移動
資料拷貝雖然簡單易行,但是還是有個小瑕疵,考慮下面這種場景:
void swap(Object& left,Object& right){
// 有新物件產生,拷貝構造,目前記憶體中有兩份一模一樣的left
Object temp=left;
// 賦值操作,生成了一個right的臨時物件
left=right;
// 賦值操作,生成了一個temp的臨時物件
right=temp;
// 三個臨時物件都被銷毀
}
int main(){
Object a;
Object b;
swap(a,b);
return 0;
}
一個簡單的交換邏輯,我們就生成了很多的臨時物件,假如我們操作的是串列,大物件,短時間內大量創建并銷毀物件,就會造成記憶體抖動,嚴重影響系統的穩定性,而且,我們的真正目的只是將兩個變數的值交換一下而已,所以相較于拷貝,我們還有更好的選擇:移動,
左值和右值
說起移動,就不得不提到左值和右值,這里的左和右是相對于=來說的,
我們知道=是用來賦值的,這下面隱藏著三個動作:生,取,寫,在記憶體中生成一個臨時資料,讀取變數保存位置,將臨時變數內容寫入保存位置,生就是指的右值,它保存在我們不知道的記憶體位置,在寫動作完成后,它就被回收了,而取對應的就是左值,我們用變數名保存了它的記憶體位置,在它作用域內可以反復讀寫,所以右值最大的特點就是不知道地址,如i=i+1就會先生成一個i+1的臨時物件,我們不知道地址,所以它是右值,與之相對的左值,是可以通過&讀到地址的,
接下來我們再來談一談參考,我們通常是用別名來理解參考的,但是可能會忽略一個小細節,別名也是需要有歸屬的,也就是它代表的地址在哪里,基于這個前提,我們就可以推匯出凡是存在記憶體中的資料,理都是有地址的,而右值是存在記憶體中的,它也應該需要一種方式來獲得地址,稱之為右值參考,相對的一般變數的參考就稱為左值參考,
說回到移動,前面的復制建構式雖然能將資料和其他物件共享,但是大部分情況下,資料其實不需要共享的,只需要轉移,也就是將資料的所有權移動到另一個物件上,原始物件就不再有效,所以C++提供了移動建構式來完成這個操作,
class Sample {
private:
int* value;
public:
Sample(const int value) :value{ new int{value} } {
std::cout << "Create value = "https://www.cnblogs.com/honguilee/archive/2023/06/21/<< value << std::endl;
}
Sample(const Sample& sample) :value{ new int {*sample.value} } {
std::cout <<"Copy create object" << std::endl;
}
Sample(Sample&& sample) :value{ sample.value } {
sample.value = https://www.cnblogs.com/honguilee/archive/2023/06/21/nullptr;
std::cout <<"Move create object" << std::endl;
}
~Sample() {
delete value;
std::cout << "destory sample" << std::endl;
}
friend std::ostream& operator<<(std::ostream& os, const Sample& sample) {
os << "Sample value is " << sample.value;
return os;
}
};
void use(Sample sample) {
std::cout << "Use sample " << sample << std::endl;
}
int main() {
// 普通變數,1被使用后馬上銷毀了
int a = 1;
//左值參考
int& b = a;
//右值參考,參考的就是1那個暫存的地址
int&& c = 1;
//可以修改參考的值
c = 2;
Sample sample{ 1 };
use(std::move(sample));
std::cout << sample << std::endl;
}
// 輸出
// Create value = https://www.cnblogs.com/honguilee/archive/2023/06/21/1
// Move create object
// Use sample Sample value is 009B8E90
// destory sample
// Sample value is 00000000
// destory sample
在上面的代碼里,我們真正使用sample物件的是函式use,use執行完后,sample就沒用了,所以我們用std::move將資料轉移到了函式實參中,外部的sample不再擁有那塊記憶體的占用,很多場景其實都是類似的情況:外部配置引數后,傳遞給某個函式使用,所以這種情況下就沒必要構造一個新的物件出來,假如業務很長的話,sample物件就會一直占用記憶體,但是它是早就沒用了的,所以移動建構式就發揮了大作用,
資料共享
除了通過復制建構式和成員函式共享資料外,還可以通過友元類和友元函式,它們都是一種特殊的訪問資料的形式,可以直接訪問到資料,不經過成員函式的呼叫,所以在有些時候友元能幫助減少函式呼叫的花銷,有些時候則會引入不可預期的行為,
class FriendClass {
public:
void useSample(const Sample& sample) {
std::cout << "Sample value is " << sample.value << std::endl;
}
};
上面的例子,如果按照常規是無法通過編譯的,因為sample的value是私有的,前面我們知道,成員函式是可以訪問私有變數的,但是這個類是定義在Sample外的,這個函式是另一個類的成員函式,完全沒辦法完成這種訪問,當然,這種情況下,我們可以修改Sample類的定義,添加一個成員函式就解決了,但是假如FriendClass有多個成員函式都需要訪問Sample的私有成員呢,這個時候添加成員函式的方式就不再適用,所以出現了友元類,
實作友元類很簡單,簡單到只需要添加一條宣告,首先友元類需要至少兩個類,一個類是想要訪問私有成員的源類,另一個是含有私有成員的目標類,然后我們把友元宣告放在目標類里,源類就可以順利訪問到目標類的私有成員了,在上面的例子FriendClass想要訪問Sample的私有成員,所以它是源類,是普普通通的類,Sample含有FeiendClass想訪問的私有成員value,所以它是目標類,宣告需要添加到它的類定義里面,
Class Sample{
private:
int value;
friend class FriendClass;
//其余不變
}
加上這一條之后,前面的FriendClass就可以正常通過編譯了,這一句的威力很大,大到FriendClass的所有成員函式都能訪問到value,假如這不是你的期望,但是還是想要直接訪問到value,那么就可以適用友元函式,
友元函式是普通的函式,雖然它宣告在類里,但是不能直接訪問到類的私有成員,而是通過函式引數的形式,為了和普通的成員函式區分開來,它的宣告最前面需要添加關鍵字friend,friend仿佛像打開了權限控制的開關,可以使函式訪問到引數的私有成員,
class Sample{
friend std::ostream& operator<<(std::ostream& os,const Sample& sample) {
os << "Sample value is " << sample.value << std::endl;
return os;
}
//其余不變
}
int main() {
Sample a{ 1 };
std::cout << a << std::endl;
}
// 輸出
// Create value = https://www.cnblogs.com/honguilee/archive/2023/06/21/1
// Sample value is 1
函式<<是友元函式,因為函式宣告有關鍵字friend,友元函式不是成員函式,想在函式體訪問到成員變數,需要添加函式引數,那么函式引數有很多個,怎樣確定引數私有成員的可訪問性呢,這就得看這個友元函式宣告在哪個類里面了,友元函式的宣告位置直接確定了它訪問私有成員的范圍,
特殊的成員函式
C++的類有極大的定制性,這種定制性不僅僅表現在資料上,還表現在成員函式上,我們知道一般的成員函式都是使用.來呼叫的,但是出于特殊的場景,有些情況下這種呼叫形式不僅僅不直觀,還效率不高,所以C++提出了運算子的概念,之所以稱為運算子,是因為函式的呼叫和傳參形式和普通的成員函式不一樣,定義良好的運算子可大大提高代碼的可讀性,如
[]運算子是下標運算子,有了它的幫助,我們就可以像obj[2]這樣取容器中的元素了,()則可以把物件當成函式一樣直接呼叫,實作函式式編程的效果,->可以回傳另外的物件,使得它可以表現出另一個物件的行為,
還有其他的諸如++,--等運算子,在定義特定型別的類時,提供合適的運算子函式能使我們的類更簡潔、好用,
總結
總的來說,類是一個資料管理器,建構式控制資料生成,來源可以使其他型別,也可以是相同型別,用相同型別生成新資料的時候,有復制和移動兩種選擇,復制建構式控制相同型別的資料共享行為,其主要目標就是實作兩個型別在建構式完成那一刻,在記憶體中的資料是完全一致的,移動建構式的目標則是將現有的資料轉移到當前構造的物件上來,然后使現有的資料失效,從而達到減少物件創建、銷毀,增加記憶體利用率的目的,除此之外,還能使用成員函式改變或者訪問資料,最終在解構式中結束資料的生命,此外友元類或者友元函式也是一種資料訪問途徑,
具體類的主要矛盾是資料,設計類的關鍵還是要弄清資料流向,資料自身在內部能有什么狀態,能實作什么行為,是成員函式該完成的作業,此外還要考慮相同型別相互構造時資料的共享情況,是完全隔離,還是相互影響,這些都是應該考慮的問題,畢竟確保資料從創建到銷毀的可靠性和有效性是一個具體類應該完成的基本功能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555754.html
標籤:其他
上一篇:【python基礎】類-模塊
下一篇:返回列表
