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

假设你在秋高气爽的一天走在街上,手里拿着一个卷饼。你想找个地方坐下来,于是四处张望。你的左边是一个公园,有修剪过的草坪和遮荫的树木,几张不太舒适的长椅,以及附近游乐场上尖叫的孩子。你的右边是一个陌生人的住所。透过窗户,你可以看到一张舒适的躺椅和一个噼啪作响的壁炉。

你重重地叹了口气,选择了公园。

你做出这个选择的关键因素是公园是一个公共空间,而住所是私有的。你(以及任何人)都可以自由进入公共空间。但只有住所的成员(或被明确允许进入的人)才被允许进入私人住所。

成员访问

类似的概念也适用于类类型的成员。类类型的每个成员都有一个称为访问级别的属性,它决定了谁可以访问该成员。

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()中,我们访问了隐式对象的yearmonthday成员。
  • 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_yearm_monthm_day成员。
  • main()中,我们直接访问today.m_day来设置它的值。
  • main()中,我们调用了成员函数today.print()

然而,如果你编译这个程序,你会注意到产生了三个编译错误。

main()中,语句today.m_day = 16today.print()现在都产生了编译错误。这是因为main()是公共代码的一部分,而公共代码不允许直接访问私有成员。

print()中,访问成员m_yearm_monthm_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:说明符,它是公共的
    {
        // 成```

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"