C++ 類和對象(三):構造函數補充、匿名對象、友元、內部類、類的static與const


構造函數補充

列表初始化

講列表初始化之前,要先討論一下構造函數裏面的語句到底是不是初始化

例子還是上次的日期類

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

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

這個構造函數內的賦值語句是初始化嗎?咋一看很可能會覺得是,但是如果這樣寫呢?

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

		_year = 2020;
	}

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

我往後面加上了一個_year = 2020,那這樣還是初始化嗎?總不可能是先用year初始化_year,再用2020來初始化它,這明顯不成立,因爲初始化只能一次,而函數體內的賦值可以多次,所以我們可以將函數體內的賦值理解爲賦初值,而非初始化

如果我們的成員是const,或者是引用,這種必須要初始化的類型,我們如果使用函數體內賦值,則是不可行的,而需要用到真正的初始化——列表初始化

這裏用了一個引用和一個const,如果採用函數體賦值的形式則會出錯,只有使用列表初始化才行

class Date
{
public:
	Date(int& year, const int month, int day)
		:_year(year),
		 _day(day),
		 _month(month)
		 
	{

	}

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

同時,這裏還有一個出錯的地方,可以看到,我這裏初始化列表的順序是year,day,month。但是初始化的順序和這個順序毫無關聯,初始化的順序是按照參數在類中聲明的順序的,也就是下面的year,month,day。

當類中包含以下成員的時候,必須要通過初始化列表初始化
  1. 引用成員變量
  2. const成員變量
  3. 自定義類型成員(該類沒有默認構造函數,只有帶參構造函數)

explicit關鍵字

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

	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2020, 4, 24);
	
	Date d2 = 2020;//C++98
	Date d3 = { 2020, 4 }; //C++11
	Date d4 = { 2020, 4, 24 }; //C+11
}

對於這裏的d2,我們用2020給它賦值,而d3和d4分別用了列表來給它賦值,並且這四個對象它們最後的值是一模一樣的,那是爲什麼呢?這裏的2020明明是一個整型,d3和d4是一個列表,爲什麼能夠給對象賦值呢?

這裏就牽扯到了隱式的類型轉換

這裏其實是先用這個整型值來調用了全缺省的構造函數來創建了一個臨時對象,再使用這個對象來爲d2,d3,d4賦值。

這是一種很容易引起誤會的寫法,所以c++提供了關鍵字explicit用這個關鍵字修飾的函數就會禁止隱式類型的轉化

在這裏插入圖片描述
這時這種隱式類型轉換就不會發生了


C++11 成員變量聲明缺省值

class Date
{
public:
	Date(int year)
	{
		_year = year;
	}
private:	
	int _year = 2020;
	int _month = 4;
	int _day = 24;
};

對於初學C++的人,在這裏很容易會將會將這裏的成員變量的操作當成初始化操作,但其實這裏的是在聲明階段給非靜態成員變量一個缺省值


還有一道我在csdn上看到的一個很有趣的題,給大家分享一下

以下代碼共調用多少次拷貝構造函數: ( )

Widget f(Widget u)
{  
  Widget v(u);
  Widget w=v;

  return w;
}

int main(){

  Widget x;

  Widget y=f(f(x));

}

這道題我第一眼看,在調用f的時候,會用實參來構造對象u,在用對象u構建v,然後用v構建w,最後因爲是傳值返回,會用w再構造一個臨時量,所以調用一次f會使用4次拷貝構造函數,最後再用這兩次f的返回值來構造y,應該是9次。

但是答案卻是7次,讓我十分不解,所以我去論壇搜索了一下,發現這裏涉及到了編譯器的優化。

我們可以看到,在第一次調用f的結束的時候,會返回一個由w構造的臨時量,在將這個臨時量作爲實參來初始化形參,編譯器覺得這一步有點多餘,會將其優化爲一步,所以每次調用f的時候其實只經過了3次的拷貝構造,最後再加上y的拷貝構造,一共是7次。


匿名對象

當我們想要調用對象中的一個方法,或者只是想在這一句使用這個對象,其他地方不再使用對象的時候,如果我們直接構造一個對象使用,這無疑是一種很大的浪費,因爲這個對象我們用了一次就扔了,不再需要了,而一個對象的生命週期是整個棧幀。

這時就需要用到匿名對象,匿名對象是一種臨時的對象,它的生命週期只有使用它的那一行語句,執行完則立即銷燬

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

	void Print(int year)
	{
		cout << "this year is " << year << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print(2020);
	//創建一個對象,生命週期爲整個函數棧幀

	Date().Print(2020);
	//創建一個匿名對象,生命週期只有這一行語句,實行完則立即調用析構函數
}

所以當我們只想在這一行使用對象時,就可以考慮使用匿名對象。


友元

友元函數

其實上一章我重載的運算符還存在一個問題,就是訪問權限的問題
Date類的所有成員變量都是private的,這裏是無法訪問的,只有將函數聲明爲友元函數,才能使用

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

友元函數可以訪問類中的所有成員,包括private,和protect,所以只需要將這個函數聲明爲友元函數即可。在這裏插入代碼片

友元函數需要在類內進行聲明,聲明時用friend關鍵字來修飾。

友元函數可訪問類的私有和保護成員,但不是類的成員函數
友元函數不能用const修飾
友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
一個函數可以是多個類的友元函數
友元函數的調用與普通函數的調用和原理相同

友元類

如果我們想在一個類中,訪問另一個類的私有成員,可以做到嗎?
答案是可以的,friend也可以用來修飾類,也就是友元類。

class A
{
	friend class Date;
	//將Date聲明爲友元類,Date可以訪問A的所有成員變量
private:
	string str;
};
class Date
{
public:
	

	void Print()
	{
		cout << a.str << endl;
		//訪問a的私有成員
	}
private:
	int _year;
	int _month;
	int _day;
	A a;
};

只需要在要被訪問的類中,使用friend class來聲明對應的類爲友元類,那個類就可以訪問了。

注意

1.友元關係是單向的,不具有交換性。
Date爲A的友元,可以訪問A的私有成員,但是A並不能訪問Date的

2.友元關係不能傳遞
如果B是A的友元,C是B的友元,則不能說明C時A的友元。


內部類

有沒有想過,既然類的成員變量可以是自定義類型,那能不能在類中再構建一個類呢?

class Date
{
public:
	

	void Print()
	{
		cout << _year << endl;
	}

private:
	class A
	{
	public:
		void Print()
		{
			cout << _data << endl;
		}

	private:
		int _data;
	};

	int _year;
	int _month;
	int _day;
	
};

可以看到,這是可以的,但是這兩個類有什麼關係嗎?

這個內部類其實是一個獨立的類,它不屬於外部類,同時外部類對它也沒有任何特權,但是它同時還是外部類的友元類,可以通過外部類的對象參數來訪問外部類的所有成員

特性:
1. 內部類可以定義在外部類的public、protected、private都是可以的。
2. 注意內部類可以直接訪問外部類中的static、枚舉成員,不需要外部類的對象/類名。
3. sizeof(外部類)=外部類,和內部類沒有任何關係


類的static成員

對於用static修飾的成員函數,稱爲靜態成員函數,成員變量稱爲靜態成員變量。如果對於一個成員,對其以static修飾,此時這個成員就不再屬於對象,而是屬於這一整個類的所有對象。因爲靜態的成員的生命域不在類中,在靜態區,所以靜態的成員只能在類外初始化。

  1. 靜態成員爲所有類對象所共享,不屬於某個具體的實例
  2. 靜態成員變量必須在類外定義,定義時不添加static關鍵字
  3. 類靜態成員即可用類名::靜態成員或者對象.靜態成員來訪問
  4. 靜態成員函數沒有隱藏的this指針,不能訪問任何非靜態成員
  5. 靜態成員和類的普通成員一樣,也有public、protected、private3種訪問別,也可以具有返回值
    6.靜態成員和全局變量雖然都存儲在靜態區,但是靜態成員的生命週期只在本文件中,而全局變量不是

因爲靜態成員函數不屬於某個對象,所以它沒有this指針,無法訪問任何非靜態的成員,但是非靜態的成員函數具有this指針,可以訪問靜態的成員。


類的const成員

因爲對於類和對象,封裝性是一個很重要的東西,但是訪問限定符只對外部有影響,對自身的成員函數沒有影響,如果我們不想讓一個成員函數對類的成員進行修改,這時,就需要用const來修飾成員函數。

class Date
{
public:
	//等價於 void print(const Date* this)
	void Print() const
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	
};

對於用const修飾的成員函數,需要將const放在最後面,來區分開const參數和const返回值。這裏的const其實修飾的是該成員函數的this指針,所以該成員函數就無法對類的成員進行修改。

  1. const對象可以調用非const成員函數嗎?
    答案:不行,因爲const對象的只能讀不能寫,而非const的成員函數則可讀可寫,使權限放大了,不行。

  2. 非const對象可以調用const成員函數嗎?
    答案:可以,因爲非const對象的可讀可寫,const成員函數只可讀,使權限縮小,可行。

  3. const成員函數內可以調用其它的非const成員函數嗎?
    答案:不行,因爲const成員函數的this指針是常量,只可讀,而非const的成員函數則可讀可寫,使權限放大了,不行。

  4. 非const成員函數內可以調用其它的const成員函數嗎?
    答案:可以,因爲非const成員函數的this指針可讀可寫,const成員函數只可讀,使權限縮小,可行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章