C/C++ 语法教程(10)——指针(1)

C/C++ 语法教程(10)——指针(1)

Contents

指针

指针基础

指针这个概念通常指代两个意思:
1. 变量的地址。
2. 存放变量地址的变量 (即指针变量,简称指针)。

此处我们介绍后者。

指针的定义和引用

直接看例子:

int *a;
double *b;
char *c;

也就是 类型 *指针型变量名 的形式。

指针类型存储的是变量的地址,所以它的赋值需要获取变量的地址,也就是要用到前面所说过的 & 一元运算符:

int *a,b;
a=&b;

同理,对于一个地址,我们理应可以对其上的值进行修改,所以可以使用 * 运算符获取对应地址值并修改。

比如:

int *a,b;
a=&b;
*a=233;

这相当于执行了 b=233; 的语句。

注意事项

指针的使用也有一些注意事项。

  1. 虽然赋值时 *a 的写法,使得其整体很像一个变量,但是指针变量指的还是 $a$ 而不是 *a

  2. 赋给指针变量其地址的变量类型应当与指针型变量定义时的类型统一,比如 double *p 应当只能赋值为 double 型的变量地址。

  3. 指针类型本质上还是一个整型用于表示内存单元的地址,所以如果直接对一个指针类型 int *p 赋值 p=123;,其相当于对应到地址为 $123$ 的内存单元。因为这样的赋值对应的内存单元极为不确定,所以绝对不能直接赋值。

  4. 在指针类型没有被赋值为明确的合法内存单元地址前,不应该被引用而去修改对应的值。比如:

    int *p;
    *p=3;
    

    上述代码就是不合法的。

  5. 指针类型同样可以直接赋初值,比如 int *p=&q; 这样。但必须保证 $q$ 在 $p$ 之前被定义了。

  6. 注意 *p++(*p)++ 的区别,后者是让其对应值加一,而前者返回的是 *p,并使指针变量加一,也就是指针后移,也就是指向后一个。

复习回忆

  1. 在最早的时候,我们学习的输入函数 scanf("%d",&n); 这种写法中,就包含了 & 运算符,这也就是前面所说的获取地址的运算符。所以输入函数的本质其实是将输入内容按照某种格式要求读入并写入对应地址的内存单元,这里传进去的变量其实都是指针变量。

  2. 函数调用中我们讲过的地址调用,也就是 int *a 这种调用,其实 a 就是一个 int 型指针变量,这种调用也可以看作是对于指针变量的传值调用。因为数组实际上就是指针,所以 *aa[0] 基本等价。

  3. 函数调用中的引用调用使用 & 这个获取地址运算符使得形参和实参使用同一个地址,可以看出其合理性。

    这里特别指出,& 本身其实就是一个引用运算符,所以不仅可以在引用调用中使用 int &a也可以在正常的定义中使用 int &a=b,这样 $a$,$b$ 两个变量就使用了同一块内存单元,所以也是同步修改的。

    甚至说,函数的返回值也可以是一个引用,比如 int &f(int x,int y),当然这样 freturn 时也得返回一个确定的,且调用函数的位置依然属于其作用域的变量。这样这个函数返回值就相当于一个确定的变量,可以对其进行赋值等修改。比如 f(a,b)+=c; 只要满足上述条件,也是合法的语句。

指向指针的指针

指针变量本身也是一个变量,所以其应该也有一个确定合法的内存单元地址。

所以同样可以有指针变量的指针。

比如看一下下面这段代码:

int a,*b,**c;
b=&a;
c=&b;
**c=4;

实际上,相当于执行了 a=4 的操作。

当然理论上可以更多个 * 代表更多层指针,也就是更多级的间接访问。

这类间接访问的使用和理解主要要抓住每一个指针变量相当于谁的指针变量。

举个例子:

#include <cstdio>
using namespace std;
void f(int **a)
{
    *a+=4;
}
int main()
{
    int a[]={0,2,4,6,8,10,12},*p=a;
    f(&p);
    printf("%d\n",*p);
    return 0;
}

这段代码的输出应该是 8。也就是说 f 函数实际上是对指针变量 $p$ 进行后移操作。

指针数组

定义形式:类型 * 数组名[数组长度说明];

一般来说,指针数组就是将变量类型改为指针类型的数组,与数组应该区别不大。

但是因为数组本身就相当于(是)一个指针,所以也有一些特殊的地方。

比如,可以这样使用:

char *a[]={"C++","Java","PHP","HTML","Python","Sql","Javascript"};

这时 a[] 这个数组中的每一个变量,都相当于一个指向某个(见初始化)常量字符串的指针。

同理应当也可以使用 a[i][j] 来使用这种指针数组。

这个具体在后面讲。

一维数组与指针

根据前面所说,我们早就知道数组本质上就是指针。

对于下面的这种定义方法:

int a[10],*p=a;

我们可以用四种方法给同一个地址赋值:a[i]*(a+i)p[i]*(p+i)

也就是说,你也可以用下列方法输入输出一个数组:

for (int i=0;i<n;i++)
    scanf("%d",a+i);

for (int i=0;i<n;i++)
    printf("%d\n",*(a+i));

同理用 int *p=a; 的方法也可以。

相关几点提醒

  1. int *p=&a[i]; 同样可以使用。这也就是相当于 int *p=a+i;

  2. 指向数组元素的指针的类型也同样需要与数组的类型相同,也就是 int 型指针只能被赋值为 int 型数组元素的地址。

  3. 数组名代表数组的首地址,但这是一个常量指针,也就是不能对这个指针进行修改。比如下面这段代码就是错误的:

    int a[100],b[100]={};
    a=b;
    

二(多)维数组与指针

对于二维数组 int a[10][10],我们类比一维数组可以得到 a=&a[0],a+1=&a[1],a+2=&a[2] 等等,或者说 a[0]=*a,a[1]=*(a+1),a[2]=*(a+2)

其次,对于数组的第二维,我们也可以类比得到 a[i]=&a[i][0],a[i]+1=&a[i][1] 等等,或者说 a[i][0]=*a[i],a[i][1]=*(a[i]+1)

也就是一般来说有 *(a+i)+j=a[i]+j=&a[i][j],a[i][j]=*(a[i]+j)=*(*(a+i)+j)

也就是说 a[i]a 其实都是一个指针变量,但是 a[i] 是指向数组元素 a[i][0] 的指针变量,a 是指向数组行的指针变量。

这里的 a[i] 指针与一维数组的指针实际无异,所以同样理解即可。

a 这个指针指向数组行,又称为行指针

若是想定义一般的指向数组行的指针变量,一般会写为:

类型 (*指针变量名)[数组行元素个数];

int (*p)[10]; 就表示 $p$ 是一个行指针变量,指向每行十个元素的数组的行。

注意:这对小括号绝对不能省去!否则就变成了指针数组。

更高维数组的指针可以类比上述方法。比如三维数组就可以使用:

int (*p)[10][10];

数组作为函数参数

前面讲过了一维数组作为函数参数,这里我们强调一下:形参与实参都可以选择使用数组写法或者指针写法,当然实参用指针表示时需要有确定的内存单元地址。

而且有一点,尽管形参可以使用 int a[] 来表示数组,但实际上,在函数中,程序仍然将 a 当作一个指针变量。

也就是说在函数内部 sizeof(a) 的值依然相当于 sizeof 一个指针类型的值。

我们重点讲一下高维数组作为函数参数。

同样可以使用数组名或者指针类型。

需要注意的是,如果使用数组名,除了第一维以外的之后几维必须给出,比如:

int a[10][10];
int f(int a[][10])
{
    //do something...
}
int main()
{
    //do something...
    f(a);
    //do something...
    return 0;
}

而如果使用指针类型,则可以将 f 换为:

int f(int *a[]);

注意到这里的 *a 不需要再加括号,这是因为,传进去的数组指针已经确定了具体的相隔大小,不需要特意强调,也就是这里的指针数组与二维数组元素的指针是一样的。(当然加括号也是更好的)

二维以上的情况也类似。但具体中括号中是否需要写明大小等等最好自己尝试。

一般推荐使用前一种也就是数组名的形参表示方法。

特别地,还可以这样传递二维及高维数组。

#include <cstdio>
using namespace std;
void add(int *b,int m,int n)
{
    int k=0;
    for (int i=0;i<m;i++)
        for (int j=0;j<n;j++)
            b[i*n+j]=++k;
}
int main()
{
    int a[5][4];
    add((int *)a,5,4);
    for (int i=0;i<5;i++)
    {
        for (int j=0;j<4;j++)
            printf("%5d",a[i][j]);
        puts("");
    }
    return 0;
}

这样的输出也就是:

    1    2    3    4
    5    6    7    8
    9   10   11   12
   13   14   15   16
   17   18   19   20

这段代码的含义是,将本来行指针 $a$ (指针的指针)转换为了普通的指向数组首元素的指针,这样直接对于 a 获取对应的值得到的就是数组元素,而不是指向数组元素的指针了。

总结

本讲主要讲之前涉及的指针重新细讲了一遍,可能内容量也是比较大。

但其实完全没有必要慌张,因为实际应用中,我们往往只需要会其中一种方式即可。

关键是理解指针的含义,也就是存储其他变量地址的变量。

因为目前还没有明显的作用,暂时没有习题。

 

点赞 0

No Comments

Add your comment