知识点

C指针与数组

定义

指针是存放地址的变量,类型写T *。指针本身占4或8个字节(取决于平台),而 *p的类型是T:取值时读几个字节、按什么规则解释,由T决定。

取地址用 &,解引用用 *:&x得到x的地址,*p取出p指向的对象。两者互逆,*&x就是x。

数组是一段连续存放的同类型元素。在表达式中,数组名(除sizeof、& 和字符串字面量初始化之外)会退化为指向首元素的指针,所以arr与 &arr[0] 的值相同、类型不同:

array-decay.c
#include <stdio.h>
int main(void) {
int a[5];
int *p = a; /* 等价于int *p = &a[0]; */
printf("%zu\n", sizeof(a)); /* 20:整个数组的字节数 */
printf("%zu\n", sizeof(p)); /* 8:指针本身的字节数 */
return 0;
}

数组名不是可修改的左值,a = p和a++ 都不合法;指针变量可以重新赋值。

性质

下标就是指针运算

标准把a[i] 定义为 *(a + i)。加法可交换,所以a[i] 与i[a] 等价(能编译,但不要这么写)。理解这一点后,下面三种写法取到的是同一个元素:

subscript.c
#include <stdio.h>
int main(void) {
int a[5] = {0, 1, 2, 3, 4};
int *p = a;
printf("%d %d %d\n", a[2], *(p + 2), p[2]); /* 2 2 2 */
return 0;
}

指针运算按元素计数

  • p + 1前进sizeof(*p) 个字节,不是1个字节。int *p在4字节int的平台上p + 1前进4字节。
  • p - q得到两个地址之间相差的元素个数,类型是ptrdiff_t,用 %td输出。
  • 指针可以加、减整数,也可以相减;两个指针相加没有定义。
  • 指针运算只在同一数组(或数组末尾后一位)内有定义,跨对象的相减是未定义行为。

传参时数组退化为指针

形参写成数组形式只是语法糖:void f(int a[10]) 与void f(int *a) 完全等价,方括号里的数字被忽略。函数内部a是指针,sizeof(a) 得到指针大小,拿不到数组长度。因此长度必须单独传:

sum.c
#include <stdio.h>
int sum(const int *a, int n) {
int s = 0;
for (int i = 0; i < n; i++) {
s += a[i]; /* 也可以写 *(a + i) */
}
return s;
}
int main(void) {
int a[4] = {1, 2, 3, 4};
printf("%d\n", sum(a, 4)); /* 实参数组名退化为指针 */
return 0;
}

const int *a表示不能通过a修改元素;如果连指针本身也不想改,写成const int * const a。

字符串是以 \0结尾的字符数组

C没有独立的字符串类型,字符串就是以 '\0'(ASCII 0)结尾的char数组。char s[] = "abc"; 占4个字节:'a'、'b'、'c'、'\0'。

  • strlen(s) 从头数到 \0为止,得到3,不含结尾符。
  • sizeof(s) 得到整个数组的字节数4,包含结尾符。
  • 对没有 \0的数组调用strlen,会一直读到数组外,属于越界读。

解引用与自增的结合

后置 ++ 的优先级高于一元 *,所以 *p++ 等价于 *(p++):先取 *p的值,再让指针前进一位。

写法等价形式效果
*p++*(p++)取当前元素的值,指针后移
(*p)++—当前元素的值加1,指针不动
*++p*(++p)指针先后移,再取新位置的值
++*p++(*p)当前元素的值加1,指针不动

越界、悬垂与未初始化

  • 下标越界:访问a[n] 之外的元素是未定义行为,写操作可能覆盖相邻变量或返回地址,且不一定立刻崩溃。
  • 悬垂指针:指向已经free的内存,或指向已经返回的函数局部变量。解引用是未定义行为。
  • 未初始化指针:值是随机的,解引用等于访问任意地址。指针应初始化为NULL或有效地址,并在解引用前判空。
  • 数组名不可修改:a++ 不合法,需要移动时另用一个指针。

例子

sizeof与strlen的区别

string-size.c
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "abc";
char *p = s;
printf("%zu %zu\n", sizeof(s), strlen(s)); /* 4 3 */
printf("%zu %zu\n", sizeof(p), strlen(p)); /* 8 3(指针宽度随平台)*/
return 0;
}

sizeof在编译期确定,包含结尾的 \0;strlen在运行期数到 \0为止。同一个字符串,用数组名和用指针得到的大小不同。

指针运算的步长与差值

pointer-step.c
#include <stdio.h>
int main(void) {
int a[4] = {10, 20, 30, 40};
int *p = a;
printf("%d\n", *p); /* 10 */
printf("%d\n", *(p + 2)); /* 30 */
printf("%td\n", (p + 3) - a); /* 3:差值是元素个数 */
printf("%zu\n", sizeof(*p)); /* 4:一个int的字节数 */
return 0;
}

如果sizeof(int) 为4,p + 1的地址比p大4个字节,但 (p + 1) - p的值是1。

*p++ 与 (*p)++ 的实际效果

deref-increment.c
#include <stdio.h>
int main(void) {
int a[3] = {1, 2, 3};
int *p = a;
int x = *p++; /* x = 1,p移到a[1] */
int y = (*p)++; /* y = 2,a[1] 变成3,p不动 */
printf("%d %d %d %d\n", x, y, a[0], a[1]); /* 1 2 1 3 */
return 0;
}

第一行取走a[0] 的值并把指针移到下一个元素;第二行读取a[1] 的值,再把这个位置上的数加1。a[0] 始终是1。

题集

练习

  1. int a[5]; 之后,表达式sizeof(a) / sizeof(a[0]) 的值是 。

    参考解答

    sizeof(a) 是整个数组的字节数,sizeof(a[0]) 是一个元素的字节数,相除得到元素个数。这个写法只在数组名仍在当前作用域时有效。

  2. 若sizeof(int) 为4,int *p执行p + 1后,地址增加 个字节。

    参考解答

    指针加法按元素计数,前进量是sizeof(*p),即4个字节。

  3. 在同一个数组中,p2 - p1得到的是:

    选择答案
    参考解答

    指针相减按元素计数,结果是ptrdiff_t;前提是两个指针指向同一数组(或末尾后一位),否则行为未定义。

  4. char s[] = "abc"; 之后,sizeof(s) 的值是 ,strlen(s) 的值是 。

    参考解答

    数组包含结尾的 \0,共4个字节;strlen不数结尾符,返回3。

  5. 下列关于指针与数组的说法,正确的是:

    题 undefined-behaviour · choice
    参考解答

    形参中的数组退化为指针,sizeof(a) 得到的是指针大小,C错。

交互探索

指针加一,地址增加多少?

沿数组移动指针,观察元素索引、地址和尾后位置。

正在准备演示…