
数组是一段连续的内存,除了定义数组变量,也会用指针来表示数组,但是数组和指针不是完全相同的,数组有很多特性,例如数组有确定数量的元素,而指针只是一个标量值。
编译器用数组名来记住这些属性,只有当数组名在表达式中使用时,编译器才会认为它产生一个指针常量(不是指针变量,数组名的值是指针常量,详见C和指针P142)。


我们习惯的认为的1D,2D,3D数组的样子:

但是计算机中数组的实际样子:

这三个函数分别是给一维二维三维数组赋值,但是他们的汇编指令显然完全相同,因此无论数组是一维的还是高维的,他们都是一段连续的一维内存。(注意理解数组存储的原理很关键)

以二维数组详细分析

1)行地址
1、二维数组中,数组名a的值,是数组a首元素a[0][0]的地址,即&a[0][0],第一行第一个元素的地址;
2、二维数组中,数组名a+1是数组a的元素a[1][0]的地址,即&a[1][0],第二行第一个元素的地址;
2)列地址
1、二维数组中,a[0]的值,即该数组的首元素a[0][0]的地址,即&a[0][0];
2、二维数组中,a[0]+1的值,是数组元素a[0][1]的值,即&a[0][1];
3)混合一下
1、二维数组中,“a[0]+1”是指向数组元素a[0][1]的地址,“a[1]+2”是指向数组元素a[1][2]的地址;
2、同样的,二维数组中,“*(a+1)+2”是指向数组元素a[1][2]的地址,与“a[1]+2”相等;
PS:*(a+1)表示第2行的行地址;*a+1表示第一行第二个元素的地址;
3、二维数组中,*(*(a+1)+2))是数组元素a[1][2]的值!!!
#include <stdio.h>
int main()
{
int a[2][3]={{1,2,3},{4,5,6}};
int i,j;
int *p1,*p2,*p3,*p4,*p5,*p6;
printf("array a is :\n");
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%d ",a[i][j]);
}
printf("\n");
}
printf("array b is :\n");
p1=a;//二维数组的数组名,即首元素a[0][0]的首地址;
p2=a+1;//a+1是数组a的元素a[1][0]的地址,即&a[1][0]
p3=a[0];//同a[0][0]的地址,即第一行第一个元素的地址
p4=a[1];//同a[1][0]的地址,即第二行第一个元素的地址
printf("p1=a指向的值是:%d;地址是:%d\n",*p1,p1);
printf("p2=a+1指向的值是:%d;地址是:%d\n",*p2,p2);
printf("p3=a[0]指向的值是:%d;地址是:%d\n",*p3,p3);
printf("p4=a[1]指向的值是:%d;地址是:%d\n",*p4,p4);
p5=a[0]+1;//指向第一行,第二列元素的地址;
p6=a[1]+1;//指向第二行,第一列元素的地址;
printf("p5=a[0]+1指向的值是:%d;地址是:%d\n",*p5,p5);
printf("p6=a[1]+1指向的值是:%d;地址是:%d\n",*p6,p6);
printf("*(a+1)指向的地址是:%d\n",*(a+1));
printf("*(*(a+1))指向的值是:%d\n",*(*(a+1)));
printf("*(a+1)+2指向的地址是:%d\n",(*(a+1)+2));
printf("*(a+1)+2指向的值是:%d\n",*(*(a+1)+2));
printf("*a+2指向的值是:%d;地址是:%d\n",*(*a+2),*a+2);
return 0;
}

注:在二维数组a[i][j]中,a[i]是“行名”,等价于指针;
a[0]等价于a,都表示指针;
a[1]等价于a+1,都表示指针;
a[1]+1等价于*(a+1)+1,都表示指针;
注意防止越界!



因此函数以数组作为参数时传入的都是数组的首地址,将首地址存放在一个临时的指针变量里。
//例如
void func(int *arr){}作为函数参数时的规范:通常为了防止数组越界还会增加一个参数来表示数组长度。越界的危害见堆栈隐患文章(数组越界可能会导致函数的地址被篡改)
//规范写法
void func(int *arr,int length){}数组定义中的中括号就是表示它是个数组,属于语法范畴(就跟函数调用里面的逗号,语句后面的分号,还有语句块的大括号一样),不算运算符,不能更改和重载,例如 int a[10]或者int *a = new int[10]等。
但是C和C++语言里中括号还有个用法就是作为一个运算符,是一个叫做“数组下标运算符”的双目运算符,即a[b]。其默认等价于*(a+b)。但是在你自己定义的类中,这个运算符是可以重载的,使之不等于*(a+b),这个时候这个运算符的意义就不是“数组下标”了,而是你自己定义的了,甚至可以像括号那样用。
int a[10];
a[1] == *(a + 1)//根据指针我们知道[1]其实就是a地址向后偏移了一个int,即指针的+操作是地址的偏移,指向下一个元素的地址。所以理解数组[]的原理尤为关键。
int a[10] = {1,2,3,4,5};
b = a[1];//b = 2 ,b = *(a + 1)
b = b[-1];//b = 1 ,b = *(a +1 -1)#include<stdio.h>
int main()
{
char *char1 = "helloworld";//指针的方式定义,注意这种定义方式在vs中报错
char char2[11] = "helloworld";//数组的方式定义
return 0;
}PS:易错点,helloworld这十个字符能否放到char char2[10]中?显然不能,勿忘字符串的结尾符号\0也占一个位置。
1.字符串的构造方式不同
char *char1 = "helloworld"是用char1这个指针指向这个字符串。这个helloworld存放在字符串常量区。
而char char2[11] = "helloworld";是在内存里创建了一块空间,把常量区的helloworld拷贝并存放到这块内存中。
2.字符串单个元素的修改情况不同
#include<stdio.h>
int main()
{
char *char1 = "helloworld";//指针的方式定义
char char2[11] = "helloworld";//数组的方式定义
//修改元素
*char1[0] = 'h';//报错,字符串常量不可以修改!
char2[0] = 'h';//正常运行
return 0;
}要操作字符串,只能把它用数组方式,引入存放变量的区,才能操作,在常量区不能对字符串常量进行修改操作。
#include<stdio.h>
int main()
{
char char2[11] = "helloworld";//数组的方式定义
char *char1 = char2;//指针的方式定义
//修改元素
*char1[0] = 'h';//可以修改,正常运行
char2[0] = 'h';//正常运行
return 0;
}3.字符串总体修改情况不同
#include<stdio.h>
int main()
{
char *char1 = "helloworld";//指针的方式定义
char char2[11] = "helloworld";//数组的方式定义
//修改元素
*char1 = 'abcde';//正常运行
char2 = 'abcde';//报错
return 0;
}用指针定义的字符串可以整体进行修改,因为其只是将指向的常量地址进行更改,指向了另一个字符串,常量区出现在程序编译前。
C/CPP标准都规定了:只有标量和结构体,才能出现在赋值操作符=的左侧,但是数组类型并不是一个标量,因此不能对数组执行赋值操作。(但是可以从键盘读入,见下面4)
4.能否用I/O函数从键盘输入字符串
#include<stdio.h>
int main()
{
char *char1 = "helloworld";//指针的方式定义
char char2[11] = "helloworld";//数组的方式定义
char1 = char2;
gets(char2);//合法,即使已经被初始化了,但是仍然能读取键盘输入重新赋值。
puts(char1);
puts(char2);
/*
Input:abcde
Output:
abcde
abcde
*/
return 0;
}指针只能指向已经有了的,定义了的,所以用指针来定义的字符串不能从键盘读入值。
用数组来定义 的可以从键盘读入(哪怕已经被初始化为一个其他的值,仍能再次通过键盘进行赋值)。
5.处理效率不同(详见C Primer Plus)
用指针直接指向字符串常量时可以节省了普通定义数组时在内存创建空间的时间和空间。
6.作为函数参数,形式不同,但是传递的都是地址(详见C Primer Plus)
函数参数char *a 和char a[]相同,传递的都是地址。