目錄
1.類和對象
1.1類的定義
1.2訪問限定符
1.3類域
2.實例化?
?2.1實例化概念
2.2對象大小
3.this指針
4.類的默認成員函數
4.1構造函數
4.2析構函數
4.5運算符重載
1.類和對象
1.1類的定義
類的定義格式
class為定義類的關鍵字,Stack為類的名字,{}中為類的主體,注意定義結束時后面分號不省略。類體中內容稱為類的成員:類中的變量稱為類的屬性或成員變量;類中的函數稱為類的方法或成員函數。
//text.cpp
#include<iostream>
using namespace std;class Stack
{//成員變量int* a;int top;int capacity;//成員函數void Push(){}void Pop(){}
};//分號不能省略
int main()
{return 0;
}
- 為了區分成員變量,一般習慣上成員變量會加一個特殊標識,如成員變量前面或后面加_或者m_開頭。這個C++語法上并沒有規定,僅憑個人或公司喜好
//為區分成員變量,一般前面加_
//成員變量
int* _a;
int _top;
int _capacity;
- C++中struct也可以定義類,C++兼容c中struct的用法,同時struct升級成了類,明顯的變化是struct中也可以定義函數,一般情況下我們還是推薦用class定義類
- 定義在類里面的成員默認為inline
1.2訪問限定符
C++一種實現封裝的方式,用類將對象的屬性與方法結合在一塊,讓對象更加完善,通過訪問權限選擇性的將其接口提供給外部的用戶使用
- public(公有)修飾的成員在類外可以直接被訪問,protected(保護)和private(私有)修飾的成員在類外不能直接被訪問,protected和private是一樣的
- 訪問權限作用域從該訪問權限出現的位置開始直到下一個訪問限定符出現為止,如果后面沒有訪問限定符,作用域就到 } 即類結束
//text.cpp
#include<iostream>
using namespace std;class Stack
{///void Push(){}//Push 沒給限定符 class默認私有 private ///
public:void Pop(){}int Swap(){}//Pop和Swap 被public修飾,直到下一個限定符出現之前都為公有///
protected:int add();
//add 被public修飾,直到下一個限定符出現之前都為保護
/// /private:int* _a;int _top;int _capacity;
//成員變量被private修飾,直到}結束都為私有
};
int main()
{Stack st;//公有可以訪問st.Pop();st.Swap();//私有不可訪問st._top;return 0;
}
額外注意:?
- class定義成員沒有被訪問限定符修飾時默認為private,struct默認為public
- 一般成員變量都會被限制為private/protected,需要給別人使用的成員函數會被放為public
1.3類域
類定義了一個新的作用域,類所有成員都在類的作用域中,在類體外定義成員時,需要使用? ::作用域操作符指明成員屬于哪個類域
類域影響的是編譯的查找規則,下面程序中Init如果不指定類域Stack,那么編譯器就會把Init當成全局函數,那么編譯時找不到_top等成員,就會到類域去找
//text.cpp
#include<iostream>
using namespace std;class Stack
{public:void Init(int x, int y);};
void Stack::Init(int x, int y){_top = x;_capacity = y;}
int main()
{return 0;
}
?注意:
- 類里面的函數聲明定義分離,類創建后形成了新的類域,需要指定類域,否則不可訪問
2.實例化?
?2.1實例化概念
- 用類型在物理內存中創建的過程,稱為類實例化出對象
- 類是對象進行一種抽象描述,是一個模型一樣的東西,限定了類有哪些成員變量,這些成員變量只是聲明,沒有分配空間,用類實例化出對象時,才會去分配空間
//text.cpp
#include<iostream>
using namespace std;class Stack
{//聲明int* _a;int _top;int _capacity;};int main()
{Stack::_top = 2024;//編譯器報錯,_top只是聲明,并未實例化return 0;
}
- 一個類可以實例化出多個對象,實例化出的對象占用實際的物理空間,存儲成員變量。打個比方:類實例化出對象就像現實中使用建筑設計圖造房子一樣,類就像設計圖,設計圖規劃出有多少個房間,房子大小等,但是并沒有實體的建筑存在,也不能住人,用設計圖修建出房子,房子才能住人。同樣類就像設計圖一樣,只是告訴編譯器即將要開多大的內存,但是不開內存,只有實例化出的對象才分配物理內存存儲數據
-
? //text.cpp #include<iostream> using namespace std;class Stack {//聲明int* _a;int _top;int _capacity;};int main() {Stack st;st._top=2024; //Stack實例化出st,系統已經給st分配內存了,可以存儲數據,編譯通過return 0; }?
2.2對象大小
- 分析一下類對象都有哪些成員呢?類實例化出的每個對象,都有獨立的數據空間,所以對象中肯定包含成員變量,那么成員函數是否包含呢?首先函數被編譯后是一段指令,對象中沒法儲存,這些指令存儲在一個單獨的區域(代碼段),那么對象非要存儲的話,只能是成員函數的指針。對象中是否有存儲指針的必要呢,Date實例化出兩個對象d1和d2,di和d2都有各自獨立的成員變量_year/_month/_day存儲各自的數據,但是d1和d2的成員函數Init/Print指針卻是一樣的,存儲在對象中就浪費了。如果用Date實例化出100個對象,那么成員函數指針就重復存儲100次,太浪費了。其實函數指針不需要存儲的,函數指針是一個地址,調用函數被編譯成匯編指令[call地址],其實編譯器在編譯鏈接時,就要找到函數的地址,不是在運行時找,只有動態多態是在運行時找,就需要存儲函數地址。
內存對齊規則
- 第一個成員在與結構體偏移量為0處的地址處
- 其他成員變量要對齊對齊數的整數倍的地址處
- 對齊數=編譯器默認的對齊數與該成員的大小的較小值
- VS x64平臺默認對齊數是4,x86默認對齊數是8
- 結構體總大小為:最大對齊數(所有類型變量最大者與默認對齊數取最小)的整數倍
- 如果嵌套了結構體的情況,嵌套的結構體對齊到自己最大對齊數的整數倍,結構體整體大小就是所有最大對齊數(含嵌套結構體對齊數)的整數倍
class A
{
public:void Print(){cout << _ch << endl;}
private:char _ch;int _i;
};
//_ch 是一個字節,默認對齊數是4,最大對齊數是4,所以開辟4個字節用來存在_ch
// _i是4個字節,默認對齊數是4,最大對齊數是4,所以開辟4個字節用來存儲_i
class B
{
public:void Print(){//。。。}};
class B
{};
//B和C里面沒有存儲任何成員變量,只有一個函數,可成員函數不存對象里面
// 按理來說是0,但是結構體怎么會沒大小,為表示對象存在C++對這種規定大小為1,為了占位標識對象存在
3.this指針
編譯器編譯后,類的成員函數默認都會在形參第一個位置,增加一個當前類的指針,叫做this指針
例如Date類中的Init原型為 void Init(Date * const this,int year ,int month,int day),類的成員函數中訪問成員變量,本質是通過this指針訪問的,如Init函數中給_year賦值,this->_year=year
?原型:
class Date
{void Print(){cout << _year << "\n" << _month << "\n" << _day << endl;}void Init( int year, int month,int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};
Date d1;
d1.Init(2024,7,10);d1.Print();
Date d2;d2.Init(2024, 7, 9);
d2.Print();
???真實原型? ? ?
class Date
{void Init(Date* const this,int year, int month,int day){this->_year = year;this->_month = month;this->_day = day;}void Printf(Date* const this){cout << this->_year << "\n" <<this-> _month << "\n" << this->_day << endl;}
private:int _year;int _month;int _day;};
Date d1;
d1.Init(&d1,2024,7,10);
d1.Print(&d1);Date d2;
d2.Init(&d2,2024, 7, 9);
d2.Print();
C++規定不準在實參和形參的位置寫this指針(編譯時編譯器會處理),但是可以在函數體內顯示使用this指針,this指針不能修改,但this指針指向的內容可以
this指針存在棧里
4.類的默認成員函數
默認成員函數就是用戶沒有顯示定義,編譯器會自動生成的成員函數稱為默認成員函數
4.1構造函數
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名字叫構造函數,但是構造函數的主要內容并不是開辟空間創造對象(我們平常使用的局部對象是棧幀創建時,空間就開好了),而是對象實例化時初始化對象。構造函數的本質是要替代我們以前Stack和Date類中寫的Init函數的功能,構造函數自動調用的特點就完美替代了Init
構造函數的特點:
- 函數名與類名相同
- 無返回值(返回值啥也不需要給,也不要寫void C++就是這樣規定)
- 對象實例化時系統會自動調用對應的構造函數
- 構造函數可以重載
- 如果類中沒有顯示定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯示定義編譯器將不再生成
class Date
{public://1.無參構造函數Date(){_year = 1;_month = 1;_day = 1;}//2.帶參構造函數Date(int year, int month, int day)
{_year = year;_month = month;_day = day;}//3.全缺省構造函數Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;};
無參構造函數,全缺省構造函數,我們不寫構造時編譯器默認生成的構造函數,都叫做默認構造函數。但是這三個有且只能有一個存在,不能同時存在。無參構造函數和全缺省構造函數雖然構成函數重載,但是調用時會存在歧義。注意并不是只有默認構造函數就是編譯器默認生成的那就是構造函數,無參構造函數,全缺省構造函數也是默認構造函數,總結一下就是不傳參就能調用
我們不寫,編譯器默認生成的構造,對內置類型成員變量的初始化沒有要求,也就是說是是否初始化是不確定的,看編譯器。
//text.cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請失敗");}_capacity = n;_top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
//兩個Stack實現隊列
class MyQueue
{
private:int size;Stack pushst;Stack popst;
};int main()
{MyQueue my;return 0;
}
?C++把類型分為自定義類型和內置類型(基本類型)。內置類型就是語言提供的原生數據類型,如int/char/double/指針等,自定義類型就是我們使用class/struct等關鍵字自己定義的類型。這里構造函數自動初始化,VS也將內置類型size初始化了,不同的編譯器初始化值不同,C++并沒有規定
對于自定義類型成員變量,要求調用這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認的構造函數,那么就會報錯,我們要初始化這個成員變量,需要用初始化列表才能解決
?總結:大多數情況下,構造函數都需要我們自己去實現,少數情況類似MyQueue且Stack有默認構造函數時,MyQueue自動生成就可以用
4.2析構函數
~Stack()
{free(_a);_a = nullptr;_top = _capacity = 0;
}
析構函數的特點:
1.析構函數名是在類名前面加上字符~
2.無參無返回值(與構造函數一致)
3.一個類只能有一個析構函數。若未顯示定義,系統也會自動生成默認的析構函數
4.對象聲明周期結束時,系統會自動調用析構函數,
5.跟構造函數類似,我們不寫編譯器自動生成的析構函數對內置類型成員不做處理,自定義類型成員會調用其他的析構函數
6.還需注意的是我們顯示析構函數,對于自定義類型成員也會調用他的析構函數,也就是說自定義類型成員無論什么情況都會自動調用析構函數。
//text.cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請失敗");}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_top=_capacity=0;}
private:STDataType* _a;size_t _capacity;size_t _top;};
//兩個Stack實現隊列
class MyQueue
{public://編譯器默認生成MyQueue的構造函數調用了Stack的構造,完成了兩個成員的初始化//編譯器默認生成MyQueue的析構函數調用了Stack的析構,釋放了Stack內部的資源//顯示寫析構也會調用Stack的析構~MyQueue(){cout << "~MyQueue" << endl;}
private: Stack pushst;Stack popst;};int main()
{MyQueue my;return 0;
}
?MyQueue里的析構啥也沒干,但是C++規定會調用其他的析構來釋放內存
如果沒有申請資源時,析構可以不寫,直接使用編譯器生成的默認析構函數,如Date,如果默認生成的析構可以用,也就不需要顯示寫析構如MyQueue,但是有資源申請時,一定要直接寫析構,否則會造成資源泄漏如Stack
4.5運算符重載
- 當運算符被用于類型的對象時,C++語言允許我們通過運算符重載的形式指定新的含義。C++規定類類型對象使用運算符時,必須轉換成調用對應運算符重載,若沒有則編譯器報錯
- 運算符重載是具有特定名字的函數,他的名字是由operator和后面要定義的運算符共同構成。和其他函數一樣,它也具有其返回類型和參數列表以及函數體
bool operator<(Date d1, Date d2)
{}
bool operator==(Date d1,Date d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
- 重載運算符函數的參數個數和該運算符作用的參數一樣多。一元運輸安撫有一個參數,二元運算符有兩個參數,二元運算符的左側運算對象傳給第一個參數,右側運算對象傳給第二個參數
//text.cpp
#include<iostream>
using namespace std;class Date
{
public:Date(int year, int month, int day){_year= year;_month = month;_day = day;}int _year;int _month;int _day;};bool operator<(Date d1, Date d2)
{}
bool operator==(Date d1,Date d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{Date d1(2024, 7, 10);Date d2(2024,7,9);
//兩種用法都可以d1 == d2;operator==(d1 , d2);return 0;
}
- 如果一個重載運算符函數是成員函數,則他的第一個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數比運算對象少一個
- 運算符重載以后,其優先級和結合性與內置類型運算保持一致
- 不能通過連接語法中沒有的符合來創建性的操作符:比如operator@