目录
1、指针和数组笔试题解析
数组名的理解 :
数组名是数组首元素的地址,
但是有2个例外:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址 。
在这之前我们复习一下字符串函数strlen和操作符sizeof怎么使用:sizeof与strlen辨析
一维数组:
创建一个含有四个整型元素的数组。
#include <stdio.h>
#include <string.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;
}
我们来依次解释每句:
printf("%d\n",sizeof(a));
a是数组名,sizeof(a)计算整个数组的大小,int类型大小是四个字节,所以结果为4*4=16个字节。
printf("%d\n",sizeof(a+0));
数组名a是数组首元素的地址,a+0还是首元素地址,地址大小为4(32位)或8(64位)字节。
printf("%d\n",sizeof(*a));
数组名a是数组首元素的地址,*a就是首元素,大小是4个字节。
printf("%d\n",sizeof(a+1));
数组名a是数组首元素的地址,a+1是第二个元素的地址,地址大小为4(32位)或8(64位)字节。
printf("%d\n",sizeof(a[1]));
a[1] 是第二个元素,大小为4个字节。
printf("%d\n",sizeof(&a));
&a 是数组的地址,数组的地址也是地址,大小是4或8个字节。
printf("%d\n",sizeof(*&a));
*&a等于a,计算整个数组大小,为16个字节
printf("%d\n",sizeof(&a+1));
&a+1相对于&a是跳过了整个数组,但是即使跳过整个数组, &a+1依然是地址,地址就是4或8个字节。
printf("%d\n",sizeof(&a[0]));
&a[0]是首元素地址,为4或8个字节。
printf("%d\n",sizeof(&a[0]+1));
&a[0]+1是第二个元素的地址,为4或8个字节。
字符数组:
#include <stdio.h>
#include <string.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));
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;
}
这么多例子不用慌,我们来依次解释每句 :
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
arr + 0与arr不同:
- 数组名单独放在sizeof内部,这里的arr表示整个数组,计算的是整个数组的大小,单位是字节,总共6个字节。
- arr表示数组首元素的地址,arr+0还是数组首元素的地址,是地址就是4/8个字节 。
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
- arr表示数组首元素的地址,*arr就是首元素,大小1个字节 。
- arr[1]就是第二个元素,大小是1个字节。
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
- &arr是数组的地址,但是数组的地址也是地址,是地址就是4/8 。
- &arr + 1是跳过整个数组后的地址,是地址就是4/8个字节。
- &arr[0] + 1是第二个元素的地址,是4/8个字节。
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(&arr));
- 数组名arr是一个指向数组首元素的指针,因为字符数组arr中没有\0,所以在求字符串长度的时候,会一直往后找,产生的结果就是随机值。
- arr + 0是首元素的地址,和第一个一样,也是随机值。
- &arr是数组的地址,数组的地址和数组首元素的地址,值是一样的,与strlen(arr)和strlen(arr+0)相同,传递给strlen函数后,依然是从数组的第一个元素的位置开始往后,统计产生的结果就是随机值
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
- arr是数组首元素的地址,*arr就是数组首元素,也就是'a'-97。strlen函数参数的部分需要传一个地址,当我们传递的是'a'时,'a'的ASCII码值是97,那就是将97作为地址传参。 这时strlen就会从97这个地址开始统计字符串长度,这就构成非法访问内存了,因为这块空间不属于当前程序,我们未申请这块空间,我们无法访问。
- arr[1]='b',将'b'的ASCII值98作为地址传参,与上述传递*arr的情况相同。
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
- &arr + 1是跳过整个数组后的地址,是地址就是4/8个字节。
- 第二个元素的地址,是4/8个字节。
字符串数组 :
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;
}
- arr与arr + 0相等,都指向字符串首地址,strlen从首地址开始向后查找到’ \0 ‘结束计算,计算出长度为6。
- arr指向字符串首元素a的地址,所以*arr=a,将a的ASCII值97作为地址传递给strlen,由于我们并未地址97处开辟空间,这样会造成非法访问内存。
- 同理,arr[1]为第二个元素b,将b的ASCII值97作为地址传递给strlen,同样造成非法访问内存。
- &arr:&数组名等于整个数组,结果为6。
- &arr + 1传递跳过整个数组后的地址并向后查,我们不知道’ \0 ‘会出现在哪,所以结果为随机值。
- &arr[0] + 1从b的地址开始向后计算长度。
想必你应该了解的差不多了,下面的我就不一一展开介绍了。
int main()
{
printf("%d\n", sizeof(arr + 0)); //arr + 0是首元素的地址4/8个字节
printf("%d\n", sizeof(*arr)); //*arr其实就是首元素,1个字节
//*arr == *(arr+0) == arr[0]
printf("%d\n", sizeof(arr[1])); //arr[1]是第二个元素,1个字节
printf("%d\n", sizeof(&arr)); //&arr是数组的地址,是地址就是4/8个字节
printf("%d\n", sizeof(&arr + 1)); //&arr + 1是跳过一个数组的地址,4/8
printf("%d\n", sizeof(&arr[0] + 1));//&arr[0] + 1是第二个元素的地址 4/8
return 0;
}
指针:
int main()
{
char* p = "abcdef";//将字符串中a的地址存放到指针p
//下标: 012345
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;
}
- 指针p指向字符串的首地址,从首地址向后计算遇到 \0 停止,结果为6.
- p+1指向字符串第二个元素b的地址,从b开始向后计算遇到 \0 停止,结果为5.
- *p等于p[0],它们都指向字符串首元素a的地址,所以*arr=a,将a的ASCII值97作为地址传递给strlen,由于我们并未地址97处开辟空间,这样会造成非法访问内存。
- p指向a的地址,所以&p指向a地址中第一个字节。 比如a地址为,则&p指向第一个字节0x
- 同理&p + 1指向a地址的第二个字节00 ,但我们并不知道a的地址中何时有使strlen停止的\0, 所以strlen( &p和&p+1)的结果为随机值。
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;
}
- p是一个指针变量,大小是4/8字节。
- p+1是'b'的地址,是地址大小就是4/8个字节。
- *p 就是'a',就是1个字节。
- p[0] == *(p+0) == *p ,为1个字节
- &p 为 char**类型, 计算的是指向指针
p
的指针的大小,大小为4/8个字节。 - 同理&p+1也一样。大小为4/8个字节。
- &p[0] + 1得到是'b'的地址 ,大小为4/8个字节。
二维数组:
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//3*4*4 = 48
printf("%d\n", sizeof(a[0][0]));//4
printf("%d\n", sizeof(a[0]));//a[0]是第一行这个一维数组的数组名
//数组名算是单独放在sizeof内部了,计算的是整个数组的大小,大小是16个字节
printf("%d\n", sizeof(a[0] + 1));//?
//a[0]作为第一行的数组名,没有单独放在sizeo内部,没有&
//a[0]表示数组首元素的地址,也就是a[0][0]的地址
//所以a[0]+1是第一行第二个元素的地址,是地址就是4/8个字节
//
printf("%d\n", sizeof(*(a[0] + 1)));//4
//计算的是就是第一行第2个元素的大小
printf("%d\n", sizeof(a + 1));//4 / 8
//a是数组首元素的地址,是第一行的地址 int(*)[4]
//a+1 就是第二行的地址
printf("%d\n", sizeof(*(a + 1)));//16
//*(a+1) --> a[1] -> sizeof(*(a+1))->sizeof(a[1]) 计算的是第二行的大小
//a+1 --> 是第二行的地址,int(*)[4]
//*(a+1) 访问的第二行的数组
printf("%d\n", sizeof(&a[0] + 1));//4/8
//&a[0]是第一行的地址 int(*)[4]
//&a[0]+1 是第二行的地址 int(*)[4]
printf("%d\n", sizeof(*(&a[0] + 1)));//16 计算的是第二行的大小
printf("%d\n", sizeof(*a));//计算的是第一行的大小-16
//a是数组首元素的地址,就是第一行的地址
//*a 就是第一行
//*a --> *(a+0) --> a[0]
printf("%d\n", sizeof(a[3]));//16
//a[3]--> int [4] // sizeof只看类型,不进行计算
//
return 0;
}
2、指针笔试题
例一:
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}* p = (struct Test*);
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
假设p 的值为0x100000。 如下表表达式的值分别为多少?
已知,结构体Test类型的变量大小是20个字节
- p + 0x1 使指针
p
增加了0x1
,即一个地址单元(根据p
的类型)。然后,它使用%p
格式符打印增加后的指针的值。这将打印出p
原始地址加上一个地址单元的地址,因为p
是一个指向struct Test
类型的指针,所以它会移动一个struct Test
的大小,即在p的地址加上20个字节,因为地址通常是十六进制形式,20的十六进制形式为14 , 所以结果为0x100000+14=0x100014。 -
(unsigned long)p + 0x1
将指针p
转换为unsigned long
类型,然后增加0x1
。这里的0x1
表示十六进制数,等于十进制的1
。在这种情况下,整数类型加1确实会将值增加1,不会根据类型不同导致加1的结果不同。所以,结果是0x100000 + 1 = 0x100001
。这个结果表示p
的地址增加了一个地址单元的大小,因此变为0x100001
。 -
(unsigned int*)p + 0x1 首先将指针
p
转换为unsigned int*
类型,然后增加0x1
。这将导致指针增加一个unsigned int* 的大小(通常是4个字节),结果为0x100004.
输出结果如下:
例二:
int main()
{
int a = 7;
short s = 4;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
- sizeof只判断类型,s=a+2将两个整型a+2的结果赋值给short类型s,s依然是short类型没有改变,所以输出结果为2。
- 程序执行流程:编译 + 链接 --> 可执行程序--> 运行---> 结果
- 内部表达式s = a + 2在运行时才计算,而sizeof在编译期间就已经知道结果,所以s = a + 2不会进行计算,s没有赋值为a+2. 所以第二个输出语句结果为s的初始值4.
例三:
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+1跳过数组大小个字节,指向了数组
a
的内存位置之后的第一个字节,同时被强制转换类型为int*类型。 - *(ptr - 1)将ptr指向的地址向前移动一个字节,使其指向数组最后一个元素5的地址,同时解引用得到整型5。
- a+1指向数组第二个元素的地址,*(a + 1)解引用得到整型2.
例四:
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;
}
首先看 int* ptr1 = (int*)(&a + 1);
- &a+1 取地址数组名a+1跳过数组大小个字节,指向了数组
a
的内存位置之后的第一个字节,同时被强制转换类型为int*类型。 - ptr1[-1]等于*(ptr-1),将ptr指向的地址向前移动一个字节,指向数组a中第四个元素4的地址,并解引用得到整型4,输出结果为4.
再来看int* ptr2 = (int*)((int)a + 1);
-
(int)a
将数组a
的起始地址转换为整数值,这个整数值代表数组的内存位置。 -
+ 1
操作将地址值增加了1,这意味着它会指向地址a的下一个字节。
- 数组a中储存的1,2,3,4各自是一个整形,各占四个字节,我们机器是小端存储,在内存中,倒着存,因为1的十六进制是0x 00 00 00 01
- 小端存储低字节放低地址,低到高地址 对应01 00 00 00 小端字节存储后,输出需倒序还原回。
- 所以(int)a + 1指向01后面的第一个00.
-
(int*)((int)a + 1)
强制类型转换为int*类型,使a变成存放四个整形元素的指针数组。 -
红框圈出部分即为指针数组a存放的四个字节的地址
*ptr2解引用访问a中的数据,输出前将其还原为,以十六进制打印时2前面的0不打印,输出结果为2000000
例五:
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+1跳过数组大小个字节,指向了数组
a
的内存位置之后的第一个字节,同时被强制转换类型为int*类型。 - *(ptr1 - 1)将ptr指向的地址向前移动一个字节,使其指向数组最后一个元素“ 10 ”的地址,同时解引用得到整型10。
- (int*)(*(aa + 1))表示将第二个一维数组的第一个元素的地址转换为int* 类型的指针。
- 在这里,(int*)将* (aa + 1)的结果转换为int* 类型的指针。
- 在没有类型转换的情况下,* (aa + 1)的结果是一个int类型的值,即第二个一维数组的第一个素的值,即6。
- 通过进行类型转换(int*)(*(aa + 1)),我们将这个int类型的值的地址转换为了int* 类型的指针。这样,指针ptr2就指向了数组aa中第二个一维数组的第一个元素,*(ptr2 - 1)得到第二个一维数组的第一个元素前一个元素,也就是第一个一维数组的第五个元素即 “ 5 ”.
例七:
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;
}
printf("%s\n", **++cpp);
- 1、这一行先对 cpp 进行递增操作,使其指向 cp 中的下一个元素也就是第二个元素。
- ++cpp指向cp第二个元素相当于cp+1,
- 然后,进行两次解引用操作,
- 第一次解引用*++cpp变成c+2,即访问cp第二个元素c+2,c+2指向数组c的第三个元素
- 第二次解引用*c+2变成"POINT",即访问数组c的第三个元素"POINT"
- 因此,这一行打印输出的是 "POINT"。
printf("%s\n", *-- * ++cpp + 3);
- 2、优先级++ -- > * > +,计算顺序*--*++cpp按照右结合性,最后计算+3 首先这一行先对cpp进行递增操作,在上一行代码中cpp指向cp第二个元素c+2,
- 所以++cpp指向cp第三个元素c+1,
- 然后解引用++cpp等于访问c+1,此时*++cpp变成c+1,
- 接着进行递减操作,c+1变成c,解引用c访问"ENTER",*c储存字符串"ENTER"的首元素地址,也就是第一个字符'E'的地址
- 最后*c+3跳过三个字符变成'E'的地址,输出从E开始到\0的字符串"ER"
printf("%s\n", *cpp[-2] + 3);
- 3、*cpp[-2]等于**(cpp-2) + 3,
- 在第二段代码中++cpp指向cp第三个元素c+1,所以cpp-2指向c+3
- 即**(cpp-2)+3 = *(c+3)+3
- *(c+3)访问"FIRST"也就是'F'的地址,
- 最后+3跳过三个字符变成'S'的地址,输出从S开始到\0的字符串"ST"
printf("%s\n", cpp[-1][-1] + 1);
- 4、 cpp[-1][-1] + 1等于 *(*(cpp-1)-1)+1
- 在第三段代码中未改变cpp,所以cpp指向的值与第二段代码中++cpp的值一样,指向cp第三个元素c+1
- *(cpp-1) = *(c+2) = c+2
- *( *(cpp-1) -1) +1 = *(c+2-1)+1 = *(c+1)+1
- *(c+1)访问"NEW"的首元素'N'的地址,然后加1访问第二个字符'E'的地址,输出从E开始到\0的字符串"ER"。