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 defition 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 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").
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 function1(); }
This is only one namespace. This also works across files.