(二) 封装与访问控制
学生记录能保存成绩后,还要阻止外部写入 -1 或 101。先用公开方法检查范围,再说明哪些成员应允许外部直接访问。
封装是把数据及其操作组织起来,通过接口维护对象的约束。例如成绩只能在 0 到 100 之间,就让修改成绩的方法统一检查范围。
| 权限 | 类内部 | 派生类成员 | 类外普通代码 |
|---|---|---|---|
public |
可访问 | 可访问 | 可访问 |
protected |
可访问 | 可以按保护成员规则访问 | 不可直接访问 |
private |
可访问 | 不可直接访问 | 不可直接访问 |
如果希望某个外部函数或类也能访问非公开成员,可以把它声明为友元。普通成员访问仍按上表中的权限进行。
这里维护的对象不变量是 0 <= score_ <= 100。 成员先用已学的默认初值设为 0;每次修改只允许通过 setScore 检查范围。因此,只经公开接口操作的对象始终保持合法成绩。
例如当前成绩为 85,调用 setScore(90) 会变成 90;调用 setScore(-1) 则先抛出异常,没有执行后面的赋值,成绩保持修改前的值。调用者需要处理异常,否则异常继续向外传播。private 限制的是访问途径,不会自动判断成绩是否合法,setScore 中的判断负责检查成绩数值。
本例用到的异常接口: <stdexcept> 的 invalid_argument 表示参数不满足约定;throw invalid_argument("...") 抛出带文字说明的异常,可用 catch(const invalid_argument& error) 捕获,再用 error.what() 读取说明。判断在赋值之前,因此抛出异常不会写入非法成绩。
// 数据成员设为 private,修改成绩统一经过 setScore 的范围检查。
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class Student {
private:
string name_;
int score_ = 0; // 对象创建时先给合法初值。
public:
void setName(const string &name) {
name_ = name; // 先用已经学过的赋值,不要求先掌握构造函数。
}
void setScore(int score) {
if (score < 0 || score > 100) {
throw invalid_argument("成绩必须在 0 到 100 之间");
}
score_ = score;
}
int score() const {
return score_;
}
const string &name() const {
return name_;
}
};
int main() {
Student student;
student.setName("Alice");
student.setScore(85);
cout << student.name() << ' ' << student.score() << '\n';
}外部不能直接写 student.score_ = -1,只能调用公开接口。name() 返回的引用仍依赖 student 的生命周期,不能在对象销毁后继续使用。