This blog post summarizes the main features of modern C++ from the developer’s point of view.
Inheritance
A derived class can inherit protected and public members of the base class. This creates an is a
relationship, so the derived class’ objects are also of type base class. And hence, a pointer of type base class can point to an object of the derived class.
A class can inherit from another class privately, protectedly, or publicly. With public inheritance, public members of the base class become public members of the derived class, protected members of the base class also become protected members of the derived class. With protected inheritance, public and protected members of the base class all become protected members of the derived class. With private inheritance, public and protected members of the base class all become private members of the derived class. (See table below.)
C++ Inhertance modes
Base class member | Inheritance mode | ||
---|---|---|---|
Public | Protected | Private | |
Public | Public | Protected | Private |
Protected | Protected | Protected | Private |
Private | not inheritable | not inheritable | not inheritable |
Note that as they are special, the destructor and friends are not inheritable. The constructor is also not inherited by default, however, from C++11, we can inherit all constructors using the keyword using.
When an object of the derived class is initialized, the constructor of the base class (we can choose which one) will be called before the constructor of the derived class. On the reverse, when an object is deleted, the derived class’s destructor will be called before it of the base class.
Multiple inheritance is when one class derives from multiple base classes. Hierarchical inheritance is when more than one class is derived from one base class. The combination of Multiple inheritance and Hierarchical inheritance (or any other 2 types of inheritance, like Single inheritance and Multilevel inheritance) is called Hybrid inheritance. Typically, these types of complex inheritance are not encouraged as they affect the comprehensibility of the program and are prone to errors.
Access specifiers: private, protected, public
These are the 3 access modes of classes and structs.
- A private member (i.e. attribute or method) cannot be accessed from outside of the class and is not inheritable by subclasses. It can only be accessed from inside of the class.
- A protected member cannot be accessed from outside of the class but is inherited by subclasses.
- A public member can be accessed outside of the class and is also inherited.
#include <iostream> #include <string> class Base { private: std::string private_attr; protected: std::string protected_attr; public: std::string public_attr; }; class Derived : public Base { public: Derived() { protected_attr = "this is accessible from the inside of derived classes"; } }; int main() { Derived variable; variable.public_attr = "this is even accessible from anywhere"; }
Struct vs Class
In C++, structs and classes are almost exactly the same. The only difference lies in their default access mode:
- Members of structs are public by default. Members of classes are private by default.
- When deriving a struct from a struct (or class), if not specified, the default inheritance mode is public, while this is private for a class.
Overloading vs Overriding
Overloading means we define multiple functions (and/or operators) with the same name but different parameter types. Overriding means when deriving a new class, we re-define the same function (i.e. same name and same parameter types) that had been defined in the base class.
Overloading functions might have the same or different return types, but they must have different parameter types. In other words, we cannot overload functions distinguished by return type alone.
In general, the compiler considers overloading functions as separate functions.
#include <iostream> class Base { public: int foo(int x) { return x + 1; } float foo(int x, float y) // overloading of `int foo(int x)` { return x + y; } }; class Derived : public Base { float foo(int x, float y) // overriding of `float foo(int x, float y)` in the base class { return x - y; } }; int main() { }
Static binding and Dynamic binding
Binding means mapping from each function call to the actual function to be triggered. Static binding (or early binding) happens at compile-time while dynamic binding (or late binding) happens at runtime.
Static binding happens with normal function calls and overloaded functions/operators. Dynamic binding can happen with virtual functions.
While static binding makes the program faster, dynamic binding is more flexible since one function call can behave differently depending on the object calling it.
Virtual function
Within the scope of inheritance, a virtual function (defined by the virtual
keyword) of the base class signals dynamic binding, i.e. the call to the function name will trigger which function, the one defined in the base class or the one defined in any subclass, is determined at runtime.
If in the base class, the body of the virtual function is not implemented, that function is called a pure virtual function. Any class that has a pure virtual function is a (virtual) abstract base class. Concrete subclasses do not need to implement all virtual functions of the base class, but only the pure ones.
When overriding a pure virtual function, while it is not a must, it is always recommended to add the override
keyword:
- It tells the reader that this function overrides a pure virtual function of the base class.
- The compiler can verify if this is truely an overriding of a pure virtual function, and notice the programmer by giving an error if this is not.
#include <iostream> class Base { public: virtual int foo(int x) = 0; // a pure virtual function float foo(int x, float y) // a pure virtual function can also be overloaded { return x + y; } }; class Derived : public Base { int foo(int x) override // we implement all pure virtual functions of the base class { return x + 1; } }; int main() { Derived var; // since we implement all pure virtual functions of Derived's parents, we can instantiate objects from Derived }
Polymorphism
Polymorphism means many forms, or the same entity acts differently in different scenarios.
There are 2 types of polymorphism: in compile-time (i.e. static binding, early binding) and in runtime (i.e. dynamic binding, late binding). Compile-time polymorphism includes overloading of functions and operators, while runtime polymorphism refers to overriding of pure virtual functions.
Data abstraction
Data abstraction is the ability to hide minor details and only show to users the essential information and functions.
In C++, data abstraction can be achieved by:
- The access specifiers: e.g. the private and protected members of a class are not to be accessed from the outside world.
- The header files: detailed implementation of the functions are hidden.
- Abstract base classes and pure virtual functions.
friend
A class can have friends that are other classes, functions, class templates, and function templates. They are marked with the keyword friend
. while class A considers X as a friend, X is not a member of A, however, X can access all private and protected members of A.
The relationship is not symmetric, that is, while A considers X a friend, X does not necessarily consider A a friend. Furthermore, a friend function can be a global function or a member of another class.
Friendship declaration can be anywhere in the body of a class (no matter in the private, protected, or public region).
Friendships are not inheritable.
Normally, we use friend functions to access private and protected members of more than 1 class. See the code below for illustration.
#include <iostream> class B; // forward declaration class A { private: int private_attr; public: A(int v) { private_attr = v; } friend float a_friend(A, B); // friendship declaration }; class B { protected: float protected_attr; public: B(float v) { protected_attr = v; } friend float a_friend(A, B); // friendship declaration }; float a_friend(A a, B b) // this function is a friend of both A and B, so it can access private and protected members of these 2 classes. { return a.private_attr + b.protected_attr; } int main() { A a(12); B b(3.5); std::cout << a_friend(a, b) << std::endl; // output: 15.5 }
Pointer and reference
While most languages only support referencing, C++ has both pointers and references. Since they are very similar, it can be confusing when to use which.
Pointer and reference both involve an original object that provides access to another one. While the pointer is a separate variable that holds the memory address of this original object, the reference is an alias (can be considered as another name) of the object.
A pointer can be declared without initialization, while a reference needs to be initialized at declaration time. While a pointer can be reassigned to another object, a reference can’t. Only pointers can be assigned to nullptr
, a reference has to be assigned to a real object.
While in general, both pointers and references can only refer to objects of a pre-defined type, a void pointer can point to any type. Note, however, that the reverse is not true unless we do type-cast.
Normally, it is recommended to use references. Pointers should only be used if the work cannot be done with references, for example when re-assignments are needed, or pointing to null is allowed. A real use-case of pointers could be to implement the linked-list data structure.
#include <iostream> int main() { int x = 3; int &y = x; // y is a reference of x std::cout << y << std::endl; // output: 3 y = 10; // changing y will also change x since both are the same std::cout << x << std::endl; // output: 10 int *p = &x; // p is a pointer that points to x std::cout << *p << std::endl; // output: 10 *p = 20; // change the `*p` (i.e. the value p points to) will also change x and y std::cout << x << std::endl; // output: 20 int z = 200; *p = z; // this changes the value at which p points to p = &z; // this changes where p points to, p is a pointer so it can be re-assigned std::cout << *p << std::endl; // output: 200 void *r = &x; // a void pointer can point to any type }
Passing by value and reference
By default, things in C++ are passed by value.
Arrays are a special case. When we pass an array, we pass the pointer to the first element of that array (and so it is very similar to passing by reference, i.e. no overhead for copying, changing inside the function will reflect in the original array).
The symbol &
makes passing by reference (e.g. void foo(int &x) {...}
). About arrays, there is usually no benefit to passing an array by reference.
One special (but quite frequent) case to remember is with threads, as described here, in which we need std::ref
to passing by reference.
The this pointer
Member methods of a class can use the this
pointer, which is a pointer to the current object that is calling the corresponding method.
Friend classes and friend methods cannot use the this
pointer, as they are not members of the class.
Template
A function (or class) template is a function (or class) that can operate on different input types. That said, if our function is meant to behave the same given either int
or double
input type, we use Template
to write only one function that works for both types.
From another point of view, templates allow us to pass data types in a similar way to passing parameter values to a regular function.
#include <iostream> template <class T> T add(T a, T b) { return a + b; } int main() { int x = 2, y = 3; std::cout << add<int>(x, y) << std::endl; // output: 5 double u = 7.1, v = 8.5; std::cout << add<double>(u, v) << std::endl; // output: 15.6 }
Block scope variable
Any variable that is declared inside a block is only accessible inside that block. Attempts to call a variable outside of its block scope will result in a compiler error. A block is any piece of code that is enclosed by a pair of curly brackets, like the if
block, for
block, etc.
Exception handling
Exception handling in C++ is done using the 3 keywords: try
, catch
, and throw
.
Note the difference between using assert
and exceptions: assert
is meant to ensure the code is correct (and is meant to catch coding errors from the programmer) while exceptions are to catch what goes wrong during users’ use. There is no need to handle (in code) the errors caught by assert
, because they are never meant to exist.
ternary operator
The ternary operator is a shorthand for an if-else block. It has the syntax:
variable = (BooleanCondition) ? ExpressionTrue : ExpressionFalse;
const and define
The const
keyword can be used to mark a variable as unchangeable, i.e. being a constant. In some cases, it gives similar effects as #define
.
#include <iostream> #define defined_variable 200 const int const_variable = 100; int main() { std::cout << const_variable << " " << defined_variable << std::endl; // output: 100 200 // const_variable = 10; // Error // defined_variable = 20; // Error }
const for pointers
There are different ways we can use the const
keyword for pointers.
1. A pointer to point to a const value: const <type> *p = &<constant variable>
. A constant pointer defined this way can point to a regular (i.e. not const) value, however, it is recommended to use it only to point to a constant value. This declaration prevents users from modifying the value to which the pointer points.
#include <iostream> int main() { int a = 1, b = 2; const int c = 3; const int *p; // This prevents changing the value p points to by assigning *p = ... int *q; p = &a; // This is allowed // *p = 10; // Error, because const pointer p is not allowed to change the value it points to p = &b; // Good, p is allowed to be re-assigned // q = &c; // Error, a pointer of type `int *` cannot point to an object of type `const int *` p = &c; // This is recommended. std::cout << *p << std:: endl; //output: 3 }
Note that as illustrated in the code above, a const pointer can point to a non-const value, however, the reverse is not true: a non-const pointer cannot point to a const value.
2. When we want to ensure a pointer doesn’t change its initialized pointing address, i.e. the pointer can point to 1 address its entire lifetime: <type> *const p = &<variable>
.
#include <iostream> int main() { int a = 1, b = 2; const int c = 3; int *const r = &a; // This prevents changing the address that r points to. r can only point to 1 address for its entire lifetime. // r = &b; // Error, cannot change r's pointing address. *r = 4; // This is allowed std::cout << *r << std::endl; }
3. Just a combination of the above 2 cases, i.e. a pointer that always points to a pre-defined constant value: const <type> *const p = &<variable>
.
#include <iostream> int main() { int a = 1, b = 2; const int c = 3; const int *const s = &c; // s = &a; // Error // *s = 5; // Error std::cout << *s << std::endl; }
An easy way to remember what is made constant by const
is: const
always affects the thing on its immediate left, if there is nothing on its left, then it affects the thing on its immediate right.
const for classes
A const attribute of a class needs to be initialized in the constructor, as below.
#include <iostream> class A { public: int attr; const int const_attr; A(int c) : const_attr(c) {} }; int main() { A a(100); }
A const method (e.g. int foo() const {...}
) is not allowed to change any member variables. If an object is declared as const (e.g. const Car car
), it can only call const members of the class. This is to prevent any const object from having its internal attributes changed.
#include <iostream> class A { public: int attr; const int const_attr; A(int c) : const_attr(c) {} int foo() const // this is a const function, so it is not allowed to alter any member attributes { return 10; } int bar() { return 20; } }; int main() { A a(100); a.foo(); a.bar(); const A b(200); b.foo(); // b.bar(); // Error, const object cannot call non-const members }
Note that a const function has the const
keyword at the end, as in the code above. A function with const
at the beginning, like const int func() {...}
simply means its return type is const int
.
static
The static
keyword has several uses in C++. The overall idea is that if an element (i.e. object, attribute, method) is static then it is only initialized once.
1. static objects: the use of static objects is best described by an object that is initialized inside a function. Even though we call that function many times, that object is only initialized during the first call.
#include <iostream> // this function shows how a static variable behaves inside a function void foo() { static int counter = 0; std::cout << counter++ << std::endl; } int main() { for (int i = 0; i < 3; ++i) { foo(); } // output: 0 1 2 }
2. static attributes: a static attribute of a class is only initialized once and used for all class objects. Static attributes can be either defined inside the class (using inline
, from C++17) or outside the class. The old rule to only initialize static attributes outside of the class was to ensure initialization happens only once even though the header files can be included in many source files. (However, this is then nicely handled with C++17.)
#include <iostream> class A { public: inline static double s_1 = 2.5; // this shows how to initialize static attributes inside class definition static float s_2; // this static attribute is not defined inside class }; float A::s_2 = 6.6; // this shows how to initialize static attributes outside class definition int main() { A::s_2 = 3.1; // static attributes can be changed like this std::cout << A::s_1 << " " << A::s_2 << std::endl; // output: 2.5 3.1 }
3. static methods: static methods are also initialized once and used by all objects of the class. Note that static methods can access and alter only static attributes (but no non-static attributes).
#include <iostream> class A { public: inline static double s_1 = 2.5; // this shows how to initialize static attributes inside class definition static float s_2; // this static attribute is not defined inside class static void bar() { std::cout << s_1 << std::endl; } }; float A::s_2 = 6.6; // this shows how to initialize static attributes outside class definition int main() { A::s_2 = 3.1; // static attributes can be changed like this std::cout << A::s_1 << " " << A::s_2 << std::endl; // output: 2.5 3.1 A a; // This is one way to call a static method A::bar(); // output: 2.5 // This is another way to call a static method a.bar(); // output: 2.5 }
inline
Traditionally, inline functions are provided by C++ to replace macros (i.e. #define
). By marking a function as inline
, we hint the compiler to expand the body of the function inline where the function is called in order to optimize for performance. However, as modern compilers are smart enough to make the decision themselves, using inline
in this way yields no more advantages.
At the moment, it is recommended to use inline
almost only when it is strictly required. One of those cases is when we initialize static attributes inside the class definition. Another is to define the full body of a function in a header (.h
) file.
By the way, it is important to note that all methods that are specified inside their class definition are implicitly inline. Thus, it is advised to define large methods outside of their class definition using the scope resolution ::
operator.
#include <iostream> class A { inline static int x = 1; // initializing static attributes is one use-case of inline public: void short_method() { // short methods can be defined here and be automatically inline } void large_method(double, int); }; void A::large_method(double u, int v) { // This is a large method. // Large methods like this should be defined outside of the class definition. } int main() { A a; }
volatile
The volatile
type qualifier is often used in multi-threading or multi-processing programs. A variable is marked as volatile (e.g. volatile int x
) if its value can be changed by another thread (or process). Thus, the compiler must not assume that the variable’s value is unchanged even if the current thread (or process) doesn’t touch it. And hence, the compiler needs to evaluate its value every time the variable is called.
extern
extern
is a storage class to indicate that the variable, or function is declared in an external scope, for example, in another .cpp
file. For a variable or function to be used in other places from where it is declared, we:
- Declare and possibly initialize it normally (e.g.
int x = 1
). - At other scopes where we need to use it, declare that we want to have an external reference to it (e.g.
extern int x
) before using it as a regular variable or function.
Take a look at the example below for illustration.
main.cpp:
#include <iostream> int v; extern void foo(); int main() { v = 100; foo(); }
support.cpp:
#include <iostream> extern int v; void foo(void) { std::cout << v << std::endl; }
terminal:
$ g++ main.cpp support.cpp -o main
$ ./main
output:
100
namespace
A namespace is a declarative region to organize code into different groups to prevent name collision. A namespace is declared using the following syntax:
namespace Space { int x = 5; // variables... void foo() { } // functions... }
Elements of a namespace can be accessed from outside using the scope resolution ::
, e.g. std::cout << Space::x;
. Elements of the global scope can always be accessed with nothing before ::
, e.g. ::foo()
(we do this if there exists a local element with the same name).
To use a namespace’s elements directly without the need to specify their namespace, we use the directive using
, like using namespace std;
.
Namespaces can even be nested, in which the child namespace has free access to the parent namespace’s elements.
namespace Space { void foo() { cout << "foo" << endl; } namespace ChildSpace { void bar() { foo(); } } }
storage classes
A storage class describes some features (the scope, visibility, and lifetime) as variables and functions. There are 5 storage classes:
auto
: this is the default storage class. If your variables and functions are not specified with any of the other storage class, the compiler implicitly understands them asauto
.static
: this controls lifetime of variables and functions, as described in this section.extern
: this controls the visibility, as described in this section.register
: this suggests the compiler to store the corresponding variables in computer registers if possible for faster access.mutable
: this is used on a class attribute to make it modifiable by any const object.
constexpr
The constexpr
(abbreviated for constant expression) keyword behaves quite similarly to const
. However, constexpr
can be considered more strict than const
in the sense that its value (for variables) or return value (for functions) needs to be computable at compile time. If a variable is constexpr
, it is also const
.
Stack and heap memory
The stack memory is used to allocate local variables. The structure of a stack is suitable for entering and exiting scopes: always, the current scope’s variables are at the head of the stack, which makes it faster to access.
The heap memory, on the other side, stores global variables and dynamically allocated data. Global variables are active during the whole program’s lifetime, they are only deleted when the program finishes, so pushing them into a heap would result in more efficient access. Any dynamic allocation of memory is performed on the heap because the heap memory has no limit on size and variable resizing is only possible in the heap (and not possible in the stack).
Here we compare the advantages of stack and heap memory:
C++ advantages of stack and heap memory
Stack | Heap | |
---|---|---|
Access time | Faster | Slower |
Deallocation | Automatically | Need to be handled by the programmer |
Size | limited | Basically no limit |
Re-size | Not possible | Possible |
The below code shows examples of when variables are stored on stack and heap:
int x; // Heap double y[10000000]; // Heap void foo() { float u = 3; } int main() { int a; // Stack foo(); // Stack double *p = new double; // Heap int *q = new int[10]; // heap // double s[10000000]; // Runtime error, as stack cannot store this big size }
Shallow and deep copy
Shallow and deep copy are different if the dynamic allocation of memory is used. In this case, with shallow copy, only the reference is copied, i.e. both objects point to the same location in memory and changes made by one object will also affect the other. With deep copy, a new memory address is created for the new object so the 2 objects are by all means separated.
Shallow copy is the default for the copy constructor and assignment operator (=
). Thus, while it works perfectly if the object’s attributes are simple, re-implementation of the copy constructor and assignment operator are needed to make them using deep copy. The problem is illustrated in the below code.
#include <iostream> class ShallowClass { public: int *p; ShallowClass(int p_) : p(&p_) {} }; int main() { ShallowClass a(1); std::cout << *a.p << std::endl; // output: 1 ShallowClass b = ShallowClass(a); // this invokes the default copy constructor *b.p = 2; // change the value of the b object, which also affects in the value of the a object. std::cout << *a.p << std::endl; // output: 2 }
explicit
The explicit
keyword prevents constructors or conversion functions from doing implicit cast. In the below example, on line 16, A c = 1;
implicitly casts the int 1
to A type using the constructor A(int)
before assigning it to the left-hand side. The same cannot happen on line 17 as A(std::string)
is declared as explicit.
#include <iostream> #include <string> class A { public: A(int) {} explicit A(std::string) {} A(int, double) {} }; int main() { A a(1); // Ok. This calls A(int) A b("abc"); // Ok. This calls A(std::string) A c = 1; // Ok. This (implicitly) calls A(int) // A d = "abc"; // Error. This doesn't implicity call A(std::string) A e = (A) "abc"; // Ok. This explicitly casts "abc" to A type before assigning it to e. A f = static_cast<A>("abc"); // Ok. This also cast "abc" to A type. }
Type inference
Since C++11, we do not need to specify the type of variables everywhere but rather can tell the compiler to make induction on variable types in some places. auto
and decltype
are our helpers. The auto
keyword commands the compiler to trace back until the declaration of the variable to see its type. On the other hand, decltype
returns the type of a variable, function, or expression.
#include <iostream> #include <vector> std::string foo() { return "foo"; } int main() { auto x = 1.2; // auto: the compiler can infer the type of x is double because 1.2 is a double std::vector<int> vec; for (auto v : vec) {} // auto: the compiler can infer the type of v is int decltype(foo) y; // decltype: declare y as of type std::string }
std::move
The
function converts an lvalue variable into xvalue. That is, std::move
std::move(x)
says: if x
‘s value needs to be copied, don’t copy, just take its value away. The advantage of doing this is to reduce the number of (costly) copies, and thus the program takes less time to run.
#include <iostream> template <typename T> void swap(T &a, T &b) { T tmp = std::move(b); // now tmp possesses the value of b without having to copy it. b = std::move(a); // similar to above a = std::move(tmp); // similar to above } int main() { int x = 1, y = 2; swap(x, y); std::cout << x << " " << y << std::endl; // output: 2 1 }
Type alias
To make aliases for data types, we can use the typedef
or using
keyword. Examples below.
typedef long long ll; // `ll` is now an alias for `long long` using ul = unsigned long long; // `ul` is now an alias for `unsigned long long` int main() { ll x = 1; ul y = 2; }
Dynamic memory allocation
In C, the canonical function for dynamic memory allocation is malloc
(or calloc
), deallocation by free
, and reallocation by realloc
. This changed when coming to C++, as now, allocation is done by the operator new
and deallocation is done by delete
and delete[]
. If reallocation is needed, it is recommended to use standard library containers like vector
instead.
The operator new
can either allocate a value or an array of values, in this case, it returns the location of the first element in that array (note that array elements are sequential in memory). delete
is used to deallocate a single value, while delete[]
is meant to deallocate an array.
#include <vector> int main() { int *p = new int; // Dynamically allocate memory, on heap double *q = new double[10]; // Dynamically allocate memory, on heap delete p; delete[] q; // If reallocation is needed std::vector<int> vec; // while vec's header info is stored in stack, its array of elements is on heap // We can let the vector grow naturally by pushing elements to it, or manually resize it, as below vec.resize(1000); }
Multi-threading
From C++11, multi-threading is well supported in the standard library. As a wrap-up, we use std::thread
to create (and by default, run) a thread to execute a function, and std::mutex
(short for mutually exclusive) for locking shared resources.
Take a look at the below example:
#include <iostream> #include <thread> #include <mutex> void accumulate(int &result, std::mutex &my_lock, int resource[], int lower, int upper) { // Can do something here without shared resource { std::lock_guard<std::mutex> my_guard(my_lock); // my_lock is locked from here and will get unlocked when my_guard is out of scope, i.e. at the end of the current block scope for (int i = lower; i <= upper; ++i) { result += resource[i]; // changing `result` will not create data race because only 1 thread can change it at a time, thanks to the lock } } // Can do something here without shared resource } int main() { int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int total_result = 0; std::mutex my_lock; // create a lock for shared resource std::thread t1(accumulate, std::ref(total_result), std::ref(my_lock), arr, 1, 3); // create and run thread t1 std::thread t2(accumulate, std::ref(total_result), std::ref(my_lock), arr, 4, 7); // create and run thread t2 t1.join(); // wait for t1 to complete t2.join(); // wait for t2 to complete std::cout << total_result << std::endl; // output: 28 }
Explanation:
- To create and run a thread, we call
std::thread
‘s constructor, which accept the function to be executed as the first parameter, and then the parameters for this function. - As by default, all parameters passed to the function are passed by value, we need
std::ref
to pass their reference. .join()
method is to wait for the thread to complete by executing further code in the main process.- As
total_result
is a shared resource of all threads (i.e. all threads attempt to modify it), we need a lock for it to prevent data racing. The lock is created withstd::mutex
and monitored bystd::lock_guard
, as illustrated in the code. Here, we use a lock guard instead of doing.lock
and.unlock
manually to (1) prevent mistakes like forgetting to unlock, and (2) prevent cases when an exception is raised making the.unlock
call not executed.
References:
- Difference between static and dynamic binding in C++, techiedelight.
- Friend Functions in C++ | All You Need to Know, mygreatlearning.
- Pointers vs References in C++, geeksforgeeks.
- Inheritance in C++, geeksforgeeks.
- Polymorphism In C++ and Types of it?, mygreatlearning.
- Templates, cplusplus.
- Const keyword in C++, geeksforgeeks.
- The C++ ‘const’ Declaration: Why & How, duramecho.
- Static Keyword in C++, studytonight.
- Inline Functions in C++, studytonight.
- What is the point of defining methods outside of a class in C++?, StackOverflow.
- Why does volatile exist?, StackOverflow.
- Namespaces (C++), Microsoft.
- Storage Classes in C++, tutorialspoint.
- Difference between `constexpr` and `const`, StackOverflow.
- constexpr (C++), Microsoft.
- Shallow vs. deep copying, learncpp.
- explicit specifier, cppreference.
- Type Inference in C++ (auto and decltype), geeksforgeeks.
- What is std::move(), and when should it be used?, StackOverflow.
- Aliases and typedefs (C++), Microsoft.
- Stack vs Heap: Know the Difference, guru99.
- Dynamic memory, cplusplus.
- Why doesn’t C++ have an equivalent to realloc()?, stroustrup.
- Learn C++ Multi-Threading in 5 Minutes, Hackernoon.
- Is C++ pass by value or pass by reference?, Quora.
A very extensive post, thanks for sharing!