代码改变世界

C 语言编程 — 高级数据类型 — 指针

2020-04-02 23:45  云物互联  阅读(486)  评论(0编辑  收藏  举报

目录

前文列表

程序编译流程与 GCC 编译器
C 语言编程 — 基本语法
C 语言编程 — 基本数据类型
C 语言编程 — 变量与常量
C 语言编程 — 运算符
C 语言编程 — 逻辑控制语句
C 语言编程 — 函数

指针

C 语言中的指针一直如洪水猛兽般存在。虽然概念上非常简单,但是用起来却变幻多端,神秘莫测,这使得指针看上去比实际要可怕得多。指针类型是基本数据类型的变体,只需基本数据类型的后面添加 * 后缀即可:

  • int i整型变量 i
  • int *p整型指针变量 p
  • int a[n]整型数组变量 a,具有 n 个整型数值元素
  • int *p[n]整型指针数组变量 p,具有 n 个指向整型数值的指针元素
  • int (*p)[n]数组指针,指向整型数组的指针变量 p
  • int func():返回整型数值的函数 func
  • int *func():返回整型的指针函数 func
  • int (*p)()函数指针 func,指向函数的指针
  • int **p指向整型指针的指针变量 p

指令是 C/C++ 编程最大的特色,通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。我们之所以需要指针,主要是由 C 语言中函数的工作方式决定的。C 语言函数的参数全部是通过值传递的。也就是说,传递给函数的实际是实参的拷贝。对于 int、long、char 此类基本数据类型以及用户自定义的结构体数据类型而言是成立的。这种方式适用于绝大多数情况,但也会偶尔出现问题:

  1. 如果我们有一个巨大结构体需要作为参数传递,则每次调用函数,就会对实参进行一次拷贝,这无疑是对性能和内存的浪费。
  2. 结构体的大小终究是有限且固定的,如果我们想向函数传递一组数据,而且数据的大小总是不固定的,例如:数组(包括字符串),结构体就明显的无能为力了。

为了解决这个问题,C 语言的开发者们想出了一个聪明的办法。他们把内存想象成一个巨大的字节(Byte)数组,每个字节都可以拥有一个全局的索引值(数据的首字节的索引作为整个数据的索引)。这有点像门牌号:第一个字节索引为 0,第二个字节索引为 1,等等。

在这种情况下,计算机中的所有数据,包括变量、结构体都有相应的索引值与之对应。所以,除了将数据本身拷贝到函数参数,我们还可以只拷贝数据的索引值。在函数内部则可以根据索引值找到需要的数据本身。我们将这个索引值称为地址,存储地址的变量称为指针。使用指针,函数可以修改指定位置的内存而无需进行拷贝。

因为计算机内存的大小是固定的,表示一个地址所需要的字节数也是固定的。但是地址指向的内存的字节数是可以变化的。这就意味着,我们可以创建一个大小可变的数据结构,并将其指针传入函数,对其进行读取及修改。

所以,所谓的指针也仅仅是一个数字而已。是内存中的一块数据的开始字节的索引值。指针的类型用来提示程序员和编译器指针指向的是一块什么样的数据,占多少个字节等

要清晰区分上述绕口令一般的关系,就要弄清楚指针的本质:

  • 指针:一个变量的地址
  • 指针变量:一个存放其他变量地址的变量

引入了指针之后,C/C++ 中就有了两种访问变量数据值的方式:

  1. 通过变量名来直接访问
  2. 通过内存地址块的指针来间接访问

指针运算相关的运算符有以下两种:

  • 取地址运算符 &:获取变量所占用的存储空间的地址,为单目运算符(只有一个操作数)。
  • 取值运算符 *:也称解引用,获取指针变量所指向的存储空间内的数据值。取值运算符的操作数只能是一个指针变量。

注意:要获取结构体指针的某个字段的值,需要使用 -> 操作符。

NOTE:取值运算和取地址运算互为逆运算。

int a = 3int b;
int * p = NULL;

p = &a;
b = *p;

前门的文章中提到过,变量 = 变量名 + 变量值,而且 C 语言是值语义的,有别于 Python 的引用语义。所以变量名就是变量在内存中的入口地址,变量值就是变量在内存空间中实际的数值。在程序中可以使用取地址运算符 & 来获取变量的入口地址。如下:

#include <stdio.h>

int main(){
    int var1;
    char var2[10] = {10, 9, 8, 7};

    printf("var1: %p\n", &var1);
    printf("var2-0: %p\n", &var2[0]);
    printf("var2-1: %p\n", &var2[1]);
    printf("var2-2: %p\n", &var2[2]);

    return 0;
}

运行:

$ ./main
var1: 0x7ffc857d59bc
var2-0: 0x7ffc857d59b0
var2-1: 0x7ffc857d59b1
var2-2: 0x7ffc857d59b2

可见,不同变量之间的内存空间很可能不是连续的,但同一数值内的顺序元素的空间是连续的。

指针的本质也是一个变量,其变量值是另一个变量的入口地址,即一个变量存储了另一个变量的内存地址,是为指针。数组名本质上也是一个指针,并且是常量指针,记录了数组的入口地址,且不能够被修改。

声明指针

type *var-name;
  • type 是指针的基类型,是一个有效的 C 数据类型
  • var-name 是指针变量的名称
  • * 用来声明指针类型变量
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;     /* 一个字符型的指针 */

需要注意的是,不管指针的基类型是什么,指针变量的数值的类型都是一个代表内存地址的十六进制数。指针的基类表示了指针所指向的变量或常量的数据类型。

使用指针

使用指针时会频繁进行以下几个操作:

  1. 定义一个指针变量
  2. 把变量的内存地址赋值给指针
  3. 访问指针变量存储的数值(内存地址)
#include <stdio.h>
 
int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */
 
   ip = &var;  /* 在指针变量中存储 var 的地址 */
 
   printf("Address of var variable: %p\n", &var  );
 
   /* 在指针变量中存储的地址 */
   printf("Address stored in ip variable: %p\n", ip );
 
   /* 使用指针访问值 */
   printf("Value of *ip variable: %d\n", *ip );
 
   return 0;
}

运行:

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

NULL 指针

在声明指令变量的时候,如果没有确切的内存地址可以赋值,那么为指针变量赋一个 NULL 值是一个良好的编程习惯,称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
   printf("ptr 的地址是 %p\n", ptr);
   return 0;
}

运行

ptr 的地址是 0x0

在大多数的操作系统上,不允许程序访问地址为 0x0 的内存,因为该内存是操作系统保留的。但按照惯例,如果指针变量的数值为 NULL 时,则假定它不指向任何东西。

判断一个空指针的方式:

if(ptr)     /* 如果 p 非空,则完成 */
if(!ptr)    /* 如果 p 为空,则完成 */

指针的算术运算

C 指针的本质是一个十六进制数值,所以可以对指针执行算术运算,可以对指针进行四种算术运算:++--+-

  • 指针的每一次递增,它会指向下一个元素的存储单元。
  • 指针的每一次递减,它会指向前一个元素的存储单元。
  • 指针在递增和递减时的步进(跳跃的字节数)取决于指针所指向的变量的数据类型,比如 int 就是 4 个字节。

我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

#include <stdio.h>


const int MAX = 3;


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

    /* 数组名就是一个指针,直接复制给指针类型变量 */
    ptr = var;
    for(i = 0; i < MAX; i++){
        printf("Address: var[%d] = %p\n", i, ptr);
        printf("Value: var[%d] = %d\n", i, *ptr);
        /* 移动到下一个位置 */
        ptr++;
    }
    return 0;
}

运行:

./main
Address: var[0] = 0x7ffe48f272d0
Value: var[0] = 10
Address: var[1] = 0x7ffe48f272d4
Value: var[1] = 100
Address: var[2] = 0x7ffe48f272d8
Value: var[2] = 200

可见,每递增一次,移动了 4 Byte。

同样地,对指针进行递减运算,即把值减去其数据类型的字节数,如下所示:

#include <stdio.h>

const int MAX = 3;

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

    /* 获得数组最后一个元素的指针,再复制给指令类型变量 */
    ptr = &var[MAX - 1];
    for(i = MAX; i > 0; i--){
        printf("Address: var[%d] = %p\n", i - 1, ptr);
        printf("Value: var[%d] = %d\n", i - 1, *ptr);

        /* 移动到下一个位置 */
        ptr--;
    }
    return 0;
}

运行:

./main
Address: var[2] = 0x7ffdbab78f88
Value: var[2] = 200
Address: var[1] = 0x7ffdbab78f84
Value: var[1] = 100
Address: var[0] = 0x7ffdbab78f80
Value: var[0] = 10

指针可以时要关系运算符进行比较,如 ==<>。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增:

#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("Address of var[%d] = %x\n", i, ptr );
      printf("Value of var[%d] = %d\n", i, *ptr );
 
      /* 指向上一个位置 */
      ptr++;
      i++;
   }
   return 0;
}

指向指针的指针

指向指针的指针是一种多级间接寻址的实现,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际数值的内存位置。

在这里插入图片描述

一个指向指针的指针变量必须如下声明,在变量名前放置两个 * 号。例如,下面声明了一个指向 int 类型指针的指针:

int **var;

当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:

#include <stdio.h>
 
int main ()
{
   int  var;
   int  *ptr;
   int  **pptr;

   var = 3000;

   /* 获取整型变量 var 的地址 */
   ptr = &var;

   /* 获取指向整型变量的指针变量 ptr 的地址 */
   pptr = &ptr;

   printf("Value of var = %d\n", var );
   printf("Value available at *ptr = %d\n", *ptr );
   printf("Value available at **pptr = %d\n", **pptr);

   return 0;
}

将指针作为实际参数传入函数

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

#include <stdio.h>
#include <time.h>
 
void getSeconds(unsigned long *par);

int main ()
{
   unsigned long sec;

   getSeconds(&sec);
   
   /* 输出实际值 */
   printf("Number of seconds: %ld\n", sec);
   return 0;
}

void getSeconds(unsigned long *par)
{
   /* 获取当前的秒数 */
   *par = time(NULL);
   return;
}

从函数返回指针

类似地,C 语言允许从函数返回指针类型。只需要一个简单的函数声明:

int * myFunction(){}

需要注意的是,C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们:

#include <stdio.h>
#include <time.h>
#include <stdlib.h> 
 
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
   static int r[10];
   int i;
 
   /* 设置种子 */
   srand((unsigned)time(NULL));
   for ( i = 0; i < 10; ++i)
   {
      r[i] = rand();
      printf("%d\n", r[i] );
   }
 
   return r;
}


int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf("*(p + [%d]) : %d\n", i, *(p + i) );
   }
   return 0;
}

一个古老的笑话

这里有个古老的笑话,说是可以根据 C 程序员的程序中指针后面的星星数 * 作为其水平的评分。😛
初级水平的人写的程序可能只会用到像 char* 或是奇怪的 int* 等一级指针,所以他们被称为一星程序员。而大多数中级的程序员则会用到诸如 lval** 这类的二级指针,所以他们被称为二星程序员。但据说能用三级指针的就真的很少见了,你可能会在一些伟大的作品中见到,这些代码的妙处凡夫俗子自然也是体会不到的。果真如此,三星程序员这个称号真是极大的赞誉了。
但据我所知,还没有人用到过四级指针。