Skip to content

Latest commit

 

History

History
300 lines (201 loc) · 10.1 KB

4.MultiFile.md

File metadata and controls

300 lines (201 loc) · 10.1 KB

Lesson 4 Multi-file Programs

1. Include header files

#include with " " tells the preprocessor to look for the file in the same directory as the current file, - not in the usual set of directories where libraries are typically stored.

2. Preprocessor Directive ("include guards")

#ifndef HEADER_EXAMPLE_H // if not define this header yet
#define HEADER_EXAMPLE_H // then define this header as HEADER_EXAMPLE_H
...
#endif // end if

or

#pragma once // do the same thing in one line

3. Using multiple header files

  • Only the header file needs to be included in another file.

    The compiler can continue without error until it finds the definition of the function, regardless of where that definition is.

  • Some libraries, like <vector> are included in multiple files.

    Each file is compiled alone and must have all the declarations and libraries necessary to compile, so the necessary libraries must be included. This is another reason why include guards are important - if multiple headers were included in main, each with the same #include <vector> statement, you wouldn't want the vector header pasted multiple times into the code.

    Example flow:

    a.h -> b.h -> main.cpp

    a.cpp, b.cpp

4. CMake and Make

Object Files

  1. The preprocessor runs and executes any statement beginning with a hash symbol: #, such as #include statements. This ensures all code is in the correct location and ready to compile.
  2. Each file in the source code is compiled into an "object file" (a .o file). Object files are platform-specific machine code that will be used to create an executable.
  3. The object files are "linked" together to make a single executable.

It is possible to have g++ perform each of the steps separately by using the -c flag. For example,

g++ -c main.cpp

will produce a main.o file, and that file can be converted to an executable with

g++ main.o

But what if you make changes to your code and you need to re-compile? In that case, you can compile only the file that you changed, and you can use the existing object files from the unchanged source files for linking.

g++ -c main.cpp
g++ *.o
./a.out

CMake

  • open-source, platform-independent build system
  • There can be multiple CMakeLists.txt files in a project. In fact, one CMakeList.txt file can be included in each directory of the project, indicating how the files in that directory should be built.
  • CMakeList.txt:
    • Specify the locations of necessary packages
    • Set build flags and environment variables
    • Specify build target names and locations, and other actions.

Step 1: CMakeList.txt

cmake_minimum_required(VERSION 3.5.1)

set(CMAKE_CXX_STANDARD 14)

project(<your_project_name>)

add_executable(your_executable_name  path_to_file_1  path_to_file_2 ...)

Step 2: Make file

In terminal,

mkdir build
cd build
cmake .. # directs the cmake command at the top-level CMakeLists.txt file with '..'
make # make finds the Makefile to build the project
# Changes of source code
make # run 'make' again to compile the changed files

5. Pointers

The symbols & and * have a different meaning, depending on which side of an equation they appear.

This is extremely important to remember. For the & symbol, if it appears on the left side of an equation (e.g. when declaring a variable), it means that the variable is declared as a reference. If the & appears on the right side of an equation, or before a previously defined variable, it is used to return a memory address, as in the example above.

#include <iostream>
using std::cout;

int main() {
    int i = 5;
    // A pointer pointer_to_i is declared and initialized to the address of i.
    int* pointer_to_i = &i;
    
    // Print the memory addresses of i and j
    cout << "The address of i is:          " << &i << "\n";
    cout << "The variable pointer_to_i is: " << pointer_to_i << "\n";
    
    // The value of i is changed.
    i = 7;
    cout << "The new value of the variable i is                     : " << i << "\n";
    cout << "The value of the variable pointed to by pointer_to_i is: " << *pointer_to_i << "\n";
}

output:

The address of i is:          0x7ffc2134883c
The variable pointer_to_i is: 0x7ffc2134883c
The new value of the variable i is                     : 7
The value of the variable pointed to by pointer_to_i is: 7
  • If you've created a pointer to v, say pv, you can retrieve v with (*pv).

Passing Pointers to a Function

Another form of pass-by-reference but more troublesome because in the function for pass-by-reference, a pointer must be dereferenced in order to access the underlying object. Moreover, it is dangerous if the variable that being pointed is out of scope after the function finished execution.

#include <iostream>
using std::cout;

void AddOne(int* j)
{
    // Dereference the pointer and increment the int being pointed to.
    (*j)++;
}

int main() 
{
    int i = 1;
    cout << "The value of i is: " << i << "\n";
    
    // Declare a pointer to i:
    int* pi = &i;
    AddOne(pi);
    cout << "The value of i is now: " << i << "\n";
}

Returning a Pointer from a Function

#include <iostream>
using std::cout;

int* AddOne(int& j) 
{
    // Increment the referenced int and return the
    // address of j.
    j++;
    return &j;
}

int main() 
{
    int i = 1;
    cout << "The value of i is: " << i << "\n";
    
    // Declare a pointer and initialize to the value
    // returned by AddOne:
    int* my_pointer = AddOne(i);
    cout << "The value of i is now: " << i << "\n";
    cout << "The value of the int pointed to by my_pointer is: " << *my_pointer << "\n";
}

References vs Pointers

References Pointers
References must be initialized when they are declared. This means that a reference will always point to data that was intentionally assigned to it. Pointers can be declared without being initialized, which is dangerous. If this happens mistakenly, the pointer could be pointing to an arbitrary address in memory, and the data associated with that address could be meaningless, leading to undefined behavior and difficult-to-resolve bugs.
References can not be null. This means that a reference should point to meaningful data in the program. Pointers can be null. In fact, if a pointer is not initialized immediately, it is often best practice to initialize to nullptr, a special type which indicates that the pointer is null.
When used in a function for pass-by-reference, the reference can be used just as a variable of the same type would be. When used in a function for pass-by-reference, a pointer must be dereferenced in order to access the underlying object.

References are generally easier and safer than pointers. As a decent rule of thumb, references should be used in place of pointers when possible.

However, there are times when it is not possible to use references. One example is object initialization. You might like one object to store a reference to another object. However, if the other object is not yet available when the first object is created, then the first object will need to use a pointer, not a reference, since a reference cannot be null. The reference could only be initialized once the other object is created.

6. Maps

A map (alternatively hash table, hash map, or dictionary) is a data structure that uses key/value pairs to store data, and provides efficient lookup and insertion of the data.

  • unordered_map is the C++ standard library implementation of a map.

  • If the key does not exist in the map, then .find() returns an unordered_map::end() type. Otherwise, .find() returns a C++ iterator, which is a pointer that points to the beginning of the iterable key-value pair.

7. Classes and Object-Oriented Programming

#include <iostream>
#include <string>
using std::string;
using std::cout;

class Car {
  public:
    void PrintCarData() 
    {
        cout << "The distance that the " << color << " car " << number << " has traveled is: " << distance << "\n";
    }

    void IncrementDistance() 
    {
        distance++;
    }
    
    // Adding a constructor here:
    Car(string c, int n) 
    {
        // Setting the class attributes with
        // The values passed into the constructor.
        color = c;
        number = n;
    }
    
    string color;
    int distance = 0;
    int number;
};

int main() 
{
    // Create class instances for each car.
    Car car_1 = Car("green", 1);
    Car car_2 = Car("red", 2);
    Car car_3 = Car("blue", 3);

    // Increment car_1's position by 1.
    car_1.IncrementDistance();

    // Print out the position and color of each car.
    car_1.PrintCarData();
    car_2.PrintCarData();
    car_3.PrintCarData();
}

Inheritance

class Sedan : public Car {
    // Sedan class declarations/definitions here.
};

Use initializer list in Constructor

Car(string c, int n) : color(c), number(n) {}

Here, the class members are initialized before the body of the constructor (which is now empty). Initializer lists are a quick way to initialize many class attributes in the constructor. Additionally, the compiler treats attributes initialized in the list slightly differently than if they are initialized in the constructor body. For reasons beyond the scope of this course, if a class attribute is a reference, it must be initialized using an initializer list.

new Operator

The new operator allocates memory on the "heap" for a new Object. In general, this memory must be manually managed (deallocated) to avoid memory leaks in your program.