C++類別和物件(中)

2022-01-07 10:00:02

類的六個預設建構函式

上一次的內容我們瞭解了什麼是類,類成員函數的this指標以及類的物件的計算。如果一個類中什麼成員都沒有,簡稱為空類。空類中什麼都沒有嗎?其實並不是的,任何一個類在我們不寫的情況下,都會自動生成下面6個預設成員函數

class 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 d1;
	d1.Init(2020, 5, 1);
	d1.Print();

	Date d2;
	d2.Init(2020, 6, 23);
	d2.Print();
	return 0;

}

在前面學習資料結構的時候我們知道,建立一個變數之後一般都會呼叫一下初始化函數,如果忘記初始化,程式就有可能會出問題。對於上面Date類,可以通過Init公有的方法給物件設定內容,那麼C++是否像C語言那樣每次建立一個變數都必須要呼叫一次初始化函數呢?

答案是否定的,C++中為了使每個物件範例化時都被初始化就有了建構函式

建構函式是一個特殊的成員函數,名字與類名相同,建立類型別物件時由編譯器自動呼叫,保證每個資料成員都有一個合適的初始值,並且在物件的生命週期內只呼叫一次。

特性

建構函式是特殊的成員函數,大家聽這個名字可能都以為是構造一個物件出來,但是其實不是的。建構函式雖然名稱叫做構造,但是需要注意的是建構函式的主要任務並不是開空間建立物件,而是初始化物件

其特徵如下:

  • 函數名與類名相同。
  • 無返回值(無返回值並不表示返回型別是void,void是有返回值的只不過返回值為空
  • 物件範例化時編譯器自動呼叫對應的建構函式
  • 建構函式可以過載(可以存在多種初始化方式)

下面我們通過程式碼來一起看一下建構函式

class Date
{
public:
	//1.無參建構函式
	Date()
	{
		cout << "Date()" << endl;
	}
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1;
    //需要注意的是:通過無參建構函式建立物件時,物件後面不用跟括號,否則就成了函數宣告。
    //以下程式碼的函數:宣告了d3函數,該函數無參,返回一個日期型別的物件
    Date d2();
    
	return 0;
}

在這裡插入圖片描述

通過上面的偵錯我們可以看到編譯器自動呼叫了無參的建構函式,但是類裡面的成員變數都是隨機值。

class Date
{
public:
	//2.帶參建構函式
	Date(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 d1(2020,5,1);
	return 0;

}

在這裡插入圖片描述

通過上面的偵錯我們可以看到,當類裡面有帶參建構函式時,並且我們給了物件一個值,物件就會按照我們想要的樣子去初始化。

class Date
{
public:
    //3.全預設建構函式
	Date(int year = 2020, int month = 5, 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;
	return 0;

}

在這裡插入圖片描述

通過上面的偵錯我們可以發現這裡編譯器通過呼叫了全預設的建構函式成功建立了一個物件。

class Date
{
public:
	如果使用者顯示定義了建構函式,編譯器將不再自動生成
	//Date(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 d1;
	return 0;

}

在這裡插入圖片描述

可以看到我們沒有定義建構函式,物件也建立成功了,這是因為此處呼叫的是編譯器預設生成的建構函式。如果我們顯示定義了建構函式,編譯器將不再生成。

總結

預設建構函式,很多同學都會以為是我們不寫,編譯器預設生成的那一個,但是這中理解是不全面的

  • 我們不寫,編譯器預設生成的

  • 我們自己寫的無參的建構函式

  • 我們自己寫的全預設的建構函式

    總結:不用引數就可以呼叫的建構函式統稱為預設建構函式

通過對上面程式碼的偵錯我們可以發現,我們顯示的寫建構函式的情況下,編譯器生成的預設建構函式好像沒有起什麼作用,成員變數依舊是隨機值。

C++把型別分成內建型別(基本型別)和自定義型別。內建型別就是語法以及定義好的型別:比如int/char等等,自定義型別就是我們使用class/struct/union自己定義的型別。我們不寫,編譯器預設生成的建構函式對內建型別不做處理,對於自定義型別會去呼叫他們的預設建構函式初始化。

大部分情況,都需要我們自己寫建構函式因為自動生成的那個不一定好用。如果一定得自己寫的話建議寫全預設的建構函式。

解構函式

概念

前面通過建構函式的學習,我們知道一個物件是怎麼來的,那一個物件又是怎麼沒的呢?

解構函式:與建構函式功能相反,解構函式不是完成物件的銷燬,區域性物件銷燬工作是由編譯器完成的。而物件在銷燬時會自動呼叫解構函式,完成類的一些資源清理工作。

特性

解構函式是特殊的成員函數

其特徵如下:

  • 解構函式名是在類名前加上字元~
  • 無引數無返回值(不能過載)
  • 一個類有且只有一個解構函式。若為顯示定義,編譯器會自動生成預設的解構函式
  • 物件生命週期結束時,C++編譯器自動呼叫解構函式

下面我們通過程式碼來看一下解構函式

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//有同學就會想,Date的解構函式好像沒啥意義?->是的
	~Date()
	{
		//資源的清理
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

在這裡插入圖片描述

我們可以看到物件生命週期結束時,C++編譯器自動呼叫了解構函式。但是有的同學就可能會想,Date的解構函式好像沒啥意義呀,對於內建型別沒有處理。是的,這裡Date的解構函式確實沒什麼意義。那解構函式在哪裡有意義呢?像Stack這樣的類,解構函式具有重大的意義,我們再來看一段程式碼

class Stack
{
public:
	Stack(int capacity = 4)
	{
		if (capacity == 0)
		{
			_a = nullptr;
			_size = _capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int)*capacity);
			_size = 0;
			_capacity = capacity;
		}
        cout << "Stack()" << endl;
	}
	void Push(int x)
	{

	}

	//像Stack這樣的類,解構函式具有重大意義
	//因為不清理資源就會造成記憶體漏失
	~Stack()
	{
		cout << "~Stack()解構函式" << endl;
		//清理資源
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	Stack st2;
	st2.Push(4);
	st2.Push(5);
	st2.Push(6);

	return 0;
}

在這裡插入圖片描述

在這裡插入圖片描述

通過偵錯我們可以發現,解構函式在Stack這樣的類中具有重大意義,因為不需要我們手動的去呼叫,物件生命週期結束時,編譯器會自動呼叫,這樣就不會發生在C語言中由於我們忘記了呼叫destroy函數而出現記憶體漏失的風險了。

因為物件是定義在函數中,函數呼叫會建立棧幀。棧幀中的物件構造和解構也要符合後進先出。因此上面程式碼的順序是:s1先構造->s2後構造->s2先解構->s1後解構。

講到這個我們就再來說一個東西吧

資料結構的棧和堆和我們講的記憶體分段區域(作業系統)也有一個叫棧和堆,他們之間有什麼區別和聯絡呢?

1.他們之間沒有絕對的聯絡,因為他們屬於兩個學科的各自的一些命名

2.資料結構棧和系統分段棧(函數棧幀)中的物件都符合後進先出

總結

一個類有且只有一個解構函式。解構函式與建構函式是類似的,我們不寫編譯器預設生成的解構函式對於內建型別不做處理,對於自定義型別會去呼叫他們的解構函式清理資源。解構函式滿足後進先出的特性

拷貝建構函式

概念

在我們現實生活中,可能存在一個與你一樣的自己,我們稱其為雙胞胎。那在建立物件的時候,可否建立一個與一個物件一模一樣的新物件呢?

拷貝建構函式:只有單個形參,該形參是對本類型別物件的參照(一般常用const修飾),在用已存在的類型別物件建立新物件時由編譯器自動呼叫。

特性

拷貝建構函式也是特殊的成員函數,其特徵如下:

  • 拷貝建構函式是建構函式的一個過載形式。
  • 拷貝建構函式的引數只有一個,且必須使用參照傳參,使用傳值方式會引發無窮遞迴呼叫。

下面我們來說明一下拷貝函數為什麼不能使用參照傳參

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// 拷貝建構函式的引數只有一個且必須使用參照傳參,使用傳值方式會引發無窮遞迴呼叫。
	Date(const Date& d)
	{
		防止自己這樣寫錯 可以在形參裡面加上一個const
		//d._year = _year;
		//d._month = _month;
		//d._day = _day;

		//
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d4(d1);

	return 0;
}

在這裡插入圖片描述

如果未顯示定義,編譯器會生成預設的拷貝建構函式。預設的拷貝建構函式物件按記憶體儲存位元組序完成拷貝,這種拷貝我們叫做淺拷貝,也稱為值拷貝。

class Date
{
public:
	Date(int year = 2020, int month = 5, 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;
	Date d2(d1);
	d1.Print();
	d2.Print();
    
	return 0;
}

在這裡插入圖片描述

在這裡插入圖片描述

通過列印結果我們可以看到,d2的列印結果和d1的列印結果一模一樣。我們通過偵錯可以發現,我們不寫編譯器預設生成的預設建構函式對內建型別也做了處理,內建型別完成了淺拷貝。

我們不寫編譯器預設生成的拷貝建構函式對於內建型別也會處理,那麼這麼說來我們是不是就不用寫拷貝建構函式了呢?我們接下來再來看一段程式碼

class Stack
{
public:
	Stack(int capacity = 4)
	{
		if (capacity == 0)
		{
			_a = nullptr;
			_size = _capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int)*capacity);
			_size = 0;
			_capacity = capacity;
		}
	}
	~Stack()
	{
		free(_a);
		_size = _capacity = 0;
		_a = nullptr;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	Stack st;

	Stack copy(st);

	return 0;
}

在這裡插入圖片描述

執行程式之後我們發現程式崩了,為什麼會出現這種情況呢?

我們結合上面學的建構函式和解構函式來分析一下吧。我們不寫編譯器預設生成的拷貝建構函式對於內建型別是處理的,對於內建型別完成了淺拷貝。**那麼會先構造st物件,然後再構造copy物件,先解構copy,再解構st。**我們知道這裡的指標a是動態開闢的,物件宣告週期結束後,編譯器就會先解構copy,再解構st。淺拷貝也就是值拷貝,那麼就證明copy物件裡面的a與st裡面的a是指向同一塊空間的,那麼先解構copy後,a已經被解構函式置成nullptr了,這時再去解構st就相當於同一塊空間被free了兩次,所以我們的程式就會崩潰。其次由於指向的是同一塊空間,其中一個物件插入刪除資料,都會導致另一個物件也插入刪除了資料。

總結

預設生成的拷貝建構函式對內建型別完成了淺拷貝,對於自定義型別會去呼叫它的拷貝建構函式。

像Date這樣的類,需要的就是淺拷貝,那麼預設生成的拷貝建構函式就已經夠用了,不需要我們自己寫。

但是像Stack這樣的類、需要的是深拷貝,淺拷貝會導致解構兩次,程式崩潰等問題。

賦值運運算元過載

運運算元過載

**C++為了增強程式碼的可讀性引入了運運算元過載,運運算元過載是具有特殊函數名的函數,**也具有其返回值型別,函數名字以及參數列,其返回值型別與參數列與普通的函數類似。

函數名字為:關鍵字operator後面接需要過載的運運算元號

函數原型:返回值型別 operator操作符(參數列)

注意:

  • 不能通過連結其他符號來建立新的操作符:比如operator@
  • 過載操作符必須有一個類型別或者列舉型別的運算元
  • 用於內建型別的操作符,其含義不能改變,例如:內建的整形+,不能改變其含義
  • 作為類成員的過載函數時,其形參看起來比運算元數目少1。成員函數的操作符有一個預設的形參this,限定為第一個形參
  • .::sizeof?:.* 注意以上五個運運算元不能過載。這個經常在筆試選擇題中出現。

下面再說一個東西:

運運算元過載跟函數過載,都有用過載這個詞,但是兩個地方直接沒有關聯。

1.函數過載時支援定義同名函數

2.運運算元過載是為了讓自定義型別可以像內建型別一樣去使用運運算元

下面我們來看一下==運運算元過載的程式碼吧

class Date
{
public:
	Date(int year = 2020, int month = 5, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//bool operator==(Date* this,const Date& d);
	//這裡左運算元是this指向的呼叫函數的物件
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 1, 1);
	Date d2(2021, 1, 2);

	//內建型別,語言層面就支援運運算元
	//自定義型別,預設不支援。C++可以用運運算元過載來讓類物件支援用某個運運算元
	d1 == d2; // d1.operator==(d2); 
	cout << (d1 == d2) << endl; //這裡<<優先順序高於==,因此需要加一個括號
	//d1 < d2;

	return 0;
}
賦值運運算元過載

賦值運運算元主要有四點

  • 引數型別
  • 返回值
  • 檢測是否自己給自己賦值
  • 返回*this
  • 與拷貝建構函式類似,一個類如果沒有顯式定義賦值運運算元過載,編譯器也會生成一個,對於內建型別完成淺拷貝,對於自定義型別會去呼叫它的賦值運運算元過載。
class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// 拷貝建構函式的引數只有一個且必須使用參照傳參,使用傳值方式會引發無窮遞迴呼叫。
	Date(const Date& d)
	{
		防止自己這樣寫錯 可以在形參裡面加上一個const
		//d._year = _year;
		//d._month = _month;
		//d._day = _day;

		//
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void operator=(const Date& d) // void operator=(Date* this,const Date& d)
	{
			_year = d._year;
			_month = d._month;
			_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//解構函式,我們不需要寫,編譯器預設生成就夠用,物件內沒有資源清理
	//預設生成的解構函式也是基本不做什麼事情,release下優化,就沒了

private:
	int _year;
	int _month;
	int _day;

};

int main()
{
    Date d1(2020, 5, 26);
	Date d2;
	d1.Print();
	d2.Print();

	d1 = d2;
	d1.Print();
	d2.Print();
}

在這裡插入圖片描述

通過列印結果來看這裡好像真的完成了把d1賦給d2,但是事實真的如此嗎?這裡的賦值運運算元過載是不完整的,因為它不能夠連續賦值,或者說如果是自己給自己賦值的話就不再需要一步一步的去賦值了,因此上面的程式碼還可以完善一下。

class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// 拷貝建構函式的引數只有一個且必須使用參照傳參,使用傳值方式會引發無窮遞迴呼叫。
	Date(const Date& d)
	{
		防止自己這樣寫錯 可以在形參裡面加上一個const
		//d._year = _year;
		//d._month = _month;
		//d._day = _day;

		//
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	
	//d1 = d2; d1.operator(&d1, d2);
	//d1 = d1; //自己給自己賦值
	Date& operator=(const Date& d)  // Date& operator=(Date* this,const Date& d)
	{
		if (this != &d)//檢查如果不是自己給自己複製,才需要拷貝
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//解構函式,我們不需要寫,編譯器預設生成就夠用,物件內沒有資源清理
	//預設生成的解構函式也是基本不做什麼事情,release下優化,就沒了

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    Date d1(2020, 5, 26);
	Date d2;
    Date d3(2020,10,1);
	d1.Print();
	d2.Print();
    d3.Print();
    
    //連續賦值
	d1 = d2 = d3;
	d1.Print();
	d2.Print();
    d3.Print();
}

在這裡插入圖片描述

如此一來這個賦值運運算元過載才算是真正的大功告成了。

下面我們再通過一段程式碼來理解對比拷貝建構函式與賦值運運算元過載吧

class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// 拷貝建構函式的引數只有一個且必須使用參照傳參,使用傳值方式會引發無窮遞迴呼叫。
	Date(const Date& d)
	{
		防止自己這樣寫錯 可以在形參裡面加上一個const
		//d._year = _year;
		//d._month = _month;
		//d._day = _day;

		//
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	
	//d1 = d2; d1.operator(&d1, d2);
	//d1 = d1; //自己給自己賦值
	Date& operator=(const Date& d)  // Date& operator=(Date* this,const Date& d)
	{
		if (this != &d)//檢查如果不是自己給自己複製,才需要拷貝
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//解構函式,我們不需要寫,編譯器預設生成就夠用,物件內沒有資源清理
	//預設生成的解構函式也是基本不做什麼事情,release下優化,就沒了

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    Date d1(2020, 5, 26);
	Date d2;
    
    Date d5(d1);
	d1 = d2; 
	Date d6 = d1;
    
    return 0;
}

你認為main函數裡面的這三個是拷貝構造還是賦值運運算元過載呢?

第一個Date d5(d1):這個是一個拷貝構造,因為這是拿一個已經存在的物件去構造初始化另一個要建立的物件

第二個 d1 = d2 這是一個賦值運運算元過載,賦值運運算元過載也是拷貝行為,但是不一樣的是,拷貝構造是建立一個物件時,拿同類物件初始化的拷貝。這裡的複製拷貝是兩個物件已經都存在了,都被初始化過了,現在想把一個物件,複製拷貝給另一個物件。因此是賦值運運算元過載

第三個 Date d6 = d1: 這是一個拷貝構造,與第一個一樣這是拿一個已經存在的物件去構造初始化另一個要建立的物件

總結

賦值運運算元它也是一個預設成員函數,也就是說我們不寫編譯器會自動生成一個。

編譯器預設生成的賦值運運算元跟拷貝建構函式的特性是一樣的

  • 針對內建型別,會完成淺拷貝,也就是說像Date這樣的類不需要我們自己寫賦值運運算元過載,Stack就得自己寫
  • 針對自定義型別,也一樣,它會呼叫它的賦值運運算元過載完成拷貝。

拷貝構造與賦值運運算元過載的區別:

賦值運運算元與拷貝構造都是一種拷貝行為。但是拷貝構造是拿一個已經存在的物件去構造初始化另一個要建立的物件,而賦值運運算元過載是兩個物件已經都存在了,都被初始化過了,現在想把一個物件,複製拷貝給另一個物件。

const成員函數

const修飾類的成員函數

將const修飾的類成員函數稱之為cosnt成員函數,const修飾成員函數,實際修飾該成員函數隱含的this指標,表明在該成員函數中不能對類的任何成員進行修改。

下面我們來看一段程式碼

class Date
{
public:
	Date(int year = 2020, int month = 5, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//bool operator==(Date* this,const Date& d);
	//這裡左運算元是this指向的呼叫函數的物件
	bool operator==(const Date& d)
	{
		return (_year == d._year)
			&& (_month = d._month)
			&& (_day == d._day);
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 1, 1);
	Date d2(2021, 5, 2);

	//內建型別,語言層面就支援運運算元
	//自定義型別,預設不支援。C++可以用運運算元過載來讓類物件支援用某個運運算元
	cout << (d1 == d2) << endl; //這裡<<優先順序高於==,因此需要加一個括號
	d1.Print();
	d2.Print();

	return 0;
}

在這裡插入圖片描述

看到列印結果我們會感到很奇怪,明明這裡我們只是通過運運算元過載比較了一下d1與d2是否相等,怎麼列印出來之後d1的月還改變了呢?

細心的你肯定發現了問題所在,成員函數bool operator==(const Date& d)的實現有問題,平時寫程式碼的時候也可能會煩這種將==寫成=的情況。我們都知道編譯器能查出來的問題都不是問題,查不出來的問題才叫做大問題這裡編譯器是不會報錯的,但是會影響結果和this指向的物件。

那我們如何解決這個問題呢?

大家可能會想在this指標前面加const不就可以了嘛,但是我們再回過頭來想想這個this指標是隱含的,我們不能顯示的寫在形參裡面。那麼我們該如何做呢?

為了防止this指標指向的物件被修改,C++類中可以在函數的後面加上const,表示this指標指向的物件不可被修改。

好處:函數中不小心改變的成員變數,編譯時就會被檢查出來。

建議:成員函數中,不需要改變成員變數,建議都加上const。

bool operator==(const Date& d)const

下面我們再來看幾個問題吧

  • const物件可以呼叫非const成員函數嗎?
  • 非const物件可以呼叫const成員函數嗎?
  • const成員函數內可以呼叫其它的非const成員函數嗎?
  • 非const成員函數內可以呼叫其它的const成員函數嗎?

答案:第二個和第四個是可以的,第一個和第三個是不可以的。

這個還是我們與我們前面所學的知識有關——許可權可以不變與縮小,但是許可權不能放大

取地址及const取地址操作符過載(瞭解即可)

這兩個預設成員函數一般不用重新定義 ,編譯器預設會生成。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 瞭解一下
	// 他們基本沒有被自己實現的價值
	// 除非你不想讓別人獲取Date類物件的地址,才有必要自己實現
	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

private:
	int _year;
	int _month;
	int _day;
};

這兩個運運算元一般不需要過載,使用編譯器生成的預設取地址的過載即可,只有特殊情況,才需要過載,比如不想讓別人獲取到指定的內容!

日期類的實現

接下來我們通過實現一下日期類來鞏固我們上面所學的知識吧。有興趣的小夥伴可以下去自己實現一下

Date.h

#pragma once
#include<iostream>
#include<assert.h>

using std::cout;
using std::cin;
using std::endl;

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 0);
	void Print();
	//解構、拷貝構造、賦值過載,可以不寫,預設生成的就夠用了
	//像Stack這樣的類才需要自己寫這三個

	// 拷貝建構函式
	//拷貝建構函式的引數只有一個且必須使用參照傳參,切記使用傳值傳參,使用傳值方式會引發無窮遞迴呼叫。
	// d2(d1)
	Date(const Date& d);

	// 賦值運運算元過載
	// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d);

	
	//d += 100
	Date& operator+=(int day);

	//d + 100
	Date operator+(int day);

	//d -= 100
	Date& operator-=(int day);

	//d - 100
	Date operator-(int day);


	//++d -> d.operator++();
	Date& operator++();

	//d++ -> d.operator++(int);
	//int引數不需要給實參,因為沒用,它的作用是為了跟前置++構成函數過載
	Date operator++(int);

	//--d -> d.operator--();
	Date& operator--();

	//d-- -> d.operator--(int);
	//int引數不需要給實參,因為沒用,它的作用是為了跟前置--構成函數過載
	Date operator--(int);

	// >運運算元過載
	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);

	// 日期-日期 返回天數
	int operator-(const Date& d);

private:
	int _year;
	int _month;
	int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

//算出這一年這一月的天數
int GetMonthday(int year,int month)
{
	// 陣列儲存平年每個月的天數
	int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = dayArray[month];

	//如果是閏年2月就得是29天
	if (month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
	{
		day = 29;
	}
	return day;
}

//預設引數不能在宣告和定義裡面同時出現
Date::Date(int year, int month, int day)
{
	//檢查日期的合法性
	if (year >= 0 && month > 0 && month <= 12 && day > 0 && day <= GetMonthday(year,month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期不合法" << endl;
		cout << year << "年" << month << "月" << day << "日" << endl;
	}
}
// 拷貝建構函式
// d2(d1)
Date::Date(const Date& d)
{
	this->_year = d._year;
	_month = d._month;
	_day = d._day;
}

// 賦值運運算元過載
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)  // void operator=(Date* this,const Date& d)
{
	if (this != &d)//檢查如果不是自己給自己複製,才需要拷貝
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

void Date::Print()
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

//d += 100
Date& Date:: operator+=(int day)
{
	// day是負數,怎麼處理?
	//複用-=
	if (day<0)
	{
		*this -= -day;
	}
	else
	{
		//先將原來日期的天數加上我們要加的天數
		_day += day;

		while (_day > GetMonthday(_year, _month))
		{
			_day -= GetMonthday(_year, _month);
			//如果當前天數已經大於該月的最大天數,月進位
			_month++;
			//如果當前月數已經大於12,年進位,並將月置為1
			if (_month > 12)
			{
				_year++;
				_month = 1;
			}
		}
	}
	return *this;
}

d + 100
//Date Date::operator+(int day)
//{
//	Date temp = *this;
//	temp._day += day;
//	while (temp._day > GetMonthday(temp._year, temp._month))
//	{
//		temp._day -= GetMonthday(temp._year, temp._month);
//		temp._month++;
//		while (temp._month > 12)
//		{
//			temp._year++;
//			temp._month = 1;
//		}
//	}
//	return temp;
//}

//d + 100
// 複用
Date Date::operator+(int day)
{
	Date ret(*this);
	// 複用operator+=
	ret += day;

	return ret;
}

//d -= 100
Date& Date::operator-=(int day)
{
	// day是負數,怎麼處理?
	//複用+=
	if (day < 0)
	{
		*this += -day;
	}
	else
	{
		//先用當前日期的天數減去我們要減的天數
		_day -= day;
		while (_day <= 0)
		{
			//如果當前天數小於0,我們得從上一月借位
			--_month;
			//如果當前月數等於0,則向年借位,並且將月置為12
			if (_month == 0)
			{
				_year--;
				_month = 12;
			}
			_day += GetMonthday(_year, _month);
		}
	}
	return *this;
}


d - 100
//Date Date:: operator-(int day)
//{
//	Date temp(*this);
//	//先用當前日期的天數減去我們要減的天數
//	temp._day -= day;
//	while (temp._day <= 0)
//	{
//		//如果當前天數小於0,我們得從上一月借位
//		--temp._month;
//		//如果當前月數等於0,則向年借位,並且將月置為12
//		if (temp._month == 0)
//		{
//			temp._year--;
//			temp._month = 12;
//		}
//		temp._day += GetMonthday(temp._year,temp._month);
//	}
//	return temp;
//}

//d1 - 100
//複用
Date Date:: operator-(int day)
{
	//調拷貝構造
	Date temp(*this);
	//複用Date& Date::operator-=(int day)
	temp -= day;
	return temp;
}


// ++d -> d.operator++(&d)
Date& Date:: operator++()
{
	*this += 1;
	return *this;
}

// d++ -> d.operator++(&d,0)
//int引數不需要給實參,因為沒用,它的作用是為了跟前置++構成函數過載
Date Date:: operator++(int)
{
	Date temp(*this);
	temp += 1;
	return temp;
}

// --d -> d.operator--(&d)
Date& Date:: operator--()
{
	*this -= 1;
	return *this;
}

//d-- -> d.operator(&d,0)
//int引數不需要給實參,因為沒用,它的作用是為了跟前置--構成函數過載
Date Date:: operator--(int)
{
	Date temp(*this);
	temp -= 1;
	return temp;
}


// >運運算元過載
//d1 > d2->d1.operator>(&d1, d2)
bool Date:: operator>(const Date& d)
{
	if (_year > d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month > d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			if (_day >= d._day)
			{
				return true;
			}
		}
	}
	return false;
}

// ==運運算元過載
//d1 == d2->d1.operator==(&d1, d2)
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);
}

// <運運算元過載
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);
}

// 日期-日期 返回天數
int Date:: operator-(const Date& d)
{
	// 效率差別不大的情況下,儘量選擇寫可讀性強的,簡單的程式
	Date Max = *this;
	Date Min = d;
	int flag = 1;

	//複用<
	if (*this < d)
	{
		Max = d;
		Min = *this;
		flag = -1;
	}
	//記錄它倆相差的天數
	int day = 0;
	while (Min != Max)
	{
		++Min;
		++day;
	}
	return day * flag;
}