一、封装的概念
封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机整体——也就是将数据和操作数据的函数代码进行有机结合,构成”类”,其中数据和函数都是类的成员。
简单来说,封装就是把抽象阶段分离出来的”是什么”(数据)和”能做什么”(行为)打包到一个 {} 中,变成一个完整的类。
二、封装的实现方式
在 C++ 中,封装通过以下机制实现:
| 机制 | 作用 |
|---|---|
class 关键字 + {} | 划定类的边界,将数据和函数组合在一起 |
访问限定符 public / private | 控制成员的访问权限,区分对外接口和内部实现 |
| 成员函数 | 提供外部与类交互的唯一通道 |
三、示例:时钟类的封装
基于上一节的抽象结果,将时钟的数据和功能封装成一个完整的类:
class Clock
{
private: // 私有成员:外部无法直接访问
int hour;
int minute;
int second;
public: // 公有成员:外部接口
void setTime(int h, int m, int s)
{
if (h >= 0 && h < 24)
hour = h;
else
hour = 0;
if (m >= 0 && m < 60)
minute = m;
else
minute = 0;
if (s >= 0 && s < 60)
second = s;
else
second = 0;
}
void showTime()
{
if (hour < 10) cout << "0";
cout << hour << ":";
if (minute < 10) cout << "0";
cout << minute << ":";
if (second < 10) cout << "0";
cout << second << endl;
}
};Code language: JavaScript (javascript)
分析:
{}限定了类的边界,hour、minute、second三个数据成员和setTime、showTime两个函数成员被封装在Clock类内部。private声明的三个整型数据是类的私有数据,外部代码无法直接读写。例如不能在类外写c.hour = 10;,编译器会报错。public声明的两个函数是类对外的接口,外界只能通过调用setTime()和showTime()与Clock类发生联系。setTime内部包含了对数据的合法性校验(如小时不能超过23),这是封装带来的好处——数据如何被修改、修改时有什么约束,全部由类自己控制。
四、封装的意义
| 方面 | 说明 |
|---|---|
| 数据保护 | 私有数据不能被外部随意修改,只有通过公有接口才能访问,避免非法数据破坏对象状态 |
| 隐藏实现 | 外部只需要知道”能做什么”(接口),不需要知道”怎么做”(内部实现),降低使用难度 |
| 降低耦合 | 类内部实现改变不影响外部调用代码,只要接口不变,外部代码无需修改 |
| 简化使用 | 使用者只需关注公有接口,不必关心类内部复杂的逻辑 |
| 增强安全性 | 配合数据校验,确保对象始终处于合法状态 |
五、代码示例
以下代码演示封装在实际使用中的效果:
#include <iostream>
using namespace std;
class Clock
{
private:
int hour;
int minute;
int second;
public:
void setTime(int h, int m, int s)
{
if (h >= 0 && h < 24)
hour = h;
else
hour = 0;
if (m >= 0 && m < 60)
minute = m;
else
minute = 0;
if (s >= 0 && s < 60)
second = s;
else
second = 0;
}
void showTime()
{
if (hour < 10) cout << "0";
cout << hour << ":";
if (minute < 10) cout << "0";
cout << minute << ":";
if (second < 10) cout << "0";
cout << second << endl;
}
void tick()
{
second++;
if (second >= 60)
{
second = 0;
minute++;
if (minute >= 60)
{
minute = 0;
hour++;
if (hour >= 24)
hour = 0;
}
}
}
};
int main()
{
Clock c;
// 通过公有接口设置时间
c.setTime(25, 61, 70); // 传入非法值
cout << "设置非法时间后:";
c.showTime(); // 输出 00:00:00(被校验修正为默认值)
c.setTime(9, 30, 45);
cout << "设置合法时间后:";
c.showTime(); // 输出 09:30:45
// 模拟走时
for (int i = 0; i < 5; i++)
{
c.tick();
}
cout << "5秒后:";
c.showTime();
// c.hour = 100; // 错误:hour 是 private,外部无法直接访问
return 0;
}Code language: PHP (php)
分析:
- 调用
setTime(25, 61, 70)传入非法值,但由于封装了数据校验逻辑,内部自动修正为0:0:0,对象状态不会被破坏。 c.hour = 100;如果取消注释会编译报错,证明私有数据确实被保护起来了。- 外部代码完全不需要知道时间是如何存储、如何校验的,只需调用接口即可。
六、访问限定符详解
| 限定符 | 类内部 | 类外部(通过对象) | 派生类中 | 说明 |
|---|---|---|---|---|
public | 可访问 | 可访问 | 可访问 | 对外接口 |
private | 可访问 | 不可访问 | 不可访问 | 内部实现细节 |
protected | 可访问 | 不可访问 | 可访问 | 继承相关(后续讲解) |
注意:
class默认访问级别是private(不写限定符时成员为私有)。struct默认访问级别是public(C 语言兼容,不推荐用于面向对象设计)。- 访问限定符可以出现多次,每次改变生效范围直到下一个限定符或类结束。
class Example
{
public:
int a; // a 是公有
private:
int b; // b 是私有
public:
void func1(); // func1 是公有
private:
int c; // c 是私有
};Code language: PHP (php)
七、封装的设计原则
- 数据成员尽量设为
private:除非有充分理由暴露,否则不要公开数据成员。 - 对外接口尽量精简:只暴露必要的操作,隐藏一切可以隐藏的细节。
- 接口设计要稳定:公有接口一旦确定并投入使用,应尽量保持不变,因为修改接口会影响所有调用者。
- 内部实现可以自由修改:只要接口不变,私有成员可以随意增删改,不影响外部代码。
- 在接口中加入必要的参数校验:防止非法数据进入对象内部。