In this answer, I will discuss each significant line.
int a = 6;
The presence of this line is not relevant to the meaning of the C code (that is, to what the C standard specifies about its behavior). If it affects the running program, it is likely just because it just happens to affect how the compiler arranges local variables in memory in some uncontrolled way (that is, in some way that was not particularly deliberately designed into the compiler). The fact that it affects the program is a distraction and is not very meaningful.
int i1 = 5;
Fine, that is a normal line.
printf("&i1 = %p\n", &i1);
This is technically wrong; it should be printf("&i1 = %p\n", (void *) &i1);
, because %p
is specified for use with void *
but not with other pointer types. However, it will not affect most C implementations.
size_t i1_address = (size_t) &i1;
size_t
is not guaranteed to hold all the information about a pointer. It is better to #include <stdint.h>
and use uintptr_t
instead of size_t
.
int *p = (int *) (i1_address + 4);
This assumes (we infer from context) the size of int
is 4 and that the result of converting &i1
to size_t
, adding 4, and converting to int *
yields a pointer to just beyond i1
. I presume the “cc” referred to in the question is some version of GCC, in which case this is sort of okay because GCC supports doing this sort of address arithmetic (I believe from memory, without looking up specific documentation).
printf("p = %p\n", p);
As above, this should be printf("p = %p\n", (void *) p);
.
*p = 12;
This is bad. p
is not pointing to a known object. In the computing model that the C standard uses, it is not pointing to an object at all, so the behavior of the expression *p
is not defined by the standard, and neither is assigning anything to it. Unlike some behaviors not defined by the C standard, such as some address arithmetic, GCC does not make any promises about this sort of abuse.
int i2;
Fine.
printf("&i2 = %p\n", &i2);
This should also be printf("&i2 = %p\n", (void *) &i2);
printf("i2 = %d\n", i2);
In the standard’s model, i2
is indeterminate because it has not been initialized (including by assignment). “Indeterminate” means not just that it does not have a particular value but that might not have any value at all in the sense of having a value that persists from use to use. While the value of i2
is indeterminate, the C standard permits each use of it to act as if it had a different value or trap representation. (In the absence of the prior statement, which contains &i2
, the use of i2
in this statement would have undefined behavior, due to a particular rule in the C standard that says using an uninitialized object with local storage duration that has not had its address taken has behavior not defined by the C standard. With the prior statement, there is merely an indeterminate value, not undefined behavior.)
To my knowledge, GCC on Ubuntu does not have trap representations for int
objects, so printf("i2 = %d\n", i2);
by itself would print some value for i2
. It is not undefined behavior, just not completely specified behavior. (However, since this statement is preceded by statements with undefined behavior, we do not know that program execution will ever reach this statement, and, if it does, the C standard does not tell us what will happen, because the prior undefined behavior makes the subsequent behavior also undefined.)
It is possible that *p = 12;
puts 12 in the space that is then used for i2
, and so printf("i2 = %d\n", i2);
might show 12 for i2
. Certainly the C standard does not require this in any way, but GCC might do that, and whether it does that or does not do that could be affected by whether the statements int a = 6;
or printf("i2 = %p\n", &i2)
are or are not present. Again, however, none of those variations in behavior from the presence or absence of the statements are very meaningful. A better way to learn how the compiler behaves is to examine the assembly language it generates with various variations in the source code and compiler switches. (With GCC, use -S
to generate assembly language.)
(One could learn more about the compiler’s behavior by reading the source code, but that is not better for many people because it requires a great deal more work to accumulate the knowledge required before the source code can be sensibility interpreted.)