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:
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:
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.
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:
“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.
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:
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:
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:
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.