Separating code files
In some C++ IDEs, you can right-click on the folder you want to create a file in and select Create header file/Create source file. However, you might need to make a new file and rename it to have the appropriate extension.
This chapter will teach you how to segment your code into files to improve organization, reusability, manageability, and compile speeds. You'll also learn how to give instructions to the compiler to change how your program compiles and how to make shortcuts using macros.
File types
We have already worked with Source files (".cpp" extension). There are also header files (".h" extension). Header files are designed to be included in other files.
Generally, you place declarations and data structures in header files, while the implementations go in source files. This is why header and source files often come in pairs (animals.h + animals.cpp).
You can include a header file with #include "yourfile.h". You use double quotes to look for the file to include in the same folder as your current file.
Folder navigation
You can navigate into a folder by using / in your include path:
main.cpp
#include "utilities/zoo_utilities.h"
zoo_utilities.h is inside the folder "utilities". utilities shares a folder with main.cpp.
You can navigate out of a folder with ../:
zoo_utilities.h
#include "../data structures/animals.h"
Header & Source files
Separating your code into header and source files improves organization and maintainability and has advantages for the compiler.
Source files compile into object files (.o/.obj) containing machine code. With these files, the compiler doesn't have to re-compile source files unless you change something.
The linker is the part of the compiler responsible for connecting the declarations in the header file to the implementations in the source file.
You can use functions declared in header files as long as they are defined somewhere. You don't need access to the source file where the functions are defined. The linker can resolve the references during compilation if you include the header file with the declaration in the source file containing the function definition.
If you declare print_animal_info(animal* a); in animals.h, and define it in animals.cpp, you only have to #include "animals.h" to use it in main.cpp or any other file. Note that animals.cpp has to include animals.h.
Preprocessing & Controlling the Compilation
Preprocessing makes changes to the code files before the compilation. #include is a preprocessor directive. Preprocessor directives give us a way of controlling the compilation process. #include tells the preprocessor to add the contents of a file where the directive appears.
Preprocessor directives are helpful but rarely needed beyond basics like #include. This chapter covers them primarily so you can understand code that uses them. They don't "unlock" new possibilities like the rest of section 1. I recommend prioritizing and practicing other chapters.
#define MACRO_NAME replacement
#define lets you create a macro. The preprocessor will replace any occurrences of MACRO_NAME with replacement.
#include <iostream>
#include EXAMPLE_1 "This is an example!\n"
int main() {
std::cout << EXAMPLE_1;
}
This is an example!
#if, #elif, else and #endif
The preprocessor has minimal logical capabilities. The #if directive checks an expression during compilation. If the expression has a non-zero value, the code until the next #elif, #else, or #endif is included in the compilation. You can think of this as conditionally commenting out code. Each #if directive needs a matching #endif.
The #elif and #else directives are the preprocessor equivalents of else if and else.
The logical preprocessor directives are almost exclusively used for simple state checking.
#define DEBUG_MODE 1
#if DEBUG_MODE
std::cout << "Debug mode is enabled." << std::endl;
#else
std::cout << "Debug mode is disabled." << std::endl;
#endif
Mixing preprocessing directives with your code makes it harder to read and maintain. I recommend avoiding directives like #if inside functions.
Preprocessing operators
Preprocessing directives don't support functions. However, they have some unique operators that you can use.
defined checks if a macro is defined. This can be used with #if to only compile a piece of code when a macro is defined.
#if defined(DEBUG)
std::cout << "Debug information here";
#endif
Useful dynamic macros
__LINE__, __FILE__, __DATE__, and __TIME__ offer unique information avaliable during compilation.
__LINE__: The current line number in the source code.
__FILE__: A string literal with the name of the current file.
__DATE__: A string literal with the date the file is compiled ("Mmm dd yyyy").
__TIME__: A string literal with the time a file is compiled ("hh:mm:ss").
Defined checks
The #ifdef MACRO and #ifndef MACRO directives check if a macro is defined.
#ifdef identifier is equivalent to #if defined(identifier)
#ifndef identifier is equivalent to #if !defined(identifier)
Both directives require a matching #endif.
The defined operator was introduced to c++ after #ifdef and #ifndef.
You can define a macro without specifying a replacement.
#define EXAMPLE_MACRO
#undef
#undef MACRO_NAME will undefine a macro. This means instances of your macro's name no longer get replaced. #undef is useful if you want to restrict the effect of a macro. You can also use it to affect #ifdef/#ifndef conditions.
Include guards
Including the same file more than once effectively declares functions and objects multiple times, leading to "multiple definition" errors. This would be a hassle to avoid when working on larger projects with more intricate dependencies. Luckily, there is a simple solution.
Include guards are there to "guard" a file from being included more than once. Using preprocessing directives, we can check if we have already included the file and ignore its contents if they are.
Previously, you would wrap the entire file in an #ifndef MACRO_NAME and then define the same macro inside. MACRO_NAME is undefined the first time you include the file, so the code is included. Now that MACRO_NAME is defined, the #ifndef MACRO_NAME makes the preprocessor remove everything in the file.
#pragma once is a modern version that tells the compiler to ignore the rest of the file if it is already included. Many IDE's will automatically add #pragma once when you create a header file.
The #pragma keyword provides special instructions to your compiler that are specific to the compiler. Most modern compilers support #pragma once.
Macros
You can use macros for code replacements and shortcuts. It isn't limited to constants.
#include <iostream>
#define output std::cout << "OUTPUT THIS!\n";
int main() {
output
}
The output inside int main intuitively seems wrong. It doesn't look like a function or a meaningful operation. It even lacks an ending semi-colon. However, since the preprocessor runs before the compiler, this isn't what the compiler sees. output is replaced by std::cout << "OUTPUT THIS!\n"; before the compiler gets the code.
Here is the code the compiler sees:
#include <iostream>
int main() {
std::cout << "OUTPUT THIS!\n";
}
Function macros
You can specify "parameters" for your macros. This won't make the macro act like a function, but it can insert your "arguments" into what it replaces.
#include <iostream>
#define MAX(x, y) ((x > y) ? (x) : (y))
int main() {
int var1 = 3;
float var2 = 4.5f;
std::cout << MAX(var1, var2);
}
#include <iostream>
int main() {
int var1 = 3;
float var2 = 4.5f;
std::cout << ((var1 > var2) ? (var1) : (var2));
}
MAX(5, 10) is replaced with (var1 > var2?(var1) : (var2)). The macro uses the ternary operator to give you the expression that evaluates as the highest.
Note: If you replace x or y with a function call, the max-function will be called 2 times.
Macro useage & Tricks
Macros, by definition, don't offer any unique functionality. All they do is replace things. They are good for saving time and ensuring consistency.
Macros are inherently unpredictable, and using them can make it harder to diagnose any issues with the code. You should only use them if they make your code significantly easier to read or use.
Macro Tricks
You can use the # operator in a macro to convert a variable name into a string.
#define TO_STRING(x) #x
std::cout << TO_STRING(Hello) << std::endl; // Outputs: Hello
You can use the ## operator to combine two tokens.
#define CONCAT(a, b) a ## b
int CONCAT(my, Variable) = 42; // Becomes int myVariable = 42;
std::cout << myVariable << std::endl; // Outputs: 42
You can also use this with specified variable extensions.
#define CREATE_DOUBLE(a) int a ## _double = a*2;
int var = 42;
CREATE_DOUBLE(var) // creates variable var_double and assigns it 84.
A macro will replace itself with everything following its name until the end of the line. You can use \ to continue your macro on the following line.
Use this when a macro gets lengthy to make it easier to digest.
Undefine macros when you're done with them to avoid misusing them later. If you aren't going to undefine it directly after use, you should name the macro something very specific.
Macros are terrible as constants and should only be used as a value to interact with other preprocessing directives. Beyond the fact that they are unpredictable, you can't scope it into a namespace or other grouping. You're better off using const or enum values for your constant values.
Communicating with the compiler
The#warning and #error directives generate custom compiler warnings and errors. They help alert developers to potential issues or enforce certain conditions during compilation.
Use these directives with #ifdef, #ifndef, and#if to create checks and alerts based on specific conditions.
#error will stop compilation. We use it when something is wrong to the extent of breaking the program if used.
// REQUIRED_HEADER_H is defined inside "required_header.h"
#ifndef REQUIRED_HEADER_H
#error "REQUIRED_HEADER_H is missing. Please include required_header.h"
#endif
// This checks the version of C++ used. Some libraries need functions from newer C++ versions to work.
#if __cplusplus < 201103L
#error "This code requires at least C++11."
#endif
#warning will allow compilation unless the compiler has instructions to stop compilation when encountering warnings. We use it to alert the programmer about code that isn't optimal but that will likely work.
#ifdef OLD_FEATURE
#warning "OLD_FEATURE is deprecated. Please use NEW_FEATURE instead."
#endif
GOOD JOB!
You finished a chapter.
This would be a good time to practice what you've just learned.
Last ChapterPracticeNext Chapter