Classes
Classes in C++ is not very different from any other OO-language, besides the syntax and the preferable separation of implementation and definition of functions. Another thing to take into consideration is that instances of classes can either be statically instantiated or dynamically instantiated with pointers, like we saw being done with arrays. We have different kind of classes: composition, association and ofcourse inherited classes. We will look at these types and how they differ. After that we will delve into the topic of polymorphism.
Composition
A class that consist of other classes and belongs to the "has-a"-category.
class Dinner {
public:
Dinner();
Dinner(char *name, char *name, int weight, );
bool eat(int w);
private:
Food food;
Drink drink;
};
class Food {
...
};
class Drink {
...
};
Association
"Use-a"-category. A class that is not composed of the other object but uses it, like when a manager employs a person. Both the employee and the manager are temporarily connected. The class has a pointer to the other class in practicality.
class Employee{
int age;
int name[30];
public:
Employee()
{
cout <<" I am Employee" << endl;
}
// other stuff
};
class Manager{
int x;
Employee *ptr;
public:
// other stuff
};
Inheritance
person.h
#ifndef PERSON_H
#define PERSON_H
#include <string.h>
#include <iostream>
using namespace std;
// copy strings
inline char *copyOf(char *s) {
if (!s) return 0;
char *n = new char[ strlen(s)+1 ]; // allocates spaces
strcpy( n, s);
return n;
}
class Person {
public:
Person(char *n);
Person(const Person &p);
~Person();
bool equal(const char *n);
void show();
protected:
char *name;
};
#endif
person.cpp
#include "person.h"
Person::Person(char *n) : name(copyOf(n))
{ cout << "Person " << name << "s constructor" << endl; }
Person::Person(const Person &p) : namn(copyOf( p.name))
{
cout << "Person " << name << "´s copyconstructor"
<< endl;
}
Person::~Person()
{
cout << "Person " << name << "´s destructor" << endl;
delete[] name;
}
bool Person::equal(const char *n)
{ return ( strcmp(name,n) == 0); }
void Person::show()
{ cout << "Person " << name << endl; }
student.h
#ifndef STUDENT_H
#define STUDENT_H
#include "person.h"
class Student : public Person {
public:
Student(char *n, char *c);
~Student();
void show();
const char *getCourse();
private:
char *course;
};
#endif
student.cpp
#include "student.h"
Student::Student(char *n, char *c) : Person(n), course(copyOf(c)) // call to super class
{
cout << course << "-student " << name <<
"´s constructor" << endl;
}
Student::~Student()
{
cout << course << "-student " << name <<
"s destructor" << endl;
delete[] course;
}
void Student::show()
{ cout << course << "-student " << name << endl; }
const char* Student::getCourse()
{ return course; }
and now the test program
#include "student.h"
int main()
{
Person p1("Adam");
Student p2("Eve", "Physics");
p1.show();
p2.show();
}
//output
Person Adam`s constructor
Person Eve`s constructor
Physics-student Eve`s constructor
Person Adam
Physics-student Eve
Physics-student Eve`s destructor
Person Eve`s destructor
Person Adam`s destructor
We see that in the case of Eve that the Person constructor is first called and then the Student constructor since the student constuctor calls that constructor. When the object is destroyed the Student constructor is first called then the Person since destructors are called up in the hierarchy.
Polymorphism
Polymorphism is used when you let a object be declared as a baseclass (Person) and in runtime let it be specified as a child class (Student). This is also called late-binding as opposed to early-binding. There are some tricks to this, that may be surprising, specially if you come from Java.
#include "student.h"
int main()
{
Student s1("Adam", "Biology");
Person* ptr;
ptr = &s1; // this is polymorphism!
s1.show();
ptr->show();
ptr = new Student("Cesar", "Cytology");
cout<<"Program done, delete objects from memory"<<endl;
delete ptr;
}
//output
Person Adam´s constructor
Biology-student Adam´s constructor
Biology-student Adam
Person Adam
Person Cesars constructor
Cytology-student Cesar´s constructor
Program done, delete objects from memory
Person Cesar´s destructor
Biology-student Adam´s destructor
Person Adam´s destructor
We see that when the ptr-pointer calls the show method, as the pointer is declared as a Person object it calls the Persons show method. Another problem is when we delete the pointer we only call the Person destructor and not the Student destructor, which leads to a memoryleak. This problem is helped with the virtual keyword.
class Person {
public:
Person(char *n);
Person(const Person &p);
virtual ~Person() {}; // inline definition
bool equal(const char *n);
virtual void show();
protected:
char *name;
};
Now that we declared the show method and the destructor as virtual methods we are telling the runtime system to wait with specifying what kind of class the actual object belongs to until runtime (late-binding). The new output is now correct.
Person Adam´s constructor
Biologi-student Adam´s constructor
Biologi-student Adam
Biologi-student Adam // now correct
Person Cesars constructor
Cytologi-student Cesar´s constructor
Program done, delete objects from memory
Cytologi-student Cesar`s destructor // now called
Person Cesar´s destructor
Biologi-student Adam´s destructor
Person Adam´s destructor
Now you cannot make a constructor virtual because you need to know what kind of object you are creating. But what if you want to create a copy of the object but you don´t know the type? Well a wellknown pattern is to create a virtual clone method in the baseclass that the other childclasses inherit and overwrite.
class Person {
public:
virtual Person* clone() = 0;
// Because of this definition we now call the Person class an abstract class.
// It cannot be instantiated itself, only the the child classes.
...
};
// Child classes
Student* Student::clone() { return new Student( *this); }
...
Employee* Employee::clone() { return new Employee( *this); }
And now you could do the following kind of thing.
Person *s = new Student("Adam", "Archeology");
Person *d = new Employee("Cesar");
Person *s1 = s->clone(); // creates a copy of s
Person *d1 = d->clone(); // creates a copy of d
If you are working with polymorphic types some time, you´ll maybe find yourself needing a way to cast that polymarphic type to its implementation class. In that case you could use the dynamic_cast<class *> function.
Circle *circle( Shape *s )
{
return dynamic_cast<Circle *>(s);
}
If the function can´t cast the polymorphic type (Shape) to the implementation class (Circle) because the object is specified as something else (a Rectangle for instance) it will return a null-pointer instead as you would expect, otherwise a pointer to the object. Well, now you maybe wonder when would you need to do this? One example:
#include <iostream>
class Shape {
public:
Shape( int xin, int yin) : x(xin), y(yin) {}
virtual bool operator==( Shape &s) = 0;
private:
int x,y;
};
class Circle : public Shape {
public:
Circle( int xin, int yin, int r ) : Shape(xin,yin), radius(r) {}
// bool operator==( Circle &c) WRONG!! You are NOT overriding the original
// { return radius==c.radius; }
bool operator==( Shape &s) // Right
{
Circle *c = dynamic_cast<Circle *>( &s);
if (c) return c->radius == radius;
else return 0;
}
private:
int radius;
};
class Square : public Shape {
public:
Square( int xin, int yin, int s ) : Shape(xin,yin), side(s) {}
bool operator==( Shape &s)
{
Square *sq = dynamic_cast<Square *>( &s);
if (sq) return sq->side == side;
else return 0;
}
private:
int side;
};
int main()
{
Square q1(10,10,5);
Square q2(20,20,5);
Circle c1(30,30,5);
Circle c2(30,30,4);
cout << (q1==q2) << endl;
cout << (q1==c1) << endl;
cout << (c1==c2) << endl;
}
Include files and implementation files
You have now maybe start wondering or even had experience with those nasty definitionfiles and you maybe think of this as a nasty business. Here follows a summary of good habits to follow when working with this issue. I stole this from another blogpost that I thought summarized it very well. If you are interested in the subject this is the blogpost.
Classes you create will often have dependencies on other classes. A derived class, for example, will always be dependent on its parent, because in order to be derived from the parent, it must be aware of its parent at compile time.
There are two basic kinds of dependencies you need to be aware of:
- stuff that can be forward declared
- stuff that needs to be #included
If, for example, class A uses class B, then class B is one of class A's dependencies. Whether it can be forward declared or needs to be included depends on how B is used within A:
- do nothing if: A makes no references at all to B
- do nothing if: The only reference to B is in a friend declaration
- forward declare B if: A contains a B pointer or reference:
B* myb; - forward declare B if: one or more functions has a B object/pointer/reference
as a parementer, or as a return type:
B MyFunction(B myb); #include "b.h"if: B is a parent class of A#include "b.h"if: A contains a B object:B myb;
You want to do the least drastic option possible. Do nothing if you can, but if you can't, forward declare if you can. But if it's necessary, then #include the other header.
Ideally, the dependencies for the class should be layed out in the header. Here is a typical outline of how a "right way" header might look:
myclass.h
#ifndef __MYCLASS_H_INCLUDED__
#define __MYCLASS_H_INCLUDED__
// forward declared dependencies
class Foo;
class Bar;
// included dependencies
#include <vector>
#include "parent.h"
// the actual class
class MyClass : public Parent // Parent object, so #include "parent.h"
{
public:
std::vector<int> avector; // vector object, so #include <vector>
Foo* foo; // Foo pointer, so forward declare Foo
void Func(Bar& bar); // Bar reference, so forward declare Bar
friend class MyFriend; // friend declaration is not a dependency
// don't do anything about MyFriend
};
#endif // __MYCLASS_H_INCLUDED__
This shows the two different kinds of dependencies and how to handle them. Because MyClass only uses a pointer to Foo and not a full Foo object, we can forward declare Foo, and don't need to #include "foo.h". You should always forward declare what you can -- don't #include unless it's necessary. Unnecessary #includes can lead to trouble.
Templates
Templates could be used by either classes or functions.
template functions
When a function is overloaded by different types, for example when defining a max function.
double max( double a, double b) { return (a>b) ? a : b; }
double max( int a, int b) { return (a>b) ? a : b; }
using a template function could be a better solution.
template <class T> T max(T a, T b)
{ return (a>b) ? a : b; }
double m = max<double>(5.0, 4.3); // call to template function
If your type now wouldn´t have the compare function, like in the case of strings, you could specialize the function for that special case.
char *max ( char *a, char *s)
{ return (strcmp(a,b)>0) ? a : b; }
The general syntax for template functions is:
template <class T, class Y, ..>
returntype name(parameterlist)
{
...
return returntype;
}
template classes
For Classes the formal definition look similar:
template <class T> class Name {...};
Here follows an example using a homegrown Array class.
array.h
#ifndef ARRAY_H
#define ARRAY_H
#include <stdexcept>
template <class T> class Array
{
public:
Array (int sz );
Array( const Array &arr);
~Array();
T& operator[](int ix);
Array& operator+=( const Array &arr );
bool includes( T &item );
private:
int size;
T *a;
};
template <class T>
Array<T>::Array ( int sz ) :
size(sz), a(new T[sz]) {}
template <class T>
Array<T>::Array ( const Array &arr ) :
size(arr.size), a(new T[size])
{
for (int i=0; i < size; i++)
a[i] = arr.a[i];
}
template <class T>
Array<T>& Array<T>::operator+=( const Array &arr )
{
if (size != arr.size)
throw length_error("Array: operator+=");
for (int i=0; i < size; i++)
a[i] += arr.a[i];
return *this;
}
template <class T>
Array<T>::~Array ()
{
delete[] a;
}
template <class T>
T& Array<T>::operator[](int ix)
{
return a[ix];
}
template <class T>
bool Array<T>::includes( T &item )
{
for (int i=0; i < size; i++)
if (item==a[i]) return true;
return false;
}
#endif
Notice that we put both the implementation and the declaration in the .h file. This is an exception for template classes. We don´t need to declare these memberfunctions or methods with inline since the compiler does that automatically. With this definition we could now write something like the following.
Array<Person> persons = Array<Person> persons(10); // creates a Person array
Array<int> ints = Array<int> ints(100); // creates a int array
Just remember that all methods and functions in the template class need support for the functions and methods they assume the template class T has. Besides template classes you can also specify a valueparameter among the template parameters.
template <class T, int sz> class Array {
public:
Array();
private:
int size;
T arr[sz];
};
//new constructor
template <class T, int sz> Array<T,sz>::Array() :
size(sz) {}
//usage
Array<int,50> intarr50;
This value parameter cannot be a float.
Inga kommentarer:
Skicka en kommentar