Programming/C++/Headers and File Including: Difference between revisions

From Dev Wiki
Jump to navigation Jump to search
(Add inline function note)
(No difference)

Revision as of 03:57, 21 April 2023

C Program Compilation Process

To understand anything else on this page, we first must go over how a C program compiles.

Step 1 - Pre-Processor & Compiler

First, a "pre-processor" runs on every file within the given C program. This pre-processor manipulates code in preparation for the compiler. For example, it:

  • Removes all code comments, since those are meant for the programmers, and ignored by the system anyways during program runtime.
  • Finds all defined macros in code, and changes them to the equivalent source code it's meant to represent.
  • Various other optimizations prior to actually running the compiler.
ToDo: Document C macros

Once the pre-processor is finished, the Compiler actually translates this code to a low-level Assembly code file (generally with the file extension .s).

By the time these steps are complete, we should have an equivalent assembly file for every file in the project. This assembly file is optimized to be as efficient as possible. See Programming/Compilers for more information on what a compiler actually does.

Step 2 - Assembler

Once the project is converted to assembly files, an Assembler reads these in, and further minimizes/optimizes them into even lower-level object files (generally with the file extension .o or .obj).

The difference between an assembly file and an object file is that assembly is a series of codes. While a pain, they technically can be read and understood by humans.

Meanwhile, object files go a step lower, and are pure binary. They are designed purely to be as efficient as possible for a processor to read, and are not human-readable.


Step 3 - Linker

A Linker then takes all of these generated object files and determines how they fit together as far as the overall program being compiled.

For example, any internal project files that are still separate at this point are combined. Any system libraries that are referenced in the code are pulled into memory, and then combined with these files. The end result is a final, single executable file, that represents the final result of the program.

Compilation is done.


Project Organization

Organizing Function Declarations

The C language is quirky in how functions have to be defined. Effectively, a given value must be defined ABOVE where it will be used. Like physically above, in the code.

So for example, int main() is the standard entry point for any given C/C++ project. If a given program were to be written all in a single file, then there are two options:

Option #1 (Naive solution) - Define all functions at the top, and main() at the bottom as the very last function.

<system library "include" headers here>

void my_func_1(...) {
   # Function logic here.
   ...
}

 void my_func_2(...) {
   # Function logic here.
   ...
}

...

 void my_func_n(...) {
   # Function logic here.
   ...
}

int main(...) {
    # Program entrypoint logic here.
    ...
}


Option #2 - Create Function Declarations at the top of the file, and define the functions lower in an organized manner:

<system library "include" headers here>


# Function declarations.
<type> my_func_1();
<type> my_func_2();
<type> my_func_3();


int main(...) {
    # Program entrypoint logic here.
    ...
}

<type> my_func_1(...) {
   # Function logic here.
   ...
}

 <type> my_func_2(...) {
   # Function logic here.
   ...
}

...

 <type> my_func_n(...) {
   # Function logic here.
   ...
}

Using Header Files

We can take the above Function Declarations logic a step further, by splitting code into separate files.

For example, we could do this:

 main.cpp 
/**
 * This is our main file.
 * It's still our entry-point into the program.
 * But we moved everything except "main()" into a separate file.
 */


// Imports.
# Include "my_file.hpp"

int main() {
    // Logic for main here, as normal. 

    ...
    
    utility_thing(some_int);
    bool return_val = test_function();

}
 my_file.hpp 
/**
 * This is the header file.
 * We only define constant variables or function declarations here.
 */


# Function declarations.
bool test_function();
void utility_thing(int some_int);
int something_else(std::string another_arg);
 my_file.cpp 
/**
 * This is our include file to go with the header file.
 * Everything declared in our header file is actually defined here.
 */


bool test_function() {
    // Function logic here.

    ...

    return some_bool;
}

void utility_thing(int some_int) {
    // Function logic here.

    ...
}

int something_else(std::string another_arg) {
    // Function logic here.

    ...

    return some_int;
}


Inline Functions

It's also possible to make Inline Functions. These are effectively functions declarations (like above), prefaced with the keyword inline in the header file.

What this does is tell the compiler to insert the function's literal body text into the code, basically as if it were a macro.

ToDo: Explain macros

Depending on the situation, this can result in better, or potentially worse performance. Generally the only reason it's done is for performance.