写在前面

这里我默认你看完了 C 语言指针详解(2),
现在尝试做一些试题吧~


sizeof 对比 strlen

  • sizeof计算变量或类型所占内存空间的大小, 单位字节. 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据
  • strlen是库函数, 用来求字符串长度. strlen函数会⼀直向后找\0字符, 直到找到为⽌,所以可能存在越界查找
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));

我们来逐行分析这段代码:

  • char *p = "abcdef";
    定义了一个指向字符串字面值"abcdef"的指针p, 字符串末尾有一个\0, 用于标志字符串的结束.
  • printf("%d\n", strlen(p));
    p是指向字符串"abcdef"的指针, strlen(p)会计算从p指向的位置开始,直到遇到\0为止的字符串长度。
    结果: 6, 因为"abcdef"的长度是 6.
  • printf("%d\n", strlen(p+1));
    p+1指向的是字符串的第二个字符'b', strlen(p+1)会从"bcdef"开始计算长度,
    结果: 5, 因为"bcdef"的长度是 5.
  • printf("%d\n", strlen(*p));
    *p解引用指针p,取的是p所指向的第一个字符,也就是'a', strlen需要一个char*类型的参数,
    而这里传入了char类型, 会导致未定义行为.
    可能的后果: 程序崩溃或输出随机值(strlen偶然处理了错误的地址)
    结果: 未定义行为
  • printf("%d\n", strlen(p[0]));
    p[0]等价于*p, 结果也是'a', 和上一行代码的问题完全相同,仍然是未定义行为.
    结果: 未定义行为
  • printf("%d\n", strlen(&p));
    &pp的地址, 类型是char**(指向char*的指针).
    strlen&p解释为一个char*, 并从这个地址开始查找字符串终止符\0.
    结果是未定义行为, 因为&p不是一个合法的字符串地址, 程序会崩溃或输出随机值.

p是一个指针, 指向字符串字面量"abcdef"的第一个字符, p的类型是char*,
&pp的地址(也就是指针的地址), 它的类型是char**(指向char*的指针).
strlen的参数必须是一个char*指针, 并且这个指针必须指向一个以\0结尾的有效字符串,
如果传入一个无效的指针, 程序会引发未定义行为(可能崩溃, 也可能输出垃圾值)
strlen(&p)&p传递给strlen, strlen&p解释为一个char*,
并尝试从&p指向的地址开始, 找到第一个\0, 如果&p恰巧指向一个内存区域,
且在读取过程中没有遇到非法访问(如越界访问), 那么程序不会崩溃;
如果在内存中碰巧找到了一个\0, strlen会认为它是一个有效字符串, 并返回一个长度(可能是垃圾值)

  • printf("%d\n", strlen(&p+1));
    &p+1是指向p后一个位置的指针, strlen&p+1解释为一个char*, 尝试从该地址读取字符串.
    这是未定义行为, 因为&p+1指向的内存内容不可预测.
    结果: 输出随机值或程序崩溃
  • printf("%d\n", strlen(&p[0]+1));
    &p[0]等价于p,因为p[0]是字符串的第一个字符'a', &p[0]+1等价于p+1,
    指向字符串的第二个字符'b', strlen(&p[0]+1)等价于strlen(p+1).
    结果: 5, 因为"bcdef"的长度是 5.

总结:

  • 代码中的strlen(*p)strlen(p[0])是不合法的(未定义行为)
  • 对于strlen(&p)strlen(&p+1), 虽然可能不会崩溃, 但仍然是未定义行为

int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

需要注意的是sizeof是一个编译期操作, 它根据类型计算结果, 不涉及实际运行时的值.
我们来逐行分析这段代码(x64):

  • int a[3][4] = {0};
    定义了一个 3 行 4 列的二维数组, 数组名是a
  • printf("%d\n",sizeof(a));
    sizeof里单独放一个数组名, 表示计算整个数组的大小.
    结果: 3 * 4 * 4 = 48 (字节)
  • printf("%d\n",sizeof(a[0][0]));
    a[0][0]是二维数组中第 1 行第 1 列的元素, 类型为int
    结果: 4 (字节)
  • printf("%d\n",sizeof(a[0]));
    a[0]是数组a的第 1 行, 类型为int[4], 也可以理解为a[0]是第 1 行的数组名.
    结果: 4 * 4 = 16 (字节)
  • printf("%d\n",sizeof(a[0]+1));
    a[0]是数组的第 1 行, 类型为int[4], 是第 1 行的数组名, 数组名又是指向首元素的指针,
    a[0] + 1是一个指针运算, a[0]退化为指向第 1 行第 1 列的指针, 类型为int*,
    a[0] + 1是指向第 1 行第 2 列的指针, 类型仍为int*
    sizeof(a[0] + 1)计算的是指针的大小.
    结果: 8 (字节)
  • printf("%d\n",sizeof(*(a[0]+1)));
    a[0] + 1是指向第 1 行第 2 列的指针, 类型为int*,
    *(a[0] + 1)解引用这个指针, 得到第 1 行第 2 列的元素, 类型为int
    结果: 4 (字节)
  • printf("%d\n",sizeof(a+1));
    a是数组名, 是数组首元素的地址, 二维数组的首元素是一维数组, 但在表达式a + 1中,
    a退化为指针, 指向数组a的第 1 行, a + 1的类型为int(*)[4](指向一维数组的指针),
    sizeof(a + 1)计算的是指针的大小.
    结果: 8 (字节)
  • printf("%d\n",sizeof(*(a+1)));
    a + 1是一个指针, 指向数组a的第 2 行, 类型为int(*)[4],
    *(a + 1)解引用这个指针, 得到数组a的第 2 行, 类型为int[4].
    结果: 4 * 4 = 16 (字节)
  • printf("%d\n",sizeof(&a[0]+1));
    a[0]是数组第 1 行的数组名, &a[0]是指向数组a的第 1 行的指针, 类型为int(*)[4],
    &a[0] + 1是指向数组a的第 2 行的指针, 类型仍然是int(*)[4], sizeof(&a[0] + 1)计算的是指针的大小.
    结果: 8 (字节)
  • printf("%d\n",sizeof(*(&a[0]+1)));
    &a[0] + 1是指向数组a的第 2 行的指针, 类型是int(*)[4],
    *(&a[0] + 1)解引用这个指针, 得到数组a的第 2 行, 类型为int[4]
    结果: 4 * 4 = 16 (字节)
  • printf("%d\n",sizeof(*a));
    a是一个二维数组, 类型为int[3][4], 但在表达式中, a退化为指针(指向首元素),
    指向数组a的第 1 行(二维数组的首元素是一维数组), *a解引用这个指针, 得到数组a的第 1 行, 类型为int[4].
    结果: 4 * 4 = 16 (字节)
  • printf("%d\n",sizeof(a[3]));
    访问a[3]是越界行为, 但sizeof是编译期操作, 不会实际访问内存.
    a[3] 被视为数组的第 4 行,类型为int[4]
    结果: 4 * 4 = 16 (字节)

总结:

  1. 指针与数组的关系:
  • 数组在表达式中常常会退化成指向首元素地址的指针, 但并不是所有情况都会退化
  • 对于多维数组, 指针的层次和数组的结果密切相关, 理解每一级指针指向的对象是关键.
  1. sizeof的本质:
  • sizeof是一个编译期操作, 仅根据类型计算大小, 而不会实际访问内存.
  • 越界访问在sizeof中并不会触发运行时错误, 但在其他表达式中可能导致未定义行为.
  1. 未定义行为 UB:
  • 未定义行为是 C 语言中需要特别警惕的问题, 即使代码能够运行出某些结果, 也无法保证这些结果在不同编译器或平台上的一致性.
  • 避免将无效指针传递给函数, 或访问越界的数组元素.

写在最后

理解 C 语言的底层实现有助于掌握其他高级语言, 比如指针、内存布局等概念, 对编写高效、可靠的程序非常重要.