深入理解 C/C++ 数组和指针
本文转载自 CSDN @WalkingInTheWind,原文链接:https://blog.csdn.net/luckyxiaoqiang/article/details/7044380
C 语言中数组和指针是一种很特别的关系,首先本质上肯定是不同的,本文从各个角度来论述数组和指针。
1. 数组与指针的关系#
数组和指针是两种不同的类型,数组具有确定数量的元素,而指针只是一个标量值。数组可以在某些情况下转换为指针,当数组名在表达式中使用时,编译器会把数组名转换为一个指针常量,是数组中的第一个元素的地址,类型就是数组元素的地址类型,如int a[5] = {0, 1, 2, 3, 4};
,数组名a
若出现在表达式中,如int *p = a;
,那么它就转换为第一个元素的地址,等价于int *p = &a[0];
。再比如int aa[2][5] = { {0,1,2,3,4}, {5,6,7,8,9} };
,数组名aa
若出现在表达式中,如int (*p)[5] = aa;
,那么它就转换为第一个元素的地址,即等价于int (*p)[5] = &aa[0];
。但是int (*p)[5] = aa[0];
这个就不对了,根据规则我们推一下就明白了,aa[0]
的类型是int [5]
,是一个元素数量为 5 的整型数组,就算转化,那么转化成的是数组(int [5]
)中第一个元素的地址,即&aa[0][0]
,类型是int *
。所以,要么是int (*p)[5] = aa;
,要么是int (*p)[5] = &aa[0];
。
只有在两种场合下,数组名并不用指针常量来表示:就是当数组名作为sizeof
操作符或单目操作符&
的操作数时。sizeof
返回整个数组的长度,使用的是它的类型信息,而不是地址信息,不是指向数组的指针的长度;取一个数组名的地址所产生的是一个指向数组的指针,而不是指向某个指针常量值的指针。如上述数组int a[5]
,&a
表示的是指向数组a
的指针,类型是int (*)[5]
,所以int *p = &a;
是不对的,因为右边是一个整型数组的指针int (*)[5]
,而p
是一个整型指针int *
。关于数组的sizeof
问题会在下文中仔细讨论。
2. 数组与指针的下标引用#
int a[5] = {0, 1, 2, 3, 4};
,如a[3]
,用下标来访问数组a
中的第三个元素,那么下标的本质是什么呢?本质就是这样的一个表达式:*(a+3)
,当然表达式中必须含有有效的数组名或指针变量。其实a[3]
和3[a]
是等价的,因为他们被翻译成相同的表达式(顶多顺序不同而已),都是访问的数组a
中的元素3
。指针当然也能用下标的形式了,如int *p = a;
,那么p[3]
就是*(p+3)
,等同于3[p]
,同样访问数组a
中的元素3
。根据这一规则,我们还能写出更奇怪的表达式,如int aa[2][5] = { {0,1,2,3,4}, {5,6,7,8,9} };
,则有1[aa][2]
,这个看起来很别扭,首先1[aa]
,就是*(1+aa)
,那么1[aa][2]
就是*(*(1+aa)+2)
,也就是aa[1][2]
。但是1[2][aa]
,这个就不对了,因为前半部分1[2]
是不符合要求的。当然在实际中使用这样的表达式是没有意义的,除非就是不想让人很容易的看懂你的代码。
3. 数组与指针的定义和声明#
数组和指针的定义与声明必须保持一致,不能一个地方定义的是数组,然后在另一个地方声明为指针。
首先我们解释一下数组名的下标引用和指针的下标引用,它们是不完全相同的。从访问的方式来讲,对于int a[5] = {0, 1, 2, 3, 4};
,有int *p = a;
,虽然a[3]
和p[3]
都会分别被解析成*(a+3)
和*(p+3)
,但是实质是不一样的。
- 对于
a[3]
,也就是*(a+3)
- 把数组名
a
代表的数组首元素地址和3
相加,得到要访问数据的地址(这里注意,数组名a
直接被编译成数组的首元素地址); - 访问这个地址,取出数据。
- 把数组名
- 对于
p[3]
,也就是*(p+3)
- 从指针变量
p
所代表的地址单元里取出内容(指针变量存储地址数据),也就是数组首元素的地址(这里注意,指针名p
代表的是指针变量的存储地址,而指针变量p
所存储的地址数据才是数组的首元素地址)【注解:正如int n = 5
,变量名n
代表的是变量n
的存储地址,而地址里为变量n
所存储的整型数据5
,指针也是普通的变量,只不过指针变量存储的是地址数据,所以这一步就是取指针变量的数据而已】; - 把取出的变量
p
的数据(即数组首元素的地址)和3
相加,得到要访问的数据的地址; - 访问这个地址,取出数据。
- 从指针变量
下面给出一个例子来说明若定义和声明不一致带来的问题:设 test1.cpp 中有如下定义:char s[] = "abcdefg";
,test2.cpp 中有如下声明:extern char *s;
,显然编译是没有问题的。那么在 test2.cpp 中引用s[i]
结果会怎样呢?如s[3]
,结果会是d
吗?下面我们对 test2.cpp 中的s[3]
进行分析:s
的地址当然是由 test1.cpp 中的定义决定了,因为在定义时就分配了内存空间。我们根据上面所给出的下标引用的示例规则来进行分析,如下所示:
- 从
s
所代表的地址单元取出内容(4 个字节的地址数据),这里实际上是数组s
中的前 4 个元素,这个值是abcd
,也就是十六进制的64636261h
【注解:由于s
声明为指针,s[3]
即*(s+3)
,则需要先取出指针变量s
所存储的地址数据。而若要得到变量s
的数据,需要知道变量s
的地址,而变量s
在其定义时地址就确定了,并且分配了存储空间,存储了"abcdefg"
,由此可以得到指针变量s
的内容为abcd
。换言之,定义为数组而声明为指针时,多了一次寻址操作】; - 然后把取到的地址数据和
3
相加,得到要访问的数据的地址,即64636261h + 3
,这个地址是未分配未定义的; - 取地址
64636261h + 3
的内容,由于这个地址单元是未定义的,访问就会出错。
下面给出分析的代码(可以只观察有注释的部分):
#include <iostream>
using namespace std;
extern void test();
char s[] = "abcdefg";
int main(void)
{
002E13A0 push ebp
002E13A1 mov ebp,esp
002E13A3 sub esp,0D8h
002E13A9 push ebx
002E13AA push esi
002E13AB push edi
002E13AC lea edi,[ebp+FFFFFF28h]
002E13B2 mov ecx,36h
002E13B7 mov eax,0CCCCCCCCh
002E13BC rep stos dword ptr es:[edi]
char ch;
int i = 3;
002E13BE mov dword ptr [ebp-14h],3
ch = s[i];
002E13C5 mov eax,dword ptr [ebp-14h]
/* s 直接被翻译成数组首元素的地址和 i(eax) 相加,得到操作数地址,然后作为 byte ptr 类型取内容,传给 cl */
002E13C8 mov cl,byte ptr [eax+011F7000h]
002E13CE mov byte ptr [ebp-5],cl /* cl 的内容传给 ch(ebp-5) */
test();
002E13D1 call 002E1073
return 0;
002E13D6 xor eax,eax
}
002E13D8 pop edi
002E13D9 pop esi
002E13DA pop ebx
002E13DB add esp,0D8h
002E13E1 cmp ebp,esp
002E13E3 call 002E113B
002E13E8 mov esp,ebp
002E13EA pop ebp
002E13EB ret
以下为 test2.cpp,则运行错误:
extern char *s;
void test()
{
011F1470 push ebp
011F1471 mov ebp,esp
011F1473 sub esp,0D8h
011F1479 push ebx
011F147A push esi
011F147B push edi
011F147C lea edi,[ebp+FFFFFF28h]
011F1482 mov ecx,36h
011F1487 mov eax,0CCCCCCCCh
011F148C rep stos dword ptr es:[edi]
char ch;
int i = 3;
011F148E mov dword ptr [ebp-14h],3
ch = s[i];
/* ds 没有影响,因为 windows 中所有的段基址都为 0,取 011F7000h 单元的内容,
这里是数组中的前四个字节(指针是四个字节)组成的整数,也就是 64636261h,
也就是这里,把 s 所指的单元计算成了 64636261h */
011F1495 mov eax,dword ptr ds:[011F7000h]
/* 然后把地址和 i 相加,也就是 64636261h + 3,这个地址是未分配未定义的,访问当然会出错 */
011F149A add eax,dword ptr [ebp-14h]
011F149D mov cl,byte ptr [eax] /* 访问错误 */
011F149F mov byte ptr [ebp-5],cl
return;
}
011F14A2 pop edi
011F14A3 pop esi
011F14A4 pop ebx
011F14A5 mov esp,ebp
011F14A7 pop ebp
011F14A8 ret
若 test2.cpp 中这样声明:extern char s[];
,这样就正确了,因为声明和定义一致,访问就没问题了。所以千万不要简单的认为数组名与指针是一样的,否则会吃大亏,数组的定义和声明千万要保持一致性。
4. 数组和指针的sizeof
问题#
数组的sizeof
就是:数组的元素个数 × 元素大小,而指针的sizeof
全都是一样,都是地址类型,32 位机器是 4 个字节。下面给出一些例子:
#include <iostream>
using namespace std;
int main()
{
int a[6][8] = {0};
int (*p)[8];
p = &a[0];
int (*pp)[6][8];
pp = &a;
cout << sizeof(a) << endl; // 192
cout << sizeof(*a) << endl; // 32
cout << sizeof(&a) << endl; // 4
cout << sizeof(a[0]) << endl; // 32
cout << sizeof(*a[0]) << endl; // 4
cout << sizeof(&a[0]) << endl; // 4
cout << sizeof(a[0][0]) << endl; // 4
cout << sizeof(&a[0][0]) << endl; // 4
cout << sizeof(p) << endl; // 4
cout << sizeof(*p) << endl; // 32
cout << sizeof(&p) << endl; // 4
cout << sizeof(pp) << endl; // 4
cout << sizeof(*pp) << endl; // 192
cout << sizeof(&pp) << endl; // 4
system("pause");
return 0;
}
VS2010 在 32 位 windows7 下的运行结果如以上代码中的注释部分(VC6.0 不符合标准),下面对程序逐一做简单的解释:
sizeof(a);
,a
的定义为int a[6][8]
,类型是int [6][8]
,即元素个数为6*8
的二维int
型数组,它的大小就是6*8*sizeof(int)
,这里是192
;sizeof(*a);
,*a
这个表达式中数组名a
被转换为指针,即数组第一个元素a[0]
的地址,*
得到这个地址所指的对象,也就是a[0]
,总的来说*a
等价于*(&a[0])
,a[0]
的类型int [8]
,即大小为 8 的一维int
型数组,它的大小就是8*sizeof(int)
,这里是32
;sizeof(&a);
,&
取a
的地址,类型是int (*)[6][8]
,地址类型,这里大小是4
;sizeof(a[0]);
,a[0]
的类型int [8]
,即大小为 8 的一维int
型数组,它的大小就是8*sizeof(int)
,这里是32
;sizeof(*a[0]);
,*a[0]
这个表达式中数组名a[0]
被转换为指针,即数组的第一个元素a[0][0]
的地址,*
得到这个地址所指的元素,也就是a[0][0]
,总的来说*a[0]
等价于*(&a[0][0])
,a[0][0]
的类型是int
,它的大小就是sizeof(int)
,这里是4
;sizeof(&a[0]);
,&
取a[0]
的地址,类型是int (*)[8]
,地址类型,这里大小是4
;sizeof(a[0][0]);
,a[0][0]
的类型是int
,它的大小就是sizeof(int)
,这里是4
;sizeof(&a[0][0]);
,&
取a[0][0]
的地址,类型是int *
,地址类型,这里大小是4
;sizeof(p);
,p
的类型是int (*)[8]
,指向一个元素个数为 8 的int
型数组,地址类型,这里大小是4
;sizeof(*p);
,*p
取得p
所指的元素,类型是int [8]
,大小为8*sizeof(int)
,这里是32
;sizeof(&p);
,&
取p
的地址,类型是int (**)[8]
,地址类型,这里大小是4
;sizeof(pp);
,pp
的类型是int (*)[6][8]
,指向一个大小为6*8
的二维int
型数组,地址类型,这里大小为4
;sizeof(*pp);
,*pp
取得pp
所指的对象,类型是int [6][8]
,即元素个数为6*8
的二维int
型数组,它的大小就是6*8*sizeof(int)
,这里是192
;sizeof(&pp);
,&
取pp
的地址,类型是int (**)[6][8]
,地址类型,这里大小是4
;
5. 数组作为函数参数#
当数组作为函数参数传入时,数组退化为指针,类型是第一个元素的地址类型。“数组名被改写成一个指针参数”,这个规则并不是递归定义的。数组的数组会被改写为“数组的指针”,而不是“指针的指针”。下面给出几个例子:
fun1(char s[10])
{
// s 在函数内部的实际类型是 char *
}
fun2(char s[][10])
{
// s 在函数内部的实际类型是 char (*)[10],即 char [10] 数组的指针
}
fun3(char *s[15])
{
// s 在函数内部的实际类型是 char **,字符型指针的指针
}
fun4(char (*s)[20])
{
// s 在函数内部的实际类型不变,仍然是 char (*)[20],即 char [20] 数组的指针
}
以上可以简单的归纳为:数组作为参数被改写为指向数组的第一个元素(这里的元素可以是数组)的指针。数组作为参数必须提供除了最左边一维以外的所有维长度。
另外,我们还要注意:char s[][10]
和char **s
作为函数参数是不一样的,因为函数内部指针的类型是不一样的(数组指针和二级指针),尤其在进行指针加减运算以及sizeof
运算时。
总结:总结了这么多,应该对数组和指针有个较深入的理解了。这些问题的归根原因还是来自于指针问题,这也正是 C 语言的精华所在,不掌握这些根本不算掌握 C 语言,不过掌握了这些也不敢说就等于掌握了 C 语言!!!
作者:bitlogic
出处:https://www.cnblogs.com/bitlogic/p/8800804.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人