一、二维数组地址计算公式
计算二维数组地址,核心只有三步:确定基址 → 数清前面元素个数 → 乘以元素字节数。
绝大多数语言(C/C++/Java)采用行优先存储(先存满第一行,再存第二行),记住这一个公式就够了:
Addr = 基址 + (i × 总列数 + j) × 单个元素大小- 下标从 0 起(如
A[i][j]):直接套用公式。 - 前面的元素个数 =
i行完整的元素 + 第i行中j前面的元素。
举个极简例子:int A[3][4](3行4列),基址=1000,int占4字节,求A[1][2]。
前面元素数 = 1×4 + 2 = 6 个,地址 = 1000 + 6×4 =1024。
二、数组名的三种含义
sizeof(数组名)—— 数组名表示整个数组,计算整个数组的大小。&数组名—— 数组名表示整个数组,取出整个数组的地址。- 其他所有情况 —— 数组名表示首元素的地址。
三、经典面试题实战
1. 一维数组a与&a的区别
#include<stdio.h>intmain(){inta[5]={1,2,3,4,5};int*ptr=(int*)(&a+1);printf("%d,%d",*(a+1),*(ptr-1));return0;}输出结果:2,5
解析:
a:数组名,转为int*,a+1偏移1个int,*(a+1) = a[1] = 2。&a:整个数组的地址,类型为int(*)[5];&a+1跳过整个数组(5个int),指向数组尾后。ptr强转为int*,ptr-1回退1个int,指向a[4],取值为5。
核心考点:地址值可能相同,但指针类型不同,步长完全不同。
2. 指针步长与强制类型转换
// x86 32位,假设struct Test大小20字节structTest{intNum;char*pcName;shortsDate;charcha[2];shortsBa[4];}*p=(structTest*)0x100000;intmain(){printf("%p\n",p+0x1);printf("%p\n",(unsignedlong)p+0x1);printf("%p\n",(unsignedint*)p+0x1);return0;}输出结果:
0x100014 0x100001 0x100004解析:
p+0x1:struct Test*指针,+1 偏移sizeof(struct Test)=20(0x14)→0x100000+20 = 0x100014。(unsigned long)p+0x1:转为普通整型,纯数字加1 →0x100000+1 = 0x100001。(unsigned int*)p+0x1:int*指针,+1 偏移4字节 →0x100000+4 = 0x100004。
考点:指针加法偏移量 = 个数 ×
sizeof(指向类型);整数强转后不能再当指针解引用。
3. 逗号运算符 + 二维数组初始化
#include<stdio.h>intmain(){inta[3][2]={(0,1),(2,3),(4,5)};int*p;p=a[0];printf("%d",p[0]);return0;}输出结果:1
解析:
(0,1)是逗号表达式,取最右侧值,所以(0,1)=1,(2,3)=3,(4,5)=5。- 数组实际初始化:
int a[3][2] = {1, 3, 5},剩余元素自动置0。 p[0]等价于a[0][0],输出1。
陷阱:
()不是{},圆括号触发逗号运算符。正确二维初始化应写成{{0,1},{2,3},{4,5}}。
4. 数组指针与指针相减
#include<stdio.h>intmain(){inta[5][5];int(*p)[4];p=a;printf("%p,%d\n",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);return0;}输出结果:FFFFFFFC,-4(32位环境)
解析:
int (*p)[4]:数组指针,指向大小为4的int数组;p+1偏移4*sizeof(int)=16字节。p=a将int[5][5]的首行地址赋给p,但p的步长是int[4]。p[4][2]等价*(*(p+4)+2)p+4偏移4个int[4]→ 偏移了4×4 = 16个int。a[4][2]是数组a第4行第2列,距起始4×5+2 = 22个int。&p[4][2]距起始16+2 = 18个int。
- 指针相减得到元素个数差:
18 - 22 = -4。 %p将-4按无符号地址打印,输出FFFFFFFC;%d正常输出-4。
考点:数组指针步长由
[]内数字决定;同类型指针相减得到的是元素个数,不是字节数。
5.&aa+1与aa+1的区别(二维数组)
#include<stdio.h>intmain(){intaa[2][5]={1,2,3,4,5,6,7,8,9,10};int*ptr1=(int*)(&aa+1);int*ptr2=(int*)(*(aa+1));printf("%d,%d",*(ptr1-1),*(ptr2-1));return0;}输出结果:10,5
解析:
&aa:整个二维数组的地址;&aa+1跳过整个数组(10个int),指向数组末尾之后。ptr1 = (int*)(&aa+1),ptr1-1回退1个int,指向最后一个元素10。aa:数组名,aa+1指向第二行,等价&aa[1];*(aa+1)得到第二行首元素地址(元素6的地址)。ptr2指向6,ptr2-1回退到上一行末尾,取值5。
考点:
&数组名+1跳过整个数组;数组名+1跳过一行。
6. 字符指针数组与二级指针
#include<stdio.h>intmain(){char*a[]={"work","at","alibaba"};char**pa=a;pa++;printf("%s\n",*pa);return0;}输出结果:at
解析:
char* a[]:字符指针数组,每个元素指向一个字符串常量。pa初始指向a[0](即指向"work"的指针),pa++后移一个char*大小,指向a[1]。*pa得到a[1],即"at"的首地址,打印at。
考点:
char* a[]是指针数组;二级指针加减步长是指针类型的大小。
7. 多级指针、指针数组与运算符优先级(压轴题)
#include<stdio.h>intmain(){char*c[]={"ENTER","NEW","POINT","FIRST"};char**cp[]={c+3,c+2,c+1,c};char***cpp=cp;printf("%s\n",**++cpp);printf("%s\n",*--*++cpp+3);printf("%s\n",*cpp[-2]+3);printf("%s\n",cpp[-1][-1]+1);return0;}输出结果:
POINT ER ST EW逐步解析:
- 初始状态:
c[]={"ENTER", "NEW", "POINT", "FIRST"}cp[]={&c[3], &c[2], &c[1], &c[0]}(即cp[0]指向"FIRST"的指针,以此类推)cpp指向cp[0]
**++cpp++cpp→cpp指向cp[1];- 第一次解引用得到
c+2; - 第二次解引用得到
c[2],即"POINT"。 →POINT
*-- * ++cpp + 3++cpp→cpp指向cp[2];*cpp得到c+1;--(*cpp)将cp[2]的值从c+1改为c(指向"ENTER");- 解引用得到
"ENTER"首地址,+3偏移三个字符,指向E后面的R,输出ER。 →ER
*cpp[-2] + 3- 此时
cpp仍指向cp[2]; cpp[-2]等价*(cpp-2),得到cp[0],即c+3(指向"FIRST");- 解引用并
+3,从F跳过三个字符得到S,输出ST。 →ST
- 此时
cpp[-1][-1] + 1cpp[-1]得到cp[1],其值仍为c+2(未被修改);(c+2)[-1]等价*(c+2-1) = c[1],即"NEW";+1偏移一个字符,得到"EW"。 →EW
考点总结:
++/--优先级高于*;[]等价于指针加减。cpp[-2]表示*(cpp-2),负下标完全合法。*-- * ++cpp中--修改了指针数组里的值,会影响后续所有用到该位置的计算。
四、总结
二维数组地址计算看似简单,但一旦结合数组名、数组指针、指针步长、强制类型转换以及运算符优先级,就会衍生出各种让人头疼的面试题。核心还是要牢记三点:
- 地址公式:
基址 + (i×列数 + j)×元素大小 - 指针步长:由指针类型严格决定(
&数组名+1跳整个数组,数组名+1跳一行) - 表达式优先级:善用括号,理解
*、++、[]的运算顺序。
希望这篇整理能帮你彻底拿下 C 语言中的二维数组与指针难题!