C 语言指针:从内存地址到灵活的数据访问

C 语言指针:从内存地址到灵活的数据访问

在 C 语言中,指针既是最有力量的工具之一,也是初学者最容易困惑的概念之一。它并不是一种神秘的"特殊数据",而是把内存地址显式地交给程序员管理。理解了地址、类型和解引用之间的关系,就能自然地掌握数组遍历、函数传参、动态内存以及复杂数据结构的基础。

一、指针到底是什么

计算机内存可以看成许多连续的小单元,每个单元通常占用 1 个字节,并拥有一个唯一的编号,这个编号就是地址。程序中的变量存放在某个内存位置,变量的地址可以通过取地址运算符 & 得到。

c 复制代码
#include <stdio.h>

int main(void)
{
    int value = 10;
    printf("value = %d\n", value);
    printf("address = %p\n", (void *)&value);
    return 0;
}

如果希望把这个地址保存起来,就需要使用指针变量:

c 复制代码
int value = 10;
int *ptr = &value;

这里的 ptr 是一个指针变量,它保存的是 value 的地址。可以把它理解为一张"内存位置的便签":便签上记录了目标数据在哪里,而不是目标数据本身。

指针变量自身也要占用内存。32 位环境中,地址通常占 4 个字节,因此指针变量常见大小为 4 个字节;64 位环境中,地址通常占 8 个字节,因此指针变量常见大小为 8 个字节。指针的大小取决于平台的地址宽度,而不是它所指向的数据类型。

二、指针类型决定了访问方式

指针声明的一般形式是:

c 复制代码
type *name;

例如:

c 复制代码
char   *char_ptr;
short  *short_ptr;
int    *int_ptr;
float  *float_ptr;
double *double_ptr;

这些指针都保存地址,但它们指向的数据类型不同。类型信息主要影响两个动作:指针移动时跨过多大的距离,以及解引用时访问多少字节。

1. 指针加减整数

假设 int 占 4 个字节:

c 复制代码
int number = 10;
int *int_ptr = &number;

int_ptr + 1 并不是让地址只增加 1 个字节,而是向后移动一个 int 的大小,通常增加 4 个字节。类似地,char * 每次移动通常跨过 1 个字节,double * 每次移动通常跨过 8 个字节。

因此,指针类型实际上为地址运算提供了"步长"。这也是指针能够方便遍历数组的原因。

2. 解引用

解引用运算符 * 用来访问指针所指向的对象:

c 复制代码
int value = 10;
int *ptr = &value;

printf("%d\n", *ptr);  // 读取 value
*ptr = 20;             // 修改 value

char * 解引用时按照字符大小访问,int * 解引用时按照整型大小访问。指针类型不只是为了让声明更清晰,它还决定了程序读写内存的范围。使用错误的指针类型可能造成数据截断、覆盖相邻字节,甚至引发未定义行为。

三、野指针:最需要警惕的指针问题

野指针是指向未知、无效或已经不再属于当前对象的地址的指针。通过野指针读写内存,结果可能是程序崩溃,也可能悄悄破坏其他数据。

1. 未初始化

局部指针变量如果没有显式初始化,里面可能是任意值:

c 复制代码
int *ptr;    // 未初始化
*ptr = 20;   // 错误:ptr 指向未知位置

声明指针时,如果暂时没有有效目标,应将其初始化为空指针:

c 复制代码
int *ptr = NULL;

2. 越过有效范围

指针遍历数组时,必须严格限制在数组范围内:

c 复制代码
int values[10] = {0};
int *ptr = values;

for (int i = 0; i < 10; ++i) {
    *(ptr + i) = i;
}

当指针移动到数组之外并继续解引用时,就会访问不属于数组的内存。数组最后一个元素后的位置可以用于比较或作为"结束位置",但不能解引用。

3. 指向的空间已经失效

动态内存释放后,原指针仍然保留旧地址,但那块空间已经不能再使用。释放后应立即置空:

c 复制代码
free(ptr);
ptr = NULL;

此外,不要从函数中返回局部变量的地址,因为函数返回后局部变量的生命周期已经结束。

4. 使用前检查有效性

在解引用前,确认指针指向的对象仍然存在,并在需要时判断它是否为 NULL

c 复制代码
if (ptr != NULL) {
    *ptr = 20;
}

空指针检查只能排除 NULL,不能自动证明一个非空地址一定有效。指针的生命周期和指向范围仍然需要程序员自己维护。

四、指针运算的三种形式

1. 指针与整数相加减

ptr + nptr - n 表示以当前指针类型为步长,向后或向前移动 n 个元素。它们最适合用于数组遍历:

c 复制代码
int values[5] = {10, 20, 30, 40, 50};
int *ptr = values;

for (int i = 0; i < 5; ++i) {
    printf("%d\n", *(ptr + i));
}

2. 指针相减

两个指针相减,得到的是它们之间相隔的元素个数,而不是字节数。这个操作要求两个指针指向同一个数组中的元素,或者一个指向数组末尾的"尾后位置":

c 复制代码
#include <stddef.h>

int my_strlen(const char *str)
{
    const char *end = str;
    while (*end != '\0') {
        ++end;
    }
    return (int)(end - str);
}

在这个例子中,end - str 得到字符串中字符的数量。

3. 指针关系比较

同一数组中的指针可以进行大小比较。常见的遍历写法是把数组末尾的尾后位置作为边界:

c 复制代码
int values[5] = {0};
int *begin = values;
int *end = values + 5;

for (int *ptr = begin; ptr < end; ++ptr) {
    *ptr = 0;
}

这种写法清楚地表达了"从首元素走到尾后位置"。不要把指针移动到数组首元素之前,也不要对尾后指针解引用。

五、指针与数组的关系

在大多数表达式中,数组名会转换为数组首元素的地址:

c 复制代码
int values[5] = {1, 2, 3, 4, 5};

printf("%p\n", (void *)values);
printf("%p\n", (void *)&values[0]);

上面两行通常会打印相同的地址,因此可以这样让指针指向数组:

c 复制代码
int *ptr = values;

数组下标表达式和指针表达式本质上是等价的:

c 复制代码
values[i]   // 等价于 *(values + i)
ptr[i]      // 等价于 *(ptr + i)

完整遍历示例:

c 复制代码
#include <stdio.h>

int main(void)
{
    int values[] = {1, 2, 3, 4, 5};
    int size = (int)(sizeof(values) / sizeof(values[0]));
    int *ptr = values;

    for (int i = 0; i < size; ++i) {
        printf("%d ", *(ptr + i));
    }
    putchar('\n');
    return 0;
}

需要注意,数组名并不总是等同于普通指针。在 sizeof 中,数组名代表整个数组;对数组名使用 & 时,得到的是整个数组的地址。这两种情况不能简单地按首元素指针理解。

六、二级指针:保存指针变量的地址

指针变量也是变量,因此它同样有自己的地址。保存指针变量地址的指针,就是二级指针:

c 复制代码
int value = 10;
int *ptr = &value;
int **pptr = &ptr;

三者的关系可以分成两层:

  • pptr 保存 ptr 的地址;
  • *pptr 访问 ptr
  • **pptr 访问 value

二级指针可以修改一级指针本身指向的对象:

c 复制代码
int first = 10;
int second = 20;
int *ptr = &first;
int **pptr = &ptr;

*pptr = &second;  // 等价于 ptr = &second
**pptr = 30;      // 等价于 *ptr = 30

当函数需要修改调用者传入的指针,或者需要动态创建二维结构时,二级指针尤其常见。

七、指针数组:数组中的每个元素都是指针

看到 int *array[5] 时,应该先结合变量名判断整体结构:方括号优先级高于星号,所以它是一个数组,数组中有 5 个元素,每个元素的类型是 int *。换句话说,这是"存放指针的数组",而不是"指向数组的指针"。

c 复制代码
int a = 10;
int b = 20;
int c = 30;

int *array[3] = {&a, &b, &c};

for (int i = 0; i < 3; ++i) {
    printf("%d\n", *array[i]);
}

指针数组可以把多个彼此独立的对象组织在一起,常见用途包括保存多组字符串的地址、实现不规则二维数组,以及作为函数参数表。

八、写好指针代码的几个习惯

  1. 声明指针时立即初始化,暂时没有目标就使用 NULL
  2. 明确每个指针的所有权和生命周期,释放后及时置空。
  3. 进行数组遍历时使用清晰的起点和尾后位置,避免越界。
  4. 解引用前确认指针有效,并确保指针类型与目标对象匹配。
  5. 阅读复杂声明时从变量名开始,结合 []* 和类型逐层分析。
  6. 调试地址问题时使用 %p,并将地址转换为 (void *) 传给 printf

指针的核心并不在于记住许多符号,而在于始终回答三个问题:这个指针保存了什么地址?它按照什么类型解释目标内存?它当前是否仍然指向一个有效对象?只要这三个问题保持清晰,指针就会从容易出错的"黑盒"变成一种精确、高效的数据访问工具。

相关推荐
phltxy4 小时前
C 语言中的数据存储:从类型到二进制位
c语言
wuminyu6 小时前
HotSpot的轻量锁与膨胀后的ObjectMonitor状态重建原理
java·linux·c语言·jvm·c++
码不停蹄Zzz7 小时前
指针用法篇——malloc和free指针详解
c语言
彧azz7 小时前
操作系统时间管理与系统核心板块学习总结
c语言·笔记·学习·系统架构
白色的北极熊7 小时前
c语言 牛客网 BC15
c语言
老当益壮梁奶奶9 小时前
ARM汇编学习笔记(六):【i.MX6ULL】时钟系统与定时器(EPIT & GPT)
c语言·arm开发·嵌入式硬件·学习·51单片机
Navigator_Z9 小时前
LeetCode //C - 1255. Maximum Score Words Formed by Letters
c语言·算法·leetcode
牵猫散步的鱼儿10 小时前
stride 图像步长
c语言
刃神太酷啦10 小时前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html