C 语言是一种强大的编程语言,它提供了指针的概念和相关的语法。指针是一种变量,它存储了内存地址,可以用于直接访问和操作内存中的数据。
-
C 指针类型的作用
-
多级指针
-
指针与内存 {#title-0} ===================
指针与 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
- 指针的运算规则 {#title-1} =====================
基本类型的变量能够进行一些算术运算,例如:对变量进行加减法计算。指针变量也可以进行一些运算。我们这里主要讲解指针的加减法、解引用两种操作。
- 指针的加减法指的是移动指针在内存中的指向,这个操作很危险,一定要注意;
- 指针的解引用就是拿到指针指向空间的值。
2.1 指针加减法 {#title-2}
#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 指针解引用 {#title-3}
当我们拿到一个指针变量,如果通过该指针变量拿到其指向内存中存储的值呢?这就是解引用操作,如下代码所示: 多字节变量的地址指的是首字节的地址。指针 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 越界内存访问 {#title-4}
我们了解了指针的算术运算和解引用操作,那么就要注意一个在 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;
}