最近在帮几个准备校招的朋友做C语言面试复盘,发现一个特别有意思的现象:十份简历里至少有八份写了"熟悉C/C++",但真到面试官问一句"struct和class到底有什么区别",很多人就卡住了。有的能答上"class默认private,struct默认public",再深一层就开始含糊。这个问题看起来基础,实际上能挖出你对C语言内存模型、C++对象模型、访问控制、甚至是POD类型理解得扎不扎实。我把这些年积累的面试经验和实际项目里踩过的坑梳理了一下,希望能帮你把这条线彻底理清楚。
这篇文章适合三类人:正在准备C/C++岗位面试的应届生、从C转C++但一直没细想过这个区别的开发、以及需要用C语言做嵌入式或系统级开发的朋友。我会从两种语言的设计初衷讲起,再落到代码层面的细节对比,最后结合真实项目讲讲struct和class该怎么选、怎么用不出问题。
1. 从一道高频面试题说起:struct在C和C++里的定位差异
1.1 为什么面试官爱问这道题
这道题之所以高频,是因为它像一面照妖镜,能快速筛掉"背了八股文但没写过项目"的人。
面试官问"struct和class的区别",表面考的是语法,实际考的是三件事:第一,你有没有真正理解C语言中struct的原始设计意图——它就是一个"数据的容器",不携带行为。第二,你能不能说清楚C++引入class之后,struct这个关键字为什么被保留下来,以及保留之后它和class在语言层面到底被拉近了多大距离。第三,你能否把"默认访问权限"这种零散知识点,串成对"封装、继承、多态"三大面向对象特性的整体理解。
我见过一个很典型的回答:候选人说"struct是C语言的,class是C++的,class能继承,struct不能"。这个回答错了一半。struct在C++里完全可以继承,甚至可以实现多态。如果你的认知还停留在"struct只是C的遗留物",那面试官基本可以判断你对C++对象模型没有系统学习过。
1.2 C语言里struct的本质:它只是内存布局的说明书
在纯C语言环境里,struct的核心作用非常朴素:把若干个不同类型的变量打包成一个整体,方便统一管理和传递。
从底层看,struct定义的是"内存布局规则"。比如:
c复制struct Point {
int x;
int y;
};
这个定义告诉编译器:Point类型占用8个字节(在int为4字节的平台上),x在偏移0处,y在偏移4处。它不包含任何函数指针,不包含任何方法,也没有构造函数和析构函数的概念。
C语言中,struct只能用来声明数据成员。如果你想对它进行"操作",只能在外部写函数,比如:
c复制struct Point* point_create(int x, int y);
void point_print(struct Point* p);
这就是C语言面向过程编程的典型风格:数据与操作分离。数据是struct,操作是函数,函数通过指针拿到数据再加工。这种设计在Linux内核代码里极其常见,比如struct file_operations就是一堆函数指针的集合,本质上是"用struct模拟面向对象的接口"。
有个细节特别值得注意:C语言里,struct类型名前必须带struct关键字,除非你用typedef给它起一个别名。这也是面试里经常出现的陷阱题:
c复制typedef struct Node {
int data;
struct Node* next;
} Node;
这里struct Node* next不能写成Node* next,因为typedef别名在结构体内部定义结束前还不生效。这个细节很基础,但真的有很多人在笔试里栽过。
我们再补一个热搜词相关的问题:"怎么知道struct中的成员大小"。这个问题的本质其实是在问内存对齐。C语言里求某个成员相对于结构体首地址的偏移量,惯用写法是:
c复制#include <stddef.h>
offsetof(struct Point, y);
求整个结构体大小用sizeof(struct Point),但注意sizeof的结果会受到对齐规则影响。比如:
c复制struct A {
char c; // 偏移0
int i; // 偏移4,不是偏移1,因为int需要4字节对齐
};
这个结构体在x86-64平台上通常占用8字节,而不是5字节。面试官问"struct成员大小",实际上是在考察你对内存对齐、填充字节(padding)的理解。这个问题我很建议你亲手写代码验证一下,因为单纯背结论很容易忘记。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++为什么还保留struct:它与class的同与不同
2.1 默认访问权限:public和private是第一个分水岭
当我们进入C++,核心事实是:struct和class在功能上几乎完全等价。它们都可以有成员函数、构造函数、析构函数、静态成员、运算符重载,甚至都可以作为基类被继承。
唯一的语言层面的区别,就是默认访问权限:
struct默认成员是publicclass默认成员是private
直接看代码:
cpp复制struct A {
int x; // 默认public
};
class B {
int x; // 默认private
};
这意味着A.x可以从外部直接访问,而B.x不行,编译直接报错。同理,继承时也有默认继承权限的差异:
cpp复制struct C : A {}; // 默认public继承
class D : A {}; // 默认private继承
这个差异看似简单,但有一个非常隐蔽的坑:如果你在C++里定义一个空struct,它的默认访问权限是public;而如果是一个空class,默认访问权限是private。这在模板推导、类型萃取等场景中会被放大。比如某些反射库或序列化库在判断"这个类型是否可聚合初始化"时,访问权限会影响结果。
2.2 struct也能做面向对象:继承和多态与class无差别
很多人误以为struct在C++里只能像C那样存放数据,这是一个很大的误解。
事实上,C++标准明确规定:struct和class在除了默认访问权限以外的所有方面都是等价的。struct可以包含虚函数、可以继承、可以多态。例如:
cpp复制struct Animal {
virtual void speak() { cout << "animal" << endl; }
};
struct Dog : public Animal {
void speak() override { cout << "dog" << endl; }
};
这段代码完全合法,Dog可以像普通派生类一样实例化,虚函数表机制照常工作。在内存布局上,Dog对象和用class声明的派生类对象没有本质区别,都会在头部通过vptr指向vtable(假设存在虚函数)。
所以在面试中,如果你说"struct不能继承"、"struct不能有虚函数",基本等于暴露了没有动手试过或只看过一些以讹传讹的博客。正确说法是:语言层面两者等价,差异只限于默认访问权限。
2.3 默认继承方式与POD类型判定的微妙影响
除了默认访问权限,还有一个容易被忽略的差异:默认继承方式不同,会影响这个类型是否满足标准布局(Standard Layout)和平凡(Trivial)等特征,进而影响它是否能被当作POD(Plain Old Data)类型使用。
C++11之后,判断一个类型是否是POD的标准变得非常严格,包括:
- 没有非平凡的构造函数、拷贝构造函数或析构函数
- 没有虚函数或虚基类
- 所有非静态数据成员具有相同的访问控制
- 在继承体系中,派生类中最多只有一个基类有非静态数据成员
这时,默认访问权限就有了连锁反应。考虑:
cpp复制struct PodStruct {
int a;
int b;
};
class NonPodClass {
int a;
int b;
};
PodStruct的所有非静态数据成员默认都是public,所以它可能满足POD的标准布局要求。而NonPodClass的所有成员默认都是private,但访问控制一致性上private也算一致,所以它实际上也可能是POD。关键问题是在更复杂的继承场景里,混合public/private成员会导致标准布局判定失败。
在实际工程里,如果你要序列化一个对象、和C代码共享内存、或者用memcpy拷贝它,最好确保它是POD或至少是标准布局类型。这时候用struct而不是class,能让你少一些心理负担,因为struct的默认public语义更接近"我只是一个数据块"。
3. 高频考点与源码级对比
3.1 代码示例:逐项对比struct与class
这部分我建议你亲手运行一遍下面的代码,观察输出和编译错误提示:
cpp复制#include <iostream>
using namespace std;
struct S {
int x;
void show() { cout << "S::x = " << x << endl; }
};
class C {
int x;
public:
C(int v) : x(v) {}
void show() { cout << "C::x = " << x << endl; }
};
int main() {
S s;
s.x = 10; // 合法,struct成员默认public
s.show();
// C c; // 编译错误:C的构造函数需要参数
C c(20);
c.show();
return 0;
}
注意几个点:
S s;可以正常默认构造,因为编译器合成的默认构造函数会帮我们做默认初始化,对内置类型x来说值未定义,但语法合法。C c;编译失败,因为class里我们声明了带参构造函数,编译器不再生成默认构造函数。这一点与struct无关,只与你写不写构造函数有关。c.x是private,外部无法访问,如果去掉public:那一行,编译会报'int C::x' is private within this context。
还有一个常考的对比是聚合初始化。C++里,如果struct没有用户声明的构造函数、没有私有或受保护的非静态数据成员、没有基类、没有虚函数,它可以作为聚合类型用花括号初始化:
cpp复制struct Point {
int x;
int y;
};
Point p{1, 2};
class默认情况下即使没有任何构造函数,如果它的成员是private,也无法进行聚合初始化。这一点在C++11标准里写得很明确:聚合类型不能有private或protected的非静态数据成员。所以如果你把class写成一个纯数据类,哪怕你没有定义构造函数,也不能用{}直接初始化成员。如果你想让它支持聚合初始化,就必须写一个带参构造函数,或者把所有成员放在public区域。在实际开发中,这就是为什么纯数据对象我更推荐用struct,因为它天然支持聚合初始化,代码更简洁。
3.2 内存布局:C的struct与C++的class在底层到底差多少
内存布局这个话题,面试官通常不会只问一个简单概念,而是会让你分析一个具体结构的对齐和大小。
先看C语言的情境,struct的内存布局主要受字节对齐限制。比如:
c复制struct X {
char a; // 偏移0
int b; // 偏移4
char c; // 偏移8
};
在x86-64默认对齐规则下,sizeof(struct X)是12,不是9。因为b要求4字节对齐,从偏移4开始;c放在偏移8,结构体总大小要对齐到4的倍数,所以补到12。如果你调整成员顺序:
c复制struct X {
int b; // 偏移0
char a; // 偏移4
char c; // 偏移5
};
sizeof会变成8。同样的成员,只是顺序不同,大小差了4字节。这个知识点在嵌入式开发和网络协议解析中非常实用。
再看C++的class,情况会复杂一些,因为可能有vptr(虚函数表指针)。当你给一个class加入虚函数后,对象头部会增加一个指针vptr,它指向该类的vtable。这个vptr在某些规则下会被当作第一个成员,占用一个指针的大小。这个指针的偏移受对齐规则影响,一般是0。因此:
cpp复制class Base {
public:
virtual void f();
int a;
};
在64位系统上,这个对象的大小通常是16字节:4字节的vptr对齐到8字节,int a从偏移8开始,总大小对齐到8,得到16。
很多面试题喜欢拿这种"虚函数+数据成员"的结构让你分析大小,如果你没实际写代码打印过,很容易漏掉vptr。我的建议是:遇到这种题,不要只凭记忆,可以在草稿纸上画一下内存布局,把vptr、数据成员、对齐填充都标出来。动手画一遍比背题目有用得多。
还有一点需要特别提醒:C++标准并没有规定vptr放在对象布局的哪个位置,以上说的是主流编译器的实现。所以严格来说,vptr的位置是"实现相关"的,不是语言标准的一部分。面试时可以直接这样回答:"在实际的常用编译器中,vptr通常被放在对象起始位置,但这属于实现细节,标准没有强制约束。"这样回答既专业又严谨。
4. 项目实战中的选型建议与避坑经验
4.1 什么场景用struct,什么场景用class
很多开发者在刚接触C++时都有这个疑问:既然两者除了默认访问权限几乎一样,那到底用哪个?我的经验是,遵循两个原则:
原则一:如果你需要一个纯数据对象,用struct。 所谓纯数据对象,就是它主要用来承载数据,而不是表现行为。比如一个坐标点、一个RGB颜色、一个网络报文的头部、一个配置项结构体。用struct的好处是:
- 默认public,省去写
public:的麻烦 - 支持聚合初始化,可以用
{}直接初始化 - 语义清晰,读者一看就知道"这是数据,不是封闭的业务对象"
- 和C代码互相调用时,更容易保持POD特性
原则二:如果你需要一个具有封装性和不变量的业务对象,用class。 比如一个银行账户类,它的余额字段必须用private保护起来,所有修改都要经过deposit或withdraw方法,不允许外部直接改。这种情况下用class更合理,因为private从语法层面就强制了"你只能通过接口操作数据"。
我举个实际项目中的例子。之前做一个图像处理管线,里面有一个ImageHeader结构体,存储图像的宽、高、格式、色彩空间等信息。这个结构体就是典型的纯数据对象,我把它定义成struct,所有字段public,然后用聚合初始化快速赋值。后来需要扩展几个量测信息,直接加字段就行,很方便。而ImageProcessor类则是class,因为它的内部有缓存、中间结果、参数校验逻辑,这些都不希望外部触碰,必须用private封装起来。
4.2 嵌入式开发与系统编程中的struct使用经验
热搜词里出现了"c语言面向对象编程:嵌入式实战"和"linux c语言删除目录",这些其实都指向一个核心主题:在C语言环境里,struct是模拟面向对象的主要工具。
常见的做法是:用struct保存"成员变量",用函数指针保存"方法",再用手工封装模拟构造和析构。看一个典型例子:
c复制struct SimpleTimer {
uint32_t start_tick;
uint32_t timeout;
void (*start)(struct SimpleTimer* self);
void (*check)(struct SimpleTimer* self);
};
使用方式:
c复制static void start_timer(struct SimpleTimer* self) {
self->start_tick = get_tick();
}
static void check_timer(struct SimpleTimer* self) {
if (get_tick() - self->start_tick > self->timeout) {
timeout_callback();
}
}
struct SimpleTimer timer = {0, 1000, start_timer, check_timer};
这种"结构体+函数指针"的模式在嵌入式固件、RTOS、驱动代码中非常常见。它能让你用C实现"类"和"接口"的概念,而且开销很小——只是多了一个指针大小,没有虚函数表那样多的额外开销。
但在嵌入式中有几个特别重要的坑:
-
位域(Bitfield)的内存布局在不同编译器下不一致。C标准只定义了位域的基本规则,具体到"是从低字节开始还是高字节开始"、"位域能否跨存储单元"这些问题,都取决于实现。如果你的代码要在多种芯片上跑,最好用掩码+移位操作代替位域,或者至少封装成宏/函数,不要直接把位域写进协议解析代码里。
-
结构体对齐在跨平台通讯中会出问题。如果你把结构体直接发到网络上,接收端和发送端如果对齐规则不同,解析出来就是错的。常用解法是
#pragma pack(1),但这会让访问效率变差,甚至在某些ARM平台上因为非对齐访问导致硬件异常。所以在协议解析时,更推荐的方案是手动按字节解析,不用结构体直接映射。 -
struct不能直接放进中断服务函数并依赖编译器优化。在嵌入式里,如果某个struct在中断和主循环之间共享,一定要用
volatile修饰,否则编译器可能把主循环里的数据读操作优化掉,导致读到的还是旧值。
这些经验不是语法层面的,而是实战中才会遇到。面试时如果你能讲出这种场景,会显得特别有真实项目经验。
4.3 从C转向C++的struct使用误区
很多从C转向C++的开发者,会习惯性把C的习惯带到C++里,最常见的一个误区是:给struct添加构造函数。
在C++里,struct可以加构造函数,但加了之后它就不再是聚合类型了,无法用{}初始化。比如:
cpp复制struct Point {
int x;
int y;
Point(int a, int b) : x(a), y(b) {}
};
定义之后,Point p{1, 2};是可以编译通过的(因为C++11允许用{}调用构造函数),但Point p = {1, 2};会失败,因为它不再是聚合类型,不能做聚合初始化。这个差异很容易让人困惑。
我的建议是:如果你用struct定义纯数据结构,就不要写构造函数,保持聚合类型,这样{1, 2}这种初始化方式最直观。如果确实需要一些校验逻辑,比如x和y不能同时为负,那说明你需要的是class而不是struct,把校验写进构造函数里,并让成员为private。
另一个常见误区是:在struct里定义无参构造函数,导致对象无法进行值初始化。看这个例子:
cpp复制struct Counter {
int count;
Counter() : count(0) {}
};
这时候虽然只是加了一个无参构造函数,但编译器会把它当作用户提供的构造函数,类型就不再是平凡类型(trivial type)。如果你要做memcpy或者作为联合体的成员,就会受到限制。所以除非必要,别给struct加构造函数。
4.4 实际面试中可能会问到的struct成员大小计算方法
针对热搜词"怎么知道struct中的成员大小",我再补充几道典型的面试题,这些我在模拟面试中经常用:
题目1:已知在32位平台上,int是4字节,char是1字节,short是2字节,求以下结构体大小:
c复制struct A {
char a;
int b;
short c;
};
计算过程:a在偏移0,b要求4字节对齐,所以a后面填充3字节,b在偏移4,c在偏移8。c只占2字节,结构体总大小需要对齐到最大对齐数4的倍数,9补到12,所以结果是12。
题目2:嵌套结构体
c复制struct B {
char a;
struct A inner;
char b;
};
这时候inner的对齐数是4,所以a后面要填充3字节,inner占12字节,偏移4到15,b在偏移16,总大小对齐到4的倍数,结果是20。
题目3:联合体大小
c复制union U {
char arr[10];
struct A inner;
};
联合体的大小是成员中最大的大小,同时对齐数也是所有成员中最大的对齐数。A大小12,所以arr[10]占10字节,但联合体要对齐到4,10会补到12,因此结果是12。
这些计算题只要画一张内存布局图就能轻松解出来。画图的方法是:先确定每个成员的对齐数,然后按照偏移规则挨个摆放,最后看总大小是否需要填充。练习几次就会有感觉。
5. 面试追问角度与答题策略
5.1 面试官常追问的几个角度
通常回答完struct和class的基本区别后,面试官会根据你的回答方向继续深挖,我总结了几种常见的追问角度:
追问一:"struct在C++里能不能有static成员?"
能。这里要分清楚:C语言里的struct不能有static成员(更准确地说,C语言没有static数据成员的概念),但C++里的struct和class一样,可以有static成员变量和static成员函数。static成员不属于某个具体对象,它属于整个类型。不过要注意,static成员变量必须在类外定义,C++17之后可以用inline static在类内初始化。
追问二:"struct和class的内存布局有什么区别?"
语言层面,如果成员声明一样、访问控制一样、虚函数情况一样,两者内存布局没有区别。但如果class里用了private成员且外部代码无法直接访问,它在API层面体现的封装性会影响你是否能对该类型进行某些底层操作,比如聚合初始化、位拷贝等。面试时可以从"关注对象布局,不关注关键字"的角度回答。
追问三:"你平时写代码时,怎么决定用struct还是class?"
这个问题没有标准答案,但面试官想听的是你的工程判断力。我的回答模板是:
"我主要看语义。如果这个类型主要是数据的聚合,我会用struct,默认public方便初始化,也方便做序列化;如果这个类型有内部状态管理或者需要保护不变量,我会用class,把成员设为private,通过接口访问。另外,写模板的时候要注意与C的互操作,这时struct更安全,因为它默认public且更可能满足POD条件。"
追问四:"C++的struct是否所有方面都和class一样?"
严格说,标准只规定了默认访问权限和默认继承权限的差异,但这两点会导致一个间接差异:默认继承权限不同会影响派生类对基类成员的访问能力,以及该派生类是否是标准布局类型。不过这些都是派生层面的影响,不是struct/class关键字本身的额外差异。所以最严谨的回答是:"语法能力上两者相同,唯一直接差异是默认访问权限和默认继承权限,间接会影响到聚合类型、POD等特性的判定,具体取决于类成员的写法。"
5.2 复习建议:动手写代码,不要只背结论
最后说一点我个人的建议。这类基础概念面试题,最忌讳的就是只看别人整理的对比表格,从来不自己动手验证。我见过太多人把"struct默认public,class默认private"背得滚瓜烂熟,但一让他现场写一个带继承的struct和class对比,就开始卡壳。
我的建议是:
- 自己写一个完整的测试程序,把struct和class声明成完全相同的成员(数据成员+成员函数+虚函数),打印
sizeof,观察内存布局。 - 跑一遍聚合初始化,看看在什么条件下struct能初始化、class不能。
- 尝试继承,观察默认继承方式对派生类可访问性的影响。
- 如果是嵌入式平台,再交叉编译到不同架构,看看结构体大小是否变化。
把这些实验做完,你脑子里才有真正的"体感",而不是一堆零散的知识点。面试的时候,你能说出"我在实际调试中发现,把虚函数加进去后对象大小多了8字节"这种具体观察,面试官会觉得你确实写过代码,而不是背答案。
5.3 关于struct与class差异的最后一个底层思考
很多文章把struct和class的区别讲成了一个简单的表格,我理解那是为了方便记忆,但你要清楚,这个表格后面还有一个更本质的东西:C++作为一种兼容C的面向对象语言,在设计时既要保持C的"简单数据聚合"概念,又要引入"类"的封装、继承体系。struct被保留下来并扩充功能,正是为了兼容C代码,同时让C++程序员能平滑过渡。而class的引入,则是为了提供更严格的封装语义和更清晰的面向对象表达。
理解了这一层,你就不难理解为什么C++标准在绝大多数场景下让struct和class等价:因为语言设计者希望"你想用哪个就用哪个",差别只是风格和语义上的偏好。真正的核心是你对封装、初始化、内存布局、POD这些底层概念的掌握程度。掌握这些,无论面试官怎么变着花样问,你都能接住。
