title: 【C语言·高级】 指针
date: 2025-03-24 22:17:43
tags: [C,学习]
categories: [学习]
excerpt: “指针也就是内存地址,指针变量是用来存放内存地址的变量。”
目录
指针
什么是指针?我们以下面的代码为例:
int a = 10;
int *p1 = &a;
在这里,&a 是 a 的地址,p1 就是存储这个地址的变量。指针本身存储的是地址,而不是数据本身。因此,p1 的值就是一个地址,它指向一个 int 类型的变量。
如果你是问指针 p1 本身的地址,可以通过 &p1
来获取。
也就是说p1是一个专门用来存储int类型的地址变量,我们管它叫指针变量
指针变量
p1 是一个指针变量,它专门用来存储指向 int 类型数据的地址。我们通常叫它指针,它存储的是一个地址,而这个地址指向的内存空间保存的是 int 类型的数据。
简而言之,指针变量就是一个存放地址的变量,指向某种特定类型的数据。在你这个例子中,int *p1 表示 p1 是一个存储 int 类型数据地址的指针变量。
&求址运算符 &[简单变量]
* 指向运算符 *[指针变量]
指针变量声明的时候要在前面加上*
,此时这个*
不是运算符,只是告诉编译器后面的变量为指针变量,例如:
-
int *p1;
指的是声明一个叫做p1的指针变量,*是用来表示p1为指针,并不是指向运算符
-
p1 = &a;
此时运用求址运算符把a的地址赋值给一个指针变量
-
printf("%d\n", *p1);
此时使用指向运算符接着一个指针变量,这代表着实际上*p1表示的是p1地址对应的值(也就是输出a)
假设我们有一串代码,p开头的是指针变量,a,b为正常变量
p1 = &a;
p2 = &b;
此时
*p1 = *p2;
这个代码等价于 a = b;
p1指向的地址是a变量的地址,p2同理
指针赋值指针
int a = 10,*p,*q;
p = &a; // p 指向 a 的地址
q = p; // q 现在也指向 a 的地址
这行代码意味着 p 和 q 都指向同一个变量a
q = p; 这行代码的作用是让 q 指向 p 指向的地址。也就是说,q 现在指向和 p 一样的内存地址,也就是 a 的地址。
从这时起,p 和 q 都指向同一个内存位置,也就是 a。
scanf与指针
int a, *pa = &a;
如果定义了以上变量,想要scanf赋值给a应该怎么写?
{% folding black::答案:点击展开 %}
scanf("%d", pa);
scanf("%d", &a);
{% note tip %}
在 scanf
中,你需要传入变量的地址,来让 scanf
能够在该地址上写入输入的值。所以,scanf
的作用是通过地址来修改变量的值
{% endnote %}
{% endfolding %}
a和*p 与 &a和p
我们了解了指针的基本原理,那么是否有一个猜想:
*p 和 a
与&a 和 p
都各自相同?答案:正确,但要注意区分是值还是地址:
假设你有以下代码,并且a是变量p是指针变量:
int a = 10;
int *p = &a; // p 是指向 a 的指针
*p 和 a
*p 是指针 p 解引用的结果,即通过 p 获取它所指向的值(也就是 a 的值)。因此,*p 和 a 是一样的,它们都代表 a 的值。
printf("%d\n", *p); // 输出 10,因为 *p 解引用了 p,等价于 a
&a 和 p
- &a 是 a 的地址,也就是 p 存储的内容。所以,&a 和 p 是一样的,它们都代表了 a 的地址。
printf("%p\n", (void*)&a); // 输出 a 的地址
printf("%p\n", (void*)p); // 输出 p 的值,实际上是 a 的地址
结论
因此,我们得出的结论是:
*p 和 a 是相同的,它们代表 a 的值。
&a 和 p 是相同的,它们代表 a 的地址。
空指针 (void指针)
空指针(Null Pointer)是指一个没有指向任何有效的内存地址的指针变量。它通常用来表示指针目前不指向任何有效的对象或数据。
在 C 语言中,空指针通常由宏 NULL 表示,或者在某些情况下,你也可以用 0 来表示空指针。空指针的主要目的是在指针没有实际地址可用时,避免它指向一个随机或无效的内存位置,减少访问无效内存的风险。
空指针不指向任何有效的内存地址,可以通过空指针检查代码中是否存在指针没有正确初始化的情况。
空指针可以用于初始化指针,确保指针不会意外地指向一个随机的内存地址。
空指针不可解引用:空指针不能解引用(不能使用 * 访问它指向的值),否则会导致程序崩溃或未定义行为。
例子:
#include <stdio.h>
int main() {
int *p = NULL; // p 是一个空指针
if (p == NULL) {
printf("p 是空指针\n");
}
// 下面这行代码是错误的,解引用空指针会导致崩溃
// printf("%d\n", *p); // 会崩溃,因为 p 没有指向任何有效的内存
return 0;
}
为什么要使用空指针?
初始化指针:使用空指针初始化指针可以确保指针不随机指向某个内存位置,防止野指针(指向不确定地址的指针)带来的潜在问题。
指示无效值:空指针可以作为一种标志,表示某个操作或函数返回失败,或者指针还没有分配有效内存。
总结
空指针就是指向 NULL 或 0 的指针,它没有指向任何有效的内存地址,常用于指示指针的无效状态或作为初始化指针的一种方式。
空指针的数据类型转换
你可能有疑问:
int *p是定义一个整型指针,void *p是定义 一个空指针,int指针可以被整形变量取地址,那空指针能吗?
int *p 和 void *p 确实是定义了不同类型的指针变量。让我们逐一解释:
int *p
int *p 是定义了一个指向 int 类型的指针,意味着 p 可以存储 int 类型变量的地址。
例如:
int a = 10;
int *p = &a; // p 存储 a 的地址,p 是 int 类型指针
这里,p 指向 a,它是一个 int * 类型的指针,可以通过 *p 访问 a 的值。
void *p:
void *p 是定义了一个指向未知类型的指针,也称为 空指针。它不指向任何特定类型的变量,所以你不能直接通过 *p 解引用它。
void *p 允许你存储任意类型的指针地址,但你必须在使用前将其转换成具体的类型。例如:
int a = 10;
void *p = &a; // p 存储 a 的地址,但 p 是 void 指针
如果你想使用 void *p 来访问 a 的值,你需要先将 void * 类型转换为 int *,然后再解引用:
printf("%d\n", *(int *)p); // 需要转换 void * 为 int * 然后解引用
void *p 能否取整型变量的地址?
是的,void *p 可以存储 int 类型变量的地址。空指针 void * 可以指向任何类型的变量地址,但是由于 void * 没有明确的类型信息,它本身不能直接解引用。你需要将它转换成具体类型的指针(如 int *)后才能访问值。
例子:
int a = 10;
void *p = &a; // p 存储 a 的地址(p 是 void 指针)
printf("%d\n", *(int *)p); // 将 void * 转换为 int *,然后解引用
总结
int *p 是一个指向 int 类型的指针,存储 int 类型变量的地址,且可以直接解引用。
void *p 是一个通用指针,可以存储任意类型的地址,包括 int 类型的地址,但在解引用前必须先将其转换为特定类型的指针。
访问数组的方式
int a[5]={1,2,3,4,5},*p;
//下面这两段代码等价
p = &a[0];
P = a;
当打印这两个变量时,会发现a[i]和p[i]输出是一样的
由此得知,数组是通过地址访问的,a[i]代表着a(首地址)第i个地址指向的值
与p[i]一样
指针变量的整数量增减
加减一个整数,例如p++,p+=1,可以让p[i]变成p[i+1]
通过修改指针变量的自增自减,也可以改变p指向的地址
例如:
int a[5]={1,2,3,4,5},*p;
p = a;
p++;
此时p指向的是a数组第2个int的内存地址,也就是说指向的是 a[1]
Tips : p[i] 与 *(p+i)是等价的
两个指针变量的差值计算
在 C 语言中,两个指针变量的减法是指针之间的差值计算。这种操作的结果不是直接的数值差异,而是两个指针之间相差的元素数量,而非字节数。这是因为指针的加减是基于它们指向的元素类型的大小来进行的。
例子
假设你有两个指向同一类型数组的指针 p 和 q,那么 p - q 会返回它们之间相差的元素个数,而不是字节数。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = &arr[1]; // p 指向 arr[1]
int *q = &arr[4]; // q 指向 arr[4]
int diff = q - p; // 计算指针之间的差值
printf("The difference between p and q is: %d\n", diff); // 输出指针间的元素个数
return 0;
}
运行:
The difference between p and q is: 3
解释
p 指向 arr[1],而 q 指向 arr[4]。
q - p 计算的是 q 和 p 之间的元素差异,在这个例子中,是 4 - 1 = 3,即 p 和 q 相差 3 个 int 元素。
注意:这是基于指针所指向的元素类型的大小。例如,如果指针是 int * 类型,那么它们之间的差值表示的是 int 类型元素的数量,而不是字节数。
数学原理
指针相减的结果是两个指针相差的元素个数。例如,p 和 q 是指向同一类型数组的指针,那么 p - q 计算的是它们之间相差的“元素数”,而不是字节数。
- p - q 返回的是 p 和 q 之间的元素数量。
- 计算时,指针会根据其指向的类型的大小(例如,int 类型为 4 字节)自动进行相应的计算,而不需要你手动去计算字节差。
总结:
- 指针减法返回的是两个指针之间相差的元素个数,而不是字节数。
- 计算结果是基于指针所指向的元素类型的大小的,指针相减会返回“元素数”而非字节差。
二维数组的赋值
int a[3][3],*p,i;
p = a; for (i = 0; i < 9; i++)
p[i] = i;
提问:这块的p[i] = i;
赋值的时候不会数组越界吗?
{% folding black::答案:点击展开 %}
在这段代码中,p[i] = i; 并不会导致数组越界,原因在于指针 p 的行为。
解释:
a 是一个二维数组:
a[3][3] 表示一个 3x3 的二维数组。二维数组的内存布局是连续的,也就是说,a[0][0], a[0][1], …, a[2][2] 都是存储在一块连续的内存区域中。
指针 p 指向一维数组:
p = a; 使得 p 指向二维数组 a 的首元素 a[0]。在内存中,二维数组 a 可以视为一个一维数组,包含了 9 个元素。也就是说,p 实际上是指向一个连续内存区域,存储了二维数组 a 的所有元素。指针 p 的类型是 int*,它可以访问这块连续的内存区域中的每个元素。
p[i] = i; 的行为:
p[i] 表示指针 p 向后移动 i 个元素的位置,依次访问内存中连续的元素。所以,p[i] 并不是访问二维数组 a[i/3][i%3],而是直接访问一维数组中的第 i 个元素。当 i 从 0 到 8 时,p[i] 会依次访问二维数组 a 的每个元素,并赋值为 i。
也就是说,p[i] = i; 并不会导致数组越界,因为 p 访问的是一个连续内存区域,而 a 是一个 3x3 的二维数组,实际上它在内存中是连续的 9 个 int 元素。
{% endfolding %}
数组不能自增自减
数组不能自增或者自减,尽管它有着和指针变量相似的"指向地址"的作用
在 C 或 C++ 中,数组名其实是一个指向数组首元素的指针。因此,当你尝试对数组名进行自增或自减时,你实际上是在改变这个指针的指向位置,而不是改变数组本身。这会导致数组头部的位置改变 (这本来就是你的职责啊喂),从而引发一些意外的结果。