3、指针基础回顾:指针声明、取地址、解引用、指针运算
说实话,指针这东西,我见过太多人栽跟头了。我自己刚入行那会儿,也因为它吃过不少苦头。但话说回来,指针就是C/C++的灵魂。你搞不懂它,调试越界bug就像在黑暗中摸索。今天咱们就把指针的底裤扒干净,看看它到底是个什么玩意儿。
3.1 指针声明:你声明了个啥?
指针声明,说白了就是告诉编译器:嘿,我要一个变量,这个变量存的是某个内存地址。它的类型决定了它指向的数据是什么。
int *p; // p是一个指向int的指针
char *cp; // cp是一个指向char的指针
void *vp; // vp是一个无类型指针,可以指向任何类型
我个人习惯把星号紧贴着变量名写,比如 int *p。这样读起来更自然——「p是一个指针,指向int」。当然也有人喜欢写 int* p,这都没问题,关键是团队统一。
int *p1, *p2;,而不是 int* p1, p2;——后者只有p1是指针,p2就是个普通int。这个坑我见过不下十次。
3.2 取地址 &:找到变量的家门
每个变量都住在内存里,有自己的门牌号。取地址运算符 & 就是帮你找到这个门牌号。
int a = 42;
int *p = &a; // p现在存的是a的地址
嗯,这里要注意:取地址操作只能用于左值(可以放在赋值号左边的玩意儿)。你不能对 &(a+1) 或者 &42 取地址,因为它们是临时值,没有固定的家。
printf("%p", p) 来打印指针的值,看看它到底指向了哪里。这在GDB里也通用,用 p p 命令就行。
3.3 解引用 *:顺着地址找东西
解引用就是拿着地址去找那个地址里存的值。星号 * 在这里是解引用运算符,跟声明时的星号是两码事。
int a = 42;
int *p = &a;
printf("%d\n", *p); // 输出42
*p = 100; // 等价于 a = 100
我曾经在项目中遇到过一个问题:一个同事解引用了一个未初始化的指针,程序直接崩溃。你想想看,一个指针没初始化,它里面存的是个随机地址,你拿着这个地址去访问内存,不崩才怪。
3.4 指针运算:地址也能做算术
指针运算不是普通的算术,它是以「类型大小」为步长的。比如 int *p,p+1 实际上跳过了4个字节(假设int占4字节)。
int arr[] = {10, 20, 30, 40};
int *p = arr; // p指向arr[0]
printf("%d\n", *p); // 10
printf("%d\n", *(p+1)); // 20
printf("%d\n", *(p+2)); // 30
为什么会这样?因为指针的类型告诉编译器:每次移动一个单位,就跳过 sizeof(类型) 个字节。说白了,p+1 在内存里实际移动了 sizeof(int) 个字节。
指针运算还有几个常用操作:
- 指针相减:两个同类型指针相减,得到的是它们之间相差的元素个数。比如
&arr[3] - &arr[0]结果是3。 - 指针比较:可以用
<、>、==等比较两个指针的地址大小。 - 指针自增/自减:
p++让指针指向下一个元素,p--指向上一个。
3.5 知识体系一览
下面这张图把指针的核心概念串起来了,你可以对照着看:
3.6 避坑指南
指针这东西,用好了是利器,用不好就是自残。我总结几个常见的坑:
- 未初始化指针:声明了指针不赋值,直接解引用。这是新手最容易犯的错。记住:指针声明后,要么让它指向有效内存,要么置为NULL。
- 野指针:指针指向的内存已经被释放了,但指针还在用。比如free之后没有置NULL,后面又去解引用。
- 越界访问:指针运算跑出了数组或分配内存的范围。这是咱们这门课的核心话题,后面会重点讲。
- 类型不匹配:比如把
int*赋值给char*,然后解引用。虽然能编译通过,但读出来的数据完全不是你想的那样。
好了,指针的基础就聊到这儿。这些东西看着简单,但真正用起来,尤其是在复杂的数据结构里,很容易就绕晕了。下一节咱们会把这些知识用到实际调试中,看看GDB里怎么玩转指针。