【原创】浅谈指针(十三)指向数组的指针

前言

这两天又在首页看见指针的文章了,随手再来写一篇。本来想先写static的下集的,后来发现似乎写的有些问题,草稿已经在写了,预计后面不久再发布。
指针其实是C++或是C语言中必不可少的一部分。即使说,很多情况下我们并不会直接使用到指针,但是指针的一些知识同样在其它的地方(哪怕看似和指针无关的地方)奏效。

1.预备知识的复习

1.1.数组

数组在表达式中,其最高一维会被转化为指针。对于一维数组,其呈现的形态就是在表达式中转化为指针。函数的参数也是表达式。

#include<iostream>
using namespace std;
void func(char s[]){
  cout<<sizeof(s)<<endl;
}
int main(){
  char s[]="hello";
  cout<<sizeof(s)<<endl;
  func(s);
}

在64位环境下的输出:

6
8

1.2.VLA

在C99中,对于非static修饰的局部变量,可以在定义中,数组的元素写成变量,这一功能叫做可变长数组(Variable Length Array,简称VLA)。
下面是一段倒序输出数组的代码:

#include<iostream>
using namespace std;

int main(){
    int n;
    cin>>n;
    int s1[n];
    for(int i=0;i<n;i++)cin>>s1[i];
    for(int i=n-1;i>=0;i--)cout<<s1[i]<<" ";
}

其中,"int s1[n]"一句就是运用到了可变长数组。
但在C11中,VLA降为了可选功能,而且VLA只能使用在非static的局部变量,用途相比起来也比较有限。当然,很多情况下还是使用这一功能比较方便。

2.指向数组的指针

指针既然可以指向单个元素,因此也可以指向其他的内容,例如数组。

2.1.指向二维数组

二维数组的类型是int [][],放入表达式中,最高一维会被转化为指针,也就是int (*)[]。我们就可以使用这样类型的指针来指向这个二维数组。

#include<iostream>
using namespace std;

int main(){
    int a[2][3]={1,2,3,4,5,6};
    for(int i=0;i<2;i++){
        for(int j=0;j<3;j++){
            cout<<a[i][j]<<" ";
        }
        cout<<endl;
    }
    
    int (*p)[3]=a;
    for(;p!=&a[2];p++){
        for(int j=0;j<3;j++){
            cout<<(*p)[j]<<" ";
        }
        cout<<endl;
    }
}

在代码的第13行中,声明了int (*p)[3],它表示“指向元素个数为3的数组的指针”。
之前的文章中提到过网页链接,二维数组在内存中是连续排列的:

如果使用普通的int*指针指向这个数组,每次前进的是sizeof(int)个字节:

而使用“指向数组的指针”,每次前进的是这个数组的大小(即int[3]的大小,3*sizeof(int))
如图所示,一次就前进了0x0c。

2.2.注意事项

指向数组的指针,类型表示:int (*a)[2];
而在表达式中,二维数组int a[2][2];的最高一维转化为了指针,因此就变为了“指向数组的指针”。

另外,“指向数组的指针”和“指向数组首个元素的指针”是截然不同的。在表达式中,数组的最高维会被转化为指针,此时的指针,指的是“指向数组初始元素的指针”。

int (*array_p)[3];

可以用来声明一个指向“长度为3的数组”的指针。

在数组前,加上&取地址,返回的就是指向数组的指针:

int array[3];
int (*array_p)[3];

array_p=&array;

原本是int[3]类型,加上一层*,结果就是int (*)[3]
对于scanf在输入字符串的时候,很多人还是这样写的:

scanf("%s",&s);

这样写看似没有问题(实际上,由于后面的s在可变长参数中,没有原型声明,也不会出问题),但是是错误的写法。因为s是数组,因此加上&后变为了“指向数组的指针”,而%s只需要传入一个指向char的指针。
对于指向数组的指针,+1之后,真正加上的是它指向的数组的长度。(由于指针前进1,前进的是它所指向的值的大小)参考下图:

2.3.数组与指针之间的转化

  • 规则:在表达式中,数组的最高一维会被转化为指针。
    • 特例1:对于sizeof(表达式)的形态,这是一个特例。(否则数组的长度是无法输出的)
    • 特例2:在初始化char数组的时候,编译器会自动把它解释为初始化的列表。
char s[]="abc";

本质上是:

char s[]={'a','b','c','\0'};

的简便写法。这种解读只有在初始化列表的时候可行。

  • 规则2:当数组解读为指针时,这个指针不可作为左值。
    • 左值在英语中称为"lvalue",但是l并不代表left,而是locator(表示位置的事物)的意思。本质上,左值就是指可以出现在表达式的左边,确切的说,就是有自己的内存空间,可以被赋值的东西、
    • 例如a=3中,a就是左值。而在a+1=3赋值语句中,由于a+1没有自己的内存空间,无法被赋值,因此它不是左值。

例如:

char s[10];
s="abc";

这段代码是错误的。

2.4.应用

在函数的参数中,二维数组会被解读为指向数组的指针。

int f(int a[10][10]);

与下面等价:

int f(int a[][10]);
int f(int (*a)[10]);

但是注意,不能这么写:

int f(int a[][]);

因为指向数组的指针需要知道它的长度,不然在a++的时候,就不知道前进多少位置了。
当第二维的数字是2的时候,每次的前进是这样的:

而当第二维的数字为5,每次的前进是这样的:

而最高一维可以省略,因为无论最高维是多少,都和前进的字节数无关。
对于数组a[m][n],二维数组的公式是a[i][j]=*(*(a+i)+j)。本质上说,二维数组可以看做“数组的数组”。
其中(a+i)每一次增加的长度也就是sizeof(a[i]),而sizeof(a[i])取决于它指向的内容(a可看做指向数组的指针,它的指向是j相关的一维),因此长度就是isizeof(int)n。
(a+i)+j中,加上的j前进的长度是sizeof(int)。

由此,我们可以看出,二维数组的寻址和j无关。

完。

posted @ 2022-05-10 18:50  计算机知识杂谈  阅读(698)  评论(0编辑  收藏  举报