七、C语言指针

指针是 C 语言赋予程序员的上帝之手,它允许我们直接操作内存。用好了,它是神兵利器;用不好,它是程序崩溃的根源。这次将带你深入内存,理解指针的本质。

思维导图


一、 指针基础概念

1.1 什么是地址?

计算机内存就像一个巨大的公寓楼,每个字节都有一个唯一的门牌号,这就是地址

变量名只是门牌号的别名,指针则是专门用来存放门牌号的变量

1.2 语法结构:定义与赋值

语法: 类型 *指针变量名;

c 复制代码
int a = 10;
int *p;      // 1. 定义:p 是一个指向 int 的指针
p = &a;      // 2. 赋值:把 a 的地址给 p(p 指向 a)

int *q = &a; // 3. 定义并初始化(推荐写法)

1.3 两个核心运算符

& (取地址) :获取变量在内存中的位置。&a 得到的是一个十六进制的地址值。
* (解引用/间接访问)拿着钥匙开门。访问指针指向的那个位置里的数据。

代码示例:用指针修改变量

c 复制代码
int a = 10;
int *p = &a; // p 存储了 a 的地址

printf("a 的地址: %p\n", (void*)p);
printf("通过指针看 a: %d\n", *p);

*p = 20; // 核心操作!远程修改 a 的值
printf("a 现在是: %d\n", a); // 输出 20

1.4 指针类型决定视野

为什么指针需要类型?int *char * 存的都是地址(在 64 位系统上都是 8 字节),区别在哪?

区别在于解引用时的视野

int *p: *p 一次性读取 4 个字节。
char *q: *q 一次性读取 1 个字节。

代码验证:

c 复制代码
int x = 0x12345678;
char *p = (char*)&x; 
printf("%x", *p); // 在小端序机器上,输出 78 (只读了最低字节)

二、 致命三剑客:NULL、野指针、悬空指针

2.1 NULL 指针

NULL 是一个宏,通常定义为 (void*)0不指向任何有效内存。
最佳实践: 定义指针时如果暂时不用,务必初始化为 NULL

2.2 野指针

定义了指针但没初始化。它指向哪里?只有上帝知道(垃圾值)。

c 复制代码
int *p; // 野指针!
// *p = 100; // 极度危险!可能覆盖关键系统数据导致崩溃

2.3 悬空指针

指针指向的内存已经被释放,但指针还在。这是最难调试的 Bug 之一。

经典错误:返回局部变量地址

c 复制代码
int* get_val() {
    int x = 10;
    return &x; // 错误!函数结束 x 就销毁了
}

int main() {
    int *p = get_val(); // p 变成了悬空指针
    // printf("%d", *p); // 未定义行为,数据可能已经被覆盖
}

三、 指针实战:交换两个数

这是理解地址传递最经典的代码。

代码示例:

c 复制代码
// 接收地址,拥有"远程修改"的能力
void swap(int *a, int *b) {
    int temp = *a; // 取出 a 指向的值
    *a = *b;       // 把 b 指向的值赋给 a 指向的地方
    *b = temp;     // 把 temp 赋给 b 指向的地方
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y); // 必须传地址!
    printf("x=%d, y=%d\n", x, y); // 输出 x=10, y=5
    return 0;
}

四、 指针与数组

4.1 数组名的真面目

在表达式中,数组名 arr退化为指向首元素的指针。

核心等价公式:
arr[i] ≡ *(arr + i)

4.2 用指针遍历数组

这种写法在嵌入式开发和操作系统内核中非常常见,效率极高。

代码示例:

c 复制代码
int arr[] = {10, 20, 30};
int *p = arr; // 指向开头

for (int i = 0; i < 3; i++) {
    // 写法 1:下标法(最直观)
    printf("%d ", arr[i]);
    
    // 写法 2:指针算术法(偏移)
    printf("%d ", *(p + i));
    
    // 写法 3:指针移动法(步进)
    printf("%d ", *p++); // 取值后,指针指向下一个
}

五、 指针算术与边界

5.1 +1 到底加多少?

指针加 1,不是地址值加 1,而是跳过一个数据类型的大小

char *p: p+1 地址增加 1 字节。
int *p: p+1 地址增加 4 字节。
double *p: p+1 地址增加 8 字节。

代码验证步长:

c 复制代码
int nums[] = {1, 2};
int *p = nums;
printf("当前: %p\n", (void*)p);
p++; 
printf("加一: %p\n", (void*)p); // 观察地址差值,一定是 4

5.2 两个指针相减

如果两个指针指向同一个数组,p1 - p2 得到的是它们之间相隔的元素个数 (不是字节数)。这是实现 strlen 的基础。

手写 strlen (指针版):

c 复制代码
size_t my_strlen(const char *s) {
    const char *start = s;
    while (*s != '\0') {
        s++; // 指针一直往后走
    }
    return s - start; // 尾地址 - 头地址 = 长度
}

六、 const 与指针(绕口令时间)

这是面试必考题,记住这句口诀:

const 在 * 左边,锁的是物(内容)
const 在 * 右边,锁的是人(指针)

6.1 三种形态对比

c 复制代码
int x = 10;
int y = 20;

// 1. 指向常量的指针 (Pointer to Const)
// "我不能改里面的值,但我可以指向别人"
const int *p1 = &x; 
// *p1 = 30; // 错!内容被锁
p1 = &y;     // 对!指针没锁

// 2. 常量指针 (Const Pointer)
// "我可以改里面的值,但我不能指向别人"
int * const p2 = &x;
*p2 = 30;    // 对!内容没锁
// p2 = &y;  // 错!指针被锁

// 3. 指向常量的常量指针 (双重锁定)
// "我都不能改"
const int * const p3 = &x;

七、 练习题

题目 1: 64 位系统下,int *pchar *q 占用的内存大小分别是多少?

题目 2: int a = 10; int *p = &a;*&a 的结果是什么?&*p 的结果是什么?

题目 3: 为什么指针被释放(free)后,最好立即置为 NULL

题目 4:short *p = 0x1000; (short占2字节),p + 2 的地址值是多少?

题目 5: int arr[5]; int *p = arr;,表达式 p[2] 合法吗?它等价于什么?

题目 6: 下面代码输出什么?

c 复制代码
int a[] = {1, 2, 3, 4};
int *p = a;
printf("%d", *p++);
printf("%d", *p);

题目 7: 解析声明:const char * pchar * const p 的区别。

题目 8: 如何定义一个指针,指向一个 int 类型的指针(二级指针)?

题目 9: 只有 void* 指针可以直接赋值给其他类型的指针而不需要强转(在 C 中),对吗?

题目 10: 下面代码有什么致命问题?

c 复制代码
int *p;
*p = 100;

题目 11: 两个指针相加(p1 + p2)有意义吗?两个指针相减(p1 - p2)有意义吗?

题目 12: 在大端序(Big Endian)机器上,int a = 0x12345678; char *p = (char*)&a;*p 的值是多少?

题目 13: 什么是"野指针"?与"悬空指针"有何区别?

题目 14: 函数参数 void func(const int *p) 里的 const 想表达什么意图?

题目 15: int a[3][4]; int *p = a; 这种赋值会报警告吗?为什么?

八、 解析

题 1 解析
答案: 都是 8 字节。
详解:

指针也是变量,用来存地址。在 64 位系统上,地址总线宽度是 64 位,所以所有类型的指针大小都是 8 字节。

题 2 解析
答案: 10a 的地址
详解:

*& 互为逆运算,相互抵消。

题 3 解析
答案: 防止悬空指针(Dangling Pointer)误用。
详解:

释放内存后,指针变量里的地址值还在。如果不置空,后续代码可能会误以为这块内存还能用,导致难以排查的 Bug。

题 4 解析
答案: 0x1004
详解:

步长 = sizeof(short) = 2。0x1000 + 2 * 2 = 0x1004

题 5 解析
答案: 合法。等价于 arr[2]*(p+2)
详解:

指针和数组名在下标访问上是语法糖 关系。[] 运算符本质上就是指针加法后解引用。

题 6 解析
答案: 12。
详解:

*p++ 先取值 *p (1),然后 p 自增指向下一个。第二次打印 *p 就是 2。

题 7 解析
答案:

前者指针指向的内容不可改(内容只读);后者指针本身的指向不可改(指针只读)。

题 8 解析
答案: int **pp;
详解:

二级指针,存放的是一级指针的地址。

题 9 解析
答案: 对。
详解:

void* 是通用指针,C 语言允许它隐式转换为任意类型指针(注意:C++ 不允许)。

题 10 解析
答案: 野指针解引用。
详解:

p 没有初始化,指向随机地址。写入可能导致程序立即崩溃。

题 11 解析
答案: 相加无意义;相减有意义。
详解:

两个地址相加没有物理意义。同类型指针相减表示中间相隔的元素个数(不是字节数)。

题 12 解析
答案: 0x12
详解:

大端序:高位字节存低地址。p 指向首字节(低地址),所以拿到的是高位数据 0x12

题 13 解析
答案:

野指针:未初始化的指针。

悬空指针:指向已释放内存的指针。

题 14 解析
答案: 只读承诺。
详解:

告诉调用者:"把你的数据传给我,我保证只读取,绝不修改。"这是一种契约。

题 15 解析
答案: 会报警告。
详解:

a 的类型是 int (*)[4](指向包含4个int的数组的指针),p 的类型是 int *。虽然数值可能一样,但步长不同,类型不兼容。

日期:2025年2月12日

专栏:C语言

相关推荐
hewence12 小时前
Kotlin CoroutineContext 详解
android·开发语言·kotlin
寻寻觅觅☆2 小时前
东华OJ-基础题-120-顺序的分数(C++)
开发语言·c++·算法
Myosotis5132 小时前
作业 第三次
开发语言·python
学编程的闹钟2 小时前
C语言WSAGetLastError函数
c语言·开发语言·学习
阿里嘎多学长2 小时前
2026-02-12 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Ronin3052 小时前
虚拟机数据管理模块
开发语言·c++·rabbitmq
3GPP仿真实验室2 小时前
【Matlab源码】6G候选波形:MIMO-OFDM-IM 增强仿真平台
开发语言·网络·matlab
wengqidaifeng2 小时前
数据结构---链表的奇特(下)双向链表的多样魅力
c语言·数据结构·链表
晓13132 小时前
第五章 【若依框架:优化】高级特性与性能优化
java·开发语言·性能优化·若依