Example 1
Your example 1 doesn't work because no student object is ever created.
student* s;
This creates a pointer s that is supposed to point to a student but currently points to an unknown memory location because it is an uninitialized variable. It definitely doesn't point to a new student, since none was created so far.
s->age = 24;
This then writes to the unknown memory location that s is currently pointing to, corrupting memory in the process. You now entered the realm of undefined behavior (UB). Your process may crash at this very moment, or later on, or it may do something crazy and unexpected.
It doesn't make sense to think about what happens after this point, because your process is already doomed by now and needs to be terminated.
Example 2
Your example 2 sort-of works but only sometimes, because yet again UB comes into play.
student s2;
Here you are creating a student as a local variable. It is probably created on the stack. The variable is valid until you leave the function create_student.
However, you are then creating a pointer to that student object and returning it from your function. That means, the outer code now has a pointer to a place where a student object once was, but since you returned from the function and it was a local variable, it no longer exists! Sort of, that is. It's a zombie. Or, better explained, it's like when you delete a file on your harddisk - as long as no other file overwrote its location on the disk, you may still restore it. And therefore, out of sheer luck, you are able to read the age of it even after create_student had returned. But as soon as you change the scenario a bit (by inserting another printf), you ran out of luck, and the printf call uses its own local variables which overwrite your student object on the stack. Oops. That is because using a pointer to an object that no longer exists is also undefined behavior (UB).
Example 3
This example works. And it is stable, it doesn't have undefined behavior (almost - see below). That is because you create the student on the heap instead of the stack, with malloc. That means it now exists for eternity (or until you call free on it), and won't get discarded when your function returns. Therefore it is valid to pass a pointer to it around and access it from another place.
Just one small issue with that - what if malloc failed, for example you ran out of memory? In that case we have a problem yet again. So you should add a check for whether malloc returned NULL and handle the error somehow. Otherwise, s->age = 24 will attempt to dereference a null pointer which again won't work.
However, you should also remember to free it when you are done using it, otherwise you have a memory leak. And after you did that, remember that now your pointer did become invalid and you can no longer use it, otherwise we are back in UB world.
As for your question when to use malloc: Basically whenever you need to create something that lives on after you leave your current scope, or when something is local but has to be quite big (because stack space is limited), or when something has to be of variable size (because you can pass the desired size as argument to malloc).
One last thing to note though: Your example 3 worked because your student only had one field age and you initialized that field to 24 before you read it again. This means all the fields (since it had only that one) got initialized. If it had had another field (say, name) and you hadn't initialized that one, you would still have carried an "UB time bomb" if your code elsewhere would have attempted to read that uninitialized name. So, always make sure all fields get initialized. You can also use calloc instead of malloc to get the memory filled with zeroes before it's passed to you, then you can be sure that you have a predictable state and it is no longer undefined.
student* s;what valid memory doesspoint to? (what address does it hold as its value) 2.s2is local to the function and is destroyed (invalidated) on function return. Back to 1.student *s = malloc (sizeof *s);then validate allocation succeedsif (!s) { /* handle error */ }3. works becausemallocreturns the beginning address to an allocated block of memory that is then stored as the value ofs(i.e.spoints to the allocated block) and you can use it until it is freed (or the program ends).reallocmore when the initially allocated block is filled. Or, you need more of something than will fit on the program stack, you can allocate or declare asstatic(or declare globally). Otherwise, if you know how many you need beforehand, and that will fit on the stack, just declare an array of them and you are done.student *create_student(){ student* s;this pointer is not initialized to point to memory that the application owns. So writing to any fields, where that pointer points is undefined behavior and can lead to a seg fault event. Suggest changing to:student* s = malloc( sizeof( student );