这一次我准备用一个实际的例子来更加深入的探讨const关键字,可能这个例子不是特别的符合要求。
这个例子的需求是这样的:
我们需要一个画折线的对象,这个对象可以添加新的点,也可以删除的点,为了方便实践,我们规定这个折线最多由20个点组成,并且可以输出当前点的个数和所有点的信息。
首先我们来分析一下需求:
- 创建这样的一个简单对象,我们需要一个Point类和Line类,而且他们的关系属于has-a,所以应该用组合的方式实现。
- Point类中需要存储点的坐标信息,并且可以修改和获取这些信息。
- Line类中需要存储点的信息,并且可以修改和获取这些信息,以及获取总共的点的数量,删除的点的功能。
首先看看Point.h
class Point
{
private:
int x;
int y;
bool isInit;
public:
Point();
Point(const int& _x,const int& _y);
void updateXY(const int& _x,const int& _y);
int getX() const {return x;}
int getY() const {return y;}
bool getIsInit() const {return isInit;}
void display() const;
};
- x,y表示点的坐标,isInit表示这个点是否被初始化。
- 有参构造函数中和updateXY方法中,参数我们使用的const reference to int的类型,这样做的原因是因为我们只需要传递这个值进入这个方法。
- 而在get以及display方法中,我们只需要传递某一个值出去,并不需要函数去修改某些变量,所以我们将方法标记为const。
接下来我们在看看Point.cpp
Point::Point():x(0),y(0),isInit(false)
{
}
Point::Point(const int& _x,const int& _y):x(_x),y(_y),isInit(true)
{
}
void Point::updateXY(const int &_x, const int &_y)
{
x = _x;
y = _y;
}
void Point::display() const
{
printf("(%d,%d)\n",x,y);
}
实现很简单,但是这里我需要谈谈另外一个话题,关于初始化的问题。
我相信很多初学者都是这样实现第一个无参构造函数的:
Point::Point()
{
x = 0;
y = 0;
isInit = false;
}
这样的做法叫做赋值,而非初始化,C++有一条这样的规定,在成员变量的初始化动作发生在进入构造函数本体之前。换句话说,你应该使用参数列表去初始化所有的成员变量,就像示例代码中所实现的一样。
这样做符合C++规定并在效率也会更高。
接下来回归正题,我们来细谈Line类的实现,我们先谈谈成员变量:
class Line
{
private:
Point pointArray[20];
int count;
bool countIsValid;
}
- 我们需要一个长度为20的Point的数组,这个没什么好多说的。
- count变量用来表示当前Line中所存储的点的数量,countIsValid用来表示点的数量是否发生的变化
接下来我们来看看怎么实现所有需要的函数,首先是构造函数:
Line::Line():count(0),countIsValid(true)
{
}
构造函数的实现方法我上面说过了,还是请记住构造函数,并不是赋值函数。
接下来是添加点的函数实现:
void Line::addPoint(const Point &_new)
{
for(int i = 0 ; i < 20 ; i++)
{
if(!pointArray[i].getIsInit())
{
pointArray[i] = _new;
break;
}
}
countIsValid = false;
}
参数我们只是需要值就行,而不是需要对象,所以我们使用const reference类型进行传递就行了,函数中我们遍历到我们第一个没有使用的空间时,便使用这一空间存储当前传入的数据。
这段实现很不合实际,但是我只想想为后面最关键的部分做铺垫而已,大家就不要吐槽了。
void Line::deletePointByIndex(const int &_index)
{
for(int i = _index;i<=20;i++)
{
if(pointArray[i+1].getIsInit())
{
pointArray[i] = pointArray[i+1];
pointArray[i+1] = Point();
}
else
{
break;
}
}
}
根据下标删除一个点,然后将后面的点前向靠拢的一个操作。没有什么特别的地方。
接下来就这一次的重点,重载[]:
const Point& Line::operator[] (const int& _index) const
{
if(_index >= 20)
{
throw "Error";
}
if(!pointArray[_index].getIsInit())
{
throw "Error";
}
return pointArray[_index];
}
第一个if语句用来表示下标超出的范围,然后抛出一个错误,第二个下标用来检测当前点是不是有有效数据,如果没有就会抛出一个错误,错误处理实现的很简单,因为这不是重点。
这里我们返回的是一个const Point&类型的值并且函数也为const类型,因为我们不希望函数修改任何值,只是用来返回一个值。
然后我们发现返回的这个值并不能被操作,这个不是我们想要的,于是我们需要再次重载一个返回Point&类型的函数,但是如果我们的类特别复杂,前面的检查方法十分的复杂,我们再这样写一遍就特别的麻烦,对了我们有粘贴复制,但整体代码会显得很长,或与又有人说,可以把检查方法写一成一个函数,但是并有这样的必要,因为这样的函数并非广泛使用,下面我就来说一种特别的方法:
Point& Line::operator[] (const int& _index)
{
return const_cast<Point&>(static_cast<const Line&>(*this)[_index]);
}
首先我们将本对象转化为一个const Line&的对象,因为我们只想使用值并不想修改static_cast<const Line&>(this)帮助我们完成了这样的想法,然后我们使用了static_cast<const Line&>(this)[_index]返回的const Point&类型的值,但是我们需要去掉const关键字,这里使用了
const_cast<Point&>()这个方法,它可以帮我们去掉const关键字。
这样我们就可以做这样的事情了:
Line l1;
l1.addPoint(Point(3,3));
l1[0].display();
接下来我们来说说怎么实现返回当前点的总和,上面那段烂代码也是为了这段代码出现的必要,因为它要告诉大家一个重要的关键字。
int Line::getPointCount() const
{
if(!countIsValid)
{
count = 0;
for(int i = 0 ; i < 20 ; i++)
{
if(pointArray[i].getIsInit())
{
count++;
}
}
countIsValid = true;
}
return count;
}
作为一个获取某个值的函数,它同样被设置成为了const类型,但是我们在这个函数中改变了count和countIsValid的量,为了完成这个方法,我们需要修改一个成员变量的类型:
mutable int count;
mutable bool countIsValid;
mutable这个关键字,允许这个变量在任何地方都可以被修改,即使它在const函数内。这样大家都懂了吧。
好了其他的函数都不重要了,代码怎么实现的也不重要,关键是他们使用的方法,如果你已经完全掌握了上面的方法,现在你可以自己写一个String类。const能帮助你完成安全性的工作。
下一次我们会分享一些构造/析构/赋值运算相关的内容。