面向对象三要素:封装、继承、多态
面向对象编程有三个核心特性,通常称为"三要素":
- 封装:将数据和操作数据的方法绑定在一起,对外隐藏实现细节
- 继承:子类复用父类的属性和行为,实现代码复用与层次化设计
- 多态:同一接口在不同对象上有不同的实现,运行时动态绑定
三者的关系:封装是基础,继承是手段,多态是目的。下面分别展开。
封装
为什么需要封装
先看面向过程的写法。以牛郎织女的故事为例,每个行为都需要通过参数明确指定执行者和对象:
qianNiu("HongNiang", "XiaoHeBian"); // 红娘将牛牵到小河边 zhuangChe("HongNiang", "FangChe"); // 红娘将纺车装车 searchNiu("NiuLang"); // 牛郎寻找牛问题很明显:函数内部需要处理各种人物和地点的判断,行为主体不明确,数据也没有保护——任何人都可以随意修改牛郎的牛。
封装的实现
封装的核心思想是将数据和操作数据的方法绑定在类中,对外只暴露必要的接口。
- 成员变量(属性)通常设为
private,保护内部状态 - 成员函数(行为)设为
public,作为对外接口 - 外部必须通过类提供的方法访问属性,方法内部可以添加校验逻辑
#include <iostream> #include <string> using namespace std; class NiuLang { private: string m_niu; // 牛,私有保护 public: NiuLang(const string& niu) : m_niu(niu) {} void searchNiu() { cout << "牛郎寻找自己的牛" << endl; } string getNiu() { // 通过公开方法访问私有属性 return m_niu; } }; int main() { NiuLang niuLang("黄牛"); niuLang.searchNiu(); // 行为主体明确:就是牛郎在找牛 string niu = niuLang.getNiu(); return 0; }封装的优势
- 数据安全:牛郎的牛不能被外部随意修改,必须通过类提供的方法访问
- 调用简单:调用者不需要关心内部实现,只需要知道接口怎么用
- 便于维护:内部实现修改不影响外部调用,只要接口保持不变
继承
继承的概念
继承允许子类复用父类的属性和行为,同时可以添加自己特有的功能。
在 C++ 中,子类可以继承父类的public和protected成员,但private成员无法直接访问——注意,私有成员仍然存在于子类对象的内存中,只是从语法层面不可访问。
一个直观的类比:基因遗传相当于公共成员继承,生来就有;而知识技能相当于私有成员,需要后天学习获得,不能直接继承。
继承的实现
#include <iostream> using namespace std; class Person { public: void meeting() { cout << "参加社交活动" << endl; } void getMarried() { cout << "结婚仪式" << endl; } private: void secretMethod() {} // 子类无法访问 }; class NiuLang : public Person { // 公有继承 public: void searchNiu() { cout << "寻找牛" << endl; } // 子类特有方法 }; class ZhiNv : public Person { public: void spin() { cout << "操作纺车" << endl; } }; int main() { NiuLang nl; nl.meeting(); // 继承自 Person nl.getMarried(); // 继承自 Person nl.searchNiu(); // 自有方法 ZhiNv zn; zn.meeting(); // 同样继承自 Person zn.spin(); // 自有方法 return 0; }NiuLang和ZhiNv都继承了Person的meeting()和getMarried(),不需要重复编写,同时各自添加了特有的行为。
继承的内存布局
子类对象包含完整的父类子对象,再加上子类新增的成员:
sizeof(Person); // 父类成员占用的空间 sizeof(NiuLang); // 父类空间 + 子类新增成员空间即使父类的私有成员在子类中不可访问,它们仍然占据内存空间。这也是为什么sizeof子类通常大于sizeof父类。
多态
多态的三要素
多态是面向对象最强大的特性,实现它需要三个条件:
- 存在继承关系
- 父类包含虚函数(用
virtual关键字声明) - 子类重写父类虚函数(用
override标记)
多态的实现
通过父类指针或引用指向子类对象,调用虚函数时会执行子类的版本,这就是动态绑定——运行时才确定调用哪个实现。
#include <iostream> using namespace std; class Person { public: virtual void introduce() { // 虚函数 cout << "我是普通人" << endl; } }; class NiuLang : public Person { public: void introduce() override { // 重写,override 关键字可校验签名 cout << "我是牛郎" << endl; } }; class ZhiNv : public Person { public: void introduce() override { cout << "我是织女" << endl; } }; void sayHello(Person* p) { // 父类指针,可接收任意子类 p->introduce(); // 动态绑定:调用实际对象的版本 } int main() { NiuLang nl; ZhiNv zn; sayHello(&nl); // 输出:我是牛郎 sayHello(&zn); // 输出:我是织女 return 0; }sayHello函数只依赖Person接口,不需要知道传入的是哪个子类。新增子类时,sayHello完全不需要修改——这就是多态带来的扩展性。
纯虚函数与抽象类
如果父类的虚函数没有默认实现,可以声明为纯虚函数:
virtual 返回类型 函数名() = 0; // 纯虚函数- 包含纯虚函数的类称为抽象类,不能实例化对象
- 派生类必须实现纯虚函数,否则派生类也会成为抽象类
抽象类的作用是定义接口规范,强制子类实现特定行为。
override 关键字
override显式标记一个函数是重写父类虚函数,编译器会检查函数签名是否匹配。如果签名写错(比如参数类型不对),编译器会直接报错,避免因拼写错误导致多态失效。
小结
面向对象三要素各有分工:
- 封装解决数据安全和代码组织问题,是面向对象的基础
- 继承解决代码复用问题,是实现多态的前提
- 多态解决扩展性问题,是面向对象的最终目的
三者结合,才能构建出可扩展、可维护、可读性好的大型软件系统。学习时建议按"封装 → 继承 → 多态"的顺序逐步深入,每学一个就写一个小例子验证。