C 语言是一种强大的编程语言,它提供了指针的概念和相关的语法。指针是一种变量,它存储了内存地址,可以用于直接访问和操作内存中的数据。
- C 指针类型的作用
- 多级指针
1. 指针与内存
指针与 int、float 等类型一样就是普通的数据类型,唯一不同的是像 int、double 这样的类型变量存储的是值,而指针存储的是另外一个变量的地址而已,如下图所示:
上面的定义的变量 value 是普通的 int 类型变量,它所属的内存空间中存储的是普通的数据。而指针变量 p 所属的内存空间中存储的则是另外一个变量的地址。这里有个问题:从内存的角度来看,无论是指针变量还是普通变量存储的都是二进制数据,我们如何区分某个变量存储的是普通的数据还是地址数据呢?这就要从语法上来区分了,如下所示:
int value = 100; int* p = 0x3456;
只要看到变量定义时旁边有个星,那就表示该变量存储的是地址。另外,指针在定义时,还应该知道存储的地址是什么类型变量的地址,比如:int* 表示变量存储的地址 0x3456 所代表的内存空间中存储的是 int 类型数据。所以,我们也可以说 p 变量指向一个 int 类型的变量。
指针也是一个变量,所以指针为了存储地址值也需要占用内存空间,并且也有自己的地址。
如果你理解了这个规律,那么如果指针变量指向的变量仍然是指针,如下图所示:
p2 变量的类型可以表示为 int*,p1 类型是什么呢?如何表示?答案就是二级指针,表示如下:
// * 星号表示 p1 变量存储的是地址,是个指针变量 // int* 表示 p1 指向的变量是 int* 类型 // 合起来写成 int** int**
以此类推,如果再来一个指针变量 p0 指向 p1,则 p0 就是三级指针,写作 int*** p0。
如何获得一个变量的地址呢?对变量使用取地址符号 &, 就可以拿到变量地址,变量地址输出时,使用占位符 %p,下面是示例代码:
#include <stdio.h> int main() { int iv = 100; int* ip = &iv; // 获得变量 iv 的内存地址,赋值到 ip 变量中 int** ipp = &ip; // 获得变量 ip 的内存地址,赋值到 ipp 变量中 printf("iv 变量的地址是: %p %p\n", &iv, ip); printf("ip 变量的地址是: %p %p\n", &ip, ipp); // 其他类型指针 double dv = 3.14; double* dp = &dv; double** dpp = &dp; return 0; }
程序输出结果:
iv 变量的地址是: 0x7ffee79be6f8 0x7ffee79be6f8 ip 变量的地址是: 0x7ffee79be6f0 0x7ffee79be6f0
2. 指针的运算规则
基本类型的变量能够进行一些算术运算,例如:对变量进行加减法计算。指针变量也可以进行一些运算。我们这里主要讲解指针的加减法、解引用两种操作。
- 指针的加减法指的是移动指针在内存中的指向,这个操作很危险,一定要注意;
- 指针的解引用就是拿到指针指向空间的值。
2.1 指针加减法
#include <stdio.h> int main() { // NULL 表示指针指向地址为0的空间 int* ip = NULL; printf("ip 存储的地址: %ld\n", ip); ip += 1; printf("ip 存储的地址: %ld\n", ip); printf("--------------\n"); double* dp = NULL; printf("dp 存储的地址: %ld\n", dp); dp += 1; printf("dp 存储的地址: %ld\n", dp); return 0; }
程序运行结果:
ip 存储的地址: 0 ip 存储的地址: 4 -------------- dp 存储的地址: 0 dp 存储的地址: 8
从输出结果可以看到,ip 和 dp 都是指针变量,但是 + 1 操作之后的值是不同的。这就要提到指针步长的概念。指针+1操作我们可以理解为指针在内存中向前移动了一步,至于这一步是多少个字节,取决于指针指向数据的类型大小,例如:
- ip 指向的数据的类型是 int,而 int 是 4 字节大小,故而 ip 移动一步就会在内存中向前移动 4 个字节;
- dp 指向的数据的类型是 double,而 double 是 8 字节大小,故而 dp 移动一步就会在内存中向前移动 8 个字节。
那如果 ip + 2 会移动多少个字节呢?
2.2 指针解引用
当我们拿到一个指针变量,如果通过该指针变量拿到其指向内存中存储的值呢?这就是解引用操作,如下代码所示:
多字节变量的地址指的是首字节的地址。指针 p 存储的是多字节类型的首字节地址,解引用时会根据类型大小读取相应数量字节的数据。
#include <stdio.h> int main() { int value = 100; int* p = &value; // *p 表示从 p 变量中取出存储的内存地址,并寻址到该空间。此时,我们可以 // 从该空间中取值,如下: printf("p 指向变量的地址: %d\n", *p); // 修改该空间的值,如下: *p = 200; printf("p 指向变量的地址: %d\n", *p); // 通过指针间接修改了 value 变量的值 printf("value 变量值是: %d\n", value); return 0; }
程序运行结果:
p 指向变量的地址: 100 p 指向变量的地址: 200 value 变量值是: 200
2.3 越界内存访问
我们了解了指针的算术运算和解引用操作,那么就要注意一个在 C/C++ 很容易出现的致命错误,叫做越界内存读写。所谓的越界内存读写,是我们通过指针操作没有操作权限的内存。有权限的内存指的是我们自己申请的内存。
#include <stdio.h> int main() { int value = 100; int* p = &value; // p 指针进行移动,此时指向我们没有申请的内存 p += 1; // 如果只是访问越界内存的值,只是数据可能是错误的 printf("越界内存中的值:%d\n", *p); // 注意: 如果尝试去修改越界内存,这是极其危险的操作 // 有些系统会直接终止我们程序的运行 // 有些则看起来允许你进行修改,但是这是未定义的行为 *p = 200; printf("越界内存中的值:%d\n", *p); return 0; }