This is a confusion of names.
node == struct data thanks to typedef .
But node ! = struct node . These are two different types. And the struct node not defined.
It is a pity that such an intricate construction is allowed by the C standard and is not even marked as a warning by compilers. But the way C is defined today, so we must live with it.
My recommendation: do not use the same names for struct and typedef . Create your own convention, for example struct thing_s and typedef struct thing_s thing_t; . This avoids confusing names such as this.
The solution is now pretty obvious. Replace:
typedef struct data { int num; struct node *link; } node;
typedef struct data { int num; struct data *link; } node;
and printf problem will now work.
But your question is: why does your program work at all without this printf ?
Now that is interesting.
Back to the original definition. As we already said, the struct node does not exist and, therefore, is incomplete. To better follow what we will explain, let's call it struct incomplete_s . Now node becomes:
typedef struct data { int num; struct incomplete_s *link; } node;
It will work printf no problem.
The reason is because node correctly defined. This is a structure with known sizes and types. Because it does not matter that struct incomplete_s is incomplete, since link is defined as a pointer to it. You could also define void * link; and it will still work.
void* or struct incomplete_s * or whatever * are the same size: they are all pointers. Thus, the structure of their placement can be correctly created.
In your main loop, your program:
while(e!=NULL) { printf("%d\n",e->num); e=e->link; }
that is, e , which is a pointer to node* , takes the value e->link , which is a pointer to struct incomplete_s * .
Please note that both pointers must point to different types. But they are both pointers, so yes, this assignment is technically possible and allowed by the standard.
Now a more careful compiler will most likely throw a warning here, since you shouldn't mix pointers of different types. This is a quiet casting type that is a recipe for future mistakes. I don’t know which compiler you are using, but you can increase its warning level (use "warning level 4" for Visual or -Wall -Wextra for gcc), and this probably will not like this operation = .
More explicit type casting will solve this (*): e = (node*)(e->link);
Now it is no more quiet, the programmer answers, the warnings disappear.
e->link definitely exists, so the compiler can capture this value. But s->link->num does not exist, since s->link is struct incomplete_s* , and we don’t know what it is, so we don’t know if it has a num member.
(*) Overkill add-on: at some higher level of optimization, it will still be inconvenient: a strict alias can interfere. Therefore, dereferencing a pointer type in another pointer type remains a dangerous operation.