Tot: https://youtu.be/8jLOx1hD3_o?t=109489 C++ Course
- See course: https://www.youtube.com/watch?v=8jLOx1hD3_o
course code: https://github.com/rutura/The-C-20-Masterclass-Source-Code
C++ compiler support: https://en.cppreference.com/w/cpp/compiler_support
GCC compiler: https://gcc.gnu.org/
Windows: gcc + clang: https://winlibs.com
Configure c++ standard: https://stackoverflow.com/questions/66975491/how-to-use-c-20-in-g
- Wandbox https://wandbox.org
- Compiler Explorer https://godbolt.org
- Coliru https://coliru.stacked-crooked.com
- CPP insights https://cppinsights.io/
- ...
There are three types errors/warnings
- compiletime errors
- runtime errors
- warnings (compiletime/runtime)
- A statement is the basic unit of computation in a C++ program
- Every c++ program is a collection of statements ment to achieve some goal
- Statements end with a semicolon
- Statements are executed in order of appearance - staring at main function
- A function has input(s) and one output - the functions is the transformatione from input(s) to the output
- A functions must be defined before it is used
There are different options
- std::cout printing data to the terminal (console - stdout)
- std::cin reading data from the terminal (stdin)
- std::cerr printing errors the the terminal (stderr)
- std::clog printing log messages to the console
In image a program as represented by the program area is stored on the hard drive and executed by the cpu. See explanation on https://youtu.be/8jLOx1hD3_o?t=10346
A C++ program is build with
- core features, datatypes, variables
- standard library, for instance iostream
- stl, standard template library with container with types, algorithms and functions
A number of variables are possible:
- int
- double
- float
- char, a simple character like 'a', 'b'
- bool, true or false
- void, typeless type
- auto, deduce type
- ...
A variable are number of 0/1's - a bit. The 0/1's are grouped together to make sense.
See also https://en.cppreference.com/w/cpp/types/numeric_limits
The computer uses a binary number system. For instance 101 means 1x2^2 + 0x2^1 + 1x2^0.
n digits represents 0..2^(n-1) different numbers.
unsigned range [0..2^n - 1]
signed range -2^(n-1) .. 2^(n-1)-1
digits | bytes | data range | data type |
---|---|---|---|
4 | 1 | 0..16 | octet |
8 | 1 | 0..255 | byte |
16 | 2 | 0..65535 | int |
32 | 4 | 0..34359738367 | long |
64 | 8 | 0..18446744073709551615 | double |
Bron: https://en.cppreference.com/w/cpp/language/types
Type | Size in bits | Format | Value range |
---|---|---|---|
char | 8 | signed | -128..127 |
char | 8 | unsigned | 0..255 |
char | 16 | UTF-16 | 0..65535 |
char | 32 | UTF-32 | 0..1114111 |
int | 16 | signed | -32768..32767 |
int | 16 | unsigned | 0..65535 |
int | 32 | signed | -2147483648..2147483647 |
int | 32 | unsigned | 0..4294967295 |
int | 64 | signed | -9223272036854775808..9223372036854775807 |
int | 64 | unsigned | 0..18446744073709551615 |
binary floating point | 32 | IEEE-754 | 0x1^-149..0x1.fffffe^127 |
binary floating point | 64 | IEEE-754 | 0x1^−1074..0x1.fffffffffffff^+1023 |
binary floating point | 80 | IEEE-754 | 0x1^−16445..0x1.fffffffffffffffe^+16383 |
binary floating point | 128 | IEEE-754 | 0x1^−16494..0x1.ffffffffffffffffffffffffffff |
^+16383 |
Representation of 15 in different number systems
int n1 = 15; // decimal
int n2 = 017; // octal
int n3 = 0x0f; // hexadecimal
int n4 = 0b00001111; //binary
Variables should start with a letter or an underscore. Variable names are case sensative. You cannot use spaces in variable names. Format for declaration of a variable
typename variablename {initial value}
type | size | precision | comment |
---|---|---|---|
float | 4 | 7 | |
double | 8 | 15 | recommended default |
long double | 12 | > double |
Only two values: true or false
Booleans use 1 byte storage
Datatype char is ASCII value. See https://en.wikipedia.org/wiki/ASCII
Unicode is out of scope for this course. See https://stackoverflow.com/questions/3010739/how-to-use-unicode-in-c
The compiler tries to deduce the type based on value. Best practice don't use.
- See Output numbers/string
- See Integer base dec/oct/hex
- See Declaration/initialization datatype
- See Integer datatype
- See Float datatype
- See Boolean datatype
- See Initialization
- See Initialization and automatic type
- See Simple operators
With integers you only get the hole number, not the remainder. To get the remainder use '%' the modulo operator.
-
See https://en.cppreference.com/w/ for general language documentation and https://en.cppreference.com/w/cpp/language/operator_precedence for operator precedence
-
See Precedence
The following operators exist
-
std::boolapha
-
std::showbase
-
std::showpoint
-
std::showpos
-
std::uppercase
-
std::internal
-
std::left
-
std::right
-
std::dec
-
std::hex
-
std::oct
-
std::fixed
-
std::scientific
-
std::flush
-
std::endl
-
std::setw()
-
std::setprecision()
-
std::setfill()
- See Bit operators
- See Conditional flow
- See Loops
- See Array basic
- See Array
- See Array bounds
- See Pointers
- See Pointers to char
Memory model
Virtual memory managed by mmu Memory management unit
All Operating Systems have there own memory map
Memory area | description |
---|---|
Sytem | |
Stack | local variables, function calls |
Heap | additional memory that can be queued at run time |
Data | |
Text | executable probram |
Comparison Stack Heap
Stack | Heap |
---|---|
memory is finit | memory is finit |
developer isn't in full control of the memory lifetime | developer is in full control when memory is allocated and when it's released |
lifetime is controlled by the scope mechanism | lifetime is controlled explicitly through new and delete operator |
- See Memory model
Allocating memory
int *p_number1{new int}; // Dynamic memory allocated on the heap, but not initialized
int *p_number2{new int(22)}; // Dynamic memory allocated on the head using direct initialization
int *p_number3{new int{23}}; // Dynamic memory allocated on the head using uniform initialization
Release allocated memory
delete p_number1;
p_number1 = nullptr;
delete p_number2;
p_number2 = nullptr;
delete p_number3;
p_number3 = nullptr;
Calling delete twice on the same pointer may lead to undefined behaviour!!!! The compiler gives no error/warning.
Pointers: Best practice
-
initialize pointers
-
reset pointers after delete
-
for multiple pointers to the same address make sure the owner of the pointer is clear
This occurs when you loose access to memory that was dynamically allocated If your program runs for a long time memory leaks might lead to a crash of your program
- See References
Comparison between references and pointers
References | Pointers |
---|---|
don't use dereferencing for reading and writing | must go through dereference operator to read/write through point somewhere else |
can't be changed to reference something else | can be changed to point somewhere else |
must be initialized at declaration | can be declared un-initialized (will contain garbage address) |
A reference can be seen as a const pointer
double * const p_value {&avalue};
p_value = &another_value; // gives an error at compiletime
These consist of c-string facilities: (see https://en.cppreference.com/w/cpp/header/cctype )
- length
- concatenate
- copy
- search...
- is alphanumeric
- is alphabetic
- is blank
- is lower/upper case
- is digit
- changing case
and cstring (see https://en.cppreference.com/w/cpp/header/cstring )
- check length of a string
c-strings are not save or convenient. std::string is a better alternative
- See String manipulations
- See C-String manipulations
- See std::string
In the context of a program there is only meaning for declarations
-
free standing variables (in their scope)
-
functions
-
classes
-
class member functions
-
class static member variables
A function takes input(s) (0..n) and produces an output A function may only be defined once and is unique by its function signature. The function signature is function name + function parameters
Function declaration and implementation can be split using .h (declaration - prototype) and .cpp (implementation) files. The prototype needs to be known before the implementation
The compiler uses different phases:
-
preprocessing reading definitions generate translation units
-
compiling translation units to generate objects
-
linking objects to generate an exacutable
- Although both passing parameters by pointer and by reference gives you the possibility to change the input parameter.
- Input parameters should not be modifiable. Use the const keyword.
The best practice is to pass by reference.
In modern compilers, return by value is commonly optimized out by the compiler when possible and modified to return by reference avoiding unnecessary copies!
- See Returning values
With function overloading the parameters may differ by:
-
order
-
number
-
type
Lambda functions is a mechanism to setup anonymous functions (without names). Once set up, we can either give them names and call them, or get the lambda functions to do things directly.
- See Lambda Functions
Instead of rewriting simular functions let the compiler help and generate them on the fly for you. Task no code repetition.
-
function templates are just blueprints, not real C++ code consumed by the compiler. The compiler generates real C++ code
-
The real C++ function generated by the compiler is called a template instance
-
A template instance will be reused when a simular function call is issued. No duplicates are generated by the compiler
-
Real function declarations and definitions are created when you call the function with arguments
-
if the template parameters are of the same type (T,T) then the arguments you call the function with must also match. Otherwise you get a compiler error.
-
template instances won't always do what you expect. A good example is when you call a function with pointers DISASTER
-
there are tools like cppinsights.io that can show you template instantiations. You can even use the debugger to infer that information
-
the arguments passed to a function template must support the operations that are done in the body of the function
When declaring a template with same name but passing variable by value and another passing parameters by reference the compile won't know what to use
template <typename T> T maximum(T a, T b) { return (a>b)?a:b; }
template <typename T> const T& maximum(const T& a, const T& b) { return (a>b)?a:b; }
int main(int argc, char **argv) {
double a{23.5};
double b{51.2};
double max1 = maximum(a,b); // By value or by reference? Compiler error
return 0;
}
There are standard builtin concepts but you can also create your own concepts Some built in concepts are
- same_as
- derived_from
- convertible_to
- common_reference_with
- common_with
- integral
- signed_integral
- unsigned_integral
- floating_point
Concepts are a mechanism to place constraints on your template type parameters
Requires clause
-
simple requirements
-
nested requirements
-
compound requirements
-
type requirements
-
See Concepts
A class is a blueprint (model) for a realworld object.
- class member variables can either be raw stack variables or pointers
- members can't be references
- classes have function (methods) that let them do things
- class methods have access to the member variables, regardless of whether they are public or private
- private members of classes (variables and functions) aren't accessible from outside the class definition
Constructors a special kind of method that is called when an instance of a class is created
- no return type
- same name as the class
- can have (0..n) parameters
- usually used to initialized member variables of a class
When you create your own constructur (without a default constructor) the compiler will nog generate a default constructor, without a parameter list. Resulting in a compilation error.
- See Classes
- See Constructors
There are special methods that are called when an object dies. They are needed when the object needs to release some dynamic memory, or for some other kind of clean up.
When are destructor called
- in weird places that may not be obvious
- when an object is passed by value to a function
- when a local object is returned from a function (some compilers)
- obvious cases are
- when a local stack object goes out of scope (dies)
- when a heap object is released with delete
When calling constructors for objects o1..o4 in a specific context the order of destructor calls are o4..o1
The this pointer points to the current object.
It can be used to chain calls. Which is also possible by using references
example for using setters in chained calls using this pointer
Dog * Dog::set_dog_name(std::string dog_name_param)
{
dog_name = dog_name_param;
return this;
}
Dog * Dog::set_dog_breed(std::string dog_breed_param)
{
dog_breed = dog_breed_param;
return this;
}
Dog * Dog::set_dog_age(int age_param)
{
if (dog_age)
{
delete dog_age;
}
dog_age = new int(age_param);
return this;
}
p_dog->set_dog_name("Mario")->set_dog_breed("Fox Terrier")->set_dog_age(5);
example for using setters in chained calls using references
Dog & Dog::set_dog_name(std::string dog_name_param)
{
dog_name = dog_name_param;
return *this;
}
Dog & Dog::set_dog_breed(std::string dog_breed_param)
{
dog_breed = dog_breed_param;
return *this;
}
Dog & Dog::set_dog_age(int age_param)
{
if (dog_age)
{
delete dog_age;
}
dog_age = new int(age_param);
return *this;
}
p_dog->set_dog_name("Mario").set_dog_breed("Fox Terrier").set_dog_age(5);
- See This pointer
- See This pointer
In general: the size of a class object is the sum of the size of each member variable.
- A defining feature of Object Oriented Programming in C++
- Building types on top of other types
- Inheritance hierarchies can be set up to suit your needs
- Code reuse is improved
Inheritance hierarchie
- The most fundamental class on the top
- The most specialized class on the bottom
Public inheritance
- derived classes can access and use public members of the base class. But the derived class can't directly access private members
- the same applies to friends of the derived class. They have access to private members of the derived class, but don't have access to the base class
class Player: public Person
Base class: Person | Derived class: Player | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
class Player: protected Person
Base class: Person | Derived class: Player | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
class Player: private Person
Base class: Person | Derived class: Player | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
-
Through the base class access specifiers we can control how relexed or constrained the access of base class members to the derived class is.
-
Regardless of the access specifier, private members of a base class are never accessible from a derived class
-
A friend function within a derived class may access public/protected members from the base class!!!
When inheriting from a class that is derived from a base class with private inheritance, none of the inherited members can be accessed.
Example:
class Person {
public:
int m_1;
protected:
int m_2;
private:
int m_3:
};
class Engineer: private Person {
// m1 private access
// m2 private access
// m3 private access
};
Class CivilEngineer: public Engineer {
// m1 private access
// m2 private access
// m3 private access
}
Although there are workarounds, it's not a good idea to use these workarounds. Code gets unclear - what is the access level of a field.
Best practice: Always provide a default constructor for your classes, especially if they will be part of an inheritance hierarchy.
Constructors are called from the most basic to the most specilized class in the hierarchy.
The compiler will create a default copy constructor which will work assumed there is no dynamic memory allocation used in member fields.
Base constructors are not inherited by default. You can use a construct to explicit define inheritance of the base constructor. This causes the compiler to generate a default constructor as result of the inheritance.
The access identifier (public, protected, private) of the base class constructor will be used for the generated derived class constructor.
Some facts about Inheritance of Base constructors
-
Copy constructors are not inherited. But you won't usually notice this as the compiler will insert an automatic copy constructor
-
Inherited constructor are base constructors. They have no knowledge of the derived class. Any member from the derived class will just contain junk or whatever default value it's initialized with
-
Constructors are inherited with whatever access specifier they had in the base class
-
On top of derived constructors, you can add your own that possibly properly initialize derived member variables
-
Inheriting constructors adds a level of confusion to you code, it's not clear which constructor is building your object. It is recommended to avoid them an only use this feature if no other option is available
In the example the class hierarchie consists of:
Person <- Engineer: public Person <- CivilEngineer: public Engineer
If a CivilEngineer object is instantiated the order in which constructors is called is:
- Person
- Engineer
- CivilEngineer
If the CivilEngineer object is destroyed the order in which destructors is called is:
-
CivilEngineer
-
Engineer
-
Person
In the inherited (child) class member variables or methods can have the same name as in the base class.
The behaviour of C++ is to hide/override the base class members/methods with the same name.
Polymorphism: a number of derived classes have a common base class. From within the derived class a different implementation of a base class member function is accessible.
Static binding: The compiler just looks at the pointer to decide with member function version to call. It sees base class * and calls base class.member function.
- See Polymorphism
Polymorphism with virtual member methods will have impact on memory claims.
The references to virtual memberfunctions are stored in a virtual table.
The override keyword protects you for overwriting methods in the inheritance hierarchy
If you overload a virtual method, you should also implement that overloaded function in all derived classes.
Using static member variables combined with inheritance
With final you can
-
restrict how you override methods in derived classes
-
restrict how you can derive from a base class
-
See Final
Functions with default aguments
- default arguments are handled at compile time
- virtual functions are called at run time with polymorphism
- if you use default arguments with virtual functions, you migth get weird (unexpected) results with polymorphism
Best practice: do not use default parameters with virtual functions!
Best practice: always define virtual destructors
Dynamic cast gives functionality
- transform from base class pointer or reference to derived class pointer or reference at run time
- make it possible to call non polymorphic methods on derived objects
Typecasts are done to make code more robust. Usually in functions which accept a base type but need functionality for a derived type.
Overusing down casts is a sign of bad design. If you are doing a lot of polymorphic function calls on derived object, you should make that function polymorphic in the first place.
- See Dynamic_cast
Reasons not to call virtual functions from constructors & destructors:
- you won't get polymorphic results, but static binding since derived object is not yet instantiated when in base constructor a virtual function is called.
Best practice: Never call virtual functions from a constructor or destructor.
Pure virtual functions
-
if a class has at least one pure virtual function, it becomes an abstract class
-
you can't create objects of an abstract class, if you do, you'll get a compiler error
-
derived classes from an abstract class must explicitly override all the pure virtual functions from the abstract parent class. It they don't they themselves become abstract
-
pure virtual functions don't have an implementation in the abstract class. They are ment to be implemented by the derived class(es)
-
you can't call the pure virtual functions from the constructor of the abstract class
-
the constructor of the abstract class is used by deriving class to build up the base part of the object
Abstract classes as interfaces
-
an abstract class with only pure virtual functions and no member variables can be uses to model what is called an interface in OO programming
-
an interface is a specification of something that will be fully implemented in a derived class, but the specification itself resides in an abstract class
-
an interface is something one can attach to a class to implement functionality