假设你在秋高气爽的一天走在街上,手里拿着一个卷饼。你想找个地方坐下来,于是四处张望。你的左边是一个公园,有修剪过的草坪和遮荫的树木,几张不太舒适的长椅,以及附近游乐场上尖叫的孩子。你的右边是一个陌生人的住所。透过窗户,你可以看到一张舒适的躺椅和一个噼啪作响的壁炉。
你重重地叹了口气,选择了公园。
你做出这个选择的关键因素是公园是一个公共空间,而住所是私有的。你(以及任何人)都可以自由进入公共空间。但只有住所的成员(或被明确允许进入的人)才被允许进入私人住所。
成员访问
类似的概念也适用于类类型的成员。类类型的每个成员都有一个称为访问级别的属性,它决定了谁可以访问该成员。
C++有三种不同的访问级别:公共(public)、私有(private)和受保护(protected)。在本课中,我们将介绍两种常用的访问级别:公共和私有。
相关内容:我们在继承章节(第24.5课——继承与访问说明符)中讨论了受保护的访问级别。
每当访问一个成员时,编译器都会检查该成员的访问级别是否允许访问。如果访问不被允许,编译器将生成一个编译错误。这种访问级别系统有时非正式地被称为访问控制。
结构体的成员默认为公共
具有公共访问级别的成员称为公共成员。公共成员是类类型的成员,访问它们没有任何限制。正如我们在开篇比喻中的公园一样,公共成员可以被任何人访问(只要它们在作用域内)。
公共成员可以被同一类类型的其他成员访问。值得注意的是,公共成员也可以被公共代码访问,我们称类类型成员之外的代码为公共代码。公共代码包括非成员函数以及其他类类型的成员。
关键洞察:结构体的成员默认为公共。公共成员可以被同一类类型的其他成员以及公共代码访问。
“公共”一词用于指代类类型成员之外的代码。这包括非成员函数以及其他类类型的成员。
默认情况下,结构体的所有成员都是公共成员。
考虑以下结构体:
#include <iostream>
struct Date
{
// 结构体成员默认为公共,可以被任何人访问
int year {}; // 默认为公共
int month {}; // 默认为公共
int day {}; // 默认为公共
void print() const // 默认为公共
{
// 类类型成员函数中可以访问公共成员
std::cout << year << '/' << month << '/' << day;
}
};
// 非成员函数main是“公共”的一部分
int main()
{
Date today { 2020, 10, 14 }; // 使用聚合初始化结构体
// 公共成员可以被公共代码访问
today.day = 16; // 正确:day成员是公共的
today.print(); // 正确:print()成员函数是公共的
return 0;
}
在这个例子中,成员在三个地方被访问:
- 在成员函数
print()
中,我们访问了隐式对象的year
、month
和day
成员。 - 在
main()
中,我们直接访问today.day
来设置它的值。 - 在
main()
中,我们调用了成员函数today.print()
。
所有这三种访问都是允许的,因为公共成员可以从任何地方访问。
由于main()
不是Date
的成员,因此它被认为是公共的一部分。然而,因为公共代码可以访问公共成员,所以main()
可以直接访问Date
的成员(包括调用today.print()
)。
类的成员默认为私有
具有私有访问级别的成员称为私有成员。私有成员是类类型的成员,只能被同一类类型的其他成员访问。
考虑以下示例,它与上面的示例几乎相同:
#include <iostream>
class Date // 现在是一个类而不是结构体
{
// 类成员默认为私有,只能被其他成员访问
int m_year {}; // 默认为私有
int m_month {}; // 默认为私有
int m_day {}; // 默认为私有
void print() const // 默认为私有
{
// 成员函数中可以访问私有成员
std::cout << m_year << '/' << m_month << '/' << m_day;
}
};
int main()
{
Date today { 2020, 10, 14 }; // 编译错误:不能再使用聚合初始化
// 私有成员不能被公共代码访问
today.m_day = 16; // 编译错误:m_day成员是私有的
today.print(); // 编译错误:print()成员函数是私有的
return 0;
}
在这个例子中,成员在相同的三个地方被访问:
- 在成员函数
print()
中,我们访问了隐式对象的m_year
、m_month
和m_day
成员。 - 在
main()
中,我们直接访问today.m_day
来设置它的值。 - 在
main()
中,我们调用了成员函数today.print()
。
然而,如果你编译这个程序,你会注意到产生了三个编译错误。
在main()
中,语句today.m_day = 16
和today.print()
现在都产生了编译错误。这是因为main()
是公共代码的一部分,而公共代码不允许直接访问私有成员。
在print()
中,访问成员m_year
、m_month
和m_day
是允许的。这是因为print()
是类的成员,类的成员允许访问私有成员。
那么第三个编译错误是从哪里来的呢?也许令人惊讶的是,today
的初始化现在导致了一个编译错误。在第13.8课——结构体聚合初始化中,我们提到聚合不能有“私有或受保护的非静态数据成员”。我们的Date
类有私有数据成员(因为类的成员默认为私有),所以我们的Date
类不再是一个聚合。因此,我们不能再使用聚合初始化来初始化它。
我们将在后续课程(第14.9课——构造函数的介绍)中讨论如何正确初始化类(类通常是非聚合的)。
关键洞察:类的成员默认为私有。私有成员可以被同一类的其他成员访问,但不能被公共代码访问。
具有私有成员的类不再是聚合,因此不能再使用聚合初始化。
命名私有成员变量
在C++中,一个常见的约定是以“m_”前缀命名私有数据成员。这有几个重要的原因。
考虑以下类的某个成员函数:
// 某个成员函数,将私有成员m_name设置为参数name的值
void setName(std::string_view name)
{
m_name = name;
}
首先,“m_”前缀允许我们轻松区分成员函数中的数据成员、函数参数或局部变量。我们可以很容易地看出“m_name”是成员,而“name”不是。这有助于明确该函数正在改变类的状态。这是很重要的,因为当我们改变数据成员的值时,这种改变会超出成员函数的作用域(而对函数参数或局部变量的改变通常不会)。
这也是我们推荐使用“s_”前缀命名局部静态变量,以及使用“g_”前缀命名全局变量的原因。
其次,“m_”前缀有助于防止私有成员变量与局部变量、函数参数和成员函数的名称发生冲突。
如果我们把私有成员命名为name
而不是m_name
,那么:
- 我们的
name
函数参数会隐藏name
私有数据成员。 - 如果我们有一个名为
name
的成员函数,我们会因为标识符name
的重定义而得到一个编译错误。
最佳实践:考虑以“m_”前缀命名你的私有数据成员,以帮助区分它们与局部变量、函数参数和成员函数的名称。
如果需要,类的公共成员也可以遵循这一约定。然而,结构体的公共成员通常不使用这个前缀,因为结构体通常没有太多成员函数(如果有)。
使用访问说明符设置访问级别
默认情况下,结构体(和联合体)的成员是公共的,而类的成员是私有的。
然而,我们可以通过使用访问说明符来明确设置成员的访问级别。访问说明符设置说明符之后所有成员的访问级别。C++提供了三个访问说明符:public:
、private:
和protected:
。
在以下示例中,我们使用public:
访问说明符来确保print()
成员函数可以被公共代码使用,同时使用private:
访问说明符来使我们的数据成员成为私有的。
class Date
{
// 此处定义的任何成员默认为私有
public: // 这是我们的公共访问说明符
void print() const // 由于上面的public:说明符,它是公共的
{
// 成```