2024-11-05

c programming

subtopics

links

single compilation unit

with a single compilation unit, a single file is compiled that includes all necessary other code to build one binary object. the main file may have .h or .c suffix, the latter especially when it contains a main function and not merely declarations for a library. this can be the most simple way for small projects.

the more common way is to compile parts of the application separately into multiple objects and maintain makefiles as well as header files with declarations for each object and then use a linker to connect references between objects. this has benefits like encapsulation and saving time in the development of big projects by only recompiling objects that changed, but introduces significant overhead and may reduce optimization opportunities.

a popular project that uses as single compilation unit is sqlite.

the style of having one file including others reminds of including javascript in html.

tips and tricks, hints

  • structs

    • it is possible to use offsetof() to get a pointer to the beginning of a structure given only a pointer to a struct field
    • assignment of large structs copies all content automatically, even with nested array types, as long as the memory is declared as being part of the structure itself and not just a pointer
    • to set all struct members to zero '= {0}' can be used on initialization. this can be used to create a variable that can be used to set structs variables after initialization '= my_struct_null'
  • functions

    • consider setting output variables only on success
    • function calls are expressions, so they can be used to wrap more complicated syntax into an expression
    • for functions to accept a variable number of arguments, the preferred solution in c is often to define _n suffix variants that take a specific number of arguments. for example, list_3(2, 3, 4)
  • preprocessor

    • with #ifndef, files can be created that support macro variables set before they are included, where the macro variables only get defined inside if undefined. this can be used for configuration. the files can be included multiple times with different macro variable values set before
    • it is not usually possible to pass macro names to macros and construct other macro names from them
  • string literals are usually signed integers by default. -funsigned-char changes this
  • c floor is often slow
  • pattern: initialize variable to zero, allocate heap memory, final function cleanup checks if non-zero/allocated and frees if necessary
  • terminology for function parameters

    • parameter: formal parameter: the variables in a function declaration/definition. describes what arguments a function can take

    • argument: actual parameter: the values or variables passed during a function call

functions that work on memory buffers can work without modification on file content using memory maps

file_buffer = mmap(0, file_size, PROT_READ, MAP_SHARED, file_descriptor, 0);
md5((unsigned char*)file_buffer, file_size, result);
munmap(file_buffer, file_size);

mmap()

  • allocators like malloc use it internally as a low-level heap memory allocator on linux
  • can be used to map files to memory addresses. the file will be read in the operating systems page size blocks. overhead occurs, for example, when addresses outside the currently loaded pages are accessed

data structures

flat multidimensional array vs array of arrays

[1 2 3 1 2 3] vs [[1 2 3] [1 2 3]]
  • it is possible to allocate one long array with n elements representing a sub-array (also called stride), accessed either using custom index calculations or as an array type using multidimensional array indices. it is also possible to use an array of arrays to represent multidimensional arrays
  • flat

    • simpler to allocate because only one contiguous memory region is needed
    • sub-arrays are fixed size
    • example index calculations for accessing elements in a multidimensional array [d1][d2][d3] without using array type access syntax: "d1_index * (d2 * d3) + d2_index * d3 + d3_index"
    • it is also possible to store sub-arrays interleaved, for example like [1 1 2 2 3 3]. this changes the indexing calculation
  • nested

    • easier to access because sub-arrays can be iterated with a single incremented index, and without having to incorporate the sub-array size in the indexing calculation

    • easier to use with generic array operations, for example sorting

    • the data for sub-arrays has to be allocated separately and later freed

structs that store array data and array size

pro

  • no need declare additional size variable, as it is available with a single struct variable
  • no need to pass array size as an extra argument to functions, especially argument-saving when the function takes multiple arrays

con

  • overall it is not that useful to save a size argument here and there compared to the complexity of preparing the struct variable, conversions, and rewriting type-specific implementations
  • a separate size and pointer is easier to use for portions of arrays because of pointer arithmetic (+n at the point of being passed as a function argument, for example)
  • a single size variable can be used for multiple arrays of the same size
  • sometimes a count of items that are to be processed is to be passed to functions. the size value in the struct may be unnecessary in this case

struct padding

general alignment: each type aligns to a multiple of its size, with padding added between members and at the struct’s end to maintain alignment. mixed type cases:

  • small before large: placing a uint8_t before an int32_t adds padding after the uint8_t to align the int32_t.
  • descending order: ordering from largest to smallest (e.g., double, int32_t, uint8_t) minimizes internal padding.
  • uneven alignments: mixing types like int16_t, uint8_t, and float can create padding between members to satisfy each type’s alignment.
  • arrays in structs: arrays follow their base type’s alignment, potentially adding padding before the next member if needed.
  • end padding: the struct may be padded at the end to ensure its total size aligns with the largest member’s alignment, crucial for arrays of structs.

linked-lists

  • a typical implementation needs one allocation per addition, but it is also possible to manage the memory differently
  • insert and removal is simple because only the adjacent elements have to be modified, instead of shifting all elements as may be necessary when inserting or removing from an array

dynamic arrays

used size and total allocated size can be tracked separately. the total length of active elements can be reduced but elements can also be added up to the allocated size, and the allocated size can be automatically expanded, for example with realloc

pro

  • length-variable or length-modifying operations are easier. for example, generating a random number of elements or reducing the number of elements

there are multiple options for when to do resizing

  • option

    • define add functions and manage allocation size on each addition
    • possibly use add-n or ensure-n functions that reduce the number of necessary free space checks
    • addition can fail because of the possible allocation, so the error status has to be checked
  • option

    • manual ensure-n before usage, which resizes if necessary

    • no free space checks when adding

    • trying to add more than what was allocated for may lead to buffer overflows

c vs higher-level languages

the biggest slowdown i have experienced when programming in c versus other languages comes from having to be more specific:

  • specific with types - each function implementation works only for the exact type combinations it was defined for. functions that process uint32 and uint64 need two full separate definitions, float and uint functions may not be easily macro templated because of low-level details. macro templating with preprocessor syntax needs distracting line-escapes
  • distraction by interleaved memory allocation code, as well as thought overhead for ownership and cleanup
  • specific in what is done - more low-level options, more possible variation, more performance implications
  • more has to be done. for example, declaration, allocation, deallocation, initialization, etc, but this fine-grained control may be worth the cost