目录
数组
一维整型数组
第一题 - sizeof
题目:int a[] = {1,2,3,4};
(1)printf("%d\n",sizeof(a));
(2)printf("%d\n",sizeof(a+0));
(3)printf("%d\n",sizeof(*a));
(4)printf("%d\n",sizeof(a+1));
(5)printf("%d\n",sizeof(a[1]));
(6)printf("%d\n",sizeof(&a));
(7)printf("%d\n",sizeof(*&a));
(8)printf("%d\n",sizeof(&a+1));
(9)printf("%d\n",sizeof(&a[0]));
(10)printf("%d\n",sizeof(&a[0]+1));
请写出输出结果:
答案:
(1) 16 (2) 4 (3) 4 (4) 4 (5) 4 (6) 4 (7) 16 (8) 4 (9) 4 (10) 4
注:此为32位平台下运行的结果
解析:
sizeof是通过表达式最终呈现的类型算出大小,并不会计算sizeof内部的表达式。
题目:int a[] = {1,2,3,4};
(1)sizeof(a);
数组名在绝大多数情况下都是表示数组首元素地址,但有两个特例:
一是 &arr,二是sizeof里单独放个数组名。
这两种表示的是整个数组的地址,即数据类型为数组。
此题就是第二个特例,sizeof计算的是整个数组大小,所以结果便是16个字节(元素个数x元素类型大小)
(2)sizeof(a+0);
此时没有把a单独放在sizeof括号里面,而是把(a+0)放在了里面
所以虽然a会表示整个数组的地址,但a+0这个表达式却会将a的类型
从数组退化成指针,因此sizeof实际上是计算指针的大小
因此,按平台的不同,计算结果会是4或者8。
除了3种情况外(sizeof, &, 用做数组初始化的字符串数组), 数组都会退化成"指向数组首元素的指针"。
-------------------数组和指针——都是“退化”惹的祸
题目:int a[] = {1,2,3,4};
(3)sizeof(*a);
a没有单独放在sizeof括号里,所以类型退化为指针。
此时a指向数组首元素,后对a进行解引用操作,a里面存储的是首元素
地址,因此从a解出来的是首元素的值,值是int类型,所以sizeof返回的
结果为4。
解引用操作的汇编代码实现
用栗子说明:
*pc所对应的汇编代码:
mov eax, dword ptr [ebp-18h]
ptr是数据类型限定符,告诉CPU要操作几个字节
dword是要操作的字节单位,dword是双字,代表四个字节
ebp是栈底指针,保存着一个地址,18h是十六进制数,ebp-18h会得到一个整型值
用[]把ebp-18h括起来,整个[ebp-18h]就表示要访问ebp-18h地址处的内存单元。
ebp-18h就是pc的地址
eax是一个通用寄存器,通常用来存储函数返回值、函数参数传递以及一些临时变量
mov是用于数据传输的,整行代码表示的意思就是:
从ebp-18h地址处开始读取四个字节的数据,然后把数据存入eax中
照着这图看,就是从0x0135FA4C开始读,把58 fa 35 01 四个字节的数据存进eax
这四个字节就是c的地址,然后mov dword ptr [eax],0Ch
把12从eax地址处向前覆盖四个字节进行存储
所以解引用操作就是通过mov指令将指针变量pc(即ebp-18h,在底层中计算机不认什么abcd,只认地址)中存储的值存到寄存器,可以看做是将pc替换成了c,类型从int*类型退到它所指向的int型。
解引用操作是在运行期进行的,运行时CPU根据解引用操作所对应的机器指令解析指针变量所指向的地址,并从该地址中获取数据。
数据可以是任何类型,指针、结构体都行。
题目:int a[] = {1,2,3,4};
(4)sizeof(a+1);
与第2题同理,sizeof计算的是(a+1)这个表达式所占内存空间的大小,
或者说计算这个表达式的类型,此时类型是指针,
所以结果是4或8。
(5)sizeof(a[1]);
a[1]表示是数组a的第2个元素,为int类型
所以结果为4。
[ ]:下标引用操作符,C/C++参考文档描述如下:
(6)sizeof(&a);
对a取地址,取出的是整个数组的地址,
(&a)这个表达式的类型是int(*)[4],一个数组指针,
所以sizeof计算的是指针类型的大小,
不管你这指针指着什么变量,用来存储地址的位数也只能为32或64位,
所以结果是4或8个字节。
C/C++参考文档描述如下:
所以&会把操作数从其他类型转成指针类型
题目:int a[] = {1,2,3,4};
(7)sizeof(*&a);
a单独放在sizeof里,所以a此时的类型为int(*)[4],此时计算则结果为16。
先对a取地址,此时(&a)为指针类型,此时计算则结果为4或8,
然后对(&a)进行解引用操作,取出(&a)这个指针所保存的值,
保存的值是首元素地址,但其类型是int(*)[4],所以相当于*&两个操作相互抵消了,
可以认为原表达式实际与sizeof(a)所表达的意思一样,
所以还是计算整个数组的大小,因此结果为16个字节。
C/C++参考文档描述如下:
(8)sizeof(&a+1);
(&a)是一个指针,类型是int(*)[4],
对指针+1,则会给指针的地址值加上1*sizeof(&a),整个表达式(&a+1)
会从(&a)处地址跳过一个int(*)[4]类型长度的距离,即给地址值加了
16字节,指向了(&a)地址处往后16个字节的地址上的元素。(小端存储是这样的)
但这些步骤sizeof都不关心,它只在乎表达式的类型,而指针+1还是指针
所以结果为4或8。
汇编下的情况:
//待补充...
(9)sizeof(&a[0]);
a[0]是一个int类型的值,&a[0]则由第六题分析可知
这是创造了一个指针,所以表达式(&a[0])的类型为int*,
sizeof的计算结果便为4或8。
(10)sizeof(&a[0]+1);
(&a[0])这个表达式的类型为int*,
指针+1后依然是指针,所以sizeof的计算结果为4或8。
一维字符数组
第一题 - sizeof
char arr[] = {'a','b','c','d','e','f'};
(1)printf("%d\n", sizeof(arr));
(2)printf("%d\n", sizeof(arr+0));
(3)printf("%d\n", sizeof(*arr));
(4)printf("%d\n", sizeof(arr[1]));
(5)printf("%d\n", sizeof(&arr));
(6)printf("%d\n", sizeof(&arr+1));
(7)printf("%d\n", sizeof(&arr[0]+1));
请写出输出结果:
答案:
(1) 6 (2) 4 (3) 1 (4) 1 (5) 4 (6) 4 (7) 4
注:此为32位平台下运行的结果
解析:
char arr[] = {'a','b','c','d','e','f'};
(1)sizeof(arr);
数组名单独放在sizeof()里,
所以arr表示的是整个数组的地址,
sizeof的结果为6。
(2)sizeof(arr+0);
数组名没有单独放在sizeof()里,
所以(arr+0)使arr从 char[6] 类型退化成了char*类型。
不管arr+0还是+几个亿,(arr+整数)这个表达式的类型
始终是指针类型,所以sizeof计算结果为4或8。
(3)sizeof(*arr);
数组名没有单独放在sizeof()里,
所以(*arr)使arr退化成了char*类型,于是arr此时代表数组首元素地址,
对数组首元素地址解引用,得到数组首元素,因此(*arr)的类型
为char,所以sizeof的计算结果为1。
(4)sizeof(arr[1]);
显而易见,arr[1]代表arr数组下标为1的元素
所以sizeof的计算结果为1
(5)sizeof(&arr);
数组名没有单独放在sizeof()里,
但arr却与&结合,所以arr此时的类型为char[6]而不是char*,
结合后(&arr)这个表达式的类型是char(*)[6],一个指向数组的指针,
所以sizeof计算的是指针大小,因此结果为4或8。
(6)sizeof(&arr+1);
(&arr)的类型为char(*)[6],一个指针。
对指针+1,(&arr+1)的类型依然是指针,所以
sizeof的计算结果为4或8。
(7)sizeof(&arr[0]+1);
[]的优先级大于&,所以arr先与[0]结合,arr[0]是数组首元素
对数组首元素取地址,因此表达式(&arr[0])的类型为char*
对指针+1,指针类型不变,所以sizeof的计算结果为4或8。
第二题 - strlen
题目:char arr[] = {'a','b','c','d','e','f'};
(1)printf("%d\n", strlen(arr));
(2)printf("%d\n", strlen(arr+0));
(3)printf("%d\n", strlen(*arr));
(4)printf("%d\n", strlen(arr[1]));
(5)printf("%d\n", strlen(&arr));
(6)printf("%d\n", strlen(&arr+1));
(7)printf("%d\n", strlen(&arr[0]+1));
请写出输出结果:
答案:
(1) 随机值 (2)随机值 (3)没有结果 (4) 没有结果 (5)随机值 (6)随机值 (7) 随机值
注:此为32位平台下运行的结果
解析:
strlen()的参数类型是 const char* ,因为有const,所以strlen函数接受的参数必须是一个常变量,是不可改变的。strlen用于计算字符串长度,开始统计的起始位置从str所指的地址开始向后直到遇见\0才停止。
题目:char arr[] = {'a','b','c','d','e','f'};
(1)strlen(arr);
strlen接收一个char*类型的指针,
所以()里的表达式会被当成地址来处理,
arr不是三种特殊情况之一,所以此时表示首元素地址,
strlen将从arr首元素地址开始向后统计字符个数,直到遇到\0为止,
但arr数组里没有放\0,所以将一直统计到数组之外,直到统计了未知个字符
才能遇到\0,所以返回值为未知值,即随机值。
(2)strlen(arr+0);
(arr+0)这个表达式将指向(首元素地址值+0*sizeof(arr[0]))地址处上的元素,
其地址就是arr此时所表示的地址,其结果同上题如出一辙,同为随机值。
(3)strlen(*arr);
arr此时表示数组首元素地址,因此解引用后,
(*arr)表示字符'a',被strlen当成地址来处理,
因此strlen将从地址值为'a',即0x00000061处开始
向后统计字符个数,直到遇到\0为止。但这个地址是属于windows
的空指针赋值分区,不能被其他进程访问,一旦试图访问就会引发异常,
因此什么都没输出,因为引发了异常,使程序崩了。
空指针赋值分区
这一分区是进程的地址空间中从0x00000000 到 0x0000FFFF 的闭区间(64K 的内存大小 ),这 64K 的内存是一块保留内存,不能被程序动态内存分配器分配,不能访问,也不能使用,保留该分区的目的是为了帮助程序员捕获对空指针的赋值。如果进程中的线程试图读取或者写入位于这一分区内的内存地址,就会引发访问违规。
---------------【C++进阶】C++中的空指针和野指针(1)空指针赋值分区
①为帮助程序员捕获对空指针的赋值,当线程试图读取或写入这一分区的内存地址,就会引发访问违规。
--------------windows系统内存结构概述u(重要概念释疑)
Ps:就算此题访问的地址是其他的内存空间也会引发异常,因为进程不能访问不属于它的内存空间。操作系统会为每个进程分配一个独立的虚拟地址空间,进程只能访问自己的虚拟地址空间。
虚拟地址空间由多个不同的区域组成,例如代码区、数据区、堆区和栈区等。进程只能在它自己的虚拟地址空间内进行读取和写入操作,无法访问其他进程的虚拟地址空间。
此外,操作系统还通过硬件机制实现了对进程内存的保护。例如,现代处理器通常支持内存保护机制,包括虚拟内存技术和访问控制列表等。这些机制可以确保进程只能访问它自己的内存空间,防止进程之间发生内存冲突和数据泄露等问题。
需要注意的是,虽然进程不能直接访问其他进程的内存空间,但是进程可以通过进程间通信机制(如管道、消息队列和共享内存等)来实现数据的共享和交换。在这种情况下,操作系统会提供相应的机制来确保数据的安全性和正确性。
题目:char arr[] = {'a','b','c','d','e','f'};
(4)strlen(arr[1]);
与上题类似,(arr[1])为arr下标为1的元素,即'b'
被strlen当成地址处理,从而引发了访问异常,
因此结果是什么都没输入。
(5)strlen(&arr);
(&arr)表示整个数组的地址,类型为char(*)[6]
但strlen不在乎这个,它会把()里的一切当成地址处理,
而(&arr)表示的是数组首元素地址,这个数组所在的内存
空间属于strlen函数所在的进程空间中,所以不发生访问异常,
但数组内无\0,所以结果是随机值。
(6)strlen(&arr+1);
(&arr)的类型是char(*)[6],所以+1将使
(&arr+1)这个表达式指向(首元素地址值+1*sizeof(&arr))地址处上的元素,
此地址即&arr后6字节的地址,结果依然为随机值。
(7)strlen(&arr[0]+1);
(&arr[0])为数组首元素地址,+1后
(&arr[0]+1)指向数组第二个元素,
但数组内无\0,结果仍是随机值。
第三题 - sizeof
题目:char arr[] = "abcdef";
(1)printf("%d\n", sizeof(arr));
(2)printf("%d\n", sizeof(arr+0));
(3)printf("%d\n", sizeof(*arr));
(4)printf("%d\n", sizeof(arr[1]));
(5)printf("%d\n", sizeof(&arr));
(6)printf("%d\n", sizeof(&arr+1));
(7)printf("%d\n", sizeof(&arr[0]+1));
请写出输出结果:
答案:
(1) 7 (2) 4 (3) 1 (4) 1 (5) 4 (6) 4 (7) 4
注:此为32位平台下运行的结果
解析:
题目:char arr[] = "abcdef";
(1)sizeof(arr);
数组名单独放在sizeof()里,表示整个数组的地址,
所以sizeof计算的是整个数组大小,
看似结果应为6,但字符串后面还隐藏了一个\0,
被一起初始化到了数组所占的内存空间中,因此结果为7。
(2)sizeof(arr+0);
数组名没有单独放在sizeof()里,
因此arr的类型从 char[7] 退化成char*,
对指针+1,指针类型依然不变,因此结果为4或8。
(3)sizeof(*arr);
数组名没有单独放在sizeof()里,
所以arr的类型从 char[7] 退化成char*,
对指针解引用,得到的值为char型,所以结果为1。
(4)sizeof(arr[1]);
显而易见,arr[1]为数组下标为1的元素,
类型是char,所以结果为1。
(5)sizeof(&arr);
arr与&相结合,(&arr)将表示整个数组的地址,
类型为 char(*)[7],一个指向数组的指针,
所以计算结果为4或8。
(6)sizeof(&arr+1);
(&arr)的类型为 char(*)[7],加1将使(&arr+1)
这个表达式指向(首元素地址值+1*sizeof(&arr))地址处上的元素,
此地址即(&arr)往后7个字节的地址,但(&arr+1)的类型还是个指针,
所以结果为4或8。
(7)sizeof(&arr[0]+1);
(&arr[0])为int*类型,指针+1后还是指针
所以结果为4或8。
第四题 - strlen
题目:char arr[] = "abcdef";
(1)printf("%d\n", strlen(arr));
(2)printf("%d\n", strlen(arr+0));
(3)printf("%d\n", strlen(*arr));
(4)printf("%d\n", strlen(arr[1]));
(5)printf("%d\n", strlen(&arr));
(6)printf("%d\n", strlen(&arr+1));
(7)printf("%d\n", strlen(&arr[0]+1));
请写出输出结果:
答案:
(1) 6 (2) 6 (3) 无结果 (4) 无结果 (5) 6 (6) 随机值 (7) 5
注:此为32位平台下运行的结果
解析:
题目:char arr[] = "abcdef";
(1)strlen(arr);
arr虽然单独放在strlen里,但strlen不是sizeof,
所以arr此时的类型是int*,表示数组首元素地址,
结果为6。
(2)strlen(arr+0);
strlen只管地址不管类型,(arr+0)存储
的地址仍是数组首元素地址,结果与上题一样是6。
(3)strlen(*arr);
对数组首元素地址解引用得到首元素'a',
被strlen解析成地址0x00000061,此地址
不处于strlen函数所在进程的内存空间中,属于越界访问,
因此程序奔溃,无结果。
(4)strlen(arr[1]);
同样的,与上题类似,都触发了越界访问异常,
因此无结果。
(5)strlen(&arr);
(&arr)代表整个数组的地址,指向的是
数组首元素地址,所以结果为6。
(6)strlen(&arr+1);
(&arr)虽然存储的是数组首元素地址,但
它代表的是整个数组的地址,即类型为char(*)[7],
因此+1会给地址值加上1*sizeof(&arr)个字节,使(&arr+1)这个
表达式指向了arr首元素地址后7个字节的地址处上的元素,因此结果
为随机值。
(7)strlen(&arr[0]+1);
(&arr[0]+1)这个表达式指向arr中第2个元素,
所以结果为5。
一级字符指针
第一题 - sizeof
题目:char *p = "abcdef";
(1)printf("%d\n", sizeof(p));
(2)printf("%d\n", sizeof(p+1));
(3)printf("%d\n", sizeof(*p));
(4)printf("%d\n", sizeof(p[0]));
(5)printf("%d\n", sizeof(&p));
(6)printf("%d\n", sizeof(&p+1));
(7)printf("%d\n", sizeof(&p[0]+1));
请写出输出结果:
答案:
(1) 4 (2) 4 (3) 1 (4) 1 (5) 4 (6) 4 (7) 4
注:此为32位平台下运行的结果
解析:
题目:char *p = "abcdef";
(1)sizeof(p);
p是一个字符指针,指向常量字符串"abcdef"的首字符地址上的元素,
类型是char*,因此sizeof的计算结果为4或8。
(2)sizeof(p+1);
(p+1)的类型为char*,因此sizeof的计算结果为4或8。
(3)sizeof(*p);
对p解引用后得到字符'a',类型为char,
因此sizeof的计算结果为1。
(4)sizeof(p[0]);
显而易见,sizeof的计算结果为1。
(5)sizeof(&p);
对一个字符指针取地址,得到p的地址,
因此(&p)是一个二级指针,类型为char**,
类型还是指针,所以sizeof的计算结果为4或8。
(6)sizeof(&p+1);
(&p)的类型为char*,是个指针,+1后还是指针,
所以sizeof计算结果为4或8。
(7)sizeof(&p[0]+1);
(&p[0])是一个指针,加1后还是指针,
所以sizeof的计算结果为4或8。
第二题 - strlen
题目:char *p = "abcdef";
(1)printf("%d\n", strlen(p));
(2)printf("%d\n", strlen(p+1));
(3)printf("%d\n", strlen(*p));
(4)printf("%d\n", strlen(p[0]));
(5)printf("%d\n", strlen(&p));
(6)printf("%d\n", strlen(&p+1));
(7)printf("%d\n", strlen(&p[0]+1));
请写出输出结果:
答案:
(1) 6 (2) 5 (3) 无结果 (4) 无结果 (5) 随机值 (6) 随机值 (7) 5
注:此为32位平台下运行的结果
解析:
题目:char *p = "abcdef";
(1)strlen(p);
p是一个字符指针,指向常量字符串"abcdef"的首字符地址上的元素,
所以计算结果为6。
(2)strlen(p+1);
p的类型是char*,+1使(p+1)指向p后一个字节的
地址上的元素,该地址中存储的值是'b',所以结果为5。
(3)strlen(*p);
对指针解引用,得到指针所指向变量的值,值为'a',
被strlen解析成地址后处理,触发越界访问异常,程序崩了,
因此无结果。
(4)strlen(p[0]);
字符串与数组在内存的分布类似,皆为连续存放,
所以可用下标引用操作符,所以(p[0])代表字符串首元素,
与上题情况一样,无结果。
(5)strlen(&p);
对p取地址,得到指针p的地址,(&p)的类型为char**,
给p分配的地址自然是属于进程的内存空间范围内,
所以没有越界访问异常,结果为随机值。
(6)strlen(&p+1);
(&p)的类型为char**,+1后跳过一个1*sizeof(&p)的长度,指向
(&p)后4个字节的地址处上的元素,结果为随机值。
(7)strlen(&p[0]+1);
(&p[0])表示字符串首字符的地址,+1则表示字符串
第二个元素的地址,结果为5。
二维整型数组
第一题 - sizeof
题目:int a[3][4] = {0};
(1)printf("%d\n",sizeof(a));
(2)printf("%d\n",sizeof(a[0][0]));
(3)printf("%d\n",sizeof(a[0]));
(4)printf("%d\n",sizeof(a[0]+1));
(5)printf("%d\n",sizeof(*(a[0]+1)));
(6)printf("%d\n",sizeof(a+1));
(7)printf("%d\n",sizeof(*(a+1)));
(8)printf("%d\n",sizeof(&a[0]+1));
(9)printf("%d\n",sizeof(*(&a[0]+1)));
(10)printf("%d\n",sizeof(*a));
(11)printf("%d\n",sizeof(a[3]));
请写出输出结果:
答案:
(1) 48 (2) 4 (3) 16 (4) 4 (5) 4 (6) 4 (7) 16 (8) 4 (9) 16 (10) 16 (11) 16
注:此为32位平台下运行的结果
解析:
题目:int a[3][4] = {0};
(1)sizeof(a);
数组名单独放在sizeof()里,表示整个数组的地址,
3x4=12个元素,12x4=48个字节,因此sizeof的计算
结果为48。
(2)sizeof(a[0][0]);
(a[0][0])表示数组第一行第一列的元素,其类型为int,
所以结果为4。
(3)sizeof(a[0]);
(a[0])表示a的首元素,二维数组存储的元素是一维数组,
首元素即是数组a中第一个一维数组,(a[0])代表整个一维数组的地址。
所以(a[0])的类型为int[4],是个指针,因此结果为16。
(4)sizeof(a[0]+1);
(a[0])表示a的第一个元素,是个数组名,表示整个一维数组的地址,类型
为int[4],+1则使a[0]类型从int[4]
退化成int*,是指针,所以结果为4或8。
(5)sizeof(*(a[0]+1));
a[0]的类型为int[4],+1后使类型退化成int*,此时(a[0]+1)表示a[1],
对a[1]解引用,a[1]是数组a的第二个一维数组的数组名,表示
整个一维数组的大小,指向的是一维数组首元素,所以解引用会得到a[1][0],
类型为int,所以结果为4。
(6)sizeof(a+1);
数组名没有单独放在sizeof()里,所以
类型从int[3][4]退化成int(*)[4],一个数组指针,
+1后类型还是int(*)[4],还是指针,所以结果为4或8。
(7)sizeof(*(a+1));
(a+1)的类型是int(*)[4],代表a[1],
对a[1]解引用后得到a的第2个一维数组,其类型为int[4],
所以结果为16。
(8)sizeof(&a[0]+1);
(a[0])的类型为int[4],(&a[0])的类型为int(*)[4],
+1后类型不变,所以结果为4或8。
(9)sizeof(*(&a[0]+1));
(&a[0])的类型为int(*)[4],+1后类型不变,(&a[0]+1)
代表(&a[1]),对(&a[1])解引用,得到a[1],类型为int[4],
所以结果为16。
(10)sizeof(*a);
a表示整个数组的地址,类型为a[3][4],指向的是第一个一维数组,
因此解引用后得到一个一维数组,(*a)的类型为int[4],所以结果为
16。
(11)sizeof(a[3]);
(a[3])应表示a的第4个一维数组,但a最多只有3个,因此(a[3])
表示一片未知空间,但其类型明确,是int[4],所以结果为16。
总结:
(1) 指针即地址
指针是一个变量,它存储地址。而地址存储变量的值,对地址解引用能得到变量的值,因此可以说地址指向变量,而指针又存储变量的地址,对指针解引用也能得到变量的值,所以指针等同于地址,地址指向变量,指针亦是指向变量。
(2)sizeof只看类型
sizeof只根据整个表达式最终呈现出来的类型计算所占字节数,而不会计算表达式里的值,而且,sizeof是在c程序编译期间就执行完成的。
变量、常量和表达式的类型是在编译阶段由编译器确定的。编译器会根据程序中变量的声明和赋值语句,推断出每个变量的数据类型。这个过程被称为类型推导。
C 语言的类型推导是指编译器在编译源代码时根据上下文环境自动推导表达式和变量的类型的过程。
C 语言中的类型推导主要包括两个方面:声明推导和表达式推导。
声明推导是指编译器在编译时根据变量的声明推导出变量的类型。例如,对于语句
int x;
,编译器会自动推导出x
的类型为整型。表达式推导是指编译器在编译时根据表达式的类型和上下文推导出表达式的类型。例如,对于表达式
x + y
,如果x
和y
的类型都是整型,编译器会自动推导出表达式的类型为整型。
因此在编写代码时,虽然写的是 sizeof(*(a[0]+1)) ,但到了编译期的预处理阶段,由于编译器自动的类型推导,使 (*(a[0]+1)) 直接被替换成了(int),因此在编译期时,sizeof(*(a[0]+1))被编译器表示成了sizeof(int),原来的什么加减乘除自增自减等等运算符统统消失,取而代之的是一个醒目的数据类型。紧接着sizeof计算该类型所占的字节数得出结果,该结果是一个size_t类型的常量表达式,也可以称之为编译时常量。最后整个sizeof()都被替换成了这个常量,因此到了运行期,在原先sizeof的位置上CPU只识别到了一个常量,其他的什么运算符都在编译期时就被替换掉了。
所以在sizeof内部的运算统统不会被执行,因为它们都还没
来得及放入寄存器,就已经"消失不见"了
int a = 1;
int b = sizeof(a++);
printf("%d", a);//输出结果为1
(3) 二维数组中的元素为一维数组
a[3][2]={ {1,2}, {3,4}, {5,6} };
可以看成:
a[3][2]={ a[0], a[1], a[2] };
二维数组a中的元素为一维数组,每个元素类型都是int[2]。并且都是以一维数组名的形式存储在二维数组中,对a解引用得到的值是a[0],这个a[0]就是a的第一行的一维数组名,并且类型为int[2],表示整个数组的地址,再对a[0]解引用才能得到a[0][0],即1。
指针经典笔试题
笔试题1:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
程序的结果是什么?
答案:2,5
注:此为32位平台下运行的结果
解析:
int *ptr = (int *)(&a + 1);
ptr为 int* 类型,是个整型指针,&a为 int(*)[5] 类型,是个数组指针,表示整个数组的地址,给&a加1将使(&a+1)这个表达式指向(&a的地址值+1*20)地址处,即&a[0]+5处。
此时(&a+1)的类型为 int(*)[5] ,(int *)则使其类型临时被强转换为 int* ,现在(int*)(&a+1) 这个表达式类型为 int* ,然后将这个int*类型的值赋给ptr。ptr此时存的是(&a[0]+5)这个地址。
printf( "%d,%d", *(a + 1), *(ptr - 1));
a为数组名,表示首元素地址,类型为 int* ,+1则使(a+1)这个表达式指向(&a[0]+sizeof(a[0]))地址处上的元素。即a[1],对(a+1)解引用,得到a[1],即2。
ptr类型为 int* ,值为(&a[0]+5),所以此时指向地址(&a[0]+5)上的元素,即a[5],减1后退sizeof(a[5])个字节长度,即(ptr-1)的值变成了(&a[0]+4),对(&a[0]+4)解引用得到a[4],即5。
所以输出结果为:2,5
笔试题2:
结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
如下表达式的值分别为多少?
已知,结构体Test类型的变量大小是20个字节
int main()
{
//假设 p 的值为0x100000。
p=(struct Text*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
答案:
00100014
00100001
00100004
注:此为32位模式下运行的结果 (不同模式下结构体的大小不同,因此64位模式下输出的结果有可能发生差异)
解析:
%p是用来打印地址的,也能打印数值,不过都会以十六进制的形式打印,而且会全部打完,即有多少位打印多少位。32位模式就打印32个二进制数字,64位模式下就打印64个二进制数字,若数值小占不到高位,那么高位就补0。
可以把%p当成十六进制格式的输出。
p的类型为struct Text*,一个指针,值是0x100000,此为十六进制表示,问如下表达式的值分别为多少。
printf("%p\n", p + 0x1);
0x1是十六进制表示的1,p+1实质是(p所保存的地址值+sizeof(p)),sizoef(p)计算出的结果为20,那么原式为(0x100000+0x14),0x14为十进制数20的十六进制表示形式,所以加起来的结果为0x100014。
printf("%p\n", (unsigned long)p + 0x1);
(unsigned long)使p的类型被临时强转换成无符号长整型,因为是整型,所以+1就是+1,不是像指针那样+1是表示偏移几个字节单位(字节单位可理解为指针类型)。所以结果为:0x100000+0x1=0x100001。
printf("%p\n", (unsigned int*)p + 0x1);
(unsigned int*)使p的类型被临时强转换成无符号整型指针,所以轻车熟路的,p+1是给p的值加了4个字节,即0x100000+0x4,结果为0x100004。
笔试题3:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
输出结果是什么?
答案:4,2000000
注:此为32位模式下运行的结果
解析:
int *ptr1 = (int *)(&a + 1);
&a的类型为int(*)[4],表示整个数组的大小,值为&a[0]。给&a+1则让&a的值加上1*sizeof(&arr)的大小,让(&a+1)这个表达式指向了&a[0]后16个字节地址处的元素。
把(&a+1)临时强转类型成int*后赋给ptr,此时ptr1指向之前(&a+1)指向的元素。
int *ptr2 = (int *)((int)a + 1);
a的类型为int*,表示数组首元素地址,强制类型转换操作符 ( (类型) ) 的优先级比加法运算符 (+) 高,所以a先被临时强转成int型后才+1,给原先的地址值加了一个字节。然后再把 ( ( int ) a+1) 这个表达式临时强转成int*类型后赋值给ptr2。
此时ptr2指向存放首元素的4个字节中的第2个字节。
printf( "%x,%x", ptr1[-1], *ptr2);
ptr1的类型为int*,ptr1[-1]相当于*(ptr1 - 1),所以ptr1的值减了4个字节后将指向a[3]的位置,解引用后得到a[3],所以输出结果为 4 。
ptr2的类型同为int*,对ptr2解引用,得到ptr2所指向的元素。
由于ptr2的类型为 int*,所以解引用将令编译器向后确定4个字节的数据并存入内存后才返回给程序,而要使 printf 打印数据,首先需要将数据放入寄存器,然后 printf 才能从寄存器中取出数据来打印。
在编译期时,由于是小端存储,00 00 00 02将被编译器当成小端存储的形式来操作,因此在 a[1] 为低字节的 02 被编译器识别成了此时正在操作的4字节数据的最高字节,随后将数据转成补码的形式放入内存,补码形式为:0010 0000 0000 0000 0000 0000 0000
然后到运行期,CPU执行读取内存数据的指令,先从内存中取出指定的数据(原码形式)放入寄存器,寄存器里的值再传给printf函数,然后按照十六进制格式(%x)打印在屏幕上,最后得到的输出结果为2000000。
笔试题4
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
输出结果为?
答案:1
注:此为32位模式下运行的结果
解析:睁大眼睛仔细看。
这是圆括号而不是大括号,所以是逗号表达式,表达式的结果为括号里最末尾的表达式结果,所以 原式为int a[3][2]={1, 3, 5 };
a[0]表示二维数组第一行,即表示第一个一维数组的数组名,类型为int[2],将a[0]赋值给指针p,p的类型为int*,指向a[0][0],即1,p[0]可表示成*(p+0),对p解引用得到a[0][0],所以输出结果为1。
笔试题5
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;
}
输出结果为?
答案:FFFFFFFC,-4
注:此为32位模式下运行的结果
解析:
int a[5][5];
int(*p)[4];
p = a;
a的类型为int[5][5],将a的值赋给p,值为a[0],值的类型为int[5],而p的类型为int(*)[4],因此p虽然得到a[0],但由于类型限制,使p只能表示 a[0] 中5个int型元素中的前4个。
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
p[4][2],p先跟[4]结合,p[4]可以表示成*(p+4),p的类型为int(*)[4],指向一个存放4个int型的数组,+1就会使(p+1)的值跳过4个int型,就是16个字节,而+4则跳过4*sizeof(p)个字节,64个字节,即16个int型,此时 p[4] 指向一维数组 a[3] 中5个int型元素的后4个。
接着p[4]再与[2]结合,整个p[4][2]可表示成 *(*(p+4)+2),(p+4)此时指向一个有4个int型元素的连续内存空间,即数组,类型为int[4]。
p的值为 &a[0] ,p的类型为int(*)[4],而(p+4)是 &( &a[3]+2个int型),类型也是int(*)[4],对(p+4)解引用,得到 ( &a[3]+2个int型) 这个值,类型为int[4],表示整个数组的大小。
+2则使其类型退化成int*,所以 ( *(p+4) )+2会让 *(p+4)跳过2个int型,而不是跳过8个int型,+2后 (*(p+4)+2)刚好等于&a[3][3],最后对&a[3][3]解引用得到a[3][3]。
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
指针相减的结果是一个整数值,这个整数值表示两个指针之间的偏移量(即指针之间相差的元素个数)
由图可知,&a[4][2]比&p[4][2]长4个int型,所以相减的结果为-4,而%p会将-4格式化成十六进制后打印,-4以十六进制表示是FFFF FFFC,所以输出结果为FFFF FFFC。
而%d是打印有符号整数,所以输出结果为 -4。
笔试题6
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;
}
输出结果为?
答案:10,5
注:此为32位模式下运行的结果
解析:
int *ptr1 = (int *)(&aa + 1);
aa是二维数组名,(&aa)表示整个二维数组的地址,其类型为 int (*) [2][5],+1则使(&aa+1)指向aa[0][0]的地址值+ (2*5*4=40) 后的地址上的元素,即指向aa[3][0]这个元素,即aa[1][4]的下一个元素,接着类型从int (*)[2][5]被强转成 int* 后被赋值给ptr1。
int *ptr2 = (int *)(*(aa + 1));
aa由于+1退化成指向数组首元素的指针,类型为 int (*)[5],+1跳过一个int(*)[5]的长度,此时(aa+1)表示&aa[1],而*(&aa+1)会得到aa[1],aa[1]是数组名,表示整个数组,类型为int[5],接着类型从int[5]被强转成 int* 后被赋值给ptr2。
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
ptr1的类型为int*,表示aa[3][0]这个元素的地址,减1后退一个sizeof(ptr1)的长度,(ptr1-1)表示aa[2][4]这个元素的地址,解引用得到aa[2][4],打印结果为 10。
ptr2的类型为int*,表示aa[1][0]这个元素的地址,减1后退一个sizeof(ptr2)的长度,(ptr2-1)表示aa[0][4]这个元素的地址,解引用得到aa[0][4],打印结果为 5。
笔试题7
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
输出结果为?
答案:at
注:此为32位模式下运行的结果
解析:
char *a[] = {"work","at","alibaba"};
[ ]的优先级比 * 高,所以a先跟[ ]结合,然后a[ ]再和 * 结合,所以a是指针数组,类型为char*[3]。
char**pa = a;
pa++;
pa是二级指针,a此时表示数组首元素,即&a[0],所以pa此时表示&a[0],注意,a[0]是一个char*类型的指针,&a[0]则是char**类型的二级指针。
pa++,即给&a[0]加了一个sizeof(pa)的字节大小,即加了4,使pa此时表示&a[4]。
printf("%s\n", *pa);
注意,格式化输出符号为%s,即从起始地址开始往后打印,直到遇上\0为止。pa此时表示&a[4],对&a[4]解引用得到a[4],而a[4]是一个指针,表示*(a[4])的地址,所以不会引发越界访问异常,a[4]指向"a",而且字符串"at"里隐藏了一个\0,所以打印结果为 at。
笔试题8
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;
}
输出结果为?
答案:
POINT
ER
ST
EW
注:此为32位模式下运行的结果
解析:
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
c为指针数组,数组中存储4个分别指向各个常量字符串首字符的指针,cp是二级指针数组。
大概个情况:
c标准没有规定数组有内存对齐的规则,图中数组c中元素之间的间隔是vs的编译器自己弄的。
char***cpp = cp;
cpp虽然是三级指针,但却是 cpp=cp,而不是 cpp=&cp,cp不是三种特殊情况之一,所以cp此时表示数组首元素地址,因此cpp此时保存的是&cp[0],而不是&cp。
printf("%s\n", **++cpp);
cpp是指针,此时代表&cp[0],自增后(++cpp)代表&cp[1],*(&cp[1]) 得到cp[1],而 cp[1] 又代表c+2,c是数组首元素地址,即 (c+2) 相当于 (&c[0]+2) ,等于&c[2],再对 &c[2] 解引用后得到c[2],c[2]此时指向"POINT",因此打印结果为 POINT。
注意,自增或自减运算符会修改变量本身的值,所以此时cpp已不再是原来的值了。
printf("%s\n", *--*++cpp+3);
cpp此时的值是&cp[1],自增后变为&cp[2],对其解引用得到cp[2],cp[2]是c+1,--(c+1)即c+1-1,得到c,c是数组首元素地址&c[0],解引用得到数组首元素c[0],c[0]是一个指向常量字符串的指针,其值是首字符地址,类型为char*,所以+3使 (c[0]+3)指向后3个字节地址处上的字符,此时指向"ER",所以打印结果为 ER。
注:切莫误以为c[3] == c[0]+3,在底层上,c[3]是 *(c+3),c是char**类型,+1是+4字节。而c[0]+3是 *(c+0)+3,即*c+3,*c是char*类型,+1是+1字节,两者的数据类型不同。
printf("%s\n", *cpp[-2]+3);
cpp此时的值为&cp[2],cpp[-2]相当于*(cpp-2),即*(&cp[2]-2),结果为cp[0],即c+3,(c+3)的值是&c[3],解引用得到c[3],类型为char*,+3会给c[3]代表的地址值+3个字节,所以 (c[3]+3)此时指向"ST",因此打印结果为 ST。
printf("%s\n", cpp[-1][-1]+1);
cpp此时的值为&cp[2],cpp[-1][-1]可表示为*(* (cpp-1)-1),* (* (cpp-1)-1) —> * (* (&cp[2]-1)-1) —>* (* (&cp[1])-1) —> * (cp[1]-1) —>*(c+2-1) —> *(c+1) —>*(&c[1]) —>c[1],最后c[1]+1,(c[1]+1)指向"EW",所以打印结果为 EW。
总结:
(1)下标引用的底层原理
下标引用操作符[]
的底层原理其实就是指针运算。对于一个数组a,使用a[i]
来访问第i个元素时,编译器会将其转化为 *(a + i)
的形式,即将数组名a转换为指向a[0]的指针,然后再对指针进行偏移,最终取得对应元素的值。
这是因为,在内存中,数组元素在一段连续的内存空间中存储,数组名本质上是一个指向数组第一个元素的指针。因此,使用a[i]
来访问数组元素时,可以看作是对指向a[0]的指针进行偏移,使其指向a[i],然后再使用间接寻址符*
来获取该元素的值。