对象与对象数组
实验介绍
本章节主要介绍对象数组和对象成员。在实际的开发中,对象数组和对象成员是经常使用的,所以首先需要学习对象数组与对象成员的各种使用方法。
提示:为了方便课程讲解,示例代码使用类内定义的方式实现,如果自己动手做实验的时候希望能够使用分文件类外定义的方式来编写代码。
知识点
- 对象数组
- 实例化对象数组
- 堆上操作对象数组
- 对象成员
- 构造和析构顺序
- 初始化对象成员
对象数组
假设定义了一个学生类,现在要实例化一个班的学生,如果逐个对学生进行实例化操作那肯定是非常麻烦的,这时使用对象数组就能很方便的完成编写。假设有一个点类,如果实例化一个矩形也可以使用对象数组的方式。
点类 - 示例代码 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
没有默认构造参数时需要使用初始化列表。 - 除了以上两种情况,可以不使用初始化列表,但是推荐使用初始化列表。
- 对象数据成员和对象成员先于对象初始化。
- 在实例化对象时需要清楚初始化数据成员的顺序。