数组与字符串
本节目标
查询 C23 数组类型、边界、参数调整、多维布局、字符数组、字符串字面量与 VLA 边界。
本章用于查询 C23 的数组对象、数组参数和语言层字符串表示。遇到数组代码时,先确认表达式在当前位置仍是数组还是已经转换为指针,再核对元素数量、遍历终点和字符序列是否有终止空字符。
数组类型、元素与有效下标
| 查找项 | 规则 |
|---|---|
| 数组类型 | 由元素类型和长度共同构成;int[5] 是含 5 个 int 元素的数组类型 |
| 有效下标 | 对长度为 N 的数组,有效下标从 0 到 N - 1 |
| 尾后位置 | 可以形成指向最后一个元素之后的指针,但该位置不对应元素,不能解引用 |
数组元素按下标递增顺序连续排列。values[index] 等价于对 values + index 间接访问,因此使用下标前必须同时证明 index >= 0(若它是有符号类型)且 index < N。关于对象大小与类型范围,可回查基本类型、对象、声明与初始化。
尾后指针适合表示半开区间 [begin, end):循环可以比较当前位置是否到达 end,却不能把 *end 当成元素。越界形成不可用的指针值或访问数组范围外的对象都会越过语言保证。
数组初始化、长度推断与 sizeof
| 写法 | 得到的数组 |
|---|---|
int values[5] = {3, 1}; | 长度为 5;其余元素执行默认初始化,对整数得到 0 |
int values[] = {3, 1, 4, 1, 5}; | 由初始化器推断长度为 5 |
char text[] = "C23"; | 由字符串字面量推断足以容纳字符和终止空字符的长度 4 |
省略数组长度时,初始化器会补全数组类型;普通初始化器列表以具有显式初始化器的最大下标确定长度。已写明更大长度但没有覆盖全部元素时,剩余元素执行默认初始化。更完整的初始化规则可回查基本类型、对象、声明与初始化。
对一个完整的数组对象,sizeof array 是整个数组的字节数,sizeof array[0] 是一个元素的字节数,二者相除得到元素数量:
const int array[] = {3, 1, 4, 1, 5};
const size_t count = sizeof array / sizeof array[0];
sizeof array / sizeof array[0] 只在 array 仍是数组对象的作用域内计算元素数。把同样的文本复制到数组形参内部会得到指针大小与元素大小之比,不再是调用方数组的长度。
数组到指针的转换
| 表达式 | 此处的含义 |
|---|---|
values | 在大多数表达式中转换为指向首元素的 int * |
sizeof values | 不转换;操作数仍具有完整数组类型 |
&values | 不转换;得到指向整个数组的 int (*)[5] |
通常,数组类型表达式会转换为指向首元素的指针,而且转换后的表达式不再携带数组长度。C23 还规定 typeof 运算符的操作数和用字符串字面量初始化数组等场景不进行这种转换;本页计算长度使用的是 sizeof 例外,区分首元素地址与整个数组地址使用的是一元 & 例外。
values 和 &values[0] 在这里都指向首元素,类型为 int *;&values 的地址数值可能相同,类型却是指向整个数组的指针,步进单位也不同。数组和指针的类型关系将在指针、指针运算与回调中继续展开。
数组参数与长度契约
| 接口写法 | 调用契约 |
|---|---|
sum(const int values[], size_t count) | values 调整为指针;调用方另传可访问的元素数量 |
sum(size_t count, const int values[static count]) | 同样调整为指针,并要求每次调用都能从实参访问至少 count 个元素 |
数组形参声明会调整为相应的指针形参类型。因此,函数体内的 values 是一个指针形参,sizeof values 给出指针大小,接口必须用显式参数或其他明确协议传递长度。
static int sum_values(const int values[], size_t count) {
int total = 0;
for (size_t index = 0; index < count; ++index) {
total += values[index];
}
return total;
}
数组形参最外层方括号里的 static 表达最小可访问元素数这一前置条件,不是自动边界检查。调用方若不能满足,就不应调用该函数;函数仍应使用与契约一致的显式长度。形参由实参值初始化的基础规则可回查函数、参数、返回值与递归。
多维数组与行优先布局
| 声明或访问 | 含义 |
|---|---|
int matrix[2][3] | 含 2 个元素;每个元素都是 int[3] 数组 |
matrix[row][column] | 先选中一行,再在该行中选中一个 int |
int matrix[][3] 形参 | 调整为指向 int[3] 的指针,外层长度另行约定 |
C 的多维数组按行优先顺序存储,最后一个下标变化最快。对 int matrix[2][3],内存中的元素顺序是第一行的 3 个 int,随后是第二行的 3 个 int。
int matrix[][3] 的列数属于元素类型的必要信息。编译器需要知道一行是 int[3],才能把 matrix + row 调整到正确的下一行;外层行数可以用另一个形参显式传入。
static int row_sum(const int matrix[][3], size_t row) {
return matrix[row][0] + matrix[row][1] + matrix[row][2];
}
调用前必须保证 row 位于实际行数内。每一行的列下标也只能是 0、1、2。
字符数组与字符串字面量
| 写法 | 对象与可修改性 |
|---|---|
char text[] = "C23 arrays"; | 创建独立字符数组,可修改其中字符 |
const char *label = "C23 arrays"; | 指针指向字符串字面量对应的数组,不通过它修改字符 |
char raw[3] = {'C', '2', '3'}; | 只是 3 个字符,不是空字符结尾字符串 |
字符串字面量初始化的字符数组包含终止空字符,前提是显式给出的数组长度留有空间;省略长度时,推断结果会容纳完整字面量和终止空字符。char text[] = "C23 arrays"; 因而得到长度 11 的独立数组,其中 10 个可见字符后跟 \0。
字符串字面量本身在翻译期间形成具有静态存储期的数组。即使历史接口允许把它转换给 char *,试图修改字符串字面量对应的数组时行为未定义。需要修改时应声明独立的 char[];只读访问时用 const char * 表达意图。
本章只讲语言层表示。字符串复制、比较、搜索和字符分类等接口属于后续“字符串、内存、字符分类与本地化”标准库专章,目前不提供可点击路由。
遍历、哨兵与边界
| 数据协议 | 正确终点 |
|---|---|
| 普通数组加显式长度 | index == count 时停止,循环条件写 index < count |
| 空字符结尾字符数组 | 读到 text[index] == '\0' 时停止 |
| 指针半开区间 | 当前指针等于尾后指针时停止,不解引用尾后指针 |
显式长度协议适用于可以包含任意值的数组;空字符哨兵协议只适用于保证存在 \0 的字符序列。不要为了寻找哨兵而越过对象边界:若输入不是有效的空字符结尾字符串,逐字符循环本身没有安全终点。
下面的代表程序分别使用显式长度遍历整数数组和二维数组,使用终止空字符遍历字符数组。循环边界可结合条件、循环与跳转中的 for 与 while 规则检查。
#include <stddef.h>
#include <stdio.h>
static int sum_values(const int values[], size_t count) {
int total = 0;
for (size_t index = 0; index < count; ++index) {
total += values[index];
}
return total;
}
static size_t text_length(const char text[]) {
size_t length = 0;
while (text[length] != '\0') {
++length;
}
return length;
}
static size_t count_digits(const char text[]) {
size_t count = 0;
for (size_t index = 0; text[index] != '\0'; ++index) {
if (text[index] >= '0' && text[index] <= '9') {
++count;
}
}
return count;
}
int main(void) {
const int values[] = {3, 1, 4, 1, 5};
const int matrix[][3] = {
{1, 2, 3},
{4, 5, 6},
};
char text[] = "C23 arrays";
const size_t value_count = sizeof values / sizeof values[0];
const size_t row_count = sizeof matrix / sizeof matrix[0];
printf("values-count=%zu\n", value_count);
printf("values-sum=%d\n", sum_values(values, value_count));
for (size_t row = 0; row < row_count; ++row) {
printf("row-%zu-sum=%d\n", row, sum_values(matrix[row], 3));
}
printf("text-length=%zu\n", text_length(text));
printf("text-digits=%zu\n", count_digits(text));
return 0;
}
输出:
values-count=5
values-sum=14
row-0-sum=6
row-1-sum=15
text-length=10
text-digits=2
VLA 与变长修改类型
| 查找项 | C23 边界 |
|---|---|
| VLA | 长度不是整数常量表达式,或元素类型没有已知常量大小的数组类型 |
| 变长修改类型 | 由 VLA 类型继续派生的类型也属于变长修改类型,声明受作用域和链接规则限制 |
| 自动存储期 VLA | C23 中仍是条件特性,实现可以不支持这种数组对象 |
sizeof | 普通固定长度数组通常在翻译期确定;涉及 VLA 时可能依赖保存的运行期长度 |
函数原型作用域中的非整数常量数组长度会被当作 *。在函数原型作用域之外,每次非整数常量长度表达式实际求值时,结果必须大于 0。
VLA 的大小由运行期表达式决定,并在该 VLA 实例的生存期内保持不变。例如下面的 values 只应在实现支持自动存储期 VLA 且 count > 0 时声明:
void use_buffer(size_t count) {
int values[count];
// 在此作用域内使用 values,并确保所有下标小于 count。
}
变长修改类型的普通标识符必须无链接,并位于块作用域或函数原型作用域;VLA 对象不能具有静态或线程存储期。指向 VLA 的指针也属于变长修改类型,但它与“对象本身是 VLA”不是同一限制。
sizeof 遇到可变长类型时可能需要在运行期确定大小。当长度表达式位于 sizeof 或 typeof 的操作数中,且改变该表达式的值不影响运算结果时,C23 未指明是否求值该长度表达式。因此不要在 VLA 长度或相关操作数里藏副作用;把长度先计算成一个已验证为正的值,更容易移植和审查。
常见陷阱
| 陷阱 | 行为边界或替代做法 |
|---|---|
访问 array[N] 或更远位置 | 越界访问是未定义行为;长度为 N 时最后一个元素是 array[N - 1] |
| 解引用尾后指针 | 尾后指针可用于比较和区间终点,但不指向元素;解引用导致未定义行为 |
在数组形参内用 sizeof parameter 推断长度 | 形参已经调整为指针;显式传入长度 |
遍历没有终止空字符的字符数组直到 \0 | 读取越过数组边界时是未定义行为;改用显式长度 |
| 通过指针改写字符串字面量 | 字符串字面量修改是未定义行为;需要修改时复制到 char[] |
| 省略二维数组形参的内层长度 | 编译器无法确定行类型;保留内层长度并另传行数 |
| 默认所有 C23 实现都支持自动存储期 VLA | 这是条件特性;可移植接口优先使用显式长度与调用方提供的存储 |
排查数组和字符串问题时,按“对象的真实类型与长度 → 是否发生数组到指针转换 → 参数长度契约 → 每一维的有效下标 → 字符序列是否终止 → 实现是否支持所用 VLA”逐项核对。