【C语言·高级】指针

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++ 中,数组名其实是一个指向数组首元素的指针。因此,当你尝试对数组名进行自增或自减时,你实际上是在改变这个指针的指向位置,而不是改变数组本身。这会导致数组头部的位置改变 (这本来就是你的职责啊喂),从而引发一些意外的结果。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值