How object-oriented can C get

Object-oriented programming with C ++

Martin Kompf

content

Objects

C ++ is not only the better C, but also offers the developer the possibility of object-oriented programming. In addition to learning new language elements, this also requires a new "object-oriented" way of thinking. This way of thinking is to be familiarized with this way of thinking in the form of a tutorial.

Traditional software development often consisted of designing algorithms to solve a given problem and pouring them into procedures that are formulated in a programming language such as C. One therefore speaks of procedural programming.

However, if one looks at the real world, one finds that things do not move in an abstract procedural manner here. The object-oriented approach tries to overcome this break between the real world and software development. If one analyzes one's material environment, one finds that it essentially consists of objects that interact with one another in different ways. There could be a bicycle object in our immediate vicinity.

If you take a closer look at the bike, you can see that this bike made the difference properties or. Attributes (like color, size, current speed) and that it Methods change these properties (e.g. the "step faster" method, which will lead to an increase in speed, or the "brake" method, which should have the opposite effect).

If you transform this knowledge from the real into the software world, you can formulate:

A software object is a bundle of attributes and related methods.

Classes

Classes as a blueprint

If we take a closer look at various bicycle objects in the real world, we find that all of these bicycle objects are similar: all of them have brakes, all of them have some color, etc. So it has to be one Blueprint that describes what a bike should look like in general. All bicycle objects have been created according to this blueprint - this is why it is also possible for us to ride any bike once we have learned to ride a bike! Again we transfer this knowledge to the software world:

A class is a blueprint that defines the attributes and methods that all objects of a certain type have.

The figure shows a class in the so-called UML (unified modeling language) notation. This class could be used in a stock research program. Such a program must be able to manage many different stock objects. For this to happen effectively, however, all of these different objects should be created according to a uniform blueprint - the class definition.

The name of the class is in the UML notation in the upper third of the rectangle. In the middle third are the attributes. Our example class defines the two attributes m_name and m_value, i.e. the name of the share and the current market value. The minus sign in front of the attributes means that these are members of the class, i.e. they cannot be accessed directly from the outside.

The methods of the class are in the lower third. The plus sign in front of them shows that they are, i.e. the methods can be called by other objects. This shows the principle of encapsulation: Instead of accessing the m_value attribute directly, have to other objects use the access methods setValue () and getValue ()! The developer of the class has the option of adding additional queries in setValue (), e.g. regarding the validity of the parameter.

When looking at the figure, attentive readers will not have missed an extension of C ++ compared to C: In C ++, functions are not just - as in C - by name, but by name and Differentiated number and type of parameters! The method setValue () can exist twice: once with a parameter of the type and with one of the type. When this method is called, the compiler automatically detects which variant to use based on the parameter type.

Class definition

It is advisable to make C ++ class definitions generally in header files (these are those with the extension .h). A good programming style is to provide a header file for each class and to name it like the class.

The class definition for StockItem is therefore in the header file with the name StockItem.h performed:

/ * 1 * / # ifndef StockItem_h / * 2 * / # define StockItem_h 1 / * 3 * // * 4 * / class StockItem {/ * 5 * // * 6 * / public: / * 7 * /// ctor / * 8 * / StockItem (constchar * name = "", double val = 0.0); / * 9 * // * 10 * /// copy ctor / * 11 * / StockItem (const StockItem & right); / * 12 * /// dtor / * 13 * / virtual ~ StockItem (); / * 14 * /// assignment operator / * 15 * / const StockItem & operator = (const StockItem & right); / * 16 * // * 17 * /// public member functions / * 18 * / virtualconstchar * getName () const; / * 19 * / virtualvoid setValue (double val); / * 20 * / virtualvoid setValue (constchar * val); / * 21 * / virtualdouble getValue () const; / * 22 * ​​// * 23 * / private: / * 24 * /// private member variables / * 25 * / char * m_name; / * 26 * / double m_value; / * 27 * /}; / * 28 * // * 29 * / # endif

The class definition begins with the keyword followed by the name of the class (line 4). The actual definition is enclosed in curly braces {}. The keywords and separate the public (lines 7-21) and private (lines 24-26) areas of the definition discussed in the previous section. There is also the option of using variables and methods to declare that should only be available to derived classes. More on that later on the topic Inheritance.

The declaration of the public methods (lines 18 to 19) and the private variables (lines 25 and 26) is done in exactly the same way as a normal declaration of functions or variables in C. The use of the specification for the functions is noticeable. Again, this is for the subject Inheritance important and will be discussed later. But you don't do much wrong if you give every function this specification right from the start.

Be on lines 7-15 Constructor, Copy constructor, Destructor and Assignment operator declared. In order to understand the meaning of these elements, we must first refer to the Object life cycle enter

PS: Another little tip: The compiler produces the most strange error messages if the programmer forgets the semicolon at the end of the class definition (line 27) ...

Object life cycle

The life cycle of a C ++ object consists of the sections

  1. generation
  2. use
  3. destruction

Creation of an object

An object is created by calling the Constructor (short ctor) of the object. This can be done statically by declaring variables or dynamically using the operator:

StockItem bay; // default constructor, static StockItem dte ("Deutsche Telekom AG", 50.34); // special ctor, static StockItem * bas = new StockItem ("BASF", 120.34); // special ctor, dynamic // the storage space required for bas is allocated on the heap

Using an object

An object is used by calling one of its methods:

dte.setValue (52.80); // calls the StockItem :: setValue () method of the dte object a = bas-> getValue (); // calls the StockItem :: getValue () method of the bas object

In addition, the complete object can be passed as a parameter to a function or returned by this via. The transfer of value takes place by value, then implicitly becomes the Copy constructor of the object called:

StockItem cnv (StockItem x) {// ... return x2; // return per value: calls the copy constructor of x2} StockItem bas ("BASF", 120.34); cnv (bas); // parameter per value: calls the copy constructor of bas on StockItem bas2 = bas; // explicit copy: calls bas2's copy constructor

Likewise, an object can be completely assigned to another already existing object if it has the same type or the type of a base class. Then the Assignment operator of the object called:

StockItem x; StockItem y ("ABC", 22); x = y; // calls the assignment operator of x

Destroying an object

If an object is no longer required, it should be destroyed so that it no longer uses up any storage space. When an object is destroyed it will be automatic Destructor (short dtor) called. In the case of an object created dynamically by, this is done with the operator. A statically generated object is automatically destroyed if the scope of the static declaration is exceeded during program execution:

delete bas; // the destructor of the object bas is called and then // the memory space occupied by bas is released if (x) {StockItem bmw; // ...} // the dtor of the statically created object bmw is called here at the end of the block // because the declaration has left the scope of validity

Note:The destructor of an object is usually never called directly, but is called implicitly, as shown above, by using or when leaving the scope!

Object implementation

What we are still missing for the first complete C ++ program is the implementation of the individual methods of the StockItem object.

The implementation is preferably in a file named StockItem.cpp performed. At the beginning the header file StockItem.h includes:

/ * 1 * / # include "StockItem.h" / * 2 * / # include / * 3 * / # include / * 4 * // * 5 * / StockItem :: StockItem (constchar * name / * = "" * /, double val / * = 0.0 * /) / * 6 * / {/ * 7 * / m_name = newchar [strlen (name) +1]; / * 8 * / strcpy (m_name, name); / * 9 * / m_value = val; / * 10 * /} / * 11 * // * 12 * / StockItem :: StockItem (const StockItem & right) / * 13 * // * 14 * / {/ * 15 * // * 16 * / m_name = newchar [ strlen (right.m_name) +1]; / * 17 * / strcpy (m_name, right.m_name); / * 18 * / m_value = right.m_value; / * 19 * /} / * 20 * // * 21 * / StockItem :: ~ StockItem () / * 22 * ​​/ {/ * 23 * / delete [] m_name; / * 24 * /} / * 25 * // * 26 * / const StockItem & StockItem :: operator = (const StockItem & right) / * 27 * / {/ * 28 * /// handle self assignment / * 29 * / if (this! = & right) {/ * 30 * / delete [] m_name; / * 31 * / m_name = newchar [strlen (right.m_name) +1]; / * 32 * / strcpy (m_name, right.m_name); / * 33 * / m_value = right.m_value; / * 34 * /} / * 35 * // * 36 * / return * this; / * 37 * /} / * 38 * // * 39 * / constchar * StockItem :: getName () const / * 40 * / {/ * 41 * / return m_name; / * 42 * /} / * 43 * // * 44 * / void StockItem :: setValue (double val) / * 45 * / {/ * 46 * / m_value = val; / * 47 * /} / * 48 * // * 49 * / void StockItem :: setValue (constchar * val) / * 50 * / {/ * 51 * / m_value = atof (val); / * 52 * /} / * 53 * // * 54 * / double StockItem :: getValue () const / * 55 * / {/ * 56 * / return m_value; / * 57 * /}

The implementation of the public methods for setting and reading the values ​​of the attributes in lines 39 to 57 contains nothing special, the code should speak for itself.

The constructor is more interesting (lines 5 to 10): Since we want to save the name of the in a C character string of the type, the memory space required for this must first be allocated (line 7). Since we program in C ++, we do not use the one we are familiar with from the C world, but the C ++ operator.

The situation is similar with the copy constructor (lines 12 to 19): Here the only argument is always a reference to an object of the same type. Its member variables must be in their own variables copied so that a 1: 1 copy of the object is created. We remember: The copy constructor is used when passing objects by value to or from functions or when explicitly copying.

Things are even more interesting with the assignment operator (lines 26 to 37): An object x already exists here, into which a second object y is copied! So must first the memory space occupied by x must be released (line 30) before the variables can be copied. By the way, in the C ++ language our object x is called. The reference to your own object is so important that it is even a reserved word in C ++ and is always available within object functions. The assignment operator returns a pointer to the object itself (i.e. to), after all it is used in assignments such as

StockItem x; StockItem y ("ABC", 22); x = y;

used.

By the way, nobody prevents the programmer from writing the assignment x = x instead of x = y! Then we have the case of Self assignments present: An object is assigned to itself. Then the assignment operator is not allowed to do anything else. The query as to whether there is a Self Assignment takes place in line 29.

The memory space explicitly requested by the operating system during the object construction must also be expressly released again when the object is destroyed. We remember: When the object is destroyed, its destructor is automatically executed. Therefore the memory area requested for m_name is released in the destructor (lines 21 to 24).

Inheritance

concept

Inheritance allows the definition of new classes on the basis of existing classes. This is a basic concept of object-oriented design. The concept of class declared as a kind of building plan for objects. Based on the example "bicycle" used there, further parallels to the real world can be drawn: It is noticeable that it is here different types of bicycles: racing bikes, mountain bikes, trekking bikes and the good old Dutch bike. Why are all these different wheels recognizable to us as bicycles? Because they have certain common features: They all have two wheels, a handlebar and can be moved by stepping on the pedals. In addition to these similarities, they also bring new properties: mountain bikes and racing bikes each have a gear shift, but differ in the type of tires.

In object-oriented language, one could say: The classes of mountain bikes, racing bikes and Dutch bikes inherit Features shared by the class of bicycles and add additional ones. In general:

  • Classes can be defined depending on other classes: "A is a kind of B". In this case, B is the Base class by A.
  • A class can also inherit from several classes: A is a type of B and C (multiple inheritance).
  • Each class inherits the (public) attributes and methods of its base class (es).
  • However, each class can add its own variables and methods.

It is important to understand that inheritance only works in one direction: a racing bike is always a bike, but not every bike is automatically a racing bike. If an object a of class A is defined in C ++ and class A is derived from B, then a can be converted into an object of type B at any time using cast. The reverse does not apply: In this case, an object b of class B cannot be converted to type A!

Inheritance in C ++

The class we have already defined and used StockItem allows a name and an associated value to be saved and is intended for displaying stock prices. Anyone who has already dealt with this matter knows that a lot more information can be stored for a share. In addition to the (daily or weekly) closing price (Close) still opening (Open), Maximum (High) and lowest price (Low). We therefore now want to expand StockItem to include the option of querying and setting the opening price.

However, this extension should not be done by changing the existing StockItem class - this class is already used in many software projects and a change in its functionality could have bad effects under certain circumstances. Nor do we want to completely reinvent the wheel - existing code should be reused as far as possible. We achieve all of this by creating a new class StockItemOC Define that from the existing class StockItem derived is:

/ * 1 * / # ifndef StockItemOC_h / * 2 * / # define StockItemOC_h 1 / * 3 * // * 4 * / class StockItemOC: public StockItem {/ * 5 * // * 6 * / public: / * 7 * / // ctor / * 8 * / StockItemOC (constchar * name = "", double open = 0.0, double close = 0.0); / * 9 * // * 10 * /// copy ctor / * 11 * / StockItemOC (const StockItemOC & right); / * 12 * /// dtor / * 13 * / virtual ~ StockItemOC (); / * 14 * /// assignment operator / * 15 * / const StockItemOC & operator = (const StockItemOC & right); / * 16 * // * 17 * /// public member functions / * 18 * / virtualvoid setValue (double val); / * 19 * / virtualdouble getValue () const; / * 20 * / virtualvoid setOpen (double val); / * 21 * / virtualdouble getOpen () const; / * 22 * ​​/ virtualvoid setClose (double val); / * 23 * / virtualdouble getClose () const; / * 24 * // * 25 * / private: / * 26 * /// private member variables / * 27 * / double m_open; / * 28 * / double m_close; / * 29 * /}; / * 30 * // * 31 * / # endif

The only extension compared to the class definition known to us is that at the beginning of line 4 the base class is specified after the colon and the keyword.

How does this inheritance relationship affect the behavior of our new StockItemOC class? Let's take a look at the UML diagram:

  • StockItemOC inherits the method getName () of StockItem. This method does not need to be implemented again.
  • StockItemOC overwrites the methods setValue () and getValue () of StockItem. These methods have to be implemented differently, since StockItemOC has two price values ​​instead of one to choose from.
  • StockItemOC has the additional Methods getOpen (), setOpen (), getClose () and setClose () in order to be able to map the additional desired functionality.
  • StockItemOC inherits the variables m_name and m_value of StockItem. Since these variables are there as private are declared, the StockItemOC methods have no access to this variable - except of course getName (), because this is also inherited from StockItem.
  • StockItemOC declares the additional private variables m_open and m_close.

Implementation of the derived class

First, let's take a look at the implementation of the derived class in StockItemOC.cpp throw:

/ * 1 * / # include / * 2 * / # include "StockItem.h" / * 3 * / # include "StockItemOC.h" / * 4 * // * 5 * / StockItemOC :: StockItemOC (constchar * name, double open, double close) / * 6 * /: StockItem (name), m_open (open), m_close (close) / * 7 * / {} / * 8 * // * 9 * / StockItemOC :: StockItemOC (const StockItemOC & right) / * 10 * /: StockItem (right), m_open (right.m_open), m_close (right.m_close) / * 11 * / {} / * 12 * // * 13 * / StockItemOC :: ~ StockItemOC () / * 14 * / {} / * 15 * // * 16 * / const StockItemOC & StockItemOC :: operator = (const StockItemOC & right) / * 17 * / {/ * 18 * /// handle self assignment / * 19 * / if (this! = & Right) {/ * 20 * / StockItem :: operator = (right); / * 21 * / m_open = right.m_open; / * 22 * ​​/ m_close = right.m_close; / * 23 * /} / * 24 * // * 25 * / return * this; / * 26 * /} / * 27 * // * 28 * / void StockItemOC :: setValue (double val) / * 29 * / {/ * 30 * / m_close = val; / * 31 * /} / * 32 * // * 33 * / double StockItemOC :: getValue () const / * 34 * / {/ * 35 * / return m_close; / * 36 * /} / * 37 * // * 38 * / void StockItemOC :: setOpen (double val) / * 39 * / {/ * 40 * / m_open = val; / * 41 * /} / * 42 * // * 43 * / double StockItemOC :: getOpen () const / * 44 * / {/ * 45 * / return m_open; / * 46 * /} / * 47 * // * 48 * / void StockItemOC :: setClose (double val) / * 49 * / {/ * 50 * / m_close = val; / * 51 * /} / * 52 * // * 53 * / double StockItemOC :: getClose () const / * 54 * / {/ * 55 * / return m_close; / * 56 * /}

The constructor (lines 5 to 7) is interesting here again: In contrast to the StockItem constructor above, the variables (m_open and m_close) are not initialized in the function body here, but in a Initialization list. Above all, this creates performance advantages in the construction of the objects. Furthermore, we have to consider that the variables declared as private in the base class (such as m_name) cannot be addressed here directly. In order to initialize this correctly anyway, the constructor of the base class is called in the initialization list (line 6).

The same applies to the copy constructor (lines 9 to 11), which lists the copy constructor of the base class in its initialization list. And the assignment operator must also explicitly call that of the base class (line 20). The behavior is only different for the destructor (lines 13 and 14). Here the C ++ runtime environment ensures that all destructors of classes derived from one another are called in the correct order. Since no dynamically allocated memory is used in StockItemOC, the destructor does not need to do anything else.

The remaining methods of StockItemOC are used to set and read out the private variables and offer nothing new.

Use of derived classes

The use of derived classes will now be demonstrated in a small test program using the StockItemOC class.

The required header files must be included at the beginning of the program file:

#include #include #include #include #include "StockItem.h" #include "StockItemOC.h" usingnamespace std; int main (int argc, char ** argv) {

As a first exercise, we will statically create two StockItem objects and one StockItemOC object and then use these to output some values:

StockItem a ("BAY", 34.9); StockItem b ("BAS"); StockItemOC c ("DTE", 57.0, 59.4); b.setValue (24.2); cout << a.getName () << ":" << a.getValue () << endl; cout << b.getName () << ":" << b.getValue () << endl; cout << c.getName () << ":" << c.getValue () << "(" << c.getOpen () << "->" << c.getClose () << ") \ n ";

At first glance, this does not offer anything new. When we take a second look, we see that the getName () method of object c, which is of the StockItemOC type, is called in the last line. The class StockItemOC has not defined any method getName ()! However, with the newly acquired knowledge of inheritance, it is clear what happens: it just becomes that of the StockItem class inherited Method used!

The use of the getValue () method is also interesting. These are available in both StockItem and StockItemOC. In this example, however, it is relatively easy to understand what happens - both for us and for the C ++ compiler: StockItemOC :: getValue () is called in the last line, and StockItem :: getValue () in the two lines before it. This is clear because the type of objects a, b and c is already known at compilation time and the compiler can decide which method call to use. This is also called the static or early attachment designated.

However, what happens if the compiler does not yet know the exact type of the object when it is compiled? Let's look at the continuation of the program:

StockItem * astocks [3]; astocks [0] = & a; astocks [1] =? astocks [2] = & c; // cast from StockItemOC * to StockItem * for (int i = 0; i <3; ++ i) {cout << astocks [i] -> getName () << ":" << astocks [i] -> getValue () << endl; }

When calling astocks [i] -> getValue (), a different method must be called depending on whether the pointer in astocks points to an object of the StockItem type (at index 0 and 1) or of the StockItemOC type (index 2). This can only be decided at runtime, one then speaks of more dynamic or later bond. But how can the runtime system decide what type the object is? The key to this is the so-called vtable, in which the specific function signatures for each object are stored. However, a function signature is only correctly entered in the vtable if the functions are declared as being in the class definition. Fortunately, we have already done this in our definitions, so the program works as expected. However, you should internalize the following rule when dealing with C ++:

Declare all member functions - including the destructor - as virtual if you intend to use inheritance mechanisms!

Finally, an advanced application of our objects. Instead of being packed into an array, they are packed into an STL vector. The objects are then sorted and output according to their value (getValue ()). All of this works even if the vector contains both StockItem * and StockItemOC * objects, because dynamic binding is used again:

// get the stock items ored by price vector stocks; stocks.push_back (& ​​a); stocks.push_back (& ​​b); stocks.push_back (& ​​c); // cast from StockItemOC * to StockItem * sort (stocks.begin (), stocks.end (), CompareStockPrice ()); cout << endl << "stock items odered by price \ n"; reverse_copy (stocks.begin (), stocks.end (), ostream_iterator (cout)); }

For the correct sorting and the output, two auxiliary functions are necessary, which can be inserted before main () in the program file: CompareStockPrice is a so-called functor. It is used as a parameter to the sorting algorithm sort and is used to compare two objects of the StockItem type. The second function is the output operator for a StockItem object. This is in the algorithm reverse_copy on the ostream_iterator used.

// compare two stock pricesclass CompareStockPrice {public: intoperator () (const StockItem * s1, const StockItem * s2) {return s1-> getValue () getValue (); }}; // operator << for class StockItem // write name and value to output stream ost ostream & operator << (ostream & ost, const StockItem * item) {ost << item-> getName () << ":" << item- > getValue () << endl; return ost; }

As you can see, it is completely sufficient to implement the auxiliary functions for the StockItem class. When the function is called, it does not matter whether the current parameter points to an object of the type StockItem or StockItemOC. This is a result (and advantage) of the inheritance hierarchy we have implemented.

Download