一、结构体核心概念(C/C++ 通用)
结构体(struct)是用户自定义的复合数据类型,核心作用是将不同类型的基础数据(如int、char、指针等)封装为一个整体,用来描述现实中 “具有多个属性的复杂对象”(比如 “学生” 包含学号、姓名、成绩;“汽车” 包含品牌、价格、排量)。
| 对比维度 | C 语言结构体 | C++ 结构体 |
|---|---|---|
| 类型名使用 | 必须加struct关键字(除非用typedef取别名) | 结构体名本身就是合法类型名,可直接使用,无需struct |
| 成员权限 | 无访问控制(所有成员默认公开,且无public/private/protected关键字) | 支持访问控制:默认public,可显式指定private/protected |
| 成员类型 | 仅支持数据成员(变量),不能定义函数 | 支持数据成员 + 成员函数(包括构造 / 析构函数、重载运算符、静态函数等) |
| 继承特性 | 无继承概念,无法继承其他结构体 / 类型 | 可继承其他结构体 / 类(默认 public 继承),支持多态 |
| 模板支持 | 无模板机制,无法泛型化 | 可结合模板(template)实现泛型结构体 |
| 与类的关系 | 无 “类” 的概念,结构体仅用于封装数据 | 和class几乎等价(仅默认权限 / 继承方式不同),可实现类的所有功能 |
| 初始化方式 | 仅支持聚合初始化(C99 起支持指定成员初始化),无构造函数 | 支持列表初始化、构造函数初始化、赋值初始化等,更灵活 |
二、结构体的定义与声明(语法)
结构体的定义需用struct关键字,常见有 3 种方式,同时可通过typedef定义别名(简化 C 中使用)。
2.1 基础定义方式
方式 1:先定义类型,再声明变量(最常用)
// 定义结构体类型:struct Student(C中使用时需加struct,C++可省略) struct Student { int id; // 成员1:学号(int类型) char name[20]; // 成员2:姓名(字符数组) float score; // 成员3:成绩(float类型) }; // 声明变量:stu1、stu2是struct Student类型的对象 struct Student stu1, stu2;方式 2:定义类型的同时声明变量
// 定义struct Teacher类型,并直接声明tea1、tea2两个变量 struct Teacher { int id; char subject[30]; } tea1, tea2;方式 3:匿名结构体(无类型名,仅能在定义时声明变量)
匿名结构体没有类型名,无法复用(后续不能再声明同类型变量),仅适用于临时、单一对象:
// 匿名结构体,仅能使用rect这一个变量 struct { double width; double height; } rect;
2.2typedef定义结构体别名(C 中常用)
C 语言中,每次使用结构体都要写
struct 类型名,可通过typedef定义别名,简化书写:// 方式1:分开写 struct Student { int id; char name[20]; }; typedef struct Student Stu; // 别名Stu,等价于struct Student // 方式2:合并写(更常用) typedef struct Student { int id; char name[20]; } Stu; // 声明变量:直接用别名Stu Stu stu1;
2.3 C++ 中 using 定义结构体别名的写法
using是 C++11 及以后推荐的别名声明方式,语法比typedef更直观,尤其是处理复杂类型时优势更明显。针对结构体,主要有两种常用写法:写法 1:先定义结构体,再用 using 取别名
// 第一步:定义结构体(C++ 中结构体名本身就是合法类型名,无需像C那样依赖struct) struct Student { int id; // 学号 char name[20]; // 姓名 }; // 第二步:用 using 定义别名 Stu,等价于 struct Student(C++ 中struct可省略) using Stu = Student; // 推荐写法,最简洁 // 也可以写:using Stu = struct Student; (兼容C的写法,不推荐)写法 2:匿名结构体 + using (更紧凑,适合一次性定义)
如果不需要保留原结构体名,可直接定义匿名结构体并取别名:
// 直接定义匿名结构体,并为其取别名 Stu using Stu = struct { int id; char name[20]; };using 对比 typedef 的优势
以函数指针这类复杂类型为例,能更明显看出
using的可读性优势:// typedef 定义函数指针别名(写法绕,新手易混淆) typedef void (*FuncPtr)(int); // using 定义函数指针别名(语法直观,“别名 = 类型”的逻辑更清晰) using FuncPtr = void (*)(int);
三、结构体成员的访问
访问结构体成员需根据变量类型(普通变量 / 指针变量)选择不同的操作符:
3.1 普通变量:用
.(成员访问操作符)#include <stdio.h> #include <string.h> typedef struct Student { int id; char name[20]; } Stu; int main() { Stu stu; stu.id = 1001; // 普通变量用.访问成员 strcpy(stu.name, "张三");// 字符串需用strcpy赋值 printf("学号:%d,姓名:%s\n", stu.id, stu.name); return 0; }3.2 指针变量:用
->(指针成员访问操作符)若结构体变量是指针,需用
->访问成员(等价于(*指针).成员):int main() { Stu stu; Stu *p = &stu; // 结构体指针,指向stu的地址 p->id = 1002; // 指针用->访问成员 strcpy(p->name, "李四");// 等价于 (*p).name = "李四" printf("学号:%d,姓名:%s\n", p->id, p->name); return 0; }
四、结构体的初始化
结构体的初始化方式因 C/C++ 标准不同而有所差异:
4.1 C 语言初始化
(1)C89 标准:按成员顺序初始化
必须严格按照结构体成员的定义顺序赋值,未赋值的成员默认初始化为
0:Stu stu1 = {1001, "张三"}; // 按顺序:id→name,score默认0.0(2)C99 标准:指定成员名初始化(推荐)
可通过
.[成员名]指定成员赋值,顺序任意,未赋值成员默认0:Stu stu2 = { .name = "王五", .id = 1003 }; // id=1003,name="王五",score=0.0注意!!!:
这个C99标准的写法是罕见的C++兼容的C语言的语法, 这种写法在大多数C++编译器中会报错!!!
(3)外部赋值初始化
先定义再赋值,缺点是容易漏掉变量,忘记赋值
Stu stu3; stu3.id=1003 stu3.name="王五" stu3.score=0.0
4.2 C++ 初始化
C++ 兼容 C 的初始化方式,同时支持更灵活的写法:
(1)列表初始化(C++11+)
struct Student { int id; char name[20]; float score; }; Student stu = {1001, "张三", 95.5}; // 顺序列表初始化 Student stu2 = {.name = "李四", .id = 1002}; // 指定成员初始化(C++11+)(2)构造函数初始化(C++ 特有)
C++ 结构体可定义构造函数,在创建对象时自动初始化(后续第 10 节详细讲)。
五、结构体的内存布局与内存对齐(核心重点)
结构体的大小不是成员大小的简单相加,而是遵循内存对齐规则(编译器为提升 CPU 访问效率的优化策略)。
5.1 内存对齐规则(通用)
- 第一个成员:偏移量(相对于结构体起始地址的距离)为
0;- 每个成员:偏移量必须是「该成员自身对齐值大小」的整数倍;
- 结构体总大小:必须是「结构体中最大基本类型成员对齐值大小」的整数倍。
5.2 示例:计算结构体大小
#include <stdio.h> // 定义结构体 struct Test { char a; // 大小1字节,偏移0(符合规则) int b; // 大小4字节,偏移需是4的整数倍 → 偏移4(a后补3字节“内存空洞”) short c; // 大小2字节,偏移8(4+4),是2的整数倍 → 符合规则 }; int main() { printf("结构体大小:%zu\n", sizeof(struct Test)); // 输出8(1+3+4+2=10?不,总大小需是最大成员(int=4)的整数倍 → 补2字节,总大小12?) // 注意:不同编译器可能有不同对齐策略,可通过#pragma pack(n)修改对齐系数 return 0; }5.3 为什么要内存对齐?
CPU 访问内存时,会按 “字长”(如 32 位 CPU 字长 4 字节)批量读取。若数据未对齐,CPU 需分 2 次读取并拼接,效率低;对齐后可 1 次读取,牺牲少量内存换效率。
5.4 关于结构体大小的详细计算
https://mp.csdn.net/mp_blog/creation/editor/155817717
六、结构体嵌套
结构体可以包含另一个结构体作为成员(嵌套),用来描述更复杂的对象:
// 定义“地址”结构体 struct Address { char city[20]; char street[50]; }; // 定义“学生”结构体,嵌套Address struct Student { int id; char name[20]; struct Address addr; // 嵌套结构体成员 }; int main() { // 初始化嵌套结构体 struct Student stu = { .id = 1001, .name = "张三", .addr = {.city = "北京", .street = "中关村大街"} }; // 访问嵌套成员:逐层用. printf("城市:%s,街道:%s\n", stu.addr.city, stu.addr.street); return 0; }
七、结构体与函数
结构体可以作为函数的参数、返回值,核心有 2 种传递方式:
7.1 值传递(拷贝传递)
将结构体的完整副本传给函数,函数内修改的是 “副本”,不影响原结构体(缺点:结构体较大时,拷贝开销高):
// 值传递:修改的是副本 void modify_stu(Stu s) { s.id = 2000; } int main() { Stu stu = {1001, "张三"}; modify_stu(stu); printf("学号:%d\n", stu.id); // 输出1001(原数据未变) return 0; }7.2 指针传递(推荐)
传递结构体的地址,函数内直接操作原数据,无拷贝开销:
// 指针传递:修改原数据 void modify_stu_ptr(Stu *s) { s->id = 2000; } int main() { Stu stu = {1001, "张三"}; modify_stu_ptr(&stu); printf("学号:%d\n", stu.id); // 输出2000(原数据被修改) return 0; }7.3 结构体作为返回值
函数可以返回结构体(本质是值传递,返回副本):
Stu create_stu(int id, const char *name) { Stu s; s.id = id; strcpy(s.name, name); return s; } int main() { Stu stu = create_stu(1004, "赵六"); printf("学号:%d,姓名:%s\n", stu.id, stu.name); return 0; }
八、结构体中的数组与指针
结构体中可以包含数组或指针,但内存管理方式完全不同:
8.1 结构体中的数组
数组内存直接存储在结构体中,大小固定:
struct Student { char name[20]; // 数组内存属于结构体,大小固定为20字节 };8.2 结构体中的指针
指针仅存储地址,指向的内存不在结构体中,需手动分配 / 释放(易引发内存泄漏):
struct Student { char *name; // 指针仅存地址,需手动分配内存 }; int main() { struct Student stu; // 手动分配内存 stu.name = (char*)malloc(20); strcpy(stu.name, "张三"); printf("姓名:%s\n", stu.name); // 手动释放内存(避免泄漏) free(stu.name); return 0; }8.3 浅拷贝问题(结构体含指针时)
若直接赋值含指针的结构体,会发生浅拷贝(仅拷贝指针地址,不拷贝指针指向的内容),导致多个结构体指针指向同一块内存,释放时重复释放:
int main() { struct Student stu1, stu2; stu1.name = (char*)malloc(20); strcpy(stu1.name, "张三"); stu2 = stu1; // 浅拷贝:仅拷贝指针地址,stu2.name和stu1.name指向同一块内存 free(stu1.name); // 释放stu1.name free(stu2.name); // 重复释放,触发内存错误 return 0; }解决方法:深拷贝手动拷贝指针指向的内容,而非仅拷贝地址:
void copy_stu(struct Student *dest, struct Student *src) { dest->id = src->id; // 深拷贝:分配新内存,拷贝内容 dest->name = (char*)malloc(strlen(src->name) + 1); strcpy(dest->name, src->name); }
九、结构体的进阶特性
9.1 柔性数组(C99 特有)
结构体的最后一个成员可以是 “未指定大小的数组”(柔性数组),用于动态分配内存(数组大小由
malloc指定):// 柔性数组:最后一个成员是未指定大小的数组 struct Buffer { int len; char data[]; // 柔性数组,大小为0(C99支持) }; int main() { // 分配内存:Buffer大小 + 100字节的data数组 struct Buffer *buf = (struct Buffer*)malloc(sizeof(struct Buffer) + 100); buf->len = 100; strcpy(buf->data, "这是柔性数组的内容"); printf("内容:%s\n", buf->data); free(buf); // 一次释放即可 return 0; }C++标准不支持柔性数组, 虽然大多数编译器做了拓展支持, 但是C++ 中优先使用
std::vector/std::string/ 智能指针替代柔性数组, 避免手动内存管理的错误, 符合 C++ 的 RAII 理念;在定义 Buffer 结构体时,为什么选择
char data[]这种柔性数组写法,而非更直观的char* data?
9.2 柔性数组 vs char* data 的本质区别
用
char* data虽然语法上可行,但会带来内存不连续、管理复杂、易出错等问题;而柔性数组的核心优势是内存连续、管理简单、性能更优,这也是 C99 引入柔性数组的根本原因。1. 内存布局:连续 vs 分散
这是最核心的区别,直接决定了后续的所有差异:
柔性数组(char data []):
data是结构体的 “一部分”,和len在同一块连续内存中。malloc 时只需一次分配:sizeof(struct Buffer) + 数据长度,内存布局如下:内存地址:低 → 高 | len (4字节) | data[0] | data[1] | ... | data[99] | (连续的一块内存)示例中,
malloc(sizeof(struct Buffer) + 100)分配的是 “4 字节 len + 100 字节 data” 的连续内存。char* date指针:
data只是结构体里的一个指针变量(4/8 字节),它指向另一块独立的内存。需要两次 malloc:第一步:分配结构体内存 → 存储 len 和 data指针 | len (4字节) | data指针 (8字节) | 第二步:分配数据内存 → data指针指向这块内存 ↓ | 数据内容 | (另一块独立内存)2. 内存管理:简单 vs 复杂(易出错)
这是实际开发中最容易踩坑的点:
(1)柔性数组的管理(简单安全)
// 你的原代码:一次分配,一次释放 struct Buffer *buf = (struct Buffer*)malloc(sizeof(struct Buffer) + 100); buf->len = 100; strcpy(buf->data, "这是柔性数组的内容"); free(buf); // 一次free即可释放所有内存,无内存泄漏(2)char* data 的管理(复杂易出错)
struct Buffer { int len; char* data; // 指针版本 }; int main() { // 第一步:分配结构体内存 struct Buffer *buf = (struct Buffer*)malloc(sizeof(struct Buffer)); if (buf == NULL) return -1; // 第二步:分配数据内存(必须单独分配) buf->len = 100; buf->data = (char*)malloc(buf->len); if (buf->data == NULL) { // 需额外判空,否则内存泄漏 free(buf); return -1; } strcpy(buf->data, "这是指针版本的内容"); printf("内容:%s\n", buf->data); // 释放:必须先释放data指向的内存,再释放结构体! // 顺序错了:先free(buf) → data指针变成野指针,无法释放data内存(内存泄漏) // 漏释放data:直接free(buf) → data指向的内存永远无法释放(内存泄漏) free(buf->data); free(buf); return 0; }
- 必须两次 malloc、两次 free,步骤繁琐;
- 释放顺序不能错(先 data 后结构体),新手极易写错;
- 任何一步 malloc 失败,都要手动回滚释放已分配的内存,否则泄漏。
3. 性能与缓存友好性:更优 vs 较差
CPU 访问内存时,会优先读取 “缓存行”(连续的内存块),连续内存的访问效率远高于分散内存:
- 柔性数组:
len和data在同一块连续内存,CPU 一次缓存就能加载,访问buf->len后再访问buf->data时,数据已在缓存中,速度快;- char* data:结构体和数据内存分散,CPU 需要两次缓存加载(先加载结构体,再加载数据),缓存命中率低,性能稍差(高频访问时差异明显)。
4. 内存碎片:更少 vs 更多
频繁分配 / 释放内存时,柔性数组的优势更明显:
- 柔性数组:一次分配大块连续内存,减少内存碎片;
- char* data:两次小内存分配,容易产生大量零散的内存碎片,导致后续 malloc 失败(即使总空闲内存足够,也没有连续的大块内存)。
5. 边界与安全性:更可控 vs 易出错
- 柔性数组:通过
sizeof(struct Buffer) + n分配,能精准控制数据区大小,且data是结构体的一部分,不会出现 “结构体有效但 data 指针悬空” 的情况;- char* data:需要手动记录
len和data指向的内存大小,容易出现 “len 和实际内存大小不一致”(比如 len 设为 100,但 data 只分配了 50 字节),导致越界访问。
9.3 匿名结构体(嵌套场景)
嵌套结构体时可省略类型名(匿名),简化代码:
struct Student { int id; // 匿名嵌套结构体 struct { char city[20]; char street[50]; } addr; }; int main() { struct Student stu = {.id=1001, .addr.city="上海"}; printf("城市:%s\n", stu.addr.city); return 0; }
十、C++ 中结构体的扩展(C++ 特有)
C++ 中的
struct本质上与class几乎一致,仅默认访问权限不同(struct默认public,class默认private),支持面向对象特性:
10.1 结构体可以包含成员函数
C 的结构体仅能存数据,C++ 的结构体可以同时存 “数据(成员变量)” 和 “行为(成员函数)”:
#include <iostream> using namespace std; struct Student { // 成员变量(默认public) int id; string name; // C++可直接用string // 成员函数:打印信息 void print() { cout << "学号:" << id << ",姓名:" << name << endl; } }; int main() { Student stu = {1001, "张三"}; stu.print(); // 调用成员函数 return 0; }
10.2 构造 / 析构函数(C++ 特有)
- 构造函数:创建对象时自动调用,用于初始化;
- 析构函数:销毁对象时自动调用,用于释放资源。
struct Student { int id; string name; // 构造函数(与结构体同名,无返回值) Student(int id_, string name_) { id = id_; name = name_; } // 析构函数(~开头,无参数) ~Student() { cout << "销毁对象:" << name << endl; } }; int main() { Student stu(1002, "李四"); // 调用构造函数 stu.print(); return 0; // 函数结束,析构函数自动调用 }
10.3 继承与多态(C++ 特有)
C++ 结构体支持继承和虚函数,实现多态:
// 基类结构体 struct Shape { virtual double get_area() { return 0; } // 虚函数 virtual ~Shape() {} // 虚析构函数(确保子类析构被调用) }; // 子类结构体:继承Shape struct Circle : public Shape { double radius; Circle(double r) : radius(r) {} // 重写虚函数 double get_area() override { return 3.14 * radius * radius; } }; int main() { Shape *s = new Circle(5); cout << "圆形面积:" << s->get_area() << endl; // 多态调用 delete s; return 0; }
10.4 运算符重载(C++ 特有)
C++ 结构体可以重载运算符,让结构体支持更直观的操作:
struct Point { int x, y; // 重载+运算符 Point operator+(const Point &p) { return {x + p.x, y + p.y}; } }; // 重载<<运算符(输出) ostream& operator<<(ostream &os, const Point &p) { os << "(" << p.x << "," << p.y << ")"; return os; } int main() { Point p1 = {1,2}, p2 = {3,4}; Point p3 = p1 + p2; cout << p3 << endl; // 输出(4,6) return 0; }
十一、结构体与联合体(union)的区别
特性 结构体( struct)联合体( union)内存分配 各成员占独立内存 所有成员共享同一块内存 大小 成员大小之和(含内存空洞) 最大成员的大小 用途 封装不同类型数据 节省内存(同一时间存一种数据)
十二、C++结构体和C++类的区别
唯一的区别就是没有区别
| 对比维度 | struct(结构体) | class(类) |
|---|---|---|
| 默认成员访问权限 | 未显式指定时,成员默认public(公开) | 未显式指定时,成员默认private(私有) |
| 默认继承方式 | 未显式指定时,默认public继承 | 未显式指定时,默认private继承 |
| 语法兼容性 | 兼容 C 语言结构体写法(可直接定义 C 风格结构体) | 纯 C++ 特性,无 C 语言兼容版本 |
| 设计意图 / 编程规范 | 偏向「数据聚合」:封装简单、无复杂行为的纯数据集合(如坐标、配置参数) | 偏向「对象」:封装完整的「数据 + 行为」,强调接口与实现分离(如学生、汽车等业务对象) |
| POD 类型兼容性 | 更容易满足 POD 类型(Plain Old Data),可直接和 C 语言内存布局兼容 | 若有private/ 虚函数等,会失去 POD 特性 |
| 模板特化写法 | template<> struct A<int> {}; | template<> class A<int> {}; |