C/C++中指针、数组、二维数组与指针数组的理解与原理

        在学习C语言的过程中我们往往会被指针与数组弄的晕头转向,指针与数组有什么相似点?指针数组与二维数组有什么不同点?指针与数组是怎么存值和取值的?在内存中又是以什么形式存在的?等等。接下来我们将探讨这些问题。(这些知识点可能有些枯燥,希望能耐下心看完)

首先我们来看一维数组,以整型数组举例:

int A[3];

        我们定义了一个标识符为A(数组名)的整型数组,数组长度为3。在32位的编译器下编译,int数据类型为4字节,那么A数组在内存中的状态如图(地址递增方向不考虑):

6ab676e365b74a7baada2218e5d9fdca.png

        我们可以看到数组A被分配了一段连续的内存空间用于存放数据,每个数组下标都有对应的地址,A[0]的地址对应1000,A[1]的地址对应1004。其中地址之所以每个下标相差4是因为int数据类型占4字节。我们可以用代码来验证:

int A[3];
for (int i = 0; i < 3; i++)
{
    printf("A[%d]的地址为:%x\n", i, &A[i]);    //将A[i]的地址以16进制输出
}
printf("\n");

运行结果如图,可以看到确实是连续的地址空间,每个下标的地址间隔4:

d3800cc7d40148aea50182434a79d593.png

 接下来我们来看指针,也以int类型指针为例,我们定义一个int型变量a,并用int型指针p指向a:

int a = 3;
int* p = &a;

指针在内存中的状态如图:

6cac8faf98074221938cc58b55429e4d.png

从图中我们可以很明显的看到,指针变量p存储的是变量a的地址值,当我们要引用变量a的值时,便可通过用p中的地址找到a的存储单元,并读出其值(*p操作)。同样,我们可以用代码验证:

int a = 3;
int* p = &a;
printf("a的地址为:%x\n", &a);
printf("指针p存储的数据为:%x\n", p);
printf("a存储的数据为:%x\n", a);
printf("以指针p中的值为地址,查找到的值为:%x\n", *p);
printf("\n\n");

 f7c392d1f04a46dda3d283340178dc12.png

        看到这里,第一个问题就来了,到底数组与指针在哪里有相似的地方?事实上在C语言中数组名其实是一个地址常量,它所对应的值为数组的起始地址。比如前面的数组A[],A所表示的值便为数组A[]的起始地址d0eff868,也即A[0]的地址。巧就巧在指针也是存储地址值,这样两者在某些方面便有相似操作,比如对数组的访问,我们可以用*(A+i)的方式(这里的A+i并不是简单的数值相加,因为数组A为int型,实际上为A+i*4,4为int类型的字节长度,对于int类型的指针也是如此),它跟A[i]是等价的。同样指针p对数组的访问也可以用p[i]的方式。代码验证:

int A[3] = { 1,2,3 };
int* p;
p = A;    //将数组A的起始地址赋值给指针p,等价于p = &A[0]
for (int i = 0; i < 3; i++)
{
    printf("A[%d]的值为:%x\n", i, *(A + i));    //等价于A[i]
    printf("A[%d]的值为:%x\n", i, p[i]);    //等价于*(p+i)
}

 7b250b40496d4f1ab4498d894cc6641e.png

        在弄懂了数组和指针的存储方式之后,我们也能解释为什么当数组与指针作为形参时,改变形参的值可能也会改变实参的值,因为传递的参数其实是地址值,这样,对地址所对应的内存进行操作的话,实际上就改变了原来的变量值。测试代码:

void fpoint(int* p)
{
	printf("形参指针p的地址:%x\n", &p);
	printf("形参指针p的值:%x\n", p);
	printf("形参指针p的存储地址值所对应的内存中的值:%x\n", *p);
	*p = 0xff;
}

void farray(int A[])    //int A[]等价于int* A
{
	printf("&A的值为:%x\n", &A);
	printf("形参数组A的起始地址:%x\n", A);
	printf("形参A[0]的地址:%x\n", &A[0]);
	printf("形参A[0]的值:%x\n", A[0]);
	A[0] = 0xff;
}

int main()
{
	int a = 0x10;
	printf("变量a的地址:%x\n", &a);
	printf("变量a的值:%x\n", a);
	int* s = &a;
	printf("实参指针s的地址:%x\n", &s);
	printf("实参指针s的值:%x\n", s);
	printf("实参指针s的存储地址值所对应的内存中的值:%x\n", *s);
	fpoint(s);
	printf("调用函数后变量a的值:%x\n\n", a);

	int B[3] = { 1,2,3 };
	printf("&B的值为:%x\n", &B);
	printf("数组B的起始地址:%x\n", B);
	printf("B[0]的地址:%x\n", &B[0]);
	printf("B[0]的值:%x\n", B[0]);
	farray(B);
	printf("调用函数后B[0]的值:%x\n", B[0]);
}

 

         看完上述代码的运行结果,可能有人注意到了这一点,就是实参指针s与形参指针p虽然两者的值一样,但各自的地址却不相同;同样,对于数组A和B也有这样的结果。事实上,在我们调用函数之后,程序会申请一段内存用来储存形参,也即形参指针p,然后再将实参指针s的值复制到指针p中,也就是说,指针s和p并不是同一个指针变量,只是保存的值相等,都为变量a的地址,也即都指向变量a。数组的传参也是同理,我们定义的形参数组A实际上也是一个指针,指针变量名为A,值为数组B的起始地址,这也就是为什么我注释中说int A[]与int* A是等价的。值得注意的是,在某些情况下,我们希望能调用函数实现对指针指向的改变,即修改指针的值,但函数中形参指针并不是我们要改变的原指针,这该怎么办?用取地址符可以解决这个问题,即:int* &p。

        在熟悉了指针与一维数组的基础上,再来学习二维数组与指针数组就简单了,二维数组实际上就是由一维数组组成的数组,我们仍旧以int型数组为例,定义一个二维数组A,对其初始化:

int A[3][3] = { {1,2,3},{4,5,6},{7,8,9} };

二维数组在内存中的状态如图:

        从图中可以看出二维数组跟一维数组一样,也是被分配了一段连续的内存空间。其实我们可以把二维数组A看成是由A[0]、A[1]、A[2]组成的一维数组,而A[0]、A[1]、A[2]各自又是一个一维数组。这样看来A就是A[0]的地址,A[0]就是A[0][0]的地址。代码验证:

int A[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
printf("A的地址:%x\n", &A);
printf("A的值:%x\n", A);
printf("A[0]的地址:%x\n", &A[0]);
printf("A[0]的值:%x\n", A[0]);
printf("A[0][0]的地址%x\n", &A[0][0]);
printf("A[0][0]的值:%x\n", A[0][0]);
printf("\n");
for (int i = 0; i < 3; i++)
{
    for (int j = 0; j < 3; j++)
	{
		printf("A[%d][%d]的地址:%x  ", i, j, &A[i][j]);
	}
	printf("\n");
}

        可能看到这里大家会有点迷糊,A是A[0]的地址,A[0]又是A[0][0]的地址,这到底是什么鬼?先不着急弄懂这个问题,我们先来看另一个结构,二级指针。前面我们知道指针是保存某一个变量的地址值,那二级指针是用来干嘛的?其实二级指针就是指向指针的指针,它保存的是另一个指针变量的地址值。还是以int型指针作为例子:

int a = 3;
int* p = &a;
int** pp = &p;

我们用一个二级指针pp指向一级指针p,一级指针p指向变量a。在内存中的状态如图:

由图可知二级指针pp为一级指针p的地址,一级指针p为变量a的地址。*pp等价于p,*p等价于a。验证代码:

int a = 3;
int* p = &a;
int** pp = &p;
printf("a的值:%x\n", a);
printf("a的地址:%x\n", &a);
printf("p的值:%x\n", p);
printf("p的地址:%x\n", &p);
printf("*p的值:%x\n", *p);
printf("pp的值:%x\n", pp);
printf("pp的地址:%x\n", &pp);
printf("*pp的值:%x\n", *pp);
printf("**pp的值:%x\n", **pp);

         到这里我们就可以得出结论了,二维数组名A实际上就是一个二级指针,它指向A[0],而A[0]是一个一级指针,它指向A[0][0]。即A+i是第一行的地址&A[i],*(A+i)是第i行首元素的地址      &A[0][0],**(A+i)是第i行首元素的值A[0][0],那么通过A来访问第i行第j列的值就为*(*(A+i)+j)。这里也有一点要注意,我们虽然说A是一个二级指针,但并不代表A是一个二级指针变量,A[0]也是一样并不是一级指针变量,它们都是地址常量,并没有独立的内存空间,也不可修改,从它们的存储地址都为&A[0][0]也可以看出来。

        接下来就是最后的指针数组了,在讨论指针数组之前我们先来看一段代码,这是C++中动态创建二维数组的代码:

int n = 3, m = 3;
int** A = new int* [n];
for (int i = 0; i < m; i++)
{
	A[i] = new int[m];
}

         如果你能理解这段代码的意思的话,那么恭喜你,你已经掌握指针数组了。事实上这就是指针数组的创建,当然你也可以把它当成二维数组,尽管在数组名含义上有些许不同,但它们使用起来都是一样的,都可以通过A[0][0]来访问数组值。理解不了这段代码也没关系,我们可以通过内存状态图来理解:

         看图再来理解代码就简单了,事实上我们定义创建的A是一个二级指针变量,它有独立的内存空间(有内存空间意味着A的值可以改变,你甚至可以让A指向其它指针变量),其中存储的就是指针数组A[]的起始地址,也即&A[0],而指针数组中的每个成员都是一级指针变量,它们同样占有内存,存储数组的起始地址,即&A[i][0]。正是因为如此,我们可以通过A的值来找到数组某行的地址,再找到某列的地址并取其值,这跟二维数组的使用基本一致。同时我们看到,由于采用了动态分配内存,各数组内的地址连续,而数组之间的地址不连续。需要说明的是,指针数组A[]之所以地址之间相差8,是因为int型指针变量在64位系统下占8个字节,这是跟寻址范围有关的。(8字节对应64位,刚好可以表示64位系统的寻址范围)接下来是验证代码:

int n = 3, m = 3;
int** A = new int* [n];
for (int i = 0; i < m; i++)
{
	A[i] = new int[m];
}
printf("指针所占字节大小:%x\n", sizeof(A[0]));
printf("二级指针A的地址:%x\n", &A);
printf("二级指针A的值:%x\n", A);
printf("\n");
for (int i = 0; i < n; i++)
{
	printf("A[%d]的地址:%x   ", i, &A[i]);
	printf("A[%d]的值:%x\n", i, A[i]);
}
printf("\n");
for (int i = 0; i < n; i++)
{
	for (int j = 0; j < m; j++)
	{
		printf("A[%d][%d]的地址:%x   ", i, j, &A[i][j]);
	}
	printf("\n");
}

 在最后我们要看的是另一种指针数组的创建:

int a = 1, b = 2, c = 3;
int* A[3];
A[0] = &a;
A[1] = &b;
A[2] = &c;

        这跟C++中动态创建指针数组并没什么太大的区别,不一样的是这里的A标识符跟二维数组的是一样的性质,它是个地址常量,没有占用内存,不可以修改。验证代码:

int a = 1, b = 2, c = 3;
int* A[3];
A[0] = &a;
A[1] = &b;
A[2] = &c;
printf("A的地址:%x\n", &A);
printf("A的值:%x\n", A);
for (int i = 0; i < 3; i++)
{
	printf("A[%d]的地址:%x  ", i, &A[i]);
	printf("A[%d]的值:%x  ", i, A[i]);
	printf("*A[%d]的值:%x\n", i, *A[i]);
}

(注:这里更正一个小错误,我的代码运行环境为x64(64位处理器),按理来说地址为64位也就是8字节,为什么输出的地址为32位?这是因为我输出变量地址时用的是printf("%x\n",&a)的形式,事实上变量的地址会被强转为int型,也就是说打印出的地址会被截断为4字节,这是错误的形式,正确的做法是printf("%p\n",&a)这里的%p就是以地址形式输出,这种形式不会忽略数字0,而如果用%x或%llx会忽略)

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值