Classes are containers for variables and the operations (methods) that will affect the variables. As we discussed earlier, as ADTs implement encapsulation techniques for grouping all similar data and functions into a group, the classes can also be applied to group them. A class has three access control sections for wrapping the data and methods, and they are:
- Public: Data and methods can be accessed by any user of the class
- Protected: Data and methods can only be accessed by class methods, derived classes, and friends
- Private: Data and methods can only be accessed by class methods and friends
Let's go back to the definition of abstraction and information hiding in the previous section. We can implement abstraction by using protected or private keywords to hide the methods from outside the class and implement the information hiding by using a protected or private keyword to hide the data from outside the class.
Now, let's build a simple class named Animal, as shown in the following code snippet:
class Animal
{
private:
string m_name;
public:
void GiveName(string name)
{
m_name = name;
}
string GetName()
{
return m_name;
}
};
As we can see in the preceding code snippet, we cannot access the m_name variable directly since we assigned it as private. However, we have two public methods to access the variable from the inside class. The GiveName() methods will modify the m_name value, and the GetName() methods will return the m_name value. The following is the code to consume the Animal class:
// Simple_Class.cbp
#include <iostream>
using namespace std;
class Animal
{
private:
string m_name;
public:
void GiveName(string name)
{
m_name = name;
}
string GetName()
{
return m_name;
}
};
int main()
{
Animal dog = Animal();
dog.GiveName("dog");
cout << "Hi, I'm a " << dog.GetName() << endl;
return 0;
}
In the preceding code, we created a variable named dog of the type Animal. Since then, the dog has the ability that Animal has, such as invoking the GiveName() and GetName() methods. The following is the window we should see if we build and run the code:
Now, we can say that Animal ADT has two functions, and they are GiveName(string name) and GetName().
After discussing simple class, you might see that there's a similarity between structs and classes. They both actually have similar behaviors. The differences are, however, that structs have the default public members, while classes have the default private members. I personally recommend using structs as data structures only (they don't have any methods in them) and using classes to build the ADTs.
As we can see in the preceding code, we assign the variable to the instance of the Animal class by using its constructor, which is shown as follows:
Animal dog = Animal();
However, we can initialize a class data member by using a class constructor. The constructor name is the same as the class name. Let's refactor our preceding Animal class so it has a constructor. The refactored code should be as follows:
// Constructor.cbp
#include <iostream>
using namespace std;
class Animal
{
private:
string m_name;
public:
Animal(string name) : m_name(name)
{
}
string GetName()
{
return m_name;
}
};
int main()
{
Animal dog = Animal("dog");
cout << "Hi, I'm a " << dog.GetName() << endl;
return 0;
}
As we can see in the preceding code, when we define the dog variable, we also initialize the m_name private variable of the class. We don't need the GiveName() method anymore to assign the private variable. Indeed, we will get the same output again if we build and run the preceding code.
In the preceding code, we assign dog as the Animal data type. However, we can also derive a new class based on the base class. By deriving from the base class, the derived class will also have the behavior that the base class has. Let's refactor the Animal class again. We will add a virtual method named MakeSound(). The virtual method is a method that has no implementation yet, and only has the definition (also known as the interface). The derived class has to add the implementation to the virtual method using the override keyword, or else the compiler will complain. After we have a new Animal class, we will make a class named Dog that derives from the Animal class. The code should be as follows:
// Derived_Class.cbp
#include <iostream>
using namespace std;
class Animal
{
private:
string m_name;
public:
Animal(string name) : m_name(name)
{
}
// The interface that has to be implemented
// in derived class
virtual string MakeSound() = 0;
string GetName()
{
return m_name;
}
};
class Dog : public Animal
{
public:
// Forward the constructor arguments
Dog(string name) : Animal(name) {}
// here we implement the interface
string MakeSound() override
{
return "woof-woof!";
}
};
int main()
{
Dog dog = Dog("Bulldog");
cout << dog.GetName() << " is barking: ";
cout << dog.MakeSound() << endl;
return 0;
}
Now, we have two classes, the Animal class (as the base class) and the Dog class (as the derived class). As shown in the preceding code, the Dog class has to implement the MakeSound() method since it has been defined as a virtual method in the Animal class. The instance of the Dog class can also invoke the GetName() method, even though it's not implemented inside the Dog class since the derived class derives all base class behavior. If we run the preceding code, we will see the following console window:
Again, we can say that the Dog ADT has two functions, and they are the GetName() and MakeSound() functions.
Another necessary requirement of ADT is that it has to be able to control all copy operations to avoid dynamic memory aliasing problems caused by shallow copying (some members of the copy may reference the same objects as the original). For this purpose, we can use assignment operator overloading. As the sample, we will refactor the Dog class so it now has the copy assignment operator. The code should be as follows:
// Assignment_Operator_Overload.cbp
#include <iostream>
using namespace std;
class Animal
{
protected:
string m_name;
public:
Animal(string name) : m_name(name)
{
}
// The interface that has to be implemented
// in derived class
virtual string MakeSound() = 0;
string GetName()
{
return m_name;
}
};
class Dog : public Animal
{
public:
// Forward the constructor arguments
Dog(string name) : Animal(name) {}
// Copy assignment operator overloading
void operator = (const Dog &D)
{
m_name = D.m_name;
}
// here we implement the interface
string MakeSound() override
{
return "woof-woof!";
}
};
int main()
{
Dog dog = Dog("Bulldog");
cout << dog.GetName() << " is barking: ";
cout << dog.MakeSound() << endl;
Dog dog2 = dog;
cout << dog2.GetName() << " is barking: ";
cout << dog2.MakeSound() << endl;
return 0;
}
We have added a copy assignment operator which is overloading in the Dog class. However, since we tried to access the m_name variable in the base class from the derived class, we need to make m_name protected instead of private. In the main() method, when we copy dog to dog2 in Dog dog2 = dog;, we can ensure that it's not a shallow copy.