Chapter 04 - Basics of 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;
  • and what operations can be performed on objects of the class.

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

class <name_of_class> {
};

For example:

class Apple {
};

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;
}

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".

WARNING - 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.

Take for example a class called RgbLed:

class RgbLed {
};

This would be placed inside a file called rgb_led.h.

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 "rgb_led.h"    // Include project header files

using namespace std;

int main(void) {
    //...
}

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.

INFO - Intermediate files

You can actually test this by telling the compiler to save the intermediate files that are generated by the different compiler tools. For example the .ii files are the result files of the preprocessor. Pass the argument -save-temps to the g++ compiler and you will be able to access the intermediate files. 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. 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:

#ifndef _HEADER_RGB_LED_
#define _HEADER_RGB_LED_

class RgbLed {

};

#endif

Basically it does as the directives state. If the label _HEADER_RGB_LED_ 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). For 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:

#pragma once

class RgbLed {

};

It also makes the code shorter and more clear.

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 red, green and blue components to the RgbLed class.

#pragma once

class RgbLed {
  // Attributes
  private:
    int red;
    int green;
    int blue;
};

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 the available those that are needed by the user of the class. This is facilitated by the access modifiers.

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
};

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 declaration of method, also known as the method prototype.

The prototype of a method takes on the following form:

<return_type> <name>(<comma_separated_argument_list);

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 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. Especially to a user of your class.

Let's for example declare a method called set_color that does not return anything (the return type is therefore void). It does however take 3 arguments, more specifically the red, green and blue colors. Again as with the attribute, we can specify the access modifier for all method declarations following.

#pragma once

class RgbLed {

    // Methods
  public:
    void set_color(int red, int green, int blue);

    // Attributes
  private:
    int red;
    int green;
    int blue;
};

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.

WARNING - 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. Methods can be instructed to compiler to make them inline so that compiler can replace those method definition wherever they are being called. 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 color of an RgbLed 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 set_color method, we will need to create a file with the same name as the header file, excluding the extension, which should now be .cpp.

// rgb_led.cpp
#include "rgb_led.h"

void RgbLed::set_color(int red, int green, int blue) {
  this->red = red;
  this->green = green;
  this->blue = blue;
}

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 "rgb_led.h", an include directive. It is necessary to include the class definition when defining the methods of that class.

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 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 red = red, 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.

Let us expand this example with methods that return the values of the different colors. 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. Note how we named the methods with a prefix of 'get_' to indicate that they are getters.

// rgb_led.h
#pragma once

class RgbLed {
    // Attributes
  private:
    int red;
    int green;
    int blue;

    // Methods
  public:
    void set_color(int red, int green, int blue);
    int get_red(void);
    int get_green(void);
    int get_blue(void);
};

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

// rgb_led.cpp
#include "rgb_led.h"

void RgbLed::set_color(int red, int green, int blue) {
  this->red = red;
  this->green = green;
  this->blue = blue;
}

int RgbLed::get_red(void) {
  return red;
}

int RgbLed::get_green(void) {
  return green;
}

int RgbLed::get_blue(void) {
  return blue;
}

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. When doing this, we will need to save that value somewhere or immediately use it.

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 two RgbLed class instances (aka objects), the following code can be used.

RgbLed aliveLed;
RbgLed busyLed;

Important to note is that you will need to include the header file that contains the class definition before this will work.

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

using namespace std;

int main()
{
    cout << "Creating LEDs" << endl;

    RgbLed aliveLed;
    RbgLed busyLed;

    return 0;
}

While this is pretty awesome, the code above doesn't do a lot. As stated before, an OOP program is 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 set the color of the alive led using the setter method and retrieve it back using the getter methods.

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

using namespace std;

int main()
{
    RgbLed aliveLed;
    RgbLed busyLed;

    aliveLed.set_color(125, 88, 33);

    cout << "R: " << aliveLed.get_red() << endl;
    cout << "G: " << aliveLed.get_green() << endl;
    cout << "B: " << aliveLed.get_blue() << endl;

    return 0;
}

Methods of objects can be called by using the member operator .. The color of the led can be set by specifying literal values as arguments or by passing the names of variables that hold an integer value.

INFO - 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.

The code below prints out the values of the colors by retrieving them via the getter methods. They could also have been saved in temporary variables first and then used for displaying.

cout << "R: " << aliveLed.get_red() << endl;
cout << "G: " << aliveLed.get_green() << endl;
cout << "B: " << aliveLed.get_blue() << endl;

Constructors

The attributes in the RgbLed example class have not been initialized. This means that the actual values will be undefined and more particular just junk. This is why it is so extremely important to initialize everything in C++ before actually using it.

This is where constructors come into play. Constructors are

  • methods that carry the same name as the class
  • have no return type

The main purpose of a constructor is to initialize new objects and put them in a valid state, ready for first use.

Let's create a basic constructor for the RgbLed class that initializes the color components to zero. First a prototype needs to be added to the class definition.

// rgb_led.h
#pragma once

class RgbLed {

    // Constructors
  public:
    RgbLed(void);

    // Methods
  public:
    void set_color(int red, int green, int blue);
    int get_red(void);
    int get_green(void);
    int get_blue(void);

    // Attributes
  private:
    int red;
    int green;
    int blue;
};

The constructor here does not take any arguments; that is also called a default constructor. While it is not mandatory to split up the constructors and methods, it often creates a clearer image for the user of the class.

INFO - Default contructor

Just as in Java, C++ generates a default constructor (one without arguments) for you if you do not define any constructor yourself. At the moment you define a constructor inside the class, even if not a default constructor, you lose this functionality from the compiler.

The implementation of the constructor could look like this.

// rgb_led.cpp
#include "rgb_led.h"

RgbLed::RgbLed(void) {
  this->red = 0;
  this->green = 0;
  this->blue = 0;
}

void RgbLed::set_color(int red, int green, int blue) {
  this->red = red;
  this->green = green;
  this->blue = blue;
}

//...

However if you take a good look at this code, you may notice that we could have create a shorter implementation for the constructor by re-using the set_color method in stead of duplicating it's implementation. So let's refactor this.

// rgb_led.cpp
#include "rgb_led.h"

RgbLed::RgbLed(void) {
  set_color(0, 0, 0);
}

void RgbLed::set_color(int red, int green, int blue) {
  this->red = red;
  this->green = green;
  this->blue = blue;
}

//...

results matching ""

    No results matching ""