前言

通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。

一、先看代码

#include <stdio.h>
int main ()
{
    int var_runoob = 10;
    int *p;              // 定义指针变量
    p = &var_runoob;
   printf("var_runoob 变量的地址: %p\n", p);
   return 0;
}

看了这张图,大家对指针应该有了初步的了解。

二、什么是指针

指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var_name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

三、使用指针

先看一个简单的例子

#include <stdio.h>

int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */

   ip = &var;  /* 在指针变量中存储 var 的地址 */

   printf("var 变量的地址: %p\n", &var  );

   /* 在指针变量中存储的地址 */
   printf("ip 变量存储的地址: %p\n", ip );

   /* 使用指针访问值 */
   printf("*ip 变量的值: %d\n", *ip );

   return 0;
}

1.指针的算术运算

C 指针是一个用数值表示的地址。因此,可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:

ptr++

在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。
指针的每一次递增,它其实会指向下一个元素的存储单元。
指针的每一次递减,它都会指向前一个元素的存储单元。
指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。

递增递减

我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;

   /* 指针中的数组地址 */
   ptr = var;
   for ( i = 0; i < MAX; i++)
   {

      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );

      /* 指向下一个位置 */
      ptr++;
   }
   return 0;
}

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;

   /* 指针中最后一个元素的地址 */
   ptr = &var[MAX-1];
   for ( i = MAX; i > 0; i--)
   {

      printf("存储地址:var[%d] = %p\n", i-1, ptr );
      printf("存储值:var[%d] = %d\n", i-1, *ptr );

      /* 指向下一个位置 */
      ptr--;
   }
   return 0;
}

这个操作是要经常用到的,如果你想要数组从0开始递增,可以直接写ptr=var;将var的地址赋给ptr,这样ptr就指向了var数组的第一个元素。接下来,可以使用指针ptr来访问数组var中的元素。例如,可以使用ptr访问第一个元素,(ptr+1)访问第二个元素,依此类推。

指针的比较

指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;

   /* 指针中第一个元素的地址 */
   ptr = var;
   i = 0;
   while ( ptr < &var[MAX - 1] )
   {

      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );

      /* 指向上一个位置 */
      ptr++;
      i++;
   }
   return 0;
}

ptr此时指向的是第一个元素的地址。那这个while只能执行两次对吧。

2.指针数组

C 指针数组是一个数组,其中的每个元素都是指向某种数据类型的指针。

指针数组存储了一组指针,每个指针可以指向不同的数据对象。

指针数组通常用于处理多个数据对象,例如字符串数组或其他复杂数据结构的数组。
让我们来看一个实例,它用到了一个由 3 个整数组成的数组:

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int i;

   for (i = 0; i < MAX; i++)
   {
      printf("Value of var[%d] = %d\n", i, var[i] );
   }
   return 0;
}

可能有一种情况,我们想要让数组存储指向 int 或 char 或其他数据类型的指针。
下面是一个指向整数的指针数组的声明:

int *ptr[MAX];

在这里,把 ptr 声明为一个数组,由 MAX 个整数指针组成。因此,ptr 中的每个元素,都是一个指向 int 值的指针。下面的实例用到了三个整数,它们将存储在一个指针数组中,如下所示:

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int i, *ptr[MAX];

   for ( i = 0; i < MAX; i++)
   {
      ptr[i] = &var[i]; /* 赋值为整数的地址 */
   }
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of var[%d] = %d\n", i, *ptr[i] );
   }
   return 0;
}

结果还是一样的
也可以用一个指向字符的指针数组来存储一个字符串列表

#include <stdio.h>

const int MAX = 4;

int main ()
{
   const char *names[] = {
                   "Zara Ali",
                   "Hina Ali",
                   "Nuha Ali",
                   "Sara Ali",
   };
   int i = 0;

   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i] );
   }
   return 0;
}

这里可能会有同学认为打印的应该是地址,但是事实上不是,先来看结果

这是因为,当我们使用%s格式说明符时,printf函数会打印出字符串的值而不是地址。这是因为在C语言中,使用%s格式说明符告诉printf函数去解引用指针,并打印指针所指向的字符串内容,而不是指针本身的地址。

3.传递指针给函数

C 语言允许传递指针给函数,只需要简单地声明函数参数为指针类型即可。

#include <stdio.h>

/* 函数声明 */
double getAverage(int *arr, int size);

int main ()
{
   /* 带有 5 个元素的整型数组  */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;

   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;  

   /* 输出返回值  */
   printf("Average value is: %f\n", avg );

   return 0;
}

double getAverage(int *arr, int size)
{
  int    i, sum = 0;       
  double avg;          

  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }

  avg = (double)sum / size;

  return avg;
}

在main函数中,首先定义了一个名为balance的整型数组,其中包含5个元素。然后,调用getAverage函数,并将balance数组的首地址和数组大小(5)作为参数传递给该函数。

在getAverage函数中,参数类型被声明为一个指向整型的指针和一个整型变量。函数内部使用循环遍历数组,并将数组元素的值累加到sum变量中。然后,通过将sum转换为double类型并除以数组大小,计算出平均值并将其存储到avg变量中。

最后,在main函数中,通过printf语句打印出平均值。

有些同学可能不理解,这里做个备注
arr[i]表示数组arr的第i个元素的值。在C语言中,数组名本身就是一个地址,指向数组的第一个元素的地址。因此,arr[i]实际上是一个存储在数组内存空间中的值,而不是一个地址。