C++類和對象之類的6個默認成員函數詳解
1.類的6個默認成員函數
默認成員函數:用戶沒有顯示實現,編譯器會生成的成員函數稱為默認成員函數。
如果一個類中什么成員都沒有,簡稱為空類。
但空類中真的是什么都沒有嗎?并不是的,任何一個類在我們不寫的情況下,都會自動生成下面6個默認成員函數。
class Date{};
- 構造函數: 完成初始化工作
- 析構函數: 完成清理工作
- 拷貝構造函數: 使用同類對象初始化創(chuàng)建對象
- 賦值重載: 把一個對象賦值給另一個對象
- 取地址操作符重載: 對普通對象取地址
- const取地址操作符重載: 對const修飾的對象取地址
2.構造函數
2.1概念
對于下面的Date類:
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 today1; today1.Init(2023,1,16); today1.Print(); Date today2; today2.Init(2023, 1, 17); today2.Print(); return 0; }
對于Date類,可以通過Init公有方法給對象設置日期,但如果每次創(chuàng)建對象時都調用該方法設置信息,未免有點麻煩,那能否在對象創(chuàng)建時,就將信息設置進去呢?
我們就需要一個函數:保證對象被創(chuàng)造出來就被初始化了。
C++的構造函數提供了這個功能:
構造函數是一個特殊的成員函數,名字與類名相同,創(chuàng)建類類型對象時由編譯器自動調用,以保證每個數據成員都有一個合適的初始值,并且在對象整個生命周期內只調用一次。
2.2特性
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創(chuàng)建對象,而是初始化對象。
其特征如下:
- 函數名與類名相同。
- 無返回值(也不用寫void)
- 對象實例化時編譯器自動調用對應的構造函數。
構造函數可以重載。(一個類可以有多個構造函數)
class Date { public: Date() { cout << "自定義默認構造函數" << endl; } //Date(int year = 1, int month= 2, int day = 3) //{ // cout << "自定義全缺省默認構造函數" << endl; //} //Date(int year, int month, int day = 1) //{ // cout << "自定義半缺省構造函數" << endl; //} Date(int year, int month, int day) { cout << "自定義構造函數" << endl; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 6); return 0; }
無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數一個類中只能有一個。
原因: 這兩個構造函數雖然滿足重載,但編譯器無法調用,存在歧義。如上面代碼的第一個無參構造函數和第二個注釋的全缺省的構造函數,所以默認構造函數一個類只能有一個。(非默認構造函數也只能有一個,如第三個半缺省構造函數和第四個構造函數,需要傳參,同時存在會有歧義)
注意: 更具構造函數需不需要傳參數,我們將其分為兩種
- 默認構造函數: 無參構造函數、全缺省構造函數,我們沒寫編譯器默認生成的構造函數(下一條)(這些不需要傳參數的構造函數,都認為是默認構造函數)
- 傳參構造函數: 不缺省構造函數、全缺省構造函數
- 創(chuàng)建對象時,調用默認構造函數不要在對象后加括號(加括號后編譯器會將其看作函數的聲明,而不是創(chuàng)建的對象)。調用傳參的構造函數在對象后加括號加參數。
如果類沒有顯示定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯示定義編譯器將不再生成。(構造函數可以重載,有很多種,只要我們寫一種,編譯器就不會默認生成構造函數)
注意: 如下,創(chuàng)建對象時不帶括號,調用的是默認構造函數,帶括號后跟參數,調用傳參構造函數。
如下圖類中已經定義了構造函數,編譯器不會在自動生成默認構造函數。
在增加默認構造函數后,正常運行
關于編譯器生成的默認構造函數,很多人會疑惑:不實現構造函數的情況下,編譯器會生成默認的構造函數。但是看起來默認構造函數又沒什么用?
如下面的代碼,today對象調用了編譯器生成的默認構造函數,但是today對象的三個成員變量_day/_month/_year,依然是隨機數,也就是說在這里編譯器生成的默認構造函數并沒有什么用?
先介紹一點,C++將類型分為以下兩種:
- 內置類型: 語言提供的數據類型,如:int、char…
- 自定義類型: 我們使用struct、class、union等自己定義的類型
如果一個類中存在自定義類型的成員變量,需要使用該成員變量對應類的默認構造函數來初始化,否則無法通過。這也就是默認構造函數存在的意義。
自定義類型的成員變量對應類存在默認構造函數
class A { public: A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: Date() { cout << "默認構造函數" << endl; } Date(int year, int month, int day) { cout << "傳參構造函數" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
自定義類型的成員變量對應類不存在默認構造函數
class A { public: A(int c) { cout << "A" << endl; } private: int a; int b; }; class Date { public: Date() { cout << "默認構造函數" << endl; } Date(int year, int month, int day) { cout << "傳參構造函數" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
注意: 在C++11中針對內置成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在類中聲明時可以給默認值。
class A { public: void Print() { cout << _a << " " << _b << endl; } private: int _a = 10; int _b = 20; }; class Date { public: void Print() { a1.Print(); } private: int _year = 2000; int _month = 1; int _day = 1; A a1; }; int main() { Date today; today.Print(); return 0; }
此為缺省值,當構造函數沒有初始化成員變量時,成員變量的值即為該缺省值,若初始化,以構造函數為主(如下面代碼,初始化了一個變量,該變量就以構造函數初始化為主,其他成員變量為缺省值)
class A { public: A() { _a = 40; } void Print() { cout << _a << " " << _b << endl; } private: int _a = 10; int _b = 20; }; class Date { public: void Print() { a1.Print(); } private: int _year = 2000; int _month = 1; int _day = 1; A a1; }; int main() { Date today; today.Print(); return 0; }
3.析構函數
3.1概念
我們知道了對象創(chuàng)建時需要構造函數來初始化,那對象銷毀時又需要什么呢?
析構函數: 與構造函數相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時自動調用析構函數,完成對象中資源的清理工作。
我們創(chuàng)建一個對象,它是在對象生命周期結束后,對應函數的棧幀銷毀時一并銷毀,而析構函數是在銷毀前函數自動調用,對該對象的資源做清理清空對象的空間或將申請的空間還給編譯器。
對于清理工作,我們必須要做,否則可能會造成內存泄漏,而我們又常常忘記這一操作,于是C++增加了這么一個函數。
3.2特性
- 析構函數名是在類名前加上字符**~**(取反符號)
- 無參數也無返回值
- 一個類只能有一個析構函數。若未顯示定義,系統(tǒng)會自動生成默認的析構函數。注意:析構函數不能重載
- 對象生命周期結束時,C++編譯系統(tǒng)自動調用析構函數。
我們編寫如下代碼,向內存申請空間,利用析構函數釋放對應的空間。
class Stack { public: Stack() { ArrStack = (int*)malloc(sizeof(int) * 4); if(!ArrStack)//下圖中未寫 { preeor("malloc fail!"); exit(-1); } _size = 4; _top = 0; } ~Stack() { if (ArrStack) { free(ArrStack); ArrStack = nullptr; _size = 0; _top = 0; } } private: int* ArrStack; int _size; int _top; }; int main() { Stack st; return 0; }
如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成默認析構函數,比如Date類;有資源申請時,一定要寫,否則會造成資源泄漏。
如下面的代碼,當我們對同一個類創(chuàng)建兩個變量時,構造函數的執(zhí)行順序為:s1、s2,而函數是一種棧的形式,創(chuàng)建變量就是壓棧,s1先入棧,s2后入棧,銷毀時,s2先出棧,s1后出棧,析構函數的調用順序為:s2、s1
class Stack { public: Stack(int num) { ArrStack = (int*)malloc(sizeof(int) * num); if(!ArrStack)//下圖中未寫 { preeor("malloc fail!"); exit(-1); } _size = 4; _top = 0; } ~Stack() { if (ArrStack) { free(ArrStack); ArrStack = nullptr; _size = 0; _top = 0; } } private: int* ArrStack; int _size; int _top; }; int main() { Stack s1(10); Stack s1(40); return 0; }
觀察下圖this->_size
的變化
當一個類中有自定義類型的成員變量,那再銷毀這個類創(chuàng)建的對象時,會調用該類中自定義類型的成員變量的析構函數
寫析構函數
class A { public: ~A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: ~Date() { cout << "Date" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
不寫析構函數
class A { public: ~A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
注意:
- 默認生成構造函數和默認生成析構函數,對內置類型不處理,處理自定義類型。(有些編譯器會,但那時編譯器的個人行為,和C++的語法無關)
4.拷貝構造函數
4.1概念
拷貝構造函數: 只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創(chuàng)建新對象時由編譯器自動調用。
該函數功能為將一個對象的數據賦值給另一個對象,發(fā)生拷貝時編譯器就會調用該函數,如下:
class Date { public: Date(int year,int month,int day) { _year = year; _month = month; _day = day; } Date(const Date& d)//拷貝構造函數 { _year = d._year; _month = d._month; _day = d._day; cout << "拷貝構造函數" << endl; } private: int _year; int _month; int _day; }; void test(Date d)//調用拷貝構造函數 {} int main() { Date today1(2023,2,7); Date today2(today1);//調用拷貝構造函數 test(today1); return 0; }
4.2特征
拷貝構造函數是構造函數的一個重載形式。
拷貝構造函數的參數只有一個 且 必須是類類型對象的引用 ,使用傳參方式編譯器會直接報錯 ,因為會引發(fā)無窮遞歸調用。
如果不使用引用,代碼如下:
class Date { public: Date(const Date d)//拷貝構造函數 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; };
這樣的拷貝構造函數,我們在調用它時會發(fā)生拷貝,而需要拷貝我們就要調用拷貝構造函數,這就會形參死循環(huán),因為要用你我調用你,而想要調用你就要用你,編譯器不會允許這樣的事情發(fā)生。
如上圖,將對象d1的數據拷貝到d2,需要調用拷貝構造函數,而調用的過程形參發(fā)生拷貝又要調用拷貝構造函數,就這樣一直下去,很明顯這是不行的。
所以在這里我們要使用引用,如下:
Date(const Date& d)//拷貝構造函數 { _year = d._year; _month = d._month; _day = d._day; }
在第一次調用的時候,使用d給對象起別名,就不用再調用其他拷貝構造函數。
對于這個函數建議使用const
修飾,防止我們在寫這個函數時不小心寫錯,使對象的成員變量發(fā)生改變。
**若未顯示定義,編譯器會生成默認的拷貝構造函數。**默認的拷貝構造函數對象按內存存儲按字節(jié)序完成拷貝,這種拷貝叫淺拷貝,或值拷貝。
即上面的Date類對象,若發(fā)生淺拷貝只是將一個對象所占空間內所有成員變量的值拷貝到另一個對象的成員變量,這么做看起來似乎很合理其實不然,對于內置類型,這么做當然沒有問題,但如棧這樣的數據結構,是萬萬不能的。如下面棧的代碼
class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(int* sizeof(int)); if (nullptr == _array) { perror("malloc申請空間失敗"); return; } _size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: int *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
這樣程序必定會發(fā)生錯誤。
如果想要讓程序正確運行我們,需要我們自己編寫拷貝構造函數,也就是深拷貝,讓他們每個對象的的成員變量在面對這種情況時都有自己獨立的空間,而不是共用一塊空間。
這也是拷貝構造函數存在的意義,編譯器只能做淺拷貝的工作,若果一個對象的拷貝需要使用深拷貝,就需要程序員手動來完成這個任務,這也是C語言存在的缺陷,C++的很好的彌補了這一點。
修改后的棧代碼如下:
class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(capacity * sizeof(int)); if (nullptr == _array) { perror("malloc申請空間失敗"); return; } _size = 0; _capacity = capacity; } Stack(const Stack& st) { _array = (int*)malloc(sizeof(int) * st._capacity); if (_array == nullptr) { perror("malloc申請空間失敗"); return; } for (int i = 0; i < st._size; i++) { _array[i] = st._array[i]; } _size = st._size; } void Push(const int& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
所以如果類中沒有涉及資源申請時,拷貝構造函數是否寫都可以,若涉及到資源申請時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。
拷貝構造函數調用頻率最多的三種場景場景如下
- 使用以存在的對象創(chuàng)建新對象
- 函數參數類型為類類型對象
- 函數返回值類型為類類型對象
通過這些我們也可以看出,拷貝在編寫代碼中是一個平常的事情,但其消耗的資源卻不少,所以在實際使用中,如果可以使用引用,盡量使用引用,減少計算機消耗,創(chuàng)出更優(yōu)得程序。
5.賦值運算符重載
5.1運算符重載
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數, 也具有其返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。
在C++中類的封裝性是做的很好的,如果想在類和類之間進行比較,拷貝等操作需要在類內調用函數,而對應普通的內置類型,只需要使用簡單的運算符即可完成,C++規(guī)定可以將部分運算符重載來完成這個功能,增強了代碼的可讀性。
函數名字為:關鍵字operator后面接需要重載的運算符符號。
函數原型:返回值類型 operator操作符(參數列表)
注意:
- 不能通過連接其他符號來創(chuàng)建新的操作符:比如operator@
- 重載操作符必須有一個類類型參數
- 用于內置類型的運算符,其含義不能改變,例如:內置的整形+,不能改變其含義
- 作為類成員函數重載時,其形參看起來比操作數數目少1.因為成員函數的第一個參數為隱藏的this
- .* :: sizeof ?: .注意以上5個運算符不能重載。在這個經常在筆試選擇題中出現。
如下代碼,若運算符重載函數作用域為全局,那類的成員變量必須為公有的,這樣封裝性就無法保證
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //成員變量變?yōu)楣?,才能使類外訪問 //private: int _year; int _month; int _day; }; bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } bool test() { Date today1(2023, 2, 7); Date today2; return today1 == today2; }
這里我們可以使用友元解決,也可以將運算符重載函數放入類中,我們一般將其放入類中。
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d1) { return _year == d1._year && _month == d1._month && _day == d1._day; } private: int _year; int _month; int _day; }; bool test() { Date today1(2023, 2, 7); Date today2; //today1.operator==(today2) return today1 == today2; }
在調用成員函數時,編譯器會自動將調用的對象作為this指針傳遞,我們只要寫入一個參數即可。
注意:
在使用時需要注意運算符優(yōu)先級,如下面使用運算符重載需使用括號
cout << (today1 == today2) << endl;
運算符重載中,如果有多個參數,第一參數為左操作數,第二個參數為右操作數,以此類推。如上面的代碼,第一個參數為today1,為左操作數,由該對象調用運算符重載函數,第二參數today2即為參數。
5.2賦值運算符重載
賦值運算符如果不自己實現,編譯器會默認生成,只有賦值和取地址是這樣的,其它的自定義類型需要使用,就要我們自己寫。(取地址在下面)
賦值運算符重載格式:
- 參數類型: const Typedef&,傳遞引用可以提高傳參效率
- 返回值類型: Typedef&,返回引用可以提高返回得效率,有返回值目的是為了支持連續(xù)賦值。
- 檢測是否自己給自己賦值
- **返回*this:**要符合連續(xù)賦值得含義
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator=(const Date& d) { if (this != &d)//檢測是否自己給自己賦值 { _year = d._year; _month = d._month; _day = d._day; } return *this;//返回*this } private: int _year; int _month; int _day; };
賦值運算符只能重載成類得成員函數不能重載成全局函數
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private: int _year; int _month; int _day; }; //全局函數不能用`this`指針,需要給兩個參數 Date& operator=(const Date& d1, const Date& d2) { if (d1 != &d2)//檢測是否自己給自己賦值 { d1._year = d2._year; d1._month = d2._month; d1._day = d2._day; } return d1;//返回*this }
其中為了訪問類中得成員變量,將其公有化,失去了封裝性。這樣得函數注定編譯失敗,其中賦值運算符沒有實現,則編譯器會在類中自己實現一個默認的賦值運算符,而在調用得時候,我們自己實現了一個,編譯器又實現了一個這就產生沖突。
所以,賦值運算符重載只能是類的成員函數。
上面已經講了,如果我們沒有自己寫,編譯器會自己實現一個默認的賦值運算符重載,在運行是是以值得方式逐字節(jié)拷貝。 上面得拷貝構造函數中,編譯器自己默認創(chuàng)建的拷貝構造函數也是相同的,只能進行淺拷貝,只能拷貝值無法為其分配內存,但賦值運算符重載還是有一點不同的,它初始化需要分配空間的時候,會先為創(chuàng)建的對象分配空間,之后在使用賦值運算符,將分配好的空間舍棄,存入其他對象的空間地址。
如下代碼:
// 這里會發(fā)現下面的程序會崩潰掉?這里就需要我們以后講的深拷貝去解決。 class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(capacity * sizeof(int)); if (nullptr == _array) { perror("malloc申請空間失敗"); return; } _size = 0; _capacity = capacity; } void Push(const int& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; s2 = s1; return 0; }
我們要注意,如果類中未涉及到資源管理,賦值運算符是否實現都可以;一旦涉及到資源管理則必須要實現。
5.3前置++和后置++重載
對于前置++,我們按照正常的運算符重載模式寫即可,但記得返回類型需要使用類類型&
,將修改后的對象返回。
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator++() { _year += 1; return *this; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 7); Date d; d = ++today; //d:2024.2,7 today:2024,2,7 return 0; }
至于后置++,為了可以讓兩個函數實現重載,規(guī)定增加一個int類型的參數,作為區(qū)分。
注意:前置++是先++后使用,所以可以直接返回其修改后的對象,對于后置++是先使用后++,所以返回的應該是未修改的對象,我們可以在修改原對象前對其進行拷貝,然后修改原對象,返回時直接返回之前拷貝的對象,這樣原對象即改變了,使用的也是未改變的對象,符合后置++
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date operator++() { Date& temp(*this); _year += 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 7); Date d; d = today++; //d:2023,2,7 today:2024,2,7 return 0; }
5.4流插入和流提取運算符重載
在C++中,我們輸出和輸入一個數據通常是通過cout、cin
,它們兩其實就是一個類對象,重載了<<、>>
兩個運算符,所以輸入、輸出其實就是調用兩個運算符重載函數。
如上圖,它們的類型分別為ostream、istream
,存放在iostream
這個頭文件中,而C++庫內定義的東西都存放在std
這個命名空間內,所以我們每次開頭需要寫這兩行代碼。
對于內置類型,如下:
int a = 10; double b = 10.0; cout << a; cout << b;
通過函數重載調用不同的運算符函數,將其打印。
下面我們一起來看一下這兩個運算符是如何重載的。
流提取
在類中定義:
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void operator<<(ostream& out) { //下面就是輸出內置類型的值,流提取調用頭文件<iostream>內的 out << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date today; //第一個參數為左操作數,第二個參數為右操作數,由創(chuàng)建的對象調用類內的重載函數 //today.operator<<(cout) today << cout; return 0; }
我們看到,函數的使用形式是today << cout;
,類對象搶占第一個參數,一定在左邊,cout在右邊,這么寫肯定不符合我們平常的習慣,如果要將cout放在第一個位置,我們需要將函數在全局定義。
class Date { public: friend ostream& operator<<(ostream& out, const Date& d); Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; //不對對象的成員變量做修改,最好使用const修飾,防止寫錯,發(fā)生錯誤 ostream& operator<<(ostream& out,const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } int main() { Date today; cout << today; return 0; }
如上面的代碼,我們函數變?yōu)槿趾?,很好的解決了位置的問題,但我們又無法訪問類中的成員變量,這里有三種方法,我們使用第一種
使該函數變?yōu)轭惖挠言瘮担陬愔衟ublic作用域下,使用friend修飾函數的聲明,即可在該函數內使用對應類的對象調用成員變量。增加接口,在類中創(chuàng)建輸出函數,調用對應函數即可得到對應的成員變量值,對象在類外無法訪問成員變量,但可以訪問對外開發(fā)的函數。(java喜歡這么做)刪除private作用域,這樣成員變量就可以訪問。(不建議這么做,破壞封裝性)
為了防止出現下面的情況,以此要輸出多個對象的值,我們需要使重載的函數返回cout
,使函數可以正常運行。
cout << d1 << d2 << d3 << endl; //cout << d1 //調用重載函數,調用后返回cout繼續(xù)執(zhí)行 //cout << d2 //同時,運行后返回cout //.. //cout << endl; //與重載的類型不匹配,調用頭文件內的函數
流插入
class Date { public: friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } //需要改變對象的成員變量,不能使用const修飾 istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; } int main() { Date today; cin >> today; cout << today; return 0; }
如上面的代碼與流提取相似。
6.const成員
如下面的代碼,是否可以正常運行呢?
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << "Print" << endl; } private: int _year; int _month; int _day; }; int main() { const Date d; d.Print(); return 0; }
它不能正常運行,因為對象d使用const修飾了,它的值是無法改的(該對象的成員變量無法修改)。在調用成員函數時,編譯器默認傳過去的值為Date* const this
,this指針表示對象本身,意味著在此函數內成員變量可以改變,這產生了沖突。更簡單的說,這就是將原本只能讀的對象變成可讀可寫,無視其權限。
想要解決這個問題,只要使用const修飾*this使其無法改變即可,而this又是編譯器默認的,它是被隱藏著的不好修改,C++給出了如下方法,在成員函數的括號后直接加const即表示修飾*this
,如下
void Print() const { cout << "Print" << endl; }
如果我們使用為被修飾的const對象調用被const修飾的成員函數,這時可以的,原本對象就可以通過調用成員函數修改和讀取,現在只是傳過去只能使成員函數讀取這沒有問題。
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() const { cout << "Print" << endl; } private: int _year; int _month; int _day; }; int main() { Date d; d.Print(); return 0; }
同理:在類中成員函數是可以相互調用的,但被const修飾的成員函數無法調用沒有被修飾的,因為被修飾的成員函數所函數*this指針是無法改變的,而沒有被修飾的是可以改變的,const失去了作用,這種寫法是錯誤的。而沒有被修飾的成員函數是可以調用被修飾的,這屬于即可讀又可寫的情況向只可讀的情況發(fā)展,沒有改變語法。
注意:
類內部不改變成員變量的成員函數,最好加上const,防止數據被修改
一般會在下面的場景用到const成員
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() const { cout << _year << endl; } private: int _year; int _month; int _day; }; void test(const Date& d) { d.Print(); } int main() { Date td; test(td); return 0; }
我們在創(chuàng)建對象之初一般不為其修飾cosnt,但我們會經常將對象作為實參傳遞給其他函數,如果形參被const修飾,那在這個函數內它只能被讀,無法修改意味著調用的成員函數也必須被const修飾。
const這種寫法只針對成員函數
若定義和聲明分離,需要修飾const時,定義和聲明都要修飾
成員函數被const修飾和不被修飾構成const重載
void Print() const { cout << _year << endl; } void Print() { cout << _year << endl; }
一個形參為Date* const this
,一個為const Date* const this
,形參不同滿足重載
若是成員函數被const修飾注意它的返回值類型,若返回的是成員變量,也需要修飾const,否則權限發(fā)生變化,編譯會出錯
7.取地址重載和const取地址操作符重載
取地址重載和const取地址操作符重載是最后兩個編譯器默認生成的成員函數,我們一般不會去寫它,而是直接去使用編譯器默認生成的。
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
我們如果想要寫出來也可以,如下:
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } Date* operator&()//取地址重載 { return this; } const Date* operator&() const //const取地址操作符重載 { return this; } private: int _year; int _month; int _day; }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
兩個使用的場景不同,取地址重載用在取一般的對象的地址,const取地址操作符重載用在取被const修飾的對象的地址。
總結
到此這篇關于C++類和對象之類的6個默認成員函數的文章就介紹到這了,更多相關C++類的默認成員函數內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!