在之前的公共和私有成员以及访问说明符中,我们讨论了公共和私有的访问级别。作为提醒,类通常会将其数据成员声明为私有,而私有成员不能被公共代码直接访问。
考虑以下Date
类:
#include <iostream>
class Date
{
private:
int m_year{ 2020 };
int m_month{ 10 };
int m_day{ 14 };
public:
void print() const
{
std::cout << m_year << '/' << m_month << '/' << m_day << '\n';
}
};
int main()
{
Date d{}; // 创建一个Date对象
d.print(); // 打印日期
return 0;
}
虽然这个类提供了一个print()
成员函数来打印整个日期,但这可能无法满足用户的全部需求。例如,如果Date
对象的用户想要获取年份,或者将年份更改为其他值,他们将无法做到,因为m_year
是私有的(因此不能被公共代码直接访问)。
对于某些类来说,在类的功能范围内,能够获取或设置私有成员变量的值是合适的。
访问函数
访问函数是一个简单的公共成员函数,其作用是检索或更改私有成员变量的值。
访问函数有两种形式:获取器(getters)和设置器(setters)。获取器(有时也称为访问器)是返回私有成员变量值的公共成员函数。设置器(有时也称为修改器)是设置私有成员变量值的公共成员函数。
术语说明
“修改器”(mutator)一词通常与“设置器”(setter)可以互换使用。但更广泛地说,修改器是任何修改(改变)对象状态的成员函数。根据这个定义,设置器是一种特定类型的修改器。然而,也有可能存在非设置器函数,它们也属于修改器。
获取器通常被声明为const
,以便它们可以在常量和非常量对象上调用。设置器应该是非const
的,以便它们可以修改数据成员。
为了说明,让我们更新我们的Date
类,使其拥有一整套获取器和设置器:
#include <iostream>
class Date
{
private:
int m_year { 2020 };
int m_month { 10 };
int m_day { 14 };
public:
void print()
{
std::cout << m_year << '/' << m_month << '/' << m_day << '\n';
}
int getYear() const { return m_year; } // 年份的获取器
void setYear(int year) { m_year = year; } // 年份的设置器
int getMonth() const { return m_month; } // 月份的获取器
void setMonth(int month) { m_month = month; } // 月份的设置器
int getDay() const { return m_day; } // 日期的获取器
void setDay(int day) { m_day = day; } // 日期的设置器
};
int main()
{
Date d{};
d.setYear(2021);
std::cout << "The year is: " << d.getYear() << '\n';
return 0;
}
该程序输出:
The year is: 2021
访问函数的命名
访问函数的命名没有通用的约定。然而,有一些比其他更受欢迎的命名约定。
使用“get”和“set”前缀:
int getDay() const { return m_day; } // 获取器
void setDay(int day) { m_day = day; } // 设置器
使用“get”和“set”前缀的优点是,它清楚地表明这些是访问函数(并且调用它们应该是廉价的)。
不使用前缀:
int day() const { return m_day; } // 获取器
void day(int day) { m_day = day; } // 设置器
这种风格更加简洁,并且对获取器和设置器使用相同的名称(依赖函数重载来区分两者)。C++标准库使用了这种约定。
不使用前缀约定的缺点是,它并不特别明显地表明这是在设置day
成员的值:
d.day(5); // 这看起来像是在将day成员设置为5吗?
关键洞察:为私有数据成员使用“m_”前缀的一个最佳理由是,避免数据成员和获取器具有相同的名称(C++不支持这一点,尽管其他语言如Java支持)。
仅使用“set”前缀:
int day() const { return m_day; } // 获取器
void setDay(int day) { m_day = day; } // 设置器
选择以上哪种方式取决于个人偏好。然而,我们强烈推荐为设置器使用“set”前缀。获取器可以使用“get”前缀或不使用前缀。
提示:在设置器上使用“set”前缀,以更清楚地表明它们正在改变对象的状态。
获取器应按值或按常量左值引用返回
获取器应提供对数据的“只读”访问。因此,最佳实践是它们应按值返回(如果复制成员的代价不大)或按常量左值引用返回(如果复制成员的代价较大)。
由于按引用返回数据成员是一个不平凡的话题,我们将在第14.7课——返回数据成员的成员函数中更详细地讨论这个主题。
访问函数的注意事项
关于何时应使用或避免使用访问函数,存在相当多的讨论。许多开发人员认为,使用访问函数违反了良好的类设计原则(这一主题很容易填满一整本书)。
目前,我们推荐一种务实的方法。在创建类时,请考虑以下几点:
- 如果你的类没有不变量,并且需要大量的访问函数,请考虑使用结构体(其数据成员是公共的)并直接提供对成员的访问,而不是使用类。
- 优先实现行为或动作,而不是访问函数。例如,与其使用
setAlive(bool)
设置器,不如实现一个kill()
和一个revive()
函数。 - 只有在公共代码合理需要获取或设置单个成员的值时,才提供访问函数。
为什么要将数据设为私有,如果我们要提供一个公共访问函数来访问它?
很高兴你问到这个问题。我们将在后续课程(第14.8课——数据隐藏的好处(封装))中回答这个问题。