指针(一):指针的概念与定义、取地址运算符(&)与解引用运算符(*)、指针的算术运算、指针与数组的关系

指针,是C语言的灵魂,也是很多初学者觉得“难啃”的硬骨头。说实话,我当年学指针时也绕了好一阵子。但一旦你理解了它的本质——指针就是地址,一切就豁然开朗了。

这一章,我们就把指针最核心的几个概念掰开揉碎,讲清楚。

1. 指针的概念与定义

指针是什么?说白了,指针就是一个变量,它存的是另一个变量的内存地址。你想想看,普通变量存的是值,指针变量存的是地址。

定义指针的语法很简单:

int *p;   // p是一个指向int类型变量的指针
char *cp; // cp是一个指向char类型变量的指针
double *dp; // dp是一个指向double类型变量的指针

这里的 * 是类型说明符,告诉编译器:我要定义一个指针变量。至于它指向什么类型,由前面的类型名决定。

个人习惯:我写指针时喜欢把 * 紧挨着变量名,比如 int *p;。这样读起来更自然——“p是一个指针,指向int”。

指针变量本身也占内存空间。在32位系统上,所有指针都是4字节;在64位系统上,都是8字节。不管它指向的是int、char还是结构体,指针变量的大小都一样。

2. 取地址运算符(&)与解引用运算符(*)

这两个运算符是操作指针的左右手。

  • &:取地址运算符,获取一个变量的内存地址
  • *:解引用运算符,通过指针访问它指向的变量

看个例子:

int a = 10;
int *p = &a;  // p保存了a的地址

printf("a的值:%d\n", a);    // 输出10
printf("a的地址:%p\n", &a); // 输出a的地址
printf("p的值:%p\n", p);    // 输出p的值,也就是a的地址
printf("*p的值:%d\n", *p);  // 输出10,通过p访问a

*p = 20;  // 等价于 a = 20
printf("a的新值:%d\n", a);  // 输出20

这里有个容易混淆的点:定义时的 * 和用时的 * 是两回事。定义时 int *p* 是类型说明符;使用时 *p* 是解引用运算符。

我曾经踩过的坑:刚学指针时,我总以为 int *p = &a; 是把 &a 赋给了 *p。其实不是!是赋给了 p 本身。记住:p 存地址,*p 才是值。

3. 指针的算术运算

指针也能做加减运算,但它的加减和普通整数不一样。指针加1,不是地址值加1,而是加上它所指向类型的大小

举个例子:

int arr[3] = {10, 20, 30};
int *p = arr;  // p指向arr[0]

printf("p = %p\n", p);      // 假设输出 0x1000
printf("p+1 = %p\n", p+1);  // 输出 0x1004(int占4字节)
printf("p+2 = %p\n", p+2);  // 输出 0x1008

为什么会这样?因为指针的加减运算考虑了类型大小。p+1 实际上跳过了4个字节,直接指向下一个int元素。

指针运算 实际效果 说明
p + n 地址 + n * sizeof(类型) 向后移动n个元素
p - n 地址 - n * sizeof(类型) 向前移动n个元素
p1 - p2 (地址差) / sizeof(类型) 两个指针之间相差的元素个数
关键点:指针的加减运算只适用于指向数组元素的指针。如果p指向一个单独的int变量,p+1的行为是未定义的——你访问了不属于你的内存。

4. 指针与数组的关系

数组名和指针之间有着千丝万缕的联系。说白了,数组名就是指向数组第一个元素的指针常量

int arr[5] = {1, 2, 3, 4, 5};

// 下面三种写法完全等价
printf("%d\n", arr[0]);   // 下标访问
printf("%d\n", *arr);     // 解引用数组名
printf("%d\n", *(arr+0)); // 指针运算

// 遍历数组的两种方式
for(int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);      // 方式一:下标
}

int *p = arr;
for(int i = 0; i < 5; i++) {
    printf("%d ", *(p + i));    // 方式二:指针
}

但要注意,数组名和指针有一个重要区别:数组名是常量,不能修改。你不能写 arr++,但可以写 p++

我在项目中遇到过:有一次调试一个通信协议解析模块,发现数据总是不对。查了半天,原来是把数组名当指针用了,在函数里写了 arr++。编译器没报错,但运行结果完全乱套。从那以后,我习惯把数组参数明确写成指针形式,提醒自己别犯这种错。

另外,当数组作为函数参数时,它会退化为指针。也就是说,下面两种函数声明是等价的:

void func(int arr[]);  // 数组形式
void func(int *arr);   // 指针形式(实际就是这种)

所以在函数内部用 sizeof(arr) 得到的是指针的大小,不是数组的大小。这个坑,我见过太多人踩了。

知识体系总览

下面这张图把本章的核心逻辑串起来了:

指针(Pointer) 概念与定义 存储地址的变量 int *p; & 与 * 运算符 &取地址 *解引用 int *p = &a; 算术运算 p+1 跳过一个元素 地址 ± n*sizeof 指针与数组 数组名 = 首元素指针 arr[i] ⇔ *(arr+i) 核心:指针就是地址,类型决定步长

这张图把指针的四个核心知识点串在了一起。你看,指针本身只是一个地址,但通过类型信息,它知道怎么解读这块内存、怎么移动、怎么和数组配合。

嗯,这一章的内容就到这里。指针的概念是基础中的基础,后面的章节我们会在这个基础上继续深入。记住我的一句话:指针不可怕,可怕的是你以为你懂了,其实没懂。多写代码,多调试,慢慢就熟了。


公众号:蓝海资料掘金营,微信deep3321