C语言中数组与指针区别精解

来源:互联网 发布:华语乐坛唱功最好知乎 编辑:程序博客网 时间:2024/06/09 07:58

1 概述

数组与指针是C语言中的难点, 也是精华的部分,没有掌握C语言的数组与指针谈不上熟悉C语言。大多数开发者,易把数组与指针混淆。本文力争用浅显的语言来描述数组与指针的异同之处,啃下这块硬骨头。
本文以32位操作系统进行说明。

2 从简单的说起(指针与一维数组)

2.1 声明

指针:
int* p; char* p;…
指针的声明不多说,很简单。

数组:
数组的声明分两种情况,
1、非char型
int a[10]; //声明时带上下标,指定长度为10个元素
int a[]={1,2,3}; //声明时不下标,但必须以{}初始化,根据初始化元素个数,确定数组大小
2、char型
首先可以用非char型的两种方式,除此之外,还可用字符串直接来初始化:
char a[] = “abc”; //长度为4,字符串末尾有’\0’
char a[] = {“abc”};
char a[10] = “abc”;
char a[10] = {‘a’,’b’,’c’,’\0’};

tips:
char a[] = “abc”; //编译正常,声明是一个数组,长度为4
char *p = “abc”; //编译警告或错误,这里”abc”成了字符串常量,
const char *p =”abc”; //编译正常,表明p指向的是const char,将“abc”字符串常量首地址赋给p

2.2 数组名与指针

先声明一个数组如下:
char a[10];

理解几个值:a、*a、&a、*&a、&a[0]、a+0、*(a+0)、a[0]

a[0], 等价于 *(a+0), 表示首元素,这个是我们常用的
a+0, 等价于&a[0],表示首元素地址,是个指针;
&a,表示数组a的地址,是个指针,指向整个数组
*&a, 表示数组a
a+i: 表示从数组首地址开始,偏移i个元素类型长度的位置(char的长度是1,也就是偏移i个字节),是一个指针,i在这里也可取负数,只是这样会访问到数组外,出现越界。

难点在于:数组名a
数组名a有两层意思:
1、a不是指针,看作是数组整体,&a才是指针,是一个指向整个数组的指针;
这层意思,易于理解如下语句值:
sizeof(a)    等于10, a是一个整体,代表的是整个数组,数组的长度sizeof(char)*10,即等于10
sizeof(&a)   等于4, &a是个指针,指针长度即为4
sizeof(a+0)   等于4, a+0是个指针,长度即为4,指向的是数组中下标为0的元素,等价于&a[0]
sizeof(*(a+0))  等于1,a+0是指向下标0元素的指针,取它的值,等价于a[0],char型,长度为1
&a+i   表示从数组首地址开始,偏移i个a数组长度(本例中长度是10)的位置,是一个指针

按如上分析,其实这层把a看作是个整体的意思,使用起来理解就容易多,但如果仅限于这层意思,那么:
如果定义一个指针,想指向数组中首元素,那么必须写成:char *p = a+0; 或p=&a[0];
如果定义一个指针想要指向整个数组, 必须写成: char (*p)[10] = &a;
上述的写法完全没有问题的,可能大家几乎没见过或没使用过,那是因为常见的还用到数组名的另一层意思。其实如果数组名仅限于这第一层,想必就不会弄混淆指针与数组名的含义。

2、a是指针,看作是一个指向数组首元素地址的指针
这一层意思,是大多数书籍描述数组名的含义,即数组名指向数组的首地址,也可以看作是一个指针。注意a指针在这是隐式地是一个const指针,不能修改a的值,如a++;a=p;这都是错误的。
有了这层意思:
*a 等价 *(a+0) 等价 a[0]
&*a 等价 (a+0) 等价 &a[0]
如果定义指针,指向数组中首元素地址,也可简写成char* p=a;,其等价于char *p = a+0; 或p=&a[0];

既然数组名有两层意思,那么什么情况下是用作整体? 什么情况下是用作指向首地址的指针呢?

用作整体:
sizeof(a) 计算数组长度,a在这表整体,长度为10
&a 取数组地址,a在这表整体,是个指针,如果进行指针运算如&a+1, 偏移是10。这种用法不常见,实际很少使用。

用作指向首地址元素的指针:
取值 *a a[0] *(a+1) a[1]…
a+i 进行指针加减运算
char *p=a; //赋值给同类型指针,隐式转换
作实参,如func(a),这是函数调用部分的知识,见下文函数参数章节

3 高级篇(指针与二维数组)

3.1 二维数组

先声明一个二维数组:
char aa[10][20];

分析:
1、把最高维(二维)以下的维数遮掉, 可以将aa看作是一个一维数组,数组有10个元素;
2、一维数组aa的每个元素都是一个有20个char数据的一维数组;
3、一维数组aa的每个元素的长度20*sizeof(char)=20;

再来看一些语句的值:
sizeof(aa)    等于 10*20*sizeof(char)=200 aa是数组的整体。
我们知道:[]符号同*(),x[i] 等价于*(x+i)
可以得出:逐层展开
aa+i    指针,aa是数组名,在这里做加减运算,用作是指针,指向的是首元素地址,由于aa看作一维数组,它的元素类型是char [20]的数组,长度为20,所以aa+i,是在首地址上偏移i*20,也就是指向二维数组的第i行,也可以理解成一维数组的地址,相当于第二节中的&a;
aa[i]    等价于*(aa+i)是一个一维数组char [20]的数组名
aa[i] +j   等价于*(aa+i)+j,是个指针,数组名aa[i]在这用作指向数组aa[i]的首元素地址,表达式表示是的第i行的序号j的元素地址
aa[i][j]    等价于 *(aa[i]+j) 等价于*(*(aa+i)+j) 是char型的元素。
在二维数组中aa[10][20], aa是数组名,aa[i]也是数组名。

通过以上分析易得出:
sizeof(aa+0)    等于4, 是个指针
sizeof(aa[0])    等于20, aa[0]在这是个数组名,在这里用作数组整体,等价于sizeof(*aa)
sizeof(aa[0]+0)    等于4 是个指针,aa[0]在这是个数组名,在这里用作指针
sizeof(*aa[0])   等于1, char型,aa[0]在这是个数组名,在这里取值,用作指针,等价于sizeof(*(aa[0]+0))
sizeof(aa[0][0])    等于1,char型元素,等价于 sizeof(*aa[0]) sizeof(*(aa[0]+0))

二维数组,可以很方便处理以行为单位的多行数据,以及工程中常用的矩阵数据等等。

3.2 指针与多维数组

[]符号同*(),x[i] 等价于*(x+i), x在这时是作指向数组首元素的指针,如果我们定义一个指针p可以指向数组首元素,那么p就完全可以像数组名那样使用下标访问元素。
char* p = a; //a是一维数组,元素类型是char型, p[i] 与 a[i]等价
char (*pp)[20] = aa; //aa是二维数组,其一维类型是char [20],pp[i][j]与aa[i][j]等价
char (*ppp)[10][20] = aaa; //aaa是三维数组,其二维的类型是char [10][20],pp[i][j][k]与aa[i][j][k]等价

可以看到将数组名赋给指针,那么指针指向的必定是数组除最高维外剩余下的维数部分。也就是,一个高维的数组,可以看成是一个元素为低一维数据的一维数组。数组名用作指针时,指向的是低一维的数组,如果低一维不是数组,即0维,指向的就是数据元素。

char (*p)[20]; 可以看到中间用了(),倘若不用括号,char* p[20],这就变成一个普通的一维数组,有20个元素,元素的类型char*的指针,也就是一个指针数组。

3.3 指针数组与数组指针

指针数组:本身是个数组,其元素的类型是指针;
数组指针:本身是个指针,指向的数据是数组。

数组指针示例:
char (*p)[4]; //p是个指针,指向的类型是数组char [4]; 显然p+1, 指针运算,偏移的是指向的数组类型的整个长度,即4;
int (*p)[4][5]; //p是个指针,指向int [4][5]; 指针运算单位偏移长度sizeof(int)*4*5=80;

指针数组示例:
char* a[4]; //a是个一维数组, 其元素类型是char*指针,sizeof(a) 为sizeof(char*)*4等于16;
char* a[4][4]; //a是一个二维数组,包含4*4总共16个char*指针

数组指针数组:本身是个数组,其元素是数组指针,即它的元素是指向数组的指针。
示例:
char (*a[4])[5]; //a是个一维数组,有4个元素,元素是类型是char (*)[5],即指向一维数组char [5]的指针;指针长度为4,sizeof(a) 为sizof(char(*)[5])*4 等于16;
char (*a[4])[10][20]; //a是个一维数组,4个元素,元素类型是指向二维数组char[10][20]的指针,sizeof(a)等于4个指针的长度,即16
char (*a[4][5])[10][20]; //a是一个二维数组,总共20(4*5)个元素,每个元素都是一个指针,指向的是一个二维数组char[10][20], sizeof(a)等于20个指针长度,即80
char* (*a[4][5])[10][20]; //a是一个二维数组,总共20个元素,每个元素都是指针,指向的是一个二维数组char* [10][20],指向的二维数组中的元素类型是指针char*,sizeof(a)等于20个指针长度,即80

4 函数参数

我们知道C语言中实参传递给形参是没法直接传递过去整个数组(用结构体封装的不算传递的是数组),即形参不会拷贝一份完整的与实参一样的数组。为什么会设计成这样?
其实,通过上文分析 ,我们发现数组有一个特点,*那就是其实每个元素,都可以通过首地址偏移,找到其地址*,应用到函数的参数传递,其实只要传入首地址指针,在调用函数中就能访问到数组中的所有元素。

要在被调函数来访问调用函数中的数组,先以一维数组为例,形参的形式有以下几种:

void func(char a[10]); //这是初学者常用的形式, 也比较直观,可以告诉调用者数组长度为10
void func(char a[]);
void func(char *a);

其实上述三种方式完全是等价的, 不管是哪种形式,形参实际上都是一个char*指针。即调用函数中的实参直接使用一维数组名a,func(a),用到的是数组名的第二层含义,即指向首元素的指针。
我们知道a+i也是一个类型为数组元素的指针,所以形如func(a+i)调用语法上也是没有问题,但是注意的是在被调函数中a[0]对应的是调用函数中的a[i]了,实际很少会这样使用。

接入来看下二维数组,同理:
void func(int a[10][10]);
void func(int a[][10]);
void func(int (*a)[10]);
上述三种形式是完全等价的, 形参a是一个指针,指向的是一维数组int [10];不等价于void func(int **a);这里a指向的是int型指针,不是一维数组,这也就是为什么多维数组名不能看作是多级指针。
void func(int a[][]); //编译报错, 形参指针a在这里指向不明确;
所以采用数组形式做形参,一定要指定最高维以下所有维的维数,最高维非必须指定,即便是指定了也不起作用。

上述的三种形参情况, 第一种数组下标实际不起作用,第三种在函数中不清楚形参是不是用作数组;实际使用中一般使用第二种,同时有点变形,由于下标无法传入,所以我们再使用一个参数,表示数组最高维长度,形式变为:
void func(char a[], int length);
void func(char a[][4], int length);

我们的main函数的参数,用得就是类似的形式:int main(int argc, char * argv[]);
main函数还有另外一种写法:int main(int argc, char** argv);
可以看到形参:char* argv[] 等价于二级指针 char** argv
这时argv作为指针指向的是数组元素char*,即指向char*的指针,也就是char**。
形参中多级指针,等价于一维数组,数组的元素是少一级的指针,如char** argv[]等价于char ***argv, 而不是多维数组。

数组名用作指针时,是个只读的指针,不可改变,在被调函数中,为了让形参一直指向数组首地址,防止中途被更改,可以加上const修饰符,变形为:
void func(char* const a, int length); //要让a只读,只能用第三种指针的写法,使用第二种及第一种,做不到a只读
void func(char (*const a)[4], int lenth);

如果只需要引用数组中的值,但不修改,可以形式变为:
void func(const char a[], int length);
void func(const char a[][4], int length);