说实话,C/C++程序员面试如果只让我押一道必考题,struct和class的区别绝对排得进前三。这道题看着基础,却特别能筛人:它能同时考你对C语言底层内存布局的理解、对C++面向对象语法演进的认识,还能顺带摸清你有没有真正写过工程代码,而不是只会背八股。很多候选人上来就答“struct默认public,class默认private”,然后戛然而止,这种回答在面试官眼里基本等于没答。今天这篇就把这道题彻底聊透,从C语言里struct的原始定位,到C++里class的语法扩张,再到面试官最爱追问的内存对齐、POD、继承权限这些细节,一次讲清楚。
如果你正在准备C/C++开发岗面试,或者写了好几年C想转C++,又或者纯粹想搞明白“这两个东西到底差在哪”——这篇文章就是给你写的。我会把底层原理、代码演示、面试答题思路全部拆开揉碎,尽量做到看完就能用。
1. 内容整体设计与思路拆解
1.1 为什么面试官爱考这道题
这道题之所以高频,是因为它横跨C和C++两门语言,直接考察你对“面向过程”和“面向对象”两种编程范式的理解。一个候选人如果只背过语法差异,却说不清C语言为什么没有class、C++为什么又要保留struct,那说明他对语言设计缺少整体感知。
从实际工作场景看,很多嵌入式项目是C和C++混编的,代码里既有纯C的struct,也有C++的class。如果你不清楚两者的边界,很容易写出“看似是C++其实是C”的代码,或者反过来在这种混编环境里埋下坑。所以面试官问这道题,本质上是想确认你有没有处理过真实工程项目,而不只是刷过题库。
1.2 我决定从“C语言视角”切入的原因
网上绝大多数帖子讲这道题,都是上来就对比C++的struct和class,默认你已经把C语言那部分吃透了。但我在实际面试中发现,恰恰是C语言的struct部分最容易翻车——很多人不知道C语言里struct压根不能直接定义空结构体,不知道struct不能包含成员函数但可以用函数指针模拟,不知道typedef struct的两种写法有什么区别。
所以这篇文章我走一条稍微“反套路”的路线:先把C语言里的struct彻底讲透,再过渡到C++的struct和class对比。这样你不仅能答清“class和struct的区别”,还能顺带答清“C的struct和C++的struct区别”,这是很多候选人完全没准备过的角度,答出来就是加分项。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C语言中struct的核心定位与实战细节
2.1 数据聚合体:struct最原始的使命
C语言里的struct,本质上就是一组变量的打包。它没有成员函数、没有访问控制、没有继承,也没有多态。你在struct里只能放数据成员,而且这些数据成员是“裸”的——外部代码可以直接访问,没有任何封装和保护机制。
c复制#include <stdio.h>
#include <string.h>
struct Student {
char name[32];
int age;
float score;
};
int main() {
struct Student s1;
// 直接操作成员,没有任何访问限制
strcpy(s1.name, "Tom");
s1.age = 20;
s1.score = 88.5;
printf("%s %d %.1f\n", s1.name, s1.age, s1.score);
return 0;
}
这段代码放到C语言里完全合法,s1.age可以直接读写。但如果你试图在struct里定义一个函数成员,比如:
c复制struct Student {
char name[32];
int age;
// 错误!C语言的struct不能包含函数
void printInfo() { }
};
编译会直接报错。C语言的struct就是纯粹的数据容器,它不承载任何逻辑。这一点是理解整个问题的基石:C语言里没有面向对象,所以struct只能是“数据的集合”。
2.2 C语言中用struct实现“伪面向对象”
那问题来了:C语言的项目比如Linux内核、嵌入式驱动里,经常能看到看起来很像“对象”的写法,这是怎么做到的?答案是函数指针。
c复制struct Animal {
char name[32];
void (*speak)(struct Animal *self); // 函数指针成员
};
void dogSpeak(struct Animal *self) {
printf("%s: 汪汪\n", self->name);
}
void catSpeak(struct Animal *self) {
printf("%s: 喵喵\n", self->name);
}
int main() {
struct Animal dog;
strcpy(dog.name, "旺财");
dog.speak = dogSpeak;
struct Animal cat;
strcpy(cat.name, "咪咪");
cat.speak = catSpeak;
dog.speak(&dog);
cat.speak(&cat);
return 0;
}
这段代码在C语言里模拟出了“对象调用方法”的效果。注意那个self参数,它其实就是C++里this指针的原始形态。C++编译器帮你隐式传递了this,C语言只能手动传。
这在嵌入式开发中非常常见,比如用struct封装一个硬件寄存器映射:
c复制typedef struct {
uint32_t CR;
uint32_t SR;
uint32_t DR;
} UART_TypeDef;
#define UART0 ((UART_TypeDef *)0x40004000)
这样UART0->DR就可以直接操作寄存器。知道这个背景,你会明白为什么C++的struct要保留C语言的结构体特性——它要兼容这些已有的C代码。
2.3 C语言struct定义和初始化的冷门技巧
C语言struct的初始化看着简单,但真到面试时,有两三种写法会被问到。
第一种:按成员顺序初始化
c复制struct Point {
int x;
int y;
};
struct Point p = {10, 20};
第二种:指定成员初始化(C99标准)
c复制struct Point p = {.y = 20, .x = 10};
这种写法不要求成员顺序,可读性好,在Linux内核代码里非常常见。
第三种:部分初始化
c复制struct Point p = {10};
// p.x = 10, p.y = 0(自动补零)
这里有个关键细节:零初始化和未初始化的区别。如果声明成全局变量或静态变量,struct会被自动零初始化;如果声明成局部变量且不初始化,成员值是随机垃圾值。这一点面试时经常被拿来当坑。
c复制struct Point p1; // 局部变量,成员是垃圾值
static struct Point p2; // 静态变量,成员自动为0
2.4 怎么知道struct中的成员大小和偏移量
这个问题本质考的是内存对齐和offsetof宏。
c复制#include <stdio.h>
#include <stddef.h>
struct Example {
char a;
int b;
char c;
};
int main() {
printf("sizeof(struct Example) = %zu\n", sizeof(struct Example));
printf("offsetof(a) = %zu\n", offsetof(struct Example, a));
printf("offsetof(b) = %zu\n", offsetof(struct Example, b));
printf("offsetof(c) = %zu\n", offsetof(struct Example, c));
return 0;
}
在我的环境(64位Linux,默认4字节对齐)输出是:
code复制sizeof(struct Example) = 12
offsetof(a) = 0
offsetof(b) = 4
offsetof(c) = 8
很多人会疑惑:char占1字节,int占4字节,char又占1字节,加起来明明是6字节,为什么sizeof是12?这就是内存对齐在起作用。编译器为了让int b的地址落在4的倍数上,在a后面填充了3个字节;为了整个结构体大小是最大对齐数(这里是4)的整数倍,又在c后面填充了3个字节。
提示:使用offsetof宏可以安全地获取成员偏移量,避免手工计算出错,这在序列化和底层协议解析时非常有用。
3. C++中struct与class的真正区别
3.1 C++的struct已经不只是“结构体”
到了C++里,struct被彻底升级了。它现在可以包含成员函数、构造函数、析构函数、运算符重载、访问控制,甚至可以参与继承和多态。同C语言里的“纯数据容器”相比,C++的struct已经变成了一个有面向对象能力的类。
cpp复制struct Student {
private:
int age;
public:
Student(int a) : age(a) {}
int getAge() const { return age; }
};
int main() {
Student s(18);
// s.age = 20; // 错误!private成员不可访问
return 0;
}
这段代码如果拿C语言的编译器编译,根本不可能通过;但在C++编译器下完全合法。所以从语法能力上说,C++的struct几乎等价于class——注意我说“几乎”,因为还有默认权限这个关键差异。
3.2 默认访问权限:struct是public,class是private
这是面试时最容易答、也最容易被追问的一个点。两者的唯一语法差异是:
- class:默认访问权限是private
- struct:默认访问权限是public
具体体现有两个层面。
第一层:成员访问权限
cpp复制struct StructObj {
int a; // 默认public
};
class ClassObj {
int a; // 默认private
};
cpp复制StructObj s;
s.a = 10; // 合法
ClassObj c;
c.a = 10; // 编译错误
第二层:默认继承权限
cpp复制class Base {};
class DerivedA : Base {}; // 默认private继承
struct DerivedB : Base {}; // 默认public继承
也就是说,即使DerivedA和DerivedB内部代码完全相同,由于基类列表前没写继承方式,class版本是private继承,struct版本是public继承。这一点非常容易踩坑,尤其是用struct写继承代码时,如果不留意容易被外部误认为是public继承从而引发访问错误。
3.3 struct与class的默认继承权限对代码的影响
默认继承权限在实际项目里会带来什么影响?让我给一个具体的例子。
cpp复制class Base {
public:
void func() {}
};
class DerivedClass : Base {}; // 等价于 private Base
struct DerivedStruct : Base {}; // 等价于 public Base
int main() {
DerivedClass dc;
// dc.func(); // 错误!private继承下,Base的公共成员变成DerivedClass的私有成员
DerivedStruct ds;
ds.func(); // 合法!public继承下,Base的公共成员还是公共成员
return 0;
}
如果团队里有人用struct写了继承代码,很容易出现“调用不了基类函数”的诡异问题。排查半天才发现是默认继承权限在作怪。所以我在实际项目里的习惯是:继承时永远显式写清楚public/private/protected,不要依赖默认值。
3.4 struct和class在POD与布局兼容性上的差异
POD(Plain Old Data)这个概念在C++里很关键,它指的是与C语言兼容的数据类型。一个POD类型的对象可以用memcpy安全复制,可以用C语言的struct布局方式访问。
关键点是:当struct满足POD条件且成员全是public时,C++保证它的内存布局与C语言完全一致。这意味着你可以把这样的C++对象直接传给C函数。
cpp复制// C++代码
struct CCompatData {
int id;
double value;
};
void processInC(void* data); // 假装这是C函数
int main() {
CCompatData d{42, 3.14};
processInC(&d); // 安全,因为CCompatData是POD
return 0;
}
但如果用class且加了private成员,编译器就不承诺这种布局兼容性了。在C/C++混编项目里,两端共用的结构体必须保持POD性质,否则就是给自己挖坑。这也是为什么很多开源库对外暴露的C接口,都是用struct而不是class来定义数据结构的。
3.5 什么时候用struct,什么时候用class
这算是一个“面试官不直接问但你一定要知道”的潜规则。主流工程实践是:
- struct:用于“数据聚合体”,即主要是存数据、没有复杂逻辑的类型,比如点坐标、配置项、协议报文
- class:用于“有封装和业务行为的对象”,即包含私有数据、对外提供方法、有继承多态需求的类型
cpp复制// 用struct:纯粹存数据
struct Point {
double x;
double y;
};
// 用class:有状态、有行为
class BankAccount {
private:
double balance;
public:
void deposit(double amount);
double getBalance() const;
};
从C++标准角度,struct和class几乎等价;从工程习惯角度,它们承载着不同的语义。你写struct,代码阅读者就知道“这是个数据结构”;你写class,阅读者就知道“这有封装和逻辑”。这个约定虽然不是语言强制的,但遵守它能让代码更易维护。
4. 面试必考的底层细节:内存布局、空结构体与对齐
4.1 结构体里的内存对齐到底怎么算
面试官考struct和class区别时,经常顺手考一个“sizeof是多少”。这背后的核心就是内存对齐规则。当你定义了一个结构体,编译器会按以下规则排布成员:
- 每个成员按照它的对齐数(通常是自身大小)进行对齐
- 整个结构体的总大小必须是对齐数最大值的整数倍
- 对齐数可以通过#pragma pack或alignas修改
具体来说:
cpp复制struct AlignTest {
char c; // 第0字节
int i; // 对齐到4,从4开始,占4-7
double d; // 对齐到8,从8开始,占8-15
char c2; // 第16字节
};
按默认对齐,这个结构体大小是24。因为double的8字节是最大对齐数,结构体大小必须是8的倍数,17会向上取整到24。
注意:如果你把结构体成员按大小降序排列(double、int、char、char),在64位平台下可能从24字节降到16字节,少了8个字节的填充空间。这在嵌入式开发中被广泛用来优化内存占用。
4.2 空struct和空class的大小是多少
这个问题非常经典,几乎每次面试都会遇到。C++标准规定:空类(没有非静态数据成员、没有虚函数)的大小不为0,通常是1个字节。
cpp复制struct EmptyStruct {};
class EmptyClass {};
int main() {
printf("sizeof(EmptyStruct) = %zu\n", sizeof(EmptyStruct)); // 1
printf("sizeof(EmptyClass) = %zu\n", sizeof(EmptyClass)); // 1
return 0;
}
为什么是1?因为C++对象必须拥有独一无二的地址。如果两个空对象大小都是0,它们就会拥有相同的地址,这在语法上是不允许的。所以编译器强制给空对象分配1个字节的空间。
这里还有个衍生考点:空基类优化(EBO,Empty Base Optimization)。如果空类作为基类,派生类通常不会增加这1字节的开销,编译器会优化掉它。
cpp复制class EmptyBase {};
class Derived : public EmptyBase {
int x;
};
int main() {
printf("sizeof(Derived) = %zu\n", sizeof(Derived)); // 4,不是8
return 0;
}
在没有EBO的编译器中,Derived的大小可能是8;但在主流编译器(GCC、Clang、MSVC)中,通常会优化为4。面试里能答出这个点,说明你对C++对象模型有深入理解。
4.3 如何让struct兼容C语言:extern "C"与typedef
如果是C和C++混编的项目,头文件里的struct定义需要考虑双语言兼容问题。常见的做法是这样的:
c复制#ifdef __cplusplus
extern "C" {
#endif
typedef struct Config {
int timeout_ms;
int retry_count;
} Config;
#ifdef __cplusplus
}
#endif
这样写,C和C++都能识别Config类型。注意这里用的是typedef struct的组合写法,这种写法在C语言中特别常见,因为它让你声明变量时不用写struct关键字:
c复制Config cfg; // 可以
struct Config c; // 也行
在C语言里,如果你不写typedef,每次声明变量都要带struct关键字,很不方便。C++里struct本身就能直接当类型名使用,所以就不需要这种typedef写法了。面试时如果被问到“为什么C语言要typedef struct”,答案就是这个语法差异。
4.4 位域:struct中冷门但面试常考的特性
struct和class的区别还体现在位域(bit field)上。位域只能在struct/class/union中定义,用于按位指定成员占用位数,常用于协议解析、寄存器定义等场景。
cpp复制struct Flags {
unsigned int a : 1;
unsigned int b : 3;
unsigned int c : 4;
};
这里a占1位、b占3位、c占4位,总共8位,也就是1字节。但实际sizeof(Flags)可能是4,因为编译器按int对齐。位域的计算规则在不同平台上有差异,面试时能答出“位域的分配顺序和总位数是实现定义的”就能超过大多数候选人。
关键提醒:位域成员不能用offsetof获取偏移量,也不能用memcpy复制,因为它的内存布局是编译器相关的,在不同编译器和平台间可能不一致。跨平台开发时,位域常用于网络协议头解析,但要注意端序问题。
5. 面试答题思路与常见追问整理
5.1 一个能拿高分的回答架构
如果你在面试中被问到“C语言和C++中struct和class的区别”,我建议按以下顺序回答:
- 先说最直接的区别:C++中struct和class默认访问权限不同,struct是public,class是private;继承时struct默认是public继承,class默认是private继承
- 再说C语言与C++的struct差异:C语言的struct只是数据聚合体,不能包含成员函数;C++的struct是升级版,功能上和class基本等价,可以做封装、继承、多态
- 补一个工程实践的约定:struct通常用来定义纯数据结构,class用来定义有封装逻辑的对象
- 如果时间允许,聊一下底层细节:内存对齐、POD、空结构体大小、位域这些,展示你的深度
这样回答,从表层语法到工程实践再到编译器行为,层次分明,面试官很容易据此判断你是“背了答案”还是“真正懂”。
5.2 几个容易被追问的衍生问题
这是我整理面试时最常遇到的衍生问题,你可以提前准备:
- union和struct的区别? union所有成员共享同一块内存,大小等于最大成员大小;struct每个成员独占内存,大小是所有成员大小之和加上填充
- C++中struct可以定义虚函数吗?多态吗? 可以。只要struct里有virtual函数,编译器就会生成虚函数表。这和class完全一样
- struct可以定义构造函数,那有什么坑吗? 可以。但一旦定义了构造函数,struct很可能不再是POD,和C语言的内存布局兼容性会受影响
- 为什么C语言不能用class,而C++却保留了struct? 因为C++要兼容C语言的大量代码,而class在C语言里没有对应实体
- struct的成员函数和内联函数的区别? 成员函数会在每个调用点展开,和普通内联函数类似,但注意类内部的函数默认是inline的
5.3 如何在简历和面试中体现这门技术
如果你正在准备校招或跳槽,建议在简历中把“熟练使用C/C++中的struct与class”升级为“理解C与C++内存模型差异,能独立处理C/C++混编项目中的结构体兼容性问题”,这比空洞地写“掌握C++面向对象”更有说服力。
面试时如果被追问底层问题,你可以举一个嵌入式或协议解析的实际案例:比如用struct映射硬件寄存器、用位域解析网络包、用POD保证结构体跨语言传递。这些例子说明你不仅懂语法,还知道它在你日常开发里是怎么落地的。
6. 常见问题、踩坑记录与自检清单
6.1 面试答案中的常见错误
我在做技术面试时,经常听到以下错误回答,你一定要注意避免:
- “struct不能有函数”:这句话在C语言里正确,但不适用于C++。正确说法是“C语言的struct不能有函数成员,C++的struct可以”
- “class和struct完全一样,只是默认权限不同”:这个说法太绝对,没有分清C语言的struct和C++的struct是两回事
- “struct是值类型,class是引用类型”:这是Java/C#的说法。C++里没有这个区分,struct和class变量在栈上默认都是值语义
- “struct一定比class快”:完全没有依据。性能取决于使用方式,和struct/class关键字无关
6.2 实际项目里的踩坑记录
我在项目的C/C++混编代码里,遇到过几次因struct/class差异导致的问题,说几个典型案例。
坑一:默认继承权限引发的“灵异”编译错误
某个同事用struct继承了基类,代码里调不到基类的public函数。他以为是头文件没包含对,排查了很久才发现是默认private继承的问题。后来我们在代码规范里强制要求:继承必须显式写继承权限,禁止依赖默认值。
坑二:在C++ struct里加了私有成员,导致C兼容破裂
某次给一个数据结构加了private成员和构造函数,结果外部C代码传入的结构体数据全部错乱。原因是这个struct不再是POD,C++编译器重新调整了布局。解决方案是把内部逻辑封装到class里,对外暴露的接口仍然用纯C struct。
坑三:内存对齐导致的结构体大小超预期
在嵌入式环境里,有人定义了一个跨平台通信用的结构体,成员顺序没有优化,结果在某个平台上sizeof是其他平台的2倍。最后通过调整成员顺序和使用#pragma pack(1)解决了问题。这里提醒一下:强制紧凑对齐能省内存,但会让访问效率下降,需要评估后再用。
6.3 自检清单:面试前的最后检查
在你去面试之前,建议拿下面这份清单快速自测一遍:
- 我能说出C语言struct和C++ struct的本质差异吗?
- 我知道class和struct的默认访问权限、默认继承权限分别是多少吗?
- 我能现场写出一个用函数指针模拟成员函数的C语言代码吗?
- 我知道空struct和空class的sizeof是多少吗?原因是?
- 我能解释内存对齐的规则和优化方法吗?
- 我知道POD类型是什么吗?为什么混编时需要保证POD?
- 我知道C++中struct和class都可以定义构造函数、虚函数吗?
- 如果在继承时不写访问权限,我能在编译前预判是public还是private继承吗?
如果你能全部答上来,这道题的面试部分基本稳了。剩下的就是保持自信,把答案组织得简洁有条理。
7. 一些面试之外的真实体会
最后聊几句我实际写代码的体会。面试考这道题,真正想招的人不是能背出“struct默认public,class默认private”的人,而是能回答“为什么要设计成两种默认权限”的人。C语言里struct把数据暴露在外,简单直接,适合做底层协议和硬件映射;C++里class强调封装,把数据和操作藏在黑盒里,适合做业务逻辑。两者没有谁更高级,只是服务的目标不同。
我自己写C++时习惯用class承担绝大部分业务类设计,用struct定义那些“其实就是一堆数据”的类型。这样代码一读就知道设计意图。C语言里我设计驱动层时,会用struct和函数指针搭配,模拟出类似面向对象的接口,这也让我后来理解C++的虚函数表和this指针的时候,毫无障碍。
如果你这次面试没答好这道题,不用太焦虑,它恰恰是查漏补缺的好机会。把C语言struct的内存布局、C++的class对象模型、默认权限差异这三个层次掌握好,你会发现自己不光能答面试题,写代码时对数据结构和类设计的理解也更深了一层。
