C++遠征之封裝篇(下)
對象數組
前面課程我們已經學會了如何實例化一個對象,只有實例化對象后,才能通過這個對象去訪問對象的數據成員和成員函數。但是在很多場合下,一個對象是遠遠不夠用的,往往需要一組對象。比如,我們想表示一個班級的學生,並且假設這個班級有50個學生。果我們還是像以前一樣,簡單的使用對象的實例化的話,就需要定義50個變量來表示這50個學生,顯然這樣做是很麻煩很愚蠢的。這時,我們就需要通過一個數組來表達這一個班的學生。還有,如果我們去定義一個坐標,那么這一個坐標只能代表一個點,但是,如果我們想去定義一個矩形的話,就需要定義4個點,然后這4個點的連線形成一個矩形,那么這4個點也可以定義成一個數組。說到這里,想必大家應該知道,今天的重點就是對象數組。
接下來我們看下面這個例子。
在這里我們定義了一個坐標類(Coordinate),並且定義了其兩個數據成員(一個表示橫坐標,一個表示縱坐標)。我們在使用的過程中,首先是在棧中實例化了一個對象數組,每個數組元素就是一個坐標的對象,並且均可以訪問對象的數據成員(如上,我們給對象數組的第2個元素的橫坐標賦值為10);其次我們又在堆上實例化了一個對象數組,同樣,每個數組元素均可以訪問對象的數據成員(如上,我們給對象數組的第1個元素的縱坐標賦值為20)。記住,在堆上實例化對象數組后,使用完畢,需要將申請的內存釋放掉(用delete []p),最后還要賦值為空(NULL)。
接下來看看在內存中是如何存儲的(如下)。
對象數組代碼實踐
題目描述:
定義一個坐標(Coordinate)類,其數據成員包含橫坐標和縱坐標,分別從棧和堆中實例化長度為3的對象數組,給數組中的元素分別賦值,最后遍歷兩個數組。
程序框架如下:
頭文件(Coordinate.h)
class Coordinate { public: Coordinate(); ~Coordinate (); public: int m_iX; int m_iY; };
源程序:
#include<iostream> #include<stdlib.h> #include"Coordinate.h" using namespace std; /* 對象數組 /* 要求 1. 定義Coordiante類 2. 數據成員:m_iX、m_iY 3. 分別從棧和堆中實例化長度為3的對象數組 4. 給數組中的元素分別賦值 5. 遍歷兩個數組 /* *****************************************/ Coordinate::Coordinate() { cout <<"Coordinate()"<<endl; } Coordinate::~Coordinate () { cout <<"~Coordinate()"<< endl; } int main() { Coordinate coor[3]; //從棧上實例化對象數組 coor[0].m_iX =3; coor[0].m_iY =5; Coordinate *p =new Coordinate[3]; p->m_iX = 7; //直接寫p的話,就說明是第一個元素 p[0].m_iY =9; //等價於 p->m_iY = 9 p++; //將指針后移一個位置,指向第2個元素 p->m_iX = 11; p[0].m_iY = 13; //這里p指向的是第二個元素,p[0]就是當前元素,等價於p->m_iY = 13 p[1].m_iX = 15;//第3個元素的橫坐標 p++; ////將指針后移一個位置,指向第3個元素 p[0].m_iY = 17;//這里p指向的是第三個元素,p[0]就是當前元素,等價於p->m_iY = 17 for(int i = 0; i < 3; i++) { cout <<"coor_X: "<< coor[i].m_iX <<endl; cout <<"coor_Y: "<< coor[i].m_iY <<endl; } for(int j = 0; j < 3; j++) { //如果上面p沒有經過++操作,就可以按下面來輪詢 //cout <<"p_X: " << p[i].m_iX <<endl; //cout <<"p_Y: " << p[i].m_iY <<endl; //但是,上面我們對p做了兩次++操作,實際p已經指向了第3個元素,應如下操作 cout <<"p_X: "<< p->m_iX <<endl; cout <<"p_Y: "<< p->m_iY <<endl; p--; } //經過了三次循環后,p指向了一個非法內存,不能直接就delete,而應該讓p再指向我們申請的一個元素的,如下 p++; //這樣p就指向了我們申請的內存 delete []p; p = NULL; system("pause"); return 0; }
運行結果:
從運行結果來看,首先看到的是打印出六行“Coordinatre()”,這是因為分別從棧實例化了長度為3的對象數組和從堆實例化了長度為3的對象數組,每實例化一個對象就要調用一次默認構造函數。
最后只打印出三行“~Coordinate()”,那是不是只是從堆上實例化的對象銷毀時調用了析構函數,而從棧實例化的對象銷毀時,沒有調用析構函數呢?
非也,從棧實例化的對象在銷毀時,系統自動回收內存,即自動調用析構函數,只是當我們按照提示“請按任意鍵結束”,按下任何鍵后,屏幕會閃一下,就在這閃的過程中,會出現三行“~Coordinate()”的字樣,只是我們不容易看到而已。
對象成員
前面我們講到的類都是比較簡單的,它們共同的特點是,其數據成員都是基本的數據類型,但是在現實的生活中,問題要遠比這個復雜的多。比如,之前我們編寫過汽車的類,但是當時我們只申明了汽車輪子的個數,如果要解決實際的問題,顯然這是不夠的。起碼輪子本身就是一個對象,汽車上還有沙發座椅,還有發動機等等。再比如,我們如果要定義一個房子的類,房子對象當中,應該有各種各樣的家俱,還有漂亮的燈飾等等,而這些家俱和燈飾其實也是一個個對象。可見,在對象當中包含着其他對象是一種非常常見的現象,接下來我們就學習一下對象成員。
為了說明對象成員,我們以坐標系中的一段線段為例,以此來說明對象成員的定義和使用方法。
上面是一個直角坐標系,在這個坐標系中,我們定義了一條線段AB,起點A的坐標為(2, 1),終點B的坐標為(6, 4)。如果我們要定義像這樣的一個線段的類,那么每條線段都有兩個點連接而成,這意味着我們需要定義一個表示點的類,這個類包含橫坐標和縱坐標,而且一個線段中應該包含兩個坐標的對象。可見,要描述這個問題,我們至少要定義兩個類,一個來定義坐標的點,一個來定義坐標系中的線段。
先來定義坐標點的類,如下:
在這個類中有兩個數據成員,分別表示點的橫坐標和縱坐標,另外還包含一個它的構造函數。
接着看一下線段類的定義,如下:
在這個線段的類中,有兩個數據成員,這兩個數據成員都是點(一個是起點,一個是終點),而且這兩個點必須是坐標類型的,另外,我們也定義了它的構造函數。
定義完點的類和線段的類后,我們就可以通過實例化來描述一條線段了,如下:
這里大家可能會有這樣一個疑問:在這種對象作為數據成員的情況下,當實例化線段(Line)時,到底是先實例化線段還是先實例化作為對象成員的坐標點的對象呢?而當我們去delete p的時候,也就是說,當線段被銷毀時,是先銷毀點對象還是先銷毀線段對象呢?
結論:
當我們實例化Line對象時,先實例化點A對象,再實例化點B對象,最后實例化Line這個對象。而銷毀時,則與創建時相反,先銷毀Line這個對象,然后銷毀點B這個對象,最后銷毀點A這個對象。
上面我們講的對象作為數據成員時,構造函數都是沒有參數的。然而作為一條線段,它的兩個點在實例化時,其實是應該可以由調用者來確定的,也就是說,這兩個坐標點在Line這個對象實例化的時候,是能夠通過給它的構造函數傳遞參數,從而可以使這兩點生成在確定的位置上。也就是說,坐標類的構造函數應該有參數,即如下所示:
從而,這就需要線段(Line)的類,它的構造函數也需要有參數,而這些參數未來可以傳值給它的數據成員,即如下所示:
如果我們在實例化線段時,僅僅如下所示肯定會是出錯的。
因此,我們需要將代碼做進一步改進,即配備初始化列表。在初始化列表中,我們要實例化m_coorA和m_coorB,並且將Line所傳入的這四個參數分配到這兩個對象成員中去。
當做完這些工作之后,我們就可以在主調函數中像之前那樣去實例化新的對象了,並且2和1必然會傳值給第一個坐標點對象,6和4必然會傳值給第二個坐標點對象。
對象成員代碼實踐
題目描述:
/* 對象成員
/* 具體要求:
定義兩個類:
坐標類:Coordinate
數據成員:橫坐標m_iX,縱坐標m_iY
成員函數:構造函數、析構函數,數據成員的封裝函數
線段類:Line
數據成員:點A m_coorA,點B m_coorB
成員函數:構造函數,析構函數,數據成員的封裝函數,信息打印函數
/* *****************************/
程序框架如下:
頭文件(Coordinate.h)
class Coordinate { public: Coordinate(); ~Coordinate(); void setX(int x); int getX(); void setY(int y); int getY(); private: int m_iX; int m_iY; };
源程序(Coordinate.cpp)
#include <iostream> #include "Coordinate.h" using namespace std; Coordinate::Coordinate () { cout <<"Coordinate()"<<endl; } Coordinate::~Coordinate () { cout <<"~Coordinate()"<<endl; } void Coordinate::setX(int x) { m_iX = x; } int Coordinate::getX() { return m_iX; } void Coordinate::setY(int y) { m_iY = y; } int Coordinate::getY() { return m_iY; }
頭文件(Line.h)
#include "Coordinate.h" class Line { public: Line(); ~Line(); void setA(int x, int y); void setB(int x, int y); void printInfo(); private: Coordinate m_coorA; Coordinate m_coorB; };
源程序(Line.cpp)
#include<iostream> #include "Line.h" #include "Coordinate.h" using namespace std; Line::Line() { cout <<"Line()"<< endl; } Line::~Line() { cout <<"~Line()"<< endl; } void Line::setA(int x, int y) { m_coorA.setX(x); m_coorA.setY(y); } void Line::setB(int x, int y) { m_coorB.setX(x); m_coorB.setY(y); } void Line::printInfo() { cout << "(" << m_coorA.getX() <<","<< m_coorA.getY()<< ")" <<endl; cout << "(" << m_coorB.getX() <<","<< m_coorB.getY()<< ")" <<endl; }
主調程序(demo.cpp)
//我們首先來實例化一個線段類的對象,如下 #include <iostream> #include Line.h" using namespace std; int main() { Line *p = new Line(); delete p; p = NULL; system("pause"); return 0; }
運行結果如下:
從運行結果來看,先連續調用了兩次坐標類的構造函數,再調用了一次線段類的構造函數,這就意味着先創建了兩個坐標類的對象,這兩個坐標類的對象就是A點和B點,然后才調用線段這個對象,線段這個對象是在A點和B點初始化完成之后才被創建。而在銷毀時,先調用的是線段類的西溝函數,然后連續調用兩次坐標類的析構函數。可見,對象成員的創建與銷毀的過程正好相反,也驗證了我們之前給出的結論。
作為一條線段來說,我們非常希望的是,在這條線段創建的時候就已經將線段的起點和終點確定下來。為了達到這個目的,我們往往希望線段這個類的構造函數是帶有參數的,並且這個參數將來能夠傳給這兩個點,所以接下來我們將進一步完善這個程序。
完善頭文件(Coordinate.h)
class Coordinate { public: Coordinate(int x,int y); ~Coordinate(); void setX(int x); int getX(); void setY(int y); int getY(); private: int m_iX; int m_iY; };
完善源程序(Coordinate.cpp)
#include<iostream> #include "Coordinate.h" using namespace std; Coordinate::Coordinate(int x, int y) { m_iX = x; m_iY = y; cout <<"Coordinate()"<< m_iX <<","<< m_iY <<endl; } Coordinate::~Coordinate () { cout <<"~Coordinate()"<< m_iX <<","<< m_iY <<endl; } void Coordinate::setX(int x) { m_iX = x; } int Coordinate::getX() { return m_iX; } void Coordinate::setY(int y) { m_iY = y; } int Coordinate::getY() { return m_iY; }
完善頭文件(Line.h)
#include"Coordinate.h" class Line { public: Line(int x1, int y1, int x2, int y2); ~Line(); void setA(int x, int y); void setB(int x, int y); void printInfo(); private: Coordinate m_coorA; Coordinate m_coorB; };
完善源程序(Line.cpp)
#include<iostream> #include"Line.h" using namespace std; Line::Line(int x1, int y1, int x2, int y2):m_coorA(x1, y1), m_coorB(x2, y2) { cout <<"Line()"<< endl; } Line::~Line() { cout <<"~Line()"<< endl; } void Line::setA(int x, int y) { m_coorA.setX(x); m_coorA.setY(y); } void Line::setB(int x, int y) { m_coorB.setX(x); m_coorB.setY(y); } void Line::printInfo() { cout <<"("<<m_coorA.getX() <<","<< m_coorA.getY()<<")"<<endl; cout <<"("<<m_coorB.getX() <<","<< m_coorB.getY()<<")"<<endl; }
完善主調程序(demo.cpp)
#include<iostream> #include "Line.h" using namespace std; int main() { Line *p = new Line(1,2,3,4); delete p; p = NULL; system("pause"); return 0; }
運行結果:
從這個結果來看,我們更能清晰的看到,在實例化對象時,先實例化點A,再實例化點B,最后實例化線段;在銷毀對象時,先銷毀線段,再銷毀點B,最后銷毀點A。
最后,我們來看一看,通過這樣的值傳遞,能否正確的打印出來,在主調函數中增加一行代碼來調用線段的信息打印函數,如下紅色標記:
#include<iostream> #include"Line.h" using namespace std; int main() { Line *p = new Line(1,2,3,4); p->printInfo(); delete p; p = NULL; system("pause"); return 0; }
運行結果:
在此結果中,我們看到了A點坐標和B點坐標,即符合信息打印。