Skip to content

Instantly share code, notes, and snippets.

@connorworley
Last active February 2, 2021 10:17
Show Gist options
  • Save connorworley/237ad106bc6c1307e547b017445485e3 to your computer and use it in GitHub Desktop.
Save connorworley/237ad106bc6c1307e547b017445485e3 to your computer and use it in GitHub Desktop.
C++ Cheatsheet

C++ Cheatsheet

Declarations

Declarations are a way of telling the compiler what the type of a name is. Names must be declared before they can be used. Declarations consist of a type followed by a name.

int numberOfApples;
// numberOfApples is an integer
std::string myName;
// myName is a string

Declarations can also tell the compiler about functions. Function declarations start with the return type of the function, followed by a name, followed by arguments types and names.

int RandomInteger();
// RandomInteger is a function that takes no arguments and returns an integer
void SetVoltage(float voltage);
// SetVoltage is a function that takes a float argument and returns nothing (void)
int AddIntegers(int a, int b);
// AddIntegers is a function that takes two integer arguments and returns an integer

Assignment

We can use the = sign to assign values to variables.

int numberOfApples;
numberOfApples = 7;

std::string myName;
myName = "John Doe";

Combined declaration and assignment

We can combine the syntax for declaration assignment to do both at the same time.

int numberOfApples = 7;
std::string myName = "John Doe";

Analogously, we can define the behavior of a function without needing to write a separate declaration.

int RandomInteger() {
    return 4; // https://xkcd.com/221
}

void SetVoltage(float voltage) {
    // do some magic
}

The auto type

We can use a special type called auto when using combined declaration and assignment syntax. The auto type will automatically deduce a type for a variable depending on what it is being assigned to it.

class RobotContainer {
    public:
        RobotContainer();
        static RobotContainer* GetInstance();
}

auto m_container = RobotContainer::GetInstance();
// The compiler will automatically decude that m_container is a RobotContainer*
// because the return type of RobotContainer::GetInstance is a RobotContainer*

Note that auto only works when declaring and assigning to a variable at the same time. The compiler needs to be able to deduce a type from the right side of the = sign.

We should only use auto in cases where the right side of the = sign has an obvious type so that it does not hinder readability.

Classes

Classes are user-defined types that combine variables and functions. Variables associated with a class are called member variables and functions associated with a class are called methods. Consider this example bank account class.

class BankAccount {
    public:
        BankAccount(float initialBalance) {
            m_balance = initialBalance;
        }
        
        void Deposit(float amount) {
            m_balance = m_balance + amount;
        }
        
        void Withdraw(float amount) {
            m_balance = m_balance - amount;
        }
        
        float GetBalance() {
            return m_balance;
        }
        
    private:
        float m_balance;
};

The class has one member variable: m_balanace. The class also has four methods: BankAccount, Deposit, Withdraw, and GetBalance. The first method is a special method called a constructor. We call the constructor in order to create a new object or instance of the class.

BankAccount myAccount = BankAccount(21.35);
// or
auto myAccount = BankAccount(21.35);
// or
BankAccount myAccount(21.35);

All of these lines would create a new BankAccount object with an initial balance of 21.35 by calling the constructor. The last style is the most succint, so we prefer it over the others.

Calling methods

Once we have an instance of a class, we can call its methods. We use the . operator to do this.

BankAccount myAccount(21.35);
myAccount.Withdraw(20);
float updatedBalance = myAccount.GetBalance();

public vs private

In the bank account example, all of the methods are defined within the public: section, while m_balanace member variable is defined within the private: section. This means that while all of the examples so far will compile, the following example will not.

BankAccount myAccount(21.35);
myAccount.m_balance = 1000000;

Private member variables can only be accessed from methods of the same instance. Similarly, private methods can only be called from methods of the same instance. In practice, this means that we can only interact with classes through their public methods and member variables. This helps prevent us from accidentally changing the internal state of an instance in an unexpected way. For example, if we were allowed to overwrite m_balance we could potentially bypass checks in the Withdraw method to ensure that account balances do not go below $0.

Split classes

We usually split classes into a .h (header) file and a .cpp file. This is not strictly required, but helps reduce compilation times. For the bank account example, this would look something like the following.

// BankAccount.h

class BankAccount {
    public:
        BankAccount(float initialBalance);
        void Deposit(float amount);
        void Withdraw(float amount);
        float GetBalance();
        
    private:
        float m_balance;
};
// BankAccount.cpp

#include "BankAccount.h"

BankAccount::BankAccount(float initialBalance) {
    m_balance = initialBalance;
}

void BankAccount::Deposit(float amount) {
    m_balance = m_balance + amount;
}

void BankAccount::Withdraw(float amount) {
    m_balance = m_balance - amount;
}

float BankAccount::GetBalance() {
    return m_balance;
}

We declare the class in BankAccount.h and define its methods in BankAccount.cpp.

Static class methods

Class methods marked as static can be called without going through an instance of the class. Essentially, the are plain old functions that are associated with a class that do not have access to non-static member variables and methods.

class RandomNumberUtilities {
    static int RandomInteger() {
        return 4;
    }
}

We call static methods using the class name followed by :: and the method name.

int randomInt = RandomNumberUtilities::RandomInteger();

Pointers

Pointers are types of variables that "point" to other variables. To declare a pointer, we write the type of the variable we want to point to followed by an asterisk (*) and a name.

int* myNumberPointer;

If we have a variable, we can use an ampersand (&) to obtain a pointer to it.

int numberOfApples = 7;
int numberOfOranges = 17;

int* foodCount = &numberOfApples;

// Suppose we want to change what foodCount points at
foodCount = &numberOfOranges;

Conversely, we can use an asterisk to dereference a pointer.

int numberOfApples = 6;
int* applesPointer = &numberOfApples;

*applesPointer = 7;
// numberOfApples is now 7

int numberOfApples2 = *applesPointer;
// numberofApples2 is now 7

We primarily use pointers with functions. Normally, calling a function creates a copy of each of its arguments. Consider the following example.

void Magic(int x) {
    x = 2135;
}

int myNumber = 1;
Magic(myNumber);
// myNumber is still 1

Calling the Magic function will not change the value of myNumber because its value is copied into the x argument. We can work around this behavior using pointers.

void Magic(int* x) {
    *x = 2135;
}

int myNumber = 1;
Magic(&myNumber);
// myNumber is now 2135

Calling the Magic function still creates a copy of the &myNumber argument, but a copy of a pointer will still point to the same place. We can derefernce this pointer to modify the variable that it points at.

Pointers to class instances

We commonly use pointers to point at class instances. Instance pointers have a special shorthand for calling methods that lets us avoid dereferencing them. Rather than using a . before the method name, we use ->.

BankAccount* GetMyBankAccount() {
    // magic goes here
}

auto myAccount = GetMyBankAccount();
float myBalance = myAccount->GetBalance();

Danger of pointers

Pointers do not necessarily have to point to a valid variable. Dereferencing a pointer that poins to an invalid location will cause the program to crash.

int* myNumberPointer = nullptr; // nullptr is not a valid location to dereference
int myNumber = *myNumberPointer; // This line causes the program to crash

References

References are a special type of pointer that can only point at other variables. Because of this requirement, references must always be declared and assigned at the same time. References are delcared similarly to pointers, but they use an ampersand instead of an asterisk.

int& myNumberReference; // This will not compile because it does not point at anything

int myNumber = 2135;
int& myNumberReference = myNumber; // This is OK

Note that we do not need to use an ampersand when setting myNumberReference to myNumber like we would have to for a normal pointer. We also do not need to use the -> syntax when calling methods on a reference of a class instance. References are transparent, meaning that they behave exactly like the referenced type.

BankAccount& GetMyBankAccount() {
    // magic goes here
}

auto myAccount = GetMyBankAccount();
float myBalance = myAccount.GetBalance();

Unlike normal pointers, references cannot be reassigned to reference a different underlying variable.

We should prefer to use references over normal pointers where possible, due to their improved safety and transparent ergonomics.

void Magic(int& x) {
    x = 2135;
}

int myNumber = 1;
Magic(myNumber);
// myNumber is now 2135
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment