C++ Notes

Started: 07 Jan 2025
Updated: 07 Jan 2025

C++ Fundamentals

Here, I follow the mental framework I discuss in fundamentals of programming . First is the familiazation of the different “objects” in C++. These are the main things you work with, the container of the information or data you want to work on. Then, a mastery of the function or mapping technique to work on these “objects”.

An important concern is how data flow in the function. This is central to developing a deeper understanding of C++. I will delve into this more.

The fundamental paradigms in C++: procedural, object-oriented, and generic programming.

Core Principles

The foundation of C++ rests on these key ideas:

A crucial aspect of C++ is its static typing, where the data type of a variable is known at compile time. This allows the compiler to catch errors early. Additionally, C++ provides developers with direct memory management capabilities, offering fine-grained control over how memory is used.

“Objects” in C++

  1. fundamental data types: int, double, char, bool
  2. compound types:
    • arrays: collections of elements of the same type stored in contiguous memory locations.
      • modern c++ object:
        • array: std::array
        • vector: std::vector
    • pointers: variables that store the memory address of another object.
    • references: an alias or another name for an existing object
  3. user-defined types: classes
    • a class is a blueprint for creating objects.
    • It defines a set of attributes (data members) and behaviors (member functions).
    • An object is an instance of a class.

Mappings/Functions

In C++, a function is a named block of code that performs a specific task. Functions are the primary mechanism for procedural programming and are essential for organizing and reusing code.

int add(int a, int b) {
  return a + b;
}

Interrelationship of Objects and Functions

The fundamental components of C++—objects and functions—are deeply intertwined. Their relationship forms the basis of how a C++ program is structured and operates.

Data Flow

When data flows in C++, the primary techniques involve how you pass data into and out of functions.

Passing Data to Functions: there are three main ways to pass data into a function:

  1. pass-by-value: copies the value of an argument into the function’s parameter.
    • Any changes made to the parameter inside the function do not affect the original argument.
  2. pass-by-reference (&): passes an alias (or reference) to the original argument, not a copy.
    • Any modifications made to the parameter inside the function will affect the original argument.
    • It’s highly efficient as it avoids making copies, making it ideal for large objects or when you intend to modify the original data.
    • Use when: You want to avoid a costly copy or when you need the function to change the original variable’s value.
void modifyReference(int& x) {
    x = 100; // The original variable is changed
}
int main() {
    int original = 10;
    modifyReference(original);
    // original is now 100
    return 0;
}
// The '&' makes it pass-by-reference.
void addElement(std::vector<int>& vec) {
    // This modifies the ORIGINAL vector.
    vec.push_back(100);
}

// const& provides efficient, safe, read-only access.
void printVector(const std::vector<int>& vec) {
    // for (int num : vec) { ... }
    // vec.push_back(100); // This would cause a COMPILE ERROR.
}
  1. pass-by-pointer (*): involves passing the memory address of an argument to the function.
    • Use when: You need to modify the original data or when you want to represent “no object” by passing a nullptr.
void modifyPointer(int* ptr) {
    if (ptr != nullptr) {
        *ptr = 100; // The data at the memory address is changed
    }
}
int main() {
    int original = 10;
    modifyPointer(&original);
    // original is now 100
    return 0;
}
// The function receives a pointer to the first element and the size.
void printArray(int arr[], int size) {
    for (int i = 0; i < size; ++i) {
        // This modifies the ORIGINAL array.
        arr[i] = arr[i] * 2;
    }
}

int main() {
    int myNumbers[] = {1, 2, 3, 4};
    int size = 4;

    printArray(myNumbers, size); // Pass the array

    // myNumbers is now {2, 4, 6, 8}
    return 0;
}

Getting Data from Functions: the most common way to get data back from a function is through its return value.

  1. Return by Value: The function returns a copy of a value to the caller. This is suitable for simple data types.
  2. Return by Reference or Pointer: For larger objects, returning by reference or pointer can be more efficient as it avoids copying. However, you must ensure the object being referred to does not go out of scope when the function ends.
  3. Output parameters: This involves passing an argument by reference or pointer, not for its input value, but so the function can place its result into it.

Data Flow in Objects: in object-oriented C++, data flow is often managed within an object itself.

class Account {
private:
    double balance;

public:
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount; // Internal data flow: method modifies the object's state
        }
    }

    double getBalance() const {
        return balance; // Data flows out of the object
    }
};

Object-Oriented Programming

The fundamentals of C++ object-oriented programming (OOP) are four core principles that help structure code around data and behavior: encapsulation, abstraction, inheritance, and polymorphism.

Encapsulation

Encapsulation is the bundling of data (attributes) and the methods (functions) that operate on that data into a single unit, called a class. It also involves restricting direct access to an object’s internal state, which is known as data hiding.

class Account {
private:
    double balance; // Data is hidden from the outside world

public:
    // Public methods provide controlled access
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
};

Abstraction

Abstraction means hiding complex implementation details and showing only the essential features of the object. It’s the result of encapsulation. The goal is to simplify a complex system by modeling classes appropriate to the problem.

Inheritance

Inheritance is a mechanism that allows a new class (the derived or child class) to acquire the properties and behaviors of an existing class (the base or parent class).

// Base class
class Vehicle {
public:
    void startEngine() { /* ... */ }
};

// Derived class inherits from Vehicle
class Car : public Vehicle {
public:
    void drive() { /* ... */ }
};

Polymorphism

Polymorphism, which means “many forms,” allows objects of different classes to be treated as objects of a common base class. The most common form in C++ is runtime polymorphism, achieved through virtual functions.

class Animal {
public:
    virtual void makeSound() { /* ... */ } // Virtual function
};
class Dog : public Animal {
public:
    void makeSound() override { /* Barks */ }
};
class Cat : public Animal {
public:
    void makeSound() override { /* Meows */ }
};

Other important details …

The constructor’s purpose is to initialize an object when it’s created, while the destructor’s purpose is to clean up resources when the object is destroyed. They work together to manage an object’s lifecycle automatically.

Constructor

A constructor is a special member function that is automatically called when an object of a class is created. Its primary job is to ensure the object is in a valid and usable state from the moment it exists.

PairVashishta::PairVashishta(LAMMPS *lmp) : Pair(lmp)
{
  single_enable = 0;
  restartinfo = 0;
  one_coeff = 1;
  manybody_flag = 1;
  centroidstressflag = CENTROID_NOTAVAIL;
  unit_convert_flag = utils::get_supported_conversions(utils::ENERGY);

  params = nullptr;

  r0max = 0.0;
  maxshort = 10;
  neighshort = nullptr;
}

Destructor

A destructor is a special member function, identified by a tilde (~) before the class name, that is automatically called just before an object is destroyed. Its purpose is to perform cleanup tasks.

PairVashishta::~PairVashishta()
{
  if (copymode) return;

  memory->destroy(params);
  memory->destroy(elem3param);

  if (allocated) {
    memory->destroy(setflag);
    memory->destroy(cutsq);
    memory->destroy(neighshort);
  }
}

This automatic calling of the constructor and destructor is a cornerstone of a powerful C++ concept called RAII (Resource Acquisition Is Initialization), which ensures that resources are properly managed throughout an object’s life.

Member access operator

Both . and -> are member access operators used to access the attributes and methods of a class object. The key difference is what you’re using to access them.

Dot operator (.): Use the dot operator when you are working directly with an object.

#include <string>

class Person {
public:
    std::string name;
};

int main() {
    Person p1;      // p1 is the actual object
    p1.name = "Alice"; // Use '.' to access the 'name' member of the object
    return 0;
}

Arrow operator (->): Use the arrow operator when you are working with a pointer to an object. It’s a convenient shorthand that dereferences the pointer (*) and then accesses the member (.).

#include <string>

class Person {
public:
    std::string name;
};

int main() {
    Person* p2 = new Person(); // p2 is a pointer to a Person object
    p2->name = "Bob";          // Use '->' to access 'name' through the pointer
    delete p2;
    return 0;
}

Generic Programming

Generic programming in C++ is built on the interplay of four key concepts, primarily found in the Standard Template Library (STL).

Basic Concepts

    #include <iostream>
    #include <string>

    // Template for a generic Box class
    template <typename T>
    class Box {
    private:
        T value;
    public:
        void set(T val) {
            value = val;
        }
        T get() {
            return value;
        }
    };

    int main() {
        // Create an object from the class template to hold an int
        Box<int> intBox;
        intBox.set(123);

        // Create another object to hold a string
        Box<std::string> stringBox;
        stringBox.set("Hello");

        std::cout << intBox.get() << std::endl;   // Outputs 123
        std::cout << stringBox.get() << std::endl; // Outputs "Hello"
        return 0;
    }
    

Compilation Stages

  1. preprocessing : the preprocessor scans the source code for directives, which are lines starting with a #. The most common directive, #include, instructs the preprocessor to insert the content of a specified file into the current file. Another key directive, #define, is used to create macros, which are essentially text replacements. The output of this stage is a single, expanded source code file.
  2. compilation : the compiler takes the preprocessed code and translates it into assembly language, which is a low-level language that is specific to the computer’s architecture. It then assembles this code into an object file, which contains machine code but is not yet a complete executable. These files typically have a .o or .obj extension.
  3. linking : the linker’s job is to take the object file(s) generated by the compiler and combine them with any necessary libraries to produce a final executable file. Libraries are collections of pre-compiled code that provide common functionalities.
g++ hello.cpp -o hello