C++类和对象之封装详解

发布时间:2026/10/26 18:40:06

C++类和对象之封装详解 前言封装encapsulation常被简化成「成员变量设为 private再给每个变量配一对 getter/setter」。这不叫封装只是把公有数据换了个写法类的内部表示仍然暴露在接口上调用方照样可以随时把余额改成负数、把年龄改成三百岁类也无法保证自己始终处于合法状态。封装的真正内容有三层。第一层是访问控制用private、protected、public决定谁能看到什么第二层是不变量invariant也就是「一个对象在任何时刻都必须满足的条件」由成员函数负责维持第三层是接口与实现的分离调用方只依赖稳定的行为不依赖内部数据结构这样内部换成别的容器、换一种算法时调用方不用改代码。本文按这三层展开先讲访问控制的实际规则再讲如何用行为接口守住不变量最后讲返回内部数据、友元、Pimpl 这些容易破坏封装的细节并给出一个完整的账户类示例。示例以 C17 为准。一、访问控制与 class/struct 的区别类体里可以出现三种访问说明符public成员对所有人可见protected成员对本类及派生类可见private成员只对本类以及声明为友元的函数或类可见。访问控制是在编译期检查的不影响对象布局也不带来运行开销。class和struct的唯一语言层面区别是默认访问级别class的成员默认privatestruct默认public继承时默认的继承方式也同样不同。所以「struct 没有封装、class 才有」是错的struct里完全可以写private:也可以有成员函数、构造函数和虚函数。惯例上只承载数据、没有不变量的聚合体用struct需要维护不变量的类型用class但这只是风格约定。还有一个常见误区是以为private能防住一切。它对编译器可见的代码有效但挡不住reinterpret_cast、指针运算、#define private public这类手段。封装的目标不是对抗恶意代码而是让「正常写代码的人不容易误用」并在误用时尽量在编译期报错。二、封装的核心是维护不变量不变量是对象在任何公开操作结束后都必须成立的条件。比如「余额永远不小于零」「矩形的宽高永远为正」「链表节点的next要么为空要么指向本链表中的节点」。一旦不变量被破坏后续所有成员函数的假设都会失效bug 会扩散到离原因很远的地方。维护不变量靠三件事构造函数建立不变量非法参数直接拒绝不要构造出半成品对象。每个修改状态的成员函数在入口校验参数在出口保证不变量仍然成立。不把内部表示以可写方式交出去否则类无法在事后检查。以账户为例balance_设为private只提供deposit和withdraw两个行为。withdraw先检查金额为正再检查余额是否足够两步都通过才修改。调用方无法写出「余额减到负数」这种状态因为根本没有直接写余额的入口。这也解释了为什么「给每个字段配 setter」是反模式。setBalance(int v)和公有字段没有本质区别它把校验责任推给了调用方。正确的做法是按业务动作设计接口不是「设置余额」而是「存款」「取款」「转账」每个动作自己负责校验和记账。三、接口与实现的分离接口是调用方能看到的东西public 成员函数的签名、公有的类型别名、public 继承关系。实现是私有的数据成员、私有辅助函数、包含的头文件。分离得好内部换个容器不需要重新编译调用方二进制接口也更稳定。最直接的做法是「不要返回内部数据的可写视图」。返回std::vectorint等于把整个容器交给调用方对方可以clear()、可以push_back()类完全失控。需要读取时返回const std::vectorint需要让调用方遍历时提供begin/endconst 版或者一个接受回调的forEach确实要交出所有权时返回值拷贝。友元friend是封装的有意破口。运算符重载、序列化函数、测试夹具有时确实需要访问私有成员这时用友元比加一堆 public 访问器更干净。但友元关系写在类内部一旦加多说明接口设计可能缺了某个操作。经验法则是能用公开成员函数实现的功能就不要用友元能写成非成员非友元函数的操作就不要写成成员函数因为非成员函数不会增加类的接口面积。当私有实现需要包含大量头文件时可以用 Pimplpointer to implementation把实现藏到一个不完整类型后面。头文件里只留一个std::unique_ptrImpl真正的数据成员定义在源文件中。代价是多一次指针间接访问以及需要显式管理析构、拷贝和移动。四、完整示例下面的BankAccount把余额和户名设为私有只通过行为接口修改构造函数负责建立不变量存款时检查溢出取款时检查余额。// 适用于 C17#include iostream#include limits#include stdexcept#include string#include utilityclass BankAccount {public:BankAccount(std::string owner, long long cents): owner_(std::move(owner)), balance_(cents) {if (owner_.empty()) {throw std::invalid_argument(owner must not be empty);}if (cents 0) {throw std::invalid_argument(initial balance must not be negative);}}const std::string owner() const noexcept { return owner_; }long long balanceCents() const noexcept { return balance_; }void deposit(long long cents) {if (cents 0) {throw std::invalid_argument(deposit must be positive);}if (balance_ std::numeric_limitslong long::max() - cents) {throw std::overflow_error(balance overflow);}balance_ cents;}void withdraw(long long cents) {if (cents 0) {throw std::invalid_argument(withdraw must be positive);}if (cents balance_) {throw std::runtime_error(insufficient funds);}balance_ - cents;}void transferTo(BankAccount other, long long cents) {withdraw(cents);try {other.deposit(cents);} catch (...) {balance_ cents; // 回滚保持两边总额不变throw;}}private:std::string owner_;long long balance_;};int main() {BankAccount a(alice, 10000);BankAccount b(bob, 500);a.transferTo(b, 2500);std::cout a.owner() : a.balanceCents() \n;std::cout b.owner() : b.balanceCents() \n;try {a.withdraw(1000000);} catch (const std::runtime_error e) {std::cout failed: e.what() \n;}return 0;}这段代码的要点如下。构造函数按值接收std::string再std::move到成员避免多一次拷贝参数校验放在成员初始化之后、函数体之内一旦抛异常已构造的成员会按逆序析构不会泄漏。deposit在加法之前检查上界因为long long的有符号溢出是未定义行为不能等溢出发生后再判断。transferTo先取款再存款如果存款因溢出失败就把取出的金额加回去并重新抛出保证两个账户的总额不变这是最基本的异常安全处理真实系统还需要事务或日志来应对进程中途崩溃。owner()返回const std::string而不是std::string调用方只能读不能改。如果返回可写引用调用方就能把户名改成空串构造函数里的校验形同虚设。balanceCents()按值返回long long因为它是算术类型拷贝成本可以忽略按值返回还能避免调用方持有内部引用。五、Pimpl 的骨架当头文件不想暴露实现细节时可以这样写// 适用于 C17 —— Engine.h#include memoryclass Engine {public:Engine();~Engine();Engine(const Engine) delete;Engine operator(const Engine) delete;Engine(Engine) noexcept;Engine operator(Engine) noexcept;void run();private:struct Impl;std::unique_ptrImpl impl_;};Impl只在这里声明不定义它的完整定义放在Engine.cpp里源文件才包含那些沉重的头文件。析构函数必须在Engine.cpp中定义因为编译器生成默认析构时需要Impl是完整类型而头文件里它不是。拷贝操作被删除移动操作显式声明并需要在源文件里定义同样是因为移动unique_ptr会触发Impl的析构。常见坑点坑点一把字段全设 private却给每个字段配 getter/setter。❌void setBalance(long long v) { balance_ v; }调用方仍然可以随意设置非法值。✅ 按业务动作设计接口例如deposit、withdraw由接口负责校验。坑点二getter 返回内部数据的非 const 引用。❌std::vectorint items() { return items_; }调用方可以清空或重排。✅const std::vectorint items() const需要修改时提供专门的成员函数。坑点三构造函数不校验留下半成品对象。❌Account(std::string name, int age) : name_(name), age_(age) {}允许空名字和负年龄。✅ 在构造函数体内检查参数非法时抛异常让对象要么完整存在、要么根本不存在。坑点四以为 struct 就没有封装。❌ 认为只有class才能写private把所有聚合体都改成class。✅struct同样可以有访问说明符和成员函数选择哪个只取决于默认访问级别和表达意图。坑点五友元加得太多。❌ 每个需要读私有成员的函数都声明成friend类的私有状态实际上全公开。✅ 优先提供公开的只读接口确实需要突破封装时写成非成员非友元函数并只依赖公开接口。坑点六Pimpl 的析构函数写在头文件里。❌ 头文件里写~Engine() default;而Impl在此处不完整编译报错。✅ 头文件只声明~Engine();在源文件中Engine::~Engine() default;。坑点七转账只做一半异常时总额改变。❌withdraw(cents); other.deposit(cents);存款抛异常后钱凭空消失。✅ 捕获异常并回滚已取出的金额或者把整个操作放进事务。总结层面手段目的访问控制private、protected、public限制可见范围不变量构造校验、修改前检查、修改后保持对象始终合法接口设计行为接口而非字段访问器调用方不依赖内部表示数据暴露返回 const 引用或值防止外部绕过校验实现隐藏Pimpl、源文件内定义降低编译依赖友元少量、必要时使用有意识地破口封装不是把字段藏起来就完事而是让「对象始终处于合法状态」成为类自己的责任。判断一个类封装得好不好可以看它的公开接口里有没有可以直接写入内部状态的入口以及构造完成后是否还存在非法组合。把这两点守住内部结构换成什么都不会影响调用方。
延伸阅读

更多相关文章

2026/10/26 18:40:06

金融科技安全实战:IAST技术在商业银行的应用与优化

1. 金融科技安全实战背景解析商业银行作为金融体系的核心枢纽,其信息系统安全直接关系到国民经济稳定和客户资产安全。近年来随着移动支付、开放银行等创新业务模式的快速发展,传统安全防护体系面临三大挑战:一是敏捷开发模式下安全测试滞后&…

2026/10/26 18:40:06

解决Spring Boot+MyBatis+PostgreSQL类型映射问题

1. 问题现象与背景分析最近在整合Spring Boot MyBatis PostgreSQL技术栈时,遇到了一个典型的类型映射问题:执行SQL查询时抛出PSQLException: 错误: 无法确定参数 $1 的数据类型。这个错误表面上看是PostgreSQL无法推断出SQL语句中参数的类型&#xff0…

2026/10/26 18:40:06

基于LabVIEW的四轮独立驱动扭矩主动分配测控系统

简介:「基于LabVIEW的汽车扭矩主动分配测控系统」源文件包取自全国虚拟仪器大赛参赛作品,面向学习虚拟仪器、车辆电子控制与扭矩分配算法的学生和工程技术人员,可用于理解数据采集、信号处理、实时控制与界面展示如何在LabVIEW中串联起来并实…

2026/10/26 20:36:00

为Codex装上可视化仪表盘:让AI编码过程不再黑盒

说实话,用Codex跑任务的那段时间,我做的最多的一件事就是盯着终端窗口发呆。看着日志一行行刷过去,我经常忍不住问自己:它现在到底在干什么?分析到第几步了?改了哪几个文件?测试到底过没过&…

2026/10/26 20:36:00

HBA-Transformer混合架构在工业故障预测中的应用

1. 项目概述:HBA-Transformer混合架构的创新实践去年在做一个工业设备故障预测项目时,我遇到了传统Transformer模型在小样本场景下表现不稳定的问题。经过大量文献调研和实验验证,最终将蜜獾算法(HBA)的优化特性与Transformer结合&#xff0c…

2026/10/26 20:36:00

让AI活着:生产环境稳定性工程实践指南

“AI活着”这句话,我琢磨了好几个月。去年我把一个RAG对话系统从Demo推到生产环境,上线头两天一切正常,第三天晚上一次流量抖动,服务容器反复重启,模型接口连续超时,用户问了一句话:“你还在吗&…

2026/10/26 20:36:00

vLLM推理引擎架构与优化实践

1. vLLM推理引擎架构全景透视vLLM作为当前大模型推理领域的热门开源引擎,其核心价值在于通过创新的内存管理和调度机制,显著提升GPU利用率并降低推理延迟。我在实际部署Llama2-70B和Qwen等百亿参数模型时,单A100节点上的吞吐量相比传统方案提…

2026/10/26 20:36:00

3D透明材质特效字体设计与实现全攻略

1. 透明材质特效字体的视觉魅力在数字设计领域,3D透明材质特效字体正成为视觉表达的新宠。这种设计风格通过材质透明度和光线折射的巧妙组合,创造出晶莹剔透的视觉效果,让文字仿佛由玻璃、水晶或冰块构成。不同于传统的立体字设计&#xff0c…

2026/10/26 20:30:58

数据流分析:从控制流图到编译器优化实战

你写下一行赋值,之后再没读过那个变量,编译器却一声不吭地把它删了;你写了一个 if 分支,两边算出来的其实是同一个常量,它直接把分支拍平成一条直线;你在循环里留了个累加器,只在调试打印时用过…

2026/10/26 0:20:20

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/26 1:53:47

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/26 0:20:17

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/26 0:00:48

Nested Learning:解决AI灾难性遗忘的分层记忆架构

1. 为什么AI需要"记忆"能力?在传统深度学习模型中,我们常遇到一个根本性问题:模型在处理新任务时,往往会完全遗忘之前学到的知识。这种现象被业界称为"灾难性遗忘"(Catastrophic Forgetting)。想象一下&#…

2026/10/26 0:00:48

企业级大模型应用:OCR预处理、Dify编排与DeepSeek落地实战

1. 这不是“用大模型”,而是重构工作流的起点“大模型的应用和工具”——这八个字现在被贴在无数技术分享、招聘JD、内部立项书的标题栏里,但绝大多数人点进去后看到的,是零散的API调用示例、几个开源UI截图、或者一句“接入LLM就能降本增效”…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑