I have a question regarding structs in C. So when you create a struct, you are essentially defining the framework of a block of memory. Thus when you create an instance of a str
The dot operator simply selects the member.
Since the compiler has information about the type (and consequently size) of the member (all members, actually), it knows the offset of the member from the start of the struct and can generate appropriate instructions. It may generate a base+offset access, but it also may access the member directly (or even have it cached in a register). The compiler has all those options since it has all the necessary information at compile time.
If it hasn't, like for incomplete types, you'll get a compile-time error.
When you use the .
operator, the compiler translates this to an offset inside the struct
, based on the size of the fields (and padding) that precede it.
For example:
struct Car {
char model[52];
int doors;
int GasMilage;
};
Assuming an int
is 4 bytes and no padding, the offset of model
is 0
, the offset of doors
is 52
, and the offset of GasMilage
is 56.
So if you know the offset of the member, you could get a pointer to it like this:
int *GasMileagePointer = (int*)((char *)&carInstance + offsetInBytes(GasMile));
The cast to char *
is necessary so that pointer arithmetic goes 1 byte at a time instead of 1 sizeof(carInstance)
at a time. Then the result needs to be casted to the correct pointer type, in this case int *
When it works, the "." behavior of the "." operator is equivalent to taking the address of the structure, indexing it by the offset of the member, and converting that to a pointer of the member type, and dereferencing it. The Standard, however, provides that there are situations where that isn't guaranteed to work. For example, given:
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;
}
a compiler may decide that there's no legitimate way that p1->x and p2->x can identify the same object, so it may reorder the code so as to the ++ and -- operations on s1->x cancel, and the ^=1 operations on s2->x cancel, thus leaving a function that does nothing.
Note that the behavior is different when using unions, since given:
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--;
}
the common-initial-subsequence rule indicates that since u->v1 and u->v2 start with fields of the same types, an access to such a field in u->v1 is equivalent to an access to the corresponding field in u->v2. Thus, a compiler is not allowed to resequence things. On the other hand, given
void test1(struct s1 *p1, struct s2 *p2);
void test3(union u *uv)
{
test1(&(u.v1), &(u.v2));
}
the fact that u.v1 and u.v2 start with matching fields doesn't guard against a compiler's assumption that the pointers won't alias.
Note that some compilers offer an option to force generation of code where
member accesses always behave equivalent to the aforementioned pointer
operations. For gcc, the option is -fno-strict-alias
. If code will need
to access common initial members of varying structure types, omitting that
switch may cause one's code to fail in weird, bizarre, and unpredictable
ways.
Yes, the dot operator simply applies an offset from the base of the structure, and then accesses the value at that address.
int x = CarInstance.GasMileage;
is equivalent to:
int x = *(int *)((char*)&CarInstance + offsetof(Car, GasMileage));
For a member with some other type T
, the only difference is that the cast (int *)
becomes (T *)
.