14-5 公有与私有成员及访问限定符

假设在一个凉爽的秋日,你走在街上吃着墨西哥卷饼。想找个地方坐下,于是环顾四周。左边是座公园,修剪整齐的草坪下有参天树荫,几张坐着不舒服的长椅,附近游乐场里还有孩子在尖叫。右边是陌生人的住宅。透过窗户,你看见一张舒适的躺椅和噼啪作响的壁炉。

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

决定你选择的关键在于:公园是公共空间,而住宅属于私人领域。任何人都可以自由进入公共空间,但只有住宅成员(或获得明确许可者)才能进入私人住宅。


成员访问

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

C++ 有三种不同的访问级别:publicprivateprotected。在本节中,我们将介绍两种常用的访问级别:publicprivate

相关内容:
受保护访问权限将在继承章节(第24.5课——继承与访问限定符)中详细讨论。

每次访问成员时,编译器都会检查该成员的访问权限级别是否允许访问。若访问未获许可,编译器将报出编译错误。该访问权限系统有时被非正式地称为访问控制access controls


结构体的成员默认为公共成员

具有公共访问权限的成员称为公共成员。公共成员Public members是类类型中不受访问限制的成员。正如开篇比喻中的公园,公共成员可被任何人访问(只要处于作用域内)。

公共成员可被同一类的其他成员访问。值得注意的是,公共成员也可被“公共领域the public”访问——即存在于特定类类型成员之外的代码。公共领域的示例包括非成员函数以及其他类类型的成员。

核心要点:
结构体的成员默认为公共成员。公共成员可被该类类型的其他成员访问,也可被公共领域访问。
术语“public”特指存在于给定类类型成员外部的代码,包括非成员函数及其他类类型的成员。

默认情况下,结构体的所有成员均为public成员。

考虑以下结构体:

#include <iostream>

struct Date
{
    // struct members are public by default, can be accessed by anyone
    int year {};       // public by default
    int month {};      // public by default
    int day {};        // public by default

    void print() const // public by default
    {
        // public members can be accessed in member functions of the class type
        std::cout << year << '/' << month << '/' << day;
    }
};

// non-member function main is part of "the public"
int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    // public members can be accessed by the public
    today.day = 16; // okay: the day member is public
    today.print();  // okay: the print() member function is public

    return 0;
}

image

在此示例中,成员变量在三个位置被访问:

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

这三处访问均被允许,因为公共成员可在任意位置被访问。

由于 main() 并非 Date 的成员,它被视为公共部分。但公共部分可访问公共成员,因此 main() 能直接访问 Date 的成员(包括调用 today.print())。


类的成员默认是私有的

具有私有访问权限的成员称为私有成员。私有成员Private members是类类型中的成员,仅能被同一类的其他成员访问。

请看以下示例,它与上文示例几乎完全相同:

#include <iostream>

class Date // now a class instead of a struct
{
    // class members are private by default, can only be accessed by other members
    int m_year {};     // private by default
    int m_month {};    // private by default
    int m_day {};      // private by default

    void print() const // private by default
    {
        // private members can be accessed in member functions
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // compile error: can no longer use aggregate initialization

    // private members can not be accessed by the public
    today.m_day = 16; // compile error: the m_day member is private
    today.print();    // compile error: the print() member function is private

    return 0;
}

image
image
image

在此示例中,成员变量在三个相同的位置被访问:

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

然而,若编译此程序,你会发现会产生三个编译错误。

在 main() 中,语句 today.m_day = 16 和 today.print() 均会引发编译错误。这是因为 main() 属于 public 范围,而 public 不允许直接访问 private 成员。

在 print() 中,访问 m_year、m_month 和 m_day 成员是被允许的。这是因为 print() 是类的成员函数,而类成员可以访问 private 成员。

那么第三个编译错误从何而来?令人意外的是,today的初始化操作现在会引发编译错误。在第13.8节——结构体聚合初始化中,我们提到聚合体“不能包含私有或受保护的非静态数据成员”。由于Date类具有私有数据成员(类成员默认是私有的),因此该类不符合聚合体的定义。因此我们不能再使用聚合初始化来初始化它。

关于如何正确初始化类(通常不属于聚合),我们将在后续第14.9课——构造函数入门中讨论。

关键要点:
类的成员默认为私有。私有成员可被该类的其他成员访问,但无法被外部访问。
具有私有成员的类不再属于聚合类,因此无法再使用聚合初始化。


为私有成员变量命名

在C++中,为私有数据成员命名时通常采用以“m_”为前缀的约定。这样做有几个重要原因。

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

// Some member function that sets private member m_name to the value of the name parameter
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
{
// Any members defined here would default to private

public: // here's our public access specifier

    void print() const // public due to above public: specifier
    {
        // members can access other private members
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

private: // here's our private access specifier

    int m_year { 2020 };  // private due to above private: specifier
    int m_month { 14 }; // private due to above private: specifier
    int m_day { 10 };   // private due to above private: specifier
};

int main()
{
    Date d{};
    d.print();  // okay, main() allowed to access public members

    return 0;
}

image

此示例可编译。由于 print() 因 public: 访问限定符而成为公共成员,属于公共范围的 main() 函数被允许访问它。

由于存在私有成员,我们无法进行聚合初始化。在此示例中,我们改用默认成员初始化(作为临时解决方案)。

由于类默认采用私有访问权限,可省略前缀 private: 访问限定符:

class Foo
{
// private access specifier not required here since classes default to private members
    int m_something {};  // private by default
};

然而,由于类和结构体的默认访问权限不同,许多开发者更倾向于显式声明:

class Foo
{
private: // redundant, but makes it clear that what follows is private
    int m_something {};  // private by default
};

尽管从技术上讲这是多余的,但显式使用 private: 限定符能明确表明后续成员为私有,无需根据 Foo 是类还是结构体的定义来推断默认访问级别。


访问级别摘要

以下是不同访问级别的简要对照表:

Access level Access specifier Member access Derived class access Public access
Public public: yes yes yes
Protected protected: yes yes no
Private private: yes no no

类类型允许使用任意数量的访问限定符,且顺序不限,这些限定符可重复使用(例如:可以先定义若干公共成员,接着定义私有成员,然后再定义更多公共成员)。

大多数类都会为不同成员同时使用私有和公共访问限定符。我们将在下一节中看到相关示例。


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

既然我们已经了解了访问级别的含义,接下来就谈谈如何正确使用它们。

结构体应完全避免使用访问限定符,这意味着所有结构体成员默认均为公有的。我们希望结构体成为聚合体,而聚合体只能拥有公有成员。使用 public: 访问限定符会与默认设置重复,而使用 private: 或 protected: 则会使结构体失去聚合特性。

类通常应仅包含私有(或受保护)的数据成员(通过默认私有访问级别或 private:(或 protected:)访问限定符实现)。其原理将在下一节 14.6 -- 访问函数中详述。

类通常具有公共成员函数(以便对象创建后可供外部使用)。但若某些成员函数不需对外公开,则可设为私有(或受保护)。

最佳实践
类应将成员变量设为私有(或受保护),成员函数设为公共。
结构体应避免使用访问限定符(所有成员默认为公共)。


访问权限按类定义

C++访问权限的一个常被忽略或误解的细节是:成员的访问权限是按类定义的,而非按对象定义。

你已知成员函数可直接访问私有成员(隐含对象的成员)。但由于访问权限是按类而非按对象定义的,成员函数还能直接访问作用域内任何同类型的其他对象的私有成员。

以下例说明:

#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;
}

这将输出:

image

这里有几点需要注意。

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

其次,由于类包含私有成员,它不再属于聚合类型,因此无法使用聚合初始化来创建Person对象。作为临时解决方案(在后续讲解正式处理方法前),我们创建了名为setName()的公共成员函数,用于为Person对象赋值姓名。

第三,由于kisses()是成员函数,它能直接访问私有成员m_name。但你可能会惊讶地发现它也能直接访问p.m_name!这是因为p本身是Person对象,而kisses()可访问作用域内任意Person对象的私有成员!
在运算符重载章节中,我们将看到更多利用此特性的示例。


结构体与类的技术与实践差异

既然我们已经讨论了访问权限,现在终于可以探讨结构体与类的技术差异了。准备好了吗?

类的成员默认是私有的,而结构体的成员默认是公开的。

……

没错,就是这样。

作者注:
严格来说To be pedantic还有一个细微差别——结构体公开继承其他类类型structs inherit from other class types publicly,而类私有继承classes inherit privately。继承章节将详细说明其含义,但这个具体点实际上无关紧要,因为你本就不该依赖继承的默认行为。

实际应用中,结构体和类的用法存在差异。

经验法则是:当以下所有条件都成立时使用结构体:

  • 数据集合简单且无需访问限制
  • 聚合初始化已足够满足需求
  • 无需类不变量、初始化或清理机制

结构体适用场景示例:constexpr全局程序数据、点结构体(简单整型成员集合,无需私有化)、函数返回数据集的结构体。

否则请使用类。

我们期望结构体保持聚合特性。因此若使用任何会导致结构体失去聚合特性的功能,则应改用类(并遵循所有类最佳实践)。


测验时间

问题 #1

a) 什么是公共成员?

显示解答

公共成员是指类中任何人都可以访问的成员,包括同一类的其他成员和公众。

b) 什么是私有成员?

显示解答

私有成员是指只能被该类其他成员访问的类成员。

c) 什么是访问限定符?

显示解答

访问限定符决定了哪些对象可以访问限定符后面的成员。

d) 共有多少种访问限定符?它们分别是什么?

显示解答

三。公共、私有和受保护。
``

---

问题 #2

a) 编写一个名为 Point3d 的类。该类应包含:

* 三个名为 m_x、m_y 和 m_z 的 int 类型私有成员变量;
* 一个名为 setValues() 的公有成员函数,用于设置 m_x、m_y 和 m_z 的值。
* 一个名为print()的公共成员函数,该函数以以下格式打印该点:<m_x, m_y, m_z>

确保以下程序能正确执行:

```cpp
int main()
{
    Point3d point;
    point.setValues(1, 2, 3);

    point.print();
    std::cout << '\n';

    return 0;
}

这应该输出:

<1, 2, 3>

image

显示解答

#include <iostream>

class Point3d
{
private:
    int m_x {};
    int m_y {};
    int m_z {};

public:
	void setValues(int x, int y, int z)
	{
		m_x = x;
		m_y = y;
		m_z = z;
	}

	void print() const
	{
		std::cout << '<' << m_x << ", " << m_y << ", " << m_z << '>';
	}
};

int main()
{
    Point3d point;
    point.setValues(1, 2, 3);

    point.print();
    std::cout << '\n';

    return 0;
}

b) 在 Point3d 类中添加一个名为 isEqual() 的函数。以下代码应能正确运行:

int main()
{
	Point3d point1{};
	point1.setValues(1, 2, 3);

	Point3d point2{};
	point2.setValues(1, 2, 3);

	std::cout << "point 1 and point 2 are" << (point1.isEqual(point2) ? "" : " not") << " equal\n";

	Point3d point3{};
	point3.setValues(3, 4, 5);

	std::cout << "point 1 and point 3 are" << (point1.isEqual(point3) ? "" : " not") << " equal\n";

	return 0;
}

这应该输出:

point 1 and point 2 are equal
point 1 and point 3 are not equal

image

显示解答

#include <iostream>

class Point3d
{
private:
	int m_x {};
	int m_y {};
	int m_z {};

public:
	void setValues(int x, int y, int z)
	{
		m_x = x;
		m_y = y;
		m_z = z;
	}

	void print() const
	{
		std::cout << '<' << m_x << ", " << m_y << ", " << m_z << '>';
	}

	// We can use the fact that access controls work on a per-class basis here
	// to directly access the private members of Point3d parameter p
	bool isEqual(const Point3d& p) const
	{
		return (m_x == p.m_x) && (m_y == p.m_y) && (m_z == p.m_z);
	}
};

int main()
{
	Point3d point1{};
	point1.setValues(1, 2, 3);

	Point3d point2{};
	point2.setValues(1, 2, 3);

	std::cout << "point 1 and point 2 are" << (point1.isEqual(point2) ? "" : " not") << " equal\n";

	Point3d point3{};
	point3.setValues(3, 4, 5);

	std::cout << "point 1 and point 3 are" << (point1.isEqual(point3) ? "" : " not") << " equal\n";

	return 0;
}
posted @ 2025-12-28 05:53  游翔  阅读(54)  评论(0)    收藏  举报