☰
深入理解C++类和类中的public、protected、private
2026/10/22 7:52:37 网站建设 项目流程

前言


public、protected、private是 C++ 中最先接触、也最容易被低估的三个关键字。多数教材只给一句「public 谁都能访问,private 只有自己能访问」,然后就进入写代码的环节。结果就是:有人把所有成员都设成 public,把类写成「带成员函数的结构体」;有人以为private是运行期的安全保护;还有人搞不清protected和继承方式组合后到底谁能看见谁。


先把几个前提讲清楚。第一,访问控制是编译期的概念,它只约束「名字能不能被使用」,不改变对象布局,也不产生运行期检查;private不是加密,指针偏移、内存拷贝照样能绕过,只是那样做属于未定义行为。第二,class和struct在 C++ 里几乎完全等价,唯一差别是默认访问级别与默认继承方式。第三,继承方式(public/protected/private inheritance)和成员访问级别是两个维度,必须放在一起看。


本文按「类定义 → 成员访问 → 继承矩阵 → 工程实践」的顺序展开,把三个关键字在每种组合下的效果讲透。示例以 C++17 为基准,涉及编译错误的行会用注释标出。


一、class 与 struct 的唯一差别


在 C++ 中,class和struct都能定义成员、成员函数、构造函数、继承、虚函数,功能上没有区别。差别只有两条:




项目classstruct



默认成员访问级别privatepublic

默认继承方式private继承public继承



也就是说,下面两段代码完全等价:


// 适用于 C++17
class A {
int x; // 默认 private
};

struct B {
int x; // 默认 public
};

社区约定是:只聚合数据、没有不变式(invariant)的类型用struct;需要维护不变式、有封装边界的类型用class。标准库也遵循这个约定,std::pair、std::array是struct,std::vector、std::string是class。约定不是语法要求,但它能让读代码的人一眼判断「这个类型的内部状态是否可以被随意改」。


访问说明符可以出现任意多次,作用范围是从它出现的位置到下一个访问说明符或类定义结束:


// 适用于 C++17
class Widget {
public:
void draw();
protected:
int id_;
private:
int cached_;
public:
int id() const { return id_; } // 又回到 public
};

这种写法合法,但可读性差;通常按public→protected→private的顺序各写一次。


二、三种访问级别的真实边界


2.1 public


public成员对所有人可见:类自己的成员函数、派生类、外部代码、友元。它是类的接口面。设计类时先想清楚「哪些操作是契约」,把这些放 public,其余一律收起来。


2.2 private


private成员只有以下位置可以访问:



  • 该类自己的成员函数(包括静态成员函数);

  • 该类的友元函数与友元类;

  • 该类的嵌套类(C++11 起嵌套类可以访问外层类的私有成员)。


派生类不能访问基类的 private 成员,无论用哪种继承方式。这一点常被误解为「继承了就看得见」,实际上继承只影响布局和名字查找的起点,不改变访问权限。


需要强调:private是一种「接口约束」,不是安全机制。通过reinterpret_cast把对象当成字节数组、或者用#define private public这种技巧,都能绕过它,但前者在严格别名规则下是未定义行为,后者是未定义行为的另一种形式(改变标准库头的语义)。正确做法是修改设计,而不是绕过访问控制。


2.3 protected


protected的可见范围介于两者之间:



  • 类自己的成员函数可以访问;

  • 派生类的成员函数可以访问;

  • 友元可以访问;

  • 外部代码不可以访问。


但protected有一个非常容易被忽略的限制:派生类只能通过「派生类类型(或更派生的类型)」的对象访问基类的 protected 成员,不能通过基类对象访问。例如:


// 适用于 C++17
class Base {
protected:
int value_ = 0;
};

class Derived : public Base {
public:
void ok() {
value_ = 1; // OK:通过 this 访问
Derived d;
d.value_ = 2; // OK:通过 Derived 对象访问
}
void bad(Base& b) {
// b.value_ = 3; // 错误:不能通过 Base 对象访问
}
};

这条规则的存在,是为了防止派生类随意修改「任意一个基类对象」的受保护状态。理解它,才能解释为什么有些代码在派生类里明明「继承了」却编译不过。


protected的争议在于:它把内部状态暴露给所有未来的派生类,形成一个比 public 更脆弱的契约——派生类可能依赖某个 protected 成员的实现细节,基类一改就全崩。Scott Meyers 早就建议「优先用 private,把需要给派生类的东西做成 protected 成员函数而不是数据成员」。现代设计更倾向于「要么 public 接口,要么 private 实现」,protected 数据成员应尽量避免。


三、继承方式与访问级别矩阵


继承方式决定了基类成员在派生类中的「最高可见级别」。把三种继承方式和三种成员级别组合起来,得到九种情况:




基类成员 \ 继承方式public继承protected继承private继承



public成员派生类中为 public派生类中为 protected派生类中为 private

protected成员派生类中为 protected派生类中为 protected派生类中为 private

private成员不可访问不可访问不可访问



规律可以概括为:派生类中的访问级别 =min(成员原级别, 继承方式),其中 private < protected < public;基类的 private 成员永远是「不可访问」,与继承方式无关。


private继承在语义上是「用基类的实现,但不暴露基类的接口」,常用于实现继承(如基于std::vector实现一个栈)。它和「成员对象」的区别在于能否利用空基类优化、能否重写虚函数。多数场景下,组合(把基类作为成员)比 private 继承更清晰。


protected继承更少见,它让基类的 public 接口对派生类的派生类可见,但对外部不可见。真正需要它的场景很少,遇到时先确认是否可以用组合替代。


C++ 还允许在派生类中用using调整某个名字的访问级别:


// 适用于 C++17
class Base {
protected:
void helper();
};

class Derived : private Base {
public:
using Base::helper; // 把 helper 提升为 public
};

using只能改变访问级别,不能改变名字本身;它也不能把基类的 private 成员变可见。


四、访问控制与虚函数、友元、嵌套类


4.1 private 虚函数也能被重写


虚函数的调用走动态分派,与访问级别无关。基类可以把虚函数设为 private,派生类照样能重写,通过基类指针调用时也照样分派到派生类实现:


// 适用于 C++17
class Shape {
public:
virtual ~Shape() = default;
void render() { doRender(); } // 模板方法:对外只暴露 render
private:
virtual void doRender() = 0; // 派生类可以重写私有虚函数
};

class Circle : public Shape {
private:
void doRender() override { }
};

这是模板方法模式(template method)的常见写法:基类用 public 非虚函数固定流程,把可变的步骤放进 private 虚函数。派生类不需要、也不应该直接调用doRender。


4.2 友元


friend声明可以授予某个函数或类访问本类 private/protected 成员的权限。要点:



  • 友元关系不传递:A 是 B 的友元、B 是 C 的友元,不代表 A 能访问 C;

  • 友元关系不继承:基类的友元不能访问派生类的私有成员,反之亦然;

  • 友元声明写在类内,但友元函数本身是命名空间作用域的普通函数,不是成员函数;

  • friend class X;授予 X 的所有成员函数访问权,范围较大,优先授予单个函数。


友元常被当作「破坏封装」的坏东西,其实它是受控的开口:类的作者主动决定「谁可以看我的内部」,比把成员改成 public 要好得多。典型合法用途是运算符重载(operator<<)和两个紧密协作的类。


4.3 嵌套类


C++11 起,嵌套类可以访问外层类的所有成员(包括 private),但外层类不能访问嵌套类的 private 成员。嵌套类与外层类之间没有隐式的this关联,它只是一个定义在类作用域内的普通类,访问外层非静态成员仍需一个外层对象。


常见坑点



  1. ❌ 以为private成员在派生类里「继承下来了就能用」。✅ 基类 private 成员在派生类中始终不可访问,只能通过基类提供的 public/protected 接口间接操作。



  1. ❌ 在派生类里通过基类引用访问 protected 成员:void f(Base& b) { b.x = 1; }。✅ 只能通过派生类类型(或更派生类型)的对象访问;改成参数类型为Derived&,或让基类提供 protected 成员函数。



  1. ❌ 用#define private public来「测试私有成员」。✅ 这是未定义行为,且会污染所有包含的头文件;测试应通过 public 接口,或把待测逻辑抽成可单独测试的组件。



  1. ❌ 把protected数据成员当作给派生类的「扩展点」。✅ 优先提供 protected 成员函数或 public 虚函数;数据成员保持 private,避免派生类绑定实现细节。



  1. ❌ 认为class默认继承是 public。✅class D : B是 private 继承,struct D : B才是 public 继承;写继承时显式写出继承方式。



  1. ❌ 以为友元关系会被派生类继承。✅ 友元不继承、不传递;需要时在派生类中重新声明。



  1. ❌ 把using Base::foo;当成「复制」了函数。✅ 它只是把基类的名字引入派生类作用域并调整可见性,函数实体仍在基类。



  1. ❌ 用private继承后又想让外部通过基类指针使用派生对象。✅ private 继承下基类子对象对外不可见,Base* p = new Derived;在类外不合法;需要「is-a」关系就用 public 继承,需要复用实现就用组合。


总结




访问位置public 成员protected 成员private 成员



本类成员函数可访问可访问可访问

派生类成员函数可访问可访问不可访问

友元可访问可访问可访问

外部代码可访问不可访问不可访问





继承方式对外表现典型用途



public派生类 is-a 基类接口继承、多态

protected只对进一步派生开放极少使用

private基类接口完全隐藏实现复用(多数情况应用组合)



三个访问关键字的核心,不是「谁能看」,而是「作者愿意承诺什么」。public 是承诺,protected 是有限承诺,private 是不承诺。把承诺面收得越小,类就越容易演进;把所有成员都设成 public,等于放弃了未来修改实现的自由。理解继承矩阵和 protected 的访问限制,能避免大部分「明明继承了却编译不过」的困惑。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询