這篇文章給大家分享的是有關C++中類和對象是什么的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
面向對象
一直以來都是面向過程編程比如C語言,直到七十年代面向過程編程在開發大型程序時表現出不足,計算機界提出了面向對象思想(Object Oriented Programming),其中核心概念是類和對象,面向對象三大特性是封裝、繼承和多態。
面向過程和面向對象只是計算機編程中兩種側重點不同的思想,面向過程算是一種最為實際的思考方式,其中重要的是模塊化的思想,面向過程更注重過程、動作或者說事件的步驟。就算是面向對象也是含有面向過程的思想,對比面向過程,面向對象的方法主要是把事物給對象化,認為事物都可以轉化為一系列對象和它們之間的關系,更符合人對事物的認知方式。
用外賣系統舉例,面向過程思想就會將訂餐、取餐、送餐、接單等等步驟模塊化再一個一個實現,體現到程序中就是一個個的函數。面向對象思想會將整個流程歸結為對象和對象間的關系,也就是商家、騎手和用戶三者和他們的關系,體現到程序中就是類的設計。
面向對象是一個廣泛而深刻的思想,不可能一時半會就理解透徹,需要再學習和工作中慢慢體會。
C++不像Java是純面向對象語言,C++基于面向對象但又兼容C所以也可以面向過程。
//C struct Student { char name[20]; int age; int id; }; struct Student s; strcpy(s.name, "yyo"); s.age = 18; s.id = 11; //C++ struct Student { //成員變量 char _name[20]; int _age; int _id; //成員方法 void Init(const char* name, int age, int id) { strcpy(_name, name); _age = age; _id = id; } void Print() { cout << _name << endl; cout << _age << endl; cout << _id << endl; } }; s1.Init("yyo", 19, 1); s1.Print(); s2.Init("yyx", 18, 2); s2.Print();
從上述代碼可以看出,在C語言中,結構體中只能定義變量,就相當于是個多個變量的集合,而且操作成員變量的方式相較于C++更加繁瑣且容易出現錯誤。
由于C++兼容C,故C++中定義類有兩個關鍵字分別是struct和class,結構體在C++中也升級成了類,類名可以直接作類型使用。類與結構體不同的地方在于,類中不僅可以定義變量,還可以定義方法或稱函數。
C++中更多用class定義類,用class定義的類和struct定義的類在訪問限定權限上稍有不同。
class className { // ... };
class是類的關鍵字,className是類的名字,{}中的內容是類體。類中的元素即變量和函數都叫類的成員,其中類的成員變量稱為類的屬性或是類的數據,類的函數成為類的方法或成員函數。
面向對象編程講究個“封裝”二字,封裝體現在兩方面,一是將數據和方法都放到類中封裝起來,二是給成員增加訪問權限的限制。
C++共有三個訪問限定符,分別為公有public,保護protect,私有private。
public修飾的成員可以在類外直接訪問,private和protect修飾的成員在類外不能直接訪問。
class類中成員默認訪問權限為private,struct類中默認為public。
從訪問限定符出現的位置到下一個訪問限定符出現的位置之間都是該訪問限定符的作用域。
和public相比,private和protect在這里是類似的,它二者具體區別會在之后的繼承中談到。封裝的意義就在于規范成員的訪問權限,放開struct類的權限是因為要兼容C。
封裝的意義就在于規范成員的訪問權限,更好的管理類的成員,一般建議是將成員的訪問權限標清楚,不要用類的默認規則。
class Student { private: //成員變量 char _name[20]; int _age; int _id; public: //成員方法 void Init(const char* name, int age, int id) { strcpy(_name, name); _age = age; _id = id; } void Print() { cout << _name << endl; cout << _age << endl; cout << _id << endl; } };
注意,訪問限定修飾符只在編譯階段起作用,之后不會對變量和函數造成任何影響。
面向對象三大特性是封裝、繼承和多態。類和對象的學習階段,只強調類和對象的封裝機制。封裝的定義是:將數據和操作數據的方法放到類中有機結合,對外隱藏對象的屬性和實現細節,僅公開交互的接口。
封裝的本質是一種管理機制。對比C語言版的數據結構實現可以看到,沒有封裝并將結構的成員全部暴露出來是危險的且容易出錯,但調用結構提供的接口卻不易出錯。一般不允許輕易的操作在函數外操作和改變結構,這便是封裝的好處。面向過程只有針對函數的封裝,而面向對象編程提出了更加全面的封裝機制,使得代碼更加安全且易于操作。
class Stack { public: void Init(); void Push(STDataType x); void Pop(); STDataType Top(); int Size(); bool Empty(); void Destroy(); private: STDataType* _a; int _top; int _capacity; };
類定義了一個新的作用域,類中所有成員都在類的作用域中。
若直接在類內定義函數體,編譯器默認將類內定義的函數當作內聯函數處理,在滿足內聯函數的要求的情況下。
在類外定義成員函數時,需要使用域作用限定符::指明該成員歸屬的類域。如圖所示:
一般情況下,更多是采用像數據結構時期那樣,聲明和定義分離的方式。
用類創建對象的過程,就稱為類的實例化。
類只是一個“模型”,限定了類的性質,但并沒有為其分配空間。
由類可以實例化得多個對象,對象在內存中占據實際的空間,用于存儲類成員變量。
類和對象的關系,就與類型和變量的關系一樣,可以理解為圖紙和房子的關系。
既然類中既有成員變量又有成員函數,那么一個類的對象中包含了什么?類對象如何存儲?
class Stack { public: void Init(); void Push(int x); // ... private: int* _a; int _top; int _capacity; }; Stack st; cout << sizeof(Stack) << endl; cout << sizeof(st) << endl;
如果類成員函數也存放在對象中,實例化多個對象時,各個對象的成員變量相互獨立,但成員函數是相同的,相同的代碼存儲多份浪費空間。因此,C++對象中僅存儲類變量,成員函數存放在公共代碼段。
類的大小就是該類中成員變量之和,要求內存對齊,和結構體一樣。注意,空類的大小為1個字節,用來標識這個對象的存在。
空類的大小若為0,相當于內存中沒有為該類所創對象分配空間,等價于對象不存在,所以是不可能的。
接下來都使用棧和日期類來理解類和對象中的知識。
class Date { public: void Init(int year, int month, int day) { //year = year;//Err //1. _year = year; //2. Date::month = month; //3. this->day = day; } private: int _year; int month; int day; };
如果成員變量和形參重名的話,在Init函數中賦值就會優先使用形參導致成員變量沒有被初始化,這種問題有三種解決方案:
在成員變量名前加_,以區分成員和形參。
使用域訪問修飾符::,指定前面的變量是成員變量。
使用 this 指針。
d1._year;的意義是告訴編譯器到d1這個對象中查找變量_year的地址。但函數并不存放在類對象中,那d1.Print();的意義是什么?
如圖所示,d1,d2兩個對象調用存儲在公共代碼區的Print函數,函數體中并沒有區分不同對象,如何做到區分不同對象的調用呢?
C++中通過引入 this 指針解決該問題,C++編譯器給每個非靜態的成員函數增加了一個隱藏的參數叫 this 指針。this 指針指向當前調用對象,函數體中所有對成員變量的操作都通過該指針訪問,但這些操作由編譯器自動完成,不需要主動傳遞。
如圖所示,在傳參時隱藏地傳入了對象的指針,形參列表中也對應隱藏增加了對象指針,函數體中的成員變量前也隱藏了 this 指針。
this是C++的一個關鍵字,代表當前對象的指針。this 指針是成員函數第一個隱含的指針形參,一般由寄存器傳遞不需要主動傳參。
調用成員函數時,不可以顯式傳入 this 指針,成員函數參數列表也不可顯示聲明 this 指針。
但成員函數中可以顯式使用 this 指針。
this 的類型為classType* const,加const是為了防止 this 指針被改變。
this 指針本質上是成員函數的形參,函數被調用時對象地址傳入該指針,所以 this 指針是形參存儲在函數棧幀中,對象中不存儲this指針。
Example 1和2哪個會出現問題,出什么問題?
class A { public: void Printa() { cout << _a << endl; } void Show() { cout << "Show()" << endl; } private: int _a; }; int main() { A* a = nullptr; //1. a->Show(); //2. a->Printa(); return 0; }
函數沒有存儲在對象中,所以調用函數并不會訪問空指針a,僅是空指針作參數傳入成員函數而已。二者沒有程序語法錯誤,所以編譯一定通過。
調用Show()函數沒有訪問對象中的內容,不存在訪問空指針的問題。調用Print()函數需到a指針所指對象中訪問成員_a,所以訪問空指針程序崩潰。
默認成員函數
一個對象都要要對其進行初始化,釋放空間,拷貝復制等等操作,像棧結構不初始化直接壓棧就會報錯。由于這些操作經常使用或是必不可少,在設計之初就被放到類中作為默認生成的成員函數使用,解決了C語言的一些不足之處。
C++在設計類的默認成員函數的機制較為復雜,一個類有6個默認的成員函數,分別為構造函數、析構函數、拷貝構造函數、賦值運算符重載、T* operator&()
和const T* operator&()const
。他們都是特殊的成員函數,這些特殊函數不能被當作常規函數調用。
默認的意思是我們不寫編譯器也會自動生成一份在類里,如果我們寫了編譯器就不生成了。自動生成默認函數有的時候功能不夠全面,還是得自己寫。
構造函數和析構函數分別是完成初始化和清理資源的工作。構造函數就相當于數據結構時期我們寫的初始化Init函數。
構造函數是一個特殊的函數,名字與類名相同,創建類對象時被編譯器自動調用用于初始化每個成員變量,并且在對象的生命周期中只調用一次。
構造函數雖然叫構造函數,但構造函數的工作并不是開辟空間創建對象,而初始化對象中的成員變量。
函數名和類名相同,且無返回類型。
對象實例化時由編譯器自動調用其對應的構造函數。
構造函數支持函數重載。
//調用無參的構造函數 Date d1; Date d2(); //Err - 函數聲明 //調用帶參的構造函數 Date d2(2020,1,18);
注意,調用構造函數只能在對象實例化的時候,且調用無參的構造函數不能帶括號,否則會當成函數聲明。
若類中沒有顯式定義構造函數,程序默認創建的構造函數是無參無返回類型的。一旦顯式定義了編譯器則不會生成。
無參的構造函數、全缺省的構造函數和默認生成的構造函數都可以是默認構造函數(不傳參也可以調用的構造函數),且防止沖突默認構造函數只能有一個。
默認構造函數初始化規則
從上圖可以看出,默認生成的構造函數對內置類型的成員變量不進行有效初始化。其實,編譯器默認生成的構造函數僅對自定義類型進行初始化,初始化的方式是在創建該自定義類型的成員變量后調用它的構造函數。倘若該自定義類型的類也是默認生成的構造函數,那結果自然也沒有被有效初始化。
默認生成的構造函數對內置類型的成員變量不作處理,對自定義類型成員會調用它們的構造函數來初始化自定義類型成員變量。
一個類中最好要一個默認構造函數,因為當該類對象被當作其他類的成員時,系統只會調用默認的構造函數。
目前還只是了解掌握基本的用法,對構造函數在之后還會再談。
析構函數同樣是個特殊的函數,負責清理和銷毀一些類中的資源。
與構造函數的功能相反,析構函數負責銷毀和清理資源。但析構函數不是完成對象的銷毀,對象是main函數棧幀中的局部變量,所以是隨 main 函數棧幀創建和銷毀的。析構函數會在對象銷毀時自動調用,主要清理的是對象中創建的一些成員變量比如動態開辟的空間等。
2.2 析構函數的特性
析構函數的名字是~加類名,同樣是無參無返回類型,故不支持重載。
一個類中有且僅有一個析構函數,同樣若未顯式定義,編譯器自動生成默認的析構函數。
對象生命周期結束時,系統自動調用析構函數完成清理工作。
多個對象調用析構函數的順序和創建對象的順序是相反的,因為哪個對象先壓棧哪個對象就后銷毀。
調用對象后自動調用析構函數,這樣的機制可以避免忘記釋放空間以免內存泄漏的問題。不一定所有類都需要析構函數,但對于有些類如棧就很方便。
默認析構函數清理規則
和默認生成的構造函數類似,默認生成的析構函數同樣對內置類型的成員變量不作處理,只在對象銷毀時對自定義類型的成員會調用它們的析構函數來清理該自定義類型的成員變量。
倘若該自定義類型成員同樣只有系統默認生成的的析構函數,那么結果就相當于該自定義類型成員也沒有被銷毀。
不釋放內置類型的成員也是有一定道理的,防止釋放一些文件指針等等可能導致程序崩潰。
除了初始化和銷毀工作以外,最常見的就是將一個對象賦值、傳參等就必須要拷貝對象。而類這種復雜類型直接賦值是不起作用的,拷貝對象的操作要由拷貝構造函數實現,每次復制對象都要調用拷貝構造函數。
根據需求我們也可以猜測出C++中的拷貝構造函數的設計。
拷貝構造函數也是特殊的成員函數,負責對象的拷貝賦值工作,這個操作只能發生在對象實例化的時候,拷貝構造的本質就是用同類型的對象初始化新對象,所以也算是一種不同形式的構造函數滿足重載的要求,也可叫復制構造函數。
拷貝構造函數僅有一個參數,就是同類型的對象的引用,在用同類型的對象初始化新對象時由編譯器自動調用??截悩嬙旌瘮狄彩菢嬙旌瘮?,所以拷貝也是構造的一個重載。
拷貝構造函數是構造函數的一個重載形式。
拷貝構造函數只有一個參數,且必須是同類型的對象的引用,否則會引發無窮遞歸。
因為傳值調用就要復制一份對象的臨時拷貝,而要想拷貝對象就必須要調用拷貝構造函數,而調用拷貝構造函數又要傳值調用,這樣就會在調用參數列表中“邏輯死循環”出不來了。
設計拷貝構造函數時就已經修改了系統默認生成的拷貝構造函數,所以在此過程不可以再發生拷貝操作。而傳引用不會涉及到拷貝操作所以沒問題。
另外,有趣的是設計者規定拷貝構造函數的參數必須是同類型的引用,如果設計成指針,系統就當作沒有顯式定義拷貝構造函數了。
一般拷貝構造另一個對象時,都不希望原對象發生改變,所以形參引用用const修飾。
只顯式定義拷貝構造函數,系統不會生成默認的構造函數,只定義構造函數,系統會默認生成拷貝構造。
默認拷貝構造拷貝規則
若未顯式定義拷貝構造,和構造函數類似,默認生成的拷貝構造函數對成員的拷貝分兩種:
對于內置類型的成員變量,默認生成的拷貝構造是把該成員的存儲內容按字節序的順序逐字節拷貝至新對象中的。這樣的拷貝被稱為淺拷貝或稱值拷貝。類似與memcopy函數。
對于自定義類型的成員,默認生成的拷貝構造函數是調用該自定義類型成員的拷貝構造函數進行拷貝的。
默認生成的拷貝函數也不是萬能的,比如棧這個結構。用st1初始化st2時,會導致二者的成員_a指向相同的一塊空間。
運算符重載是C++的一大利器,使得對象也可以用加減乘除等各種運算符來進行相加相減比較大小等有意義的運算。默認情況下C++不支持自定義類型像內置類型變量一樣使用運算符的,這里的規則需要開發者通過運算符重載函數來定義。
運算符重載增強了代碼的可讀性也更方便,但為此我們必須要為類對象編寫運算符重載函數以實現這樣操作。運算符重載是具有特殊函數名的函數,也具有返回類型、函數名和參數列表。重載函數實現后由編譯器自動識別和調用。
函數名是關鍵字operator加需要重載的運算符符號,如operator+,operator=等。
返回類型和參數都要根據運算符的規則和含義的實際情況來定。
bool operator>(const Date& d1, const Date& d2) { if (d1._year > d2._year) { return true; } else if (d1._year == d2._year && d1._month > d2._month) { return true; } else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) { return true; } return false; } d1 > d2; operator>(d1, d2);
兩個日期類進行比較大小,傳參采用對象的常引用形式,避免調用拷貝構造函數和改變實參,返回類型為布爾值,同樣都是符合實際的。編譯器把
operator>(d1,d2)
轉換成d1>d2
,大大提高了代碼的可讀性。
只能重載已有的運算符,不能通過連接其他符號來定義新的運算,如operator@。
重載操作符函數只能作用于自定義類型對象,且最多有兩個參數,自定義類型最好采用常引用傳參。
重載內置類型的操作符,建議不改變該操作符本身含義。
共有5個運算符不可被重載,分別是:.*,域訪問操作符::,sizeof,三目運算符?:,結構成員訪問符.。
運算符重載不像構造函數是固定在類中的特殊的成員函數,運算符重載適用于所有自定義類型對象,并不單獨局限于某個類。但由于類中的成員變量是私有的,運算符重載想使其作用于某個類時,解決方法有三:
修改成員變量的訪問權限變成公有,但破壞了類的封裝性,是最不可取的。使用友元函數,但性質與修改訪問權限類似,同樣不可取的。
使用Getter Setter方法提供成員變量的接口,保留封裝性但較為麻煩。
將運算符重載函數放到類中變成成員函數,但需要注意修改一些細節。作為類成員的重載函數,形參列表默認隱藏 this 指針,所以必須去掉一個引用參數。
class Date { public: Date(int year = 0, int month = 1, int day = 1); bool operator>(const Date& d); private: int _year; int _month; int _day; }; //bool Date::operator>(Date* this, const Date& d) {...} bool Date::operator>(const Date& d) { // ... } d1 > d2; d1.operator>(d2); //成員函數只能這樣調用
賦值運算符重載實現的是兩個自定義類型的對象的賦值,和拷貝構造函數不同拷貝構造是用一個已存在的對象去初始化一個對象,賦值運算符重載是兩個已存在的對象進行賦值操作。和兩個整形數據的賦值意義相同,所以定義時也是參考內置類型的賦值操作來的。
參數列表 —— 兩個對象進行賦值操作,由于放在類中作成員函數,參數列表僅顯式定義一個對象的引用。
返回類型 —— 賦值表達式的返回值也是操作數的值,返回對象的引用即可。
// i = j = k = 1; Date& Date::operator=(const Date& d) { if (this != &d) { //優化自己給自己賦值 _year = d._year; _month = d._month; _day = d._day; } return *this; }
不傳參對象的引用或者不返回對象的引用都會調用拷貝構造函數,為使減少拷貝和避免修改原對象,最好使用常引用。
默認賦值重載賦值規則
類中如果沒有顯式的定義賦值重載函數,編譯器會在類中默認生成一個賦值重載函數的成員函數。默認賦值重載對于內置類型的成員采用淺拷貝的方式拷貝,對于自定義類型的成員會調用它內部的賦值重載函數進行賦值。
所以寫不寫賦值重載仍然要視情況而定。
Date d5 = d1; // 用已存在的對象初始化新對象,則是拷貝構造而非賦值重載
掌握以上四種C++中默認的函數,就可以實現完整的日期類了。
class Date { public: Date(int year = 0, int month = 1, int day = 1); Date(const Date& d); ~Date(); void Print(); int GetMonthDay(); bool operator>(const Date& d); bool operator<(const Date& d); bool operator>=(const Date& d); bool operator<=(const Date& d); bool operator==(const Date& d); bool operator!=(const Date& d); Date& operator=(const Date& d); Date& operator+=(int day); Date operator+(int day); Date& operator-=(int day); int operator-(const Date& d); Date operator-(int day); Date& operator++(); Date operator++(int); Date& operator--(); Date operator--(int); private: int _year; int _month; int _day; };
日期類很簡單,一樣的函數一樣的變量再封裝起來,把之前聯系的代碼放到一起。接下來就是函數接口的具體實現細節了。
//構造函數 Date(int year = 0, int month = 1, int day = 1); //打印 void Print(); //拷貝構造 Date(const Date& d); //析構函數 ~Date(); //獲取當月天數 int GetMonthDay(); // >運算符重載 bool operator>(const Date& d); // >=運算符重載 bool operator>=(const Date& d); // <運算符重載 bool operator<(const Date& d); // <=運算符重載 bool operator<=(const Date& d); // ==運算符重載 bool operator==(const Date& d); // !=運算符重載 bool operator!=(const Date& d); // =運算符重載 Date& operator=(const Date& d); //日期+天數=日期 Date& operator+=(int day); //日期+天數=日期 Date operator+(int day); //日期-天數=日期 Date& operator-=(int day); //日期-日期=天數 int operator-(const Date& d); //日期-天數=日期 Date operator-(int day); //前置++ Date& operator++(); //后置++ Date operator++(int); //前置-- Date& operator--(); //后置-- Date operator--(int);
從上述函數聲明的列表也可以看出,構造函數、析構函數等都是相對簡單的,實現類的重點同樣也是難點是定義各種運算符的重載。
日期類的構造函數
日期類的構造函數之前實現過,但仍需注意一些細節,比如過濾掉一些不合法的日期。要想實現這個功能就要定好每年每月的最大合法天數,可以將其存儲在數組MonthDayArray,并封裝在函數GetMonthDay中以便在判斷的時候調用。
//獲取合法天數的最大值 int Date::GetMonthDay() { static int MonthDayArray[13] = { 0, 31 ,28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day = MonthDayArray[_month]; //判斷閏年 if (_month == 2 && ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))) { day += 1; } return day; } //構造函數 Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; //判斷日期是否合法 if (month > 12 || day > GetMonthDay()) { cout << "請檢查日期是否合法:"; Print(); } }
數組MonthDayArray的定義也有講究,定義13個數組元素,第一個元素就放0,這樣讓數組下標和月份對應起來,使用更加方便。確定每月的天數還要看年份是否是閏年,所以還要判斷是否是閏年,因為閏年的二月都要多一天。這些封裝在函數GetMonthDay中,調用時返回當月具體天數,放在構造函數中判斷是否日期是否合法。
由于兩個函數都是定義在類中的,默認將類對象的指針作函數參數,調用時更加方便。
析構函數、打印函數和拷貝構造函數都很簡單和之前一樣,這里就不寫了。接下來就是實現的重點運算符重載。
比較運算符的重載
//運算符重載 > bool Date::operator>(const Date& d) { if (_year > d._year) { return true; } else if (_year == d._year && _month > d._month) { return true; } else if (_year == d._year && _month == d._month && _day > d._day) { return true; } return false; } //運算符重載 >= bool Date::operator>=(const Date & d) { return (*this > d) || (*this == d); } //運算符重載 < bool Date::operator<(const Date& d) { return !(*this >= d); } //運算符重載 <= bool Date::operator<=(const Date& d) { return !(*this > d); } //運算符重載 == bool Date::operator==(const Date& d) { return (_year == d._year) && (_month == d._month) && (_day == d._day); } //運算符重載 != bool Date::operator!=(const Date& d) { return !(*this == d); }
比較運算符的重載不難實現,注意代碼的邏輯即可。主要實現>和==的重載,其他的都調用這兩個函數就行。這樣的實現方法基本適用所有的類。
加法運算符的重載
加法實現的意義在于實現日期+天數=日期的運算,可以現在稿紙上演算一下探尋一下規律。
可以看出加法的規律是,先將天數加到天數位上,然后判斷天數是否合法。
如果不合法則要減去當月的最大合法天數值,相當于進到下一月,即先減值再進位。
若天數合法,則進位運算結束。
在天數進位的同時,月數如果等于13則賦值為1,再年份加1,可將剩余天數同步到明年。
先減值再進位的原因是,減值所減的是當月的最大合法天數,若先進位的話,修改了月份則會減成下個月的天數。
//運算符重載 += //日期 + 天數 = 日期 Date& Date::operator+=(int day) { _day += day; //檢查天數是否合法 while (_day > GetMonthDay()) { _day -= GetMonthDay();//天數減合法最大值 --- 先減值,再進位 _month++;//月份進位 //檢查月數是否合法 if (_month == 13) { _month = 1; _year += 1;//年份進位 } } return *this; }
這樣的實現方法會改變對象的值,不如直接將其實現為+=,并返回對象的引用還可以避免調用拷貝構造。
實現+重載再去復用+=即可。
//運算符重載 + Date Date::operator+(int day) {//臨時變量會銷毀,不可傳引用 Date ret(*this); ret += day; // ret.operator+=(day); return ret; }
創建臨時變量并用*this初始化,再使用臨時變量進行+=運算,返回臨時變量即可。注意臨時變量隨棧幀銷毀,不可返回它的引用。
減法運算符的重載
//運算符重載 -= //日期 - 天數 = 日期 Date& Date::operator-=(int day) { //防止天數是負數 if (_day < 0) { return *this += -day; } _day -= day; //檢查天數是否合法 while (_day <= 0) { _month--;//月份借位 //檢查月份是否合法 if (_month == 0) { _month = 12; _year--;//年份借位 } _day += GetMonthDay();//天數加上合法最大值 --- 先借位,再加值 } return *this; }
實現減法邏輯和加法類似,先將天數減到天數位上,再檢查天數是否合法:
如果天數不合法,向月份借位,再加上上月的最大合法天數,即先借位再加值。并檢查月份是否合法,月份若為0則置為12年份再借位。
如果天數合法,則停止借位。
先借位再加值是因為加值相當于去掉上個月的過的天數,所以應加上的是上月的天數。
值得注意的是,修正月數的操作必須放在加值的前面,因為當月數借位到0時,必須要修正才能正常加值。
//運算符重載 - //日期 - 天數 = 日期 Date Date::operator-(int day) { Date ret(*this); ret -= day; return ret; } //日期 - 日期 = 天數 int Date::operator-(const Date& d) { int flag = 1; Date max = *this; Date min = d; if (max < min) { max = d; min = *this; flag = -1; } int gap = 0; while ((min + gap) != max) { gap++; } return gap * flag; }
日期-日期=天數的計算可以稍微轉化一下變成日期+天數=日期,讓小的日期加上一個逐次增加的值所得結果和大的日期相等,那么這個值就是二者所差的天數。
加的時候,日期不合法是因為天數已經超出了當月的最大合法天數,既然超出了,就將多余的部分留下,把當月最大合法天數減去以增加月數。減的時候同理,日期不合法是因為天數已經低于了0,回到了上一個月,那就補全上一個月的最大合法數值用此去加上這個負數,這個負數就相當于此月沒有過完的剩余的天數。
自增自減的重載
C++為區分前置和后置,規定后置自增自減的重載函數參數要顯式傳一個int參數占位,可以和前置構成重載。
//前置++ Date& Date::operator++() { return *this += 1; } //后置++ Date Date::operator++(int) { return (*this += 1) - 1; } //前置-- Date& Date::operator--() { return *this -= 1; } //后置-- Date Date::operator--(int) { return (*this -= 1) + 1; } // 實現方式2 Date ret = *this; *this + 1; return ret;
實現對象的前置后置的自增和自減,要滿足前置先運算再使用和后置先使用再運算的特性。也用上面實現好的重載復用即可?;蛘咭部梢灾苯永门R時變量保存*this,改變*this之后返回臨時變量即可。
++d2; d2.operator(); d1++; d1.operator(0);
可以看出,對于類對象來說,前置++比后置++快不少,只調用了一次析構函數,而后置++ 調用了兩次拷貝構造和三次析構。
被const修飾的類即為 const 類,const 類調用成員函數時出錯,因為參數this指針從const Date*到Date*涉及權限放大的問題。如圖所示:
想要避免這樣的問題,就必須修改成員函數的形參this,但 this 指針不能被顯式作參數自然不可被修改。為解決這樣的問題,C++規定在函數聲明后面加上 const ,就相當于給形參 this 指針添加 const 修飾。
//運算符重載 != //聲明 bool Date::operator!=(const Date& d) const; //定義 bool Date::operator!=(const Date& d) const { return !(*this == d); }
像上述代碼這樣,由 const 修飾的類成員函數稱之為 const 成員函數,const 修飾類成員函數,實際修飾函數的隱含形參 this 指針,這樣該函數就不可修改對象的成員變量。
還有兩個類的默認成員函數,取地址操作符重載和 const 取地址操作符重載,這兩個默認成員函數一般不用定義,編譯器默認生成的就夠用了。
Date* operator&() { return this; //return NULL; //不允許獲取對象的地址 } const Date* operator&() const { return this; }
當不允許獲取對象的地址時,就可以將取地址重載成空即可。
感謝各位的閱讀!關于“C++中類和對象是什么”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。