11. 指针

一、地址与指针与变量

  内存区的每一个字节都有一个编号,这就是 “地址”。如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)。通过访问这个地址,就可以找到所需要的变量,这个变量的地址称为该变量的 指针指针的实质就是内存 “地址”。指针就是地址,地址就是指针。

  变量的地址是变量和指针两者之间连接的纽带,如果一个变量包含了另一个变量的地址,则可以理解为第一个变量指向第二个变量。所谓的指向就是通过地址实现的。因为指针变量是指向一个变量的地址,所以将一个变量的地址赋给这个指针变量后,这个指针变量就指向了该变量。

  在程序代码中是通过变量名对内存空间单元进行存取操作的,但是代码经过编译后已经将变量名转换为该变量在内存中的存放地址,对变量值的存储都是通过地址进行的。

  我们可以使用 & 运算符来 获取变量的地址,使用 * 运算符来 获取指针类型所指向的值

二、指针变量

2.1、什么是指针变量

  由于通过地址能访问指定的内存存储单元,可以说地址 “指向” 该内存单元。地址可以形象地称为指针,意思是通过指针能找到内存单元。一个变量的地址称为该变量的指针。如果有一个专门用来存放另一个变量的地址,它就是 指针变量。在 C 语言中,有专门存放内存单元地址的变量类型,即 指针类型

2.2、指针变量的定义

  指针也是一种数据类型,指针变量也是一种变量。所有的指针类型的变量存储的都是内存地址。指针变量指向谁,就把谁的地址赋值给指针变量。“*” 操作符操作的是指针变量指向的内存空间。

数据类型 * 指针变量名;

  其中,“*” 表示该变量是一个指针变量,变量名即为定义的指针变量名,数据类型表示本指针变量所指向的变量的数据类型;

int * p;

指针类型决定了指针在被解引用的时候访问几个字节;

2.3、指针变量的赋值

  给指针变量所赋的值只能是变量的地址值。在 C 语言中,提供了地址运算符 “&” 来表示变量的地址。

指针变量名 = &变量名;

  我们可以在定义指针变量的同时进行赋值:

int a = 10;
int* p = &a;

  同样,我们也可以先定义指针变量,再进行赋值:

int a = 10;
int *p;
p = &a;

2.4、指针变量的引用

  引用指针变量是对变量进行间接访问的一种形式,对指针变量的引用形式如下:

*指针变量名;

  其含义是引用指针变量所指向的值;

#include <stdio.h>

int main(void)
{
  
    int a = 10;
    // int *代表是一种数据类型,int*指针类型,p才是变量名
    // 定义了一个指针类型的变量,可以指向一个int类型变量的地址
    int * p;
    // 将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
    p = &a;

    printf("a的值为:%d\n",a);
    printf("a的地址为:%p\n",&a);

    printf("p的地址为:%p\n",p);
    // p指向了a的地址,*p就是a的值
    printf("p指向的值为:%d\n",*p);

    return 0;
}

& 可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在 CPU 里面,所以是没有地址的。

  我们可以通过指针间接修改变量的值。

#include <stdio.h>

int main(void)
{
  
    int a = 10;
    int * p = &a;

    printf("修改前a的值为:%d\n",a);
    *p = 100;
    printf("修改后a的值为:%d",a);

    return 0;
}

三、指针的大小

  使用 sizeof() 测量指针的大小,得到的总是:4 或 8。sizeof() 测的是指针变量指向存储地址的大小。在 32 位平台,所有的指针(地址)都是 32 位(4 字节);在 64 位平台,所有的指针(地址)都是 64 位(8 字节);

#include <stdio.h>

int main(void)
{
    printf("sizeof(short *): %zd\n",sizeof(short *));
    printf("sizeof(int *): %zd\n",sizeof(int *));
    printf("sizeof(long *): %zd\n",sizeof(long *));
    printf("sizeof(long long*): %zd\n",sizeof(long long *));
    printf("sizeof(float *): %zd\n",sizeof(float *));
    printf("sizeof(double *): %zd\n",sizeof(double *));
    printf("sizeof(long double *): %zd\n",sizeof(long double *));

    return 0;
}

在 32 位的机器上,地址是 32 个 0 或 1 组成的二进制序列,那地址就得用 4 个字节的空间来存储,所以一个指针变量的大小就应该是 4 个字节;

那如果在 64 位机器上,如果由 64 个地址线,那一个指针变量的大小应该是 8 个字节,才能存放一个地址;

四、指针的运算

4.1、指针加减整数

  指针式一个用数值表示的地址,可以对指针进行算数运算:+、-。指针的算数运算不同于普通变量的算数运算,指针会按照它所指向的数据类型字节长度进行增或减。

  我们可以使用 + 运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向的类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效。

#include <stdio.h>

int main(void)
{
    int array[10] = {1,2,3,4,5,6,7,8,9};
    int * ptr = array;
    int i = 0;

    for(i = 0; i < sizeof(array)/sizeof(array[0]); i++)
    {
        printf("%-3d",*(ptr+i));
    }
    printf("\n");

    return 0;
}

  我们还可以使用 - 运算符从一个指针减去一个整数。指针必须是第 1 个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效。

#include <stdio.h>

int main(void)
{
    int array[10] = {1,2,3,4,5,6,7,8,9};
    int * ptr = &array[9];
    int i = 0;

    for(i = 0; i < sizeof(array)/sizeof(array[0]); i++)
    {
        printf("%-3d",*(ptr-i));
    }
    printf("\n");

    return 0;
}

在 C 语言中,指针加(减) 1 指的是增加(减少)一个存储单元;

4.2、指针自增自减

  递增指向数组元素的指针可以让该指针移动至数组的下一个元素。

#include <stdio.h>

int main(void)
{
    int array[10] = {1,2,3,4,5,6,7,8,9};
    int * ptr = array;
    int i = 0;

    for(i = 0; i < sizeof(array)/sizeof(array[0]); i++)
    {
        printf("%-3d",*ptr++);
    }
    printf("\n");

    return 0;
}

  递减指向数组元素的指针可以让该指针移动至数组的前一个元素。

#include <stdio.h>

int main(void)
{
    int array[10] = {1,2,3,4,5,6,7,8,9};
    int * ptr = &array[9];
    int i = 0;

    for(i = 0; i < sizeof(array)/sizeof(array[0]); i++)
    {
        printf("%-3d",*ptr--);
    }
    printf("\n");

    return 0;
}

前缀或后缀的自增和递减运算符都可以使用;

4.3、指针的减法

  指针的减法就是内存地址的偏移量,即 内存地址的差值/sizeof(数据类型),得到的是指针和指针之间的元素的个数。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求得两元素之间的距离。差值的单位与数组类型的单位相同。

  只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错位。

#include <stdio.h>

int main(void)
{
    int array[] = {1,2,3,4,5,6,7,8,9};
    int * p = &array[3];
    int step = p-array;

    printf("%d\n",step);
    printf("%p\n",p);
    printf("%p\n",p-2);
    printf("%p\n",&array[1]);
    printf("%d\n",*(p-2));
  
    return 0;
}

如果两个指针变量指向的是同一个连续空间的不同存储单元,则这两个指针变量才可以相减,表示两个变量相隔的元素个数。

4.4、指针的比较

  指针可以用关系运算符进行比较,前提是两个指针都指向相同类型的对象。如果 p1 和 p2 指向两个变量,比如指向同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

#include <stdio.h>

int main(void)
{
    int array[] = {1,2,3,4,5,6,7,8,9};
    int * p = array;
  
    if(p == &array[0])
    {
        printf("%p\n",p);
    }
  
    return 0;
}

C 语言允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较;

五、野指针和空指针

  指针变量也是变量,是变量就可以任意赋值,不要越界即可(32 位为 4 字节,64 位为 8 字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。

#include <stdio.h>

int main(void)
{
    // 野指针 --> 指针变量指向一个未知的空间
    // 程序中允许出现野指针
    int * p;
    *p = 100;
    // 操作野指针对应的内存空间可能报错
    // 操作系统将地址编号0-255作为系统占用,不允许访问操作
    printf("%d\n",*p);

    return 0;
}

  但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C 语言中,可以把 NULL 赋值给此指针,这样就标志此指针为空指针,没有任何指针。

int * p = NULL;

  NULL 是一个值为 0 的宏常量:

#define NULL    ((void *)0)

操作空指针对应的内存空间一定报错;

不建议直接将一个变量的值赋值给指针变量,因为这有可能会导致野指针的出现。那我们如何规避野指针:

  1. 指针初始化;
  2. 小心指针越界;
  3. 指针指向空间释放及时置空;
  4. 避免返回局部变量的地址;
  5. 指针使用之前检测有效性;

六、万能指针

  void * 指针可以指向任意变量的内存空间,它不指向任何的数据类型,在通过万用指针修改变量的值时,需要找到变量对应的指针的类型。这是因为 void * 是无具体类型的指针,它不能进行解引用操作,也不能加减一个整数。

#include <stdio.h>

int main(void)
{
    int a = 10;
    char ch = 'a';
    // 万能指针可以接收任意类型变量的内存地址
    void * p;
  
    printf("万能指针在内存占的字节大小:%d\n",sizeof(void *));

    // 在通过万用指针修改变量的值时,需要找到变量对应的指针的类型
    p = &a;
    *(int *)p = 100;
    printf("%d\n",a);
    printf("%d\n",*(int *)p);
    printf("%p\n",p);

    p = &ch;
    *(char *)p = 'c';
    printf("%c\n",ch);
    printf("%c\n",*(char *)p);
    printf("%p\n",p);
  
    return 0;
}

七、const修饰的指针变量

7.1、指向常量的指针

  指针指向的是一个常量,所以只能访问数据,不能通过指针对数据进行修改。不过指针本身是变量,可以指向另外的数据类型。这是 const 应该加在类型前面。

const 数据类型 变量名 = 值;
const 数据类型 * 指针变量名 = &变量名;
#include <stdio.h>

int main(void)
{
    const int a = 100, b = 200;
    int c = 300;

    // 指向常量的指针
    const int * pc = &a;
    printf("*pc = %d\n", *pc);

    // 可以修改指针变量存储的地址值
    pc = &b;
    printf("*pc = %d\n", *pc);

    // 也可以指向变量
    pc = &c;
    printf("*pc = %d\n", *pc);

    // 但是不能修改指针指向内存空间的值
    // *pc = 400;

    return 0;
}

7.2、指针常量

  指针本身是一个数据类型,所以也可以区分变量和常量。如果指针本身是一个常量,就意味着它保存的地址不能更改,也就是它永远指向同一个对象。而数据对象的内容是可以通过指针改变的。这种指针一般叫作 “指针常量”。指针常量在定义的售后,需要在星号 * 后,标识符前加上 const。

数据类型 变量名 = 值;
数据类型 * const 指针变量名 = &变量名;
#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;

    // const修饰指针变量
    int * const cp = &a;

    // 可以修改指针指向内存空间的值
    *cp = a;
    // 但是不能修改指针变量存储的地址值
    //cp = &b;
  
    return 0;
}

7.3、指向常量的指针常量

  const 同时修饰指针类型和指针变量,表示我们既不可以修改指针指向内存空间的值,也可以修改指针变量存储的地址值;

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;

    // const 修饰指针类型,修饰指针变量
    const int * const ccp = &a;

    // 不可以修改指针指向内存空间的值
    //*ccp = a;
    // 不可以修改指针变量存储的地址值
    //ccp = &b;
  
    return 0;
}

八、多级指针

  一个指针变量既可以 整型变量、浮点型变量 和 字符类型变量,当然也可以指向指针类型变量。当指针变量用于指向指针类型变量时,则称之为 指向指针的指针变量,也就是 多级指针。指向指针的指针变量定义如下:

数据类型 **指针变量名;

  其含义为定义一个指针变量,它指向另一个指针变量,该变量又指向一个基本数据类型。

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;
    int * p = &a;
    // pp指二级指针变量的值
    // *pp指一级指针的值
    // **pp指变量的值
    int **pp = &p;

    **pp = 100;
    printf("%d\n",*p);
    printf("%d\n",a);

    *pp = &b;          // 相当于p = &b;
    printf("%d\n",*p);
  
    return 0;
}

  整型变量 a 的地址是 &a,将其值传递给指针变量 p,则 p 指向 a;同时,将 p 的地址 &p 传递给 pp,则 pp 指向 p。这里的 pp 就是指向指针变量的指针,即指针的指针。int * 是说明 pp 指向的对象是 int * 类型;

多级指针

二级指针是用来存放一级指针变量的地址;

posted @ 2023-03-05 13:46  星光樱梦  阅读(53)  评论(0编辑  收藏  举报