C 语言对“指针”的理解


在 C/C++ 里,永远都绕不开一个东西:指针,他和计算机的内存有关。

指针是什么?

在计算机里,有一种东西叫内存,相当于一个非常巨大的仓库,这个仓库存放了非常多的信息、数据。而指针的作用就是让你可以精确定位到这个仓库的某一个货架号,并且顺利获取货物的小东西。

如果要拿一个东西来比喻指针,那么他就是你手上写了某个货架号的便签,你可以通过阅读这张便签,来精准定位到仓库的某一个位置,并取得货物。

初见和使用指针

从 C 语言的概念上来说,指针本质就是一个存储内存地址的类型,和基本数据类型一样,他也是有占用大小的,不过他的大小取决于计算机系统的位数,比如 32 位的系统,那么指针类型就是 32 位(4 字节);64 位的系统就是 64 位(8 字节),可以通过编写一个小程序来确认。

#include <iostream>

int main(){
  printf("%d\n",sizeof(int *));
  // 如果是 32 位系统,那么将会输出 '4';如果是 64 位系统,将会输出 '8'.
  // 位数指的是比特(Bit),而 8 个比特组成 1 个字节(Byte)。
  return 0;
}

下面的程序展示了指针基本的两个运算符:

int val = 100; // 定义一个变量,赋值为 100
int *ptr = &val; // 定义一个 int 指针,将 val 的地址赋值给 ptr
printf("%d",*ptr); // 从 ptr 指向的指针取出数据,并输出
// 输出: 100

 以上程序可知,'&' 运算符用于读取某个变量的地址,也就是 ' 取地址 ';'*' 用于读取某个地址的数据,也就是 ' 取数据 '。

在定义指针变量的时候,需要把 'int *' 看作一个整体,这是写 C/C++ 指针的一种习惯,因为强制转换也是需要写成 '(int *)' 的。

指针本质是地址

上面说过,指针实际上就是一个存储某个地址的整型变量,如果你知道了某个变量的地址,那么你是可以直接把地址赋值到指针变量并输出的。

int val = 100; // 定义一个变量,赋值为 100
printf("%d\n",&val); // 将 val 变量的地址输出到屏幕
// 这里假设输出 32,实际输出是完全不一样的。
int *ptr; // 定义一个 int 指针
scanf("%d",&ptr); // 输入 32 , 也就是上一行输出的 val 变量的地址.
printf("%d\n",*ptr); // 从 ptr 指向的指针取出数据,并输出
// 输出: 100

以上可以看出,在 C 语言中,所谓地址也就是这个仓库某一个货架的 ' 货架编号 ',当中输出的 '32' 就是变量 val 的货架编号,而 ' 100 ' 就是这个货架上的货物。

其他数据类型的指针

毕竟作为一个标准的仓库,肯定是需要能存放不同种类的货物。所以指针也分成不同类型,以确保取出的数据是程序员所想的。

只要指定好这个指针的类型,那么程序就知道从这个地址读取多少个字节,来获取你所需要的数据。

int *p = 0x1000;

*p
↓
| 00 | 00 | 00 | 0F | 12 | 34 | 56 | 78 |
int 占用的字节数为 4,取 4 个字节得到数据
| 00 | 00 | 00 | 0F |

printf("%d",*p);
//输出: 15
//16 进制的 0x0F = 10 进制的 15

反正要使用其他类型指针,只需要把 'int' 换成其他数据类型即可。

short *s; // 短整型指针
long *l; // 长整型指针
char *c; // 字符指针
float *f; // 单精度浮点指针
double *d; // 双精度浮点指针
void *v; //不定(任意)类型指针

typedef struct{
  int Num1;
  int Num2;
  short Short1;
  short Short2;
  char* Name;
} Custom;

Custom *cptr; // 自定义类型指针

也就是说,要声明一个特定数据类型的指针变量,只需要按照这个格式进行编写就可以了:

<数据类型> *<变量名>;

数组即指针

如何理解这个意思?其实很简单:char 数组,也就是常见 C 语言教程书籍、教科书所说的“字符串”。

之所以称之为字符串,是因为在内存中的表现就像是被一条绳子串起来一样:

char p[] = "Hello World!\n";

*p
↓
| H | e | l | l | o | <空格> | W | o | r | l | d | ! | \n | \0 |

如果尝试过将 'p'(不带后面的方括号) 直接输出为整型,那么你将会得到一串数字,其实这个就是 'p' 保存的地址,'p' 是一个 char 指针!

若我们输出这个字符串的第五个字符,一般情况下教科书应该是这样写的:

printf("%c",p[4]);
// 输出 'o'

如果将 'p[4]' 替换为' *(p + (sizeof(char) * 4)) ',你会得到一模一样的输出!这是因为,C 语言里所谓的数组,其真正含义是:一片规律内存里的 '偏移',而不是数组 '索引' 。只不过实现本身和数组几乎没有差别,所以人们更习惯将其成为 '数组' 。

如果以 int 整型作为数组的话,他的内存分布其实是这样的:

int p[] = {
  0x12345678,
  0x11451419,
  0x88488848
};

*p
↓
| 12 | 34 | 56 | 78 | 11 | 45 | 14 | 19 | 88 | 48 | 88 | 48 |

同 char 数组可知,如果我们要拿到这个数组的第二个元素,那么就需要这样做:

p[1] 等价于 *(p + (sizeof(int) * 1));

最终得到的数据:
| 11 | 45 | 14 | 19 | = 0x11451419

指针是把双刃剑

用好了指针,C 语言编程基本上就没什么可以拦住你的了,但是使用指针并不是没有任何代价,使用指针通常意味着危险操作:越界访问。

对于操作系统来说,每时每刻都会运行着上百个程序,如果某天 程序 A 不小心让指针指向了程序 B 的内存,并且覆写了这个值;而又因为这个值是程序 B 的关键信息,结果因为读取到了错误的数据而发生错误,那系统不就混乱了吗?所以操作系统的其中一个职责就是管理内存以及识别程序的内存访问是否合规,也就是这个内存仓库的“总经理”。

当然,操作系统这个经理的管理思路是非常死板的,一旦你尝试访问不属于你负责的货架编号(越界访问),他就会立刻把你停职(强制结束进程),部分经理甚至不会给你原因(一般是导致非法访问的程序段日志)。

char p[] = "ABCD";

*p
↓
| 41 | 42 | 43 | 44 | 00 | ?? | ?? | ?? | ?? | ?? |...(不属于系统划定的内存范围,数据未知)

printf("%d", p[6]);
// 尝试访问不属于自己的内存
// 无法输出:越界访问指针被系统强制结束。

所以说,使用指针的时候千万要小心,即使作为初学者也要留个心眼,一旦程序变得越来越复杂,开发阶段出现的越界访问的情况就会变得越来越常见。就算你认为修复的差不多了,到了可发布的阶段,也可能会出现这种问题,毕竟人并不是机器,做不到十全十美,总会有疏忽的时候。

在实际开发中,涉及直接操作内存,一定要记得:不用的指针变量一定要写 nullptr(也就是 0x00),涉及地址操作的一定要判断是否为 nullptr,循环结构内操作内存需要非常小心,避免非法指针和越界访问的情况出现!

分组于: 编程

发布于: 2024年09月25日 22时25分

编辑于: 2024年09月25日 22时25分

C/C++
指针

用 Cookie 保存: 别名、Email