指针(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);
- 这里,&a取得数组a的地址,即数组首元素的地址。但是,&a的类型是int (*)[5],即指向一个包含5个int的数组的指针。当我们对&a加1时,不是简单地加1个字节或加1个int的大小,而是加上整个数组的大小(即5个int的大小,通常是20个字节)。
- 因此,ptr指向的是a数组之后的那个位置,即a数组的末尾之后的第一个位置。
现在,我们来看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
在这个图中,ptr指向a数组之后的某个位置(用省略号表示的其他数据)。当我们将ptr减1时,它指向a数组的最后一个元素。而a+1直接指向a数组的第二个元素。
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代码解析:
在X86环境下,指针的算术运算依赖于指针所指向的数据类型的大小。当对结构体指针p进行加法运算时,编译器会根据结构体的大小来计算偏移量。由于题目假设struct Test的大小是20个字节,因此p + 1将增加20个字节的偏移量。
现在,我们来分析每个printf语句的输出结果:
printf("%p\n", p + 0x1);
这里对结构体指针p进行加法运算,0x1被解释为结构体的数量,而不是字节偏移量。因此,这将增加20个字节的偏移量(因为每个struct Test的大小是20个字节)。原始地址是0x100000,增加20个字节(16进制是14)后,新地址将是0x100014。
printf("%p\n", (unsigned long)p + 0x1);
这里首先将结构体指针 p 强制转换为unsigned long类型,这是一个无符号长整型,其大小通常与平台相关,但通常足够大以存储一个指针值。转换为unsigned long后,p不再被视为指向结构体的指针,而是一个普通的无符号长整型数。然后,0x1被加到这个整数值上,这里增加的是1个字节的偏移量,因为unsigned long是按字节进行运算的。因此,新地址将是0x100001。
printf("%p\n", (unsigned int*)p + 0x1);
这里首先将结构体指针p强制转换为 unsigned int* 类型,即无符号整型指针。与上一个情况类似,p不再被视为指向结构体的指针,而是一个指向无符号整型的指针。然后,0x1被加到这个指针上,由于unsigned int的大小通常是4个字节(在32位系统上),所以这里增加的是4个字节的偏移量。因此,新地址将是0x100004。
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) };
- 这里有一个需要注意的地方,即初始化列表中的逗号运算符。在C语言中,逗号运算符,会计算其左侧和右侧的操作数,但整个表达式的结果是右侧操作数的值。
- 因此,(0, 1)实际上只将1赋值给数组的第一个元素,同理(2, 3)将3赋值给数组的第二个元素,(4, 5)将5赋值给数组的第三个元素。
- 数组的第二列没有被显式初始化,因此它们会被自动初始化为0(在大多数情况下)。
所以,数组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代码解析:
在x86环境下,首先我们需要理解数组在内存中的布局以及指针运算的规则。对于二维数组a[5][5],它在内存中是连续存储的,按照行优先的顺序排列。每个元素占用int类型的大小,通常是4个字节(但这取决于具体的编译器和平台)。
接下来,我们分析代码中的指针声明和赋值:
int a[5][5];
int(*p)[4];
p = a;
- 这里声明了一个指向具有4个int元素的数组的指针p。注意,p被声明为指向一个包含4个int的数组,而不是指向一个int。然后,我们将a的地址赋值给p。尽管a是一个5x5的二维数组,但由于数组名在大多数上下文中会退化为指向其首元素的指针,这里a会退化为指向其第一行(即一个包含5个int的数组)的指针。
- 由于p是指向一个包含4个int的数组的指针,这意味着当我们通过p来访问数组元素时,编译器会假设每行有4个元素,而不是实际的5个。这可能会导致未定义的行为,因为编译器可能会错误地计算地址。
现在,我们来看printf语句中的表达式:
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
- 这里有两个地址相减的操作。对于第一个%p格式化输出,我们需要理解地址相减的结果。地址相减的结果是它们之间元素个数的差,乘以每个元素的大小(在这里是int的大小)。但是,由于p是指向包含4个int的数组的指针,通过p访问p[4][2]实际上会越界,因为p只认为每行有4个元素。
- 对于第二个%d格式化输出,我们试图将地址差转换为整数,这通常是不安全的,因为地址差应该是一个指针类型的值。然而,为了分析这个表达式,我们假设编译器允许这种转换(尽管它可能产生警告或错误)。
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));
- ptr1 的赋值:
&aa 是二维数组 aa 的地址,它是一个指向包含5个整数的数组的指针。&aa + 1 则指向 aa 数组下一个“行”的地址,即第二行之后的位置。这里需要注意的是,&aa + 1 实际上跳过了整个二维数组,因为它增加的是指向包含5个整数的数组的指针,而不是单个整数的指针。
因此,ptr1 指向的是 aa 数组后面紧接着的内存位置,即 aa 数组的末尾之后。
- ptr2 的赋值:
aa + 1 是指向 aa 数组第二行的指针,它实际上是一个指向整数的指针(因为数组名在大多数上下文中退化为指向其首元素的指针)。*(aa + 1) 解引用这个指针,得到第二行的第一个元素的值,即6。然后,(int )((aa + 1)) 将这个整数值强制转换为整数指针,这通常是不安全的,因为整数值6不是一个有效的内存地址。
然而,如果我们假设这种转换是出于某种教学目的,并忽略其不安全性,我们可以继续分析。由于这种转换没有逻辑意义,我们在这里假设 ptr2 被赋予了某个有效的内存地址。
现在,代码执行了以下打印语句:
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
- *(ptr1 - 1):
由于 ptr1 指向 aa 数组末尾之后的位置,ptr1 - 1 将指向 aa 数组的最后一个元素,即10。因此, *(ptr1 - 1) 的值是10。
- *(ptr2 - 1):
由于 ptr2 的赋值逻辑上存在问题,我们无法准确知道 ptr2 - 1 会指向哪里,除非我们知道确切的转换结果。但是,如果我们忽略这个不安全的转换并假设 ptr2 指向某个有效的内存位置,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,它包含三个字符串字面量的地址:“work”, “at”, 和 “alibaba”。然后,我们定义了一个指向字符指针的指针pa,并将它初始化为指向数组a的第一个元素(即字符串"work"的地址)。
现在,让我们逐步分析代码并画出简化的内存布局图:
- 初始化字符指针数组 a:
+--------+ +-----+ +----+ +-------+
| a | -> |work | -> |at | -> |alibaba|
+--------+ +-----+ +----+ +-------+
|
V
pa
这里,a 是一个字符指针数组,它的每个元素都是一个指向字符串字面量的指针。pa 是一个指向字符指针的指针,它最初指向 a 的第一个元素(即 work 的地址)。
- 执行 pa++:
+--------+ +-----+ +----+ +-------+
| a | -> |work | -> |at | -> |alibaba|
+--------+ +-----+ +----+ +-------+
|
V
pa
执行 pa++ 之后,pa 指针向前移动了一个指针的大小(在这个平台上,通常是一个机器字的长度,比如4字节或8字节,取决于系统是32位还是64位)。现在,pa 指向 a 的第二个元素,即字符串 “at” 的地址。
- 执行 printf(“%s\n”, *pa); :
在打印语句中,*pa 解引用 pa 指针,得到它指向的值,即字符串 “at” 的地址。然后,printf 使用这个地址作为参数来打印字符串 “at”。
所以,程序的输出将是:
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 |
+-------+
也可以是:
这里,c 是一个字符指针数组,存储了四个字符串的地址;cp 是一个指向字符指针的指针数组,它的四个元素分别指向 c 数组的不同位置(从后往前);cpp 是一个指向 cp 的指针。
接下来,我们分析每个 printf 语句:
1.
printf("%s\n", **++cpp);
首先,cpp 自增,指向 cp 的下一个元素(即 c+2)。然后,*cpp 解引用得到 c+2,再次解引用得到 *(c+2),即字符串 “POINT”。
因此,输出为:
POINT
更新后的图:
+-------+ +--------+ +-------+
| c |->|"ENTER" |->| ... |
+-------+ +--------+ +-------+
| |->|"NEW" |->| ... |
| |->|"POINT" |->| ... |
| |->|"FIRST" |->| NULL |
+-------+
+-------+ +-------+ +-------+ +-------+
| cp |->| c+3 |->| c+2 |->| c+1 |->| c |
+-------+ +-------+ +-------+ +-------+
+-------+
| cpp |->| c+2 |
+-------+
printf("%s\n", *--*++cpp+3);
这个表达式有些复杂。首先,cpp 自增,指向 c+1。然后,cpp 解引用得到 c+1,再次自增得到 c+2。然后, --*++cpp 解引用得到 *(c+2),即字符串 “POINT”。最后,+3 使得指针跳过字符串的前三个字符,指向 “INT”。
因此,输出为:
INT
更新后的图:
+-------+ +--------+ +-------+
| c |->|"ENTER" |->| ... |
+-------+ +--------+ +-------+
| |->|"NEW" |->| ... |
| |->|"POINT" |->| ... |
| |->|"FIRST" |->| NULL |
+-------+
+-------+ +-------+ +-------+ +-------+
| cp |->| c+3 |->| c+2 |->| c+1 |->| c |
+-------+ +-------+ +-------+ +-------+
+-------+
| cpp |->| c+1 |
+-------+
printf("%s\n", *cpp[-2]+3);
这里,cpp[-2] 相当于 *(cpp-2),即 c+3。解引用得到 *(c+3),即字符串 “FIRST”。然后,+3 使得指针跳过字符串的前三个字符,指向 “ST”。
因此,输出为:
ST
更新后的图不变。
4.
printf("%s\n", cpp[-1][-1]+1);
这里,cpp[-1] 相当于 *(cpp-1),即 c+2。然后,cpp[-1][-1] 相当于 *((c+2)-1),即 *(c+1),即字符串 “POINT”。最后,+1 使得指针跳过字符串的第一个字符,指向 “OINT”。
OINT
更新后的图依然不变。
所以,整段代码的输出是:
POINT
ER
ST
WE
结语
这七道题目,是对前面五节课关于指针内容回顾的综合题目,难度比较大,耐心一点看,尽量把题目吃透,对以后的数据结构的分析有帮助。