您好,登錄后才能下訂單哦!
這篇文章主要講解了“什么是C++識類和對象”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“什么是C++識類和對象”吧!
一個類中如果什么成員都沒有,那么這個類稱為空類。空類中是什么都沒有嗎?其實不然,任何一個類,再我們不寫的情況下,都會自動生成下面6個默認成員函數:
本篇文章將對這幾個默認成員函數進行簡單介紹。
我們先來看一下下面這個日期類:
class Date { public: void SetDate(int year = 0, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.SetDate(); d1.Print(); return 0; }
對于Date類,每次創建對象時可以調用SetData函數來設置對象的日期,但是如果每次創建對象時都需要調用該函數來設置日期信息,未免有些麻煩,那么能否再對象創建的同時就進行初始化呢?
這里就需要用到類的默認成員函數–構造函數了。
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,保證每個數據成員都有 一個合適的初始值,并且在對象的生命周期內只調用一次。
需要注意,構造函數雖然名為構造函數,但是其作用并非為成員變量開辟空間,而是初始化對象。其特征如下:
函數名與類名相同。
沒有返回值。
編譯器會再對象實例化時自動調用構造函數。
構造函數可以重載。
需要注意的是在類實例化對象的時候,如果變量后面帶上了(),而括號內沒有參數,那么這就成了函數聲明,該函數無參,且返回值為類名。
class Date { public: Date()//無參的構造函數 { _year = 0; _month = 1; _day = 1; } //帶參的構造函數 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1;//調用無參的構造函數 Date d2(0, 1, 1);//調用帶參的構造函數 Date d3();//無參,返回值為Date的函數聲明 return 0; }
如果類中沒有顯式定義構造函數,那么c++編譯器將會自動生成一個無參的默認構造函數,而如果用戶顯式定義了構造函數,那么編譯器將不再生成構造函數。
需要注意的是編譯器自己生成的構造函數在初始化對象時做了一個偏心的處理:即對于內置類型,編譯器不會處理;而對于自定義類型,編譯器會自定義類型調用它自己的默認構造函數。內置類型指的是語法已經定義好的類型,如:int,double,long等等;自定義類型是使用struct/class/union定義的類型。
這是什么意思呢?我們通過下面這個代碼來理解:
class C { public: C() { cout << "C()" << endl; } private: int _c; }; class Date { public: //若用戶顯式定義了構造函數,那么編譯器將不再生成 /*Date() { _year = 0; _month = 1; _day = 1; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; }*/ private: //內置類型 int _year; int _month; int _day; //自定義類型 C c1; }; int main() { Date d1;//調用無參的構造函數 return 0; }
通過調試可以發現,d1自身的內置類型變量仍為隨機值,編譯器調用的構造函數并沒有處理,而對于自定義類型,可以看到編譯器調用了自定義類型中的默認函數,但是實際上如果調用編譯器自己生成的默認構造函數,最終的結果就是所有的內置類型變量仍然為隨機值,這么看下來好像編譯器自己生成的構造函數好像沒什么用?
實則不然,比如我們曾做過用棧實現隊列的題,這道題的思路是用兩個棧來回倒保證隊列的先進先出,而這里面的兩個結構棧和用棧實現的隊列的代碼為:
class Stack//棧 { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { cout << "malloc fail" << endl; exit(-1); } _top = 0; _capacity = capacity; } private: int* _a; int _top; int _capacity; }; struct MyQueue//用兩個棧實現隊列 { Stack s1; Stack s2; };
可以看到在用MyQueue這個類實例化對象時,編譯器調用Stack中的構造函數分別對成員變量s1和s2初始化,因此,我們無需再對其進行初始化了,這相對來說方便了許多。
無參的構造函數和全缺省的構造函數都被稱為默認構造函數,但是需要注意的是:無參的構造函數和全缺省的構造函數二者只能存在一個,這是因為,如果二者都存在的話,那么在實例化對象不帶參數時,編譯器無法區分是調用哪一個函數。
class Date { public: Date() { _year = 0; _month = 1; _day = 1; } Date(int year = 0, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1;//錯誤,編譯器無法識別要調用哪一個構造函數 return 0; }
在實際過程中,我們更傾向于使用全缺省的構造函數,因為它包含了無參的構造函數的情況。
可以注意到的是在定義類的時候成員變量前都加了一個_,這是為了防止下面這種情況:
class Date { public: Date(int year = 0, int month = 1, int day = 1) { year = year; month = month; day = day; } void Print() { cout << year << "/" << month << "/" << day << endl; } private: int year; int month; int day; }; int main() { Date d1; d1.Print(); return 0; }
可以看到,d1調用了構造函數后,其成員變量認為隨機值。這是因為在year = year這句代碼中,兩個year變量均為函數形參,實際上編譯器在處理這種變量時,會遵循局部優先原則,即編譯器在函數形參中找到了year變量,就不會繼續擴大搜索范圍去尋找成員變量中的year變量,而在Print函數中,編譯器由于在形參中未找到year變量,因此繼續擴大搜索范圍,在成員變量中找到了year并使用之。
因此,在聲明成員變量的命名時需要遵循一定的規范,常見的有:(1)在變量名前加_,如_year (2)在變量名后加_,如year_ (3)駝峰法,如mYear,m表示member。
另外,上述情況可以通過使用this指針進行解決,即將代碼改為this->year = year;但在實際使用過程中,最好還是注重成員變量的命名
由于早期c++語法設計的缺陷,編譯器默認生成的構造函數并不會對內置類型變量初始化,因此在c++11后,語法委員會在成員變量聲明處打了一個補丁,運行,變量聲明的同時加上缺省值,比如:
class Date { public: Date(int year = 0, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: //注意,此處僅為缺省值,仍為變量聲明,而非初始化(定義) int _year = 0; int _month = 1; int _day = 1; };
與構造函數相比,析構函數相對簡單一些。析構函數的作用與構造函數的相反,析構函數并不是完成對象的銷毀,因為局部對象的銷毀工作是由編譯器來完成的。一個詞來概括析構函數的作用就是清理,即對象在銷毀的時候會自動調用析構函數,完成類當中的一些資源清理工作。
析構函數是一種特殊的成員函數,其特征如下:
析構函數名是類名前加上~號
析構函數無參數無返回值
一個類有且只有一個析構函數
若析構函數為顯式定義,那么系統會自動生成默認的析構函數。
與構造函數一樣,系統的默認析構函數對于內置類型變量不會處理,對于自定義變量會調用其自身的析構函數。
其次,對于Date類這樣的類,由于其內部沒有什么資源需要處理,因此不需要析構函數;對于Stack這樣的類,其內部由資源需要處理,比如對malloc出來的空間進行釋放,因此需要實現析構函數。
還是之前的代碼,在用兩個棧實現隊列中,在Stack類中實現了構造函數和析構函數,那么用MyQueue實例化my變量后無法自己實現初始化和空間的釋放:
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { cout << "malloc fail" << endl; exit(-1); } _top = 0; _capacity = capacity; } ~Stack() { free(_a); _a = NULL; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; struct MyQueue { Stack s1; Stack s2; }; int main() { //我們無需自己對mq進行初始化和清理空間 //編譯會自動調用構造函數和析構函數 MyQueue mq; return 0; }
class Date { public: Date(int year = 0, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } ~Date() { cout << "~Date()" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0;//編譯器在執行這句代碼的同時會調用類中的析構函數 }
拷貝構造函數,顧名思義,其作用就是創建一個和被拷貝對象一模一樣的對象。
拷貝構造函數只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創建新對象時由編譯器自動調用。
拷貝構造函數也是特殊的成員函數,其特征是:
拷貝構造函數是構造函數的一個重載形式
參數只有一個且為引用傳參
拷貝構造函數的參數只有一個且必須為引用傳參,使用傳值方式會引發無窮遞歸調用。
class Date { public: Date() { _year = 0; _month = 1; _day = 1; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; } Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
那么為什么說傳值會導致無窮遞歸調用呢?首先我們需要理解到調用函數傳值給形參也是一種拷貝,比如說:
同樣的,對于拷貝構造函數,若形參為傳值調用,那么在上述代碼中將d2賦值給形參d時也會調用拷貝構造函數,而每一次調用拷貝構造函數都會經過依次賦值操作,從而導致無窮遞歸調用:
而傳引用就能夠很好的解決這個問題,其次,傳指針也可以達到目的,不過一般傳引用的話可以增強代碼可讀性。
與構造函數一樣,如果我們自己沒有實現拷貝構造函數,那么編譯器會生成默認的拷貝構造函數;但是與構造函數不同的是,默認的拷貝構造函數對于內置類型和自定義類型變量都會處理:
(1)對于內置類型,默認的拷貝構造函數會對對象進行淺拷貝,即按照內存存儲中的字節序對對象進行拷貝,也叫值拷貝。
(2)對于自定義類型,默認的拷貝構造函數會調用自定義類型中自己的拷貝構造函數。
class A { public: A() { _a = 0; } A(const A& a) { cout << "A(const A& a)" << endl; } private: int _a; }; class Date { public: Date() { _year = 0; _month = 1; _day = 1; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //調用默認的拷貝構造函數 /*Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; }*/ private: int _year; int _month; int _day; A aa; }; int main() { Date d1; Date d2(d1); return 0; }
通過上面我們知道了默認的拷貝構造函數能夠實現淺拷貝,也就是說,對于Date這樣的類,我們無需自己實現拷貝構造函數只用默認的拷貝構造函數就能夠實現拷貝目的,那么是否用編譯器自己的函數就夠了呢?
其實不然,比如我們熟知的Stack類,如果直接調用系統默認的拷貝構造函數:
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { cout << "malloc fail" << endl; exit(-1); } _top = 0; _capacity = capacity; } ~Stack() { free(_a); _a = NULL; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack s1(8); Stack s2(s1); return 0; }
上述代碼,我們運行后發現,程序崩潰了,這是為什么呢?這是因為系統默認的拷貝構造函數拷貝出了一份與s1一模一樣的s2:
而我們知道當對象的生命周期結束時,系統會自動調用析構函數對類空間進行清理,由于s2是后壓棧的,因此會先清理,這時s2._a所指的空間已經free還給操作系統了,但是s1還會再次調用析構函數,將已經釋放的s1._a所指向的空間再一次釋放(注意,s2._a釋放完后s1._a仍指向原空間,此時s1._a為野指針),這個操作最終會導致程序崩潰。
可見編譯器默認的拷貝構造函數并不能解決所有的問題,淺拷貝會導致一些錯誤,那么要如何解決淺拷貝的帶來的問題呢?這就要我們之后介紹的深拷貝來解決了。
感謝各位的閱讀,以上就是“什么是C++識類和對象”的內容了,經過本文的學習后,相信大家對什么是C++識類和對象這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。