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.

float* arr = malloc(10 * sizeof(float));
free(arr);
 
// Manually set `arr` to NULL because it still holds the value of an address,
// except it's now invalid to use it for anything
arr = NULL;

Structs

Setting one struct to another essentially copies every single field.

int main() {
    // Uninitialized, fields contain garbage values
    Point p1;
    Point p2 = { 1, 2};
 
    // This...
    p1 = p2;
    // ...is equivalent to
    p1.x = p2.x;
    p1.y = p2.y;
}

typedef structs

typedef structs allow you to use structs more easily when declaring new variables.

typedef struct point_st {
    int x;
    int y;
} Point;
 
int main() {
    // Now you can do this:
    Point pt = { 0, 0 };
    pt.x = 2;
    pt.y = 5;
 
    // Instead of having to do this every time:
    struct point_st pt = { 1, 2 };
}

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.

void some_function(Point* pt) {
    // Do work...
}
 
int main() {
    Point pt = { 1, 2 };
    some_function(&pt);
}

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. in int 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.

int core[3] = { 0, 0, 0 };
 
// `ptr` holds the address of the *second* element of the `core` array.
int* ptr = &(core[1]);
 
// Actually refers to core[1]
ptr[0] = 2;
 
// Actually refers to core[2]
ptr[1] = 3;
 
// Going out of bounds! Invalid! This is core[3]
ptr[2] = -1;

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 by 5 * 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 type struct 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 by ptr: sizeof(*ptr) tells us how big my_type is
  • 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() returns void*, the C standard specifies that automatic casting will occur to whatever pointer type is on the RHS.1
  • 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.

Footnotes

  1. Stack Overflow discussion