對象與對象數組
實驗介紹
本章節主要介紹對象數組和對象成員。在實際的開發中,對象數組和對象成員是經常使用的,所以首先需要學習對象數組與對象成員的各種使用方法。
提示:為了方便課程講解,示例代碼使用類內定義的方式實現,如果自己動手做實驗的時候希望能夠使用分文件類外定義的方式來編寫代碼。
知識點
- 對象數組
- 實例化對象數組
- 堆上操作對象數組
- 對象成員
- 構造和析構順序
- 初始化對象成員
對象數組
假設定義了一個學生類,現在要實例化一個班的學生,如果逐個對學生進行實例化操作那肯定是非常麻煩的,這時使用對象數組就能很方便的完成編寫。假設有一個點類,如果實例化一個矩形也可以使用對象數組的方式。
點類 - 示例代碼 1
定義一個點類,在本小節以后的示例代碼中都是用該類,在以下的示例代碼中盡量使用之前學到過的知識點。
為了方便查看運行結果,分別在構造函數、拷貝構造函數和析構函數中打印函數的名稱。
#include <iostream>
using namespace std;class Point
{
public:// 使用帶參數默認構造函數,并使用初始化列表初始化 x,yPoint(double x = 0, double y = 0) : x(x), y(y) {//cout << "Point(double x = 0, double y = 0)" << endl;cout << "Point(double x = " << x << ", double y = " << y << ")" << endl;}// 拷貝構造函數Point(const Point & p) {//cout << "Point(const Point &p)" << endl;// 打印點的值cout << "Point(const Point &p:(" << p.x << ", " << p.y << ")" << endl;this->x = p.x;this->y = p.y;}// 析構函數,由于沒有申請內存,析構函數中不需要做什么~Point() {//cout << "~Point()" << endl;cout << "~Point():(" << x << ", " << y << ")" << endl;}// x, y 綁定的成成員函數void setPoint(const Point &p) {this->x = p.x;this->y = p.y;}void setPoint(double x, double y) {this->x = x;this->y = y;}void setX(double x) { this->x = x; }void setY(double y) { this->y = y; }double getX() { return x; }double getY() { return y; }
private:double x;double y;
};
棧上實例化
示例代碼 2
為了效果演示,示例代碼將對象數組定義在一個函數中,可以在函數執行完之后調用對象數組的析構函數。
// 棧上實例化
void stackInstantiation()
{// 實例化對象數組Point point[3];// 對象數組操作cout << "p[0]: (" << point[0].getX() << ", " << point[0].getY() << ")" << endl;cout << "p[1]: (" << point[1].getX() << ", " << point[1].getY() << ")" << endl;cout << "p[2]: (" << point[2].getX() << ", " << point[2].getY() << ")" << endl;point[0].setPoint(3, 4);cout << "p[0]: (" << point[0].getX() << ", " << point[0].getY() << ")" << endl;
}int main()
{stackInstantiation();return 0;
}
運行結果:
棧上實例化對象數組小結:
- 實例化對象數組時,每一個對象的構造函數都會被執行。
- 系統自動銷毀棧上對象數組,并且銷毀對象數組時,每一個對象析構函數都會被執行。
- 訪問對象數組時使用 [ i ] 的方式訪問相應位置的對象。
- 建議將類數據成員都初始化,可以使用默認值初始化。
void setPoint(const Point &p)
; // 如果是自定義類作為參數時,建議使用引用的方式傳入參數,如果該參數在函數中無需修改且沒有輸出,建議加上const
。
示例代碼 3
void stackInstantiation()
{Point point[3];Point *p = point;cout << "p: (" << p->getX() << ", " << p->getY() << ")" << endl;p++;cout << "p: (" << p->getX() << ", " << p->getY() << ")" << endl;p++;cout << "p: (" << p->getX() << ", " << p->getY() << ")" << endl;point[2].setPoint(3, 4);cout << "p: (" << p->getX() << ", " << p->getY() << ")" << endl;
}int main()
{stackInstantiation();return 0;
}
運行結果:試驗中聲明的是對象數組,但是數組其本身也是可以當做指針使用。
堆上實例化
在堆上操作對象數據會比在棧上操作對象數組復雜,但卻比棧上操作更加的靈活,如果數據量比較大建議在堆上操作。
示例代碼 4
int main()
{// 堆上實例化對象數組Point *point = new Point[3];cout << "p[0]: (" << point[0].getX() << ", " << point[0].getY() << ")" << endl;cout << "p[1]: (" << point[1].getX() << ", " << point[1].getY() << ")" << endl;cout << "p[2]: (" << point[2].getX() << ", " << point[2].getY() << ")" << endl;point[0].setPoint(3, 4);cout << "p[0]: (" << point[0].getX() << ", " << point[0].getY() << ")" << endl;// 釋放內存delete [] point;point = nullptr;return 0;
}
運行結果:按照示例代碼 3 中的訪問方式與棧上訪問方式是一樣的,跟棧上訪問的結果也是一樣的。但是別急,后面還有堆上特有的操作。
示例代碼 5
在堆上操作對象數據會比在棧上操作對象數組復雜,但卻比棧上操作更加的靈活,如果數據量會比較大建議在堆上操作。
// 堆上實例化
int main()
{// 實例化對象Point *p = new Point[3];Point *point = p;cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;p++;cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;p++;cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;point->setPoint(3, 4);cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;cout << "p: (" << p->getX() << ", " << p->getY() << ")" << endl;// 釋放內存delete [] point;point = nullptr;return 0;
}
運行結果:發現使用指針的方式一樣可以訪問對象數組,但是使用時也要注意幾個問題。
- 使用
->
的方式來訪問類成員函數,并且不需要使用下標。 Point *point = p;
可以發現我又重新聲明一個指針,因為一個指針只能指向一個對象,通過指針++
或者--
運算符的方式來訪問對象數組中對象。
示例代碼 6
強調堆上申請空間與釋放空間的問題,請注意一下代碼與之前的異同之處,在銷毀對象數組時使用的是
delete point;
而在之前的示例代碼中使用的是delete [] point;
來銷毀對象數組的。
// 堆上實例化
int main()
{// 實例化對象Point *point = new Point[3];cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;point++;cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;point++;cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;point->setPoint(3, 4);cout << "point: (" << point->getX() << ", " << point->getY() << ")" << endl;// 指針使用完成后需要將指針指到起始地址point -= 2;// 釋放內存delete point;point = nullptr;return 0;
}
運行結果:
- 在
linux
環境直接運行報錯,但在Windows
環境下可以正確運行,這就造成了內存泄漏。
對象成員
對象成員即對象中包含其他的對象。
這里示例代碼將繼續使用示例代碼 1 中的點類。
示例代碼 7
首先看一下當對象 A
有對象 B
時調用構造函數與析構函數的順序。
class Line
{
public:Line(const Point & pA, const Point &pB) : pointA(pA), pointB(pB) {cout << "Line(const Point & pA, const Point &pB)" << endl;}Line(double aX, double aY, double bX, double bY) : pointA(aX, aY), pointB(bX, bY) {cout << "Line(double aX, double aY, double bX, double bY)" << endl;}~Line() {cout << "~Line()" << endl;}
private:Point pointA;Point pointB;
};int main()
{// 實例化Line *line = new Line(1, 2, 3, 5);// 釋放內存delete line;line = nullptr;return 0;
}
運行結果:可以看到先調用 pointA
的構造函數,再調用 pointB
的構造函數,最后調用 Line
的構造函數;而析構函數時正好反過來的。這也是為什么當對象成員沒有默認構造函數時必須要使用初始化列表的原因,因為對象成員先于對象初始化。
示例代碼 8
如果將對象成員類型作為參數輸入時看看其調用構造函數以及析構函數的順序。
int main()
{// 實例化Line *line = new Line(Point(1, 2), Point(3, 5));// 釋放內存delete line;line = nullptr;return 0;
}
運行結果:對象成員類型作為參數傳入時,傳入的參數時會臨時創建兩個對象,初始化完成后臨時對象自動銷毀。
示例代碼 9
int main()
{Line *p = new Line(1, 2, 3, 4);cout << "sizeof (p) = " << sizeof (p) << endl;cout << "sizeof (Line) = " << sizeof (Line) << endl;delete p;p = nullptr;return 0;
}
運行結果:p
指針占 8 字節,Line
類中有兩個 Point
類數據成員,Point
類有兩個 double
類型數據成員,所以 Line
一共占 32 個字節。
實驗總結
- 使用對象數組時會調用每個對象的構造函數和析構函數。
new
與delete
,new []
與delete []
一定要配套使用。- 不要越界,不管是棧還是堆,訪問數組時都不要越界。
- 對象數組指針變量本身就是一個指針。
- 堆上實例化的數組,要注意指針使用方法。
- 如果是做項目,要考慮使用在堆上實例化申請內存,棧空間比堆空間小很多。
- 當對象
A
中有常量時必須使用初始化列表。 - 當對象
A
有其他的對象B
并且對象B
沒有默認構造參數時需要使用初始化列表。 - 除了以上兩種情況,可以不使用初始化列表,但是推薦使用初始化列表。
- 對象數據成員和對象成員先于對象初始化。
- 在實例化對象時需要清楚初始化數據成員的順序。