☰
C++访问权限详解:public、private、protected与继承实战
2026/9/29 5:07:50 网站建设 项目流程

在C++的类设计里,访问权限是每个初学面向对象的人必踩的第一道门槛。这个系列我打算把C++知识点做得很细,上一篇聊过类的基本概念,这一篇专门把public、private、protected三个关键字掰开揉碎,从使用规则、继承场景、实际工程用法到面试高频坑,一次性讲透。你不用再去翻那些干巴巴的官方文档,按照这篇文章走一遍,基本就能把权限这部分吃得比较透。

先说结论类的东西:访问权限解决的是"谁能碰我的数据"这个问题。如果没有访问权限的概念,类里的成员变量可以被外部随便改,那封装就成了一句空话。你辛辛苦苦设计的对象内部状态,一个不小心的外部赋值就能给搅乱。所以,理解访问权限不仅仅是应付考试和面试,更是写出健壮代码的基础。

这篇文章适合刚学完C++基础语法、开始接触类的读者,也适合工作了一两年但没系统梳理过访问权限的开发者。我会把权限规则、继承下的变化、工程中的最佳实践,以及那些容易让人懵的边界情况都过一遍。

1. 从封装说起:访问权限为什么重要

先聊聊封装。面向对象三大特性是封装、继承、多态,访问权限就是实现封装的技术手段。所谓封装,就是把你不想让人知道的实现细节藏起来,只对外开放必要的接口。为什么非要藏?举个现实的例子,你买台洗衣机回来,只需要会用面板上的按钮就行,洗衣机的电机、线路板、进水阀这些内部结构都封在壳子里,不允许你随手乱摸。洗衣机把"内部实现"隐藏起来,只暴露"按钮接口",这就是封装。

C++类里的访问权限扮演的正是这个"外壳"的角色。它把类成员分成三类:公开的(public)、私有的(private)、受保护的(protected)。公开的成员相当于洗衣机面板上的按钮,谁都能用;私有的成员相当于线路板和电机,外部不能直接碰;受保护的成员则有点特殊,相当于机身内部的某个调试接口,你自己不能轻易摸,但你的子类可以。

访问权限带来的最直接的好处是提高代码的可靠性。如果类的数据成员全是public,那么任何外部代码都可以直接修改它们。比如你写了一个表示银行账户的类,里面有个余额字段balance,如果它是public的,外面的人可以直接写account.balance = 1000000,把余额改成一百万,完全绕过了你设计好的记账逻辑,这就出大问题了。把balance设为private,再提供setBalance和getBalance之类的接口,就能在设置余额时做校验,保证账户状态永远合法。

另一个好处是方便后期维护。当你把内部实现藏起来,你就有自由去替换它。比如原来你用一个数组存储学生成绩,后来想改成vector,只要对外接口不变,调用方的代码一行都不用动。反之,如果所有成员都公开,改一个成员类型就可能波及全工程。这种"接口稳定、实现自由"的灵活性,就是封装换来的。

还有一点容易被忽略,访问权限也是一种程序员之间沟通的约定。一个类里,标着public的成员,就是在告诉别人"这是你可以安全使用的能力";标着private的成员,就是在说"这块你别碰,里面是我自己的事"。团队协作时,这种信号比什么注释都直观。别人看你的头文件,第一件事就是扫一遍各个成员的访问级别,快速判断哪些能用、哪些不能动。

2. 三个访问级别的逐字拆解

2.1 public:对外开放的接口

public成员是访问限制最宽松的,任何地方都可以通过对象名、指针、引用来访问。类外部的普通函数、其他类、甚至整个程序都能直接用点号或箭头操作符访问。

#include <iostream> #include <string> using namespace std; class Student { public: string name; // public成员,外部可以随便访问 int age; // public成员 }; int main() { Student s; s.name = "张三"; // 直接赋值,合法 s.age = 20; // 直接赋值,合法 cout << s.name << " " << s.age << endl; return 0; }

这段代码里,name和age都是public成员,所以在main函数里可以直接s.name、s.age进行读写。这看起来很省事,但就像我前面说的,这种省事是有代价的。如果你的类只是为了打包几个数据,不涉及任何业务规则,那全部public也未尝不可,比如C语言风格的struct。但一旦你希望类能表达某种约束或者状态,就不应该把所有数据都暴露出来。

public最典型的使用场景是类的接口部分,包括构造函数、析构函数、对外提供的成员函数。用户通过public成员函数来操作对象,而不直接碰底层的成员数据。比如下面的Student类,虽然数据是private的,但通过public的构造函数和getter/setter函数来完成操作,外部依然能正常使用这个类。

2.2 private:完全封装的核心肌肉

private成员是访问限制最严格的,只有类的成员函数以及类的友元(后面会讲)才能访问。类的外部,包括派生类,都不能直接访问private成员。

#include <iostream> #include <string> using namespace std; class BankAccount { private: double balance; // 私有数据,外部不能直接改 public: BankAccount(double initBalance) : balance(initBalance) {} void deposit(double amount) { if (amount > 0) { balance += amount; } } void withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } } double getBalance() const { return balance; } }; int main() { BankAccount account(1000); account.deposit(500); account.withdraw(200); cout << account.getBalance() << endl; // account.balance = 99999; // 这行如果取消注释,编译会报错 return 0; }

在这个例子里,balance被设成private,外部代码尝试account.balance = 99999时,编译器会直接拒绝,报错信息大概是"balance is private within this context"。这其实是一件好事,它从语法层面杜绝了非法修改。存款和取款都经过deposit和withdraw两个函数,里面做了金额校验,所以balance永远不可能被改成负数或者被任意篡改。

这里就牵出一个重要原则:成员变量默认设为private。这不是什么玄学,而是C++社区长期实践下来的共识。数据成员一旦暴露在外面,类的内部逻辑就成了摆设。哪怕你只是在读数据,一旦以后想换存储结构,外部代码就会跟着遭殃。所以,把数据藏在private后面,通过public函数提供访问,是C++类设计的标准姿势。

2.3 protected:给派生类留的"半公开"空间

protected和private的差别只有一个地方:protected成员可以被派生类的成员函数访问,而private成员不能。在类的外部,protected和private一样,都是不可见的。

#include <iostream> using namespace std; class Base { protected: int protectedValue = 100; private: int privateValue = 200; public: int publicValue = 300; }; class Derived : public Base { public: void show() { cout << protectedValue << endl; // 可以访问基类的protected成员 // cout << privateValue << endl; // 这行编译报错,privateValue是基类私有 cout << publicValue << endl; // 可以访问基类的public成员 } }; int main() { Base b; // cout << b.protectedValue << endl; // 编译报错,外部不能访问protected Derived d; d.show(); return 0; }

从这个例子能看出,protected是专门为继承机制设计的。基类想把一些成员留给派生类用,但又不希望被外部世界直接使用,这时候就适合用protected。它在public和private之间提供了一个中间档位:类内部可以用,派生类可以用,外部不行。

那protected用得多吗?说实话,如果基类的数据成员用protected,有时候会带来麻烦。因为protected成员对派生类是可见的,派生类就可以直接修改基类的数据,这又破坏了封装。很多C++老手其实更倾向于把所有数据成员设为private,然后提供protected的成员函数作为"受保护的接口"给派生类调用。关于这个设计倾向,我在第4节会展开说。

2.4 三个级别的对比速查

访问级别类内部派生类内部类外(对象/外部代码)
public可访问可访问可访问
protected可访问可访问不可访问
private可访问不可访问不可访问

这张表是整个访问权限体系的浓缩。背下来不亏,因为不管是写代码、看别人的代码还是面试,它都是最高频的判断依据。记住一个顺序:private < protected < public,能访问的范围逐渐扩大。类内都能访问,区别只在类外和派生类。

还有一点要注意,如果你在类里不写任何访问修饰符,C++的class里的成员默认为private。这一点和struct不同,struct里的成员默认是public。这个区别虽然简单,却是真正写代码时经常让人头疼的点,后面第5节会专门提。

3. 继承场景下,访问权限是怎么变化的

继承是C++面向对象的核心机制之一,而访问权限在继承中会经历一次"再分配"。这个部分坑最多,也最值得花时间理解。

3.1 三种继承方式:public、protected、private继承

继承的时候,类名后面跟着的访问修饰符(public、protected、private)决定了一种特殊的权限映射规则。这里的public/protected/private并不是前面说的"成员级别",而是"继承方式"。

class Base { public: int a = 1; protected: int b = 2; private: int c = 3; }; class PubDerived : public Base { ... }; // public继承 class ProDerived : protected Base { ... }; // protected继承 class PriDerived : private Base { ... }; // private继承

继承方式决定了基类成员在派生类中变成什么访问级别。规则如下:

  • public继承:基类的public在派生类中保持public,基类的protected保持protected,基类的private在派生类中不可直接访问。
  • protected继承:基类的public和protected在派生类中都变成protected,基类的private仍然不可直接访问。
  • private继承:基类的public和protected在派生类中都变成private,基类的private同样不可直接访问。

也就是说,继承方式只会对基类中本来就可见于派生类的成员(public和protected)做降级处理,而基类的private成员无论什么继承方式,都不会被派生类直接访问。

#include <iostream> using namespace std; class Base { public: int a = 1; protected: int b = 2; private: int c = 3; }; class PriDerived : private Base { public: void show() { cout << a << endl; // a在PriDerived中变成private,可以访问 cout << b << endl; // b在PriDerived中变成private,可以访问 // cout << c << endl; // 错误,c是Base的private,不能访问 } }; class SecondLayer : public PriDerived { public: void show2() { // cout << a << endl; // 错误!a在PriDerived里是private,SecondLayer不能访问 // cout << b << endl; // 错误!b在PriDerived里是private,不能访问 } };

这个例子能看出private继承的"切断"效果:基类里的public成员,经过private继承到了PriDerived里成了private,于是再往下继承时,SecondLayer就访问不到它们了。这相当于把基类的能力完全隐藏在下一层里,不再向外传递。private继承在实际开发中不算常用,它的经典应用场景是实现"has-a"关系时避免多重继承的复杂性,但一般新手阶段很少用,我后面会简单提一下。

3.2 直达结论:继承方式对访问级别的影响表

为了清楚,我把上面那段文字整理成表格:

基类成员级别public继承后在派生类中protected继承后在派生类中private继承后在派生类中
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可直接访问不可直接访问不可直接访问

记住这张表,你会发现判断变得很机械:基类public成员每经过一次非public继承,权限就降一档;protected成员经过public继承还是protected,经过其他两种继承就直接变成private,而private成员在派生类里永远碰不到。

3.3 派生类中访问基类成员的实际操作技巧

实际操作中,最常用的是public继承,因为public继承表达的是"is-a"关系,也就是派生类是一种基类。C++标准库里的派生类绝大多数都是public继承。写代码时你主要关心两个问题:第一,派生类的成员函数能否访问基类的某些成员;第二,外部通过派生类对象能否访问基类的某些成员。

先看第二个问题。假设B是A的public派生子类,那么外部代码可以通过B的对象访问A的public成员,但不能访问A的protected成员。这是因为public继承不会改变A的public成员在B里的可见性。假如B是private继承A,那么A的public成员在B里变成private,外部代码就无法通过B对象访问A的public成员了。

再看第一个问题。派生类成员函数能访问基类的public和protected成员,无论继承方式是哪种。除非基类的某个成员本来就是private,那派生类无论如何都碰不到。这是一个很多初学者会弄混的点:把基类的成员设为protected,意思就是"我允许我的子类直接访问这个成员"。

还有一个小技巧,当派生类和基类存在同名成员时,派生类会隐藏基类的同名成员。这时候如果想在派生类里访问基类的被隐藏成员,可以用作用域运算符:Base::func()。这和访问权限没有直接冲突,但和protected的可见性一起,会在实际代码里形成一些隐蔽的坑,建议测试的时候多留意。

4. 实战原则:该怎么选、怎么用

规则讲完,接下来是很多教程不会细讲的部分:实际写代码的时候,到底应该怎么分配访问权限?我的经验是"数据默认私有,接口按需公开,继承尽量用public"。

4.1 数据成员默认放进private

把数据成员设置为private,是C++社区长期实践下来的最佳选择。这样做的直接好处是外部代码无法直接改写数据,所有对数据的修改必须经过你定义的成员函数,你可以在这些函数里加上校验、日志、权限判断等逻辑。

class Time { private: int hour; int minute; public: void setHour(int h) { if (h >= 0 && h <= 23) { hour = h; } else { hour = 0; } } int getHour() const { return hour; } };

这段代码里,hour和minute是private,外部只能通过setHour和getHour来操作。setHour做了合法性校验,把非法数据挡在门外。如果hour是public的,外部随手写一个obj.hour = 25,这个Time对象就从内部烂掉了,而你根本不知道是哪儿改的。所以,哪怕你觉得某个数据成员没什么约束,我也建议先把它设为private,等证明有必要暴露时再开放。

那protected呢?对数据成员来说,protected其实也有风险。因为protected数据可以被派生类直接修改,如果派生类代码写得不够谨慎,同样会破坏基类数据的完整性。而且,一旦派生类直接依赖基类的protected数据成员,基类想改变内部存储结构,所有派生类都得跟着改。所以我的习惯是:数据成员一律private,给派生类提供protected的成员函数来间接访问这些数据。这样兼顾了封装和扩展性。

4.2 构造函数和析构函数的访问级别

构造函数和析构函数默认是public的,因为正常情况下我们得能在外部创建对象、销毁对象。但有一个经典设计模式叫单例(Singleton),它的构造函数就是private的,这样外部无法直接new对象,只能通过一个静态成员函数来获取唯一的实例。

class Singleton { private: Singleton() {} // 构造函数私有 Singleton(const Singleton&) = delete; // 拷贝构造禁用 public: static Singleton& getInstance() { static Singleton instance; // C++11及以后,局部静态变量线程安全 return instance; } }; int main() { // Singleton obj; // 错误,构造函数是private的 Singleton& s = Singleton::getInstance(); // 正确 return 0; }

这个例子很经典,它展示了private不是"不能用",而是用来实现特定设计约束。构造函数私有以后,类的外部就无法随意创建对象,对象的生命周期全部由类自身管理。

析构函数私有的情况也有,但相对少见。如果析构函数是private的,那么在外部就不能直接delete一个栈对象(栈对象生命周期结束时会自动调用析构),一般用于禁止在栈上创建对象,只允许new对象并配合特殊的释放函数。说实话,这种用法面试题里会出现,实际工程里较少,但能让你对访问权限的理解更深一层。

4.3 静态成员与const成员函数的访问权限细节

静态成员变量和静态成员函数同样受访问权限控制。静态成员属于类本身,不依赖对象实例,但它public还是private,决定了外部能不能通过类名直接访问。

class Config { private: static int version_; // 静态私有变量 public: static int getVersion() { return version_; } }; int Config::version_ = 1; // 静态成员定义和初始化 int main() { // cout << Config::version_ << endl; // 错误,version_是private cout << Config::getVersion() << endl; // 正确,通过public静态函数访问 return 0; }

静态成员和普通成员在访问权限上的规则完全一样,区别只在访问方式上,静态成员用类名加作用域运算符访问,普通成员用对象点号或指针箭头访问。

const成员函数也和访问权限有关。一个const成员函数承诺不会修改对象的内部状态,它可以访问类的const和非const成员,但不能调用非const成员函数、也不能修改非const成员变量。这在类设计中是一种"语义约束",它和访问权限是相互配合的:public的const函数既向外开放了查询能力,又保证了不会意外更改对象。

class Circle { private: double radius; public: Circle(double r) : radius(r) {} double area() const { // const成员函数 return 3.14159 * radius * radius; } };

4.4 friend:访问权限的"例外门禁卡"

有时候你会遇到这种情况:某个函数并不是类的成员函数,但它又确实需要访问类的private成员。这时候就可以用friend,也就是友元。友元函数和友元类可以访问这个类的所有成员,包括private和protected。

#include <iostream> using namespace std; class Complex { private: double real; double imag; public: Complex(double r, double i) : real(r), imag(i) {} friend Complex operator+(const Complex& a, const Complex& b); }; Complex operator+(const Complex& a, const Complex& b) { // 可以访问private成员 return Complex(a.real + b.real, a.imag + b.imag); } int main() { Complex c1(1, 2); Complex c2(3, 4); Complex c3 = c1 + c2; return 0; }

友元机制在重载运算符(如<<、>>)的时候特别有用,因为流运算符的左侧操作数是std::ostream,你没法把它变成Complex的成员函数,但又需要它访问Complex的私有数据,这时friend是最直接的解法。不过friend也是一把双刃剑,它会破坏封装性。滥用friend会让类的私有成员暴露给多个外部函数,维护起来比较痛苦。建议只在运算符重载、跨类协作确实必要的场景下使用,不要没事就friend一下。

5. 高频坑位与经典面试题实录

讲完规则和实战,这一节来点更接地气的。我自己教过不少学生,也在代码评审里见过不少踩坑现场。这个部分我挑几个高频问题,你如果能顺清楚,访问权限这块基本就过关了。

5.1 常见编译错误:xxx is private within this context

这个报错信息几乎每个学C++的人都见过。当你试图在外部访问一个private成员时,编译器就会甩出"xxx is private within this context"。这句话的意思很简单:这个成员是私有的,你在当前这个场景(context)里没有权限访问。

我看到很多初学者的反应是:直接把private改成public,让编译通过。这个思路不是不行,但改完之后要想一下:当时为什么把它设计成private?如果是写demo,那问题不大;如果是正规项目,这种拍脑袋的改动很可能引入潜在bug。正确的做法是:

  • 检查是不是应该提供public接口来操作这个成员(getter/setter)。
  • 检查是不是真的需要外部直接访问,还是应该调整设计。
  • 检查是不是应该用friend来开放访问权限。

在Visual Studio或者VS Code配置C++环境后,编译器报错通常会直接定位到出错行和成员声明的位置,你点开错误信息,跟着上下文走,通常一两分钟就能定位到问题。

5.2 struct和class的区别:默认访问权限不一样

这是面试里出现频率极高的问题。struct和class在C++里几乎是一样的,唯一区别就是默认访问权限不同,struct默认是public,class默认是private。

struct Point { int x; // 默认为public int y; // 默认为public }; class Point2 { int x; // 默认为private int y; // 默认为private };

所以如果你写了个class,忘记加public,那么你所有的成员函数和成员变量都是private的,外部代码无法访问。很多新手一开始用class写小项目,写完main函数里一编译就报错,结果发现是忘了在最上面加public:。这种问题用struct就顺理成章,因为struct成员默认public,但如果你要写的是带完整封装的类,还是老老实实用class加private加public,结构清晰得多。

5.3 访问权限和继承方式混合使用时容易犯的错

当访问权限和继承方式混合在一起,判断就变得复杂了。一个常见的错误是认为"基类的private成员可以通过public继承在派生类中变成public",这是不对的。private成员在任何继承方式下都不会被派生类直接访问。能降到public的只有基类的public成员经过public继承,其他情况都会降级。

还有一种常见错误是把基类指针指向派生类时,误以为可以通过基类指针访问派生类的public新成员。实际上,基类指针只能访问基类中定义的public成员,这是静态类型决定的。如果你想通过基类指针调用派生类的新接口,需要做向下转型(dynamic_cast),而且必须保证基类有虚析构函数,否则转型和删除对象都有风险。这是继承和多态的范畴了,和访问权限的交叉点在于:基类指针的"访问能力"和对象的真实类型是两回事,编译器管的是你指针的静态类型。这个知识点面试常考,建议自己写一个小程序验证一下。

5.4 三条实战建议,值得写进你的代码规范里

基于这些经验,我给出三条非常直接的编程建议,你写类的时候照着做,能少踩很多坑:

  1. 成员变量一律private,想改数据就提供public接口。如果确实没必要开放,连public接口都可以不写,别怕代码"太封闭",接口是慢慢长出来的,不是一开始就设计完的。
  2. 想让子类能访问但不想让外部访问的成员,用protected。但protected成员尽量是成员函数,而不是数据成员。
  3. 继承方式不用纠结,99%的场景用public继承。看到protected继承和private继承时,多问一句"这里这么写的目的是什么",如果是代码库里的既有设计,不要轻易改成public继承。

6. 关于访问权限的一些个人体会

说实话,我刚开始学C++的那段时间,总觉得访问权限是件麻烦事,很多人一样,一开始写类是什么都往public里塞,省得想那么多。后来在项目里重构一个老模块,看到那个巨大的全是public成员的类,所有地方都可以随意修改它的内部状态,排查一个数据错乱问题查了整整一天,最后发现是某个调用方直接给一个public计数器赋了负值。从那以后,我对访问权限的态度就变了:它不是阻碍你写代码的麻烦,而是替你挡住无妄之灾的护栏。

C++是一门既给你高度自由、又要求你自律的语言。访问权限就是这套自律规则的基石之一。用好了,代码结构清爽可靠;用不好,项目会慢慢腐烂。希望这篇文章能帮你把这部分基础彻底打牢,后面学继承、多态、设计模式,你会发现所有高级特性都建立在这个简单的权限模型之上。

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

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

立即咨询