Memory allocation
There are three types of memory allocation that a program can do.
- We can statically allocate memory by initializing variables in the global scope. Memory is allocated when the program starts, and deallocated when it exits.
- Automatic allocation occurs when we initialize local variables, like inside a function. These are pushed to the stack, and deallocated when the function returns.
Dynamic memory allocation
Dynamic allocation occurs at runtime during program execution, and requires us to manually manage the memory that we request. (By freeing it at the end.)
free()
Deallocates the memory pointed to by the pointer we pass in. The pointer must point to the first byte of heap-allocated memory (i.e. the pointer returned by malloc()
).
Defensive programming
The actual value of the pointer we just freed is not modified after calling free()
. Therefore, we can be a bit defensive and manually set the pointer to NULL
after freeing it.
Structs
Setting one struct to another essentially copies every single field.
typedef
structs
typedef
structs allow you to use structs more easily when declaring new variables.
Pass by value or reference
C does pass by value by default. To emulate passing by a reference, we can have a function parameter take in a pointer to the variable, and use the address-of operator &
when calling the function and the variable we want to take a reference of.
Arrays
- No bounds checking
- Can initialize using
= { 1, 2, 3 }
syntax. This syntax also allows us to leave out explicitly stating how many elements the array will have, e.g. inint arr[5]
the “5” can be left out. - Values in an array are contiguous in memory, guaranteed in C
You can have a pointer to a C array. When you do, it points to the first element of the array. We can use index []
syntax on the pointer to access different array values.
Because of arrays decaying to pointers when passed into the function, we must also pass in the size of the array to know about bounds.
Strings
- Arrays of characters
- Special character at the end to denote end of string:
\0
- Length of the array includes the null terminator
- String literals automatically include the null terminator at the end. So
char* str = "Hello";
would automatically include the\0
at the end.
Pointer arithmetic
Incrementing a pointer (e.g. via ++
operator) adds by whatever sizeof(datatype)
returns, not just “one” as one would expect when incrementing an integer.
This is actually how arrays work under the hood. When we do
arr[5]
, for example, we’re actually just writing*(arr + 5)
. We first skip ahead in memory by5 * sizeof(arr_type)
, and then dereference the pointer to access the element at that index.This is also why arrays are zero-indexed in many, many languages. I assume it’s because C started the trend, but writing
arr[0]
translates to dereferencing the element at an offset of zero from the beginning, aka the first element.
Miscellaneous
sizeof
takes a variable or a type and it evaluates to the size of that type in bytes.- Can ask for
sizeof(int)
,sizeof(double)
- Can input a variable
s
of typestruct my_struct
like so:sizeof(s)
, allowing us to get the size of the struct - We can dereference a pointer
my_type* ptr
and get the size of the type pointed to byptr
:sizeof(*ptr)
tells us how bigmy_type
is
- Can ask for
void*
is a “generic” pointer. It holds an address, but doesn’t specify what it’s pointing at.- Attempting to deference
void*
may not work. Therefore we have to cast it to an actual type before we can use it. - Note that while
malloc()
returnsvoid*
, the C standard specifies that automatic casting will occur to whatever pointer type is on the RHS.1
- Attempting to deference
static
function variables are variables that are private to the function, but its value is preserved across calls to the function. Essentially, it allows the function to remember things.