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.
// Can also entirely omit assigning `point_st` nametypedef struct point_st { int x; int y;} Point;int main() { // Now you can do this: Point pt = { 0, 0 }; // Instead of having to do this every time: struct point_st pt = { 1, 2 };}
Memory alignment
C performs two kinds of Memory alignment for structs. They both may insert padding which increases the struct size, so be cautious of always declaring the most optimal struct size.
Padding between member fields
Padding may be added between member fields in a struct to allow each field to be aligned with its Data type. For instance, take this struct:
Assuming that sizeof(size_t) == 4, packing the struct would cause size to start on the 8+8+1=17th byte of the struct, which is not a multiple of 4.
Instead, the compiler usually will add 3 bytes of padding in between allocated and size to make sure that size starts on the 20th byte instead.
Aligning based on biggest member
C structs will also try to be a multiple of its biggest member. From the example above, since the biggest member is sizeof(alloc_info*) == 8, C will try to make sure that the size of alloc_info is a multiple of 8.
Therefore, if C didn’t align based on member field data types like above, it would still have added 3 bytes of padding to the end of the struct so that it is 24 bytes.
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.
Another fun fact: this is why the syntax 5[arr] also works. Because this simply translates to *(5 + arr), essentially doing the same thing as arr[5].
Miscellaneous
sizeof() takes a variable or a type and it evaluates to the size of that type in bytes. It is evaluated at compile time.
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
sizeof() also counts the null terminator byte e.g. sizeof("hello") returns 6.
typeof() is also a compile time operator that was officially added in C23 (but was a GNU extension for a long time).
More interestingly, because it’s compile time, typeof() and sizeof() are one of the only times that we can dereference a null pointer.
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.