指针和动态内存管理是C语言学习中的重点和难点,本篇主要总结自己对指针的理解,深入浅出,言简意赅。

指针的本质

用最简洁的话来描述指针的本质就是:一个存储了内存地址的变量。所以说指针变量和普通变量并没有本质的区别,只不过指针中存储的是一个内存地址,而普通变量存储的是具体的数值(其实内存地址也是个数值)。当然,如果仅仅是存值类型的差异,并不能给指针带来如此大的威力。指针厉害的地方在于,基于存储地址这一特性:指针可以访问存储在这个地址中的内容并修改;另外,可以通过给指针赋不同的地址来改变指针指向的变量。指针也支持四则、比较等运算,这更丰富了指针的用途。

这听起来没什么了不起,毕竟知道了地址,支持查看和修改这个地址中的值,再理所当然了。没错,这就是计算机底层CPU和内存模型的工作方式,而C语言是最接近底层硬件的高级语言。所以说,C语言的精髓是指针,指针是C语言的一切。

指针的表示

定义一个指针变量就是在变量名前面加上一个*,然后将一个变量的地址赋给指针变量。下面有一个详细的例子,相信看完对指针的理解会有一个质的提升。

看例子前,让我们先了解一些符号的含义。*除了可以用来定义一个指针,单独用在指针变量名前面表示“解引”一个指针。“指针解引"的含义就是获取指针所指向的变量的值。&是取址符号,表示获取该符号后面的变量的地址。

#include<stdio.h>
#include<stdlib.h>

int main () {
  int a = 2;
  int *ptr = &a;
  
  printf("a的地址是:%p\n", &a);
  printf("a的内容是:%d\n", a);
  printf("ptr指针的地址是:%p\n", &ptr); // 指针变量自己所在的地址
  printf("ptr指针的内容是:%p\n", ptr); // a的地址
  printf("ptr指针所指向的内容是:%d\n", *ptr); // 指针指向的a的值
  printf("----------\n");
  
  *ptr = 5; // 修改指针指向的变量a的值
  printf("修改值后,a的地址是:%p\n", &a);
  printf("修改值后,a的内容是:%d\n", a);
  printf("修改值后,ptr指针的地址是:%p\n", &ptr); // 指针变量自己所在的地址
  printf("修改值后,ptr指针的内容是:%p\n", ptr); // 指针的指向未变:还是a的地址
  printf("修改值后,ptr指针所指向的内容是:%d\n", *ptr); // 指针指向的a的值,此时被修改了
  printf("----------\n");
  
  int b = 3;
  ptr = &b; // 修改指针的指向为变量b
  printf("修改指向后,a的地址是:%p\n", &a); // 指针已经不指向a了
  printf("修改指向后,a的内容是:%d\n", a); // 指针已经不指向a了
  printf("修改指向后,b的地址是:%p\n", &b);
  printf("修改指向后,b的内容是:%d\n", b);
  printf("修改指向后,ptr指针的地址是:%p\n", &ptr); // 指针变量自己所在的地址,未修改
  printf("修改指向后,ptr指针的内容是:%p\n", ptr); // b的地址
  printf("修改指向后,ptr指针所指向的内容是:%d\n", *ptr); // 指针指向的b的值
  printf("----------\n");
}

打印结果:

a的地址是:0x30981757c
a的内容是:2
ptr指针的地址是:0x309817570
ptr指针的内容是:0x30981757c
ptr指针所指向的内容是:2
----------
修改值后,a的地址是:0x30981757c
修改值后,a的内容是:5
修改值后,ptr指针的地址是:0x309817570
修改值后,ptr指针的内容是:0x30981757c
修改值后,ptr指针所指向的内容是:5
----------
修改指向后,a的地址是:0x30981757c
修改指向后,a的内容是:5
修改指向后,b的地址是:0x30981756c
修改指向后,b的内容是:3
修改指向后,ptr指针的地址是:0x309817570
修改指向后,ptr指针的内容是:0x30981756c
修改指向后,ptr指针所指向的内容是:3
----------

值得注意的是,例子中有一个“ptr指针的地址”,注意这是指针变量所在的地址,不要和指针所存储的地址混淆了。前面提到指针的本质也是一个变量,因此当然也可以把这个指针变量的地址赋值给另一个指针,得到的这个指针就叫“指针的指针”,再赋值一次,就是“指针的指针的指针”… 依次类推,万变不离其宗。

指针的用法

通常我们用*定义的指针叫做:指向非常量的指针。就像我们前面使用的那样,“指向非常量的指针”可以修改指针的指向(地址),也可以通过解引来修改指针指向的数据。但在一些特殊情况下,我们可能需要控制指针这两种操作的权限。这就需要引入"常量"的概念。C语言中,用const关键字定义一个常量。

下面介绍三种和常量相关的指针:

  • 指向常量的指针: 可以修改指针的指向,但是不能修改指向的数据。
  • 指向非常量的常量指针:不可以修改指针的指向,但可以修改指向的数据。
  • 指向常量的常量指针:即不可以修改指针的指向,也不可以修改指向的数据。

要想理解上面的几种指针,我们要先理解“常量”这个概念:在计算机世界中,“常量”即意味着不可修改。因此,我们只需要关注常量这个词是用来 修饰指针,还是修饰指针所指向的那个对象就可以很好的区分。

常量指针指针本身是不可修改的,即指针的值(所指向的地址)不可修改。因此,常量指针表示不可以修改指针的指向,即不能给指针赋值一个新的地址。

指向常量指向非常量说的是指针所指向的那个对象是否可修改。如果指针指向的那个对象本身是不可以修改的,自然也就不可以通过指针解引的方式修改它的数据。

总结一下:

指针类型 指针(的指向)是否可修改 指针指向的数据是否可修改
指向非常量的指针(普通指针)
指向常量的指针
指向非常量的常量指针
指向常量的常量指针

几种指针的定义:

  • 指向常量的指针:

      const int a = 2;
      const int *ptr = &a; // 等价:int const *ptr = &a;
    
  • 指向非常量的常量指针:

      int a = 2;
      int *const ptr = &a;
    
  • 指向常量的常量指针:

      const int a = 2;
      const int *const ptr = &a;
    

总结

掌握本质后,指针也并不难理解。下一篇会介绍一些指针在C语言中的具体应用,比如:数组,字符串、结构体和函数等。