C++ Crash Course

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

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:

0 => false => 0
otherwise => true => 1

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 for class and struct), while the semicolon is optional for namespace 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:

*** Construct objects
Construct BaseClass 1 1
Construct BaseClass
Construct SubClass
*** Call non virtual methods
BaseClass MethodNonVirtual 1 1 0
BaseClass MethodNonVirtual 2 0 3
SubClass MethodNonVirtual 2 0 5

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:

*** Call virtual methods
BaseClass MethodVirtual 1 1 0
SubClass MethodVirtual 2 0 5
SubClass MethodVirtual 2 0 5

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:


Copyright © 2011-2024 Spraylight GmbH.