Prolog
This crash course is intended for programmers who already have programming experience with object oriented programming languages like Java or C# and should give those developers a quick start into C++ programming with the Murl Engine. Programming beginners should rather start with a good C++ beginner’s book. The last section of this chapter contains a list with recommended books and online resources about C++ for further reading.
Content
- Header
- Namespace
- Basic Data Types
- Control Structures
- Classes and Objects
- References & Pointers
- Copy Constructor, Copy Assignment Operator
- Cast Operators
- Inheritance and Polymorphism
- Exceptions
- Dynamically Created Elements
- String, Container
- Further Reading
Header
C++ differs between header files (.h
) and cpp files (.cpp
). Header files contain the declarations while cpp files contain the implementations. To use elements of another cpp file (file_b.cpp
) in a cpp file (file_a.cpp
), it is sufficient to include the header file (file_b.h
) using the #include
statement.
#include "file_b.h"
Each header file is enclosed by the preprocessor directives #ifndef __HEADER_NAME__
#define __HEADER_NAME__
and #endif
. These directives represent an include guard and prevent multiple evaluations of the same header file.
#ifndef __MY_CLASS_H__ #define __MY_CLASS_H__ // declarations … #endif
Further details can be found here: www.cplusplus.com/forum/articles/10627/
Namespace
Global names may lead to name conflicts, especially in bigger projects. Namespace sections can be used to divide the global namespace and avoid such conflicts. A namespace is created with the namespace
statement and is delimited by curly braces:
namespace MyNamespace { UInt32 myVar; void MyFunction(); }
A namespace can be built up from multiple unconnected blocks and multiple namespaces can be nested into each other.
Outside a namespace block, elements can be accessed with the scope operator (::
):
MyNamespace::myVar = 42;
The keyword using namespace …
can be used to import all elements of a namespace, thus avoiding the need to qualify the name (with the scope operator). This statement should be used with care, because it somewhat annuls the concept of namespaces.
using namespace MYNameSpace; myVar = 42;
All Murl Engine classes are defined in the namespace Murl. For the full namespace list see Murl Engine namespace list.
- Note
- Attention! Never use the
using namespace …
statement in a header file!
Basic Data Types
Fundamental Data Types
C++ compilers are free to define the standard C++ data types (short
, int
, float
etc.) with e.g. different bit depths and value ranges. For this reason, it should be considered to always use the platform-independent data types defined in the framework (Note the upper-case, e.g. Char
instead of char
):
Initialization can be done in two different notations:
UInt32 myFirstVariable = 42; UInt32 mySecondVarable(42);
Bool
A Bool
variable can only represent one of two states, true
or false
. The following applies for conversions between Bool and integer variables:
Example:
Bool myBool = false; // myBool is false myBool = 0; // myBool is false myBool = Bool(-42); // myBool is true SInt32 myInt = myBool; // myInt is 1
enum
The keyword enum
can be used to define an enumeration. An integer number in ascending order starting from 0 is assigned to each name in the enumeration. It is also possible to explicitly assign a number to an enumeration name.
Example:
enum MyState { IDLE, RUNNING, SLEEPING, }; ... MyState currentState = IDLE; ... UInt32 intValue = currentState; intValue = (intValue + 1) % 3; currentState = static_cast<MyState>(intValue); ... switch(currentState) { case IDLE: Debug::Trace("IDLE"); break; case RUNNING: Debug::Trace("RUNNING"); break; case SLEEPING: Debug::Trace("SLEEPING"); break; }
- Note
- The definition
enum { ... };
needs to end with a semicolon (as forclass
andstruct
), while the semicolon is optional fornamespace
definitions.
const
The keyword const
can be used to define elements that may not change their value:
UInt32 const MY_INT(400); // Alternative: UInt32 const MY_INT = 400;
Besides variables, also parameters, return values, member methods, pointers etc. may be declared as const
. The const
keyword is always applied to the element to the left of the keyword, except if no left element exists. In this case it is applied to the element to the right of the keyword.
UInt32 myInt1; UInt32 myInt2; UInt32 const * p1 = &myInt1; // Zeiger auf konstanten UInt32 p1 = &myInt2; // geht *p1 = myInt2; // geht nicht, UInt32 konstant UInt32 * const p2 = &myInt1; // konstanter Zeiger auf UInt32 p2 = &myInt2; // geht nicht, Zeiger konstant *p2 = myInt2; // geht
An object that has been made const
, for example by being passed as const
parameter, can only have those of its methods called that are explicitly declared const
. Therefore it is important to use const
consistently and comprehensively – see also isocpp.org/wiki/faq/const-correctness.
Control Structures
if Statement
Example for an if
/ else
conditional expression:
if (varA >= varB) { i = varA; } else { i = varB; }
Example for an equivalent statement using the ternary operator (?:
):
i = (varA >= varB) ? varA : varB;
switch Statement
Example switch
statement:
switch(currentState) { case IDLE: Debug::Trace("IDLE"); break; case RUNNING: Debug::Trace("RUNNING"); break; case SLEEPING: Debug::Trace("SLEEPING"); break; default: Debug::Trace("Unknown state"); }
for Loop
Example for
loop:
for (UInt32 size = myArray.GetCount(), i = 0; i < size; i++) { Debug::Trace("Element %d has value %d\n", i, myArray[i]); }
while loop
Example while
loop:
while (!finished) { // do something }
Example do
while
loop:
do { // do something } while (!finished)
Break and Continue
The break
statement terminates the current loop. The continue
statement causes the loop to skip the remainder of its body and jump to the next loop iteration.
Classes and Objects
Class Definition
Example of a point class Point2d
in the namespace Murl::MyApp
:
#ifndef __POINT_2D_H__ #define __POINT_2D_H__ #include "murl_app_types.h" namespace Murl { namespace MyApp { class Point2d { public: Point2d(); // Default Constructor Point2d(Double x, Double y); // Constructor ~Point2d(); // Destructor Double GetX() const; // Method declaration Double GetY() const; void SetXY(Double x, Double y); private: Double mX, mY; // Member variable }; } } #endif
#include "point_2d.h" using namespace Murl; // Default Constructor MyApp::Point2d::Point2d() : mX(0) //Initialization of mX with 0 , mY(0) //Initialization of mY with 0 {} // Constructor MyApp::Point2d::Point2d(Double x, Double y) : mX(x) //Initialization of mX with x , mY(y) //Initialization of mY with y {} // Destructor MyApp::Point2d::~Point2d() { } Double MyApp::Point2d::GetX() const { return mX; } Double MyApp::Point2d::GetY() const { return mY; } void MyApp::Point2d::SetXY(Double x, Double y) { mX = x; mY = y; }
The constructor is called automatically whenever a new instance of a class is created and serves as initializer for the new object. The initialization of the member variables is preferrably done with an initialization list. An initialization list is started by a colon after the constructor method name. All variables and object constructors are then specified as a comma separated list.
The destructor is called automatically whenever an existing object needs to be destroyed (released) and can be used to release allocated storage.
Both constructor and destructor can also be omitted. In such a case, an empty constructor/destructor is created implicitly.
- Note
- Base classes (classes which serve as base classes for derived classes) should in almost all cases have a
virtual
declared destructor – see also stackoverflow.com/questions/461203/when-to-use-virtual-destructors.
Access rule attributes are used to specify which elements are accessible from outside the class:
public: // Can be accessed by anyone. private: // Not accessible from outside. protected: // Can be accessed by derived classes.
Instances
Creation of an object instance:
MyApp::Point2d point1; //Create instance with parameterless constructor MyApp::Point2d point2(42.3,0); //Create instance with parameter constructor
- Note
- Attention! No new is used and no braces are used with the parameterless constructor!
MyApp::Point2d point2(); // Wrong! A parameterless method with the name point2 is declared! MyApp::Point2d point3 = new Point2d(); // Wrong! Invalid syntax.
Alternative equivalent notations:
MyApp::Point2d point1; // no braces! MyApp::Point2d point2 = MyApp::Point2d(); MyApp::Point2d point3(42.3, 0); MyApp::Point2d point4 = MyApp::Point2d(42.3, 0);
The memory for such an instantiated object is released automatically, as soon as the object is out of scope. Hence there is no need to release memory (automatic storage duration).
Alternatively an object can also be instantiated with new
. Such an object must be released with delete
(dynamic storage duration) - see also Dynamically Created Elements.
Usage of member methods:
Double x2 = point2.GetX(); point1.SetXY(x2, 0);
References & Pointers
Reference
References in C++ are data types which point to an element. They always refer to the element that was used during initialization. A reference is declared with the &
operator and must be initialized on creation. After creation the reference can be used like a normal variable.
UInt32 myInt = 41; UInt32 & myIntRef = myInt; // Reference to myInt myIntRef++; // myInt == 42 MyApp::Point2d myPoint; MyApp::Point2d & myPointRef = myPoint; myPointRef.SetXY(1, 1); // myPoint == (1/1)
Pointer
Pointers are variables that point to an element on a specific memory location. They have a type and store a memory address.
The indirection operator (*
) is used to access the variable at the stored memory address. Without indirection operator the memory address value is accessed.
The address-of operator (&
) can be used to determine the memory address of a variable.
UInt32 myInt = 41; UInt32* myIntPointer; // Pointer to UInt32 myIntPointer = &myInt; // Store address of myInt in pointer *myIntPointer += 1; // increment myInt by one (myInt == 42) myIntPointer += 1; // Attention! Without dereferencing the addr. is incremented by 1 (element size)! // The pointer is now pointing to an unknown memory location!
If a pointer is pointing to an object, the elements of the object can also be accessed with the arrow operator (->
):
MyApp::Point2d myPoint; MyApp::Point2d* myPointPointer = &myPoint; myPointPointer->SetXY(1.0, 1.0); (*myPointPointer).SetXY(1.0, 1.0); // equivalent notation
When should you use pointers and when references? As a rule of thumb: Use reference wherever you can, pointers wherever you must. Avoid pointers until you can't. The reason is that pointers make things harder to follow/read, are less safe and far more dangerous.
- Note
- The
&
character is interpreted differently depending on the usage (as characters of other operators), e.g.:- Operator to declare references
- Adress-of operator
- Bitwise AND operator
Call by Reference / Call by Value
Parameters of function calls can be passed as value (call by value) or as reference (call by reference).
For objects it is usually better to pass the object as reference. If you want to make sure that a passed variable may not be altered during the function call, the parameter must be declared as const
additionally.
void f1(const MyApp::Point2d& p) // Call by reference { Double x = p.GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f1(myPoint1); ...
Alternatively a pointer to the object can be passed.
void f2(const MyApp::Point2d* pPointer) // Call by pointer reference { Double x = p->GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f2(&myPoint1); ...
If an object is passed as value, a copy of the object will be created and the copy is passed to the function.
void f3(MyApp::Point2d p) // Call by Value { Double x = p.GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f3(myPoint1); ...
Copy Constructor, Copy Assignment Operator
The copy constructor is called every time when a copy of an object is created. A default copy constructor which copies all member variables is always created implicitly. Alternatively you can define your own copy constructor:
Point2d(const Point2d& object) : mX(object.mX) , mY(object.mY) {}
The same applies to the copy assignment operator. This operator is called when an object is assigned to another one. An implicit copy assignment operator is defined automatically which copies the content of all member variables. Alternatively you can define your own copy assignment operator:
Point2d& operator=(const Point2d& object) { mX = object.mX; mY = object.mY; return *this; }
In most cases it is not necessary to create your own copy constructor or copy assignment operator. If so, keep the rule of three in mind, which claims that if a class defines one of the following three elements, destructor, copy constructor or copy assignment operator, it should probably explicitly define all three.
Cast Operators
C++ provides four different cast operators for the conversion of data types.
static_cast - for any normal conversion between types according to an existing conversion rule:
Double d = 42.1; UInt32 i = static_cast<UInt32>(d);
dynamic_cast - for the purpose of casting a pointer or reference up the inheritance chain:
class A{ virtual ~A(){} }; class B: public A{}; // derived from A B& b; A& a = b; B& new_b = dynamic_cast<B&>(a);
const_cast - to cast const elements to non const elements:
UInt32 myInt = 42; const UInt32& a = myInt; UInt32& b = const_cast<UInt32&>(a);
reinterpret_cast - to cast one type bitwise to another:
Double value=42.0; UInt64 address_of_value = reinterpret_cast<UInt64>(&value);
Inheritance and Polymorphism
- Note
- Unlike Java or C#, C++ supports multiple inheritance, which means that one class can inherit from multiple base classes.
Inheritance
A class can be derived from a base class (often also called super class) and inherits all methods and variables from the base class. It is also possible to overwrite variables and methods of the base class. The overwritten elements are only overlapped by the new elements and actually exist twice.
Example base class:
#ifndef __BASE_CLASS_H__ #define __BASE_CLASS_H__ #include "murl_app_types.h" #include "murl_util_string.h" #include "murl_debug_trace.h" namespace Murl { namespace MyApp { class BaseClass { public: BaseClass(); // Constructor BaseClass(UInt32 x, UInt32 y); // Constructor virtual ~BaseClass(); // Destructor void MethodNonVirtual(); virtual void MethodVirtual(); UInt32 mX, mY; UInt32 mMyVar; }; } } #endif
#include "base_class.h" using namespace Murl; MyApp::BaseClass::BaseClass() : mX(0) , mY(0) , mMyVar(0) { Debug::Trace("Construct BaseClass"); } MyApp::BaseClass::BaseClass(UInt32 x, UInt32 y) : mX(x) , mY(y) , mMyVar(0) { Debug::Trace("Construct BaseClass %d %d",x,y); } MyApp::BaseClass::~BaseClass() { } void MyApp::BaseClass::MethodNonVirtual() { Debug::Trace("BaseClass MethodNonVirtual %d %d %d", mX, mY, mMyVar); } void MyApp::BaseClass::MethodVirtual() { Debug::Trace("BaseClass MethodVirtual %d %d %d", mX, mY, mMyVar); }
Derived sub class:
#ifndef __SUB_CLASS_H__ #define __SUB_CLASS_H__ #include "base_class.h" namespace Murl { namespace MyApp { class SubClass : public BaseClass // Derived from BaseClass { public: SubClass(); // Constructor SubClass(UInt32 x, UInt32 y); // Constructor virtual ~SubClass(); // Destructor void MethodNonVirtual(); virtual void MethodVirtual(); UInt32 mMyVar; }; } } #endif
#include "sub_class.h" using namespace Murl; MyApp::SubClass::SubClass() : mMyVar(42) { Debug::Trace("Construct SubClass"); } MyApp::SubClass::SubClass(UInt32 x, UInt32 y) : BaseClass(x, y) // Constructor of base class , mMyVar(42) { Debug::Trace("Construct SubClass %d %d",x,y); } MyApp::SubClass::~SubClass() { } void MyApp::SubClass::MethodNonVirtual() { Debug::Trace("SubClass MethodNonVirtual %d %d %d", mX, mY, mMyVar); } void MyApp::SubClass::MethodVirtual() { Debug::Trace("SubClass MethodVirtual %d %d %d", mX, mY, mMyVar); }
Overwritten (overlapped) variables and methods can be accessed by explicitly specifying the base class.
Debug::Trace("*** Construct objects"); MyApp::BaseClass baseClass(1,1); MyApp::SubClass subClass; MyApp::BaseClass &b1 = baseClass; MyApp::BaseClass &b2 = subClass; MyApp::SubClass &b3 = subClass; b3.mX = 2; b3.mMyVar = 5; //mMyVar of SubClass b3.BaseClass::mMyVar = 3; //mMyVar of BaseClass Debug::Trace("*** Call non virtual methods"); b1.MethodNonVirtual(); b2.MethodNonVirtual(); b3.MethodNonVirtual();
We get as result:
The result confirms the statements above:
- The elements of the base class are also available in the derived class.
- The overwritten variable
mMyVar
obviously exists twice, and so do all other overwritten elements.
Polymorphism, Virtual Methods
The reference b2
is accessing the elements of the BaseClass
even though it is referencing a SubClass
object. We need to declare the method as virtual
to change this behavior. With a virtual method the "right" method is selected automatically, even if the pointer or the reference is from the type of a base class.
Debug::Trace("*** Call virtual methods"); b1.MethodVirtual(); b2.MethodVirtual(); b3.MethodVirtual();
We get as result:
This technique is often also called late binding or dynamic binding because the decision which method will be called is determined during run time and not during compile time - see also www.tutorialspoint.com/cplusplus/cpp_polymorphism.htm.
Interfaces, Pure Virtual Methods
C++ does not differentiate between interfaces and abstract classes. An interface method is declared in the base class as pure virtual. A pure virtual method is a class method that is defined as virtual
and assigned to 0. The derived class then overwrites and implements the pure virtual method.
... virtual void InterfaceMethod() = 0; ...
Exceptions
The Murl Engine does not use exceptions for various reasons (see also stackoverflow.com/..arguments-for-exceptions-over-return-codes). Users of the Murl Engine are of course free to use or avoid exceptions in the user code.
General syntax:
try { if (value == 0) throw 42; } catch (SInt32 ex) { Debug::Trace("An exception occurred. Exception Nr. " + Util::SInt32ToString(ex)); }
- Note
- C++ allows to
throw
objects as well as basic data types.
Dynamically Created Elements
C++ provides the possibility to dynamically create elements with the new
operator. The new
operator returns a pointer to the new element. Memory is allocated in the heap for such dynamically created elements. This memory must also be released with the delete
operator (or better with Murl::Util::Release
).
MyApp::Point2d* point4 = new MyApp::Point2d(); // Allocate object in heap MyApp::Point2d* point5 = new MyApp::Point2d(42.3,0); // Allocate object in heap Double x = point4->GetX(); // ... if (point4 != 0) { delete point4; // Release memory point4 = 0; } Util::Release(point5); // Release memory
String, Container
The Murl Engine does not use the STL container classes but uses adapted NTL container classes instead (see also www.ultimatepp.org/NTLvsSTL). A detailed description about the usage of the container classes can be found here: Tutorial #09: Container & Basics
Further Reading
This crash course is deliberately kept to a minimum and cannot replace a good C++ book. Further information about C++ can e.g. be found here:
- Books
- Online
- German