深入探讨const关键字

这一次我准备用一个实际的例子来更加深入的探讨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(static_cast(*this)[_index]);
}

首先我们将本对象转化为一个const Line&的对象,因为我们只想使用值并不想修改static_cast(this)帮助我们完成了这样的想法,然后我们使用了static_cast(this)[_index]返回的const Point&类型的值,但是我们需要去掉const关键字,这里使用了
const_cast()这个方法,它可以帮我们去掉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能帮助你完成安全性的工作。

下一次我们会分享一些构造/析构/赋值运算相关的内容。

你可能感兴趣的:(深入探讨const关键字)