章节目录

Const类对象和Const成员函数

本节阅读量:

在前面,我们知道基本数据类型(int、double、char等)的对象可以通过const关键字设置为常量。所有常变量都必须在创建时初始化。

1
2
3
const int x;      // 编译失败: 未初始化
const int y{};    // ok: 值初始化
const int z{ 5 }; // ok: 列表初始化

类似地,通过使用const关键字,类类型对象(结构体、类和联合)也可以成为const。这样的对象也必须在创建时初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Date
{
    int year {};
    int month {};
    int day {};
};

int main()
{
    const Date today { 2020, 10, 14 }; // const class 类型的对象

    return 0;
}

就像普通变量一样,当需要确保类类型对象在创建后不会被修改时,通常将它们设置为const(或constexpr)。


不允许修改常量对象的数据成员

一旦初始化了常量类类型对象,就不允许任何修改它的数据成员的操作,因为这将违反const属性。这包括直接更改成员变量(如果它们是public的),或调用设置成员变量的成员函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct Date
{
    int year {};
    int month {};
    int day {};

    void incrementDay()
    {
        ++day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const

    today.day += 1;        // 编译失败: 不能修改const 对象的成员变量
    today.incrementDay();  // 编译失败: 不能调用修改const 对象成员变量的成员函数

    return 0;
}

Const对象不能调用非const成员函数

您可能会惊讶地发现,此代码也会导致编译错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print()
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const

    today.print();  // 编译失败: 不能调用非const成员函数

    return 0;
}

即使print()不尝试修改成员变量,对today.print()的调用仍然是无法编译。这是因为print()成员函数本身没有声明为const。编译器不允许对常量对象调用非const成员函数。


Const成员函数

为了解决上述问题,需要使print()成为const成员函数。来保证不会修改对象或调用任何非const成员函数(因为它们可能会修改对象)。

使print()成为const成员函数很容易——只需将const关键字附加到函数原型中,在参数列表之后,在函数体之前:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // 现在是个const成员函数
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const

    today.print();  // ok: const 对象可以调用 const 成员函数

    return 0;
}

在上面的例子中,print()是一个const成员函数,这意味着可以在const对象上调用它(例如today)。

试图更改成员变量或调用非const成员函数的const成员函数将导致发生编译器错误。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct Date
{
    int year {};
    int month {};
    int day {};

    void incrementDay() const // 设置为 const
    {
        ++day; // 编译失败: const 成员函数不能修改成员变量
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const

    today.incrementDay();

    return 0;
}

在本例中,incrementDay() 已标记为const成员函数,但它试图更改day。这将导致编译器错误。


可以在非常量对象上调用const成员函数

也可以在非常量对象上调用const成员函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // const
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // non-const

    today.print();  // ok: 可以调用

    return 0;
}

由于const成员函数可以在常量和非常量对象上调用,因此如果成员函数不修改对象的状态,则应将其设置为常量。

一旦成员函数成为const,就可以在常量对象上调用该函数。


通过常量引用传递的常量对象

尽管实例化常量局部变量是创建常量对象的一种方法,但获取常量对象的更常见方法是通过常量引用将对象传递给函数。

在前面,介绍了通过常量引用而不是通过值传递类类型参数的优点。概括地说,按值传递类类型参数会导致生成类的副本(这很慢)——大多数情况下,不需要副本,对原始参数的引用工作得很好,并避免生成副本。

你能找出下面的代码有什么问题吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() // non-const
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

void doSomething(const Date &date)
{
    date.print();
}

int main()
{
    Date today { 2020, 10, 14 }; // non-const
    today.print();

    doSomething(today);

    return 0;
}

答案是,在doSomething()函数内部,date被视为常量对象(因为它是通过常量引用传递的)。对于该常量,调用非const成员函数print()。由于不能对常量对象调用非const成员函数,这将导致编译错误。

修复很简单:将 print() 设置为 const:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // now const
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

void doSomething(const Date &date)
{
    date.print();
}

int main()
{
    Date today { 2020, 10, 14 }; // non-const
    today.print();

    doSomething(today);

    return 0;
}

现在,在函数doSomething() 中,const date将能够成功调用const成员函数print()。


成员函数常量和非常量重载

最后,尽管不经常这样做,但可以重载成员函数,使其具有同一函数的const版本和非const版本。这是因为const限定符被认为是函数签名的一部分,所以两个仅在const上不同的函数被认为是不同的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

struct Something
{
    void print()
    {
        std::cout << "non-const\n";
    }

    void print() const
    {
        std::cout << "const\n";
    }
};

int main()
{
    Something s1{};
    s1.print(); // 调用 print()

    const Something s2{};
    s2.print(); // 调用 print() const
    
    return 0;
}

这将打印:

1
2
non-const
const

当返回值的const需要不同时,通常会使用const和非const版本重载函数。这是相当罕见的。


14.2 成员函数

上一节

14.4 公共和私有成员以及访问说明符

下一节