I am taking an open online course CS50 from Harvard. The last lecture I had was about memory allocation and pointers (two concepts which are absolutely new to me).
Syntactical considerations:
First of all, the types of c
and x
are different: The type of x
is what you expect char*
, while the type of c
is char[10]
, which is an array of ten character elements.
Thus, x
and c
cannot be fully equivalent: When you say x
, the compiler simply thinks of a single address of a single char
. However, when you say c
the compiler thinks about the entire array object with all its ten char
elements. Consequently, the code
printf("sizeof(x) = %zd\n", sizeof(x));
printf("sizeof(*x) = %zd\n", sizeof(*x));
printf("sizeof(c) = %zd\n", sizeof(c));
will print
sizeof(x) = 8
sizeof(*x) = 1
sizeof(c) = 10
on a 64 bit machine. sizeof(x)
gives the amount of bytes required to store an address, sizeof(*x)
gives the amount of bytes the pointer x
points to, and sizeof(c)
gives the amount of bytes required to store a complete array of ten char
elements.
So, why can I use c
pretty much anywhere where I can use x
in C?
The trick is called array-pointer decay: Whenever you use an array in a context where a pointer is expected, the compiler silently decays the array into a pointer to its first element. There are only two places in C, where you can actually use an array. The first is sizeof()
(which is the reason why sizeof(x) != sizeof(c)
), and the second is the address operator &
. In all other cases, any use of c
invokes the array-pointer decay. This includes stuff like c[3]
. This expression is defined to be equivalent to *(c+3)
, so the compiler decays the array c
into a pointer to its first element, then applies pointer arithmetic c+3
, and then dereferences the resulting pointer. Sounds complicated, is mind-boggling, but has the desired effect of accessing the fourth element of the array.
Anyway, with the syntactical considerations out of the way, lets look at the actual memory allocation:
malloc()
reserves a memory block of the given size, and that block remains valid until you call free()
on the pointer that malloc()
returned.
This is independent of the control flow in your program: A function can return the result of malloc()
to its caller and let the caller free it. Or it may pass the result of malloc()
to some other function which frees it. Or it may return the result to its caller, and the caller passes it to some other function to free it. Or the result may be stored in some other memory object for some time. And so on, and so forth. The possibilities are as varied as the source code that's being written around the world.
It must be stressed that it is a big error to use a pointer after freeing it. If you do that, it is possible that pink elephants appear and trample you to death, as far as the C standard is concerned. It is your job as the programmer to ensure that each malloc'ed pointer is free'd exactly once. If you fail to do so, well, all bets are off.
If you say
char c[10];
in file scope (outside of functions or struct
definitions), you are declaring a global variable. This array will exist from before main()
is called right unto the death of your process (either by returning from main()
, or by executing exit()
).
If you say
char c[10];
within a function, you are declaring a local variable. This array comes into existence when its declaration is executed, and ceases to exist at the end of the enclosing block (the part between a pair of braces {}
).
Thus, allocation and deallocation are strictly tied to your program's control flow.
If you say
char c[10];
within the definition of a struct
, you are declaring a member variable. This array will exist as long as the enclosing object (the struct
) exist. If the enclosing object is global, the arrays lifetime is that of a global, if the enclosing object is local, the arrays lifetime is that of a local, and if the enclosing object is a member of some other object, the arrays lifetime is that of the other object (this is recursive).
"char c[10];" reserves space either on the local stack or in a global data area that is created when the program is loaded, it is not heap memory as far as the program is concerned (to the OS it may be different).
When used in an expression an array decays to a pointer to its first element (this is how something like the following works, for example):
You cannot free an array declared with type var[size] syntax because it is either local to the current function or global to the entire program. Heap memory is not bound to any particular context in this way (the variables used to reference the heap memory are but not the pointed-to memory).
What was taught is that
malloc(10*sizeof(char))
allocates enough bytes on the heap to store 10 characters and returns a pointer to the first byte which can be saved in another variable as followschar *x = malloc(10*sizeof(char))
. To free the memory one would usefree(x)
.
"On the heap" is an implementation concept, not a C-language concept. The C language itself is not concerned with partitioning memory into separate areas with different characteristics, and in fact it is not necessarily the case that any given C implementation in fact does so.
Even in an introductory course -- maybe especially in an introductory course -- it is better to use C language concepts than concepts native to a certain implementation style. The relevant C language concept in this case is storage duration:
An object has a storage duration that determines its lifetime. There are four storage durations: static, thread, automatic, and allocated.
(C2011, 6.2.4/1)
The object allocated by your malloc()
call, (in)to which your pointer x
points, has "allocated" duration. That means its lifetime lasts until that object is freed by a call to free()
. Note well the distinction here between variable x
, a pointer with automatic storage duration, and the object to which x
initially points, an untyped object the size of 10 char
s.
There is (much) more, but this point in your journey is early for delving deeply into the standard. Nevertheless, I find this characterization more useful for addressing questions such as those you pose.
But there is another way to make a computer to reserve enough memory to store 10 characters i.e.
char c[10]
.
Yes, that's true.
- Is in the code snippet above c also a pointer of type
char*
?
No. Within the scope of that declaration, the identifier c
refers to an array of 10 char
s. There is a close relationship between arrays and pointers, but they are not at all the same thing. This is a crucially important point, and one over which many new C programmers stumble, so I repeat: arrays and pointers are not the same thing. The details would make for a whole other answer, however, and one which you can already find several times over here on SO.
To put it another way, identifier c
designates one kind of thing to which x
's value could point, but remember that x
's (pointer) value is distinct from the object to which it points.
- Does
char c[10]
also reserve memory on the heap asmalloc
does?
If your declaration of c
appears inside a function then it declares an array with automatic storage duration. This means that the array's lifetime lasts until identifier c
goes out of scope. It is the implementation's concern where the storage for that array is located, but on an implementation that provides a heap / stack distinction, the storage would most likely be on the stack, not the heap.
- Are the ways to allocate memory equivalent?
No. malloc()
allocates an object with allocated storage duration, whose lifetime the program is responsible for managing explicitly. The other allocates an object with automatic storage duration, whose lifetime is determined by the identifier's scope.
char c[3] = "aaa";free(c);
returns a runtime error; so it seems I can not free the memory I have allocated with charc[3]
. Why is that?
Most directly, it is because the specifications for the free()
function explicitly say
[I]f the argument does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to free or realloc, the behavior is undefined.
(C2011, 7.22.3.3/2)
That is, the standard does not require a runtime error (or any particular other behavior) if you try to free a pointer to an object with automatic duration, but it explicitly disclaims any promise that you can free memory that way.
But a more satisfying answer, I think, is that free()
is how you mark the end of the lifetime of an object with allocated storage duration, not one with automatic (or other) duration. Where the storage for the object is located (e.g. stack vs. heap) is ancillary.
- Is in the code snippet above c also a pointer of type char*?
No it is not. It is an array of ten char
.
However, the name of an array can, when used in a context where a pointer is expected, be converted to a pointer, and therefore effectively used as if it is a pointer.
- Does char c[10] also reserve memory on the heap as malloc does?
No. Heap and stack are not completely accurate terms either, but I won't expand on that further.
What malloc()
does is called "dynamic memory allocation" according to the standard.
The behaviour of char c[10];
depends on context.
{}
) it creates an array of automatic storage duration. That array ceases to exist, as far as your program is concerned, when the scope is exited (e.g. if the function returns).
- Are the ways to allocate memory equivalent?
Nope.
char c[3] = "aaa";free(c); returns a runtime error; so it seems I can not free the memory I have allocated with char c[3]. Why is that?
Because free()
only has defined behaviour when passed a pointer to dynamically allocated memory - i.e. returned by malloc()
, calloc()
, or realloc()
or a NULL
pointer (which causes free()
to do nothing).
c
is an array of either static or automatic storage duration, depending on context, as I mentioned above. It is not dynamically allocated, so passing it to free()
gives undefined behaviour. A common symptom of that is a runtime error, but not the only possible symptom.