65.9K
CodeProject 正在变化。 阅读更多。
Home

Const 的故事

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.73/5 (31投票s)

2003年5月17日

5分钟阅读

viewsIcon

115796

这是一篇关于 const 关键字的文章,介绍它的细节以及为什么你应该关心它。

引言

很长一段时间以来,我在我的代码中添加 const 关键字或阅读他人代码时,都需要停下来思考。许多程序员根本不关心 const 关键字,他们的理由是他们迄今为止没有它也过得很好。本文旨在强调改进软件和代码质量,希望能成为理解 const 故事的一个一站式地点。初学者可能会发现它是一个关于使用 const 不同方法的简单介绍,而有经验的程序员可以将这里的示例用作快速参考。

免责声明/警告

下面示例中提供的所有代码都已(如果测试过的话)非常随意地测试过,因此请在使用前仔细分析。尤其是指针运算可能会导致严重问题,在编写生产代码时应格外小心。

const 变量

当一个变量前面加上 const 关键字时,它的值就不能被改变。任何修改都会导致编译器错误。无论 const 关键字出现在数据类型之前还是之后,都没有区别。

这是一个例子。

int main(void)
{
    const int i = 10;  //both variables i and j are constants
    int const j = 20;
    i = 15;            //Error, cannot modify const object
    j = 25;            //Error, cannot modify const object
}

const 指针

const 关键字可以以两种方式与指针一起使用。

  1. const 可以用来指定指针是常量的,即指针指向的内存地址不能改变。然而,该内存地址的当前值可以随意更改。
    int main(void) 
    { 
        int i = 10;
        int *const j = &i;  //j is a constant pointer to int
        (*j)++;     //The int value pointed to by j can be modified.
                    // i.e. *j = i = 11;
        j++;        //ERROR: cannot modify const object j
    }
    
  2. const 也可以与指针一起使用,以指定内存地址的值不能改变,但指针可以被赋值给新的地址。
    int main(void)
    {
        int i = 20;
        const int *j = &i;  //j is a pointer to a constant int
        *j++;     //the pointer value of j can be incremented. 
                  //Note that the value at pointer location j 
                  //may be garbage or cause a GPF 
                  //but as far as the const keyword is concerned 
                  //the compiler doesnt care
        (*j)++;   //ERROR; cannot modify const object *j
    }
    

理解上面两个示例的一个好方法是:在第一种情况下,j 是一个常量(指向 int 的常量指针),而在第二种情况下,*j 是常量(指向 const int 的指针)。

这两种情况可以结合起来,使指针及其值都不能改变。

int main(void)
{
    int i = 10;
    const int *const j = &i; //j is a constant pointer to constant int
    j++;                //ERROR: cannot modify const object j
    (*j)++;          //ERROR: cannot modify const object *j    
}

const 和引用

引用只是实体的一个别名。以下是一些规则:

  1. 引用必须初始化。
  2. 一旦初始化,引用就不能再指向另一个实体。
  3. 对引用的任何更改都会导致对原始实体的更改,反之亦然。
  4. 引用和实体指向同一个内存位置。

这是一个示例来说明上述规则:

int main(void)
{ 
    int i = 10;     //i and j are int variables 
    int j = 20;    
    int &r = i;    //r is a reference to i 
    int &s;        //error, reference must be initialized 
    i = 15;         //i and r are now both equal to 15. 
    i++;         //i and r are now both equal to 16 
    r = 18;        //i and r are now both equal to 18 
    r = j;        //i and r are now both equal to 20, 
               //the value of j. However r does not refer to j.
    r++;        //i and r are now both equal to 21, 
                //but j is still 20 since r does not refer to j
}

const 与引用一起使用可以确保引用不会被修改。然而,对引用所指向的实体的任何更改都会反映在引用中。此外,const 关键字出现在数据类型说明符之前还是之后都没有区别。

这是一个例子。

int main(void)
{
    int i = 10;
    int j = 100;
    const int &r = i;
    int const &s = j;
    r = 20;          //error, cannot modify const object
    s = 50;          //error, cannot modify const object
    i = 15;          //both i and r are now 15
    j = 25;          //both j and s are now 25
}

const 和成员函数

const 关键字附加到成员函数声明时,this 指针指向的对象就不能被修改。这通常用于定义仅用于读取对象数据值的访问器方法。这很有用,因为使用你的代码的开发人员可以查看声明并确信你的访问器方法不会以任何方式修改对象。

这是一个例子。

class MyClass 
{ 
public:
    int i;    //member variable
    MyClass()
    {
        //void constructor    
        i = 10;
    }
    
    ~MyClass()
    {
        //destructor
    }

    int ValueOfI() const    //const method is an accessor method
    {
        i++;        //error, cannot modify object
        return i;    //return the value of i
    }
};

const 的重载

const 关键字也可以用于函数重载。考虑以下代码:

class MyClass
{
public:
    int i;    //member variable
    MyClass()
    {
        //void constructor    
        i = 10;
    }
    
    ~MyClass()
    {
        //destructor
    }

    int ValueOfI() const //const method is an accessor method
    {
        return i;        //return the value of i.    
    }
    
    int& ValueOfI()      //method is used to set the 
                         //value by returning a reference
    {
        return i;
    }
};

在上面的示例中,两个 ValueOfI() 方法实际上是重载的。const 关键字实际上是参数列表的一部分,它指定第一个声明中的 this 指针是常量,而第二个不是。这是一个人为的例子,旨在强调 const 可用于函数重载。

实际上,由于引用的美妙之处,实际上只需要第二个 ValueOfI() 定义。如果 ValueOfI() 方法用于 = 运算符的右侧,它将充当访问器;如果它用作左值(赋值运算符左侧的值),则返回的引用将被修改,该方法将用作设置器。这在重载运算符时经常这样做。

class MyClass 
{ 
public:
    CPoint MyPoint; 
    MyClass() 
    {
        //void    constructor 
        MyPoint.x = 0; 
        MyPoint.y = 0;
    }
    ~MyClass()
    { 
        //destructor
    }
    
    MyPoint& MyPointIs()    //method is used to set the value 
                            //by returning a reference 
    { 
        return MyPoint;

    }
};

int main(void)
{
    MyClass m;
    int x1 = m.MyPointIs().x;        //x1 = 0
    int y1 = m.MyPointIs().y;        //y1 = 0
    m.MyPointIs() = CPoint(20,20);   //m.MyPoint.x = 20, m.MyPoint.y = 20
    int x2 = m.MyPointIs().x;        //x2 = 20
    int y2 = m.MyPointIs().y;        //y2 = 20
    CPoint newPoint = m.MyPointIs();    //newPoint = (20,20)
}
如上面 main 函数所示,同一个 MyPointIs() 方法被用作访问器,用于设置 x1 和 y1 的值,当它用作左值时也用作设置器。之所以可行,是因为 myPointIs() 方法返回一个引用,该引用会被右侧的值(CPoint(20,20))修改。

我为什么应该关心这些 const 玩意?

在 C/C++ 中,数据默认是通过值传递到函数的。这意味着当一个参数传递给函数时,会创建一个副本。因此,函数内对参数的任何修改都不会影响函数外的参数。缺点是每次调用函数时都必须创建一个副本,这可能会很低效。如果函数在执行(例如)1000 次的循环中被调用,情况尤其如此。

这是一个例子。

class MyClass
{
public:
    CPoint MyPoint;
    MyClass()
    {
        //void constructor
        MyPoint.x = 0;
        MyPoint.y = 0;
    }
    ~MyClass()
    {
        //destructor
    }

    SetPoint(CPoint point)    //passing by value 
    {
        MyPoint.x = point.x;
        MyPoint.y = point.y;

        point.x = 100;        //Modifying the parameter has 
                              //no effect outside the method
        point.y = 101;    
    }
};

int main(void)
{
    MyClass m;
    CPoint newPoint(15,15);
    m.SetPoint(newPoint);   //m.Mypoint is (15,15) and so is newPoint.
}

如上例所示,newPoint 的值保持不变,因为 SetPoint() 方法操作的是 newPoint 的副本。为了提高效率,我们可以按引用传递参数而不是按值传递。在这种情况下,会将参数的引用传递给函数,而无需创建副本。但是,现在的问题是,如果参数在方法中被修改(如上所示),那么方法外的变量也会改变,从而可能导致错误。在参数前加上 const 可以确保参数不能在方法内修改。

class MyClass
{
public:
    CPoint MyPoint;
    MyClass()
    {
        //void constructor
        MyPoint.x = 0;
        MyPoint.y = 0;
    }
    ~MyClass()
    {
        //destructor
    }

    SetPoint(const CPoint& point)   //passing by reference
    {
        MyPoint.x = point.x;
        MyPoint.y = point.y;

        point.x = 100;        //error, cannot modify const object
        point.y = 101;        //error, cannot modify const object

    }
};

int main(void)
{
    MyClass m;
    CPoint newPoint(15,15);
    m.SetPoint(newPoint);        //m.Mypoint is (15,15) and so is newPoint.
}

在这种情况下,const 用作一种安全机制,可以防止你编写可能让你吃亏的代码。你应该尽可能多地尝试使用 const 引用。通过(在适当的情况下)将你的方法参数声明为 const,或声明 const 方法,你实际上是在立下一个合同,即该方法永远不会更改参数的值,也不会修改对象数据。这样,其他程序员就可以确信你提供的方法不会破坏他们传入给它的数据。

© . All rights reserved.