CITS2002 Systems Programming  
CITS2002 CITS2002 schedule  

The Hello World program in C++

Let's first consider the basic (anticipated) Hello World program written in C++.
As with many of the examples in this lecture, we will compare C++'s different features with equivalent ones of C11:

#include <iostream>

int main(void)
{
    std::cout << "Hello, world!\n"; 

    return 0;
}

Important points from this example:

  • #include <iostream>
    C++ source code is first passed through the same preprocessor as are C11 programs. Here, one of the standard C++ header files (with a similar role to C11's <stdio.h>) is included. Note that the C++ header file does not provide a filename suffix, and that the preprocessor will search a different set of directories to find the file. As much of C11's syntax is compatible with C++, we can also include (most) standard C11 header files.

  • std::cout << string;
    This statement outputs a text string (similar to C11) to a character output stream. The destination for the string is the std::cout stream, akin to stdout in C11. The << operator is not a bit-wise shift (as in C11), but indicating that the string is 'appended' to the stream.

  • What is std:: ?
    In C++, identifiers can be defined within a context, similar to a directory of identifiers, termed a namespace. When we want to access an identifier defined in a namespace, we tell the compiler to look for it in that namespace using the scope resolution operator (::). Here, we're telling the compiler to look for cout in the std namespace, in which many standard C++ identifiers are defined. A cleaner alternative is to use:

    using namespace std;

    This line tells the compiler that it should look in the std namespace for any identifier we haven't defined. We can omit the std:: prefix when writing cout.

CITS2002 Systems Programming, Lecture 23, p1, 17th October 2023.


Standard cin and cout I/O streams

Similar to C11's use of its standard I/O header file and library functions, C++ employs its iostream header and functions.

A little confusingly, it is possible to mix C11 and C++ I/O in the same programs, but it's not always safe to mix them in the same statement.

#include <iostream>

using namespace std;

int main(void)
{
    int x;

    cin >> x;
    cout << x/3 << ' ' << x*2;

    return 0;
}

In particular, many programmers moving from C11 to C++ prefer C11's printf() family of functions, over C++'s use of appending strings to a file-stream:

#include <iostream>

using namespace std;

void printVector(double x0, double x1, double y0, double y1)
{
//  equivalent to C11's  printf("(%f,%f) -> (%f,%f)\n", x0, y0, x1, y1);

    cout << "(" <<
         x0 << "," << y0 <<
         ") -> ("
	 << x1 << "," << y1 <<
         ")" << endl;
}

CITS2002 Systems Programming, Lecture 23, p2, 17th October 2023.


Function Overloading

In C11, if we wish to perform very similar operations, but on different datatypes, we need to define multiple functions, with different (but similar) names.

In contrast, C++ supports function overloading which permits functions with the same name to support different parameter types and different return types.
This is often termed "a function with multiple type-signatures"

#include <iostream>

void outputWithNewline(int x)
{
    cout << "Integer value: " << x << endl;
}

void outputWithNewline(char *x)
{
    cout << "String value: " << x << endl;
}

The C++ compiler recognises that the multiple functions are strongly related, and calls the correct function (generates the correct instructions) depending on how the functions are called (i.e. with what parameter types).

CITS2002 Systems Programming, Lecture 23, p3, 17th October 2023.


Passing parameters by reference

Recall that when we introduced pointers in C11, we used an example attempting to swap the values of two function parameters. Because the parameters were passed by value (a copy of them was made), the original memory destinations (provided when the function was called) were not changed:

#include <stdio.h>

//  Unsuccessful at swapping values

void swapints1(int a, int b)
{
    int tmp  =  a;

    a  =  b;
    b  =  tmp;
}

To address the problem, we employed the address-of operator so that the function would have access to the (original) memory locations. the result, however, was additional (confusing) punctuation:

#include <stdio.h>

//  Successful at swapping values when called as  swapint2(&x, &y);

void swapints2(int *a, int *b)
{
    int tmp  =  *a;

    *a  =  *b;
    *b  =  tmp;
}

To address this confusion, C++ introduces pass-by-reference parameters, which 'invisibly' treats parameters as pointers, without the additionsl syntax for dereferencing identifiers. Note that C++ still permits C11's dereferencing operator, and the code may be safely mixed within statements:

#include <iostream>

void swapints3(int &a, int &b)
{
//  Successful at swapping values when just called as  swapint3(x, y);

    int tmp  =  a;

    a  =  b;
    b  =  tmp;
}

CITS2002 Systems Programming, Lecture 23, p4, 17th October 2023.


Classes and internal methods

As with other object-oriented languages, C++ 'collects' strongly related data and functions operating on that data in classes.

C++ classes may legally have only data members (fields), with no internal methods, and they are then very similar to C11's structures.

In this example, the class MyVector has two public methods that have access to the class's data members.
They can access or modify the values associated with an instance of the class.

#include <iostream>

class MyVector {
public:
    Point start;
    Point end;

    void offset(double offsetX, double offsetY) {
	start.x += offsetX;
	end.x   += offsetX;
	start.y += offsetY;
	end.y   += offsetY;
    }

    void print(void) {
	cout << "(" << start.x << "," << start.y << ") -> ("
	     << end.x << "," << end.y << ")" << endl;
    }
};

Note that the methods offset() and print() are internal to the class definition and, thus, act as both declarations and definitions of the methods.

CITS2002 Systems Programming, Lecture 23, p5, 17th October 2023.


Classes with methods defined externally

As with functions in C11, we can separate the declarations and definitions of C++ methods.

As with C11, we employ header files to provide the declarations - this 'thing' exists elsewhere, and then define (implement) the method in a C++ source file:

#include <iostream>

// myvector.h - header file

class Point {
public:
    double x, y;

    void   offset(double offsetX, double offsetY);
    void   print(void);
};

class MyVector {
public:
    Point start, end;

    void  offset(double offsetX, double offsetY);
    void  print(void);
};

CITS2002 Systems Programming, Lecture 23, p6, 17th October 2023.


Classes with methods defined externally, continued

Here, the class's methods are defined 'away' from their declarations in the header file. Thus, we need to preface each method name with the name of class to which it belongs. To ensure that the defintions are type-compatible with the class's declarations, we use the preprocessor to include the class definition:

Here, we can think of Point as the namespace to which the methods belong:

#include <iostream>
#include "myvector.h"

// myvector.cpp - method implementation

void Point::offset(double offsetX, double offsetY)
{
    x += offsetX; y += offsetY;
}

void Point::print(void)
{
    cout << "(" << x << "," << y << ")";
}

void MyVector::offset(double offsetX, double offsetY) 
{
    start.offset(offsetX, offsetY);

    end.offset(offsetX, offsetY);
}

void MyVector::print(void)
{
    start.print();
    cout << " -> ";
    end.print();
    cout << endl;
}

CITS2002 Systems Programming, Lecture 23, p7, 17th October 2023.


Initialising the data members of a class with constructors

In most C11 programs, when introducing a new variable (often near the top of a block of statements) we immediately follow the introduction by multiple statements which initialise the variable.

#include <iostream>

MyVector vec;

vec.start.x = 0.0;
vec.start.y = 0.0;
vec.end.x   = 0.0;
vec.end.y   = 0.0;

Point p;

p.x         = 0.0;
p.y         = 0.0;

If a scalar variable, this presents little problem, but for structures or arrays, we must ensure that the initial value is 'suitable', and that we don't miss any values.

This is often achieved with an additional function, defined physically 'close' to the definition of the structure or array datatype.

In C++, as with most object-oriented languages, we can combine the class's fields (internal variables) with a method to initilise them. This special method, termed a constructor, doesn't require any 'external' code to have knowledge of the implementation of the class, or how its fields are initialised.

A constructor is a method that is called when a class instance is created.

#include <iostream>

class Point
{
public:
    double x, y;
    Point(void) {
	x = 0.0; y = 0.0;
	cout << "Point instance created" << endl;
    }
};

int main(void) {
    Point p; // Point instance created
    // p.x is now 0.0, p.y is now 0.0
}

#include <iostream>

class Point
{
public:
    double x, y;
    Point(double nx, double ny) {
	x = nx; y = ny;
	cout << "2-parameter constructor" << endl;
    }
};

int main(void) {
    Point p(2.0, 3.0); // 2-parameter constructor
    // p.x is 2.0, p.y is 3.0
}

CITS2002 Systems Programming, Lecture 23, p8, 17th October 2023.


Multiple contructors using function overloading

Previously we saw that C11 permits a single function 'name' to have multiple type-signatures - multiple functions with same name, but with different sets of parameters.

The same concept applies to class constructors - a class may have multiple constructors (each named after the class), that permits an instance of the class to be created and initialised in different ways:

#include <iostream>

class Point {
public:
    double x, y;

    Point(void) {
	x = 0.0; y = 0.0;
	cout << "default constructor" << endl;
    }

    Point(double nx, double ny) {
	x = nx; y = ny;
	cout << "2-parameter constructor" << endl;
    }
};

int main(void)
{
    Point p; // default constructor
    // p.x is 0.0, p.y is 0.0

    Point q(2.0, 3.0); // 2-parameter constructor
    // q.x is 2.0, q.y is 3.0
}

CITS2002 Systems Programming, Lecture 23, p9, 17th October 2023.


Scoping and Memory

Recall some important issues with C11 and memory allocation for identifiers:

  • Whenever we declare a new variable, such as int x, memory is allocated

  • When can this memory be freed up (so it can be used to store other variables)?

  • When the variable goes out of scope

  • When a variable goes out of scope, that memory is no longer guaranteed to store the variable's value

C++'s new operator

C++ provides another mechanism to allocate memory, which will remain allocated until manually de-allocated.

This is very similar to C11's malloc family of functions.

The new operator returns a pointer to the newly allocated memory:

#include <iostream>

    int *x = new int;

Of nore:

  • If using int x; the allocation occurs on the run-time stack,
  • If using new int; the allocation occurs within the heap.

C++'s delete operator

In a manner similar to C11's free() function, C++'s delete operator de-allocates memory that was previously allocated using new

#include <iostream>

int *x = new int;

// use memory allocated by new
....
delete x;

CITS2002 Systems Programming, Lecture 23, p10, 17th October 2023.


Dynamically allocating memory for arrays

If we use new[] to allocate arrays, they can have variable size

#include <iostream>

    int numItems;
    cout << "how many items?";

    cin >> numItems;
    int *arr = new int[numItems];

We then de-allocate arrays with delete[] :

    delete[] arr;

Allocating class instances

new can also be used to allocate a class instance The appropriate constructor will be invoked

#include <iostream>

class Point {
public:
    int x, y;
    Point(void) {
	x = 0; y = 0;
	cout << "default constructor" << endl;
    }
};

int main(void) {
    Point *p = new Point;
    ....
    delete p;
}

The appropriate constructor will be invoked.

#include <iostream>

class Point {
public:
    int x, y;
    Point(int nx, int ny) {
	x = ny; x = ny; cout << "2-arg constructor" << endl;
    }
};

int main(void) {
    Point *p = new Point(2, 4);

    delete p;
}

CITS2002 Systems Programming, Lecture 23, p11, 17th October 2023.


Class destructors

A class destructor is called when the class instance is de-allocated

  • If the class was allocated with new, when delete is called, and

  • If allocated on the runtime stack (introduced in a block of statements), when the identifier goes out of scope.

#include <iostream>

classs Point {
public:
    int x, y;

    Point(void) {
	cout << "constructor invoked" << endl;
    }

    ~Point(void) {
      cout << "destructor invoked" << endl;
    }
}

CITS2002 Systems Programming, Lecture 23, p12, 17th October 2023.


File streams

File handling in C++ works almost identically to terminal input/output. To use files, we #include <fstream<> and then access two standard classes from the std:: namespace:

  • ifstream : allows reading input from files

  • ofstream : allows outputting to files

Each open file is represented by a separate ifstream or an ofstream object.

You can use ifstream objects in exactly the same way as cin and ofstream objects in the same way as cout, except that you need to declare new objects and specify what files to open.

#include <fstream>

using namespace std;

int main(void)
{
    ifstream source("source-file.txt");
    ofstream destination("dest-file.txt");

    int x;

    source >> x; // Reads one int from source-file.txt
    source.close(); // close file as soon as we're done using it

    destination << x; // Writes x to dest-file.txt

    return 0;
}   // close() called on destination by its destructor

CITS2002 Systems Programming, Lecture 23, p13, 17th October 2023.


C++ Templates

We have seen that functions can take arguments of specific types and have a specific return type. We consider templates, which allow us to work with generic types.

#include <iostream>

int add(const int x, const int y) {
    return x + y;
}

int add(const double x, const double y) {
    return x + y;
}

Using templates, rather than repeating function code for each new type we wish to support, we create a 'generic' function that performs the same operation independent of the types of its parameters.

#include <iostream>

template <typename T>
T add(const T a, const T b) {
    return a + b;
}

The Standard Template Library (STL)

Part of the C++ Standard Library, the Standard Template Library (STL) contains many useful container classes and algorithms. As you might imagine, these various parts of the library are written using templates and so are generic in type. The containers found in the STL are lists, maps, queues, sets, stacks, and vectors. The algorithms include sequence operations, sorts, searches, merges, heap operations, and min/max operations.

The STL has many, many more containers and algorithms that you can use. Great reference material:

  • http://www.cplusplus.com/reference/stl and
  • http://www.cplusplus.com/reference/algorithm/.

CITS2002 Systems Programming, Lecture 23, p14, 17th October 2023.