Skip to content

Instantly share code, notes, and snippets.

@yannlandry
Last active February 10, 2022 01:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yannlandry/b7a57e733949019c3b4741b97d3752a1 to your computer and use it in GitHub Desktop.
Save yannlandry/b7a57e733949019c3b4741b97d3752a1 to your computer and use it in GitHub Desktop.
C++ study guide

C++ Study Guide

Types

Base types

(unsigned) char | short | int | long | long long | float | double

Explicit types

(we prefer to use those because the sign/bits are clear and the base types above vary across compilers)

int8_t  | uint8_t  // 1 byte  (char)
int16_t | uint16_t // 2 bytes (short)
int32_t | uint32_t // 4 bytes (int/long)
int64_t | uint64_t // 8 bytes (long long)

Also common:

size_t // usually alias of `uint64_t`, used for size/cursor, returned by `size()` methods on STL containers

We rarely use floating-point types but when we do (e.g. for video timestamps) we use double.

C-style strings

const char* str_a = "Hello World";
const char* str_b = "Hello World";
// "Hello World" is a string literal denoted by a `const char*` and since it shouldn't change str_a and str_b probably point to the same memory location

In the above examples, 12 bytes are set aside for the "Hello World" string literal; 11 for characters + one at the end for a null-terminator. When manually allocating C-style strings (rare but sometimes needed) that need to be printed out, it's good to allow an extra character and set it to 0 or '\0'.

When printing a const char* or char*, std::cout will print characters until it hits a null-terminator. If it's missing, lots of garbage/extra data may be printed (security issue!).

Delimiters

  • 'a' (single quotes) represents a single char
  • "a" (double quotes) represents a const char* (string literal) that only contains one character

Cannot interchange both!

Pointers

Thing* ptr             // pointer to Thing
Thing** ptr            // pointer to pointer to Thing
const Thing* ptr       // non-const pointer to a const Thing
Thing* const ptr       // const pointer to a non-const Thing
const Thing* const ptr // const pointer to a const Thing

(tip: read backwards)

const Thing* const ptr
/*
    ptr:   "ptr"
    const: "is a const"
    *:     "pointer"
    Thing: "to a Thing"
    const: "that is const"
*/

nullptr

nullptr is the C++ standard to denote NULL in regards to pointers.

Instantiate and delete

Thing* ptr = new Thing(); // create and call constructor
delete ptr;               // call destructor and deallocate
Thing* ptr = new Thing[n]; // create contiguous array of Thing instances, note the resulting pointer type doesn't change
delete [] ptr;             // delete contiguous array of Thing instances, size doesn't need to be provided
// both are equivalent
*(ptr + n)
ptr[n]

// also equivalent
*ptr
ptr[0]

"Smart" pointers

std::shared_ptr<Thing> ptr(new Thing()); // create Thing pointer and IMMEDIATELY wrap into shared_ptr
std::shared_ptr<Thing> ptr_2 = ptr;      // every subsequent shared_ptr to the Thing instance should be created from an existing shared_ptr
ptr.reset();                             // resets the shared_ptr to 0, when the last shared_ptr is destroyed/reassigned deletion is done automatically

WATCH OUT for:

Thing* ptr = new Thing();
std::shared_ptr<Thing> ptr_1(ptr);
std::shared_ptr<Thing> ptr_2(ptr);

This is VERY BAD because both ptr_1 and ptr_2 doesn't know the other exists. This is better:

std::shared_ptr<Thing> ptr_1(new Thing()); // better to just call `new` inside the call
std::shared_ptr<Thing> ptr_2(ptr_1);       // create ptr_2 from ptr_1

A shorthand for creating shared pointers:

std::shared_ptr<Thing> ptr = std::make_shared<Thing>(/* constructor args */);
auto ptr = std::make_shared<Thing>(/* constructor args */); // even shorter

// easy copy shared_ptr, 100% safe
auto ptr_copy = ptr;

Dereference:

auto ptr = std::make_shared<Thing>();
*ptr;            // access
ptr->method();   // arrow operator
(*ptr).method(); // same as arrow operator, but very hard to chain, generally never used

Verify if pointer is valid:

if (ptr.get() != nullptr) { /* ... */ }
/* or */ if (ptr) { /* ... */ } // shorthand

weak_ptr

Similar to shared_ptr but doesn't count against the reference count. Good to avoid circular references or for any time you don't need strong ownership.

Create from an existing shared_ptr:

auto ptr = std::make_shared<Thing>(); // the original shared_ptr
std::weak_ptr<Thing> weak(ptr);

To use:

std::shared_ptr<Thing> temp_ptr = weak.lock();
/* or */ auto temp_ptr = weak.lock();

temp_ptr is a shared_ptr to make sure the object stays alive until you're done with it.

Tip:

if (auto temp_ptr = weak.lock()) {
    // use temp shared_ptr and it will be discarded at the end of the if block
}
else {
    // lock() returns nullptr if the underlying thing doesn't exist anymore, if will fail and go to else
}

unique_ptr

Similar to std::shared_ptr but cannot be copied, unique ownership. Rarely used.

std::unique_ptr<Thing> unique(new Thing());
std::unique_ptr<Thing> unique_2(std::move(unique)); // must be moved, now the original pointer no longer owns the Thing

Casting

class A            { /* ... */ };
class B : public A { /* ... */ };
class C : public A { /* ... */ };
class D            { /* ... */ };

A* a = new A;
B* b = new B;
C* c = new C;
D* d = new D;

a = static_cast<A*>(b);      // cast from child to parent (upcast)
b = dynamic_cast<B*>(a);     // cast from parent to child (downcast)
c = dynamic_cast<C*>(a)      // C is child of A but A isn't of type C but rather B, so dynamic_cast returns nullptr
d = reinterpret_cast<D*>(a); // yolo i don't care about consequences; A-D aren't related, but this always works

With shared pointers, it's similar, except we use dynamic_pointer_cast and static_pointer_cast.

// Assuming a|b|c|d are shared_ptr instead of regular_ptr
a = static_pointer_cast<A>(b);
b = dynamic_pointer_cast<B>(a);
c = dynamic_pointer_cast<C>(a); // c will receive nullptr

We commonly do this to check what child type we have:

if (auto child = dynamic_pointer_cast<Child>(parent)) {
    /* ... */
}

child will be assigned to the cast pointer if possible, otherwise dynamic_pointer_cast will return nullptr, the assignment will resolve to false, and the if block won't execute.

Specifiers

auto: Automatically deduce type. Somewhat common. Generally, it's fine to use auto when the return type is obvious (e.g. iterators, std::make_shared) or unnecessary.

extern: Thing/object/variable accessible outside the present .cpp file, assumed by default.

static outside function/class: Thing/object/variable not accessible outside the present .cpp file.

static inside function: Instantiated once, shared by all calls to this function.

static inside class: Instantiated once, shared by all instances of this class.

const about an object: Cannot be modified.

const on a class method: Method cannot modify the object; if the object is const, only its const methods can be called.

class Thing {
    static int shared_; // shared by all class instances
    static int shared() {
        // can only interact with static class members
        // single instance for all calls to shared()
        static int times = 1; // this line is only called once per program
        times += 1;           // this line is called once per function call
        // random logic lol
        return shared_ * times;
    }

    int local_;         // one per class instance
    int local() const { // can be called on `const Thing` instances
        return local_;
    }
    int inc_local() {   // not-const; cannot be called on `const Thing` instances, even if it didn't actually modify the instance
        ++local_; // pre-increment more efficient than post-increment (local_++)
        return local();
    }
};

Static members can be accessed without having an instance of the class:

Thing::shared_;  // shared_ should be private, but if was public, this would work
Thing::shared(); // also works

constexpr: Used for expressions that can be evaluated at compile-time:

constexpr int my_constant = 2 + 2;              // evaluated to 4
constexpr std::string my_string("Hello World"); // DOESN'T WORK. constexpr only works on basic types e.g. int, char, uint16_t, etc.

References and move semantics

int func(std::string str)        { /* ... */ } // pass copy of string; expensive
int func(std::string& str)       { /* ... */ } // pass reference of string; careful if needs to be modified
int func(const std::string& str) { /* ... */ } // pass const reference of string for read-only; VERY common
int func(std::string&& str)      { /* ... */ } // must call func(std::move(str)) to invoke the move signature
                                               // func(str) defaults to any of the first 3
                                               // mostly expects that the string will be moved and nullified

When passing a standard reference, the parameter must already be allocated (l-value):

int func(int& val) { /* ... */ }
int n = 3;
func(n); // succeeds since `n` is an l-value
func(3); // fails since `3` is an r-value only

Some compilers make exceptions for certain const references especially strings, that will allow string literals:

void func(const std::string& str) { /* ... */ }
std::string abc("abc");
func(abc);   // works
func("abc"); // also works even though we passed an r-value

Move references (also called r-value references) are more flexible and allow anything:

void func(const std::vector<uint8_t>& data) { /* ... copy or something ... */ }
void func(std::vector<uint8_t>&& data)      { /* ... move or something ... */ }
std::vector<uint8_t> stuff {10, 20, 30};
func(std::move(stuff)); // works; don't forget `std::move` to invoke the 2nd signature instead of the 1st
func({10, 20, 30});     // should invoke the 2nd signature automatically since this is an r-value

In most cases, we use const with formal parameters:

int func(const uint8_t n);
int func(const int64_t n);                  // for anything simple, 64 bits or less, pass by copy, since ref would internally pass a 64-bit pointer anyway
int func(const std::string& str);           // more complex, so use const ref instead of const copy; we pass nearly (95%+) all strings with this
int func(const std::vector<uint8_t>& data); // for vectors or anything else

Move construction

It's much faster to move vs copy, when the original data will be discarded anyway.

move is O(1) and copy is O(n); good for long buffers of video content.

class Thing {
public:
    // traditional copy constructor
    Thing(const std::vector<uint8_t>& data): data_(data) {}
    // move constructor
    Thing(std::vector<uint8_t>&& data): data_(std::move(data)) {}
    // alternative way of doing this:
    Thing(std::vector<uint8_t>&& data) {
        data_.swap(data); // internal pointer swap, O(1) and very fast
    }
private:
    std::vector<uint8_t> data_;
};

std::shared_ptr<Thing> make_thing() {
    std::vector<uint8_t> data {0, 1, 2, 3, 4};

    // ... processing or whatever ...
    
    // invoke copy constructor
    auto slow = std::make_shared<const Thing>(data);            // needs to deep copy the entire `data` buffer, could be several Mbs or Gbs

    // invoke move constructor
    auto fast = std::make_shared<const Thing>(std::move(data)); // don't copy the data into the new instance
                                                                // since we won't need it, move without copying
                                                                // according to the spec, `data` is unspecified after and shouldn't be reused
    return thing;
}

Classes and structs

class members are private by default while struct members are public by default. Otherwise, they are (mostly) similar. Examples use classes.

class Thing {
public:
    Thing() = delete;                                             // don't allow the default constructor
    Thing(const std::vector<uint8_t>& data): data_(data) {}       // new Thing(data) will invoke this
    Thing(std::vector<uint8_t>&& data): data_(std::move(data)) {} // new Thing(std::move(data)) will invoke this
    Thing(const Thing& thing) = default;                          // just generate a default copy constructor
    Thing(Thing&& thing) { data_.swap(thing.data_); }             // to move a Thing around
private:
    std::vector<uint8_t> data_;
};  // never forget the FUCKING SEMICOLON after classes and structs

Virtual methods

class Parent {
public:
    /* ... */
    virtual ~Parent();
    virtual void method();
};

class Child : public Parent {
public:
    /* ... */
    virtual ~Child();
    virtual void method() override final; // override keyword not mandatory but highly recommended
                                          // final keyword ensures it cannot be overridden in further child classes
};

// ...

std::shared_ptr<Parent> ptr = std::make_shared<Child>();
parent->method(); // virtual keyword was required so the child's method is called, not the parent
ptr.reset();      // virtual destructor was called to make sure the child's destructor is called
                  // for destructors, all destructors are called from child to parent

Pure virtual methods

In this case method() must be defined in a child class, making Thing an abstract class that cannot be instantiated directly.

class Thing {
public:
    virtual void method() = 0; // virtual methods use pointers internally so this method's pointer is nullptr
};

Namespaces

Every C++ container/function is in the namespace std, therefore:

std::string
std::vector<...>
std::shared_ptr<...>
std::rand()
// etc

The need for std can be negated by using the instruction using namespace std; but this is not recommended in practice because it can cause name collisions.

Namespaces can be chained:

namespace A {
    namespace B {
        class Thing;
    }
}

// ...

A::B::Thing thing; // call it

using can be practical for long namespaces, but should only be used with a minimum scope:

int func_a() {
    A::B::Thing thing;
    // ...
}

int func_b() {
    using namespace A::B;
    Thing thing;
    // or
    using A::B::Thing;
    Thing thing; // to only use one object from the namespace
    // or
    using AB = A::B;
    AB::Thing thing;
}

Type definitions

Types can be aliased:

typedef std::unordered_map<std::string, VeryLongClassNameThatsGodAwfullyLong> LongThingMap;

// ...

LongThingMap long_thing_map; // shortens code
LongThingMap::iterator it;   // also works

Nesting

namespace MyNamespace {
    class Thing {
        class SubThing {
            typedef std::string String;
        };
    };
}

// ...

MyNamespace::Thing::SubThing sub_thing;   // instantiate
MyNamespace::Thing::SubThing::String str; // literally a std::string

STL containers

std::string       // introduced before all the others so it has some non-standard methods/behaviours
std::vector<Type> // standard expansible array, contiguous storage
std::deque<Type>  // vector that can have insertions at both ends without shifting all elements; deque = Double Ended QUEue
std::list<Type>   // linked list with insertion/removal at both ends
std::queue<Type>  // linked list with insertion at one end and removal at the other
std::stack<Type>  // linked list with insertion and removal at one end only
                  // heaps can be important to know too but there is no C++ container for that (yet)
                  // same for circular buffers

std::unordered_map<KeyType, ValueType> // straight up hash map
std::map<KeyType, ValueType>           // works pretty much exactly like like a hash map but insertion/lookup are a bit slower
                                       // advantage is that when iterating over keys are stored in order
                                       // recommend always using unordered_map on interviews unless things have to be iterated in order

std::unordered_set<KeyType> // hash map with no values, simply to store a set of keys (e.g. nodes that have already been seen/visited)
std::set<KeyType>           // works the same but uses a binary tree underneath; keeps keys in order

std::(unordered_)(multi)(map|set)<...> // the `multi` prefix on maps and sets creates structures that can store multiple objects with the same key, but is very rarely used;
                                       // sometimes easier than having a map of vectors or whatever

std::array<Type, size_t Size> // constant-size array, VERY rare, size is declared statically at compile-time

Useful STL container methods

size(): is universal for the size/length of any of the containers above.

length(): is same as size() but only available for std::string for backwards compatibility; better not to use.

push()/pop(): for elements where you can only push/pop at one end. pop() doesn't return the element, should be accessed/copied with front() (top() for stacks) before pop()ing.

front()/back(): access the first/last element (directly by reference) on all containers except std::stack.

begin()/end(): iterators to the beginning/ending of the container; only for containers that are iterable aka everything except std::stack/std::queue.

at()/operator[index]: directly access element at index N; only for containers that store things in contiguous memory (so no linked lists) and that aren't maps e.g. std::string, std::vector, std::deque, std::array. at() throws an exception if out-of-bounds while operator[] is undefined if out-of-bounds and will often seg fault. However, since exceptions are not recommended in C++, it's better to use size() + operator[];

at(key)/operator[key]: for maps, pass in the actual key instead of an index; with operator[key], if the element doesn't exist, one is created and instantiated by default; with at(key), if the element doesn't exist, an exception is thrown; STRONG preference for operator[key] (most C++ pros avoid using exceptions altogether) and find(key) if/when needed.

find(key)/find(value) for maps, find() returns an iterator to an element with the specified key; for all other containers that support find() takes in a value and returns an iterator. Returns an iterator to the end of the container if key/value isn't found. EXCEPT for std::string (predates iterators) which returns the index (size_t) of the element and std::string::npos (generally equal to -1) if the element is not found. See std::find() below for a more generic method.

Iterators

Most STL containers have iterators (we call them iterables) except like std::stack and std::queue where the middle elements are never accessible.

Iterators can be treated mostly like pointers with *it to dereference and it-> to access member elements.

Often we use ranges where we pass a beginning iterator and an ending iterator. In a range context, the beginning is the first element while the ending is the element after the last element. In STL containers, end() points to a fake element that doesn't exist and often seg faults if accessed.

With maps, the iterator doesn't point directly to the element but to a std::pair<Key, Value>. The key can be accessed with it->first and the value with it->second. The key is const since modifying it could mess up the map while the value can be modified.

Functions

std::find(begin, end, value): attempts to find value between begin and end and returns end if the item is not found. Works on all STL containers including std::string; still recommend using the find method on respective containers but to get the same behaviour on std::strings this is what should be used. Also useful to search partial ranges. Since iterators have identical interfaces as raw pointers and std::find is templated, std::find can also take raw pointers. end is never searched.

std::copy(source_begin, source_end, destination_begin): C++ version of memcpy to copy blocks of data. Like std::find, supports iterators and pointers alike.

std::find(a_begin, a_end, b_begin): C++ version of memcmp which compares 2 ranges. Also supports iterators and pointers alike. Returns true/false depending on whether the ranges are equal.

<algorithm> contains many other but less important functions for ranges and other stuff.

For loops and iterating in general

// By index
for (size_t i = 0; i < container.size(); ++i) // size_t is the usual type for sizes and numerical indexes

// By iterator
for (auto it = container.begin(); it != container.end(); ++it) // `<` and `>` comparisons on iterators only work for containers with contiguous elements
                                                               // e.g. std::string, std::vector, etc.
                                                               // otherwise only `!=` works, so it's better to use `!=` when iterating with iterator

// Range-based for loop (container must be iterable so not std::stack or std::queue)
for (auto& element : container)       // Gets a modifiable reference to every element in the container
for (const auto& element : container) // Gets a const reference to every element in the container, for read-only

// Range-based for loop on maps
for (auto& entry : map) { // syntax is the same
    // but you get a reference to a pair instead of the element directly
    entry.first;
    entry.second;
}

Templates

template<typename T> // <- sometimes the `class` keyword is used instead of `typename`
T add(T a, T b) {
    return a + b;
    // will accept any element that can be added with an element of the same type
}

add<int>(2, 3); // returns 5
add(2, 3);      // also works; when types are easy to deduct by the compiler the type doesn't need to be specified

When types are less obvious (especially for return):

// allows to add 2 elements of different types, as long as those 2 types allow to be added together
template<typename ReturnT, typename LeftT, typename RightT>
ReturnT add(LeftT left, RightT right) {
    return left + right;
}

add<int, int, uint16_t>(100000, 500); // returns 100500
add<int>(100000, 500U);               // the input types can be reasonably inferred but the return type is ambiguous so mandatory
                                      // note the `U` prefix; `500` denotes a generally signed number whereas `500U` denotes a strictly unsigned number; this is generally not required unless strict type checking is enabled on the compiler

Templates can be variadic aka take an arbitrary number of elements (this is not super important):

template<typename... Types>
void something(Types&&... things) {
    // ... do something ...
    // pass on the arguments
    something_else(std::forward<Types>(things)...);
}

To print all arguments, we can do recursion (it's hard to count/iterate the elements):

// `print(...)` takes an arbitrary list of arguments of any heterogenous types and print them, as long as each element/type is printable with std::cout

void print() {
    // stop condition, called when the list is empty
    // do nothing or
    std::cout << std::endl;
}

template<typename First, typename... Types>
void print(First&& first, Types&&... things) {
    // the function signature "extracts" the first argument
    std::cout << first;
    print(std::forward<Types>(things)...);
}

print("Hello", ' ', "World", ' ', "it is ", 18, ' ', "degrees outside.");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment