C++从C语言继承而来的struct最初只用于聚合数据,而class作为面向对象扩展引入了封装与继承机制。发展至今,二者在语言标准层面已经高度接近,很多开发者仅凭默认访问权限做选择,却容易忽略它们在对象模型和工程语义上的细微差别。本文将从语法、底层内存布局以及实际工程使用三个维度拆解struct与class的差异,并说明为什么运行时几乎看不到区别。

一、默认访问权限与继承方式是语法层最直观的区别
在不写任何访问修饰符的情况下,struct的成员默认具有public访问级别,而class的成员默认是private。这意味着同样一份成员声明,用struct定义时外部代码可以直接读取和修改,改成class后如果忘记加public就会立刻变成私有成员。这个默认值差异在编写头文件或数据载体时尤其明显,很多封装漏洞正是来源于把class误写成struct却依赖了外部直接访问。
继承场景同样存在默认值差异。struct Derived : Base默认是public继承,基类的公有成员在派生类中仍然对外可见;而class Derived : Base默认是private继承,基类的公有成员会在派生类中变成私有,外部无法通过派生类对象直接访问。这一条经常被忽视,因为部分开发者在class继承时习惯显式写出public,但在阅读遗留代码时,如果遇到不带继承方式的class派生,就需要意识到它是private继承。
struct BaseStruct {
int value;
void show() const {}
};
class BaseClass {
int value;
public:
void show() const {}
};
struct DerivedStruct : BaseStruct { }; // 默认public继承
class DerivedClass : BaseClass { }; // 默认private继承
int main() {
DerivedStruct ds;
ds.value = 10; // 合法,BaseStruct成员public且public继承
ds.show(); // 合法
DerivedClass dc;
// dc.value = 10; // 错误,value在BaseClass中是private
// dc.show(); // 错误,public继承不成立,show在DerivedClass中变为private
return 0;
}除了默认权限和默认继承方式,struct与class在语法能力上没有任何区别。二者都可以声明成员变量、成员函数、静态成员、构造函数、析构函数,也都可以使用public、protected、private三个访问修饰符。换句话说,struct并不是只能当数据容器,它同样可以支持封装、继承和多态。理解这一点可以避免写出类似struct不能有成员函数这一类错误判断。
二、从对象模型看内存布局:编译器眼里没有struct与class之分
C++对象模型通常由各编译器实现决定,但标准要求struct和class在内存布局上不应产生差异。一个对象中非静态数据成员按照声明顺序依次排列,访问权限public、private或protected只存在于编译器的符号表中,不会在运行时占用任何存储空间。换句话说,假设有一个全部public的struct和一个混合访问权限的class,只要非静态数据成员的类型、顺序和数量完全相同,它们的sizeof结果就完全一致。
如果类中声明了虚函数,无论使用struct还是class,编译器都会在对象头部插入一个指向虚函数表的指针,通常称为vptr。这个vptr的生成与否只取决于类本身是否包含虚函数或是否从带虚函数的基类继承,与定义时写的是struct还是class毫无关系。因此从二进制层面看,二者经过编译后没有可区分的标记,对象模型完全统一。
下面这段代码分别用struct和class定义两个成员布局相同的类型,可以观察到它们的大小和地址偏移表现一致。这里class B的成员被声明为private,但private并不影响内存排列,只是禁止外部直接访问。
#include <iostream>
struct A {
int x;
int y;
};
class B {
private:
int x;
int y;
public:
B(int a, int b) : x(a), y(b) {}
};
int main() {
std::cout << sizeof(A) << " " << sizeof(B) << std::endl;
A a{1, 2};
B b(1, 2);
std::cout << &a.x << " " << &a.y << std::endl;
return 0;
}需要留意的是,代码中使用了#include <iostream>以及输出运算符<<,这些在C++里都是正常语法,但在HTML展示时做了转义。真正开发时按标准C++写法即可。通过这个例子可以看出,访问级别的差异不会改变对象在内存中的物理布局,sizeof和成员偏移与关键字无关。
三、模板参数、POD兼容性与工程实践中的选择依据
在模板编程中,class和typename可以互换使用,template<class T>与template<typename T>完全等价,但注意这里不能写成template<struct T>。模板参数声明中的class并不限制传入类型必须是类,普通int、指针甚至函数类型都可以作为模板实参。这一历史用法有时会让初学者困惑,但本质上只是关键字复用,跟struct和class的对象差异没有关系。
POD(Plain Old Data)概念在C++11之后被细分为trivial和standard layout两类。过去开发者习惯用struct表示与C语言兼容的纯数据集合,因为struct默认成员public,写起来更简单。但现代标准下,是否满足POD条件与使用struct还是class无关,关键在于没有虚函数、没有非平凡构造函数、没有非平凡析构函数,以及成员权限不影响布局。因此一个简单的class同样可以满足标准布局,只是工程上通常会约定struct表示数据聚合,class表示具有行为和封装的逻辑对象。
工程实践中有一条常见约定:如果类型只承载数据,成员全部公开且没有复杂行为,就使用struct;如果需要封装状态、控制访问、提供接口,就使用class。这样可以让阅读代码的人快速判断类型定位,也能减少因默认private导致的意外编译错误。但这条约定只是风格问题,并不影响编译结果。真正需要警惕的是把C接口与带虚函数的struct混合使用,那会破坏内存布局兼容性,带来未定义行为。
综上,struct与class的差异集中在默认访问权限和默认继承方式上,在对象模型、内存布局、虚函数支持、模板使用等层面完全一致。理解这一点后,选择关键字时可以更多从可读性和团队规范出发,而不是纠结于运行期性能差异。对于面试和代码评审,也能准确说出底层原因,避免停留在表面。