Carlos Enrique Hernández Ibarra bio photo

Carlos Enrique Hernández Ibarra

Passionated Software Engineer with experience in the Automotive Industry. Interested in the state of the art of design, development, and test of technology based on C/C++, Autosar, and Matlab/Simulink. Nevertheless, always with an open mind to new technologies, industries and solutions.

Email LinkedIn Github

The cornerstone of Object-Oriented Programming (OOP) in C++ is the class. This article offers a concise introduction to classes and their key features, including access specifiers, encapsulation, constructors, destructors, copy constructors, and copy assignment operators. Additionally, we explore related concepts such as cascading member functions and the explicit keyword.

Definition

In C++, classes and structs are defined as structured chunks of memory capable of storing a diverse set of data. They share principal features such as data members, member functions, and access specifiers.

The sole distinction between a class and a struct lies in the default member access level: class members are private by default, whereas struct members are public. To illustrate this, consider the following code snippet:

01

We have defined a Fraction class and a Byte struct for demonstration purposes, where you can observe the use of the terms “public” and “private.” These terms are known as access specifiers, which determine the level of visibility and availability of class members. There are three access specifiers:

  • Public: Public members can be accessed anywhere, including within the class definition file. If an object class instance is in scope, public static members in the class definition do not require an instance.
  • Protected: Protected members can be accessed within the class definition or inside its derived class definitions.
  • Private: Private members are only available to the class member functions and class friends within the class definition.

Class Declaration and Definition

The class declaration serves as the blueprint for the class, encompassing variable declarations and function prototypes. Typically, class declarations are placed in a header file (.h), as demonstrated in the following snippet:

02

The class definition, also known as implementation, is where the functionality of the class is developed. It defines “what the class does” based on the functions and variables declared in the class declaration. Any definition outside the class necessitates the use of a scope resolution operator (ClassName::) before its name. To illustrate this, the following code displays the implementation of the Fraction class, incorporating the scope resolution operator:

double Fraction::toDouble(){
 
  return 1.0*(m_nNumerator/m_nDenominator);
}

//Don't you remember the const reference used as function parameter?
//Check past articles
//Two fraction addition
Fraction& Fraction::add(const Fraction & other){

  Fraction tempFraction;
  if(m_nDenominator==other.m_nDenominator){
 
    tempFraction.m_nNumerator=m_nNumerator+other.m_nNumerator;
    tempFraction.m_nDenominator=m_nDenominator;
  }
 
  else{  
    tempFraction.m_nNumerator=(m_nNumerator*other.m_nDenominator)+(m_nDenominator*other.m_nNumerator);
    tempFraction.m_nDenominator=m_nDenominator*other.m_nDenominator;
  }
 
  return simplify(tempFraction);
}

//We wish to change f inside the function-so isn't a const reference
//in constrast than the last function
//This function is awesome!!

Fraction& Fraction::simplify(Fraction& f){

//They may be static member also... but we're using them only here. 
  const int sizeArray=46;
  const int aPrimes[]={ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,
                        43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
                        101, 103, 107, 109, 113, 127, 131, 137, 139, 149,
                        151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199 };
 
 
  for(int i=0; i< sizeArray;){
    //If we found a number which is the divisible for numerator and denominator
    if((f.numerator()%aPrimes[i]==0)&&(f.denominator()%aPrimes[i]==0))
      f.set(f.numerator()/aPrimes[i],f.denominator()/aPrimes[i]);
    
    else
      ++i;
  }
  return f;
}

//End

Cascading Member Function

Have you ever encountered a programming language expression like this:_ a = b = c_?

If you’re curious about how this is possible, it’s known as method cascading. This involves returning a reference of the pointer “this” (a pointer pointing to the data of its own instance). Consequently, method cascading enables multiple functions to be called in the same expression, typically by overloading operator functions, returning the same types that serve as parameters for the next function. The evaluation occurs from right to left in the expression. In the following code, the operator += begins with the instance f4, where “frac” is f3. We create a temporary variable to store the addition of f4 and f3, subsequently returning “temp” as a parameter for the next operation with f5.

03

Encapsulation

Encapsulation stands as a fundamental pillar of C++ Object-Oriented Programming (OOP). It reflects a best practice for developers to conceal data from users or other instances. In essence, encapsulation restricts direct access through the implementation of crucial components, simultaneously offering a well-defined public interface.

Encapsulation provides clients with uniform and clean access to data, offering flexibility to modify implementation decisions later. For instance, the Fraction class encapsulates the members m_numerator and m_denominator, preventing external functions from freely corrupting these variables. As an illustration:

04

“private” is the only member access specifier truly capable of encapsulation. While some developers may believe that “protected” serves a similar purpose, consider the scenario where you delete some protected data from the base class. This could potentially break a significant amount of code in the derived class. On the other hand, if private data members are deleted, you can be confident that it only affects a single class, ensuring real encapsulation.

Friend of a class

Friends in C++ are a powerful feature that allows specific classes or non-member functions to access non-public members of a class directly. While this capability can be useful, it should be employed judiciously as it breaks the principle of encapsulation. Here’s a more refined explanation:

class Window
{
private:
    QApplication app();
};

class Square
{
    friend class Window;
    friend void appInstanceCreation(Window w, Square sq);
private:
    unsigned int m_CoordX;
    unsigned int m_CoordY;
};

void appInstanceCreation(Window w, Square sq)
{
    /**/
}

Static keyword.

In C++ there are several ways to use the static keyword based on the use case:

  1. Static in member function of a class: In this case, the member function defined as static is only one for all the instances of the object from that class.
  2. Static inside a function or namespace: This static variable is alive from the very beginning of the execution of the program, and it is only accessible in the namespace or function where it is declared. The value is changed only in this namespace and it keeps that behavior until the end of execution of the program. Static variables keep their scope in the file they are declared.

Const member function.

When a member function has a const qualifier after its parentheses in the signature, it indicates that the function will not modify any non-mutable members of the host object. This is because the implicit this pointer is treated as a pointer to a const object within the function. This distinction is important when working with const objects. When you declare an object as const, you can only call member functions that are also declared as const. Alternatively, you can define specific members of the class as mutable, allowing them to be modified even when the object is declared as const.

Constructor

When the compiler encounters a class definition, it always looks for a constructor, destructor, copy constructor, and copy assignment operator. If these functions are not explicitly declared, the compiler generates default ones. Therefore, it is crucial to pay special attention to these functions in our class implementation, as they play a pivotal role in establishing practices that contribute to the creation of sustainable software.

A constructor is a special member function that manages the process of object initialization. It must share the same name as the class, have no return type, and can include a member initialization list as part of its implementation.

The primary advantage of using initialization lists is that they often lead to better performance. For example, consider the following code:

05

The initial constructor implementation involves four function calls: two default constructors and two copy assignments. While this might not be a concern for smaller objects, it could become significantly expensive for larger ones. In contrast, the second constructor implementation utilizes only a copy constructor for each declared member variable, resulting in a total of two operations. This highlights that by using an initialization list in the constructor, we can achieve a 200% performance improvement.

Copy constructor & Copy Assignment operator

While constructors and destructors mark the birth and death of objects, the replication of objects is solely handled by two operations: the copy constructor and the copy assignment operator. The copy constructor generates an identical copy of an existing object using the same class definition. Similarly, the copy assignment operator, overloaded with the symbol “=”, creates an identical object from another one by copying every non-static member variable. The following code illustrates the syntax for both:

55

Keyword explicit

The keyword “explicit” should be used before constructors with a single parameter (also known as conversion constructors) to prevent automatic conversions by the compiler. This is beneficial when we want to avoid implicit conversions that might lead to expensive operations.

For example, the following code prevents implicit creation of a Fraction. We define two constructors: one with the “explicit” keyword and another without it. In this case, f1 calls the normal constructor, posing the risk that the compiler might get confused and call a copy constructor, resulting in expensive operations and potential runtime errors. On the other hand, f2 calls the explicit constructor with a straightforward double variable as a parameter, avoiding unnecessary conversions:

06

Another alternative to address this issue is to take advantage of the fact that the compiler automatically generates a copy constructor if one is not defined. To prevent the implicit conversion discussed earlier, you can define a copy constructor with type checking. This approach allows you to control and explicitly handle the conversion, avoiding unintended implicit conversions that may lead to unexpected behavior.

Destructor

The destructor is another special member function tasked with cleaning up a specific object. Its name is identical to the class name but is preceded by a tilde (~). The destructor is invoked when a local object goes out of scope, a dynamically allocated memory object is explicitly destroyed by the operator delete, or just before the program terminates. A default compiler-generated destructor calls the destructor of every member variable before destroying the main object.

Inheritance

Inheritance in object-oriented programming (OOP) is based on a base class, where its members and functions marked as protected (denoted with a # in UML class diagrams) can be accessed by derived classes. Derived classes always have access to the protected and public members of the base class but cannot access its private members. A useful way to visualize this is to think of derived classes as having a consecutive memory structure: they first include the memory of the base class, followed by their own specific memory. In the case of inheritance, if a derived class overrides a non-private function from the base class, an object of the derived class will call the overridden function. To explicitly call the original function from the base class, you need to use scope resolution, like Base::function(). Regarding construction and destruction, derived classes must handle the base class’s part in a specific order:

  1. During construction, the base class’s part is initialized first, followed by the construction of the derived class’s specific part.
  2. During destruction, this process is reversed: the derived class’s part is destroyed first, and then the base class’s part is cleaned up.

In inheritance, there are four types of functions that are not automatically inherited and require specific implementation considerations:

  1. Constructors: Constructors must first initialize the base class and then proceed down through the levels of derived classes until reaching the final class being constructed.
  2. Assignment Operators: The assignment operator must explicitly handle the assignment of base class members first, as the compiler does not automatically perform this step.
  3. Copy Constructors: The copy constructor must ensure that the base class’s portion is properly copied along with the derived class’s members.
  4. Destructors: While the compiler can generate a destructor, destruction must occur in reverse order of the initialization performed during object construction, starting with the derived class and ending with the base class.

Polymorphism

Polymorphism is based on dynamic binding, which differs from static binding.

  1. **Static binding **occurs at compile time. This happens when an derived object is handled directly (not through a pointer or reference), meaning its type is fully known during compilation. The compiler determines the function to call based on the object’s type.
  2. Dynamic binding, on the other hand, occurs at runtime. This happens when a base class pointer is used to handle an object. Since the pointer only “sees” the initial memory layout of the base class, it can point to any derived class that extends the base class. Dynamic binding is enabled in C++ by using the virtual keyword for functions, indicating that they can be resolved at runtime rather than at compile time.

It is important to note that when a virtual function enables dynamic binding, the base class must also have a virtual destructor. This ensures that when a base class pointer points to a derived class object, the correct destructor for the derived class is called during destruction. Without a virtual destructor, only the base class destructor would execute, potentially leading to resource leaks or incomplete cleanup.

During the construction or destruction of objects in polymorphic hierarchies, virtual methods should not be called. At these stages, the type of the object is not fully determined because the derived class’s part has not yet been constructed or has already been destroyed. Calling virtual methods in these scenarios can lead to undefined behavior.

Abstraction

An abstract class allows the definition of common characteristics for a group of derived classes without requiring the ability to instantiate it. In other words, it establishes a main framework where every derived class is obligated to override certain methods or properties, enforcing the implementation of shared behaviors. Without abstraction, there would be no way to enforce or utilize general properties across a group of derived classes, and each one could define its own set of properties, leading to potential development and maintenance issues.

In C++, class abstraction is achieved by declaring one or more functions as pure virtual using the syntax virtual void func() = 0;. All derived classes, including those inheriting indirectly from the abstract class, are required to define and implement the methods specified by the abstract class.

Reference

[1] Ezust, A., & Ezust, P. (2006). An Introduction to Design Patterns in C++ with Qt 4. Prentice Hall. ISBN-10: 0131879057, ISBN-13: 978-0131879058.