在常量变量(命名常量)中,你已经学习了如何使用const
关键字将基本数据类型(如int
、double
、char
等)的对象声明为常量。所有const
变量都必须在创建时初始化。
const int x; // 编译错误:未初始化
const int y{}; // 正确:值初始化
const int z{ 5 }; // 正确:列表初始化
类似地,类类型对象(结构体、类和联合体)也可以使用const
关键字声明为常量。这些对象也必须在创建时初始化。
struct Date
{
int year {};
int month {};
int day {};
};
int main()
{
const Date today { 2020, 10, 14 }; // 常量类类型对象
return 0;
}
与普通变量一样,当你需要确保对象在创建后不被修改时,通常希望将类类型对象声明为const
(或constexpr
)。
禁止修改常量对象的数据成员
一旦const
类类型对象被初始化,任何尝试修改该对象的数据成员的行为都是不允许的,因为这会违反对象的const
属性。这包括直接修改成员变量(如果它们是公开的),或者调用会设置成员变量值的成员函数。
struct Date
{
int year {};
int month {};
int day {};
void incrementDay()
{
++day;
}
};
int main()
{
const Date today { 2020, 10, 14 }; // 常量
today.day += 1; // 编译错误:不能修改常量对象的成员
today.incrementDay(); // 编译错误:不能调用会修改常量对象成员的成员函数
return 0;
}
常量对象不能调用非常量成员函数
你可能会惊讶地发现,以下代码也会导致编译错误:
#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 }; // 常量
today.print(); // 编译错误:不能调用非常量成员函数
return 0;
}
尽管print()
没有尝试修改任何成员变量,但对today.print()
的调用仍然是一个const
违规行为。这是因为print()
成员函数本身没有被声明为const
。编译器不允许我们在常量对象上调用非常量成员函数。
常量成员函数
为了解决上述问题,我们需要将print()
声明为常量成员函数。常量成员函数是一种保证不会修改对象或调用任何非常量成员函数(因为它们可能会修改对象)的成员函数。
将print()
声明为常量成员函数很容易——我们只需在函数原型的参数列表之后、函数体之前添加const
关键字:
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() const // 现在是一个常量成员函数
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
const Date today { 2020, 10, 14 }; // 常量
today.print(); // 正确:常量对象可以调用常量成员函数
return 0;
}
在上述示例中,print()
已被声明为常量成员函数,这意味着我们可以在常量对象(如today
)上调用它。
对于高级读者:
对于在类定义之外定义的成员函数,const
关键字必须同时用于类定义中的函数声明和类定义之外的函数定义。我们在第15.2课——类和头文件中展示了这样的示例。
构造函数不能被声明为const
,因为它们需要初始化对象的成员,这需要修改它们。我们在第14.9课——构造函数的介绍中讨论了构造函数。
如果一个常量成员函数尝试修改一个数据成员或调用一个非常量成员函数,将会导致编译错误。例如:
struct Date
{
int year {};
int month {};
int day {};
void incrementDay() const // 被声明为常量
{
++day; // 编译错误:常量函数不能修改成员
}
};
int main()
{
const Date today { 2020, 10, 14 }; // 常量
today.incrementDay();
return 0;
}
在本例中,incrementDay()
被标记为常量成员函数,但它尝试修改day
。这将导致编译错误。
常量成员函数可以像往常一样修改非成员(如局部变量和函数参数)并调用非成员函数。const
仅适用于成员。
关键洞察:
- 常量成员函数不能:修改隐式对象,调用非常量成员函数。
- 常量成员函数可以:修改非隐式对象的对象,调用常量成员函数,调用非成员函数。
常量成员函数可以被非常量对象调用
常量成员函数也可以被非常量对象调用:
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() const // 常量
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
Date today { 2020, 10, 14 }; // 非常量
today.print(); // 正确:可以在非常量对象上调用常量成员函数
return 0;
}
由于常量成员函数可以被常量和非常量对象调用,如果一个成员函数不修改对象的状态,那么它应该被声明为const
。
最佳实践:
如果一个成员函数不(且永远不会)修改对象的状态,那么它应该被声明为const
,以便它可以被常量和非常量对象调用。
在将const
应用于成员函数时要小心。一旦一个成员函数被声明为const
,那么该函数就可以被常量对象调用。如果稍后从成员函数中移除const
,将会破坏任何在常量对象上调用该成员函数的代码。
通过常量引用传递创建常量对象
虽然声明常量局部变量是一种创建常量对象的方式,但更常见的获取常量对象的方式是通过将对象以常量引用的方式传递给函数。
在按左值引用传递中,我们讨论了将类类型参数以常量引用而不是按值传递的优点。回顾一下,按值传递类类型参数会导致类的副本被创建(这是低效的)——大多数情况下,我们不需要副本,对原始参数的引用就足够了,并且可以避免创建副本。我们通常将引用声明为const
,以便函数可以接受常量左值参数和右值参数(例如字面量和临时对象)。
你能找出以下代码中的问题吗?
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() // 非常量
{
std::cout << year << '/' << month << '/' << day;
}
};
void doSomething(const Date& date)
{
date.print();
}
int main()
{
Date today { 2020, 10, 14 }; // 非常量
today.print();
doSomething(today);
return 0;
}
答案是:在doSomething()
函数中,date
被视为一个常量对象(因为它是以常量引用传递的)。而对于这个常量date
,我们调用了非常量成员函数print()
。由于我们不能在常量对象上调用非常量成员函数,这将导致编译错误。
修复方法很简单:将print()
声明为const
:
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() const // 现在是常量
{
std::cout << year << '/' << month << '/' << day;
}
};
void doSomething(const Date& date)
{
date.print();
}
int main()
{
Date today { 2020, 10, 14 }; // 非常量
today.print();
doSomething(today);
return 0;
}
现在,在doSomething()
函数中,常量date
将能够成功调用常量成员