代码改变世界

白话C++系列(11)-- 对象数组、对象成员

2016-04-24 21:59  Keiven_LY  阅读(6798)  评论(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点坐标,即符合信息打印。