18 KiB
ECE 150: C++
Non-decimal numbers
Binary numbers are prefixed with 0b
.
!!! example The following two snippets are equivalent:
```cpp
int a{0b110001};
```
```cpp
int a{25};
```
To convert from binary to decimal, each digit should be treated as a power of two much like in the base 10 system.
!!! example \[0 \text{0b1011}=1\times2^3 + 0\times2^2+1\times2^1+1\times2^0=11 \]
Binary addition is the same as decimal addition except \(1+1=10\) and \(1+1+1=11\).
To convert from decimal to binary, the number should be repeatedly divided by 2 and the binary number taken from the remainders from bottom to top.
!!! example \[ \begin{align*} 13 &= 2\times6 + 1 \\ 6 &= 2\times3 + 0 \\ 3 &= 2\times1 + 1 \\ 1 &= 2\times0 + 1 \\ &\therefore 13 = \text{0b1101} \end{align*} \]
To convert from binary to hexadecimal, each group of four digits beginning from the right should be converted to their hexadecimal representation.
To convert from hexadecimal to binary, each hexadecimal digit should be expanded into its four-digit binary representation.
To convert from decimal to hexadecimal, the number should be repeatedly divided by 16 and the hex number taken from the remainders from bottom to top.
!!! example \[ \begin{align*} 37 &= 16\times2 + 5 \\ 2 &= 16\times0 + 2 \\ &\therefore 37 = \text{0x25} \end{align*} \]
Numbers
Integers
!!! definition - A carry occurs if an overflow or underflow happens in an unsigned number.
The \(k\)th bit of a number is as known as its coefficient because it can be expressed in the form \(n\times 2^k\) in binary or \(n\times 16^k\) in hexadecimal.
Type | Bits | Can store |
---|---|---|
short |
16 | \(\pm2^{15}-1\) |
int |
32 | \(\pm2^{31}-1\) |
long |
64 | \(\pm2^{63}-1\) |
char |
8 | N/A |
unsigned short |
16 | \(2^{16}-1\) |
unsigned int |
32 | \(2^{32}-1\) |
unsigned long |
64 | \(2^{64}-1\) |
unsigned char |
8 | N/A |
The sizeof()
operator evaluates the size the type takes
in memory at compile time.
Signed numbers use the first bit to represent positive or negative numbers. A negative number is equal to the two’s complement of its positive form. This allows subtraction to be done by taking the two’s complement of the subtracter.
!!! definition The two’s complement form of a number flips all bits but the rightmost digit equal to one.
Floating point numbers
Type | Bits | Digits of precision |
---|---|---|
float |
32 | ~7 |
double |
64 | ~16 |
Floating point numbers let a computer work with numbers of arbitrary precision. However, the limited digits of precision mean that a small number added to a large number can result in the number not changing. This results in odd scenarios such as:
\[ x+(y+z)\neq(x+y)+z \]
References
The ampersand (&) represents a reference variable and an argument passed into a parameter with an ampersand must be a valid lvalue.
Effectively, it is a pointer, letting you do weird shit such as:
void inc(int &n) {
++;
n}
where the variable passed into inc
will actually
increase in the caller function.
This can also be used in variable declarations to not create a second local variable:
#include <climits>
double const &pi{M_PI}; // pi links back to M_PI
Arrays
// typename identifier[n]{};
int array[5]{};
int partial[3]{2};
int filled[3]{1, 2, 3};
Arrays are contiguous in memory and default to 0 when initialised. If field initialised with values, the array will fill the first values as those values and set the rest to 0.
Because arrays do not check bounds, array[n+10]
or
array[-5]
will go to the memory address directed without
complaint and ruin your day.
Pros | Cons |
---|---|
Random access is \(O(1)\) | \(O(n)\) push front |
Fixed size and unused allocated memory | |
Concatenation is slow |
Local arrays
Local arrays cannot be assigned to nor returned from a function. If
an array is marked const
, its entries cannot be
modified.
Arrays can be passed to functions by reference (via pointer to the first entry).
Memory
!!! definition - Volatile memory is erased after the memory is powered off. - Byte-addressable memory is memory that has an address for each byte, such that to change a single bit the whole byte must be rewritten.
Main memory (random access memory, RAM) is volatile and any location in the memory has the same access speed.
An address bus with \(n\) lines allows the CPU to update \(n/8\) bytes at once (one address bit per line). The number of total memory addresses is limited by the number of lanes in the address bus.
When a program is run, the operating system (OS) allocates a block of memory for it such that the largest address is at the bottom of the memory block for the program.
- Instructions (the code segment) are stored at the top of the block
- Constants (the data segment, including string literals) are stored after the instructions
- Local variables (the call stack) are stored beginning from the bottom of the block
Dynamically allocated variables and static variables are stored between the call stack and the data segment.
Call stack
The call stack represents memory and variables are allocated space from bottom to top.
At the moment a function is run, its parameters are allocated space at the bottom, followed by all local variables that may or may not be defined.
The return value of the function overwrites whatever is at the bottom of the function-allocated block such that the caller can simply reach up to get return data.
!!! warning Arrays are allocated top-down such that indexing is made easy.
C-style strings
C-style strings are char arrays that end with a null
terminator (\0
). By default, char arrays are
initialised with this character.
If there is not a null terminator, attempting to access a string continues to go down the call stack until a zero byte is found.
Dynamic allocation
Compared to static memory allocation, which is done by the compiler, dynamic memory is managed by the developer, and is stored between the call stack and data segment in the heap.
The new
operator attempts to allocate its type operand,
optionally initialising the variable and returning its memory
address.
char *c{new char{'i'}};
If the operating system cannot allocate that much memory,
std::bad_alloc
is raised, but passing in
nothrow
can return a nullptr
instead if
allocation fails.
char *c{new(nothrow) char{`i`}};
if (c == nullptr) {
}
The delete
operator tells the OS that the memory address
passed is no longer needed. Generally, it is a good idea to set the
deleted pointer afterward to a null pointer.
delete c;
= nullptr; c
If deleting arrays, delete[]
should be used instead.
!!! warning Statically allocated memory cannot be deallocated manually as it is done so by the compiler, so differentiating the two is generally a good idea.
Vectors at home
Dynamic allocation can be used to mimick an std::vector
by creating a new array whenever an element would be full and doubling
its size, copying all elements over.
!!! example Sample implementation:
```cpp
std::size_t capacity{10};
double *data{new double[capacity]};
std::size_t els{0};
while (true) {
double x{};
std::cin >> x;
++els;
if (els == capacity) {
std::size_t old_capacity{capacity};
double *old_data{data};
capacity *= 2;
data = new double[capacity];
for (int i{}; i < old_capacity; ++i) {
data[i] = old_data[i];
}
delete[] old_data;
old_data = nullptr;
}
}
```
Wild pointers
A wild pointer is any uninitialised pointer. Its behaviour is undefined. Accessing unallocated memory results in a segmentation fault, causing the OS to terminate the program.
!!! warning Occasionally, the OS does not prevent program access to deallocated memory for some time, which may allow the program to reuse garbage pointers, allowing the pointer to work. This causes inconsistent crashing.
To avoid wild pointers, pointers should always be initialised and set
to nullptr
if not needed even if they would go out of
scope.
Dangling pointers
A dangling pointer is one that has been deallocated, which has the same issues as a wild pointer, especially if two pointers have the same address.
!!! example p_2
is dangling.
```cpp
int* p_1{};
int* p_2{p_1};
delete p_1;
```
To avoid dangling pointers, pointers should be immediately set to
nullptr
after deleting them. Deleting a
nullptr
is guaranteed to be safe.
Memory leaks
A memory leak occurs when a pointer is not freed, such as via an early return or setting a pointer to another pointer. This causes memory usage to grow until the program is terminated.
!!! example The new int[20]
has leaked and is no longer
accessible to the program but remains allocated memory.
cpp int* p{new int[20]}; p = new int[10];
Pointers
!!! definition - A pointer is a variable that stores a memory address.
The asterisk *
indicates that the variable is a pointer
address and can be dereferenced. &
can
convert an identifier to a pointer.
int array[10];
int *p_array{array};
int num{2};
int *p_num{&num};
!!! warning Only addresses should be passed when
assigning pointer variables — this means that primitive types must be
converted first to a reference with &
.
The default size of a pointer (the address size) can be found by
taking the sizeof
of any pointer.
sizeof(int*);`
The memory at the location of the pointer can be accessed by setting the pointer as the lvalue:
*var = 100;
The const
modifier only makes constant the value
immediately after const
, meaning that the expression after
it cannot be used as an lvalue.
!!! example ```cpp int* const p_x{&x};
p_x = &y; // not allowed
*p_x = y; // allowed
```
```cpp
int const *p_x{&x};
p_x = &y; // allowed
*p_x = y; // not allowed
```
```cpp
int const * const p_x{&x};
p_x = &y; // not allowed
*p_x = y; // not allowed
```
Pointers to const
values must also be
const
.
!!! example BAD:
cpp const int x = 2; int *p_x{&x};
GOOD:
```cpp
const int x = 2;
int const *p_x{&x};
```
Sorting algorithms
Selection sort
Selection sort takes the largest item in the array each time and adds it to the end.
fn selection_sort(array: &mut [i32]) {
for i in (0..array.len()).rev() {
let mut max_index = 0;
for j in 0..i + 1 {
if array[j] > array[max_index] {
= j;
max_index }
}
let _ = &array.swap(i, max_index);
}
}
Insertion sort
Insertion sort assumes the first element of the array is sorted and expands that partition by moving each element afterward to the correct spot.
fn insertion_sort(array: &mut [i32]) {
for i in 1..array.len() {
let mut temp = array[0];
for j in 0..i {
if array[j] < array[i] {
= array[i];
temp for k in (j..i).rev() {
+ 1] = array[k]
array[k }
= temp;
array[j] break;
}
}
}
}
Recursion
Merge sort
Merge sort is a recursive sorting algorithm with the following pseudocode:
- If the array length is one or less, do not modify the array
- Otherwise, split the array into two halves and call merge sort on both halves
- Merge the split arrays together in sorted order (adding each in sequence such that it is sorted)
void merge_sort( double array[], std::size_t capacity ) {
if ( capacity <= 1 ) {
return;
} else {
std::size_t capacity_1{ capacity/2 };
std::size_t capacity_2{ capacity - capacity_1 };
( array, capacity_1 );
merge_sort( array + capacity_1, capacity_2 );
merge_sort
( array, capacity_1, capacity_2 );
merge}
}
void merge( double array[], std::size_t cap_1,
std::size_t cap_2 ) {
double tmp_array[cap_1 + cap_2];
std::size_t k1{0};
std::size_t k2{cap_1};
std::size_t kt{0};
// As long as not everything in each half is not
// copied over, copy the next smallest entry into the
// temporary array.
while ( (k1 < cap_1) && (k2 < cap_1 + cap_2 ) ) {
if ( array[k1] <= array[k2] ) {
[kt] = array[k1];
tmp_array++k1;
} else {
[kt] = array[k2];
tmp_array++k2;
}
++kt;
}
// Copy all entries left from the left half (if any)
// to the temporary array.
while ( k1 < cap_1 ) {
[kt] = array[k1];
tmp_array++k1;
++kt;
}
// Copy all entries left from the right half (if any)
// to the temporary array.
while ( k2 < cap_1 + cap_2 ) {
[kt] = array[k2];
tmp_array++k2;
++kt;
}
// Copy all the entries back to the original array.
for ( std::size_t k{0}; k < (cap_1 + cap_2); ++k ) {
[k] = tmp_array[k];
array}
}
Classes
By convention, class member variables are suffixed with an underscore.
Classes inherently have two default constructors — one if passed
another version of itself and one if the user does not define one, using
the list of public
variables.
!!! example the following initialisers both do the same thing — they
both copy earth
into a new variable (not by reference).
```cpp
Body earth{};
Body tmp{earth};
Body tmp2 = earth;
```
Namespaces
Namespaces allow definitions to be scoped, such as
std
.
namespace eggy {
std::string name{"eggy"};
std::string get_name() {
return eggy::name;
}
}
Namespaces can also be nested within namespaces.
!!! warning std::cout
does weird shenanigans that passes
itself to every function afterward, such as std::endl
.
This means that `std::cout << std::endl;` is equivalent to `std::endl(std::cout);`.
Operator overloading
Operators can be overloaded for various classes.
!!! example Overloading for displaying to cout
:
```cpp
std::ostream operator<<(std::ostream &out, ClassName const &p) {
out << "text here";
return out;
}
Constructors
Constructors can be defined with default values after a colon and before the function body:
::Rational():
Rationalnumer_{0},
denom_{0} {
}
Subsequent members can even use the values of previous ones.
Constructors can also contain parameters with default values, but default values must also be present in the class declaration.
Member functions
A const
after a member function forbids the modification
of any member variables within that function.
int get_val() const {
}
Exceptions
#define NDEBUG
turns off assertions.
static_cast<double>(var)
performs the typical
implicit conversion explicitly during compile time.
Exceptions are expensive error handlers that do not protect from program termination (e.g., attempt to access invalid memory).
The following are all exception classes in std
:
domain_error
runtime_error
range_error
overflow_error
underflow_error
logic_error
length_error
out_of_range
...
is a catch-all exception.
!!! example
cpp try { throw std::domain_error{"cannot compute stupidity"}; } catch (std::domain_error &e) { std::cerr << e->what(); } catch (...) { }
Copies and moves
The copy constructor by default copies the value of every public and private field, including pointers, both from field initialisation or assignment.
{};
MyClass n{n}; // copy
MyClass m= n; // copy MyClass p
The move constructor copies every field from the other object and
resets the original object to no longer point to that data, typically
called only via std::move
.
{};
MyClass a{std::move(a)}; MyClass b
It is an excellent idea to blank out the two constructors to do nothing so that unexpected behaviour does not occur.
class MyClass {
public:
(MyClass const &rhs) = delete;
MyClass(MyClass &&rhs) = delete;
MyClass&operator=(MyClass const &rhs) = delete;
MyClass &operator=(MyClass &&rhs) = delete;
MyClass }
If a move occurs and the compiler determines that the original object is no longer needed, its destructor is automatically called immediately after the constructor / assignment finishes.
During construction, default initialisation picks the one with the fewest parameters if ambiguous. Parameters passed by value are copied by reference via the copy constructor.
Much like statically allocated arrays, dynamically allocated arrays also automatically dereference when accessed by index.
Linked lists
Dynamic memory allocation for many objects instead of one like arrays is slow.
Inheritance
All member functions and the destructor must be virtual
functions if they can be inherited.
class Base {
public:
();
Basevirtual ~Base();
virtual Base get_base() const;
virtual void set_base();
virtual void do_base() const;
A class that inherits another should contain
public <Base>
after the name of the class. Overriden
functions must have override
if they should have the same
type signature. Otherwise, they reference the base function.
class ExtendedBase: public Base {
public:
();
ExtendedBase() const override;
Base get_basevoid set_base() override;
virtual void do_base() override;
}
Functions can be overriden completely ignoring the function signature
by excluding the override
keyword.
The base class’s functions implicitly refer to the current class, so they can be directly called:
void set_base() override {
::set_base();
Base}
protected
The protected
access keyword only allows the original
class as well as classes that extend the original one to access it.
Extending exceptions
Exceptions should have two constructors: one for a char array pointer
and another a string for the exception message, as well as any
additional parameters as desired. The base exception constructor (not
std::exception
because that can’t be instantiated`) should
be called to do all of the base constructor things.
In addition, a what()
function with the following
signature should always be defined that cannot throw an exception,
returning a C-style array.
char const *error::what() const noexcept;