C 语言中的指针

指针可以说是 C 语言中比较重要的内容了,跟很多方面都存在联系。这里对 C 语言中的指针部分做个小总结。

内存和指针的关系

  • 内存是线性的
  • 内存是以字节为单位进行编址的,内存中的每个字节都对应一个地址
  • 程序的执行过程中,数据都要保存到内存当中
  • 数据是有类型的,计算机会根据数据类型分配连续的一段内存作为数据的存储空间
  • 指针存储的是指向数据的地址

变量地址

  • & 运算符后加变量名就变成了变量名的地址,& 可以称为取地址运算符
  • 变量地址前加 * 运算符就能够访问到变量,* 可以成为取内容运算符
  • 地址一般是用存储空间的第一个字节的地址表示变量地址,即低字节地址
  • 地址也是有大小的
#include <stdio.h>

int main()
{
    char a = 97;
    int b = 97;
    float c = 97.0;
    double d = 97.0;

    printf("&a = %p,sizeof(&a) = %d,a = %c\n",&a,sizeof(&a),*(&a));
    printf("&b = %p,sizeof(&b) = %d,b = %d\n",&b,sizeof(&b),*(&b));
    printf("&c = %p,sizeof(&c) = %d,c = %f\n",&c,sizeof(&c),*(&c));
    printf("&d = %p,sizeof(&d) = %d,d = %f\n",&d,sizeof(&d),*(&d));

    return 0;
}

运行上述的代码,结果为:

&a = 0060FEAF,sizeof(&a) = 4,a = a
&b = 0060FEA8,sizeof(&b) = 4,b = 97
&c = 0060FEA4,sizeof(&c) = 4,c = 97.000000
&d = 0060FE98,sizeof(&d) = 4,d = 97.000000

从上边的结果可以看出,计算机对每个变量都赋予了一个地址,每个地址的大小是固定的,可以通过 & 获得变量的地址,* 获得变量地址对应的内容。

指针与地址

每个变量地址对应的字节大小是相同的,这是由计算机和开发工具决定的。但是变量地址却是有类型的常量。我们由上边的代码已经知道 a,b,c,d 所对应的地址,试看下边的代码:

#include <stdio.h>

int main()
{
    int a = 0x12345678;

    printf("&a = %p,sizeof(&a) = %d\n",&a,sizeof(&a));
    printf("char a = %x\n",*((char *)&a));
    printf("short a = %x\n",*((short *)&a));
    printf("int a = %x\n",*((int *)&a));

    return 0;
}

运行上边的代码,可以得到下面的结果:

&a = 0060FEAC,sizeof(&a) = 4
char a = 78
short a = 5678
int a = 12345678

从上边的运行结果来看,&a 对应的就是 0060FEAC 这一个地址。根据这个结果再看下面的程序:

#include <stdio.h>

int main()
{
    int a = 0x12345678;

    printf("&a = %p,sizeof(&a) = %d,a = %x\n",&a,sizeof(&a),*(0x0060FEAC));

    return 0;
}

这个程序是不能运行的,因为没有指定最后一项输出的类型。从这里来看,我们可以知道:

  • &a 取出来的地址是有类型的
  • *(0xXXXXXXXX) 这样的形式是不对的,至少应该写成 *((datatype *)0xXXXXXXXX) 的形式,但不推荐这样用
  • 指针的类型决定的是指针的寻址能力,即将该起始位置后连续的几个字节当作一个数据

指针变量

定义

上边提到用 &a 的形式可以取到变量 a 的地址,但是这个地址是不可更改的。有时我们希望定义一个变量,用来存放指针的量,该变量叫做指针变量,定义为:

type * variable;

上面指针变量定义中的 * 就表示后方的变量为指针变量,type 则代表的是该指针变量的寻址能力。

初始化与赋值

根据之前提到的,地址是赋值给指针变量,又不推荐直接给指针变量赋值地址的形式,因此一般情况下都是把一个已经声明的变量地址赋值给指针变量,如:

#include <stdio.h>

int main()
{
    int a;
    int *pa = &a;
    printf("&a = %p,pa = %p\n",&a,pa);
    printf("a = %d,*pa = %d\n",a,*pa);

    return 0;
}

与数值变量类似,指针变量也可以重新赋值,从而可以指向新的地址。

NULL

指针变量如果指向无效空间,该指针变量就变成了 invalid。通常有两种情况会出现 invalid:

  • 指针未进行初始化
  • 该指针变量指向的空间已经被释放

此时,通常用 NULL 给未进行初始化的指针向量初始化,或者对已经释放的内存空间进行指针赋值。

NULL 是一个宏,定义为:

define NULL ((void *) 0)

NULL 是一个很特别的指针,但读不出东西,也不能写入东西,所以当指针变量被赋值为 NULL 后进行读写操作,是不会有内存数据损坏的。

指针运算

指针的运算主要是改变指针指向的运算。

算术运算

算术运算主要是加减运算,对于指针来说并不是简单的加减数值的运算。之前已经提到过,指针是有类型的,所以说在指针的运算中也是包含类型的,因此指针的算术运算可以说是一种数值和类型的复合运算。可以看下边的程序:

#include <stdio.h>

int main()
{
    short *a;
    int *b;
    float *c;
    double *d;

    printf("a = %p\tb = %p\tc = %p\td = %p\n",a,b,c,d);
    printf("a+1 = %p\tb+1 = %p\tc+1 = %p\td+1 = %p\n",a+1,b+1,c+1,d+1);

    return 0;
}

结果为:

a = 00000059	b = 00000059	c = 00000000	d = 00401790
a+1 = 0000005B	b+1 = 0000005D	c+1 = 00000004	d+1 = 00401798

由于没有定义指针的指向,所以忽略该段程序的实用性。只有当指针变量能够指向一串连续的存储单元时,指针变量的算术运算才有意义,因为这时指针变量才能够访问到有意义的数据。

但是从最后的结果可以看出,由于指针变量定义的类型不同,对指针变量进行算术运算之后的结果也是不同的,这就是之所以说指针的算术运算是数值和类型的复合运算的原因。

比较运算

除了可以对指针变量进行简单的算术运算之外,还能够对指针变量进行比较:

#include <stdio.h>

int main()
{
    int *p1, *p2;
    int value1 = 1;
    int value2 = 2;

    p1 = &value1;
    p2 = &value2;

    printf("p1 = %p\tp2 = %p\n",p1,p2);

    if (p1 > p2)
        printf("The pointers are same\n");
    else
        printf("The pointers are different\n");

    return 0;
}

结果为:

p1 = 0060FEA4   p2 = 0060FEA0
The pointers are same

可以看出指针变量是能够比较大小的,但是这基本上没有意义,因为无法确定计算机分配内存的位置。但是可以利用 == 判断两个指针指向的是否是同一块内存。

指针和一维数组

之前已经提到过数组名其实就是指针,可以对该指针进行算术运算进而访问数组中的其它数据。

下标访问

下标访问即是通过 [] 运算符或者数组名加数值的形式实现的操作,可以看下边的程序:

#include <stdio.h>

int main()
{

    int array[10] = {1,2,3,4,5,6,7,8,9,10};

    for(int i=0; i<10; i++)
        printf("array[%d] = %d\n",i,array[i]);

    printf("\\***************************\\\n");

    for(int i=0; i<10; i++)
        printf("array[%d] = %d\n",i,*(array+i));

    return 0;
}

结果为:

array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10
\***************************\
array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10

需要注意的是 array[i] = *(array+i),这就是 [] 运算符的含义了。

指针访问

指针访问是通过指针变量进行访问,可以看下边的程序:

#include <stdio.h>

int main()
{

    int array[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = array;

    for(int i=0; i<10; i++)
        printf("array[%d] = %d\n",i,*p++);

    return 0;
}

结果为:

array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10

可以看出两者的执行结果是一样的。但是也有一些区别:

  • 数组名是一个常量,不允许重新赋值
  • 指针变量是一个变量,可以重新赋值
  • p + i  和 array + i 所表示的含义相同
  • *(p + i ) 和 *(array + i) 所表示的含义相同
  • p[i] 和 array[i] 所表示的含义相同
  • 由于运算符的优先级和结合性,要明白 *p++ == *(p++)

指针和二维数组

二维数组虽然也是由一维数组扩展过来的,但是两者也是有些许区别的。

下标访问

二维数组可以通过 array[i][j] 来访问数组 array 第 i 行第 j 列的对应元素。

指针偏移访问

一维数组可以通过 *(array + i) 来访问数组 array 的第 i 个元素,但是二维数组却有些不同:

#include <stdio.h>

int main()
{
    int array1[10] = {1,2,3,4,5,6,7,8,9,10};
    int array2[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

    for (int i = 0; i < 10; i++)
    {
        printf("array1 + %d = %p\tarray1[%d] = %d\n",i,array1 + i,i,array1[i]);
    }

    printf("\\*******************************************************************\\\n");

    for (int i = 0; i < 3; i++)
    {
        printf("array2 + %d = %p\t*(array2 + %d) = %08X\t**(array2 + %d) = %d\n",i,array2 + i,i,*(array2 + i),i,**(array2 + i));
    }

    printf("\\*******************************************************************\\\n");

    for (int i = 0; i < 3; i++)
    {
        printf("&array2[%d] = %p\tarray2[%d] = %08X\t*array2[%d] = %d\n",i,&array2[i],i,array2[i],i,**(array2 + i));
    }

    printf("\\*******************************************************************\\\n");

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("*(array2 + %d) + %d = %p\t*(*(array2 + %d) + %d) = %d\n",i,j,*(array2 + i) + j,i,j,*(*(array2 + i) + j));;
        }
    }

    printf("\\*******************************************************************\\\n");

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("&array2[%d][%d] = %p\tarray2[%d][%d] = %d\n",i,j,&array2[i][j],i,j,array2[i][j]);
        }
    }

    return 0;
}

结果为:

array1 + 0 = 0060FE6C   array1[0] = 1
array1 + 1 = 0060FE70   array1[1] = 2
array1 + 2 = 0060FE74   array1[2] = 3
array1 + 3 = 0060FE78   array1[3] = 4
array1 + 4 = 0060FE7C   array1[4] = 5
array1 + 5 = 0060FE80   array1[5] = 6
array1 + 6 = 0060FE84   array1[6] = 7
array1 + 7 = 0060FE88   array1[7] = 8
array1 + 8 = 0060FE8C   array1[8] = 9
array1 + 9 = 0060FE90   array1[9] = 10
\*******************************************************************\
array2 + 0 = 0060FE3C   *(array2 + 0) = 0060FE3C        **(array2 + 0) = 1
array2 + 1 = 0060FE4C   *(array2 + 1) = 0060FE4C        **(array2 + 1) = 5
array2 + 2 = 0060FE5C   *(array2 + 2) = 0060FE5C        **(array2 + 2) = 9
\*******************************************************************\
&array2[0] = 0060FE3C   array2[0] = 0060FE3C    *array2[0] = 1
&array2[1] = 0060FE4C   array2[1] = 0060FE4C    *array2[1] = 5
&array2[2] = 0060FE5C   array2[2] = 0060FE5C    *array2[2] = 9
\*******************************************************************\
*(array2 + 0) + 0 = 0060FE3C    *(*(array2 + 0) + 0) = 1
*(array2 + 0) + 1 = 0060FE40    *(*(array2 + 0) + 1) = 2
*(array2 + 0) + 2 = 0060FE44    *(*(array2 + 0) + 2) = 3
*(array2 + 0) + 3 = 0060FE48    *(*(array2 + 0) + 3) = 4
*(array2 + 1) + 0 = 0060FE4C    *(*(array2 + 1) + 0) = 5
*(array2 + 1) + 1 = 0060FE50    *(*(array2 + 1) + 1) = 6
*(array2 + 1) + 2 = 0060FE54    *(*(array2 + 1) + 2) = 7
*(array2 + 1) + 3 = 0060FE58    *(*(array2 + 1) + 3) = 8
*(array2 + 2) + 0 = 0060FE5C    *(*(array2 + 2) + 0) = 9
*(array2 + 2) + 1 = 0060FE60    *(*(array2 + 2) + 1) = 10
*(array2 + 2) + 2 = 0060FE64    *(*(array2 + 2) + 2) = 11
*(array2 + 2) + 3 = 0060FE68    *(*(array2 + 2) + 3) = 12
\*******************************************************************\
&array2[0][0] = 0060FE3C        array2[0][0] = 1
&array2[0][1] = 0060FE40        array2[0][1] = 2
&array2[0][2] = 0060FE44        array2[0][2] = 3
&array2[0][3] = 0060FE48        array2[0][3] = 4
&array2[1][0] = 0060FE4C        array2[1][0] = 5
&array2[1][1] = 0060FE50        array2[1][1] = 6
&array2[1][2] = 0060FE54        array2[1][2] = 7
&array2[1][3] = 0060FE58        array2[1][3] = 8
&array2[2][0] = 0060FE5C        array2[2][0] = 9
&array2[2][1] = 0060FE60        array2[2][1] = 10
&array2[2][2] = 0060FE64        array2[2][2] = 11
&array2[2][3] = 0060FE68        array2[2][3] = 12

一维数组我们已经很熟悉了,就不再赘述了。对于二维数组,从上边的结果可以看出:

  • array2 是数组首元素的地址,所以 array2,*(array2),&array2[0],array2[0],*(array2 + 0) + 0,&array2[0][0] 所对应的地址都是相同的
  • array2[0] 本身就是一个一维数组,所以 array2 + i,&array2[i] 中间的地址间隔为 16
  • 可以将 array2 看作是一个二重指针
  • array2[0] 对应的是一个一维数组的起始地址,即一个整数大小对象的地址
  • 而 array2 对应的是一个二维数组的起始地址,即一个一维数组大小对象的地址

对上述的结果还可以总结为:

  • 二维数组名解引用,降维为一维数组名
  • 一维数组名,对其引用,升级为二维数组名
  • & 和 * 可以认为是互为相反的关系

指针访问

与之前提到过的类似,对比推理即可。

发布了77 篇原创文章 · 获赞 5 · 访问量 4887

猜你喜欢

转载自blog.csdn.net/SAKURASANN/article/details/104480801