1 一维数组
例:
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
return 0;
}
输出结果(x86平台):
首先我们来复习一下对数组名的理解。数组名是数组首元素的地址,但是有两个例外:
sizeof(数组名)
时,数组名表示整个数组,计算是整个数组的大小,单位是字节。&数组名
时,数组名表示整个数组,取出的是数组的地址。
下面我们逐个 printf 语句来进行分析:
- 由于 sizeof(数组名)计算的是整个数组的大小,而1个 int 类型是4个字节,所以4个就是16字节。
- 由于
a+0
不是数组名,所以 a 表示的就是数组首元素的地址,所以sizeof(a+0)
计算的是首元素地址大小,是地址大小就是4或者8个字节。 - 由于
*a
不是数组名,所以 a 表示的就是数组首元素的地址,而*a
其实就是第一个元素,也就是a[0]
,所以大小为4个字节。 - 此处的情况和
2
一样,sizeof(a+1)
计算的是数组第二个元素地址大小,是地址大小就是4或者8个字节。 - 显然
sizeof(a[1])
计算的是数组第二个元素的大小,所以大小为4个字节。 - 由于
&数组名
时,数组名表示整个数组,取出的是数组的地址,但是数组的地址也是地址,是地址大小就是4或者8个字节。 - 虽然
*&a
不是数组名,但是&a
取出的是数组的地址。对数组的地址进行解引用操作那么实际上*
和&
就可以抵消掉,也就是说sizeof(*&a)
实际上等价于sizeof(a)
。除此之外,我们还可以从另外一个角度进行分析,&a
的类型实际上是数组指针,而对这个数组指针进行解引用实际上访问的就是整个数组,那么整个数组的大小自然而然就是16字节。 - 从
7
的分析不难知道,&a + 1
表示的是跳过整个数组 a 后的一个地址,是地址大小就是4或者8个字节。 - 显然,
&a[0]
就是首元素的地址,是地址大小就是4或者8个字节。 - 同理,
&a[0] + 1
是第二个元素的地址,其实就是a[1]
的地址,是地址大小就是4或者8个字节。
2 字符数组
例:
#include <stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
- 由于 sizeof(数组名)计算的是整个数组的大小,而1个 char 类型是1个字节,所以6个就是6字节。
- 由于
arr+0
不是数组名,所以arr
表示的就是数组首元素的地址,所以sizeof(arr+0)
计算的是首元素地址大小,是地址大小就是4或者8个字节。 - 由于
*arr
不是数组名,所以arr
表示的就是数组首元素的地址,而*arr
其实就是第一个元素,也就是arr[0]
,所以大小为1个字节。 - 显然,
arr[1]
表示的是第二个元素,大小为1个字节。 &arr
取出的是数组的地址,是地址大小就是4或者8个字节。&arr + 1
表示的是跳过整个数组arr
后的一个地址,是地址大小就是4或者8个字节。- 显然,
&arr[0] + 1
是第二个元素的地址,其实就是arr[1]
的地址,是地址大小就是4或者8个字节。
例:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
- 由于 strlen 函数计算的是
\0
之前字符串中字符的个数,而数组中没有明确给出\0
,所以计算出来的结果应该是个随机值。 arr + 0
表示的是首元素的地址,而arr
的地址正是从首元素的地址开始,也就是说strlen(arr)
和strlen(arr + 0)
中传给strlen
的地址其实是一模一样的,那么它的输出结果应该也是随机值且和上面的结果一致。*arr
表示对首元素的地址进行解引用,而strlen
接收的参数是地址,也就是说在这里相当于把首元素也就是a
的ASCII码值作为地址传给strlen
,而这个地址是否有被分配其实是不可知的,所以会造成非法访问,无法输出结果。- 同
3
,arr[1]
表示的是第二个元素,把元素的ASCII码值作为地址传给strlen
会造成非法访问。 &arr
表示的是数组的地址,但是这个地址也是指向数组的起始位置,也就是说这里的参数实际上和1
和2
是一样的,那么输出的结果应该也是一样的随机值。&arr + 1
表示跳过整个数组后的地址,从这里开始找\0
的话,由于arr
后的地址存放的内容是随机的,那么输出结果应该也是随机值,且和1
、2
、5
的随机值不同。&arr[0] + 1
表示的是第二个元素的地址,从第二个元素开始找\0
的话,那么和1
、2
、5
相比,7
统计到的个数就应该少一个,且是随机值。
由于上面3
和4
两条语句造成非法访问后程序就已经停止运行了,所以程序运行时只会看到1
和2
输出的结果,我们把3
和4
注释掉之后再来运行就可以看到下面的结果了:
例:
#include <stdio.h>
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
sizeof(数组名)
计算的是整个数组的大小。由于用字符串初始化字符数组时,字符串最后会隐藏一个\0
,而sizeof
并不关心数组中存放了什么,只关心存放了几个元素,所以把\0
算上就是7个元素。arr + 0
表示的是首元素的地址,是地址大小就是4或者8个字节。*arr
表示的是首元素,一个char
类型是1个字节。- 同
3
,sizeof(arr[1])
计算的是数组中第一个元素的大小,所以结果为1. &arr
表示数组的地址,是地址大小就是4或者8个字节。&arr + 1
表示跳过整个数组后的地址,是地址大小就是4或者8个字节。&arr[0] + 1)
表示的是第二个元素的地址,是地址大小就是4或者8个字节。
例:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
- 由于用字符串初始化字符数组时,字符串最后会隐藏一个
\0
,而strlen
函数计算的就是\0
之前字符串中字符的个数,所以结果为6。 arr + 0
表示的是首元素的地址,而arr
的地址正是从首元素的地址开始,也就是说strlen(arr)
和strlen(arr + 0)
中传给strlen
的地址其实是一模一样的,那么它的输出结果应该也是6。*arr
表示对首元素的地址进行解引用,而strlen
接收的参数是地址,也就是说在这里相当于把首元素也就是a
的ASCII码值作为地址传给strlen
,而这个地址是否有被分配其实是不可知的,所以会造成非法访问,无法输出结果。- 同
3
,arr[1]
表示的是第二个元素,把元素的ASCII码值作为地址传给strlen
会造成非法访问。 &arr
表示的是数组的地址,但是这个地址也是指向数组的起始位置,也就是说这里的参数实际上和1
和2
是一样的,那么输出的结果应该也是6。&arr + 1
表示跳过整个数组后的地址,从这里开始找\0
的话,由于arr
后的地址存放的内容是随机的,那么输出结果应该是随机值。&arr[0] + 1
表示的是第二个元素的地址,从第二个元素开始找\0
的话,那么和1
、2
、5
相比,7
统计到的个数就应该少一个,即为5。
由于上面3
和4
两条语句造成非法访问后程序就已经停止运行了,所以程序运行时只会看到1
和2
输出的结果,我们把3
和4
注释掉之后再来运行就可以看到下面的结果了:
例:
#include <stdio.h>
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p + 1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p + 1));
printf("%d\n", sizeof(&p[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
- 由于
p
是一个指针变量,而指针变量的大小为4或者8个字节,所以结果应该为4或者8. - 同理,
p+1
表示的是b
的地址,是地址大小就为4或者8个字节。 *p
表示的是对首元素的地址进行解引用,也就是a
,所以大小为1个字节。- 实际上,
p[0]
从计算的角度会被转换成*(p + 0)
,所以p[0]
表示的还是首元素,所以大小为1个字节。 &p
表示的是指针变量p
的地址,是地址大小就为4或者8个字节。&p + 1
表示的是跳过指针变量p
后的地址,是地址大小就为4或者8个字节。&p[0] + 1
表示的是b
的地址,是地址大小就为4或者8个字节。
例:
#include <stdio.h>
#include <string.h>
int main()
{
char* p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p + 1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p + 1));
printf("%d\n", strlen(&p[0] + 1));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
- 由于用字符串初始化字符数组时,字符串最后会隐藏一个
\0
,且p
中存放的是a
的地址,所以strlen
函数就是从a
的地址开始找\0
,所以结果为6。 p + 1
表示的是b
的地址,所以strlen
函数是从b
的地址开始找\0
,所以结果为5。*p
表示的是a
,而strlen
接收的参数是地址,也就是说在这里相当于把首元素也就是a
的ASCII码值作为地址传给strlen
,而这个地址是否有被分配其实是不可知的,所以会造成非法访问,无法输出结果。- 同
3
,p[0]
表示的还是a
,把元素的ASCII码值作为地址传给strlen
会造成非法访问。 &p
表示的是p
的地址,而\0
在p
所占空间中的具体位置是不可知的,所以结果应该是个随机值。- 同
5
,&p + 1
表示的是跳过指针变量p
后的地址,而\0
在其所占空间中的具体位置是不可知的,所以结果也是个随机值。 &p[0] + 1
表示的是第二个元素的地址,从第二个元素开始找\0
的话,那么和1
相比,7
统计到的个数就应该少一个,即为5。
由于上面3
和4
两条语句造成非法访问后程序就已经停止运行了,所以程序运行时只会看到1
和2
输出的结果,我们把3
和4
注释掉之后再来运行就可以看到下面的结果了:
3 二维数组
例:
#include <stdio.h>
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a[0][0]));
printf("%d\n", sizeof(a[0]));
printf("%d\n", sizeof(a[0] + 1));
printf("%d\n", sizeof(*(a[0] + 1)));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(*(a + 1)));
printf("%d\n", sizeof(&a[0] + 1));
printf("%d\n", sizeof(*(&a[0] + 1)));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a[3]));
return 0;
}
输出结果:
下面我们逐个 printf 语句来进行分析:
sizeof(a)
计算的是整个二维数组的大小,这个二维数组的大小为三行四列,也就是说这个数组中有12个int
类型的元素,那么大小应该是48个字节。a[0][0]
表示的第一行第一个元素,大小为4个字节。a[0]
表示的是第一行的数组名,数组名单独放在sizeof
内部,计算的就是第一行的大小,也就是16个字节。a[0] + 1
中a[0]
是第一行的数组名,但是数组名并没有单独放在sizeof
内部,所以这里的a[0]
表示的是第一行第一个元素的地址,那么a[0] + 1
表示的是第一行第二个元素的地址,是地址大小就是4或者8个字节。- 由
4
可知,a[0] + 1
表示的是第一行第二个元素的地址,那么*(a[0] + 1)
就是第一行第二个元素,大小为4个字节。 a + 1
中a
没有单独放在sizeof
内部,也没有&
,那么a
就是首元素的地址,也就是第一行的地址,所以a + 1
就是第二行的地址,是地址大小就是4或者8个字节。- 由于
a + 1
是一个数组指针,指向的是第二行的地址,对数组指针进行解引用,访问的是一个数组的大小,所以*(a + 1)
的大小就是16个字节。 &a[0] + 1)
中a[0]
是第一行的数组名,那么&a[0]
取出的就是第一行的地址,那么&a[0] + 1)
就是第二行的地址,是地址大小就是4或者8个字节。- 由于
&a[0] + 1)
指向第二行的地址,对地址进行解引用,访问的是一个数组的大小,所以*(&a[0] + 1)
的大小就是16个字节。 - 由于数组名
a
就是数组首元素的地址,也就是第一行的地址,所以*a
就是第一行的元素,大小为16个字节。 - 虽然
a[3]
这一行实际上并不存在,但是由于sizeof
不会真实计算表达式里面的值,而是根据类型进行推断,所以sizeof
根据推断得到a[3]
和a[0]
、a[1]
、a[2]
为同一个类型,所以结果为16个字节。
4 指针
例:
#include <stdio.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
输出结果:
a
表示首元素地址,那么a + 1
表示的就是第二个元素的地址,对地址进行解引用,所以第一个输出的值为2.
&a
表示的是取出整个数组的地址,所以&a + 1
表示的就是跳过数组a
后一个位置的地址,那么int* ptr = (int*)(&a + 1)
表示的就是把跳过数组a
后一个元素的地址强制转化为int*
类型后再赋给指针变量ptr
,那么*(ptr - 1)
实际上就是数组最后一个元素,所以第二个输出的值为5。
例:
#include <stdio.h>
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结构是啥?
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
输出结果:
题目中定义了一个类型为struct Test
的结构体指针变量p
,并把0x100000
作为地址赋给了p
。由于题目已经告诉我们结构体的大小是20个字节,那么p + 1
加的就应该是20,而%p
表示的是以十六进制的形式打印地址,所以结果为0010014
。
而(unsigned long)p + 0x1
中,由于p
被强制转换为了unsigned long
也就是无符号整型,所以此时p
不再是指针变量,那么p + 1
表示的就是整型加1,所以结果为00100001
。
类似地,(unsigned int*)p + 0x1
中,p
被强制转换为(unsigned int*)p
类型的指针,那么p + 1
加的就应该是4,所以结果为00100004
。
例:
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
输出结果:
由于逗号表达式是从左到右依次计算,返回的是括号里面最右边的值,所以实际上初始进数组的值只有1
、3
、5
,剩下的空间被初始化为0
,p = a[0]
中的a[0]
是第一行的数组名,而数组名又是首元素的地址,所以就相当于把第一个元素也就是1
的地址赋给p
,也就是说p = a[0]
等价于p = &a[0][0]
,而p[0]
又等价于*(p + 0)
,所以输出的值自然就是1
。
例:
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
输出结果:
不难发现,&p[4][2] - &a[4][2]
本质就是指针的加减,而指针 - 指针值的绝对值是指针之间元素的个数。我们不难找到a[4][2]
的位置,p
的首元素地址是a
,而p
指向的是一个4个整型元素的数组,也就是说它每加一次只会跳过4个整型元素,那么p[4][2]
的位置就应该如下图所示:
从图中可以看出,a[4][2]
的地址比p[4][2]
要大,那么&p[4][2] - &a[4][2]
的值就应该是个负数,而两个指针之间元素的个数为4,所以结果应为-4
,但是%p
表示的是按十六进制的形式打印地址,又由于-4
在内存中是以补码的形式存储,而地址其实是一个无符号数,所以这里就相当于把-4
的补码按十六进制的形式打印出来,10000000000000000000000000000100
为-4
的原码,11111111111111111111111111111100
为-4
的补码,那么把二进制转换成十六进制结果就为FFFFFFFC
.
例:
#include <stdio.h>
int main()
{
int aa[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));
return 0;
}
输出结果:
&aa
取出的是整个数组的地址,那么&aa + 1
就应该取的是跳过整个数组后的地址。将&aa + 1
这个地址赋给int *
类型的ptr1
后,那么ptr1 - 1
实际上就是回退一个整型,也就是10
的地址,解引用自然就是10
。
aa
是数组名,在二维数组中就是第一行的地址,那么aa + 1
就是跳过一行也就是第二行的地址,对第二行的地址进行解引用实际上就相当于拿到了第二行的数组名,也就是说*(aa + 1)
实际上就等价于aa[1]
, 而aa[1]
在没有sizeof
和&
修饰的情况下表示的是首元素的地址,把aa[1]
赋给ptr2
,那么ptr2
代表的就是6
的地址。ptr2 - 1
表示回退一个整型,指向的就是5
的地址,解引用自然就是5
。
注意:int *ptr1 = (int *)(&aa + 1)
由于两边的类型是一样的,所以这里进行强制类型转换是没有意义的,只是起到干扰的作用,int *ptr2 = (int *)(*(aa + 1))
同理。
例:
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
输出结果:
字符串作为元素存储在char *
类型的指针数组中时,存放的实际上是首字符的地址,所以数组a
中的内存布局实际上如下图所示:
又由于a
指向的是首元素的地址,而a
数组元素的类型为char*
,那么存放char*
类型元素的地址就应该用char**
类型,所以char**pa = a
就是把a
中首元素的地址赋给pa
。那么pa++
指向的位置就应该如下图所示:
那么解引用之后输出的就应该是at
。
注意:char *a[] = {"work","at","alibaba"}
这种写法并不是把字符串存放到数组中去,而是把字符串首字符的地址存放进去,也就是字符指针数组。
例:
#include <stdio.h>
int main()
{
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);
return 0;
}
输出结果:
首先我们来看数组c
的内存布局:
而数组cp
中存放的c + 3,c + 2,c + 1,c
,就应该存在下面一个关系:
此时,cp
中存放都是char**
类型的地址,那么char*** cpp = cp
表示的就是把cp
首元素的地址赋给cpp
,如下图所示:
搞清楚了内存布局,这个时候我们再来理解**++cpp
。
首先我们来看第一个输出。cpp
本来指向的是cp
首元素的地址,++cpp
操作后改为指向cp
第二个元素的地址,对cpp
进行两次解引用操作后访问到的就是POINT
.
注意:这个时候,cpp
指向的就已经不是cp
首元素的地址了,即后续对cpp
进行的加减操作都是基于上一次运算的结果进行的。
第二个输出中,我们首先要理清表达式的运算顺序。*-- * ++cpp + 3
中,先进行++cpp
操作,解引用一次后得到c+1
,然后进行--
操作,得到的结果是c
,也就是说cp
的第三个元素本来指向的是c
中的第二个元素,经过--
后改为指向了c
中的第一个元素。--
之后再解引用,拿到的就是ENTER
中首字母的地址,这个时候再+3
,拿到的就是第四个字母的地址,这个时候再按照%s
的形式打印,输出的结果就是从第四个字母开始的字符串,也就是ER
。
第三个输出。*cpp[-2] + 3
中,我们要清楚cpp[-2]
实际上等价于*(cpp-2)
,也就是说*cpp[-2] + 3
和**(cpp-2) + 3
是等价的。
那么这条表达式先执行的是cpp-2
,让cpp
指向c+3
的地址,然后进行两次解引用操作,拿到FIRST
中首字母的地址,这个时候再+3
,拿到的就是第四个字母的地址,这个时候再按照%s
的形式打印,输出的结果就是从第四个字母开始的字符串,也就是ST
。
注意:由于cpp-2
没有让cpp
本身的值发生改变,所以此时的cpp
实际上指向的还是cp
中第三个元素的位置。
第四个输出。cpp[-1][-1] + 1
实际上等价于*(*(cpp-1)-1)+1
,那么这条表达式先执行的是cpp-1
,让cpp
指向c+2
的地址,解引用后-1
,将c+2
改成c+1
,然后再进行一次解引用,拿到NEW
首字母的地址,这个时候再+1
,拿到第二个字母的地址,这个时候再按照%s
的形式打印,输出的结果就是从第四个字母开始的字符串,也就是EW
。