1. 程式人生 > >C++:類與物件(最終)

C++:類與物件(最終)

前兩篇關於類與物件的部落格,都是類與物件中不可或缺的物件,這篇就是在前兩篇的基礎上,再對類與物件進行補充。

一.簡識深淺拷貝

當我們進行拷貝造作函式,或者賦值運算子過載的時候,我們不給出這兩個函式,編譯器就會預設自動生成,預設對類進行位拷貝(按照基本型別進行值的拷貝)。

那麼編譯器給的到底有沒有問題呢?

看程式碼:

class Slist
{
public:
	Slist()
	{

	}
	Slist(int size,int cap)
	{
		int* arr = (int*)malloc(sizeof(int)*cap);
		_arr = arr;
		_size = size;
		_cap = cap;
	}
	void PopSlsit(Slist& p,int data)
	{
		p._arr[p._size] = data;
		p._size++;
	}
	void print()
	{
		for (int i = 0; i < _size; i++)
		{
			cout << _arr[i] << endl;
		}
	}
	~Slist()
	{
		if (_arr)
		{
			free(_arr);
			_arr = NULL;
		}
	}
private:
	int* _arr;
	int _size;
	int _cap;
};
int main()
{
	Slist s1(0, 10);
	Slist s2;
	s1.PopSlsit(s1, 1);
	s1.PopSlsit(s1, 2);
	s1.PopSlsit(s1, 3);
	s2 = s1;
	s2.print();
	system("pause");
	return 0;
}

上邊的程式碼能正確的打印出 1,2,3,但是最後就會觸發一個斷點,為什麼會觸發一個斷點?

 

通過除錯,我們看到當s1釋放之後,s2的_arr也被釋放,所以就是對同一個地址進行了兩次釋放,所以就會出錯。

在拷貝的時候只拷貝地址,而並未複製資源,我們就成之為淺拷貝;

如果一個類擁有資源,當這個類的物件發生複製過程的時候,資源也進行相應複製,這個 過程就可以叫做深拷貝。

如圖:

 

二.類的const成員

1.const修飾普通變數
在C++中,const修飾的變數已經為一個常量,具有巨集的屬性,即在編譯期間,編譯器會將const所修飾的常量進行替換。

int main()
{
	const int a = 10;
	a = 30;//錯誤 error C3892: “a”: 不能給常量賦值	
	system("pause");
	return 0;
}

2.const修飾類成員


(1)const修飾類成員變數時,該成員變數必須在建構函式的初始化列表中初始化


(2) const修飾類成員函式,實際修飾該成員函式隱含的this指標,該成員函式中不能對類的任何成員進行修改
 看下邊程式碼:

class Date
{
public:
	void print()
	{
		_year = 2018;
		_month = 11;
		_day = 1;
		cout << _year << " " << _month << " " << _day << " " << endl;
	}
	void print()const//相當於 void print (const Date* this)
	{
		_year = 2018;//出錯
		_month = 11;//出錯
		_day = 1;//出錯
		cout << _year << " " << _month << " " << _day << " " << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.print();

	const Date d2;
	d2.print();
	system("pause");
	return 0;
}

思考題:

1. const物件可以呼叫非const成員函式和const成員函式嗎?

2. 非const物件可以呼叫非const成員函式和const成員函式嗎?

3. const成員函式內可以呼叫其它的const成員函式和非const成員函式嗎?

4. 非const成員函式內可以呼叫其它的const成員函式和非const成員函式嗎? 


class Date
{
public:
	void print()
	{
		_year = 2018;
		_month = 11;
		_day = 1;
		cout << _year << " " << _month << " " << _day << " " << endl;
	}
	void print1()const
	{
		cout << _year << " " << _month << " " << _day << " " << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date d1;
	//d1.print();//不能通過編譯,error C2662: “void Date::print(void)”: 不能將“this”指標從“const Date”轉換為“Date &”	
        d1.print1();
	  
	Date d2;
	d2.print();
	d2.print1();
	system("pause");
	return 0;
}
class Date
{
public:
	void print()
	{
		cout << _year << " " << _month << " " << _day << " " << endl;
	}
	void print1()const
	{
		cout << _year << " " << _month << " " << _day << " " << endl;
	}
	void test()
	{
		print();
		print1();
	}
	void test()const
	{
		//print();//error C2662: “void Date::print(void)”: 不能將“this”指標從“const Date”轉換為“Date &”	
		print1();
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.test();
	const Date d2;
	d2.test();
	system("pause");
	return 0;
}

通過上述的兩個程式碼:

const物件只能呼叫const修飾的成員函式;

非const物件可以呼叫const修飾的成員函式,也可以呼叫非const修飾的成員函式。

在const成員函式內,只能呼叫const修飾的成員函式

在非const成員函式內,既可以呼叫const修飾的成員函式,也可以呼叫非const修飾的成員函式。

三.再談建構函式

前邊說過的建構函式,由於能給成員多次賦值,所以只能稱之為賦初值,而不是初始化(初始化只能有一次)。

1.初始化列表:在建構函式後邊加一個冒號,後邊在跟用逗號隔開的所有資料成員列表,並在每一個“成員變數”後面跟一個放在括號中的初始值或者表示式。

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

特性:


(1)每個成員在初始化列表中只能出現依次,也就是初始化只進行依次。

(2)初始化列表只能用於初始化資料成員,資料成員在類中的定義順序就是初始化列表中的初始化順序,否則就會出現混亂;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	const int _year;
	int _month;
	int _day;
};
class Date1
{
public:
	Date1(int year, int day, int month)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	const int _year;
	int _month;
	int _day;
};
int main()
{
	Date d(2018,11,2);
	Date1 d1(2018, 11, 2);
	system("pause");
	return 0;
}

 

                   

(3)儘量避免用成員初始化成員,成員的初始化順序最好和成員的定義順序一致;

(4)如果類中有以下三個成員,一定要在初始化列表中進行初始化:

           。引用成員變數

           。const成員變數

           。類型別的成員

2.建構函式的作用

(1)構造物件

(2)給物件中成員變數一個初始值

(3)型別轉化

 

對於單參建構函式,可以接受的引數轉化成類型別的物件

class Date
{
public:
	 Date(int year)
		:_year(year)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d(2018);
	int a = 2019;  //把整形變數給日期型別物件賦值
	d = a;       //實際編譯器做一個隱式的型別轉化,用2019構造一個無名函式,最後賦值給d
	system("pause");
	return 0;
}

用explicit修飾建構函式,可以抑制這種建構函式的隱式轉化。

class Date
{
public:
	explicit Date(int year)
		:_year(year)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d(2018);
	int a = 20;
	d = a;             //錯誤error C2679: 二進位制“=”: 沒有找到接受“int”型別的右運算元的運算子(或沒有可接受的轉換)

	system("pause");
	return 0;
}

3.預設建構函式

如果一個類未顯示定義建構函式,編譯器就會合成一個預設的建構函式;如果類顯示定義了,編譯器就不再合成。

看程式碼:

class Date
{
public:
	/*Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}*/
	void setdate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	d.setdate(2018, 11, 1);
	system("pause");
	return 0;
}

由於d在建立的時候,是沒有任何意義的,完全可以不用合成,合成就需要呼叫,呼叫這個建構函式又沒有實際的意義,所以編譯器就不會自動合成。

 

d物件建立時,並沒有相關的彙編程式碼。

所以在呼叫建構函式的時候,編譯器會根據自己需求來選擇合適建構函式。如果類中沒有顯示定義建構函式,如果需要編譯器會自己預設合成一個建構函式,但這個建構函式一定沒有引數。

如下面程式碼:

class Time
{
public:
	Time(int hour=11, int minute=59, int second=59)
		:_hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void setdate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	Date d;
	d.setdate(2018, 11, 1);
	system("pause");
	return 0;
}

 

因為在Date類建立當中有一個Time類,編譯器在呼叫Date類的建構函式完成對Date類物件的構造, Date類為顯示,所以編譯器必須合成:因為Date類中中存在Time類,對Date類構造的時候必須對類中的Time類進行構造,所以編譯器必須合成。

四.友元類

友元分成友元函式和友元類

1.友元函式

友元函式可以訪問一個類的私有成員,它是定義在類外部的普通函式,在需要時在類中進行宣告,在函式面前加friend關鍵字。

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date()
	{

	}
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
int main()
{
	Date d(2018, 11, 1);
	cout << d << endl;
	system("pause");
	return 0;
}

特性:

(1)友元函式可以訪問類中的私有成員,但不是類的成員;

(2)友元函式可以在類中任何一個地方進行宣告;

(3)友元函式不能有const修飾

(4)一個函式可以是多個類的友元函式

2.友元類

class Time
{
	friend class Date;//友元類的宣告
public:
	Time(int hour=0, int minute=0, int second=0)
		:_hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void TimeOfDay(int hour,int minute,int second)//直接訪問私有成員
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d(2018, 11, 1);
	d.TimeOfDay(11, 59, 59);
	system("pause");
	return 0;
}

特性:

(1)優點:友元類可以提高程式碼的效率;

在一個類中訪問另一個類的私有成員,原來需要通過呼叫函式的方法,要花費大量的時間可空間。

(2)缺點:友元類破壞了類的封裝性和隱藏性

類中私有的成員本來就是不能被別的類或者函式訪問,友元的加入就是的這一種特性失去。

(3)友元類是單向的;

在A類中,用friend加B類進行宣告,只能是B類中訪問A的私有成員,而A類不能訪問B類的私有成員。

(4)友元類沒有傳遞性。

在友元的加持下,A類可以訪問B類的私有成員,B類可以訪問C類的私有成員,但不代表A類就可以訪問C類的私有成員。

五.static成員

先來一個問題:如何知道一個類建立了多少個物件?以下兩種方式是否可行,為什麼?
(1) 在類中新增一個普通的成員變數進行計數

(2) 使用一個全域性變數來計數

1.宣告為static的類成員稱為類的靜態成員,用static修飾的成員變數,稱之為靜態成員變數;用static修飾的成員 函式,稱之為靜態成員函式。靜態的成員變數一定要在類外進行初始化
 

​class A
{
public:
	A()
	{
		_count++;
	}
	A(const A& d)
	{
		_count++;
	}
	~A()
	{
		--_count;
	}
	static int GetAcount()
	{
		return _count;
	}
private:
	static int _count;
};
void test()
{
	cout << A::GetAcount() << endl;        //0 
	A a1, a2, a3;
	A a4(a3);
	cout << A::GetAcount() << endl;        //4
}
int A::_count = 0;//在類的外部進行初始化
int main()
{
	test();
	system("pause");
	return 0;
}​

特性:

(1)靜態成員變必須在類的外部定義,定義是不加static關鍵字。

(2)靜態成員函式沒有this指標,不能訪問任何非靜態的成員。

(3)靜態成員會被所有的類共享,不屬於某個具體的例項。

(4)靜態成員和普通的成員一樣。

問題:

1. 靜態成員函式可以呼叫非靜態成員函式嗎?

class A
{
public:
	A()
	{
		_count++;
	}
	A(const A& d)
	{
		_count++;
	}
	~A()
	{
		--_count;
	}
	static int GetAcount()
	{
		return _count;//錯誤error C2597: 對非靜態成員“A::_count”的非法引用
	}
	int GetACount()
	{
		return _count;
	}
private:
	int _count;
};
void test()
{
	cout << A::GetAcount() << endl;
	A a1, a2, a3;
	A a4(a3);
	cout << A::GetAcount() << endl;
}
int main()
{
	test();
	system("pause");
	return 0;
}

答:靜態成員函式是不能呼叫非靜態成員函式的。

2. 非靜態成員函式可以呼叫類的靜態成員函式嗎? 

​class A
{
public:
	A()
	{
		_count++;
	}
	A(const A& d)
	{
		_count++;
	}
	~A()
	{
		--_count;
	}
	static int GetAcount()
	{
		return _count;
	}
	int GetACount()
	{
		return _count;
	}
private:
	static int _count;
};
void test()
{
	cout << A::GetAcount() << endl;
	A a1, a2, a3;
	A a4(a3);
	cout << A::GetAcount() << endl;
	//cout << A::GetACount() << endl;//錯誤error C2352: “A::GetACount”: 非靜態成員函式的非法呼叫	

}
int A::_count = 0;
int main()
{
	test();
	system("pause");
	return 0;
}​

答:非靜態成員函式不能呼叫類的靜態成員函式。

問題:在不用乘除法,for,while,if,else,switch,case,等關鍵字以及條件判斷語句,完成1+2+3+4+......+n。

我們可以先定義一個類,接著在建立n個這樣的型別,然後通過在建構函式函式中完成累加的工作。

class Test
{
public:
	Test()//建構函式
	{
		++n;
		sum += n;
	}
	static int getsum()
	{
		return sum;
	}
private:
	static int sum;
	static int n;
};
int Test::sum = 0;   //類外初始化靜態變數
int Test::n = 0;     //類外初始化靜態變數
int main()
{
	Test*ptr = new Test[10];//設 n=10
	cout << Test::getsum() << endl;
	delete[]ptr;
	ptr = NULL;
	system("pause");
	return 0;
}