参考链接:Structure pointer pointing to different structure instance
注:可以查看此篇的问题和唯一的回复,那是相对正确的,不要看comment,有很多错误。
我是拒绝分析这种问题的,因为似乎没有人会这么乱用,但是……在华保健老师的编译原理示例代码和Linux0.11内核中,就遇到了这么神奇的代码,那就不得不研究一下了!毕竟是大神写的代码,我不知道应该是我渣。
1 测试代码
#include <stdio.h>
#include <stdlib.h>struct A {char a;int b;
};struct B {int c;int d;
};struct C {int e;char f;
};int main() {struct A a = { 'a', 100 };struct B b = { 101, 300 };struct C c = { 200,'c' };// 根据字节对齐,都占据8字节printf("A: size %d %c %d\n", sizeof(a), a.a, a.b);printf("B: size %d %d %d\n", sizeof(b), b.c, b.d);printf("C: size %d %d %c\n", sizeof(c), c.e, c.f);struct A *ap = &b; // A结构体指针,指向结构体Bprintf("%d %d\n",ap->a, ap->b);printf("%c %d\n", ap->a, ap->b);char *chp = &b;chp[1] = 'b'; // 这块区域其实是字节对齐导致的空闲空间printf("%d %d\n", ap->a, ap->b);printf("%c %d\n", ap->a, ap->b);/* 如何访问这块内存,取决于ap指针,能访问多大地方,取决于内存区域本身 */ap->a = 'c'; // ap->a = 'c'就是相当于 char a = 'c';ap->a = 1000; // ap->a = 1000 就是相当于 char a = 1000; 1000过大会被截断高位ap->b = 3000; // ap->b <=> int b ...struct C *cp = &b; // C结构体指针,指向结构体Bprintf("%d %d\n", cp->e, cp->f);printf("%d %c\n", cp->e, cp->f);cp->e = 3000;cp->f = 'e';cp->f = 1000;// 整形指针指向结构体Aint *bp = &a;bp[0] = 1000;bp[1] = 2000;printf("A: %c %d\n", a.a, a.b);printf("A: %d %d\n", a.a, a.b);bp[2] = 2000; // 可以修改内存,但是堆栈溢出,// 因为该空间没有被分配(局部变量是保存在堆栈中的)return 0;
}
2 结构体占据空间问题 & 字节对齐
struct A {char a;int b;
};struct B {int c;int d;
};struct C {int e;char f;
};...
struct A a = { 'a', 100 };
struct B b = { 101, 300 };
struct C c = { 200,'c' };// 根据字节对齐,都占据8字节
printf("A: size %d %c %d\n", sizeof(a), a.a, a.b);
printf("B: size %d %d %d\n", sizeof(b), b.c, b.d);
printf("C: size %d %d %c\n", sizeof(c), c.e, c.f);
...
运行以上程序,我们可以直到,三个结构体分别创建了一个变量,并且每个结构体占据的空间大小都是8字节。
至于为什么都是8字节,这是内存对齐问题,不展开说明了,我们看看这几个结构体被分配的空间情况吧。
- 每个结构体都占8字节的内存空间
- 红色部分表示实际占用的空间
- 蓝色部分表示空闲空间
注意:这就意味着,凡是被分配的8字节空间,是可以任意访问的,而空间外面是不允许访问的。
让结构体A的指针ap,指向结构体B的变量b
现在我们建立一个结构体A的指针,让其指向b。
struct A *ap = &b; // A结构体指针,指向结构体B
printf("%d %d\n",ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);
我们看看内存的情况,再分析一下打印的结果。
上面是内存的分布情况,现在
- 访问
ap->a
打印出来的是:101
,e
- 访问
ap->b
打印出来的是300
所以ap指针实际访问的应该是下面重点标出的部分:
而这部分,是不是很熟悉?
所以,ap指针尽管指向了结构体B,但是实际还是按照结构体A的结构访问内存的。
2.1 使用char指针指向结构体B
刚才我们发现,使用结构体A的指针,可以直接访问结构体B,那么,如果是基本数据类型呢?我们试一下。
char *chp = &b;
chp[1] = 'b'; // 这块区域其实是字节对齐导致的空闲空间
printf("%d %d\n", ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);
我们看到内存分布如上图,现在执行chp[1] = 'b'
(b的ASCII码是62)
之后就变成了:
哦!这是令人惊讶的,char类型的指针指向了一块内存区域,然后使用下标修改了内存的值!
还记得动态数组申请吗?和内个是一样的原理!
int *a = (int *)malloc(sizeof(int) * 10);
a[0] = 1; // 使用下标访问
a[1] = 2;
...
free(a);
告诉我们两件事
- 指针默认指向最开始的元素,索引是0
- 使用下标索引可以依次访问后面的元素,每次向后移动的内存数,取决于指针的数据类型
所以上面的事情不难理解。
然后我们继续执行程序
printf("%d %d\n", ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);
尽管之前的空闲空间改变了,但是结果依然不变,也就是说我们之前的说法是正确的。
再进一步验证
/* 如何访问这块内存,取决于ap指针,能访问多大地方,取决于内存区域本身 */
ap->a = 'c'; // ap->a = 'c'就是相当于 char a = 'c';
ap->a = 1000; // ap->a = 1000 就是相当于 char a = 1000; 1000过大会被截断高位
ap->b = 3000; // ap->b <=> int b ...
结果显而易见,对于ap->a = 1000
,尽管1000已经超过了1字节大小,但是最终只修改了第一个字节,这就好比char a = 1000
一样,a = 0xe8
是的,1000 = 0x3e8,但是只有一个字节,所以最高位的3被舍弃了。
2.2 用结构体C指针cp指向结构体B
struct C *cp = &b; // C结构体指针,指向结构体B
printf("%d %d\n", cp->e, cp->f);
printf("%d %c\n", cp->e, cp->f);cp->e = 3000;
cp->f = 'e';
cp->f = 1000;
我们再试一试!
最终结果显而易见。
2.3 用int指针指向结构体A
// 整形指针指向结构体A
int *bp = &a;
bp[0] = 1000;
bp[1] = 2000;
printf("A: %c %d\n", a.a, a.b);
printf("A: %d %d\n", a.a, a.b);
bp[2] = 2000; // 可以修改内存,但是堆栈溢出,// 因为该空间没有被分配(局部变量是保存在堆栈中的)
其实这个事情我们之前干过了,之前用char,现在用int再干一下。
这个事情进一步说明了什么呢?
- a提供了有限的8字节内存空间
- bp指针能够修改哪里,取决于它指向的地址;一次修改多大空间,取决于它数据类型的大小
- 指针不能修改未被分配的空间,最后
bp[2]
访问了外界空间,因此产生了
因为局部变量都是被分配在栈中的,现在这个局部变量访问越界了,产生了错误,栈被破坏。
栈破坏这里情况非常复杂,先粗浅理解为,使用了未分配的空间导致了错误吧。
Linux0.11 内核中,使用上述方法,实现了GDT和IDT。
3 小结:精华在这里
分析了这么多,最终小结一下吧。
我们的眼中只有两件事
- 已分配的内存空间
- 某数据类型的指针
现在,我们就让指针指向内存空间的起始地址,然后就可以操作这个内存空间了。
再增加一些限制
- 内存空间就这么大,不能访问外面
- 指针每次访问的地址,是通过下标访问的,一次只能移动数据类型大小的整数倍
这个时候你眼中的C语言,分配一块内存,再创建一个指针,打遍天下无敌手!
当然了,除了特殊情况一般没人这么干,你会疯掉,看你代码的人也会疯掉!
4 补充:直接深入底层,看汇编代码
之前我们的分析是基于C语言层级的,比较抽象,实际上,编译完成之后的汇编语言,一看就明白了。
你可以看到ap->a
直接访问的是byte
,而ap->b
访问的是dword
,一个是字节,一个是双字,大小自然清晰。
这也是编译器的功能,把C语言提供的,方便人类使用的大量抽象,给翻译成方便机器使用的少量指令的复杂排列组合。
5 什么叫打遍天下无敌手呢?
其实就是瞎玩儿吧……但是的确可以这么干的!我们试一试。
int main() {char aaa[4] = { 1,2,3,4 };char aaa2[4] = { 1,2,3,4 };int *bbb = &aaa;printf("\n\n%x\n\n\n", bbb[0]);return 0;
}
会打印什么呢?显而易见的!内存是01 02 03 04
,然后一个int *
指针访问了它,打印04030201
。
我们可以使用bbb[0]
或者*b
都行,因为b指向起始地址。
那,能不能通过bbb[1]
访问aaa2
的内存呢?
不行! 因为aaa1
和aaa2
是两个数组变量,他们在内存中的位置不是连续的,是随机的,如果你想达到内种效果,那就是前面提到的结构体了,把这两个放进一个结构体里面,就是连续分配内存了,就能使用bbb[1]
了。
最后,记住只有两件事
- 一块已分配的内存
- 一个指针