那时候我们只讲内存
我最早学 C 的时候。
老师不爱讲“对象”。
他只讲内存。
讲地址。
讲 malloc。
然后拍着黑板说。
“结构体就是一段连续的字节。”
这句话听起来粗。
但它很诚实。
后来我开始写 C++。
同样的结构体。
突然就能有函数了。
还能有 private。
还能在析构里干活。
新同学一兴奋。
就容易把它当成魔法。
我一般会把手按住。
先把魔法擦掉。
只留一句。
对象。
先是一块内存。
其它都是你和编译器谈出来的“合同”。
C 的 struct:一段连续的字节
先从 C 的世界开始。
你写一个很朴素的结构体。
struct Point {
int x;
int y;
};
这东西在内存里长什么样。
大多数平台上。
就是两个 int 挨着放。
你甚至能用偏移量去“摸”它。
#include <cstddef>
static_assert(offsetof(Point, x) == 0);
static_assert(offsetof(Point, y) == sizeof(int));
别急着纠结对齐。
对齐我们下一篇专门聊。
你现在只要记住感觉。
这就是一段字节。
有一个类型名字。
叫 Point。
类型的意义。
是告诉编译器。
这段字节里。
第 0 个位置开始。
按 int 读。
再往后。
再按 int 读。
C 的套路:数据和操作分开
然后 C 程序员会干一件很熟练的事。
把“数据”和“操作”分开。
double length(Point p) {
return std::sqrt(double(p.x) * p.x + double(p.y) * p.y);
}
数据在 struct 里。
操作在函数里。
函数拿到一份 Point。
就可以算。
这套写法。
很适合早年的 C 工程。
文件是文件。
函数是函数。
大家靠约定维护边界。
靠代码评审盯“不要乱改字段”。
你说它原始。
它也确实能跑。
只是容易跑偏。
C++ 的第一步:把函数挪到 struct 旁边
到 C++。
Bjarne 当年干了一件挺“实用主义”的事。
他没有发明一种新机器。
他只是说。
既然你们总在写“数据 + 一堆函数”。
那我让它们挨在一起。
struct Point {
int x;
int y;
double length() const {
return std::sqrt(double(x) * x + double(y) * y);
}
};
你看到的是“成员函数”。
编译器看到的。
更像是这个。
double Point_length(const Point* self) {
return std::sqrt(double(self->x) * self->x + double(self->y) * self->y);
}
这不是标准规定的形式。
但它很接近真实的实现思路。
成员函数。
本质是普通函数。
外加一个隐藏参数。
也就是那根经常被你忽略的 this。
所以我才说。
对象先是一块内存。
成员函数不住在对象里。
住在代码段。
你对象的 sizeof。
只受数据成员影响。
你加十个成员函数。
对象不会变胖。
除非你引入了额外的隐藏成员。
比如虚函数那根 vptr。
那是后面的故事。
struct 和 class:只差默认访问权限
讲到这里。
struct 到 class。
很多人以为是“两个不同的东西”。
其实它们真没那么戏剧。
它们的唯一区别。
就是默认访问权限。
struct S {
int x;
};
class C {
int x;
};
S::x 默认是 public。
C::x 默认是 private。
就这么点。
标准就这么写。
别的都是习惯。
但工程世界里。
习惯往往比标准更能决定你会不会挨骂。
为什么团队会开始怀念 private
我见过不少团队。
喜欢“全员 struct”。
理由也很直。
少打几个字。
然后某天。
有人把一个成员从 int 改成了 int64_t。
顺手改了序列化。
忘了改校验。
线上一晚上。
你都在追一个奇怪的负数。
这时候你会开始怀念 private。
不是因为它优雅。
是因为它能把“修改成本”显式化。
你想改字段。
你就得走接口。
你得想一秒。
这就是 class 在团队协作里的真实价值。
它不是“面向对象”。
它是“别乱来”。
一点护栏:把口号写成代码
给你一个最小的例子。
我们做一个范围合法的百分比。
class Percent {
public:
explicit Percent(int v) {
if (v < 0) v = 0;
if (v > 100) v = 100;
v_ = v;
}
int value() const { return v_; }
private:
int v_;
};
如果这是一个 struct。
大家很容易写成。
Percent p{150};
p.v_ = 150;
然后你所有的“范围保证”。
就变成了一句口号。
用 class。
你就把口号写成了代码。
新人没记住也没关系。
编译器会替你拦。
这就是我说的“合同”。
对象还是那块内存。
只是你开始用类型系统。
给它加了一圈护栏。
把生命周期写进合同:RAII 的口气
再往下。
C++ 还会让你把生命周期也写进合同。
在 C 里。
你经常能看到这种组合拳。
struct File {
int fd;
};
File open_file();
void close_file(File*);
写错一次 close。
你就会在某次压力上来时付账。
到 C++。
你会更愿意让对象自己收尾。
class File {
public:
explicit File(int fd) : fd_(fd) {}
~File() { if (fd_ != -1) ::close(fd_); }
private:
int fd_ = -1;
};
你看。
还是一块内存。
里面放了一个 int。
但它现在带着规则。
带着边界。
带着“离开作用域就收工”的纪律。
老程序员喜欢它。
不是因为浪漫。
是因为它能少写几页事故复盘。
最后带走一句话
所以。
这一篇你如果只带走一句话。
就带走这句。
C++ 的对象。 本质还是内存布局。 class 只是让你更容易把规则绑在这块内存上。
你理解了这一点。
你再去看拷贝。
看移动。
看“深拷贝还是浅拷贝”。
看 Rule of 0/3/5。
就不会只背结论。
你能顺着内存去推。
也能顺着合同去评审。
下一篇。
我们就继续从这块内存往下挖。
为什么 sizeof 经常比你想的大。
为什么一个“看起来很瘦”的对象。
会被对齐和填充撑胖。