1. 先聊清楚为什么C里会有两个长得一样的东西很多刚学C的朋友看到struct和class都会有一个疑问这俩写起来几乎一模一样能定义成员变量能写成员函数能继承能搞多态那要两个关键字干嘛我刚入门那会儿也纠结过这个问题甚至一度觉得struct就是C语言的遗留产物早晚会被淘汰。后来写了几年代码踩了些坑才慢慢理解Stroustrup当年为什么要这么设计。简单一句话概括struct和class在C里几乎是同一个东西唯一被语言标准硬性规定的差别只有默认访问权限和默认继承权限。一个是public一个是private。就这么点差别。但就是这点差别衍生出了两套完全不同的编码习惯和工程约定。这篇文章我打算把这两个关键字从语言规则、编译器底层、实际工程用法、踩坑经验四个层面全部讲透读完你应该能在任何项目里做出清晰的选择。适合谁看如果你是刚开始学C、正在写数据结构课设的学生这篇文章能帮你避开很多低级错误如果你已经写过一些项目但团队里对struct和class的使用没有统一规范这篇文章可以给你一个可以落地的判断标准如果你是从C语言转过来的那更值得看因为C语言里的struct和C里的struct其实已经不是一回事了。我先摆明一个态度不要把它们当成两种对立的类型而是当成同一个类型系统的两种表达风格。C标准里从来没说过struct不能有虚函数、不能继承、不能封装这些都是坊间流传的误解。下面我们一层层拆。1.1 Struct和Class的表面对比先把两者能用和不能用的东西列出来你一眼就能看到它们几乎是重叠的。下面这张表是我自己总结的写代码的时候经常拿出来给新人看特性structclass定义成员变量支持支持定义成员函数支持支持构造函数/析构函数支持支持运算符重载支持支持继承支持支持虚函数与多态支持支持模板成员支持支持静态成员支持支持访问控制符 public/private/protected支持支持默认成员访问权限publicprivate默认继承方式publicprivate看到没除了最后两行其他完全一致。也就是说struct根本不是简化版的class它就是一个默认权限不同的class。你完全可以把struct写成带虚函数、带多态、带封装的复杂类型编译器不会拦你。同样你也可以把class写成只有几个public成员变量的纯数据容器也没人拦你。那为什么还要分两个关键字因为语义约定。语言给了你两个词是希望你用它们表达不同的设计意图。struct传递的信号是这是一坨数据成员之间是公开的、聚合的class传递的信号是这是一个有行为、有封装、有不变式约束的对象。编译器不强制但读代码的人会依赖这个信号。1.2 从C语言到C的历史演化逻辑要理解这个设计得回到C语言。C语言里的struct只做一件事把几个变量打包在一起方便整体传递。比如struct Point { int x; int y; };这种结构体里不能有函数不能有访问控制纯粹是内存布局的描述。C程序员习惯直接访问成员p.x 10;天经地义。C在C的基础上扩展需要引入类这个概念来表达封装、继承、多态。如果直接复用struct关键字那么所有C代码的语义都会变——原来所有成员都是公开的现在默认变成private那C代码移植到C就全炸了。Stroustrup的做法很聪明保留struct的C语义默认为public引入新的class关键字表达OOP语义默认为private。这样C代码可以平滑过渡新代码也能用上新特性。所以struct不是遗留而是兼容性的桥梁。它的存在让C和C能共享同一套类型定义。这也是为什么你在系统头文件比如POSIX、Windows API里看到的几乎都是struct——因为那些接口要保证C编译器也能识别。提示C的struct和C的struct在语言规则上已经不同了C的struct可以有成员函数。但如果你写的头文件要同时给C和C用那里面只能放C风格的struct不能有成员函数和访问控制。2. 编译器视角Struct和Class在底层到底差在哪搞清楚了历史我们从编译器角度看看它们到底有没有性能差异、内存布局差异。这个部分对性能敏感的场景特别重要因为网上有一种流传很广的说法struct比class快、struct没有虚表开销。这纯属胡说我们拆开看。2.1 唯一默认差别默认访问权限语言标准里明文规定的差别只有两条第一条是默认成员访问权限struct S { int a; // 默认 public void f(); // 默认 public }; class C { int a; // 默认 private void f(); // 默认 private };注意是默认不是只能。你在struct里可以写private:在class里可以写public:完全合法。所以如果你足够无聊可以写出这样的代码struct S { private: int secret; public: int get() const { return secret; } };这种写法语法上没问题但语义上会让人困惑——你用了struct却做封装读代码的人会愣一下。所以风格一致性比语法合法性更重要。2.2 继承时的默认权限差异第二条差别是默认继承方式struct Derived : Base { }; // 默认 public 继承 class Derived : Base { }; // 默认 private 继承这个坑非常隐蔽。我带过的一个实习生曾经写过class Base { public: void f(); }; class Derived : Base { }; // 注意这里默认是 private 继承 int main() { Derived d; d.f(); // 编译错误 }他纳闷为什么f()明明是public却调不到原因就是class的默认继承是private导致基类的public成员在派生类里变成private。改成class Derived : public Base就好了。这种错误编译器会报但新手经常看不懂报错信息。我的建议是无论用struct还是class继承时都显式写public/protected/private别依赖默认值。2.3 内存布局上它们其实是一回事这是最关键的结论如果两个类型的成员声明完全相同、访问控制符提供的可见性相同、继承方式显式写清楚那么编译器生成的struct和class在内存布局、对象大小、对齐方式、虚表结构上完全一致。也就是说下面两个类型在二进制层面是等价的struct A { public: int x; double y; virtual void f(); }; class B { public: int x; double y; virtual void f(); };sizeof(A)和sizeof(B)一定相等它们的虚表指针位置、成员偏移都一样。所以任何说struct比class性能好的言论都是错的。性能差异只取决于你的成员是什么、有没有虚函数、对齐怎么安排跟用哪个关键字无关。注意真正影响内存布局的是成员类型、访问控制符切换的时机、虚函数的有无以及继承链的结构。比如你把成员从public改成private对象大小不会变但你加一个虚函数对象里就多一个虚表指针64位下8字节。3. 实战场景什么情况下用Struct什么情况下用Class语言规则讲完了接下来是我觉得最有价值的部分工程里到底怎么选。这个问题没有标准答案但有一些被广泛验证的约定。我把它总结为数据用struct对象用class下面展开。3.1 用Struct的典型场景第一种场景是纯数据聚合。比如坐标、尺寸、颜色、配置项这类东西它们的主职责就是装数据没有复杂的行为没有需要维护的约束条件。用struct最合适struct Point2D { double x; double y; }; struct Color { uint8_t r, g, b, a; }; struct HttpConfig { std::string host; int port; int timeoutMs; };这些类型的特点是所有成员都是公开的用户可以随意读写没有不变式需要维护比如半径必须大于0这种约束没有需要隐藏的实现细节。用struct能让调用方直接访问字段代码简洁。第二种场景是与C接口交互。当你需要把数据传给C库、做系统调用、或者做网络协议序列化时struct是天然选择。因为C只认struct而且这些场景下数据必须是PODPlain Old Data类型不能有虚函数、不能有自定义构造。比如struct PacketHeader { uint32_t magic; uint16_t version; uint16_t length; };这种类型可以直接memcpy、可以直接写文件、可以直接跨语言解析用class反而多余。第三种场景是函数返回值打包。当函数需要返回多个值时用一个struct打包比std::tuple可读性好得多struct ParseResult { bool success; std::string message; int errorCode; };调用方写result.success比std::get0(result)清楚一百倍。第四种场景是模板元编程中的trait类型。STL里大量的std::is_integral、std::iterator_traits这类东西都是struct因为它们就是承载类型信息的空壳没有任何行为。3.2 用Class的典型场景第一种场景是有封装需求的对象。当你的类型有内部状态需要保护有不变式需要维护有实现细节不想暴露时用class。比如一个银行账户class BankAccount { public: explicit BankAccount(double initial) : balance_(initial) {} void deposit(double amount) { if (amount 0) throw std::invalid_argument(amount must be positive); balance_ amount; } double balance() const { return balance_; } private: double balance_; };这里的balance_是private外面不能直接改必须通过deposit这就保证了余额不会变成负数这个不变式。如果用struct谁都能account.balance_ -100约束就丢了。第二种场景是有继承层次的类型。当你想表达是一种的关系需要多态、需要虚函数时用class。虽然struct也能继承但语义上class更符合我设计了一个类族的意图class Shape { public: virtual ~Shape() default; virtual double area() const 0; }; class Circle : public Shape { public: explicit Circle(double r) : r_(r) {} double area() const override { return 3.14159 * r_ * r_; } private: double r_; };第三种场景是生命周期需要管理的类型。如果你的类型在构造时申请资源、在析构时释放资源也就是RAII用class更合适因为你需要精确控制构造和析构的行为class FileHandle { public: explicit FileHandle(const std::string path); ~FileHandle(); FileHandle(const FileHandle) delete; FileHandle operator(const FileHandle) delete; private: FILE* fp_; };第四种场景是对外发布的库API。当你写一个给别人用的库时用class能更好地控制接口边界。你可以随时修改private成员而不影响用户代码因为用户的编译单元里看不到你的实现细节配合Pimpl惯用法更是如此。3.3 混合使用与团队规范怎么定现实项目里不可能非黑即白。我一般遵循这样的判断顺序如果这个类型只有public数据成员、没有成员函数或者只有极少数简单工具函数用struct。如果这个类型需要维护任何不变式、需要隐藏任何成员、需要继承、需要多态用class。如果是POD类型或者要和C交互用struct。如果拿不准用class因为它的默认更保守暴露出去的东西少将来改造余地大。团队里最重要的是统一。我见过有的团队规定所有类型都用class也见过所有类型都用struct需要封装时才切class。两种都能跑怕的是混合没有标准同一个文件里一会儿struct一会儿class读起来乱。我自己比较推荐的是Google C Style Guide那一派的思路用struct表示被动数据对象用class表示有行为的对象。判断标准是这个类型有没有需要维护的不变式。有就是class没有就是struct。简单、可执行。4. 踩坑实录与常见问题排查这部分是我这些年真正踩过的坑也是我觉得比官方文档更有价值的部分。很多问题在语法书上查不到只有写过才能遇到。4.1 新手最容易犯的5个错误错误一以为struct不能有构造函数。struct Point { int x, y; Point(int a, int b) : x(a), y(b) {} // 完全合法 };很多人从C转过来以为struct只能做聚合初始化其实C的struct可以有构造、析构、运算符重载随便写。错误二在struct里写了private却以为是public。struct S { int a; // public private: int b; // private public: int c; // public };一旦出现private:或protected:标签后面所有成员都受影响直到下一个访问控制符。这不是struct特有的class也一样但struct里因为默认public容易让人放松警惕。错误三class默认private继承导致基类public成员不可见。前面讲过这里再强调一次class Derived : Base等价于class Derived : private Base基类的public成员会变成private。想保留可访问性必须显式写public。错误四聚合初始化在class上失败。class Config { int port; // private }; Config c{8080}; // 编译错误不是聚合类型聚合初始化Type obj{a, b, c}这种要求类型是聚合体没有用户声明的构造函数、没有private/protected非静态成员、没有虚函数、没有虚基类。class默认成员private所以通常不是聚合体不能这么初始化。如果确实想用聚合初始化用struct或者把成员改成public。错误五把一个类型一会儿当struct一会儿当class。见过最离谱的代码是同一个头文件里前面声明struct Foo;后面定义class Foo {...};。这在不同编译器上行为可能不一致有的报错有的警告。前向声明和定义必须用同一个关键字。4.2 几个容易被忽略的细节细节一struct可以有虚函数也可以有虚析构。struct Base { virtual ~Base() default; // 合法 virtual void run() 0; };没有任何理由说struct不能有虚函数。只是工程上不推荐这么写因为语义不清。但语法完全支持。细节二空struct也占空间。struct Empty {}; std::cout sizeof(Empty); // 输出1不是0C要求每个对象有唯一的地址所以空类型至少占1字节。这是很多面试题会问的点。细节三class可以写在全public风格。class Vec3 { public: double x, y, z; };这种写法和struct Vec3在二进制层面完全一样唯一的区别是如果你后续添加成员忘了写public它会默认private。所以从安全角度class的默认更保守改代码时不容易误暴露。细节四模板参数里的class和struct不表示类或结构体。templateclass T void f(); templatetypename T void f(); // 等价这里的class关键字只是历史原因和类没关系和struct也不冲突。别混淆。4.3 常见问题速查表问题现象可能原因解决方式d.f()调用不到基类public函数class默认private继承显式写public Base聚合初始化报错类型不是聚合体改用struct或添加public构造前向声明后定义报错关键字不一致声明和定义用同一关键字成员改不了class默认private显式加public:标签sizeof比预期大对齐填充或虚表指针检查对齐、调整成员顺序、排查虚函数结构体放进容器后字段被改直接public暴露改用class封装5. 更进一步从Struct/Class延伸到工程实践讲完基础差别和场景选择我想再往工程深处走一步。因为实际项目里struct和class的选择往往不是孤立的它和序列化、跨语言交互、模板编程这些话题绑在一起。5.1 涉及序列化、JSON、跨语言交换时的选择当你需要把类型序列化成JSON、Protobuf、或者传给其他语言时struct通常是更自然的选择。原因是这类场景需要透明的数据布局所有字段能被反射、能被遍历、能被无差别访问。struct UserDto { std::string name; int age; std::string email; };用nlohmann/json这种库序列化时你可以写一个简单的转换函数把字段一个个取出来。如果字段是private你就得给每个字段加getter序列化代码会变得很啰嗦。但这里有个坑要提醒别用裸指针或裸数组当struct成员去做序列化。因为指针序列化出去毫无意义加载回来指向的是野地址。跨语言传输要用定长整型uint32_t、int64_t字符串用std::string但要明确编码数组用std::vector或者连续内存。另外如果你在做网络协议或者嵌入式通信结构体的内存对齐会直接影响字节流布局。这时候要在结构体外包一层#pragma pack或者用alignas明确指定对齐#pragma pack(push, 1) struct WireHeader { uint32_t magic; uint16_t version; uint16_t length; }; #pragma pack(pop)不加pack的话编译器可能在这几个字段之间插入填充字节接收方按相同结构体解析就会错位。我第一次做协议解析时就踩过这个坑发送端解析正常接收端读到的length字段全是乱码排查了一整天才发现问题出在结构体对齐上。5.2 与const、模板、STL的结合struct和class在与现代C特性结合时表现也不一样。先说const。对于一个纯数据struct你经常需要传const引用避免拷贝void draw(const Point2D p);而对于有封装行为的classconst的语义更重要因为它牵扯到哪些成员函数可以修改状态。你需要把不改状态的函数标成constclass Counter { public: int value() const { return value_; } // 不改状态 void increment() { value_; } // 改状态 private: int value_ 0; };如果忘了标const一个const Counter就没法调用value()代码会报错。这是新手常见的困惑点。再说模板和STL。当你的类型要放进std::vector、std::map、std::unordered_map时需要考虑拷贝、移动、哈希、比较这些操作。如果是struct通常自己写构造函数、比较运算符就行如果是class还要考虑封装后怎么优雅地提供这些操作。struct Key { int id; std::string name; bool operator(const Key other) const { return id other.id name other.name; } }; namespace std { template struct hashKey { size_t operator()(const Key k) const { return std::hashint()(k.id) ^ std::hashstd::string()(k.name); } }; }注意上面那个std::hashKey的特化我用了template struct因为标准库的hash本身就允许用struct表达这也侧面说明了struct在纯工具类型上的地位。5.3 工程中值得坚持的几条准则写到这里我把这些年总结的几条准则分享一下都是我实际项目里坚持执行的第一默认POD用struct有行为用class。判断标准不是哪个语法更短而是这个类型有没有需要保护的内部状态。没有就用struct有就用class。第二继承一律显式写访问控制符。不管基类是struct还是class我都写public或protected从不依赖默认避免踩默认private继承的坑。这个习惯帮我在review代码时省了不少时间。第三接口边界上的类型一律用class。如果这个类型会暴露给其他模块或者作为库的公开API用classprivate成员可以随时改用户代码不受影响。struct适合内部使用不适合长期对外承诺。第四序列化和跨语言数据一律用struct并明确对齐。避免封装、避免虚函数、避免指针成员保证内存布局可控。第五团队代码风格要统一。这不是语法问题是协作问题。一个文件里混杂两种风格会让维护者分心也容易在重构时出错。我们团队的做法是在代码规范文档里明确写清楚新人入职第一周就要读。最后再分享一个我自己的习惯每当我犹豫一个类型该用struct还是class时我会先问自己——如果将来这个类型需要添加一个private成员改动会不会很大如果答案是不大那就直接上class因为它更保守如果答案是根本不需要private那就struct。这个判断方法虽然简单但这些年用下来从没让我后悔过。真正难的不是语法规则而是在具体场景里做出和团队、和项目节奏一致的选择这需要时间积累也需要踩够坑。