内存操作 中
356.2.3.2 内存操作(中)
/* 引入 printf 函数的声明。 */
#include <stdio.h>
/* 引入 memset 函数的声明。 */
#include <string.h>
/* 定义本节继续使用的内存填充测试函数。 */
void Memset_Test()
{
/* 定义含 15 个 int 元素的局部数组。 */
int buf[15];
/* 输出当前测试项目的提示文字。 */
printf("memset test func\r\n");
/* 输出课程计算的元素个数和 int 大小;原代码的 %ld 并非 size_t 的可移植格式。 */
printf("sizeof buf byte num:%ld, int size:%ld\r\n", sizeof(buf) / sizeof(int), sizeof(int));
/* 视频末尾暂时注释了填充操作。 */
// memset(buf, 0, sizeof(buf));
/* 视频末尾暂时注释了原来的遍历循环。 */
// for (char i = 0; i < sizeof(buf); i++)
/* 这是被注释循环原来的左花括号。 */
// {
/* 这是被注释循环原来的元素输出语句。 */
// printf("%d ", buf[i]);
/* 这是被注释循环原来的右花括号。 */
// }
/* 视频末尾也暂时注释了循环后的换行。 */
// printf("\r\n");
}
/* 程序从 main 函数开始执行。 */
int main()
{
/* 输出整个内存函数实验的标题。 */
printf("GNU Memory Function Test\r\n");
/* 调用上面定义的测试函数。 */
Memset_Test();
/* 返回 0,表示程序正常结束。 */
return 0;
}
上面是本节视频结束时的 main.c。课程先用 char buf[15] 验证 sizeof 和 memset,随后暂时注释填充与遍历代码,把数组改成 int buf[15],专门观察数组总字节数、单个元素大小和元素个数之间的关系。本节尚未开始 memcmp、memcpy 或 memmove,因此没有提前收录配套源码中的后续函数。
顶部代码严格保留了视频中的 %ld。但 sizeof 的结果类型是 size_t,并不保证等于 long int;可移植的输出格式应为 %zu。这一错误会在正文中详细说明。
一、实际项目内存操作
上一节把数组长度 8 同时写在声明、memset 和循环条件中。一旦把数组扩为 15 个元素,就必须修改多处数字,容易漏改。视频因此把代码逐步改为使用 sizeof。
本节实验经历了三个阶段:
| 阶段 | 数组声明与表达式 | 视频平台结果 | 说明 |
|---|---|---|---|
| 1 | char buf[15]、sizeof(buf) | 15 | char 的大小按定义就是 1 个 C 字节 |
| 2 | int buf[15]、sizeof(buf) | 60 | 该平台的 sizeof(int) 为 4,所以总大小为 15 × 4 |
| 3 | sizeof(buf) / sizeof(int) | 15 | 总字节数除以单个元素的字节数,得到元素个数 |
这里的函数名和关键字是 sizeof,不是转录中的“size off”;它是 C 语言运算符,不是运行库函数,所以更准确的说法是“sizeof 表达式产生一个结果”,而不是“函数返回”。
sizeof 有两种常见写法:
sizeof expression
sizeof(type_name)
对表达式使用时,括号通常不是必需的,因此 sizeof buf 与 sizeof(buf) 等价;对类型名使用时必须写括号,例如 sizeof(int)。
sizeof 产生其操作数所占的 C 字节数,结果类型为 size_t。size_t 是足以表示对象大小的无符号整数类型,具体底层类型由实现决定:可能是 unsigned int、unsigned long 或其他无符号整数类型,不能写死成“long unsigned int”。
视频第一次写成:
printf("sizeof buf len:%d\r\n", sizeof(buf));
GCC 警告 %d 需要 int,而实参是当前平台上的 long unsigned int。老师随后改成 %ld,虽然画面中的数值能正常显示,但 %ld 要求的实参是 long int,与 unsigned long 仍然不是同一类型。可变参数函数不会替调用者自动修复这种格式类型不匹配;严格来说,格式说明与实参类型不匹配会导致未定义行为。
C99 为 size_t 定义了 z 长度修饰符,正确、可移植的写法是:
printf("sizeof buf:%zu, int size:%zu\r\n", sizeof(buf), sizeof(int));
顶部最终代码中的除法结果仍是 size_t,因此同样应使用 %zu:
printf("element count:%zu, element size:%zu\r\n",
sizeof(buf) / sizeof(buf[0]),
sizeof(buf[0]));
后一个版本还把 sizeof(int) 改成 sizeof(buf[0])。这样即使以后把数组元素类型从 int 改成别的类型,元素个数公式也会自动保持一致。
需要进一步理解 sizeof 的几个边界:
- C 语言规定
sizeof(char) == 1,所以char buf[15]占 15 个 C 字节;一个 C 字节包含多少位由CHAR_BIT给出,标准要求至少 8 位,常见 GNU/Linux 平台为 8 位。 sizeof(int)是实现定义结果。视频所用 Ubuntu/目标 ABI 得到 4,不能据此断言“所有 Linux 的int永远都是 4 字节”。标准只规定基本整数类型之间的最小范围和大小顺序。- 对普通定长数组,本例的
sizeof(buf)通常可在翻译时确定,且不会读取数组元素。对变长数组(VLA),大小依赖运行时长度,相关sizeof表达式可能在运行时求值。 sizeof的单位始终是字节,不直接表示物理内存页、CPU 字长或数组元素个数。
视频使用的编译和运行命令仍是:
gcc main.c -o main
./main
建议学习阶段打开更多诊断:
gcc -std=c99 -Wall -Wextra -Wformat=2 main.c -o main
-Wall 会启用常用警告,其中包括 -Wformat;若还希望专门检查格式字符串的有符号/无符号不匹配,可再启用 -Wformat-signedness。警告不一定阻止生成程序,但不应因为“仍能运行”就忽略它。
二、内存操作注意事项
对于真正的数组对象,元素个数的通用计算式是:
[ \text{元素个数} = \frac{\operatorname{sizeof}(\text{整个数组})}{\operatorname{sizeof}(\text{一个元素})} ]
对应 C 代码为:
size_t count = sizeof(buf) / sizeof(buf[0]);
以视频最终的 int buf[15] 为例,在该平台上:
[ \operatorname{sizeof}(buf)=15\times\operatorname{sizeof}(int)=15\times4=60 ]
所以:
[ 60\div4=15 ]
视频最后仍把输出标签写成 sizeof buf byte num,但实际传入的第一个值已经是 sizeof(buf) / sizeof(int),即元素个数 15,不再是字节数 60。变量名、日志标签和实际含义必须保持一致,否则调试输出会误导读者。
同一个 sizeof(buf) 在不同位置承担的含义也不同:
memset(buf, 0, sizeof(buf));
memset 的第三个参数本来就要求字节数,所以填充整个数组时直接传 sizeof(buf) 是正确的。相反,遍历数组时循环次数应是元素个数:
for (size_t i = 0; i < sizeof(buf) / sizeof(buf[0]); i++) {
printf("%d ", buf[i]);
}
如果把 int buf[15] 的 sizeof(buf)(视频平台上为 60)直接当作循环次数,循环会访问 buf[15] 到 buf[59],严重越界。越界读写属于未定义行为,可能破坏其他局部数据或造成崩溃。
课程原循环使用 char i。数组较小时可以计数,但不适合作为通用数组下标:char 的范围有限,而且与无符号的 size_t 比较时会发生整数转换。与数组大小和元素个数配套的下标类型通常应使用 size_t。
数组与指针是本公式最容易踩的坑
元素个数公式只在 buf 仍是数组对象的作用域中成立。数组作为函数实参传递时,形参声明中的数组形式会调整为指针:
void print_array(int buf[])
{
/* 此处的 sizeof(buf) 是指针大小,不是调用者数组大小。 */
}
即使形参写成 int buf[15],函数体内的 buf 仍按 int * 处理,sizeof(buf) / sizeof(buf[0]) 得不到 15。正确设计是由调用者把元素个数一并传入:
void print_array(const int *buf, size_t count);
动态分配得到的内存也只有一个指针,sizeof(pointer) 同样无法恢复分配时的字节数或元素个数,程序必须另外保存长度。
memset 与整数数组
对视频中的 int buf[15] 执行:
memset(buf, 0, sizeof(buf));
会把整个数组对象表示的所有字节清零;对于标准整数类型,这可得到数值 0。若第二个参数改为非零值,例如 2,则每个字节都会写成 0x02,并不是把每个 int 元素赋值为整数 2。给整数数组设置任意非零值时,应使用逐元素赋值,而不能把 memset 当作通用的数组赋值函数。