C语言----指针

基本知识点 :指针的定义、指针运算符和指针运算等基本概念。
重 点 :字符指针、指针数组和多级指针。
难 点:利用指针类型解决复杂的应用问题。

指针的概念

要点归纳

1.指针变量

在计算机中,所有数据都通过变量存放在内存中,每个变量都有存储地址,对于语句int n;定义的变量n,其地址为&n,其中可以存放整数,如 2、5、8等,如图5.1(a)所示。指针就是地址(当一个数据占用多个内存单元时,该地址默认指的是首地址),它也是一种数据,可以定义专门存放变量地址的变量,如图5.1(b)所示,整型变量a、b、c可以将它们的地址存放到变量p中,这个存放变量地址的变量称为指针变量。实际上,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

既然指针变量的值是一个地址,那么这个地址不仅可以是变量的地址,也可以是其他数据的地址。在一个指针变量中存放一个数组或一个函数的首地址有何意义呢?因为数组或函数都是连续存放的。通过访问指针变量取得了数组或函数的首地址,也就找到了该数组或函数。这样一来,凡是出现数组、函数的地方都可以用一个指针变量来表示,只要该指针变量中赋予数组或函数的首地址即可。这样做,将会使程序的概念十分清楚,程序本身也变得精炼、高效。

在C语言中提供两种指针运算符:

*:取指针目标运算符

&:取地址运算符

其中,"*"表示的运算是访问地址的目标。"&"表示的运算是提取一个变量的存储区域的地址。

例如,在定义int x,*px之后,以下赋值语句均成立:

px = &x /* 将x的地址赋给 px */

x = *px /* 将px所指的值赋给x */

px = &(*px) /* 将px所指值的地址赋给 px,等价于 px=px */

x = *(&x) /* 将x的地址的值赋给x,等价于x=x */
提示:

指针变量中存放的是地址值,无论何种数据的指针变量,占用的内存大小是相同的,例如char *p1;double*p2;,其sizeof(p1)和sizeof(p2)均为4(在32位操作系统中),即p1和p2 两个指针变量均占用4个字节。在任何时刻,一个指针变量只能存放一个地址值,即只能指向一个数据。

2. 指针的定义和初始

由于指针是一个变量,所以具有和普通变量一样的属性,在使用之前也需要定义,在指针定义的同时也可以进行初始化。

指针定义的一般形式如下:

数据类型 *指针名

其中,指针名前的 "*" 号仅是一个符号,表示其后的名称是一个指针变量名。这里的 "*" 号并没有访问指针目标的含义。

与普通变量不同,指针定义时指定的数据类型并不是指针变量本身具有的数据类型而是其目标的数据类型。无论目标数据类型如何,所有指针都是具有相同格式的地址量,由于机器硬件不同,地址量的数据长度也不同。

指针初始化的一般形式如下:

数据类型 *指针名 = 初始地址值;

指针初始化的过程是,系统按照给出的数据类型,在一定的存储区域为该指针分配存储空间,同时把初始值置入指针的存储空间内,从而该指针就指向了初始地址值所给定的内存空间。例如:

double x;

double *px = &x;

把变量x的地址作为初值赋给 double 型指针 px,从而px指向了变量x的存储空间。

3. 指针运算

指针运算是以指针变量所持有的地址值为运算量进行的运算,所以指针运算实际上是地址的计算。C语言提供的这种地址计算方法,适合于指针、数组等类型的数据。

  • 指针与整数的加减运算

C语言的地址计算规则规定,一个地址量加上或减去一个整数n,其计算结果仍然是一个地址量,它是以运算数的地址量为基点的前方或后方第n个数据的地址。因此,指针作为地址量加上或减去一个整数n,并不是用它的地址量直接与整数n进行加法或减法运算。其运算结果应该是指针当前指向位置的前方或后方第n个数据的地址。由于指针可以指向不同数据类型,即数据长度不同的数据,所以这种运算的结果取决于指针指向的数据的类型,即目标类型(或基类型)。

对于目标类型为type的指针p,p+n(或p-n)表示的实际位置的地址值是:

p+n(或p-n)*sizeof(type)

  • 指针加1、减1运算

指针加1、减1运算也是地址运算,是指针本身地址值的变化。指针++运算后就指向下个数据的位置,指针--运算后就指向上一个数据的位置。运算后指针地址值的变化量取决于它指向的数据类型。

  • 指针的相减

在C语言中,两个地址量相减,并非它们的两个地址值之间直接做减法运算,两个指针相减的结果值是整数,该值表示这两个指针所指地址之间的数据个数。

  • 指针的关系运算

目标类型相同的两个指针之间的关系运算表示它们指向的地址位置之间的关系。假设数据在内存中的存储顺序是由前向后,那么指向后方的指针大于指向前方的指针。指向不同数据类型的指针之间的关系运算是没有意义的。指针与一般整数常量或变量之间的关系运算也是无意义的。但是指针可以和零(指针零用NULL表示)之间进行等于或不等于的关系运算,即p==NULL或p!=NULL,以判定指针p是否为一空指针。

指针和数组

要点归纳

1. 指针和一维数组

在C语言中,指针与数组之间的关系十分密切,它们都可以处理内存中连续存放的一系列数据。数组与指针在访问内存时采用统一的地址计算方法。在进行数据处理时,指针和数组的表示形式具有相同的意义。

在C语言中规定数组名代表数组的首元素的首地址,也就是说,数组名具有地址的概念。因此,可以将数组名(即在内存中存放该数组的首地址)赋给指针。例如:

int a[10] ,*p;

p = &a[0];

p = a;

两个语句是等价的,其作用都是把a数组的首元素的首地址赋给指针变量p。之后p+i就是数组元素a[i]的地址。访问数组元素的一般形式是:

数组名[下标]

进一步得到访问数据运算的一般形式为:

地址量[整数n]

可以看出,它是一个二目变址运算,要求两个运算量。其中"口"左边的运算量必须是地址量,它可以是地址常量或地址变量。"口"内的运算量必须是整数。该运算表达式的意义是,访问以地址量为起点的第n个数据。例如,表达式a[i]的运算结果是,以地址a为起点的i号元素如果a是某个数组名,则a[i]恰好是该数组的i号元素。由此可知,C语言中数组元素的表示形式实质上是访问数据运算表达式,通过表达式的运算结果达到访问数组元素的目的。

访问数据表达式a[i]的运算过程是,首先计算a+i得到i号元素的地址,然后访问该地址中的数据。其中a+i是按照C语言的地址计算规则进行的。由上述a[i]的运算过程看到,它与表达式*(a+i)的运算完全相同。因此在程序中a[i]和*(a+i)是完全等价的。

在程序中,使用指针处理内存中连续存储的数据时,可以使用以下形式:

*(指针变量+i)

例如,对于*(p+i),根据上述等价原理,它可以写为p[i]的形式。注意不要把它误解为存在一个数组p,并访问p的i号元素。应该把它看做是一个访问数据运算表达式,它是访问以地址p为起点的i号元素。

由于数组名是地址常量,不能对它进行任何运算。而指针可以进行一系列的运算,所以,采用指针对数组元素进行运算更方便灵活。

归纳起来,在定义了int a[10],*p=a;的情况下:

  • p+i或 a+i就是 a[i]的地址。地址值都要进行 a+i*d(d为a中元素对应的数据类型的字长)的运算。
  • *(p+i)或*(a+i)就是 p+i或 a+i所指向的数组元素 a[i]。数组元素中的"[]"是变址运算符,相当于*(+),a[i]相当于*(a+i)。
  • 指向数组元素的指针变量也可带下标,如p[i]与*(p+i)等价。所以,a[i]、*(a+i)、p[i]、 *(p+i)四种表示法全部等价。
  • 注意p与a的差别。p是变量,a是符号常量,不能给a赋值,语句a=p;和a++;都是错误的。

注意:

对于定义的一维数组,如int a[10];,它含有10个整数,数组名a代表的是该数组首元素的首地址,而不是数组a的首地址,&a才是整个数组的首地址,a与&a[0]的含义相同。如"a==&a[0]"返回真,是正确的比较,而"&a==&a[0]"是错误的比较,尽管&a和&a[0]的地址值相同,但两者的含义不同。

例如,有一下程序:

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int a[] = {1, 2, 3}, *p = a;
    int i;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", *(a + i));    //通过*(a+i)操作
    }
    putchar(10);
    for (i = 0; i < 3; i++)
    {
        printf("%d ", a[i]);       //通过a[i]操作
    }
    putchar(10);
    for (i = 0; i < 3; i++)
    {
        printf("%d ", *(p + i));    //通过*(p+i)操作
    }
    putchar(10);
    for (i = 0; i < 3; i++)
    {
        printf("%d ", p[i]);        //通过p[i]操作
    }
    putchar(10);
    return 0;
}

上述程序每次for循环输出的结果都是123,说明*(a+i)、a[i]、*(p+i)和p[i]是等价的。另外,几种指针混合运算方式如下:

  • *p++,由于++和*同优先级,结合方向为自右向左,而这里++为后缀++,因此它等价于*p(返回其值),p++。
  • *++p而这里++为前缀++,它等价于++p,*p(返回其值)。
  • (*p)++,由于括号优先,它等价于*p(返回其值),然后将*p的结果加1。

例如,以下程序的输出结果见其中的注释:

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int a[] = {10, 20, 30}, *p = a;
    printf("%d,", *p);   /*输出:10*/
    printf("%d,", *p++); /*输出:10*/
    printf("%d,", *p);   /*输出:20*/
    p = a;
    printf("%d,", *p);   /*输出:10*/
    printf("%d,", *++p); /*输出:20*/
    printf("%d,", *p);   /*输出:20*/
    p = a;
    printf("%d,", (*p)++); /*输出:10*/
    printf("%d\n", *p);    /*输出:11*/

    return 0;
}

2. 字符指针和字符串

字符指针指的是指向char型数据的指针。显然,字符指针也是一个指针变量。字符指针变量和字符数组有如下区别。

①字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串的首地址),决不是将字符串放在字符指针变量中。

②)赋值方式。对字符数组只能对各个元素赋值,但不能直接给字符数组进行整体赋值(可使用strcpy()函数),例如,一下赋值是错误的:

char str[10];

str = "Hello world";

③ 在定义一个数组时,在编译时就已分配内存单元,有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个地址值,也就是说,该指针变量可以指向一个字符型数据,但如果未对它赋一个地址值,则它并未具体指向哪个字符数据。

C语言编译系统提供了动态分配和释放存储单元的函数。

  • malloc(size):在内存的动态存储区中分配一个长度为 size 的连续空间,此函数的返回值是一个指向分配域起始地址的指针,如果此函数未能成功地执行,则返回值为0。
  • calloc(n,size):在内存的动态存储区中分配n个长度为size 的连续空间,此函数的返回值是一个指向分配域起始地址的指针,如果此函数未能成功地执行,则返回值为 0。
  • free(ptr):释放由 ptr 指向的内存区,ptr 是最近一次调用 calloc或 malloc 函数时返回的值。

上面三个函数中,参数n和size 均为整型,ptr 为字符型指针。

3. 指针和二维数组

以二维数组为例,设二维数组a有3行5列,定义如下:

int a[3][5]={{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};

其中,a是数组名,它的各元素是按行顺序存储的。a数组有3行,将它们看成3个一维数组元素,即a={a[0],a[1],a[2]},每个一维数组元素又含5个元素。这种降维的思路可以扩展到三维或三维以上的数组。

提示:

数组名a代表的是该二维数组首元素的首地址,而不是数组a的首地址,&a才是整个数组的首地址,即a与&a[0]含义相同。如"a==&a[0]"返回真,是正确的比较,而"a==&a[0][0]"是错误的比较,也就是说,a并不是a[0][0]元素地址,同时,"&a==&a[0]"也是错误的比较。尽管a、&a、&a[0]和&a[0][0]的地址值相同,但它们各有自己的含义。归根到底,可将二维数组看成是其元素为一维数组的一维数组,a代表其首一维数组元素的首地址。

如图 5.2所示,由于数组名a是首元素a[0]的首地址,也就是说,a指向元素a[0],所以a+1 和a+2 分别为a[1]和a[2]元素的指针。

a[0]是二维数组a的首元素,它又是由{a[0][0],a[0][1],a[0][2],a[0][3],a[0][4]}元素构成的,a[0]代表该一维数组的首元素的首地址,即a[0]指向首元素a[0][0],因此有"a[0]==&a[0][0]成立。a[0]+1和a[0]+2分别为a[0][1]和a[0][2]元素的指针,依次类推。

数组元素中的"[ ]"是变址运算符,相当于*(+),对于一维数组b,b[i]相当于*(b+j)。对二维数组元素a[i][j],将分数组名a[i]当作b代入*(b+j)得到*(a[i]+j),再将其中的a[i]换成*(a+i)又得到*(*(a+i)+j)。a[i][j]、*(a[i]+j)、*(*(a+i)+j)三者相同,都表示第i行j列元素。根据以上分析,对于图5.2所示的二维数组,可得到表5.1。

4. 数组指针

因为数组名是常量,不能像变量那样操作,为此可以设计指向数组的指针变量,以便于数组的操作。

  • 一位数组指针

一维数组的内存空间是连续的且大小在定义时已指定,所以可以定义一个同类型的指针对其元素进行操作。

例如,以下程序定义一个数组a和指针pa,通过数组名a和指针pa操作输出所有元素:

cs 复制代码
#include <stdio.h>
int main(int argc, char const *argv[])
{
    int i;
    // pa作为一维数组a的指针
    int a[5] = {1, 2, 3, 4, 5}, *pa = a;
    /*①:通过a[i]方式输出所有元素------*/
    for (i = 0; i < 5; i++)
        printf("%d ", a[i]);
    printf("\n"); // 输出结果:1 2 3 4 5
    /*②:通过*(a+i)方式输出所有元素----*/
    for (i = 0; i < 5; i++)
        printf("%d ", *(a + i));
    printf("\n"); // 输出结果:1 2 3 4 5
    /*③:通过*(pa+i)方式输出所有元素---*/
    for (i = 0; i < 5; i++)
        printf("%d ", *(pa + i));
    printf("\n"); // 输出结果:1 2 3 4 5
    /*4:通过*pa方式输出所有元素------*/
    for (i = 0; i < 5; i++)
        printf("%d ", *pa++);
    printf("\n"); // 输出结果:1 2 3 4 5

    return 0;
}

上述程序中的4种输出方式的结果相同,只是第④种方式中,当输出完毕后pa指向a[4]元素的后一个位置。

  • 二维数组指针

二维数组可以看成是一维数组作为元素的一维数组,对每个一维数组元素可以采用前面介绍的一维数组指针的方式进行操作。

例如,以下程序定义了一个二维数组a,a[1]是它的1号元素,也是一个一维数组,通过指针p输出a[1]的所有元素:

cs 复制代码
#include <stdio.h>
int main(int argc, char const *argv[])
{
    int i;
    int a[3][2] = {{0, 1}, {2, 3}, {4, 5}};
    int *p = a[1];
    for (i = 0; i < 2; i++)
        printf("%d ", *p++);
    printf("\n"); // 输出2 3
    return 0;
}

上述程序的思路是对a的每个一维数组元素分别操作,其中p指针是一维数组指针,C语言提供了二维数组指针的概念。二维数组指针变量的一般的定义格式如下:

基类型 (*指针变量) [整型表达式]

其中,"整型表达式"指出二维数组中的列大小,即对应数组定义中的"下标表达式2,例如,有如下定义:

cs 复制代码
int a[2][3], (*pa)[3] = a; 

在(*pa)[3]中,由于存在一对圆括号,所以"*"首先与pa结合,表示pa是一个指针变量,然后再与"[]"结合,表示指针变量pa的基类型是一个包含有3个int元素的数组,也就是说,pa为一个二维数组的指针变量,该数组中每列有3个元素。

一旦定义了二维数组的指针变量,该数组指针变量可以像数组名一样使用,且可以在数组元素中移动。在前面定义二维数组指针变量pa并初始化后,有:

  • pa[i]:引用 a[i]元素。
  • pa++:让pa指向数组a的后一个一维数组元素。
  • pa--:让 pa指向数组a的前一个一维数组元素。
  • pa+1 等价于 a+1。

当pa指向a数组的开头时,可以通过以下形式来引用 a[i][j]:

  • *(pa[i]+j)对应于*(a[i]+j)
  • *(*(pa+i)+j)对应于*(*(a+i)+j)
  • (*(pa+i))[j]对应于(*(a+i))[j]
  • pa[i][j]对应于 a[i][j]
    提示:

数组指针pa与对应的二维数组a的差别是:二维数组a是一个常量,而数组指针pa是一个变量。

例如,以下程序定义一个二维数组a和该数组的指针pa,通过数组名a和指针pa操作输出所有元素:

cs 复制代码
#include <stdio.h>
int main(int argc, char const *argv[])
{
    int i, j;
    // pa作为一维数组a的指针
    int a[2][3] = {{1, 2, 3}, {4, 5, 6}}, (*pa)[3] = a;
    /*①:通过a[i][j]方式输出所有元素------*/
    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            printf("%d ", a[i][j]);
    printf("\n"); // 输出结果:1 2 3 4 5 6
    /*②:通过*(*(a+i)+j)方式输出所有元素----*/
    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            printf("%d ", *(*(a + i) + j));
    printf("\n"); // 输出结果:1 2 3 4 5 6
    /*③:通过*pa[i]+j方式输出所有元素---*/
    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            printf("%d ", *(pa[i] + j));
    printf("\n"); // 输出结果:1 2 3 4 5 6
    /*4:通过*(*(pa+i)+j)方式输出所有元素------*/
    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            printf("%d ", *(*(pa + i) + j));
    printf("\n"); // 输出结果:1 2 3 4 5 6

    return 0;
}
  • 三维数组指针

三维数组指针变量的一般的定义格式如下:

基类型 ((*指针变量)[整型表达式1])[整型表达式2]

其中,"整型表达式1"对应数组定义中的"下标表达式2'"整型表达式2"对应数组定义中的"下标表达式3"。例如,有如下定义:

cs 复制代码
int a[2][3][2]={1,2,3,4,5,6,7,8,9,10,11,12},((*pa)[3])[2]=a;

pa即为三维数组a的指针变量。其定义是分两步考虑的,先定义二维数组a[2][3]的指针变量,为(*pa)[3],再定义三维数组a的指针变量为((*pa)[3])[2]。

三位数组指针的使用与二维数组指针类似。

5. 指针和数组的对比

指针和数组的对比如表 5.2 所示。

指针数组和多级指针

要点归纳

1. 指针数组

当一系列有次序的指针变量集合成数组时,就形成了指针数组。指针数组是指针的集合,它的每个元素都是一个指针变量,并且指向相同的数据类型。指针数组的定义形式如下:

数据类型 *指针数组名[元素个数];

和一般的数组一样,系统在处理指针数组定义时,也为它在一定的内存区域中分配连续的存储空间,这时指针数组名就表示该指针数组的存储首地址。例如:

cs 复制代码
int *p[3];

由于"[]"比"*"优先级高,因此p先与[3]结合,形成p[3]的数组形式,它有3个元素。然后再与p前面的"*"结合,表示是指针类型的数组,该数组的每个元素都是整数的指针,所以每个元素都具有指针的特性。

2. 多级指针

在C语言中,除了允许指针指向普通数据之外,还允许指针指向另外的指针,这种指向指针的指针称为多级指针。其定义形式如下:

cs 复制代码
int *p,**pp,***ppp;

其中,p为一级指针,pp为二级指针,ppp为三级指针。

一般地,p用于指向普通的整数,或整型数组的元素;当指向整型数组的元素时,p++表示指向该数组的下一个元素;

pp用于指向一个指针,p为整数的指针。大多数情况下,pp作为一个指针数组的指针,这时,pp++表示指向该指针数组的下一个元素;

ppp用于指向一个二级指针的指针。大多数情况下,ppp作为一个二维数组指针的指针。

温馨提示:

对于指针有大量练习题,如有需要,请留言......

希望能对您的学习有所帮助!

相关推荐
TeYiToKu15 分钟前
笔记整理—内核!启动!—linux应用编程、网络编程部分(6)随机数与proc文件系统
linux·c语言·arm开发·笔记·嵌入式硬件
一休哥助手20 分钟前
Java/Spring项目中包名以“com”开头的原因分析
java·开发语言·spring
TravisBytes32 分钟前
虚假唤醒(Spurious Wakeup)详解:从概念到实践
开发语言·网络
杭电码农-NEO39 分钟前
【C++拓展(四)】秋招建议与心得
开发语言·c++·求职招聘
让开,我要吃人了1 小时前
HarmonyOS鸿蒙开发实战( Beta5.0)页面加载效果实现详解实践案例
开发语言·前端·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统
生命的演绎1 小时前
Java将驼峰命名转化为下划线命名
java·开发语言
软行1 小时前
LeetCode 每日一题 最佳观光组合
c语言·数据结构·算法·leetcode
郑州吴彦祖7721 小时前
数据在内存中的存储以及练习(一篇带你清晰搞懂)
c语言·数据结构
杰信步迈入C++之路1 小时前
【Java 问题】基础——IO
java·开发语言
Navigator_Z1 小时前
LeetCode //C - 386. Lexicographical Numbers
c语言·算法·leetcode