1. 程式人生 > >C++ 面向物件- -類和物件的使用(三)

C++ 面向物件- -類和物件的使用(三)

目錄

物件的動態建立和釋放

物件的賦值和複製

1、物件的賦值

2、物件的複製

 

靜態成員

1、靜態資料成員

2、靜態成員函式

友元

1、友元函式

2、友元類

類模板


 

物件的動態建立和釋放

前面我們知道了 C++ 語言中可以用 new 運算子動態地分配記憶體,用 delete 運算子釋放這些記憶體空間。這兩個運算子也同樣適用於物件的動態建立和撤銷。

如果已經定義了一個 Time 類 ,可以用下面的方法動態的建立一個物件:

new Time ;

在執行這個語句時,系統開闢了一段記憶體空間,並在此記憶體空間中存放一個 Time 類物件,同時呼叫該類的建構函式,以使該物件初始化,但是此時使用者還無法訪問這個物件,因為這個物件既沒有物件名,使用者也不知道它的地址。這種物件稱為無名物件,它確實是存在的,但它沒有名字。

用 new 運算子動態地分配記憶體後,將返回一個指向新物件的指標,即所分配的記憶體空間的起始地址,使用者可以獲得這個地址,並通過這個地址來訪問這個物件,這樣就需要定義一個指向本類的物件的指標變來存放該地址,這樣就可以通過指標去訪問這個新建的物件。如:

Time *p ;
p = new Time ;
p->show();

另系統允許在執行 new 時,對新建的物件進行初始化。如:

Time *q = new Time ;
q->show();
Time *p_1 = new Time(1,1,1);
p_1->show();
Time *p =new Time(1,1,1);
p->show();

呼叫物件既可以通過物件名,也可以通過指標。用 new 建立的動態物件一般是不用物件名,而是通過指標訪問,一般主要應用於動態的資料結構,如連結串列。在執行 new 運算子時,如果記憶體量不足,則無法開闢所需的記憶體空間,此時大多系統都使 new 返回一個 0 指標值(NULL)。只要檢測返回值是否為 0 ,就可判斷分配記憶體是否成功,這在資料結構方面會經常用到。而在不需要使用 new 建立的物件時,可以用 delete 運算子予以釋放,如:

Time *p =new Time(1,1,1);
p->show();
delete p ;
//p->show();

這就撤銷了 p 指向的物件,此後程式不能再使用該物件。如果用一個指標變數 p 先後指向不同的動態物件,應注意指標變數的當前指向,以免刪除錯了物件。

 

物件的賦值和複製

1、物件的賦值

同類的物件之間可以相互復=賦值,這裡說的物件的值是指物件中所有的資料成員的值。物件之間的賦值也是通過賦值運算子 “=” 進行的,一般形式為:物件名1 = 物件名2 ; 注意需要是同一類中的物件。

Time t(1,1,1) , t1 ;
t1=t ;
t1.show(); 

物件的賦值只是對其中資料成員的賦值,而不對成員函式賦值。資料成員是佔儲存空間的,不同物件的資料成員佔有不同的儲存空間,賦值的過程是將一個物件的資料成員在儲存空間的狀態複製給另一物件的資料成員的儲存空間。而不同物件的成員函式是同一個函式程式碼段,不需要、也無法對它們賦值。

注意在物件的賦值過程中,類的資料成員中不能包括動態分配的資料,否則在賦值時可能出現嚴重後果(在此不作詳細分析,只需記住這一結論即可)

 

2、物件的複製

物件的複製機制:用一個已有的物件快速地複製出多個完全相同的物件。其一般形式是: 類名 物件2(物件1); 如:

Time t ;
Time t1(t) ;

其作用是用已有的物件 t 去克隆出一個新物件 t1 。這裡和賦值不一樣的是,在建立物件時呼叫了一個特殊的建構函式—複製建構函式,這個函式的形式如下:

Time :: Time(const Time &t){
	hour=t.hour;
	min=t.min;
	sec=t.sec;
} 

複製建構函式也是建構函式,但它只有一個引數,這個引數是本類的物件(不能是其他類的物件),而且採用物件的引用的形式(一般約定加 const 宣告。使引數值不能改變,以免在呼叫此函式時因不慎而使實參物件被修改)。此複製建構函式的作用就是將實參物件的各成員值一一賦給新的物件中對應的成員。

回顧複製物件的語句:Time t1(t);這實際上也是建立物件的語句,建立一個新物件 t1 ,由於在括號內給定的實參是物件,因此編譯系統就呼叫複製建構函式(它的形參是物件),而不會去呼叫其他建構函式,實參 t 的地址傳遞給形參 t (t 就是實參 t 的引用),在執行復制建構函式的函式體時,將物件 t 中各資料成員的值賦給 t1 中各資料成員。 

C++還提供另一種複製形式,用賦值號代替括號,其一般形式為: 類名 物件名1 = 物件名2 ;可以在一個語句中進行多個物件的複製,如:

Time t ;
Time t1=t , t2=t1 ;
t2.show();

物件的複製和賦值的區別:物件的賦值是對一個已經存在的物件賦值,因此必須先定義被賦值的物件,才能進行賦值;而物件的複製則是從無到有地建立一個新物件,並使它與一個已有的物件完全相同(包括物件的結構和成員的值)。

 

普通建構函式和複製建構函式的區別

  • 在形式上:

類名(形參表列); // 普通建構函式的宣告,如 Time (int h, int m ,int s);

類名(類名 &物件名);  //複製建構函式的宣告,如 Time(Time &t);

  • 在建立物件時,實參型別不同,系統會根據實參的型別決定呼叫普通建構函式或複製建構函式:

Time t (1,1,1);  // 實參為整數,呼叫普通建構函式

Time t1(t);  //實參為物件名,呼叫複製建構函式

  • 在什麼情況下被呼叫:

普通建構函式在程式中建立物件時被呼叫。

複製建構函式在用已有物件複製一個新物件時被呼叫,在以下三種情況下需要複製物件:

  • 程式中需要建立一個物件,並用另一個同類的物件對它初始化,像前邊的例子。
  • 當函式的引數為類的物件時。在呼叫函式時需要將實參物件完整地傳給形參,也就是需要建立一個實參的拷貝,按實參複製一個形參,系統通過呼叫複製建構函式來實現。
void set(Time t){
    cout << "hour=" << t.hour << endl;
}
int main(){
    Time t;
    set(t);
}
  • 函式的返回值是類的物件
Time set(){    //函式的返回型別是 Time 型別
	Time t_1(1,1,1);
	return t_1;    //返回值是 Time 類的一個物件
}
int main(){
	Time t;
	t=set();    //呼叫函式,返回Time的臨時物件,並將它賦值給 t 。
	t.show();
}

對於上邊函式的解釋: 由於 t_1 是在函式 set 中定義的,在呼叫 set 函式結束時, t_1 的生命週期就結束了,因並不是將 t_1 帶回 main 函式,而是在函式 set 結束前執行 return 語句時,呼叫 Time 類中的複製建構函式,將 t_1 複製一個新的物件,然後將它賦值給 t 。

 

靜態成員

前邊瞭解到全域性變數,它能夠實現資料共享。如果在一個程式檔案中有多個函式,在每一個函式中都可以改變全域性變數的值,全域性變數的值為各函式所共享。但是用全域性變數的安全性得不到保證,由於在各處都可以自由地修改全域性變數的值,很有可能偶一失誤,全域性變數的值就被修改,導致程式的失敗。因此在實際工作中很少會用到全域性變數。

如果想在同類的物件之間實現資料共享,也不要用全域性變數,可以用靜態的資料成員。

1、靜態資料成員

靜態資料成員是一種特殊的資料成員,它以關鍵字 static 開頭。為各物件所佔有,而不只屬於某個物件的成員,所有的物件都可以引用它,靜態的資料成員在記憶體中只佔一份空間(而不是每個物件都分別為它保留一份空間)。靜態資料成員的值對所有的物件都是一樣的,如果改變它的值,則在各物件中這個資料成員的值都同時改變了。

前邊已提到。如果只聲明瞭類而沒有定義物件,則類的一般資料成員是不佔用記憶體空間的,只有在定義物件才為物件的資料成員分配空間。但是靜態資料成員不屬於某一個物件,在為物件所分配的空間中不包括靜態資料成員所佔的空間。靜態資料成員是在所有物件之外單獨開闢空間,只要在類中指定了靜態資料成員,即使不定義物件,也為靜態資料成員分配空間,它可以被引用。它不隨物件的建立而分配空間,也不隨物件的撤銷而釋放(一般資料成員是在物件建立時分配空間,在物件撤銷時釋放),靜態資料成員是在程式編譯時分配空間,到程式結束時才釋放空間

靜態資料成員可以被初始化,但只能在類體外進行初始化,注意也不能用引數初始化表對其初始化,其一般形式為  資料型別  類名 :: 靜態資料成員 = 初值 ; 在類體中宣告靜態資料成員時加 static ,不必在初始化語句中加 static靜態資料成員可以通過物件名引用,也能通過類名引用。下面舉個例子:

class Time{
		int hour;
		int min;
	public:
		static int sec;
		//Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){}	// 初始化表不能對靜態資料成員初始化。 
		Time(int h=0,int m=0):hour(h),min(m){}
		void show(){
			cout<<"時間是: "<<hour<<":"<<min<<":"<<sec<<endl;
		}
};
int Time::sec = 1 ;
int main(){
	Time t , t1 ;
	t.show(); 
	cout<<t.sec<<endl;
	t1.sec=2;
	cout<<Time::sec<<endl;
}

需要注意的是,靜態資料成員的訪問屬性,根據自己後續程式中是如何引用來設定他的訪問屬性。

有了靜態資料成員後,各物件之間的資料有了溝通的渠道,實現資料共享,因此就不需要再用全域性變數,全域性變數破壞了封裝的原則,不符合面向物件程式的要求。

注意全域性靜態資料成員和全域性變數的不同,靜態資料成員的作用域只限於定義的該類的作用域內(如果是在一個函式中定義類,那麼其中靜態資料成員的作用域就是此函式內),在此作用域內,可以通過類名和域運算子 “::” 引用靜態資料成員,不論物件是否存在。

 

2、靜態成員函式

在類中宣告函式的前面加上 static 就成了靜態成員函式,和靜態資料成員一樣,靜態成員函式是類的一部分而不是物件的一部分,同樣可以通過物件名和類名去呼叫靜態成員函式。但與靜態資料成員不同的是,靜態成員函式的作用不是為了物件之間的溝通,而是為了去處理靜態資料成員(當然一般的成員函式也可引用和修改靜態資料成員)。靜態成員函式可以直接引用本類中的靜態成員,因為靜態成員同樣是屬於類的,故能直接飲用,但靜態成員函式不能訪問非靜態成員。(此處說的不能訪問是指不能預設訪問,因為無法知道去找哪個物件,如果一定要引用本類中的非靜態成員,應該加物件名和成員運算子 “” )。在前邊我們已提出,當呼叫一個物件的成員函式(非靜態成員函式)時,系統會把該物件的起始地址賦給成員函式的 this 指標 ,而靜態成員函式不屬於某個物件,它與任何物件都無關,因此靜態成員函式沒有 this 指標(這也是靜態成員函式和普通函式的區別),既然它不能指向某一物件,就無法對一個物件中的非靜態成員進行預設訪問(即在引用資料成員時不指定物件名)。

class Time{
		int hour;
		int min;
	public:
		static int sec;
		//Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){}	// 初始化表不能對靜態資料成員初始化。 
		Time(int h=0,int m=0):hour(h),min(m){}
		void show(){
			cout<<"時間是: "<<hour<<":"<<min<<":"<<sec<<endl;
		}
		static void set();
		static void set_1(Time t){
			cout<<t.hour<<endl;		//訪問非 static 資料成員 
		}
};
int Time::sec = 1 ;
void Time::set(){
	sec++ ;
//	cout<<hour<<endl;	找不到
//	cout<<Time::hour<<endl; 
	cout<<sec<<endl;
}
int main(){
	Time t  ;
	t.show(); 
	t.set();
	t.set_1(t);
}

下面是一個使用靜態成員函式統計學生的平均成績的例子,挺有代表性:

#include <iostream>
using namespace std;
class Student{
		int num ;
		int age ;
		float score ;
		static int count ;
		static float sum ;
	public:
		Student(int n,int a,float s):num(n),age(a),score(s){}
		void total(){
			sum += score ;
			count++;
		}
		static float average();
};
int Student::count=0;        //靜態資料成員的初始化
float Student::sum=0;
float Student::average(){
	return(sum/count);
}
int main(){
	Student std[3]={        //物件陣列
		Student(1,18,90),    //注意物件陣列元素是如何初始化的
		Student(2,18,80),
		Student(3,19,70)
	};                        //分號別忘了!!!
	for(int i=0;i<3;i++)		//注意陣列是從0開始的 
		std[i].total();
	cout<<"他們的平均分是: "<< Student::average()<<endl;   // 其實也不用非要靜態成員函式,用物件也可直接引用,只是看著不舒服。
} 

用靜態成員函式也只是比較方便一點,讀起來也比較容易理解,但並不是要非用不可。

 

友元

前邊在介紹類的訪問許可權時提到過友元(friend),friend 的意思就是朋友,朋友顯然要比一般人關係要親密一些。友元可以訪問與其有好友關係的類中的私有成員,這種關係以關鍵字 friend 宣告。友元包括友元函式和友元類。

1、友元函式

友元函式又分為兩類:友元普通函式和友元成員函式。一個函式(包括普通函式和成員函式)可以被多個類宣告為“朋友”,這樣就可以引用多個類中的私有資料。

  • 友元普通函式就是將普通函式宣告為友元函式,進而可以讓該函式去訪問類中的私有成員。
class Time{
		int hour;
		int min;
		int sec;
	public:
		Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){}
		void show(){
			cout<<"時間是: "<<hour<<":"<<min<<":"<<sec<<endl;
		}
		friend void display(Time ) ;
};
void display(Time t){
	cout<<t.hour<<":"<<t.min<<":"<<t.sec<<endl;
}
int main(){
	Time t  ;
	display(t);
}

需要注意的是,在用友元函式引用該類的私有資料成員時,要加上物件名(即要使物件作為形參),因為普通函式並不是該類的成員函式,沒有 this 指標,不能預設引用 類的資料成員,必須指定要訪問的物件,畢竟友元函式也只是能夠訪問私有成員。就比如有一個人是兩家人的鄰居,被兩家人都確認為好友,可以訪問兩家的各房間,但他在訪問時理所當然要指出他要訪問的是哪家。

  • 友元成員函式是將另一個類中的成員函式宣告為友元函式。

成員函式和普通函式宣告為友元函式時略微有所不同,成員函式需要指出它是屬於哪個類中的成員函式。

此處涉及到類的提前引用宣告,在一般情況下,物件必須先宣告然後才能使用它,但是在特殊情況下(正式宣告類之前)需要使用類名,此時就該作提前引用宣告。如下:

#include <iostream>
using namespace std;
class Time;
class Date{
		int year ;
		int month ;
		int day ;
	public:
		Date(int y,int m,int d):year(y),month(m),day(d){}
		void display(Time );
};
class Time{
		int hour;
		int min ;
		int sec;
	public:
		Time(int h, int m,int s):hour(h),min(m),sec(s){}
		void show(){
			cout<<"時間: "<< hour<<":" <<min <<":" <<sec<<endl;
		}
		friend void Date::display(Time );
    //此處定義這個函式,編譯是出錯的,想一下為什麼?
};

void Date::display(Time t){
			cout<<"日期: "<< year <<"/"<<month <<"/"<<day<<endl;
			t.show();
		}
int main(){
	Time t(10,47,20);
	Date d(2018,12,31);
	d.display(t);
}

類的提前宣告的使用範圍是有限的,只有在正式宣告一個類後才能用它去定義類物件(比如上方如果在第三行後邊加上 Time t 是出錯的),因為在定義物件時是需要為這些物件分配儲存空間的,在正式宣告類之前編譯系統不知道應為該物件分配多大的空間,只有 “見到” 類體後,才能確定具體的分配大小。仍用上邊那個例子再解釋一下:

class Date;
class Time{
		int hour;
		int min ;
		int sec;
	public:
		Time(int h, int m,int s):hour(h),min(m),sec(s){}
		void show(){
			cout<<"時間: "<< hour<<":" <<min <<":" <<sec<<endl;
		}
		friend void Date::display(Time );
};
class Date{
		int year ;
		int month ;
		int day ;
	public:
		Date(int y,int m,int d):year(y),month(m),day(d){}
		void display(Time );
};

這段程式碼僅僅將兩個類的定義換了一下位置,然後會發現編譯是出錯的,出錯的位置就是Time類中友元函式的位置,我們也已經在Time 類上邊對 Date 作了提前引用聲明瞭,那為什麼還是會出錯呢?這是因為在定義時,系統並不知道 Date 類中有什麼成員,我們想讓Date類中的成員函式指定為Time 類的友元函式,語法上是沒錯的,但在前邊僅僅是做了提前引用宣告,我們通過提前引用宣告也只能使用其類名。

在對一個類做提前引用聲明後,可以用該類的名字去定義一個指向該型別物件的指標變數或者物件的引用,這是因為指標變數(四個位元組大小)和引用(不需要分配空間)與它所指向的類物件大小無關。另外需要注意:友元成員函式必須在類內部宣告,在類外部定義。因為在內部定義成員函式,要用到其物件,此刻必須定義完整的類,但是類完整的定義在右 中括號出現後才是,故此刻會編譯出錯(這就是第一段程式碼註釋中的那個問題)。這是我所知道的知識,但至於正確不正確我沒有去考證。

 

2、友元類

如果一個類 B 宣告為 類 A 的友元類,那麼 B 類中的所有成員函式都是 A 類的友元函式,即可以訪問 A 類的所有成員。其宣告一般形式為:friend 類名 ; 需要注意的是:友元的關係是單向的而不是雙向的(B 是 A 的友元類 其內的成員函式可以訪問 A 中所有的成員,但不意味著 A 也是 B 的友元 , A 中的成員函式無權訪問 B 中的私有成員)。友元的關係也不能傳遞(如果 B 是 A 的友元類 , C 是 B 的友元類,不等於 C 是 A 的友元類)。 

class Time;
class Date{
		int year ;
		int month ;
		int day ;
	public:
		Date(int y,int m,int d):year(y),month(m),day(d){}
		void display(Time t);
		void show_hour(Time);
};
class Time{
		int hour;
		int min ;
		int sec;
	public:
		Time(int h, int m,int s):hour(h),min(m),sec(s){}
		void show(){
			cout<<"時間: "<< hour<<":" <<min <<":" <<sec<<endl;
		}
		friend Date ;
};
void Date::display(Time t){
			cout<<"日期: "<<year<<"/"<<month<<"/"<<day<<endl;
			t.show();
		}
void Date::show_hour(Time t){
	cout<<"hour="<<t.hour<<endl; 
}

 

類模板

前邊提到過函式模板,是對於功能一樣而僅資料型別不一樣的一些函式,可以定義一個任何型別變數都能進行操作的函式模板,在呼叫時系統根據實參的型別取代函式模板中的型別引數,得到具體的函式。同樣也有相同功能的類模板。宣告形式也是需要加一個關鍵字 template 表示模板的含義。

#include <iostream>
using namespace std;
template <class numtype> 			//注意無分號,和函式模板的宣告一樣。 
//宣告模板,template後面的尖括號內的內容為模板的引數表,關鍵字calss表示後面的是虛擬型別引數名numtype  
class Compare{			//類模板名為 Compare  
		numtype a;
		numtype b;
	public:
		Compare(numtype a1,numtype b1){			//建構函式 
			a=a1;
			b=b1;
		}
		numtype max(){
			return (a>b)?a:b ;
		}
		numtype min();
};

//注意這是在類外定義模板時的正確寫法,不能用一般定義類成員函式的形式。 
template <class numtype> 	//必須有 
numtype Compare <numtype> :: min(){		// Compare <numtype> 是一個整體,是帶參的類 
	if(a<b)
		return a;
	else
		return b;
}

int main(){
	Compare <int> c(1,3);	//和普通定義物件不一樣,這是正確表示 
	cout<<"c_max="<<c.max()<<endl;
	Compare <double> c1(1.67,3.89);
	cout<<"c1_min="<<c1.min()<<endl;
}

在上邊程式碼中,Compare是類模板名,而不是一個具體的類,Conpare <numtype> 是一個整體 ,numtype 也並不是一個實際的型別,只是虛擬的,無法用它去定義物件,必須用實際型別名去取代虛擬的型別,即在類模板名後在尖括號中指定實際的型別名,這樣系統在編譯時就會用實際型別名取代類模板中的型別引數numtype ,也就是例項化。另外類模板和函式模板一樣,它的型別引數同樣可以有一個或多個,每個型別引數前面都必須加上 class ,定義物件時也要分別代入實際的型別名。

template <class numtype_1,class numtype_2>
class Compare{
	
};
int main(){
	Compare <int,double> c1(1,1.69) ;
} 

最後多說一點,使用類模板時也要注意其作用域,只能在其有效作用域內用它定義物件。另外模板可以有層次,一個類模板可以作為基類,派生出派生模板類。這方面查閱相關資料可以瞭解到,我具體也不是很瞭解,很少會用到。