白话C++系列(11)-- 对象数组、对象成员
2016-04-24 21:59 Keiven_LY 阅读(6803) 评论(0) 编辑 收藏 举报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点坐标,即符合信息打印。