C/C++ 中的 struct:从 C 到 C++ 的进化
struct 就是用来把一堆数据打包到一起的东西。C 和 C++ 里它长不一样,一个一个说。
1. C 语言中的 struct------纯粹的数据打包工具
C 语言没有 class,没有面向对象那套东西。struct 就是 把多个不同类型的变量组合成一个整体 的自定义数据类型。
1.1 基本语法
c
struct 结构体名 {
类型 成员1;
类型 成员2;
// ...
}; // 分号不能少
举个例子:
c
struct Student {
char name[20];
int age;
float score;
};
1.2 定义变量
C 语言里定义结构体变量时,struct 关键字不能省:
c
struct Student s1; // 必须带 struct
嫌麻烦可以用 typedef 重命名:
c
typedef struct Student {
char name[20];
int age;
float score;
} Stu;
Stu s1; // 现在不用写 struct 了
1.3 初始化
按成员顺序依次赋值:
c
struct Student s1 = {"张三", 20, 90.5};
1.4 访问与赋值
用 . 操作符:
c
s1.age = 21;
相同类型的结构体变量可以整体赋值:
c
struct Student s2 = s1; // 整体拷贝
1.5 传参
传值 :拷贝整个结构体,浪费空间和时间。
传址:传指针,只传 4/8 个字节,更快。
c
void printStudent(const struct Student* s) {
printf("%s, %d\n", s->name, s->age);
}
所以结构体传参最好传指针。
1.6 C 语言 struct 的局限
- 不能有成员函数,只能放函数指针
- 没有访问控制,所有成员对外可见
- 没有继承,没有多态
- 不能有静态成员
2. C++ 中的 struct------升级版
C++ 的 struct 是 C 的 struct 的超集。几乎所有合法的 C 结构体在 C++ 里也合法。
C++ 的 struct 和 class 几乎一样,唯一区别是默认访问权限:
| 特性 | struct | class |
|---|---|---|
| 默认成员访问权限 | public |
private |
| 默认继承方式 | public 继承 |
private 继承 |
2.1 可以有成员函数
cpp
struct Student {
char name[20];
int age;
void print() {
cout << name << ", " << age << endl;
}
};
2.2 可以有构造函数和析构函数
cpp
struct Student {
string name;
int age;
Student(const string& n, int a) : name(n), age(a) {}
~Student() { /* 清理资源 */ }
};
2.3 可以有访问控制
cpp
struct Student {
private:
int id; // private,外部不能访问
public:
string name;
int age;
};
2.4 可以继承
cpp
struct Person {
string name;
};
struct Student : Person { // 默认 public 继承
int grade;
};
2.5 定义变量不用写 struct
C++ 里直接用结构体名:
cpp
Student s1; // 不用写 struct
2.6 空结构体
C++ 允许空结构体,大小为 1 字节:
cpp
struct Empty {};
sizeof(Empty); // 1
3. 内存对齐------为什么 sizeof 比你算的大
这是 C 和 C++ 里最容易踩的坑。
3.1 什么是对齐
CPU 读取内存不是逐字节读的,而是按"块"读的(32 位系统一次 4 字节,64 位一次 8 字节)。
如果数据没对齐到合适的边界,CPU 可能需要两次读取再加拼接,效率大打折扣。有些 ARM 平台甚至直接崩溃。
所以编译器会在成员之间插入空白字节(padding),用空间换时间。
3.2 对齐规则
规则一:第一个成员放在偏移量为 0 的位置。
规则二 :其他成员的起始偏移量必须是其自身大小的整数倍。
规则三 :结构体总大小必须是最宽成员大小的整数倍。
看例子:
c
struct Test {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
直觉:1 + 4 + 1 = 6 字节。
实际 sizeof(struct Test) 通常是 12 字节。
手动算一遍:
| 步骤 | 操作 | 偏移量 |
|---|---|---|
| 1 | 放 char a |
0 |
| 2 | 放 int b,对齐要求 4,跳到偏移 4 |
4~7 |
| 3 | 放 char c |
8 |
| 4 | 结构体总大小必须是最大成员(int,4)的整数倍 | 8→12(补 3 字节) |
布局:[a][_][_][_][b][b][b][b][c][_][_][_]
3.3 优化技巧:大类型放前面
同样的成员,换个顺序能省空间:
c
struct Bad {
char a; // 偏移 0
int b; // 偏移 4~7
char c; // 偏移 8,总大小 12
};
struct Good {
int b; // 偏移 0~3
char a; // 偏移 4
char c; // 偏移 5,总大小 8(6 对齐到 8)
};
把大的成员放前面,小的放后面,能减少填充。
3.4 #pragma pack------强行改对齐
用 #pragma pack(n) 可以控制对齐:
c
#pragma pack(1) // 1 字节对齐,取消所有填充
struct Packed {
char a;
int b;
char c;
}; // 大小 = 1+4+1 = 6
#pragma pack() // 恢复默认
注意 :#pragma pack() 直接恢复默认,不保存旧值,可能污染后续代码。安全写法是用 push/pop:
c
#pragma pack(push, 1)
struct Protocol {
char head;
int len;
short cmd;
}; // 大小 1+4+2=7
#pragma pack(pop)
使用场景:
- 日常开发:默认对齐就行
- 网络报文、串口通信、驱动、文件存储:必须
pack(1)精确控制布局
4. 位段(位域)------按 bit 分配
位段让你按二进制位分配空间,而不是按字节。
c
struct Flags {
unsigned int flag1 : 1; // 占 1 bit
unsigned int flag2 : 1; // 占 1 bit
unsigned int flag3 : 3; // 占 3 bits
unsigned int data : 8; // 占 8 bits
};
适合节省内存的场景,比如协议头、状态标志。
注意:位段的跨平台性不好,不同编译器对位段的内存布局处理不一样。嵌入式里用得比较多。
5. 柔性数组------C99 的变长数组
C99 引入的特性:结构体的最后一个成员 可以是一个没有指定大小的数组。
c
struct flex {
int len;
int data[]; // 柔性数组成员,不占结构体大小
};
5.1 特点
- 柔性数组前面必须至少有一个其他成员
sizeof不包含柔性数组的大小- 需要用
malloc动态分配额外空间
c
struct flex* p = malloc(sizeof(struct flex) + 10 * sizeof(int));
p->len = 10;
for (int i = 0; i < 10; i++) {
p->data[i] = i;
}
5.2 为什么用柔性数组而不是指针
对比两种方案:
方案一:柔性数组
c
struct S {
int n;
int arr[];
};
// 一次 malloc,一次 free
方案二:指针
c
struct S {
int n;
int* arr;
};
// 两次 malloc,两次 free
柔性数组的好处:
- 内存释放更方便 :一次
free全释放,不容易漏 - 访问速度更快:数据在结构体后面连续存放,缓存友好
6. 完整示例
C 语言版本------学生管理系统(纯数据)
c
#include <stdio.h>
#include <string.h>
typedef struct Student {
char name[20];
int age;
float score;
} Student;
// 传指针,省拷贝
void printStudent(const Student* s) {
printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", s->name, s->age, s->score);
}
int main() {
Student s1 = {"张三", 20, 90.5};
Student s2 = s1; // 整体赋值
s2.age = 21;
strcpy(s2.name, "李四");
printStudent(&s1);
printStudent(&s2);
// 内存对齐演示
struct Test {
char a;
int b;
char c;
};
printf("sizeof(Test) = %zu\n", sizeof(struct Test)); // 通常是 12
return 0;
}
C++ 版本------带成员函数和构造
cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Student {
private:
int id; // private,外部不能直接访问
public:
string name;
int age;
vector<float> scores; // C++ 可以用 STL
// 构造函数
Student(const string& n, int a, int id) : name(n), age(a), id(id) {}
// 成员函数
void addScore(float s) {
scores.push_back(s);
}
float getAverage() const {
if (scores.empty()) return 0;
float sum = 0;
for (float s : scores) sum += s;
return sum / scores.size();
}
void print() const {
cout << "ID: " << id << ", 姓名: " << name << ", 年龄: " << age << endl;
cout << "平均分: " << getAverage() << endl;
}
};
// 继承
struct GraduateStudent : Student { // 默认 public 继承
string researchTopic;
GraduateStudent(const string& n, int a, int id, const string& topic)
: Student(n, a, id), researchTopic(topic) {}
};
int main() {
Student s1("张三", 20, 1001);
s1.addScore(85.5);
s1.addScore(92.0);
s1.addScore(78.5);
s1.print();
GraduateStudent gs("李四", 25, 2001, "人工智能");
gs.addScore(95.0);
gs.print();
cout << "研究方向: " << gs.researchTopic << endl;
// C++ struct 和 class 的唯一区别:默认 public
struct Point { int x, y; }; // 默认 public
class Point2 { int x, y; }; // 默认 private,编译报错
return 0;
}
7. 总结与建议
| 场景 | 用什么 |
|---|---|
| 纯 C 项目,只打包数据 | struct |
| C++ 项目,简单的 POD 类型(Plain Old Data) | struct |
| C++ 项目,有复杂行为、需要封装 | class |
| 需要和 C 代码交互 | struct(两边兼容) |
| 网络协议、文件格式解析 | struct + #pragma pack(1) |
一句话总结:
- C 语言 :
struct就是数据容器,简单直接 - C++ 语言 :
struct约等于class,区别只是默认publicvsprivate - 内存对齐是编译器干的活,但你知道规则才能写出内存紧凑的代码
- 柔性数组是 C 语言里处理变长数据的利器,比指针方案更优雅