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.GasMileageint,因为GasMileage 成员被定义为int @KeithThompson:OP 要求提供实施细节。这是 CC 的一部分。此外,我预计这不会是他在这个领域可能遇到的唯一问题。所以打好基础是个好主意。 【参考方案1】:

当您使用 . 运算符时,编译器会将其转换为 struct 内的偏移量,具体取决于它前面的字段(和填充)的大小。

例如:

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

假设int为4字节且无填充,则model的偏移量为0doors的偏移量为52GasMilage的偏移量为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 结构点运算符到底是做啥的(低级视角)?的主要内容,如果未能解决你的问题,请参考以下文章

~> 运算符是做啥的? [复制]

-> <- 运算符是做啥的?

使用 cout 和 cin 时,“<<”和“>>”运算符是做啥的,我们为啥要使用它们?

.join() 方法到底是做啥的?

.join() 方法到底是做啥的?

例如 %+% 是做啥的?在 R 中