C 结构点运算符到底是做啥的(低级视角)?
Posted
技术标签:
【中文标题】C 结构点运算符到底是做啥的(低级视角)?【英文标题】:What exactly does the C Structure Dot Operator Do (Lower Level Perspective)?C 结构点运算符到底是做什么的(低级视角)? 【发布时间】:2016-02-23 21:56:34 【问题描述】:我有一个关于 C 中的结构的问题。因此,当您创建一个结构时,您实际上是在定义一块内存的框架。因此,当您创建结构的实例时,您正在创建一个内存块,以便它能够容纳一定数量的元素。
但是,我对点运算符的作用有些困惑。如果我有一个struct Car
并且有一个名为GasMileage
的成员(这是一个int
成员),我可以通过执行类似的操作来获得GasMileage
的值,
int x = CarInstance.GasMileage;
但是,我对这个点运算符的实际情况感到困惑。点运算符是否只是作为基地址的偏移量?它又是如何推断出它是一个 int 的呢?
我想我很好奇幕后发生了什么。是否可以通过做其他事情来引用GasMileage
?比如
int *GasMileagePointer = (&carInstance + offsetInBytes(GasMileage));
int x = *GasMileage
这只是我迅速编造出来的。我一直在努力寻找一个好的解释,但似乎没有什么比将点运算符视为魔术更能解释它了。
【问题讨论】:
How can I get/set a struct member by offset。至少解决您问题的第二部分。 也许一本关于编译器构造的书会是一本不错的读物。 @Olaf:您无需了解编译器构造即可了解.
运算符的语义。
编译器推断CarInstance.GasMileage
是int
,因为GasMileage
成员被定义为int
。
@KeithThompson:OP 要求提供实施细节。这是 CC 的一部分。此外,我预计这不会是他在这个领域可能遇到的唯一问题。所以打好基础是个好主意。
【参考方案1】:
当您使用 .
运算符时,编译器会将其转换为 struct
内的偏移量,具体取决于它前面的字段(和填充)的大小。
例如:
struct Car
char model[52];
int doors;
int GasMilage;
;
假设int
为4字节且无填充,则model
的偏移量为0
,doors
的偏移量为52
,GasMilage
的偏移量为56。
所以如果你知道成员的偏移量,你可以像这样得到一个指向它的指针:
int *GasMileagePointer = (int*)((char *)&carInstance + offsetInBytes(GasMile));
强制转换为char *
是必要的,这样指针算术一次只处理1 个字节,而不是一次处理1 个sizeof(carInstance)
。然后需要将结果转换为正确的指针类型,在本例中为int *
【讨论】:
【参考方案2】:是的,点运算符只是从结构的基址应用一个偏移量,然后访问该地址处的值。
int x = CarInstance.GasMileage;
相当于:
int x = *(int *)((char*)&CarInstance + offsetof(Car, GasMileage));
对于具有其他类型T
的成员,唯一的区别是演员(int *)
变为(T *)
。
【讨论】:
【参考方案3】:点运算符只是选择成员。
由于编译器有关于成员(实际上是所有成员)的 type(以及因此 size)的信息,它知道该成员从一开始的偏移量结构,并且可以生成适当的指令。它可能会生成一个 base+offset 访问,但它也可以直接访问该成员(甚至将其缓存在寄存器中)。编译器拥有所有这些选项,因为它在编译时拥有所有必要的信息。
如果没有,例如 不完整类型,您将收到编译时错误。
【讨论】:
【参考方案4】:当它起作用时,“。” “。”的行为运算符相当于获取结构的地址,通过成员的偏移量对其进行索引,并将其转换为成员类型的指针,然后取消引用。但是,该标准规定在某些情况下不能保证有效。例如,给定:
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 取消的操作,以及对 s2->x 取消的 ^=1 操作, 从而留下一个什么都不做的函数。
请注意,使用联合时的行为是不同的,因为已给出:
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
。如果代码需要
访问不同结构类型的常见初始成员,省略
switch 可能会导致一个人的代码以怪异、怪异和不可预测的方式失败
方式。
【讨论】:
以上是关于C 结构点运算符到底是做啥的(低级视角)?的主要内容,如果未能解决你的问题,请参考以下文章