CPP 访问修饰符

PointY Lv2

三个关键字

C++ 提供三个访问修饰符来控制类成员的可访问范围:

修饰符类内部派生类外部
public
protected
private

class vs struct 默认访问级别

class 默认 privatestruct 默认 public,这是二者唯一的语义差异:

1
2
3
4
5
6
7
class Foo {
int x; // private
};

struct Bar {
int x; // public
};

访问控制 ≠ 可见性

一个常见误解:private 成员对外部”不可见”。实际上,private 成员参与名称查找,只是在访问检查阶段被拒绝:

1
2
3
4
5
6
7
8
9
10
class A {
private:
void f(int) {}
public:
void f(double) {}
};

A a;
a.f(42); // 错误

编译器的执行流程是:

  1. 名称查找,找到所有名为 f 的候选函数
  • f(int) (private)
  • f(double) (public)
  1. 重载决议
    从候选中选最佳匹配。42int 字面量。
  • f(int) 是精确匹配
  • f(double) 需要 int→double 隐式转换
    所以选用 f(int)
  1. 访问检查
    检查选中的 f(int) 是否可访问。
    它是 private,外部不可访问,编译报错。

注意:编译器不会回退到重载决议重新选定,一旦重载决议选定了最佳匹配,流程就锁定了。

继承中的访问收窄

继承方式决定基类成员在派生类中”暴露”给外界的上限:

基类成员public 继承protected 继承private 继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
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
27
28
29
30
31
32
33
34
35
36
37
38

class Base {
public:
void pub_func() {} // public
protected:
void prot_func() {} // protected
private:
void priv_func() {} // private,子类永远不可访问
};

// public 继承:原样保留
class PubChild : public Base {
// pub_func() → public
// prot_func() → protected
};

// protected 继承:public 被收窄为 protected
class ProtChild : protected Base {
// pub_func() → protected ↓ 收窄了!
// prot_func() → protected(不变)
};

// private 继承:全部收窄为 private
class PrivChild : private Base {
// pub_func() → private ↓ 收窄了!
// prot_func() → private ↓ 收窄了!
};

// 实际影响:
int main() {
PubChild a;
ProtChild b;
PrivChild c;

a.pub_func(); // OK — 仍然是 public
b.pub_func(); // 编译错误!已被收窄为 protected
c.pub_func(); // 编译错误!已被收窄为 private
}

规则:取基类成员原级别继承方式中更严格的一个。

using 调整访问级别

派生类可以用 using 声明将继承来的成员在当前类中重新设定访问级别:

1
2
3
4
5
6
7
8
9
class Base {
protected:
void helper();
};

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

注意:using 只能恢复或提升到成员在基类中的原始级别,不能突破基类已有的限制。

protected 的微妙规则

派生类只能通过自身类型(或更下层派生类型)访问基类的 protected 成员,不能通过基类引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
protected:
int x;
};

class Dog : public Base {};

class Cat : public Base {
void foo(Base& b, Cat& c) {
x = 1; // OK:通过 this
c.x = 2; // OK:通过 Cat 类型(同族)
b.x = 3; // 错误:b 背后可能是 Dog,Cat 无权修改 Dog 的数据
}
};

设计意图:protected 意味着”信任子类管理自己的那份数据”,而非”信任子类管理所有兄弟的数据”。如果允许通过 Base& 访问,Cat 就能修改毫无关系的 Dog 的 protected 成员。

private virtual 与 NVI 模式

private 不影响 override 能力。基类可以声明 private virtual 函数,派生类可以覆写但不能直接调用。

一个经典的应用是 Non-Virtual Interface(NVI)模式。公开接口是非虚的,虚函数藏在 private 中,基类掌控调用的前后逻辑。

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
void execute() { doWork(); } // 公开接口
private:
virtual void doWork() = 0; // 实现点
};

class Impl : public Base {
private:
void doWork() override { /* ... */ }
};

friend:突破封装边界

friend 声明授予指定的函数或类对当前类所有 private / protected 成员的完全访问权。

friend 函数

friend 函数不是类的成员,但被类主动授权访问 private / protected 成员。控制权在类手里——类自己决定信任谁。

1
2
3
4
5
6
7
8
9
10
class Account {
friend void transfer(Account&, Account&, int);
private:
int balance_;
};

void transfer(Account& from, Account& to, int amount) {
from.balance_ -= amount; // OK:被授权了
to.balance_ += amount;
}

什么时候必须用 friend 函数?——成员函数做不到的时候:

左操作数不是当前类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Vec2 {
public:
// a op b 这种二元运算,写成成员函数,永远是左操作数调用
// a * b → a.operator*(b)
Vec2 operator*(double s) const { return {x_*s, y_*s}; }
// Vec2.operator*(2.0)

// 2.0 * v 左操作数是 double,double 是内置类型,你不可能给它加成员函数
// 自由函数 operator*(double, Vec2),编译器会在非成员函数中查找匹配。但这个自由函数要访问 Vec2 的 private 成员,所以需要 friend。
friend Vec2 operator*(double s, const Vec2& v) {
return {s * v.x_, s * v.y_};
}

// operator<< 同理,左操作数是 ostream
friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << "(" << v.x_ << ", " << v.y_ << ")";
}
private:
double x_, y_;
};

编译器对自由函数的查找分两路进行,以 2.0 * v 为例:

  1. 普通查找(Unqualified Lookup)
    从调用点所在作用域逐层向外找名为 operator* 的函数:
    当前块作用域 → 外层函数 → 命名空间 → 全局作用域

  2. ADL(Argument-Dependent Lookup,参数依赖查找)
    看参数类型属于哪个命名空间,去那里额外搜一遍:

    • 2.0 是 double(内置类型),无关联命名空间,跳过
    • v 是 Vec2,去 Vec2 所在命名空间搜索
      → 找到通过 friend 声明注入的 operator*(double, const Vec2&)
  3. 合并候选 → 重载决议 → 访问检查(与成员函数流程一致)

这就是为什么在类体内定义的 friend 函数(hidden friend)不需要在命名空间里显式声明也能被调用——ADL 会根据参数类型找到它。

同时访问两个类的 private

1
2
3
4
5
6
7
8
9
10
11
12
13
class B;
class A {
friend void sync(A&, B&);
private:
int data_;
};
class B {
friend void sync(A&, B&);
private:
int data_;
};

void sync(A& a, B& b) { b.data_ = a.data_; } // 两边的 private 都能碰

friend 类

friend class X 让 X 的所有成员函数都获得访问权。用于两个类紧密协作的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LinkedList {
friend class Iterator;
private:
struct Node { int data; Node* next; };
Node* head_ = nullptr;
};

class Iterator {
public:
explicit Iterator(LinkedList::Node* p) : cur_(p) {} // 能访问 private Node
int& operator*() { return cur_->data; }
private:
LinkedList::Node* cur_;
};

friend 关系的三个性质

  • 不对称:A friend B 不意味着 B friend A
  • 不传递:A friend B、B friend C,C 访问不了 A
  • 不继承:基类的 friend 不是派生类的 friend

第三条容易踩坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
friend class Inspector;
private:
int base_secret_;
};

class Derived : public Base {
private:
int derived_secret_;
};

class Inspector {
void inspect(Derived& d) {
d.base_secret_ = 1; // OK:Inspector 是 Base 的 friend
// d.derived_secret_ = 1; // 错误!Inspector 不是 Derived 的 friend
}
};

friend 与模板

模板每个实例化是独立的类,Foo<int>Foo<double> 互相就是陌生人。想访问对方的 private 就得声明 friend。

不同类型参数的同一模板互访——最典型的场景是智能指针的隐式类型转换,就像裸指针 Derived* 能隐式转为 Base* 一样:

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
template<typename T>
class SharedPtr {
template<typename U>
friend class SharedPtr; // SharedPtr<Base> 和 SharedPtr<Derived> 互相可见

public:
explicit SharedPtr(T* p) : ptr_(p), ref_count_(new int(1)) {}

// 从 SharedPtr<Derived> 隐式转换到 SharedPtr<Base>
template<typename U>
SharedPtr(const SharedPtr<U>& other) : ptr_(other.ptr_),
ref_count_(other.ref_count_) {
// other 是 SharedPtr<U>,和 SharedPtr<T> 是不同的类
// 没有 friend 就访问不了 other.ptr_ 和 other.ref_count_
++(*ref_count_);
}

private:
T* ptr_;
int* ref_count_;
};

struct Base {};
struct Derived : Base {};

SharedPtr<Derived> d(new Derived);
SharedPtr<Base> b = d; // 隐式转换,需要访问 SharedPtr<Derived> 的 private

另一个模板的所有实例为 friend——用于序列化、工厂等通用工具类:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Container {
template<typename U>
friend class Serializer; // 任意 Serializer<X> 都能访问任意 Container<Y>
private:
T* data_;
size_t size_;
};

// Serializer<JSON> 能访问 Container<int> 的 data_
// Serializer<XML> 能访问 Container<string> 的 data_

friend 的粒度问题

friend 是全有或全无的——无法只对某个方法开放部分成员。当你只想授权特定操作时,friend 过于粗暴。解决方案见最后一节的 Passkey 模式。

嵌套类与 lambda

嵌套类的访问权

C++11 起,嵌套类对外围类的 private 成员拥有完全访问权:

1
2
3
4
5
6
7
8
9
10
class Outer {
private:
int secret_ = 42;

class Inner {
void peek(Outer& o) {
return o.secret_; // OK since C++11
}
};
};

反过来不成立:外围类不能访问嵌套类的 private 成员(除非声明 friend)。

lambda 与访问权限

lambda 定义在成员函数内时,继承该成员函数的访问权限:

1
2
3
4
5
6
7
8
class Widget {
private:
int data_ = 10;
public:
auto makeGetter() {
return [this]() { return data_; }; // OK:lambda 在成员函数内
}
};

lambda 并非类的 friend,而是作为成员函数体的一部分,自然具有相同的访问能力。

细粒度访问控制惯用法

语言本身的访问控制粒度有限,以下惯用法弥补这一不足。

Passkey 模式

通过一个只有授权方能构造的”钥匙”类型来限制调用。比 friend class 好在:friend 打开整个类的全部 private,Passkey 只锁住特定入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
class CreationKey {
friend class WidgetFactory;
CreationKey() = default;
};
Widget(int id, CreationKey) : id_(id) {} // 公开构造函数,但只有工厂能调用
private:
int id_;
};

class WidgetFactory {
public:
Widget create(int id) { return Widget(id, Widget::CreationKey{}); } // OK
};

// Widget w(1, Widget::CreationKey{}); // 编译错误:CreationKey 构造函数是 private

PIMPL 与访问隔离

Pointer to Implementation 将私有成员完全移出头文件,从编译层面隔离访问。Qt 框架大量使用此模式(Q_D 宏),核心价值:开发SDK或框架时,隐藏具体实现;在大型项目中,修改一个底层类的private不会触发依赖方重编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// widget.h — 公开头文件,调用方只看到这些
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
struct Impl;
std::unique_ptr<Impl> pimpl_;
};

// widget.cpp — 实现细节完全藏在 cpp 里
struct Widget::Impl {
int internal_state;
std::string cache;
};

效果:

  • 调用方头文件中完全看不到实现细节
  • 修改 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 进行许可。
评论