C结构点运算符究竟做了什么(低级透视)?

我对C中的结构有疑问。所以当你创建一个结构时,你实际上是在定义一块内存的框架。 因此,当您创建结构的实例时,您正在创建一个内存块,以便它能够容纳一定数量的元素。

但是,我对点运算符正在做什么感到有些困惑。 如果我有一个struct Car并且有一个名为GasMileage的成员(这是一个int成员),我可以通过做类似的事情获得GasMileage的值,

 int x = CarInstance.GasMileage; 

但是,我对这个点运算符实际发生的事情感到困惑。 点运算符是否只是作为基址的偏移量? 它究竟是如何推断它是一个int?

我想我对幕后发生的事情很好奇。 通过做其他事情可以引用GasMileage吗? 如

 int *GasMileagePointer = (&carInstance + offsetInBytes(GasMileage)); int x = *GasMileage 

这只是我快速弥补的事情。 我一直在努力寻找一个好的解释,但似乎没有什么能比将点运算符视为魔术更能解释它。

当你使用. 运算符,编译器根据其前面的字段(和填充)的大小将其转换为struct内部的偏移量。

例如:

 struct Car { char model[52]; int doors; int GasMilage; }; 

假设int是4个字节且没有填充,则model的偏移量为0doors的偏移量为52GasMilage的偏移量为56。

因此,如果您知道成员的偏移量,您可以像这样获得指向它的指针:

 int *GasMileagePointer = (int*)((char *)&carInstance + offsetInBytes(GasMile)); 

转换为char *是必要的,因此指针算法一次sizeof(carInstance) 1个字节而不是1个sizeof(carInstance) 。 然后需要将结果转换为正确的指针类型,在本例中为int *

是的,点运算符只是从结构的基数应用偏移量,然后访问该地址的值。

 int x = CarInstance.GasMileage; 

相当于:

 int x = *(int *)((char*)&CarInstance + offsetof(Car, GasMileage)); 

对于具有其他类型T的成员,唯一的区别是cast (int *)变为(T *)

点运算符只是选择成员。

由于编译器具有关于成员的类型 (以及因此大小 )的信息(实际上是所有成员),因此它知道成员从结构的开头的偏移量并且可以生成适当的指令。 它可以生成基本+偏移访问,但它也可以直接访问成员(或者甚至将其缓存在寄存器中)。 编译器具有所有这些选项,因为它在编译时具有所有必要的信息。

如果没有,就像不完整类型一样 ,你会得到一个编译时错误。

当它工作时,“。” 行为的“。” 运算符等效于获取结构的地址,通过成员的偏移量对其进行索引,并将其转换为成员类型的指针,并取消引用它。 但是,标准规定,有些情况不能保证有效。 例如,给定:

 struct s1 {int x,y; } struct s2 {int x,y; } void test1(struct s1 *p1, struct s2 *p2) { s1->x++; s2->x^=1; s1->x--; s2->x^=1; } 

编译器可能会认为没有合法的方式p1-> x和p2-> x可以识别同一个对象,所以它可能会重新排序代码,以便对s1-> x取消的++和 – 操作,以及^ = 1对s2-> x取消的操作,从而留下一个什么都不做的函数。

请注意,使用联合时行为是不同的,因为给定:

 union u { struct s1 v1; struct s2 v2; }; void test2(union u *uv) { u->v1.x^=1; u->v2.x++; u->v1.x^=1; u->v2.x--; } 

common-initial-subsequence规则表示由于u-> v1和u-> v2以相同类型的字段开头,因此访问u-> v1中的这样一个字段相当于访问u-中的相应字段> V2。 因此,不允许编译器重新排序。 另一方面,给定

 void test1(struct s1 *p1, struct s2 *p2); void test3(union u *uv) { test1(&(u.v1), &(u.v2)); } 

u.v1和u.v2以匹配字段开头这一事实并不能防止编译器假设指针不会出现别名。

请注意,某些编译器提供强制生成代码的选项,其中成员访问的行为始终与上述指针操作相同。 对于gcc,选项是-fno-strict-alias 。 如果代码需要访问不同结构类型的公共初始成员,则省略该开关可能会导致一个人的代码以奇怪,奇怪和不可预测的方式失败。