前言public、protected、private是 C 中最先接触、也最容易被低估的三个关键字。多数教材只给一句「public 谁都能访问private 只有自己能访问」然后就进入写代码的环节。结果就是有人把所有成员都设成 public把类写成「带成员函数的结构体」有人以为private是运行期的安全保护还有人搞不清protected和继承方式组合后到底谁能看见谁。先把几个前提讲清楚。第一访问控制是编译期的概念它只约束「名字能不能被使用」不改变对象布局也不产生运行期检查private不是加密指针偏移、内存拷贝照样能绕过只是那样做属于未定义行为。第二class和struct在 C 里几乎完全等价唯一差别是默认访问级别与默认继承方式。第三继承方式public/protected/private inheritance和成员访问级别是两个维度必须放在一起看。本文按「类定义 → 成员访问 → 继承矩阵 → 工程实践」的顺序展开把三个关键字在每种组合下的效果讲透。示例以 C17 为基准涉及编译错误的行会用注释标出。一、class 与 struct 的唯一差别在 C 中class和struct都能定义成员、成员函数、构造函数、继承、虚函数功能上没有区别。差别只有两条项目classstruct默认成员访问级别privatepublic默认继承方式private继承public继承也就是说下面两段代码完全等价// 适用于 C17class A {int x; // 默认 private};struct B {int x; // 默认 public};社区约定是只聚合数据、没有不变式invariant的类型用struct需要维护不变式、有封装边界的类型用class。标准库也遵循这个约定std::pair、std::array是structstd::vector、std::string是class。约定不是语法要求但它能让读代码的人一眼判断「这个类型的内部状态是否可以被随意改」。访问说明符可以出现任意多次作用范围是从它出现的位置到下一个访问说明符或类定义结束// 适用于 C17class Widget {public:void draw();protected:int id_;private:int cached_;public:int id() const { return id_; } // 又回到 public};这种写法合法但可读性差通常按public→protected→private的顺序各写一次。二、三种访问级别的真实边界2.1 publicpublic成员对所有人可见类自己的成员函数、派生类、外部代码、友元。它是类的接口面。设计类时先想清楚「哪些操作是契约」把这些放 public其余一律收起来。2.2 privateprivate成员只有以下位置可以访问该类自己的成员函数包括静态成员函数该类的友元函数与友元类该类的嵌套类C11 起嵌套类可以访问外层类的私有成员。派生类不能访问基类的 private 成员无论用哪种继承方式。这一点常被误解为「继承了就看得见」实际上继承只影响布局和名字查找的起点不改变访问权限。需要强调private是一种「接口约束」不是安全机制。通过reinterpret_cast把对象当成字节数组、或者用#define private public这种技巧都能绕过它但前者在严格别名规则下是未定义行为后者是未定义行为的另一种形式改变标准库头的语义。正确做法是修改设计而不是绕过访问控制。2.3 protectedprotected的可见范围介于两者之间类自己的成员函数可以访问派生类的成员函数可以访问友元可以访问外部代码不可以访问。但protected有一个非常容易被忽略的限制派生类只能通过「派生类类型或更派生的类型」的对象访问基类的 protected 成员不能通过基类对象访问。例如// 适用于 C17class 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派生类中为 privateprotected成员派生类中为 protected派生类中为 protected派生类中为 privateprivate成员不可访问不可访问不可访问规律可以概括为派生类中的访问级别 min(成员原级别, 继承方式)其中 private protected public基类的 private 成员永远是「不可访问」与继承方式无关。private继承在语义上是「用基类的实现但不暴露基类的接口」常用于实现继承如基于std::vector实现一个栈。它和「成员对象」的区别在于能否利用空基类优化、能否重写虚函数。多数场景下组合把基类作为成员比 private 继承更清晰。protected继承更少见它让基类的 public 接口对派生类的派生类可见但对外部不可见。真正需要它的场景很少遇到时先确认是否可以用组合替代。C 还允许在派生类中用using调整某个名字的访问级别// 适用于 C17class Base {protected:void helper();};class Derived : private Base {public:using Base::helper; // 把 helper 提升为 public};using只能改变访问级别不能改变名字本身它也不能把基类的 private 成员变可见。四、访问控制与虚函数、友元、嵌套类4.1 private 虚函数也能被重写虚函数的调用走动态分派与访问级别无关。基类可以把虚函数设为 private派生类照样能重写通过基类指针调用时也照样分派到派生类实现// 适用于 C17class Shape {public:virtual ~Shape() default;void render() { doRender(); } // 模板方法对外只暴露 renderprivate: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 嵌套类C11 起嵌套类可以访问外层类的所有成员包括 private但外层类不能访问嵌套类的 private 成员。嵌套类与外层类之间没有隐式的this关联它只是一个定义在类作用域内的普通类访问外层非静态成员仍需一个外层对象。常见坑点❌ 以为private成员在派生类里「继承下来了就能用」。✅ 基类 private 成员在派生类中始终不可访问只能通过基类提供的 public/protected 接口间接操作。❌ 在派生类里通过基类引用访问 protected 成员void f(Base b) { b.x 1; }。✅ 只能通过派生类类型或更派生类型的对象访问改成参数类型为Derived或让基类提供 protected 成员函数。❌ 用#define private public来「测试私有成员」。✅ 这是未定义行为且会污染所有包含的头文件测试应通过 public 接口或把待测逻辑抽成可单独测试的组件。❌ 把protected数据成员当作给派生类的「扩展点」。✅ 优先提供 protected 成员函数或 public 虚函数数据成员保持 private避免派生类绑定实现细节。❌ 认为class默认继承是 public。✅class D : B是 private 继承struct D : B才是 public 继承写继承时显式写出继承方式。❌ 以为友元关系会被派生类继承。✅ 友元不继承、不传递需要时在派生类中重新声明。❌ 把using Base::foo;当成「复制」了函数。✅ 它只是把基类的名字引入派生类作用域并调整可见性函数实体仍在基类。❌ 用private继承后又想让外部通过基类指针使用派生对象。✅ private 继承下基类子对象对外不可见Base* p new Derived;在类外不合法需要「is-a」关系就用 public 继承需要复用实现就用组合。总结访问位置public 成员protected 成员private 成员本类成员函数可访问可访问可访问派生类成员函数可访问可访问不可访问友元可访问可访问可访问外部代码可访问不可访问不可访问继承方式对外表现典型用途public派生类 is-a 基类接口继承、多态protected只对进一步派生开放极少使用private基类接口完全隐藏实现复用多数情况应用组合三个访问关键字的核心不是「谁能看」而是「作者愿意承诺什么」。public 是承诺protected 是有限承诺private 是不承诺。把承诺面收得越小类就越容易演进把所有成员都设成 public等于放弃了未来修改实现的自由。理解继承矩阵和 protected 的访问限制能避免大部分「明明继承了却编译不过」的困惑。
阅读完成 · 觉得有帮助?