指针(6)
前言
这是上一节指针(5)最后的七道代码题的详解。
1.代码1
#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;
}
1.1代码解析:
首先,我们需要理解数组和指针在内存中的布局,以及如何通过指针运算来访问数组的元素。
数组 a 在内存中是连续存储的,每个元素占用 int 的大小(通常是4个字节,但这取决于具体的编译器和平台)。数组 a 的声明和初始化如下:
int a[5] = { 1, 2, 3, 4, 5 };
在内存中,a可能看起来像这样(假设每个 int 是4个字节):
+-------+-------+-------+-------+-------+
| 1 | 2 | 3 | 4 | 5 |
+-------+-------+-------+-------+-------+
^ ^ ^ ^ ^
| | | | |
a a+1 a+2 a+3 a+4
接下来,我们看指针 ptr 的声明和初始化:
int *ptr = (int *)(&a + 1);
现在,我们来看printf语句中的两个表达式:
printf( "%d,%d", *(a + 1), *(ptr - 1));
- *(a + 1):这个表达式访问数组a的第二个元素,即值2。
- *(ptr - 1):这个表达式首先把ptr减1,然后解引用得到该位置的整数值。由于ptr指向a数组末尾之后的位置,ptr - 1将指向a数组的最后一个元素,即值5。
因此,printf语句将输出:
2,5
1.2简化的内存布局图
下面是一个简化的内存布局图,用来解释指针运算和访问:
+-------+-------+-------+-------+-------+-------+
| 1 | 2 | 3 | 4 | 5 | ... | (其他数据)
+-------+-------+-------+-------+-------+-------+
^ ^ ^ ^ ^ ^
| | | | | |
a a+1 a+2 a+3 a+4 ptr
2.代码2
//在X86环境下
//假设结构体的大小是20个字节
//程序输出的结果是啥?
#include <stdio.h>
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;
}
2.1代码解析:
现在,我们来分析每个printf语句的输出结果:
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
2.2简化的内存布局图
下面是对应的代码和内存布局图分析:
+------------------+ 0x100000 (p指向的初始地址)
| struct Test |
| ... |
+------------------+
+------------------+ 0x100014 (p + 0x1的地址)
| struct Test |
| ... |
+------------------+
+------------------+ 0x100001 ((unsigned long)p + 0x1的地址)
| (任意数据) |
+------------------+
+------------------+ 0x100004 ((unsigned int*)p + 0x1的地址)
| (任意数据) |
+------------------+
因此,程序的输出应该是:
0x100014
0x100001
0x100004
3.代码3
#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;
}
3.1代码解析:
首先,我们需要理解int a[3][2]这个二维数组在内存中的布局。二维数组在内存中是按照行优先的方式存储的,即第一行的所有元素连续存放,然后是第二行的所有元素,依此类推。
数组a的定义如下:
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
所以,数组a在内存中的实际布局如下:
+---+---+
| 1 | 0 | <- a[0][0] 和 a[0][1]
+---+---+
| 3 | 0 | <- a[1][0] 和a[1][1]
+---+---+
| 5 | 0 | <- a[2][0] 和a[2][1]
+---+---+
接下来,我们声明了一个指向int的指针p,并将其初始化为a[0],即第一行的首地址:
int *p;
p = a[0];
此时,p指向数组a的第一行的第一个元素,即值为1的那个元素。
现在,我们来看printf语句:
printf( "%d", p[0]);
由于p指向a[0][0],p[0]实际上就是a[0][0],所以输出的值将是1。
总结: 这段代码将输出1,因为p指向a数组的第一行的第一个元素,而p[0]就是这个元素的值。
3.2简化的内存布局图
+---+---+ +---+
| 1 | 0 | ----> | 1 | <- p 指向这里
+---+---+ +---+
| 3 | 0 |
+---+---+
| 5 | 0 |
+---+---+
4.代码4
//假设环境是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;
}
4.1代码解析:
接下来,我们分析代码中的指针声明和赋值:
int a[5][5];
int(*p)[4];
p = a;
现在,我们来看printf语句中的表达式:
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
4.2简化的内存布局图
通过画图来展示内存布局和指针运算:
+---+---+---+---+---+
| a | a | a | a | a | <- a[0] (指向第一行的指针)
+---+---+---+---+---+
| a | a | a | a | a | <- a[1]
+---+---+---+---+---+
| a | a | a | a | a | <- a[2]
+---+---+---+---+---+
| a | a | a | a | a | <- a[3]
+---+---+---+---+---+
| a | a | a | a | a | <- a[4]
+---+---+---+---+---+
p -> | | | | | <- p[0] (指向一个假设的包含4个int的数组)
但是实际上,p指向的是a[0],即a的第一行。
通过p访问p[4]实际上会访问到数组a之外的地方,这是越界行为。
接下来是p[4][2]和a[4][2]的地址:
- &a[4][2] 是数组a中第5行第3个元素的地址。
- &p[4][2] 试图通过p访问第5行第3个元素,但由于p的声明,这会访问到数组a之外的地方。
因此,&p[4][2] - &a[4][2]这个表达式没有意义,因为它试图计算一个越界地址和一个有效地址之间的差。在实际运行中,这可能会导致未定义的行为,包括程序崩溃。
5.代码5
#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;
}
5.1代码解析:
首先,定义了一个二维数组 aa,它有两行五列,并初始化为1到10的整数。
+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+
| 6 | 7 | 8 | 9 | 10|
+---+---+---+---+---+
在内存中,二维数组是以行优先的方式存储的,也就是说,第一行的所有元素连续存放,然后是第二行的所有元素。
接下来,定义了两个整数指针 ptr1 和 ptr2。
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
- ptr2 的赋值:
然而,如果我们假设这种转换是出于某种教学目的,并忽略其不安全性,我们可以继续分析。由于这种转换没有逻辑意义,我们在这里假设 ptr2 被赋予了某个有效的内存地址。
现在,代码执行了以下打印语句:
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
- *(ptr2 - 1):
综上所述,如果我们忽略 ptr2 的不安全赋值,并假设 ptr2 指向某个合理的内存位置,代码的输出将是:
10,5
5.2简化的内存布局图
6.代码6
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
6.1代码解析:
现在,让我们逐步分析代码并画出简化的内存布局图:
- 初始化字符指针数组 a:
+--------+ +-----+ +----+ +-------+
| a | -> |work | -> |at | -> |alibaba|
+--------+ +-----+ +----+ +-------+
|
V
pa
- 执行 pa++:
+--------+ +-----+ +----+ +-------+
| a | -> |work | -> |at | -> |alibaba|
+--------+ +-----+ +----+ +-------+
|
V
pa
- 执行 printf(“%s\n”, *pa); :
所以,程序的输出将是:
at
通过上面的图形分析,我们可以清楚地看到指针是如何移动以及它们如何指向不同的字符串字面量的。在执行 printf 时,*pa 指向的是 “at” 字符串,因此打印出 “at”。
7.代码7
#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;
}
7.1代码解析:
首先,我们定义了一个字符指针数组 c 和一个字符指针的指针数组 cp,以及一个字符指针的指针的指针 cpp。
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
根据代码,我们可以画出以下初始状态图:
+-------+ +--------+ +-------+
| c |->|"ENTER" |->| ... |
+-------+ +--------+ +-------+
| |->|"NEW" |->| ... |
| |->|"POINT" |->| ... |
| |->|"FIRST" |->| NULL |
+-------+
+-------+ +-------+ +-------+ +-------+
| cp |->| c+3 |->| c+2 |->| c+1 |->| c |
+-------+ +-------+ +-------+ +-------+
+-------+
| cpp |->| cp |
+-------+
也可以是:
接下来,我们分析每个 printf 语句:
1.
printf("%s\n", **++cpp);
因此,输出为:
POINT
更新后的图:
+-------+ +--------+ +-------+
| c |->|"ENTER" |->| ... |
+-------+ +--------+ +-------+
| |->|"NEW" |->| ... |
| |->|"POINT" |->| ... |
| |->|"FIRST" |->| NULL |
+-------+
+-------+ +-------+ +-------+ +-------+
| cp |->| c+3 |->| c+2 |->| c+1 |->| c |
+-------+ +-------+ +-------+ +-------+
+-------+
| cpp |->| c+2 |
+-------+
printf("%s\n", *--*++cpp+3);
因此,输出为:
INT
更新后的图:
+-------+ +--------+ +-------+
| c |->|"ENTER" |->| ... |
+-------+ +--------+ +-------+
| |->|"NEW" |->| ... |
| |->|"POINT" |->| ... |
| |->|"FIRST" |->| NULL |
+-------+
+-------+ +-------+ +-------+ +-------+
| cp |->| c+3 |->| c+2 |->| c+1 |->| c |
+-------+ +-------+ +-------+ +-------+
+-------+
| cpp |->| c+1 |
+-------+
printf("%s\n", *cpp[-2]+3);
因此,输出为:
ST
更新后的图不变。
4.
printf("%s\n", cpp[-1][-1]+1);
OINT
更新后的图依然不变。
所以,整段代码的输出是:
POINT
ER
ST
WE