Programming in C++
Lab 7 (extension lab): More on objects and OO
The purpose of this lab is to repeat some elements of object orientation and provide additional information and detail. So far, we have seen that objects can be used to:
- Combine data and related code into classes
- Objects are instances of classes and have a state, represented by attribute values
- Methods must called for an object and have acces to the attributes
- Encapsulation is an important principle in OO programming
Similar to functions, the definition of the interface of a class can be separated into a header file in C++. We have also seen that instances can be created on the heap or the stack. The new keyword is not necessary for instance creation, it indicates usage of the heap.
Inheritance
Classes can inherit from other classes (this is a common feature in OO languages). In C++, this is written as follows:
class Student : public Person { ... }
The consequence of this is that there is a new class Student which has the methods and attributes of Person plus any additional methods and attributes specified in Student. One could say that Student is a specialized Person (but notice "specialized" here means Student "has more" and "can do more"). There could be other classes inheriting from Person, say StaffMember. There can be several levels of inheritance, for example PartTimeStudent could inherit from Student. A PartTimeStudent would have properties of Person + Student + own properties. C++ allows multiple inheritance, where a class inherits from more than one class (say a Car inherits from Vehicle and Machine), see below for details. In C++, creating a class will call the default constructors of all base classes automatically.
An instance of a derived class is an instance of its base class as well, and can take its place. So we can do something like (notice no pointers involved):
class Base { private: int a; }; class Derived : public Base { private: int b; }; Base x; Derived y; x = y;
Here, y will be copied to x. During this process, only those parts of Derived which come from Base (i. e. a) will be copied. b will not be copied. This is relevant if it comes to arrays - if we have an array of type Base, we cannot put a full Derived object into it (why not?), even though a Derived "is a" Base as well. So a variable of type x is always an object of type x, and any method executed is from x (this will be relevant when we discuss overloading).
Inheritance can be used to restrict a type for templates. Instead of template <typename T>
write template <std::derived_from<MyClass> T>
(C++ 20, earlier versions require a different construct). Though this is not needed, we will get errors if a method used is not in the type used.
Inheritance and data accessibility
Similar to other OO languages, attributes and methods can have different access levels. In C++, there are (only) three of them:
- private: visible within the class
- protected: visible within the class and in derived classes
- public: visible within the class, in derived classes, and from the outside
In addition, the inheritance can be public, protected or private:
class Base { public: int x; protected: int y; private: int z; }; class PublicDerived: public Base { // x is public // y is protected // z is not accessible from PublicDerived }; class ProtectedDerived: protected Base { // x is protected // y is protected // z is not accessible from ProtectedDerived }; class PrivateDerived: private Base { // x is private // y is private // z is not accessible from PrivateDerived };
The inheritance modifier "downgrades" the inherited methods, e.g. if inheritance is protected, all public methods/attributes in the class become protected. Notice private members of the base class are not visible in any case.
A class can be declared a friend. This means it can access privte and protected members. This has to happen in the class whose methods are to be accesses: If A declares B a friend, B can access A, not the other way round. There can also be friend functions. Those functions can be injected from the outside:
#include <iostream> using namespace std; class Distance { private: int meter; // friend function friend int addFive(Distance); public: Distance() : meter(0) {} }; // friend function definition int addFive(Distance d) { //accessing private members from the friend function d.meter += 5; return d.meter; } int main() { Distance D; cout << "Distance: " << addFive(D); return 0; }
Overloading and overriding
We have seen that it is possible to have several methods with the same name, but different parameter lists. This is referred to as overloading. In C++, overloading does not work as a standard with inheritance:
#include <iostream> using namespace std; class Base { public: int f(int i) { cout << "f(int): "; return i+3; } }; class Derived : public Base { public: double f(double d) { cout << "f(double): "; return d+3.3; } }; int main() { Derived* dp = new Derived; cout << dp->f(3) << '\n'; cout << dp->f(3.3) << '\n'; delete dp; return 0; }
Here, both calls to f will call the function f(double), even though f(int) is public. If you want to have the base class method visible in Derived, you need:
class Derived : public Base { public: using Base::func; double f(double d) { cout << "f(double): "; return d+3.3; } };
There is also overriding of methods, which means that a class re-implements a method from the base class:
#include <iostream> using namespace std; class Base { public: void print() { cout << "Base Function" << endl; } }; class Derived : public Base { public: void print() { cout << "Derived Function" << endl; } }; int main() { Derived derived1; derived1.print(); Base base1; base1.print(); return 0; }
Notice the two print methods have the same name and parameters (none here), otherwise it is not overriding. Here, the first call to print will use the implementation from Derived, the second that from Base.
Static and dynamic binding
Overriding methods relates to the issue of static or dynamic binding. Static binding is the standard choice in C++, and it happens at compile time. If you do something lik this:
Base base1; base1.print();
with the base class defined as before, then the compiler can know which method is meant by print and include appropriate addresses in the code.
Things become more complicated in situations like this:
#include <iostream> using namespace std; class B { public: // Virtual function virtual void f() { cout << "The base class function is called.\n"; } }; class D: public B { public: void f() { cout << "The derived class function is called.\n"; } }; int main() { B base; D derived; B *basePtr = &base; basePtr->f(); basePtr = &derived; basePtr->f(); return 0; }
Here, we create an instance of B and one of D (both on the stack, btw). Then we declare a variable of type "pointer to B" and assign to a pointer to the instance of B created before. Calling the functio f with that pointer executes f in B (that seems easy). Now we assign a pointer to D to the same variable. When we call function f, then f from class D is executed. Note this is not obvious: The variable is of type pointer to B, but the code executed comes from D. So D is the "real" type here, and that determines the execution.
In order to be able to do this, we need the virtual keyword. This triggers dynamic binding. If we leave out virtual, the code compiles, but both calls to f will use B, since the type of the variable now decides. The compiler uses static binding and assigns at compile time the function to call. Of course things can get more complicated, you could for example decide at runtime which instance to allocate using and if, which might check e. g. user input.
What happens if we now add a function to D like this (B is unchanged) and try to call it?
class D: public B { public: void f() { cout << "The derived class function is called.\n"; } void g(){ cout << "The derived class function is called.\n"; } }; int main() { B base; D derived; B *basePtr = &base; basePtr->f(); basePtr->g(); basePtr = &derived; basePtr->f(); basePtr->g(); return 0; }
This cannot compile. The type of basePtr is B, so there is no method called g in it (of course we can use D.g()). On the other hand, we know that, at runtime, there is actually a D behind basePtr, even though it is not visible. So in way something is hidden which is there. An option to make the D visible is to use a case:
int main() { B base; D derived; B *basePtr = &base; basePtr->f(); basePtr = &derived; basePtr->f(); D *derivedPtr=dynamic_cast<D*>(basePtr); derivedPtr->g(); return 0; }
Notice the line D *derivedPtr=dynamic_cast<D*>(basePtr); - this assignes the instance behind basePtr to derivedPtr, which is of type D. Now we can use g() with it, since derivedPtr is a D. Notice though, that this is all dynamic and at runtime. If there is not instance of D behind basePtr, we get an error at runtime. The cast operator is often misunderstood as a type of conversion operation. I think this is not a good description, since it only makes something visible which is there anyway. For example, we could not cast base
to derived
(there is a logic behind it, because an object of type D might need additional data compared to an instance of B, and where is the cast supposed to take that information from?).
Here we have seen polymorphism: A variable can take members of different types, as long as they are derived from the type of the variable. So the variable can take multiple shapes (polymorph means something like "multi shaped"). A complete example is this:
#include <string> #include <iostream> class Person{ public: std::string name="N. N."; virtual std::string getName(){//A virtual function is needed to enable the cast and a virtual destructor would be good return name; } }; class Student : public Person { public: std::string course="Computer Science"; }; class Staffmember : public Person { public: std::string title="Professor"; }; int main (int argc, char* argv[]) { Person* department[3];//this is an array of pointers, won't work with array of Persons department[0]=new Student(); department[1]=new Staffmember(); department[2]=new Person();//You could argue that persons are either staff or student, but for demonstration, we allow this for(int i=0;i<3;i++){ std::cout<<i<<" is "<< department[i]->getName()<< std::endl;//that works for all, we ignore that some in here may be students or staff if(i==0) std::cout<<i<<" is a student of "<< dynamic_cast<Student*>(department[i])->course<< std::endl;//only students can be cast to students if(i==1) std::cout<<i<<" is a staff member and ranks as a "<< dynamic_cast<Staffmember*>(department[i])->title<< std::endl;//only for staff members } return 0; }
Multiple inheritance
C++ allows multiple inheritance. So a class can be declared as:
class C: public B, public A
Note that many programming languages don't allow that, due to a number of problems which can arise (Java works around this by allowing multiple interfaces, but not inheritance). One problem is the diamond problem: A class inherits from two classes, which both inherit from the same class:
#include<iostream> using namespace std; class Person { // Data members of person public: Person(int x) { cout << "Person::Person(int ) called" << endl; } }; class Faculty : public Person { // data members of Faculty public: Faculty(int x):Person(x) { cout<<"Faculty::Faculty(int ) called"<< endl; } }; class Student : public Person { // data members of Student public: Student(int x):Person(x) { cout<<"Student::Student(int ) called"<< endl; } }; class TA : public Faculty, public Student { public: TA(int x):Student(x), Faculty(x) { cout<<"TA::TA(int ) called"<< endl; } }; int main() { TA ta1(30); }
This will lead to the constructor of Person being called twice. A virtual keyword avoids this:
class Faculty : virtual public Person { }; class Student : virtual public Person { };
Another issue is what happens if both parents have an identical method (with potentially different implementations)?
struct A { int idA; void setId(int i) { idA = i;} int getId() { return idA;} virtual void foo() = 0; }; struct B { int idB; void setId(int i) { idB = i;} int getId() { return idB;} virtual void foo2() = 0; }; struct AB : public A, public B { void foo() override {} void foo2() override {} };
If we want to use setId, we have to say which one we want
AB * ab = new AB(); ab->B::setId(10); ab->A::setId(10);
It could be argued that this is not really in line with the idea of object orientation.
Namespaces
Namespaces are an option in C++ to structure code. Identifiers can be used in different namespaces and can still be used:
#include <iostream> using namespace std; // first name space namespace first_space { void func() { cout << "Inside first_space" << endl; } } // second name space namespace second_space { void func() { cout << "Inside second_space" << endl; } } using namespace first_space; int main () { // This calls function from first name space. func(); return 0; }
Notice without the namespaces, we could not have func twice. Members of namespaces can be addressed using the :: operator, or with a using namespace
directive, which makes all members of the namespace directly available. Notice cout is in the std namespace. We could either use std::cout, or a using namespace std
as above. Scope rules apply for using, so inside a function is only applies to that function etc. Namespaces can be nested, they are then used with multiple ::.
Notice the :: is also used to define methods for a class. This does not mean namespaces and classes are the same, just the operator is reused.
Namespaces can be re-opened:
namespace my_namespace { int function1(); } namespace my_namespace { int function2(); }
This is only one namespace. This also works across files.