数组、字符串、结构体与枚举:C 如何组织一组数据
变量能保存单个值。 真实程序需要组织一组值。 C 用数组表示连续元素,用字符串表示以零结尾的字符序列,用结构体把多个字段聚合成一个对象,用枚举给整数状态起名字。 这些语法看似基础,却直接连接到内存布局、越界访问、协议格式和 ABI。
数组是一段连续元素
数组保存一组同类型元素。
int scores[3] = {90, 80, 70};
内存里通常是连续排列:
scores
├── scores[0] = 90
├── scores[1] = 80
└── scores[2] = 70
下标从 0 开始。
长度为 3 的数组,合法下标是 0、1、2。
访问 scores[3] 越界。
数组不会自动记录长度
在表达式中,数组名经常会转换为指向首元素的指针。 函数参数里的数组写法也会退化。
void print_scores(int scores[], size_t count);
这里 scores 本质上是指针参数。
函数不知道数组长度,所以必须单独传 count。
void print_scores(const int* scores, size_t count) {
for (size_t i = 0; i < count; ++i) {
printf("%d\n", scores[i]);
}
}
指针加长度是 C API 的经典组合。 长度丢失是大量越界问题的根源。
sizeof 数组和指针不同
int values[4] = {1, 2, 3, 4};
size_t bytes = sizeof values;
size_t count = sizeof values / sizeof values[0];
在数组仍是数组的作用域里,sizeof values 是整个数组字节数。
但如果传入函数,参数变成指针。
void f(int values[]) {
printf("%zu\n", sizeof values); // 指针大小,不是数组大小
}
这类错误很常见。
不要在函数里用 sizeof 推断数组参数长度。
C 字符串以 '\0' 结束
C 字符串不是对象类型。 它是一段以空字符结束的字符数组。
char name[] = "ZeroBug";
实际内存:
Z e r o B u g \0
最后的 '\0' 是结束标记。
缺少它时,字符串函数会继续向后读,直到碰到某个零字节。
这会造成越界读取和信息泄漏。
字符数组和字符串字面量
char a[] = "hello";
const char* b = "hello";
a 是可修改数组副本。
b 指向字符串字面量,通常不应修改。
b[0] = 'H'; // 高风险,通常是未定义行为
初学时记住:字符串字面量当只读数据处理。
安全处理字符串需要长度意识
很多传统 C 字符串函数依赖 '\0'。
如果输入不可信,必须限制长度。
char buffer[16];
snprintf(buffer, sizeof buffer, "%s", input);
snprintf 知道目标缓冲区大小。
这比无界复制安全。
但仍要检查返回值,确认是否被截断。
结构体聚合不同字段
typedef struct Point {
int x;
int y;
} Point;
使用:
Point p = { .x = 10, .y = 20 };
printf("%d\n", p.x);
结构体把多个字段合成一个对象。 它适合表达业务实体、协议解析结果、资源句柄状态。
结构体有 padding
结构体字段之间可能有填充字节。
typedef struct Packet {
char tag;
int length;
} Packet;
内存可能是:
offset 0: tag
offset 1-3: padding
offset 4-7: length
因此不要默认结构体大小等于字段大小相加。 跨网络和文件格式时,不要直接写出结构体原始内存。
结构体指针使用 ->
Point p = {1, 2};
Point* ptr = &p;
printf("%d\n", ptr->x);
ptr->x 等价于 (*ptr).x。
它先解引用指针,再访问字段。
如果 ptr 为空或悬垂,就会出错。
嵌套结构体
typedef struct Rect {
Point left_top;
Point right_bottom;
} Rect;
嵌套结构体让数据模型更接近真实对象。 但也要注意整体复制成本和字段对齐。
Rect r = {
.left_top = {0, 0},
.right_bottom = {100, 100},
};
枚举给状态起名字
typedef enum Status {
STATUS_OK,
STATUS_NOT_FOUND,
STATUS_PERMISSION_DENIED
} Status;
枚举让整数状态可读。
Status status = STATUS_OK;
不要用裸整数到处表示状态。 状态越清晰,错误处理越可靠。
switch 和枚举配合
switch (status) {
case STATUS_OK:
break;
case STATUS_NOT_FOUND:
break;
case STATUS_PERMISSION_DENIED:
break;
}
编译器 warning 可以帮助发现遗漏分支。 状态机和错误码尤其适合枚举。
union 共享存储
union 的多个成员共享同一段存储。
typedef union Value {
int i;
float f;
} Value;
它适合节省空间或表达底层布局。 但读取的成员和最后写入的成员不匹配时,语义复杂且风险高。 初学阶段先谨慎使用。
数据模型设计
结构体和枚举可以组合成清晰模型。
typedef enum TokenKind {
TOKEN_NUMBER,
TOKEN_PLUS,
TOKEN_END
} TokenKind;
typedef struct Token {
TokenKind kind;
int value;
} Token;
这比用一堆平铺变量更容易维护。 数据结构应该表达不变量。
工程风险
数组和结构体常见风险:
- 数组越界。
- 函数参数丢失数组长度。
- 字符串缺少
'\0'。 - 无界字符串复制。
- 结构体 padding 泄漏敏感数据。
- 直接把结构体当协议格式。
- 结构体浅拷贝复制资源句柄。
- 枚举缺少默认错误处理。
这些都是安全和稳定性问题,不是语法细节。 如果结构体里包含文件句柄、权限标记或外部 buffer 指针,还要把资源释放、权限隔离和审计日志纳入设计,避免一次普通复制变成泄漏或越权访问。
小结
数组让同类型元素连续排列。 字符串用零结尾约定表达文本。 结构体把多个字段聚合为对象。 枚举给状态和值域起名字。 这些基础数据组织方式,是后续指针、内存布局、ABI 和 C++ 对象模型的前置地基。