CPP 访问修饰符
三个关键字
C++ 提供三个访问修饰符来控制类成员的可访问范围:
| 修饰符 | 类内部 | 派生类 | 外部 |
|---|---|---|---|
public | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✗ |
private | ✓ | ✗ | ✗ |
class vs struct 默认访问级别
class 默认 private,struct 默认 public,这是二者唯一的语义差异:
1 | class Foo { |
访问控制 ≠ 可见性
一个常见误解:private 成员对外部”不可见”。实际上,private 成员参与名称查找,只是在访问检查阶段被拒绝:
1 | class A { |
编译器的执行流程是:
- 名称查找,找到所有名为 f 的候选函数
f(int) (private)f(double) (public)
- 重载决议
从候选中选最佳匹配。42是int字面量。
f(int)是精确匹配f(double)需要int→double隐式转换
所以选用f(int)。
- 访问检查
检查选中的f(int)是否可访问。
它是private,外部不可访问,编译报错。
注意:编译器不会回退到重载决议重新选定,一旦重载决议选定了最佳匹配,流程就锁定了。
继承中的访问收窄
继承方式决定基类成员在派生类中”暴露”给外界的上限:
| 基类成员 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
1 |
|
规则:取基类成员原级别与继承方式中更严格的一个。
using 调整访问级别
派生类可以用 using 声明将继承来的成员在当前类中重新设定访问级别:
1 | class Base { |
注意:using 只能恢复或提升到成员在基类中的原始级别,不能突破基类已有的限制。
protected 的微妙规则
派生类只能通过自身类型(或更下层派生类型)访问基类的 protected 成员,不能通过基类引用:
1 | class Base { |
设计意图:protected 意味着”信任子类管理自己的那份数据”,而非”信任子类管理所有兄弟的数据”。如果允许通过 Base& 访问,Cat 就能修改毫无关系的 Dog 的 protected 成员。
private virtual 与 NVI 模式
private 不影响 override 能力。基类可以声明 private virtual 函数,派生类可以覆写但不能直接调用。
一个经典的应用是 Non-Virtual Interface(NVI)模式。公开接口是非虚的,虚函数藏在 private 中,基类掌控调用的前后逻辑。
1 | class Base { |
friend:突破封装边界
friend 声明授予指定的函数或类对当前类所有 private / protected 成员的完全访问权。
friend 函数
friend 函数不是类的成员,但被类主动授权访问 private / protected 成员。控制权在类手里——类自己决定信任谁。
1 | class Account { |
什么时候必须用 friend 函数?——成员函数做不到的时候:
左操作数不是当前类
1 | class Vec2 { |
编译器对自由函数的查找分两路进行,以 2.0 * v 为例:
普通查找(Unqualified Lookup)
从调用点所在作用域逐层向外找名为 operator* 的函数:
当前块作用域 → 外层函数 → 命名空间 → 全局作用域ADL(Argument-Dependent Lookup,参数依赖查找)
看参数类型属于哪个命名空间,去那里额外搜一遍:- 2.0 是 double(内置类型),无关联命名空间,跳过
- v 是 Vec2,去 Vec2 所在命名空间搜索
→ 找到通过 friend 声明注入的 operator*(double, const Vec2&)
合并候选 → 重载决议 → 访问检查(与成员函数流程一致)
这就是为什么在类体内定义的 friend 函数(hidden friend)不需要在命名空间里显式声明也能被调用——ADL 会根据参数类型找到它。
同时访问两个类的 private
1 | class B; |
friend 类
friend class X 让 X 的所有成员函数都获得访问权。用于两个类紧密协作的场景:
1 | class LinkedList { |
friend 关系的三个性质
- 不对称:A friend B 不意味着 B friend A
- 不传递:A friend B、B friend C,C 访问不了 A
- 不继承:基类的 friend 不是派生类的 friend
第三条容易踩坑:
1 | class Base { |
friend 与模板
模板每个实例化是独立的类,Foo<int> 和 Foo<double> 互相就是陌生人。想访问对方的 private 就得声明 friend。
不同类型参数的同一模板互访——最典型的场景是智能指针的隐式类型转换,就像裸指针 Derived* 能隐式转为 Base* 一样:
1 | template<typename T> |
另一个模板的所有实例为 friend——用于序列化、工厂等通用工具类:
1 | template<typename T> |
friend 的粒度问题
friend 是全有或全无的——无法只对某个方法开放部分成员。当你只想授权特定操作时,friend 过于粗暴。解决方案见最后一节的 Passkey 模式。
嵌套类与 lambda
嵌套类的访问权
C++11 起,嵌套类对外围类的 private 成员拥有完全访问权:
1 | class Outer { |
反过来不成立:外围类不能访问嵌套类的 private 成员(除非声明 friend)。
lambda 与访问权限
lambda 定义在成员函数内时,继承该成员函数的访问权限:
1 | class Widget { |
lambda 并非类的 friend,而是作为成员函数体的一部分,自然具有相同的访问能力。
细粒度访问控制惯用法
语言本身的访问控制粒度有限,以下惯用法弥补这一不足。
Passkey 模式
通过一个只有授权方能构造的”钥匙”类型来限制调用。比 friend class 好在:friend 打开整个类的全部 private,Passkey 只锁住特定入口。
1 | class Widget { |
PIMPL 与访问隔离
Pointer to Implementation 将私有成员完全移出头文件,从编译层面隔离访问。Qt 框架大量使用此模式(Q_D 宏),核心价值:开发SDK或框架时,隐藏具体实现;在大型项目中,修改一个底层类的private不会触发依赖方重编译。。
1 | // widget.h — 公开头文件,调用方只看到这些 |
效果:
- 调用方头文件中完全看不到实现细节
- 修改 Impl 不触发依赖方重编译
- 比 private 更彻底——连类型信息都不暴露
- 标题: CPP 访问修饰符
- 作者: PointY
- 创建于 : 2026-09-02 00:26:11
- 更新于 : 2026-09-02 13:31:52
- 链接: https://siyuhong.github.io/2026/09/02/cpp-modifier/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。