ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++访问权限详解:public、private、protected与继承实战

C++访问权限详解:public、private、protected与继承实战 在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里是privateSecondLayer不能访问 // cout b endl; // 错误b在PriDerived里是private不能访问 } };这个例子能看出private继承的切断效果基类里的public成员经过private继承到了PriDerived里成了private于是再往下继承时SecondLayer就访问不到它们了。这相当于把基类的能力完全隐藏在下一层里不再向外传递。private继承在实际开发中不算常用它的经典应用场景是实现has-a关系时避免多重继承的复杂性但一般新手阶段很少用我后面会简单提一下。3.2 直达结论继承方式对访问级别的影响表为了清楚我把上面那段文字整理成表格基类成员级别public继承后在派生类中protected继承后在派生类中private继承后在派生类中publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可直接访问不可直接访问不可直接访问记住这张表你会发现判断变得很机械基类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; // C11及以后局部静态变量线程安全 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默认是publicclass默认是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 三条实战建议值得写进你的代码规范里基于这些经验我给出三条非常直接的编程建议你写类的时候照着做能少踩很多坑成员变量一律private想改数据就提供public接口。如果确实没必要开放连public接口都可以不写别怕代码太封闭接口是慢慢长出来的不是一开始就设计完的。想让子类能访问但不想让外部访问的成员用protected。但protected成员尽量是成员函数而不是数据成员。继承方式不用纠结99%的场景用public继承。看到protected继承和private继承时多问一句这里这么写的目的是什么如果是代码库里的既有设计不要轻易改成public继承。6. 关于访问权限的一些个人体会说实话我刚开始学C的那段时间总觉得访问权限是件麻烦事很多人一样一开始写类是什么都往public里塞省得想那么多。后来在项目里重构一个老模块看到那个巨大的全是public成员的类所有地方都可以随意修改它的内部状态排查一个数据错乱问题查了整整一天最后发现是某个调用方直接给一个public计数器赋了负值。从那以后我对访问权限的态度就变了它不是阻碍你写代码的麻烦而是替你挡住无妄之灾的护栏。C是一门既给你高度自由、又要求你自律的语言。访问权限就是这套自律规则的基石之一。用好了代码结构清爽可靠用不好项目会慢慢腐烂。希望这篇文章能帮你把这部分基础彻底打牢后面学继承、多态、设计模式你会发现所有高级特性都建立在这个简单的权限模型之上。
返回列表