My personal notes on C++.
import
how does this effect header filesThe main function is the entry point of a program.
int main()
- Doesn’t take in command line arguments.int main(int argc, char *argv[])
- Allows command line arguments.
argc
is the number of argumentsargv
is an array of strings for the arguments. This includes the program name.return 0;
- return without any errorsreturn 1;
- return with an errorThe smallest memory unit is 1 byte(8 bits).
Name | Bytes | Notes |
---|---|---|
bool | 1 | Can use true or false keywords |
char | 1 | Uses '' s |
wchar_t | 2 or 4 | Larger char encoding standards like unicode. |
short | 2 | Same as short int |
int | 2 or 4 | Bytes depends on the platform |
long long | 8 | Same as long long int |
""
s return a const char*
array that has a null terminating character.Name | Bytes |
---|---|
float | 4 |
double | 8 |
long double | 12 |
0
is falsy.(x = #)
returns x
not #
x = 10
returns 10
x = 0
returns 0
int32_t x = 2'147'483'648
returns -2147483648
int x, y;
int x = 10, y = 20;
x = y = 20;
x
and y
will both be equal to 20
x += x + 3;
is the same as x = x + x + 3;
_this_is_a_valid_variable_name
'
to separate very long numbers.
1'999'999
6.08e2
= 6.08 x 10^2
= 608
6.08e-2
= 0.0608
0
instead of a .
1/2
will return 0
1.0/2
will return 0.5
1/2.0
will return 0.5
1.0/2.0
will return 0.5
0.0/0.0
outputs not a number(nan)1.0/0.0
outputs inf-1.0/0.0
outputs -infx++
returns x
then increments it by 1++x
increments x
then returns x
const
over #define
#define
const
has type checking and syntax checkingconst
is known to debuggersconst
can be scoped to the blockconst
means that the returned value cannot be modified, but this only really applies to references or pointers.auto
tells the compiler to determine the variable’s type by the initial value given.
auto x = "apple";
gets assigned const char*auto x = 0.01;
gets assigned doubleauto x
gives an errorThe purpose of header files is to allow you to separate the declaration(.h) and the definitions(.cpp). The declarations(.h) can be shared across multiple files. This has a couple of benefits:
// .h
#pragma once
class Class {
private:
int _x = 0;
public:
void print();
};
// .cpp
#include "Class.h"
#include <iostream>
Class::print() {
std::cout << _x << "\n";
}
#pragma once
- This is newer and is recommended #ifndef __CLASS_NAME_H__
#define __CLASS_NAME_H__
// Header file
#endif
Namespaces define a region/scope used to prevent naming conflicts
namespace namespaceName {
int x;
}
namespaceName::x = 10;
using namespace namespaceName
.
{}
to define a new scope.Character | Description |
---|---|
\' |
|
\" |
|
\\ |
backslash |
\a |
audible bell |
\b |
backspace |
\f |
form feed - new page |
\n |
new line |
\r |
carriage return |
\t |
horizontal tab |
\v |
vertical tab |
\o## |
octal output |
\x## |
hexadecimal output |
\u#### |
prints unicode. Only works on g++ |
\Nname |
unicode character from name |
\0 |
Null |
Code | Description |
---|---|
int* ptr; |
Creates a pointer pointing to an int. Enough memory for an address. |
*ptr |
Dereferences the ptr |
int& x = y; |
x is a reference to y . x and y always have the same value. |
&ptr |
Gets address of ptr. |
ptr |
Gets the value in ptr. The address it points to. |
ptr-> |
Is the same as (*ptr). |
const int* xPtr = &x; |
Pointer to a const int. Can’t change the dereference. |
int* const xPtr = &x; |
Pointer to an int, but the pointer cannot change. |
void (*func)(int, int);
You can also use #include <functional>
and function<void(int, int)> func;
Arrays in C++ store a sequences of variables in contiguous memory. The array is a pointer to the first element of the array. Accessing an element with arr[index]
is equivalent to dereferencing the pointer at the memory location *(arrPtr + index)
.
int arr[3];
int arr[3] = {1};
initializes with 1, 0, 0int arr[3] = {};
initializes with 0, 0, 0int arr[] = {0, 0, 0};
the size is 3Since arrays are stored in continuous memory, a two dimensional array(or more dimensions) can also be represented with a one dimensional array.
int twoDArr[row][col]
or int oneDArr[row * col]
- Creates a 2d array.arr[row][col]
or arr[(cols * row) + col]
- Access an element in the 2d array.Each of these array may contain the same memory, but their data types are different.
int twoDArr[row][col]
type is int *[col]
int oneDArr[row * col]
type is int *
Operations with the same precedence are executed according to their associativity, left to right or right to left.
Precedence | Operation | Associativity |
---|---|---|
1 | :: - scope | Left to right |
2 | x++, x– | Left to right |
type() - functional case | ||
func(), arr[] | ||
., -> | ||
3 | ++x, –x | Right to left |
+x, -x, !, ~ | ||
(type) - C-style case | ||
*ptr, &var, sizeof | ||
new, new[], delete, delete[] | ||
4 | ., -> | Left to right |
5 | x*y, x/y, x%y | Left to right |
6 | x+y, x-y | Left to right |
7 | «, » | Left to right |
8 | <, <=, >, >= | Left to right |
9 | ==, != | Left to right |
10 | & | Left to right |
11 | ^ | Left to right |
12 | | | Left to right |
13 | && | Left to right |
14 | || | Left to right |
15 | x?y:z, throw, = | Right to left |
+=, -=, *=, /=, %=, «=, »= | ||
&=, ^=, |= | ||
16 | , | Left to right |
switch (x) {
case 0:
break;
case 1:
case 2:
// 1 or 2
break;
default:
break;
}
do {
} while (true);
if (true) cout << "true" << endl;
if (true)
cout << "true" << endl;
else
cout << "false" << endl;
for (int i = 0; i < 10; i++)
cout << i << endl;
while (true)
cout << "infinity ";
std::vector<int> arr = {1, 2, 3, 4};
for (int num : arr) {
}
// This is the same as
for (std::vector<int>::iterator it = arr.begin(); it != arr.end(); it++) {
int num = *it;
}
::const_iterator
is used instead.Term | Example |
---|---|
prototype | void func(int); |
header | void func(int arg) |
implementation | { /* body */ } |
constexpr void func(); |
A function can be evaluated at compile time if possible. |
inline void func(); |
Compiler searches and replaces. Speed up performance, but uses more memory. |
void func(int x)
void func(const Class& x)
void func(Class* x)
Following these rules makes it clear that an object is being modified by a function because you have to specify the address when passing it as an argument.
int (*arr)[]
const int arr[]
int test(int a, int b = 4, int c = 10){}
test(1, 2, 3);
test(1, 2);
test(1);
Lambda functions allow you to easily pass small functions into another function. They take the form of [capture list](arguments){function body}
. You can also specify a return type with []() -> type {}
[&]
to have access to all the variables in the outside scope as pass by reference.[=]
to have access to all the variables in the outside scope as pass by value.exit(1);
int add(int x, int y)
you can’t return -1 as an error because it could be a legitimate value.void func1() {
throw "String error";
}
void func2() {
func1();
}
void func3() {
try {
func2();
} catch (const char* error) {
// handle error
}
}
catch (...)
allows you to catch all errors regardless of their typethrow;
allows you to throw the previously thrown errordynamic_cast
sqrt
receiving a negative num.)Templates are used to allow the same piece of code to use different data types.
template <typename T>
void print(T value) {
std::cout << value << "\n";
}
print(5) // Defined implicitly based upon the type of the argument
print<char>('a') // Defined explicitly
class
is the same as using typename
template<typename T>
void Class<T>::method() {}
Heap memory is used when teh size is only known at run time.
new
allocates memory on the heap and returns its address.delete
frees the allocated memory.
int* ptr = new int;
delete ptr;
// With arrays
int* arr = new int[10];
delete[] arr;
Enums define a variable’s allowed values by restricting it to a predefined set of named constants, improving code clarity and reducing invalid inputs.
enum Num {ONE = 1, TWO, THREE}
enum Shape {CIRCLE, SQUARE, TRIANGLE};
enum class Color {RED, GREEN, BLUE};
Shape s = CIRCLE;
Color c = Color::RED;
CIRCLE == 0
is true while Color::RED == 0
is false.enum Color : char {RED, GREEN, BLUE};
The only difference between structs and classes is that by default structs’ members are public. However, the convention is to only use Structs to group variables together.
.
to get each element in the struct.struct Point {
int x;
int y = 10; // Default value
};
Point pt = {10, 20};
// Or
Point pt;
pt.x = 10;
pt.y = 20;
// Or
Point pt = {.x=10, .y=20};
Classes are used to combine data and functions that handel that data.
// Declarations
// These are usually in .h files
class Point {
private:
double _x = 0, _y = 0;
public:
double x() const {return _x;}
double y() const {return _y;}
double distance(const Point& pt) const;
};
// Definitions
// These are usually in .cpp files
double Point::distance(const Point& pt) const {
double a = _x - pt.x();
double b = _y - pt.y();
return std::sqrt(a * a + b * b);
}
const
after the methods means that the method doesn’t change any of the internal members for the class.
static
members which belong to the class itself rather than any specific object. They can be accessed by using Class::staticMember
static
variables are shared across all instances of the class.static
methods can only access static
variables.this
keyword is a pointer to the object itself.
this
is often used to differentiate between local and member variables. Ex: this->memberVar
Point&
and then return *this;
. This allows methods to be chained together.
pt.method1().method2();
A common pattern is to have one class represent what you want to store and another class hold the database(like vector or linked list) of the units.
class Points {
private:
std::vector<Point*> _points;
public:
~Points();
Point*& at(int index) const; // Used to get and update
void remove(int index);
void add(int index, Point* pt);
};
Constructors are used to initialize the member variables of an object. Constructors have the same name as the class itself, but do not have a return type.
Point::Point(int x, int y) {
_x = x;
_y = y;
}
If member variables have default values, they are initialized with those defaults before the constructor runs. The constructor then overrides them, which is often unnecessary. Additionally, if the default value involves dynamic memory allocation, it can lead to a memory leak. Using an initializer list allows the constructor to directly initialize the values, bypassing the default initialization and avoiding potential inefficiencies or leaks.
Point::Point(int x, int y) : _x(x), _y(y) {}
// Formatting if there are a lot of member variables
Point::Point(int x, int y)
: _x(x),
_y(y)
{}
The default constructor is automatically generated by the compiler if no other constructors are defined. It allows you to create an object without providing any arguments, such as Point pt;
Class() = default;
, which reinstates the default constructor.There are only a few ways to create object.
// Uses teh default constructor
Point pt;
Point pt = Point();
Point pt{};
// The most vexing parse
Point pt();
// This defines a function called pt which returns a Point. This doesn't create an object.
When using heap memory(new
) in a class, it is recommended to follow the “Rule of Three”:
new
), then the destructor is called when the object goes out of scope. If the object is created on the heap(using new
), then the destructor is only called when the object is deleted with delete
.Class::~Class() {
// Deletes all the heap memory in the class
for (int* element : _arr) {
delete element;
}
}
Class& Class::operator=(const Class& rhs) {
if (this == &rhs) return *this; // Avoid self assignment
// Assign any non pointer member variables
_var = rhs.var();
// Delete your old heap
delete _heap;
// Create new heap
_heap = new int;
// Assign value from rhs to new heap
*_heap = rhs.value();
return *this;
}
Class::Class(const Class& rhs) {
// Assign any non pointer member variables
_var = rhs.var();
// Create new heap
_heap = new int;
// Assign value from rhs to new heap
*_heap = rhs.value();
}
Left hand side(lhs) means that it can be assigned to, like a variable. The right hand side(rhs) means that i cannot be assigned to, like a literal.
// This is what most operator overloading looks like
Point Point::operator+(const Point& rhs) const {
Point result;
result.x() = _x + rhs.x();
result.y() = _y + rhs.y();
return result;
}
These compare the left and right hand side and return a bool.
bool Point::operator==(const Point& rhs) const {
return _x == rhs.x() && _y == rhs.y();
}
bool Point::operator<(const Point& rhs) const {
if (_x == rhs.x()) {
return _y < rhs.y();
}
return _x < rhs.x();
}
You can define all the other comparisons from just these two(==
and <
).
bool operator!=(const Point& rhs) { return !(*this == rhs); }
bool operator>(const Point& rhs) { return rhs < *this; }
bool operator<=(const Point& rhs) { return !(*this > rhs); }
bool operator>=(const Point& rhs) { return !(*this < rhs); }
Inheritance allows one class to acquire the members of another class. This decreases the amount of code needed because different classes can share the same code.
class Shape {
protected:
Point _center = Point(0, 0);
double _length = 1;
public:
Point& center() const {return _center;}
};
class Circle : public Shape {
public:
double diameter() const {return _length * 2;}
};
protected:
allows the child class to access it, but it’s private to any instance of the class.: public Parent
, : private Parent
, or protected Parent
Runtime polymorphism allows an object of a child class to be treated as an object of its parent class.
virtual
keyword is used on the parent’s method and the child class overrides that method, then when the parent’s instance calls that method, then the child’s implementation is used.
override
keyword when doing this. Ex: void method() override;
virtual
keyword isn’t used the child class can still override, but when the parent’s instance calls that method, then the parent’s implementation is used.
override
keyword in this case.class Shape {
public:
virtual void draw() const { std::cout << "Shape\n"; }
void draw2() const { std::cout << "Shape\n"; }
};
class Circle : public Shape {
public:
void draw() const override { std::cout << "Circle\n"; }
void draw2() const { std::cout << "Circle\n"; }
};
class Rectangle : public Shape {
public:
void draw() const override { std::cout << "Rectangle\n"; }
void draw2() const { std::cout << "Rectangle\n"; }
};
int main() {
Shape shape;
Circle circle;
Rectangle rectangle;
std::vector<Shape*> shapes;
shapes.push_back(&shape);
shapes.push_back(&circle);
shapes.push_back(&rectangle);
for (Shape* shape : shapes) {
shape->draw();
}
/* virtual
Shape
Circle
Rectangle
*/
for (Shape* shape : shapes) {
shape->draw2();
}
/* non-virtual
Shape
Shape
Shape
*/
}
An abstract class is a class that contains at least one pure virtual method, which must be overridden by any concrete child class.
virtual void print() const = 0;
Composition is used when you have a “has a” relationship between classes. Inheritance is used when you have a “is a” relationship.