(unsigned) char | short | int | long | long long | float | double
(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
.
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!).
'a'
(single quotes) represents a singlechar
"a"
(double quotes) represents aconst char*
(string literal) that only contains one character
Cannot interchange both!
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
is the C++ standard to denote NULL
in regards to pointers.
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]
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
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
}
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
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.
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.
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
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;
}
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
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
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
};
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;
}
Types can be aliased:
typedef std::unordered_map<std::string, VeryLongClassNameThatsGodAwfullyLong> LongThingMap;
// ...
LongThingMap long_thing_map; // shortens code
LongThingMap::iterator it; // also works
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
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
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.
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.
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::string
s 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.
// 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;
}
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.");