1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Bolyai College
    3. C++ (for mathematicians)
    4. Imperative programming (BSc)
    5. Multiparadigm programming (MSc)
    6. Programming (MSc Aut. Sys.)
    7. Programming languages (PhD)
    8. Software technology lab
    9. Theses proposals (BSc and MSc)
  3. Research
    1. Sustrainability
    2. CodeChecker
    3. CodeCompass
    4. Templight
    5. Projects
    6. Conferences
    7. Publications
    8. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

Classes

Video:

cpp-mat-ea13-1.mkv

cpp-mat-ea13-2.mkv

cpp-mat-ea13-3.mkv

cpp-mat-ea13-4.mkv

In this chapter we describe how to implement (non-templated) classes in the C++ programming language. This chapter is intended to introduce basic object-oriented programming concepts, we will explain the notion of class, encapsulation, interface and implementation. In this chapter, we also teach how these can be implemented in C++.

Struct

It frequently happens that we have to express some information with multiple smaller data item. For example, suppose we want to create a type to express calendar date: a tuple of year, month and day is a natural way to express a Date item. All the year, month and day can be represented by a single integer (or even by some smaller type). However, to use a calendar date it will be extremely unpleasant to use three variables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int days_between( int, int, int, int, int, int);

void f()
{
 int year1 = 2018; 
 int month1 = 1;   
 int day1 = 25;

 int year2 = 2017;
 int month2 = 12;  
 int day2 = 25;

  // ok ?
  int n = days_between(year1,month1,day1,year2,month2,day2);
  // bug ?
  int n = days_between(year1,month2,day1,year2,month1,day2);
}

Such a usage is too verbose, and error-prone as one can mix e.g. month1 and month2. We would like to use a Date similar to built-in types: create a single Date variable, which represents the three integers together.

Most of the programming languages let us to create such aggregate data types, which we can build from existing types as a cartesian product. In Pascal, this is called record, in C and C++ this is called struct.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Date    // declaration of struct
{              // just a type, not a variable:
  int year_;   // no memory is allocated here
  int month_;
  int day_;
};             // struct is closed with semicolon 

// should declare after Date has been introduced
int days_between( Date later, Date earlier);

void f()
{
  // two Date variables:
  Date exam;   // memory is allocated here: 3 int (at least)
  Date xmas;   // memory is allocated here: 3 int (at least)

  exam.year_ = 2018;
  exam.month_ = 1;   // January
  exam.day_ = 25;

  xmas.year_ = 2017;
  xmas.month_ = 12;  // December
  xmas.day_ = 25;

  // ok, can't mix months
  int n = days_between( exam, xmas);
}

In lines 1-6 we declared the new type called Date with 3 fields, all of them is an integer. All Date variables will contain 3 integers, (but the overall size might be longer). The integers can be referred by their field names using the . (dot, field access) operator.

To make the initialization of a struct easier, we can use the { } syntax, similarly as we initialize arrays.

A struct can be assigned, can be passed as parameter (by value or by reference), could be returned from a function. A pointer (of type Date) can point to an instance of Date. If we have a pointer (let’s say ptr) to a Date variable, then (ptr).month_ can be abbreviated as ptr->month_.

1
2
3
4
  Date exam = { 2018, 1, 25 };  // aggregate initialization
  Date *ptr = &exam;
  ptr->month_ = 1; // same as (*ptr).month_ 
                   // which also refers to exam.month_

By default, there are no equality (==) or inequality (!=) operators defined for the struct.

Methods

Up to now, what we wrote about struct is true both for the C and the C++ programming languages. However, from now on we are speaking about features exist only in C++.

Suppose we delay the exam by 10 days. One can add 10 days to the exam.day_ field, however, January 35 is not a valid day.

1
2
3
  Date exam = { 2018, 1, 25 };  // aggregate initialization
  exam.day_ += 10;
  // exam date is now invalid (January 35)

It is both impractical and dangerous to let the users do this. To avoid such problems, we may provide functions, like next and add to update the Date variables. These functions can handle the edge-cases, like overflowing to the next month or even to the next year.

However, if these functions are global (namespace) functions, we would always have to pass the object (the variable) which we want to modify. Moreover, we have to pass it either as a reference or a pointer to it, since we want to change their value. This works, but is really inconvenient. Another problem with global functions is that the names might be collide with other global function names.

In object-oriented programming languages, we can express, that a function belongs to a struct (or class), writing it as a member function. A member function is declared inside the struct/class body and belongs to its scope. We can define member functions either inside the struct/class body or out of it (but in this second case we have to express, that this is a function that belongs to the scope of the class).

In some programming languages, member functions are called as methods. From now on, we will use the term class instead of struct. We will see, that a struct is just a very specific form of a class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct Date    
{              
  void next();    // only declaration
  void add(int n) // declaration with inline definition  
  { 
    for (int i=0; i<n; ++n) 
      next();     
  } 
  int year_;   
  int month_;
  int day_;
};
void Date::next() // the 'next' function of the 'Date' class
{
  ++day_;
  if ( /* day_ is over the last day of month_ */ )
  {
    day_ = 1;   // this->day_  
    ++month_;   // this->month_
    if ( 13 == month_ )
    {
      month_ = 1;
      if ( 0 == ++year_ ) year_ = 1; // quiz: why do this?
    }
  }
}
void f()
{
  Date xmas = { 2017, 12, 25 }; 
  Date exam = { 2018, 1, 25 }; 
  xmas.next();    // day_ inside next() will be xmas.day_
  exam.add(10);   // day_ inside add() will be exam.day_
  // xmas date is now December 26
  // exam date is now valid (February 4)
}

(Non-virtual) member functions are implemented as normal functions, but the hidden this parameter is passed as their very first argument pointing to the actual object on which we called the method. Every access to members are done via the this pointer, e.g. the day_ member is accessed under-the-hood by this->day_. The this pointer is special, must not be changed or take its address.

Encapsulation

Previously, we provided the next() and add() functions to the users of the Date type. However, users still could circumvent using these functions since they could access the day_, month_, and year_ data members directly. And if an interface (set of functions, contracts) can be misused, there will be someone misusing it.

It would be nice to ask the compiler to detect if somebody makes such a mistake. For this purpose, object-oriented programming languages give you the ability to hide the implementation details and restrict the users to use only the public interface. This gives some flexibility to the implementation for possible improvements and provides a guarantee to the user that whatever they can use, will be maintained in the future. The separation of the interface and the implementation is called encapsulation.

In C++, the interface and the implementation is separated by the keywords public, protected and private. These are called access specifiers and each of these introduces a different accessibility (or in other words visibility) of the subsequent members.

The interface usually consists of member functions: actions that you can execute on the objects of a class. The data members are usually read and written via member functions, this way we can control which values are set (e.g. we can forbid to set a month to 13 or so we can ensure it’s invariants ). The interface can contain other elements too, like exceptions, type definitions, etc. We don’t discuss these elements in details in this course. In the C++ programming language, the interface is marked by the public keyword. Members with public visibility are accessible anyone.

Global (namespace) functions working on objects of a class are also considered as the part of the interface of the class, even they are not members. Since they are not members, their access is not restricted, so they can be used from anywhere of the program.

The implementation is the place where we put the data members and functions which we do not want to expose to the user. These methods are called as helper functions. In the C++ programming language, the implementation details are marked by the private keyword. Members with private visibility are only accessible from the members (and friends) of the same class. However, visibility is a class and not object level notion. By that said, private members of object x are accessible from any members of object y if x and y have the same type.

The members with protected visibility are not accessible from the outer world, but only from the members of the class and the members of the derived classes of the class. (See derived classes later.) Protected members form a kind of interface to its derived classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Date    
{    
public:          // subsequent members are public
  Date( int y, int m, int d)   // constructor
  : year_(y), month_(m), day_(d) {} //initialize members

  void next();   
  void add(int n)  
  { 
    for (int i=0; i<n; ++n) 
      next();     
  } 
private:         // subsequent members are private 
  int year_;   
  int month_;
  int day_;
};
void Date::next()  // can not express visibility here, 
{                  // it is done in the class body
  ++day_;
  if ( /* day_ is over the last day of month_ */ )
  {
    day_ = 1;  // ok: day_ and month_ are private, but 
    ++month_;  // accessible from members of the same class
    if ( 13 == month_ )
    {
      month_ = 1;
      if ( 0 == ++year_ ) ++year_; 
    }
  }
}
void f()
{
  Date xmas = { 2017, 12, 25 }; 
  Date exam = { 2018, 1, 25 }; 
  xmas.next();     // ok: next() is public
  exam.add(10);    // ok: add()  is public
  exam.day_ += 10; // error: day_ is private
}

Structs can also have members with different visibility. The only two differences between structs and classes are that

  • the default visibility for structs is public while for classes is private.
  • in case of inheritance (see later) the default inheritance type for structs is public, for classes is private.

Otherwise, struct and class keywords are mostly interchangeable.

Constructor

As now we have no access to the data members, we have to provide public member functions to access them: functions to set and function to read them. As we have private data members we cannot use any more the aggregate initialization. In lines 4-5 we created a constructor which is responsible to initialize the object. In this example, the body of the constructor is empty, but we use the initialization list; a comma separated list of data members to initialize them from the constructor parameters. We can write multiple constructors overloaded by their parameters. The constructor in C++ is a function with the same name as the class it belongs to. Constructors have no return type (not even void).

Files

The Date class is intended to be a reusable type. Various programs and types could use it, e.g. a Date field can be a data member of an Account or a Person (as a birth date).

For this purpose, we have to compile the code of the Date class into a separate object, so we can link it (statically or dynamically, see Lecture 1) to any program which wants to use the Date type. Therefore, we will implement the Date class in a separate source file: date.cpp, and compile to a separate object. However, a client should know all the essential information about what is a Date class, how it can be used: its interface. Therefore, we separate the declaration of the Date class in a header file: date.h, and this header file should be included to every client code which want to use the Date class (e.g. main.cpp). For keeping consistency between the interface and the implementation, we will include date.h also into date.cpp.

alt text

For a class template, the setup is a bit different. We provide both the interface and the implementation for templates in the header file, so we do not write a separate source file for its definition - for technical reasons.

An example class

Here we define a simple class to represent Date information storing year, month and day as integers and providing basic access and modification functions. Naturally, this class is just for demonstration purposes, for real programs one should use a more detailed implementation.

First, we place the declaration of the class (with all public, protected and private member functions and fields) into date.h header file. (The const keyword marks constant member functions, we discuss this in the detailed description section.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef DATE_H
#define DATE_H

#include <iostream>
class Date
{
public:
  // constructor with default paramameters
  Date( int y, int m=1, int d=1); 

  int   getYear()  const;  // get year
  int   getMonth() const;  // get month
  int   getDay()   const;  // get day
  void  setDate( int y, int m, int d);   // set a date

  void  next();       // set next date
  void  add( int n);  // add n dates

  void get( std::istream& is);       // read from a stream
  void put( std::ostream& os) const; // write to a stream
private:
  void checkDate(int y, int m, int d); // check date, 
                                       // exit on fail
  int year_;    // data members 
  int month_;   // to represent a date
  int day_;     // with three integers
};
#endif // DATE_H

Then we define (provide the body for) the member functions in date.cpp source file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <cstdlib>  // for std::exit

#include "date.h"

/* anonymous namespace, visible only in this source */
namespace 
{
  const int day_in_month[] = {
    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
  };
}
Date( int y, int m, int d)
{
  setDate( y, m, d);
}
void Date::setDate( int y, int m, int d)
{
  checkDate( y, m, d);
  year_  = y;
  month_ = m;
  day_   = d;
}
void Date::next()
{
  ++day_;
  /* TODO: leap year */
  if ( day_-1 == day_in_month[month_-1])
  {
    day_ = 1;
    ++month_;
  }
  if ( 13 == month_ )
  {
    month_ = 1;
    if ( 0 == ++year_ ) ++year_;
  } 
}
void Date::add( int n)
{
  for (int i = 0; i < n; ++i)
  {
    next(); /* KISS == Keep It Simple, Stupid */
  }
}
void Date::get( std::istream& is)
{
  int y, m, d;
  if ( is >> y >> m >> d ) // have to check for success
  {
    setDate( y, m, d);
  }
}
void Date::put( std::ostream& os) const
{
  // better to build on accessors;
  os << "[ " << getYear()  << "."
             << getMonth() << "."
             << getDay()   << " ]";
}
void Date::checkDate( int y, int m, int d)
{
  // TODO: leap year
  if ( 0 == y )                         std::exit(1);
  if ( m < 1 || m > 12 )                std::exit(1);
  if ( d < 1 || d > day_in_month[m-1] ) std::exit(1);
}

To implement a correct behavior for the leap year problem is left to the reader as an exercise.

Detailed description

Members

A class is introduced by either the class or struct keyword. The only difference between using class or struct is the default visibility of the class and the default behavior on inheritance.

Classes form a namespace with its own name. All members are declared inside this namespace. When members of a class are referred from inside a member function of the same class, the member names are accessible (except when they are hidden by some locally declared name). When members are referred from outside of the class namespace (e.g. from an other class or from a namespace function) they should be qualified by the class name.

1
2
3
4
5
6
7
8
9
struct XClass
{
  void func();
  int  i;
};
void XClass::func()
{
  i = 5;  // inside Xclass namespace "i" means "Xclass::i"
}

Language elements declared inside a class are simply called members. Members of a C++ class can be data members (frequently called as fields or attributes), member functions (in C++ the expression method is sometimes restricted to virtual functions), type declarations and some other language elements, like constants, enums, etc.

Static data members and member functions have special behaviour, we will discuss them later.

Data members

The (non-static) data members form the memory area of the objects belonging to the class. Data members can be of any type, including pointers, arrays, etc. The lifetime of the data members are the same as the object itself.
The creation order is the same as the order of declaration (even when the constructor initializer list is different). The destruction order is the opposite of the construction order.

Class data members can be const. Const data members are immutable under the lifetime of the objects, but they might have different value in each objects.

Data members declared as mutable can be changed even for constant objects or inside constant member functions.

1
2
3
4
5
6
7
class XClass
{
            int  ifield; // data field 
            int *iptr;
  const     int  id;     // const, must be initialized
  mutable mutex  mut;    // mutable field
};

A data member declaration does not denote an actual memory area: it is rather an offset inside an object. It becomes a real memory area only when a real object is defined, and the data member designates a certain part inside that object. As a consequence, normal pointers can not be set to a data member itself, it is only a member of a specific object that can be pointed by a usual pointer.

There are, however, pointer to member which can be assigned to a member, with the meaning as the offset inside the object.

Member functions

The non-static member functions are functions called on an object. In every non-static member function a pointer to the actual object called this can be used. The body of a member function is inside the namespace of the class, i.e. names used inside the function body belong primarily to the class namespace.

Member functions are implemented as “real” functions with a hidden extra first parameter: the this pointer. The this pointer is declared as a no-const pointer to the class for normal member functions, and as a pointer to const in constant member functions. The actual argument of the this parameter is set to a pointer to the actual object on which the member function is called. All the members (data or function) accessed inside a member function is accessed via the this pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
void XClass::func(int x)  // non-const member function
{
  // pointer this is declared here as "XClass *"
  ifield = 5; // same as this->ifield    
  id = 6;     // ERROR: id is const member, must not changed
  mut.lock(); // m is XClass::m if m is declared in XClass
};
void XClass::func(int x) const  // const member function
{
  // pointer this is declared here as "const XClass *"
  ifield = 5; // ERROR: func() is a const member function
  mut.lock(); // mut can be modified since it is mutable
};

This ensures two things:

  1. only constant member functions can be called on constant objects.

  2. inside a constant member function (whether it is called on a const or non-const object) no data member can be modified.

1
2
3
4
5
6
7
      XClass obj;  // non-const object
const XClass cobj; // const object

 // call XClass::f(int), passing this as XClass*
 obj.func(1);  
 // call XClass::f(int) const, passing this as const XClass*
 cobj.func(1);  

Usually the minimal necessary set of member functions on a class is defined. The rest of the convenience operations can be defined as non-member, so-called namespace functions.

1
2
3
4
void print(const XClass &xc)
{
  // calling public methods on XClass
}

Operators defined on classes are discussed in the next chapter.

Visibility

There are three visibility categories of class members: public, protected and private. Each category is denoted with the label of the same keyword. Visibility categories can be repeated and may appear in any order. When the class is introduced by the class keyword, the default visibility area is private, in case of using struct it is public.

1
2
3
4
5
6
7
8
9
10
class XClass
{
  // private for class, public for stuct
public:
  // visible from everywhere
protected:
  // visible only for derived classes and friend
private:
  // visible only for class members and friends
};

There is a special way to access non-public data members and calling non-public member functions: friends. Friend functions have the unlimited access to all data members to the class. Friend classes are classes which have all members as friend functions to the class.

1
2
3
4
5
6
7
8
9
class XClass
{
  friend void nonMemberFunc();
  friend class OtherClass;
};
void nonMemberFunc()
{
  // can access all XClass members
}

We can place the friend declarations in any visibility sections.

Special member functions

There are special member functions in the class we can define.

Constructor

The constructor has the same name as the class. The constructor is called when a new object has been created and is responsible to initialize it. The programmer may define multiply constructors overloaded by their parameters. The one without parameters is called default constrator.

Copy constructor

The copy constructor has also the same name as the class but has a special signature: receives a single parameter of the same class by reference. The copy constructor is responsible to initialize the newly created object from an other object of the same class.

The signature of the copy constructor is usually const reference of the type to ensure that the source of the initialization is not modified, but there are exceptions: smart pointers may declare the parameter as non-const.

Assignment operator

An assignment operator is used when a new value is to be assigned to an existing object. The parameter list is optional, but it is usually the same as that of the copy constructor. Although the return value can be freely defined, most design rules requires returning the freshly assigned object by reference.

Destructor

The destructor is an optional single method with no parameter. A destructor is called when an object goes out of life. A destructor is mainly repsonsible to automatically free allocated resources of the object.

Move constructor (since C++11)

Similar to the copy constructor but has a non-const right value reference parameter to initialize the object moving the value from the source (and such way stealing the resources from it).

Move assignment (since C++11)

Similar to assignment operator, but has a right value reference parameter to assign new value to an existing object moving the value from the source (and such way stealing the resources from it).

1
2
3
4
5
6
7
8
9
class XClass
{
  XClass();                       // default constructor
  XClass(const XClass &);         // copy constructor
  XClass(XClass &&);              // move constructor (C++11)
  XClass& operator=(const XClass &); // assignment operator
  XClass& operator=(XClass &&);   // move operator (C++11)
  ~XClass();                      // destructor
};

Special member functions are not needed to be defined. When a special member function is not defined the compiler generates one by the default memberwise copy semantics. Generation rules of the move special member functions are a bit more complex.

Constructors must not be declared as virtual functions. Destructor can be, and in certain design must be defined as virtual function. We will discuss copy constructor and destructor in the chapter on POD classes.

Static members

Static members are global variables with static lifetime. They are placed into the namespace of a class for only logical purposes. Static data members are not phisically part of any object: they are allocated outside of any objects. As their lifetime is static, they can be referred even when no object is exists of their class.

Inside the class deficition using the static keyword only declares the static data member. We must define it in exactly one source file (this is usually the one we implement the class methods).

Static member functions are global functions placed the namespace of the class. They also can be referred without existing objects of the class. Static member functions do not receive this parameter, therefore they must not refer to other members (data or function) without specifying the actual object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// xclass.h
// necessary includes here
class XClass
{
public:
  static XClass *create();  // static member function
private:
  XClass(int i) : id(nid++) { }  // not thread safe 
  const   int   id;   // contant data member definition
  static  int   nid;  // only declaration of static member
  static mutable std::mutex mut; // lock to defend nid
};

// xclass.cpp
int XClass::nid = 0;    // definition of static data member
XClass *XClass::create()  // factory method: thread safe
{
  std::lock_guard<std::mutex> guard(mut);  // lock the mutex
  return new XClass(); // now the constructor is thread-safe
} // unlock on dectructing guard

Even if static members are not phisically part of the objects, they can be declared public, private or protected. An example for public static member functon is a factory function, which role is to create objects of the class based on the _factory or abstract factory patterns. As static functions are members, they can access private members of their class.

Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government.