# Creating Classes

Classes are the central feature of C++ that support the Object Oriented Programming paradigm. They are often called user-defined types. Classes are the representation of elements from the problem space inside the solution space. They combine both data (attributes) and behavior (methods) into one neat little package (also known as encapsulation).

In C++ the attributes and methods within a class are called the members of that class.

# Class Definition

To define a class is to create a sort of blueprint for objects of that class. A class definition does not actually reserve any memory. It does however inform the compiler:

  • what the name of the class is;
  • what data an object of the class will hold - called the attributes;
  • and what operations can be performed on objects of the class - called the methods.

A class definition in C++ is constructed using the format shown below:

class <name_of_class> {
};
1
2

For example:

class Apple {
};
1
2

A class definition consists of the following important parts:

  • the class keyword
  • the name of the class
    • PascalCased starting with capital letter
    • Make sure to use sane and clear names for your classes. This is considered a good programming skill
    • Most often a Noun (Car, Point, Student, Record, Cat, Animal, ...)
  • Two curly brace {}
    • Data and behavior will be defined between these curly braces.
  • A termination semicolon ;

# Header files

The class definition can be placed before your main function in C++ as shown below.

#include <iostream>

class Apple {
};

int main(void) {
  return 0;
}
1
2
3
4
5
6
7
8

This will however make for a terrible mess once your program starts to become bigger. Secondly, it is not possible to share classes like this between different programs without copy pasting the code from one application to another. And that's a big no-no.

For these reasons, class definitions will always be placed in separate files called header files. These files carry the same name as the class, though mostly in lower case letters (preferred snake_case, although not mandatory), and have the extension ".h". Take for example a class called RgbLed, the header file would be called rgb_led.h.

Case-sensitivity

Do take note that where Windows is not case-sensitive, other operating systems might be. Take for example Linux. This means that if you are not careful when naming and including your files, you might have a working application on Windows but not on Linux.

Let's continue with a class Point (representing a point in 2D space), where the class definition is placed inside a file called point.h.

// point.h
class Point {
};
1
2
3

However if you want to create objects of your class somewhere else you will need to tell the compiler where to find the class definition. This can be achieved by including the header file using a preprocessor #include directive.

#include <iostream>   // Include standard/external libraries
#include "point.h"    // Include project header files

using namespace std;

int main(void) {
  //...
}
1
2
3
4
5
6
7
8

As can be seen from the previous example code, there are different ways to use an #include directive.

  • When using <....> with an #include directive you are telling the compiler to search for the header file within the standard libraries of C++ and within the include paths made available to the compiler (for example externally installed libraries).
  • When using "...." with an #include directive you are telling the compiler to search for the header file within the current project directory.

# Include Guards

When including files, the preprocessor will actually take the content from the header-file and replace the #include directive with the content.

Intermediate files

You can actually test this by telling the compiler to save the intermediate files that are generated by the different compiler tools. Pass the argument -save-temps to the g++ compiler and you will be able to access the intermediate files. For example the .ii files are the result files of the preprocessor. Open the .ii file and take a look how big it has gotten.

Now what happens if you include the same header file multiple times? Actually C++ states that a variable, a function, a class, ... can only be defined once in a single application, of course taking scope into consideration. This is also known as the One-Definition Rule (ODR). When the linker is uniting all the object modules, it will complain if it finds more than one definition for the same variable/function/class/...

This is where include guards come into play. These are a safety mechanism that will make the preprocessor only include header files that have not been included yet. A typical include guard looks something like this:

// point.h
#ifndef _HEADER_POINT_
#define _HEADER_POINT_

class Point {

};

#endif
1
2
3
4
5
6
7
8
9

Basically it does as the directives state. If the label _HEADER_POINT_ has not yet been defined, then define it, include the class definition and end it. If the label has already been defined than the class definition is not included anymore. The label can be chosen freely by the developer.

These include guards are only of importance for the preprocessor, therefore they are prefixed with a hashtag #.

One disadvantage of the include guards as shown above it that you need to declare a label which must be unique throughout your whole program (including all libraries you include). Because of this, a new system was introduced using the #pragma once directive. This directive tells the preprocessor to only include the file once.

Rewriting the previous example using the #pragma once directive looks like this:

// point.h
#pragma once

class Point {

};
1
2
3
4
5
6

It also makes the code shorter and more clear.

# Always use Namespaces

When defining classes one should always place them inside a namespace. This can be achieved by surrounding the class definition with a namespace definition as shown in the next code snippet.

// point.h
#pragma once

namespace Geometry {

  class Point {

  };

};
1
2
3
4
5
6
7
8
9
10

A namespace is a declarative region that provides a scope to the identifiers (the names of types, functions, variables, etc) inside it. Namespaces are used to organize code into logical groups and to prevent name collisions that can occur especially when your code base includes multiple libraries. All identifiers at namespace scope are visible to one another without qualification.

Identifiers outside the namespace can access the members by:

  • using the fully qualified name for each identifier, for example std::string;

    #include <iostream>
    #include <string>
    
    int main() {
    
      std::string hello = "Hello World";
      std::cout << hello << std::endl;
    
      return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • by a using declaration for a single identifier, for example using std::string;;

    #include <iostream>
    #include <string>
    
    using std::string;
    using std::cout;
    using std::endl;
    
    int main() {
    
      string hello = "Hello World";
      cout << hello << endl;
    
      return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • or by a using directive for all the identifiers in the namespace, for example using namespace std;.

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
    
      string hello = "Hello World";
      cout << hello << endl;
    
      return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

Header files should use fully qualified names

Code in header files should always use the fully qualified namespace name and never include a namespace or use a using declaration as including the header would automatically include these as well.

Note that one doesn't have to invent a namespace name for every class. A namespace can hold an unlimited number of classes/functions/structs/... Namespaces can even include other namespace, allowing for a hierarchical namespace structure.

Unique Namespace

Make sure that the name of your namespace is unique if possible. The chances of including a library that has the same namespace name is low but not uncommon. Just make sure to name namespaces differently than your classes, otherwise their might be ambiguity between the types.

# Adding Data - Attributes

Attributes or class instance variables are the way to store data inside an object of that particular class.

Defining attributes is very similar to creating a variable inside a method or function. Let's add an x and y value to the Point class:

// point.h
#pragma once

namespace Geometry {

  class Point {

    // Attributes (instance variables)
    private:
      double x = 0;
      double y = 0;

  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Initialize Attributes

Make sure to always initialize your attributes as shown in the previous example using direct initialization or use a constructor. Never leave attributes uninitialized as they will contain undefined values.

Notice that an access modifier (such as private, public and protected), followed by a colon :, can be specified for all the attributes/methods that follow it. Default access in C++ classes is private. This means that not specifying anything would have had the same effect in the previous example. However it is always safer to explicitly declare the attributes as private as it may lead to leaks when adding public things before the attributes.

Data hiding is an important feature of OOP. It allows developers to prevent direct access to internal representation of a class. In other words access to class members can be restricted making only those things available that are needed by the user of the class. This is facilitated by the access modifiers.

Another important concept is that other classes should not depend on data but rather on behavior. So its better to make data private and the required methods public.

C++ has the following definitions for the access modifiers inside a class definition:

  • public: A public member is accessible from anywhere outside the class. Attributes should almost never be made public, to prevent direct access from outside the class. Methods such as getters, setters, constructors and such are often made public as they need to be accessible.
  • private: A private member cannot be accessed from outside the class (not even read - in case of an attribute). Only the class and friend functions/methods can access private members. Not even an inherited class can access it's parent private attribute/methods.
  • protected: A protected attribute or method is very similar to a private member but it provided one additional benefit - that is that they can be accessed in child classes (aka derived classes).

A class can have multiple public, protected, or private access modifiers. Each modifier remains in effect until either another modifier or the closing brace of the class body is seen. The default access for members and classes is private.

Analyze the example below. The comments show what access is granted to each section.

class Foo {
  //... default is private here

  protected:
    //... all protected here

  private:
    //... all private here

  public:
    //... all public here

  protected:
    //... all protected here

  public:
    //... again public
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Adding Behavior - Methods

While attributes store the data of our objects, methods allow objects to have behavior. In C++ the declaration and actual definition of methods are separated. Inside the class definition we put the declarations of the methods, also known as the method prototypes.

The prototype of a method takes on the following form:

<return_type> <name>(<comma_separated_argument_list);
1

This consists of:

  • The return type of a method can be void (nothing to return), a primitive type such as int, char, double, ..., a pointer, a custom type such as a class, an enum, ...
  • The name of a method should be a very clear and indicating name of what the actual method does. There are no real naming conventions in C++ as there are in C# or Java. The most important rule is to stay consistent.
  • A comma separated list of the arguments, and for the prototype the only thing that is mandatory are the types. This means that you actually don't have to state the names. However most of the time is good practice to do it anyway.

Let's for example declare a method called move_to that does not return anything (the return type is therefore void). It does however take 2 arguments, more specifically the new x and y coordinates. Again as with the attributes, we can specify the access modifier for all method declarations following.

// point.h
#pragma once

namespace Geometry {

  class Point {

    // Methods
    public:
      void move_to(double x, double y);

    // Attributes (instance variables)
    private:
      double x = 0;
      double y = 0;

  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Notice how the private attributes are placed at the bottom of the class definition. This is not a requirement. It is however often done by developers as the public members are more important for a user of the class, so they should be encountered first.

# Separating method declaration from definition

The header file contains the class definition and the class contains both the attribute definitions and method declarations. This makes for a clear separation between method declaration and actual implementation (definition).

Why? Because when we supply a class to a user of that class we may not want to provide the actual implementation as readable code. We may just wish to provide the compiled object code. However for the compiler and linker of the user to be able to use our class it will actually need to know what the class looks like - it needs to know the interface of that class, because the compiler performs all sort of checks to make sure you use the class as intended. This is where the header file comes into play. The user of the class will provide both the header file and the object file to the compiler and linker, allowing it to be integrated into his/her program.

Inline methods

Do note that it is also possible to place the definition of a method inside the header file. This is called inline methods. The inline methods are a C++ enhancement feature to increase the execution time of a program. When compiling the application, the compiler can be instructed to make methods inline, meaning that a method call in code is actually replaced with the code inside of that method. This does not mean that it is a good idea to make every method inline, as this will be a burden on the actual memory usage of your class. Actually, compilers these days are smart enough to decide for themselves if they should make methods inline or not.

Let us start by implementing a method that allows us to change the coordinates of a Point object. This is called a setter method because it does nothing else but set a part of the state of the object. To actually implement the move_to method, we will need to create a file with the same name as the header file, excluding the extension, which should now be .cpp.

// point.cpp
#include "point.h"

namespace Geometry {

  void Point::move_to(double x, double y) {
    this->x = x;
    this->y = y;
  }

};
1
2
3
4
5
6
7
8
9
10
11

A lot can be said about the code above, so let's start.

In the example above, the name of the file is specified in comments. While some programmers do this, mostly it is a bad idea. It is a comment that may become misleading and stand in the way of change (changing the name of file if ever needed). Every decent editor will display the name of the file you are working in. It is only done here to make it more clear to you as a reader of this course.

The first actual line of code is #include "point.h", an include directive. It is necessary to include the class definition when defining the methods of that class.

Next the namespace is stated again. This is required as the actual method definition must also be placed inside of the namespace. Note again that after the closing curly brace of the namespace {, a semicolon ; is required.

Next is the method definition. The signature is almost the same as the prototype except for the scope resolution operator :: prefixed with the name of the class. This tells the compiler that the method definition that follows belongs to the specified class. Here the names of the arguments are mandatory, contrary to the method prototype.

The last part of this definition is a C++ block indicated by the parentheses {}. Inside of these parentheses the implementation of the method is specified. Here we just save the values provided as arguments to the method, inside the attributes that we defined in our class definition. Because both the arguments and the attributes have the same names, we need to use this-> when we want to use the attributes.

If we had used x = x, it would have resulted in the arguments being assigned to themselves. This because of the fact that the scope of the arguments is closer by and would of overruled the attributes.

No access modifier needs to be specified here for the definition of the methods. This is only done inside the class definition.

# Adding Getters

Let us expand this example with methods that return the values of the coordinates. Since these methods do not change the state of the object and just return a part of it's state to the outside world, they are called getter methods.

// point.h
#pragma once

namespace Geometry {

  class Point {

    // Methods
    public:
      void move_to(double x, double y);

    public:
      double get_x(void) const;
      double get_y(void) const;

    // Attributes (instance variables)
    private:
      double x = 0;
      double y = 0;

  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Because these getter should not change the internal state of the object, it is a good idea to declare them as being const. A function or method becomes const when the const keyword is used in its declaration. The idea of const functions is not to allow them to modify the object on which they are called. It is recommended practice to make as many functions const as possible so that accidental changes to objects are avoided.

Getters

Often getter methods are named with a prefix get_. This is however not mandatory. C++ does not allow methods to have the same name as attributes, otherwise the methods could been called x and y which would be preferred. That is why some developers name attributes starting with an m from member or _ to differentiate between attributes and arguments or methods.

As a next step it is required to give the getter methods an implementation.

// point.cpp
#include "point.h"

namespace Geometry {

  void Point::move_to(double x, double y) {
    this->x = x;
    this->y = y;
  }

  double Point::get_x(void) const {
    return x;
  }

  double Point::get_y(void) const {
    return y;
  }

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Note that for the getters there is no ambiguity for the attributes. Therefore, we do not need to explicitly specify this->, however it is not wrong to do so. Just more typing work. Basically when a getter method of an object is called, a value is returned.

Note that also in the method definition the const keyword is repeated.

# Creating Objects

To create objects on the stack, the same syntax can be used as for creating variables of a primitive type.

So to create a Point class instance (aka object), the following code can be used.

Point center;
1

Important to note is that you will need to include the header file that contains the class definition before this will work. You will also need to add a using namespace Geometry or specify the namespace before the classname using the scope resolution operator, for example Geometry::Point center.

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

using namespace std;

int main() {
  cout << "Point demo" << endl;

  Geometry::Point point;

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

While this is pretty awesome, the code above doesn't do a lot. As stated before, an OOP application is a collection of objects that send messages to each other. These messages are actual requests made to the objects to show of their behavior. So in other words, we need to make method calls to the objects to make them perform actions.

Let us change the x and y coordinates of the center Point using the move_to method and retrieve them back using the getter methods.

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

using namespace std;

int main() {
  cout << "Point demo" << endl;

  Geometry::Point center;

  center.move_to(12.5, 23);

  cout << "The point is now at ["
    << center.get_x() << ", " << center.get_y()
    << "]" << endl;

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Output

Point demo
The point is now at [12.5, 23]

Methods of objects can be called by using the member operator .. The coordinates of the Point object can be set by specifying literal values as arguments or by passing the names of variables that hold a double value.

Literals

A literal is some data that's presented directly in the code, rather than indirectly through a variable or method call. Some examples are "Hello World", 15, 0x05, 'a', ... The data constituting a literal cannot be modified by a program, but it may be copied into a variable for further use.

To compile this application one will need to supply both implementation files to the compiler using the following command:

g++ main.cpp point.cpp -o points
./points
1
2

# Exercises

Try to solve the exercises yourself. Don't go copy pasting other people's solutions.

Mark the exercises using a ✅ once they are finished.

# ❌ Move by delta

Extend the Point class with a method move_by(double deltaX, double deltaY) that allows the user of a Point object to move the point using delta-coordinates.

// point.h
#pragma once

namespace Geometry {

  class Point {

    // Methods
    public:
      void move_to(double x, double y);

    // Attributes (instance variables)
    private:
      double x = 0;
      double y = 0;

  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// point.cpp
#include "point.h"

namespace Geometry {

  void Point::move_to(double x, double y) {
    this->x = x;
    this->y = y;
  }

};
1
2
3
4
5
6
7
8
9
10
11

Basically the delta is added to the current coordinates making it move by delta.

# ❌ Copying Objects

What would you expect to be the output of the following code? What happens when you create a second object and initialize it with the first?

// Compile using:
// g++ main.cpp point.cpp -o points

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

using namespace std;

int main() {
  Geometry::Point first;
  first.move_to(10, 20);
  cout << "First is at ["
    << first.get_x() << ", " << first.get_y() << "]" << endl;

  // Initialize second point with first
  Geometry::Point second = first;

  cout << "Second is at ["
    << second.get_x() << ", " << second.get_y() << "]" << endl;

  cout << "Moving first point to 103, 1234" << endl;
  first.move_to(103, 1234);

  cout << "First is at ["
    << first.get_x() << ", " << first.get_y() << "]" << endl;

  cout << "Second is at ["
    << second.get_x() << ", " << second.get_y() << "]" << endl;

  return 0;
}
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
Last Updated: 9/25/2021, 7:37:47 AM