3. 封装与数据隐藏:把秘密锁进保险箱
封装这个词,听起来挺唬人的。其实说白了,就是「把数据和操作数据的方法打包在一起,然后对外只开放必要的接口」。我刚开始学C++的时候,觉得这东西不就是把变量和函数塞进一个class里吗?有什么了不起的。后来在项目里被坑了几次,才真正明白——封装不是为了炫技,是为了保命。
核心思想:封装 = 数据 + 操作数据的函数 + 访问控制。缺一不可。
3.1 封装的概念与意义
想象一下你有一台咖啡机。你只需要按几个按钮(接口),就能喝到咖啡。你不需要知道内部的水箱怎么加热、咖啡豆怎么研磨、管道怎么清洗。这就是封装——把复杂的内部实现藏起来,只暴露简单的操作方式。
在C++里,我们用class来实现封装。成员变量通常设为private,成员函数设为public。这样外部代码就不能直接修改内部数据,只能通过我们提供的函数来操作。
class CoffeeMachine {
private:
int waterLevel; // 水箱水位,外部不能直接改
int beanAmount; // 咖啡豆量
bool isHeating; // 是否在加热
public:
void makeCoffee() {
// 外部只需要调用这个函数
heatWater();
grindBeans();
brew();
}
private:
void heatWater() { /* 加热逻辑 */ }
void grindBeans() { /* 研磨逻辑 */ }
void brew() { /* 冲泡逻辑 */ }
};
封装的意义在哪?我举个例子。有一次我接手一个老项目,里面有个BankAccount类,余额是public的。结果到处都有代码直接写account.balance -= 100;,连个日志都没有。后来出账对不上,查了三天才发现有个地方少写了一个零。如果当初把balance设为private,只通过withdraw()函数操作,那里面加个日志、加个校验,问题早发现了。
注意:不要把所有成员都设为public。那是C语言struct的做法,不是面向对象。你想想看,如果别人能随便改你的内部数据,那你的类还有什么控制力?
3.2 setter/getter方法
既然数据要隐藏,那外部怎么读写呢?答案就是setter和getter。这两个函数就像看门人——你想拿数据,得经过getter;你想改数据,得经过setter。
class Student {
private:
string name;
int age;
double score;
public:
// getter
string getName() const { return name; }
int getAge() const { return age; }
double getScore() const { return score; }
// setter
void setName(const string& n) { name = n; }
void setAge(int a) {
if (a > 0 && a < 150) {
age = a;
} else {
// 可以抛出异常或记录日志
}
}
void setScore(double s) {
if (s >= 0.0 && s <= 100.0) {
score = s;
}
}
};
我个人习惯是:能不暴露getter就不暴露,能不提供setter就不提供。很多新手喜欢给每个成员变量都配上getter/setter,结果类变成了一个「透明的袋子」,封装形同虚设。你想想看,如果外部代码能直接拿到内部数据,那以后你想改内部实现,所有调用方都得跟着改——这不又回到C语言的老路上了吗?
小技巧:如果某个数据只是内部使用,连getter都不要给。比如缓存计算结果、临时状态等。我见过一个项目,把内部缓存都暴露了,结果外部代码直接读缓存,导致缓存更新策略完全没法改。
3.3 友元函数与友元类
封装有时候会带来一个问题:两个类需要紧密合作,但各自的数据都藏起来了。这时候友元就派上用场了。友元函数或友元类可以访问另一个类的私有成员。
class Matrix; // 前置声明
class Vector {
private:
double data[3];
public:
Vector(double x, double y, double z) : data{x, y, z} {}
friend Matrix multiply(const Matrix& m, const Vector& v);
};
class Matrix {
private:
double m[3][3];
public:
Matrix(double arr[3][3]) {
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
m[i][j] = arr[i][j];
}
friend Matrix multiply(const Matrix& m, const Vector& v);
};
// 友元函数可以访问两个类的私有成员
Matrix multiply(const Matrix& mat, const Vector& vec) {
Matrix result = mat;
for (int i = 0; i < 3; i++) {
double sum = 0;
for (int j = 0; j < 3; j++) {
sum += mat.m[i][j] * vec.data[j];
}
// 这里直接访问了Matrix和Vector的私有成员
}
return result;
}
警告:友元是C++里的一把双刃剑。它破坏了封装性,所以能用成员函数解决的问题,尽量不要用友元。我曾经在一个项目里看到有人把整个类都声明为另一个类的友元,结果两个类耦合得死死的,改一个就得改另一个。记住:友元是特例,不是常规手段。
3.4 静态成员(static)
静态成员属于类本身,而不是某个对象。所有对象共享同一个静态成员。这有点像全局变量,但作用域被限制在类内部。
class GameCharacter {
private:
string name;
int health;
static int totalCharacters; // 所有角色共享
public:
GameCharacter(const string& n) : name(n), health(100) {
totalCharacters++;
}
~GameCharacter() {
totalCharacters--;
}
static int getTotalCharacters() {
return totalCharacters;
}
};
// 静态成员必须在类外定义
int GameCharacter::totalCharacters = 0;
静态成员函数只能访问静态成员,不能访问非静态成员。为什么?因为静态函数没有this指针,它不知道你指的是哪个对象。我刚开始学的时候老犯这个错——在静态函数里直接写name,编译器报错我还一脸懵。
常见用途:
- 统计对象数量(像上面的例子)
- 实现单例模式
- 共享配置信息
- 工具函数(比如数学计算)
3.5 常量成员函数(const)
常量成员函数承诺不会修改对象的状态。在函数声明后面加const关键字,编译器就会帮你检查——如果你在const函数里修改了成员变量,编译器会报错。
class Rectangle {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// const成员函数,不会修改对象
double getArea() const {
return width * height;
}
double getPerimeter() const {
return 2 * (width + height);
}
// 非const成员函数,可以修改
void setWidth(double w) {
width = w;
}
void setHeight(double h) {
height = h;
}
};
void printRectangle(const Rectangle& rect) {
// 这里只能调用const成员函数
cout << "Area: " << rect.getArea() << endl;
// rect.setWidth(10); // 编译错误!const对象不能调用非const函数
}
我个人习惯是:只要不修改成员变量的函数,一律加上const。这不仅是语法要求,更是一种设计承诺。你想想看,如果你写了一个函数参数是const Rectangle&,结果它还能偷偷改数据,那const还有什么意义?
避坑指南:const成员函数里不能修改普通成员变量,但可以修改mutable成员变量。mutable通常用于缓存、锁、引用计数等场景。比如:
class DataCache {
private:
mutable int cacheHitCount = 0; // 即使const函数也能修改
// ...
public:
int getData() const {
cacheHitCount++; // 合法,因为是mutable
// ...
}
};
不过别滥用mutable,它本质上是const的一个后门。
知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到封装是基础,setter/getter、友元、static、const都是围绕封装展开的机制。
本章小结
封装是面向对象的第一道防线。它把数据和操作打包在一起,通过访问控制来保护内部状态。setter/getter提供了受控的访问入口,友元在必要时打破封装,static让成员属于类本身,const则是对调用者的承诺。
记住一句话:好的封装,让使用者不需要知道内部细节,也让实现者可以自由修改内部实现。这两者缺一不可。我在项目里见过太多「封装了个寂寞」的代码——所有成员都是public,或者所有成员都有getter/setter。那样的类,跟C语言的struct有什么区别?
实践建议:
- 默认所有成员变量为private
- 能不提供setter就不提供
- const成员函数能加就加
- 友元是最后的手段,不是首选
- static成员记得在类外定义
公众号:蓝海资料掘金营,微信deep3321