章节目录

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

本节阅读量:

假设你在一个秋高气爽的日子走在街上,吃着煎饼。想找个地方坐,所以你四处看看。左边是一个公园,有修剪过的草地和荫凉的树木,几张不舒服的长椅,附近的操场上有尖叫的孩子。右边是一个陌生人的住所。透过窗户,你可以看到舒适的躺椅。

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

选择的关键决定因素是公园是公共空间,而住宅是私人空间。您(和任何其他人)都可以自由进入公共空间。但只有居住区的成员(或明确允许进入的人)才允许进入私人住宅。


成员访问权限

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

C++有三种不同的访问级别:公共(public)、私有(private)和受保护(protected)。在本课中,将介绍两个常用的访问级别:public和private。

每当访问成员时,编译器都会检查该成员的访问级别,是否允许访问该成员。如果不允许访问,编译器将生成编译错误。


默认情况下,结构体的成员是public的

具有公共访问级别的成员称为公共成员。公共成员,对如何访问它们没有任何限制。就像前面类比中的公园一样,任何人都可以访问公共成员。

公共成员可以被同一类的其他成员访问,也可以被同一类之外的其它地方访问到。

默认情况下,结构体的所有成员都是公共成员。

考虑以下结构:

 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
#include <iostream>

struct Date
{
    // 结构体成员默认都是public,意味着可以被任何人访问
    int year {};       // public 默认
    int month {};      // public 默认
    int day {};        // public 默认

    void print() const // public 默认
    {
        // public成员可以被同一类中的其它成员函数访问
        std::cout << year << '/' << month << '/' << day;
    }
};

// 非成员函数 main,也意味着 "public"
int main()
{
    Date today { 2020, 10, 14 }; // 聚合初始化

    // public成员可以在此访问
    today.day = 16; // okay: day 是 public
    today.print();  // okay: print() 是public

    return 0;
}

在此示例中,可以在三个位置访问public成员:

  1. 在成员函数print()中,访问隐式对象的year、month和day成员。
  2. 在main()中,直接访问 today.day 来设置其值。
  3. 在main()中,调用成员函数 today.print()。

所有这三种访问都是允许的,因为可以从任何地方访问pubilc成员。

因为main()不是Date的成员,所以它被认为是公共的一部分。然而,由于public中可以访问public成员,main() 可以直接访问Date的成员(包括对today.print()的调用)。


默认情况下,类的成员是私有的

私有成员是只能由同一类的其他成员访问的类类型的成员。

考虑下面的示例,它几乎与上面的示例相同:

 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
#include <iostream>

class Date // 现在是 class 而不是 struct
{
    // class 成员默认是是 private, 只能被其它本类的成员访问
    int m_year {};     // 默认 private
    int m_month {};    // 默认 private
    int m_day {};      // 默认 private

    void print() const // 默认 private
    {
        // 私有成员可以在成员函数中访问到
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // 编译失败: 无法再使用聚合初始化

    // 私有成员不能在 public 域内访问
    today.m_day = 16; // 编译失败: m_day 是 private
    today.print();    // 编译失败: print() 是 private

    return 0;
}

在此示例中,成员在相同的三个位置进行访问:

  1. 在成员函数print()中,访问隐式对象的m_year、m_month和m_day成员。
  2. 在main()中,直接访问 today.m_day来设置其值。
  3. 在main()中,调用成员函数 today.print()。

然而,如果您编译这个程序,您将注意到生成了三个编译错误。

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

在print()中,允许访问成员m_year、m_month和m_day。这是因为 print() 是类的成员,并且类的成员可以访问私有成员。

那么,第三个编译错误是从哪里来的呢?也许令人惊讶的是,today 的初始化现在会导致编译错误。在前面学习结构体聚合初始化中,可以注意到聚合“没有私有或受保护的非静态数据成员”。Date类具有私有数据成员(因为默认情况下类的成员是私有的),因此Date类不符合聚合的条件。因此,不能再使用聚合初始化来初始化它。

在即将学习构造函数里,将讨论如何正确初始化类(通常是非聚合的)。


命名私有成员变量

在C++中,以“m_”前缀开头命名私有数据成员是一种常见的约定。这样做有几个重要的原因。

考虑某个类的以下成员函数:

1
2
3
4
5
// 某个成员函数,将成员变量 m_name 设置为参数 name
void setName(std::string_view name)
{
    m_name = name;
}

首先,“m_”前缀允许轻松地将数据成员与成员函数中的函数参数或局部变量区分开来。可以很容易地看到,“m_name”是成员,而“name”不是。这有助于明确此函数正在更改类的状态。这很重要,因为当更改数据成员的值时,它会持续存在于成员函数的作用域之外(而对函数参数或局部变量的更改通常不会)。

这与建议对局部静态变量使用“s_”前缀,对全局变量使用“g_”前缀的原因相同。

其次,“m_”前缀有助于防止私有成员变量与局部变量、函数参数和成员函数的名称之间的命名冲突。

如果私有成员名是name而不是m_name,则:

  1. name函数参数将隐藏名称私有数据成员。
  2. 若有一个名为name的成员函数,那个么由于重新定义标识符名称,将得到一个编译错误。

通过访问说明符设置访问级别

默认情况下,结构体(和联合)的成员是公共的,类的成员是私有的。

然而,可以通过使用访问说明符来显式设置成员的访问级别。访问说明符设置说明符之后的所有成员的访问级别。C++提供了三个访问说明符:“public:”、“private:”和“protected:”。

在下面的示例中,使用“public:”访问说明符来确保public域可以使用print()成员函数,使用“private:”访问说明符来使数据成员私有。

 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
class Date
{
// 这里定义的所有成员都是 private

public: // 这里是 public: 访问说明符

    void print() const // public
    {
        // 可以访问 private 成员
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

private: // 这里是 private: 访问说明符

    int m_year { 2020 };  // private
    int m_month { 14 }; // private
    int m_day { 10 };   // private
};

int main()
{
    Date d{};
    d.print();  // okay, main() 可以访问 public 成员

    return 0;
}

此示例编译。由于print() 是“public:”访问说明符的公共成员,因此允许main()(它是public的一部分)访问它。

因为有私有成员,所以无法聚合初始化。对于本例,使用默认成员初始化(作为临时解决方案)。

由于类默认为private访问,因此可以省略前导的“private:”访问说明符:

1
2
3
4
5
class Foo
{
// 默认 private 访问级别
    int m_something {};  // 默认 private
};

然而,由于类和结构体具有不同的访问级别默认值,许多开发人员更喜欢显式声明:

1
2
3
4
5
class Foo
{
private: // 多余, 但是更清晰
    int m_something {};  // 默认 private
};

尽管这在技术上是多余的,但使用显式“private:”说明符可以清楚地表明以下成员是私有的,而不必根据Foo是定义为类还是结构体来推断默认访问级别。


访问级别摘要

下面是不同访问级别的快速摘要表:

访问级别 访问说明符 成员可访问 子类可访问 public可访问
公共 public:
受保护 protected:
私有 private:

允许类类型以任何顺序使用任意数量的访问说明符,并且它们可以重复使用(例如,可以有一些公共成员,然后有一些私有成员,然后是更多公共成员)。

大多数类都为各种成员使用私有和公共访问说明符。


结构体和类的访问级别最佳实践

现在已经介绍了什么是访问级别,讨论一下应该如何使用它们。

结构体应该完全避免访问说明符,这意味着默认情况下所有结构体成员都是公共的。我们希望结构体是聚合,并且聚合只能具有公共成员。使用“public:”访问说明符在默认情况下是多余的,使用“private:”或“protected:”将使该结构体成为非聚合结构。

类通常应仅具有私有(或受保护)数据成员(通过使用默认的“private:”或“protected:”)。

类通常具有公共成员函数(因此这些成员函数可以在创建对象后由公共使用)。如果成员函数不打算供公众使用,则有时会将其设为私有(或受保护)。


访问级别按类工作

C++访问级别的一个细微差别是,对成员的访问是在每个类的基础上定义的,而不是在每个对象的基础上。

成员函数可以直接访问(隐式对象的)私有成员。然而,由于访问级别是按类的,而不是按对象的,因此成员函数还可以直接访问作用域中同一类类型的任何其他对象的私有成员。

让我们用一个例子来说明这一点:

 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
29
30
31
32
33
#include <iostream>
#include <string>
#include <string_view>

class Person
{
private:
    std::string m_name{};

public:
    void kisses(const Person& p) const
    {
        std::cout << m_name << " kisses " << p.m_name << '\n';
    }

    void setName(std::string_view name)
    {
        m_name = name;
    }
};

int main()
{
    Person joe;
    joe.setName("Joe");
    
    Person kate;
    kate.setName("Kate");

    joe.kisses(kate);

    return 0;
}

这将打印:

1
Joe kisses Kate

这里有几点需要注意。

首先,m_name被设为私有,因此它只能由Person类的成员(而不是公共)访问。

其次,因为类有私有成员,所以它不是聚合,并且不能使用聚合初始化来初始化Person对象。作为一种变通方法(直到我们讨论了这个问题的适当解决方案),我们创建了一个名为setName()的公共成员函数,该函数允许为Person对象分配一个名称。

第三,因为kisss()是一个成员函数,所以它可以直接访问私有成员m_name。然而,您可能会惊讶地看到它也可以直接访问p.m_name!这是有效的,因为p是Person对象,而kisses()可以访问范围内任何Person对象的私有成员!

在关于操作符重载的一章中,我们将看到更多的例子来使用它。


结构体和类之间的技术和实践差异

现在我们已经讨论了访问级别,终于可以讨论结构体和类之间的技术差异了。准备好了吗?

类将其成员默认设置为private,而结构体将其成员默认设置为public。

是的,就是这样。

在实践中,以不同的方式使用结构体和类。

根据经验法则,在满足以下所有条件时使用结构体:

  1. 有一个简单的数据集合,不能从限制访问中受益。
  2. 聚合初始化已足够。
  3. 没有类不变量、设置限制或清理需要。

可以在何处使用结构体的几个示例:constexpr全局程序数据、简单结构(例如int成员的简单集合,不能从私有化中受益)、用于从函数返回一组数据的结构。

否则使用类。

我们希望结构体是聚合。因此,如果您使用任何使结构成为非聚合的功能,那么您可能应该改用类(并遵循类的所有最佳实践)。


14.3 Const类对象和Const成员函数

上一节

14.5 访问函数

下一节