Ganze Zahlen als Objekt: Die Klasse Integer

1. Aufgabe

In dieser Aufgabe betrachten wir eine kleine Spielerei, mit der Sie speziell Ihre Kenntnisse zum Überladen von Operatoren vertiefen können. Wie Sie wissen, benötigen wir für ganze Zahlen in C++ keinen separaten Klassentyp, es gibt ja bereits den vordefinierten Datentyp int. Ganzzahlige Datentypen haben eine große Bedeutung in Programmiersprachen, da ja gerade in ihrer arithmetischen Verarbeitung eine der großen Stärken von Softwareprogrammen liegt. Auch innerhalb einer Programmiersprache kommen ganzzahlige Datentypen zum Zuge, wie etwa in der gängigen for-Wiederholungsanweisung oder der switch-Auswahlanweisung.

Prinzipiell hätte man den Datentyp int durchaus als Klasse definieren können, wie es beispielsweise in Smalltalk der Fall ist. Smalltalk ist im Gegensatz zu Sprachen wie C++ oder Java eine rein objektorientierte Programmiersprache, das heißt Daten des Typs int, char usw., die in anderen objektorientierten Sprachen als primitive Datentypen umgesetzt werden, werden in Smalltalk über Objekte und zugehörige Klassen realisiert. Aus Performancegründen hat man diesen Ansatz in C++ nicht gewählt.

Ziel dieser Aufgabe ist es nun, ganz im Sinne von Smalltalk einen Klassentyp Integer zu realisieren, dessen Objekte sich – so gut es geht – wie int-Variablen verhalten. Dies heißt insbesondere, dass durch geschicktes Überladen der arithmetischen Operatoren in der Anwendung des Klassentyps nicht mehr erkennbar sein soll, ob es sich bei den benutzten Daten in Wirklichkeit um Integer-Objekte oder um int-Variablen handelt – würde man den Deklarationsteil der beteiligten Variablen abdecken. Das folgende Beispiel möge dies verdeutlichen:

void Main ()
{
    Integer Fak, Max, j;

    // begin of demonstration
    Fak = 1;
    Max = 5;

    for (j = 1; j <= Max; j++)
        Fak *= j;

    cout << "Fak(" << Max << ") = " << Fak << endl;
}

Ausgabe:

Fak(5) = 120

Natürlich ließe sich der Klassentyp Integer, ohne ihn zu verstecken, auch direkt in einer for-Wiederholungsanweisung benutzen:

void Demonstration02 ()
{
    Integer Fak = 1;
    Integer Max = 5;

    for (Integer j = 1; j <= Max; j++)
        Fak *= j;

    cout << "Fak(" << Max << ") = " << Fak << endl;
}

2. Lösung

Quellcode: Siehe auch github.com/peterloos/Cpp_Integer.

Wir stellen die Klasse Integer in einer kompakten und in einer ausführlichen Form vor. In der kompakten Variante sind zahlreichen Methoden und Operatoren inline in der Header-Datei, also in der Schnittstelle der Klasse, implementiert. Da diese Funktionen allesamt sehr trivial sind, kann man nicht davon reden, dass ein besonderes Implementierungs-KnowHow offen gelegt wird.

Zu Demonstrationszwecken stellen wir auch eine ausführliche Form der Integer-Klasse vor. Hier sind die Schnittstelle und ihre Implementierung strikt voneinander getrennt. In Listing 1 und Listing 2 finden Sie die ausführliche Form vor:

01: class Integer
02: {
03: private:
04:     int m_value;
05: 
06: public:
07:     // c'tors
08:     Integer ();
09:     Integer (int);
10:     Integer (char*);
11: 
12:     // unary arithmetic operators
13:     friend Integer operator+ (const Integer&);
14:     friend Integer operator- (const Integer&);
15: 
16:     // binary arithmetic operators
17:     friend Integer operator+ (const Integer&, const Integer&);
18:     friend Integer operator- (const Integer&, const Integer&);
19:     friend Integer operator* (const Integer&, const Integer&);
20:     friend Integer operator/ (const Integer&, const Integer&);
21:     friend Integer operator% (const Integer&, const Integer&);
22: 
23:     // arithmetic-assignment operators
24:     friend Integer& operator+= (Integer&, const Integer&);
25:     friend Integer& operator-= (Integer&, const Integer&);
26:     friend Integer& operator*= (Integer&, const Integer&);
27:     friend Integer& operator/= (Integer&, const Integer&);
28:     friend Integer& operator%= (Integer&, const Integer&);
29: 
30:     // increment / decrement operators
31:     friend Integer& operator++ (Integer&);           // prefix increment
32:     friend const Integer operator++ (Integer&, int); // postfix increment
33:     friend Integer& operator-- (Integer&);           // prefix decrement
34:     friend const Integer operator-- (Integer&, int); // postfix decrement
35: 
36:     // comparison operators
37:     friend bool operator== (const Integer&, const Integer&);
38:     friend bool operator!= (const Integer&, const Integer&);
39:     friend bool operator<  (const Integer&, const Integer&);
40:     friend bool operator<= (const Integer&, const Integer&);
41:     friend bool operator>  (const Integer&, const Integer&);
42:     friend bool operator>= (const Integer&, const Integer&);
43: 
44:     // conversion operator
45:     operator int ();
46:     
47:     // getter / setter
48:     int GetValue ();
49:     void SetValue (int);
50:     static int MaxValue ();
51:     static int MinValue ();
52: 
53:     // public methods
54:     void Parse (char*);
55:     void ToHexString (char[]);
56:     void ToBinaryString (char[]);
57: 
58:     // private helper methods
59: private:
60:     static void ToUnsignedString (char[], int, int);
61: 
62:     // output
63:     friend ostream& operator<< (ostream&, const Integer&);
64: };

Beispiel 1. Klasse Integer: Schnittstelle – ohne Inline-Code.


001: #include <iostream>
002: using namespace std;
003: 
004: #include "Integer.h"
005: 
006: // c'tors
007: Integer::Integer ()
008: {
009:     m_value = 0;
010: }
011: 
012: Integer::Integer (int n)
013: {
014:     m_value = n;
015: }
016: 
017: Integer::Integer (char* s)
018: {
019:     Parse (s);
020: }
021: 
022: // unary arithmetic operators
023: Integer operator+ (const Integer& i)
024: {
025:     return Integer (i.m_value);
026: }
027: 
028: Integer operator- (const Integer& i)
029: {
030:     return Integer (-i.m_value);
031: }
032: 
033: // binary arithmetic operators
034: Integer operator+ (const Integer& i1, const Integer& i2)
035: {
036:     return Integer (i1.m_value + i2.m_value);
037: }
038: 
039: Integer operator- (const Integer& i1, const Integer& i2)
040: {
041:     return Integer (i1.m_value - i2.m_value);
042: }
043: 
044: Integer operator* (const Integer& i1, const Integer& i2)
045: {
046:     return Integer (i1.m_value * i2.m_value);
047: }
048: 
049: Integer operator/ (const Integer& i1, const Integer& i2)
050: {
051:     return Integer (i1.m_value / i2.m_value);
052: }
053: 
054: Integer operator% (const Integer& i1, const Integer& i2)
055: {
056:     return Integer (i1.m_value % i2.m_value);
057: }
058: 
059: // arithmetic-assignment operators
060: Integer& operator+= (Integer& i1, const Integer& i2)
061: {
062:     i1.m_value += i2.m_value;
063:     return i1;
064: }
065: 
066: Integer& operator-= (Integer& i1, const Integer& i2)
067: {
068:     i1.m_value -= i2.m_value;
069:     return i1;
070: }
071: 
072: Integer& operator*= (Integer& i1, const Integer& i2)
073: {
074:     i1.m_value *= i2.m_value;
075:     return i1;
076: }
077: 
078: Integer& operator/= (Integer& i1, const Integer& i2)
079: {
080:     i1.m_value /= i2.m_value;
081:     return i1;
082: }
083: 
084: Integer& operator%= (Integer& i1, const Integer& i2)
085: {
086:     i1.m_value %= i2.m_value;
087:     return i1;
088: }
089: 
090: // comparison operators
091: bool operator== (const Integer& i1, const Integer& i2)
092: {
093:     return i1.m_value == i2.m_value;
094: }
095: 
096: bool operator!= (const Integer& i1, const Integer& i2)
097: {
098:     return !(i1 == i2);
099: }
100: 
101: bool operator<  (const Integer& i1, const Integer& i2)
102: {
103:     return i1.m_value < i2.m_value;
104: }
105: 
106: bool operator<= (const Integer& i1, const Integer& i2)
107: {
108:     return i1.m_value <= i2.m_value;
109: }
110: 
111: bool operator>  (const Integer& i1, const Integer& i2)
112: {
113:     return ! (i1 <= i2);
114: }
115: 
116: bool operator>= (const Integer& i1, const Integer& i2)
117: {
118:     return ! (i1 < i2);
119: }
120: 
121: // conversion operator
122: Integer::operator int ()
123: {
124:     return m_value;
125: }
126: 
127: // getter / setter
128: int Integer::GetValue ()
129: {
130:     return m_value;
131: }
132: 
133: void Integer::SetValue (int value)
134: {
135:     m_value = value;
136: }
137: 
138: int Integer::MaxValue ()
139: {
140:     return ~Integer::MinValue ();
141: }
142: 
143: int Integer::MinValue ()
144: {
145:     return 1 << (sizeof (int) * 8 - 1);
146: }
147: 
148: // increment / decrement operators
149: Integer& operator++ (Integer& i)  // prefix version
150: {
151:     i += 1;
152:     return i;
153: }
154: 
155: Integer& operator-- (Integer& i)  // prefix version
156: {
157:     i -= 1;
158:     return i;
159: }
160: 
161: const Integer operator++ (Integer& i, int)  // postfix version
162: {
163:     Integer tmp(i);  // construct a copy
164:     ++ i;            // increment integer
165:     return tmp;      // return the copy
166: }
167: 
168: const Integer operator-- (Integer& i, int)  // postfix version
169: {
170:     Integer tmp(i);  // construct a copy
171:     -- i;            // decrement integer
172:     return tmp;      // return the copy
173: }
174: 
175: // public methods
176: void Integer::Parse (char* s)
177: {
178:     int len = 0;
179:     while (s[len] != '\0')
180:         len ++;
181: 
182:     m_value = 0;
183:     for (int i = 0; i < len; i ++)
184:     {
185:         if (s[i] < '0' || s[i] > '9')
186:         {
187:             m_value = 0;
188:             return;
189:         }
190: 
191:         m_value = 10 * m_value + (s[i] - '0');
192:     }
193: }
194: 
195: void Integer::ToHexString (char buf[])
196: {
197:     Integer::ToUnsignedString(buf, m_value, 4);
198: }
199: 
200: void Integer::ToBinaryString (char buf[])
201: {
202:     Integer::ToUnsignedString(buf, m_value, 1);
203: }
204: 
205: // convert an integer to an unsigned number
206: void Integer::ToUnsignedString (char buf[], int number, int shift)
207: {
208:     const int BufSize = 64;
209:     char tmp[BufSize];
210: 
211:     int mask = (1 << shift) - 1;
212:     int seperator = 0;
213: 
214:     const int NumDigits = 32 / shift;
215: 
216:     int pos = BufSize;
217:     for (int i = 0; i < NumDigits; i ++)
218:     {
219:         // calculate digit
220:         int rest = number & mask;
221:         char ch = (rest < 10) ? '0' + rest : 'A' + (rest - 10);
222:         pos --;
223:         tmp[pos] = ch;
224: 
225:         // insert blank
226:         seperator ++;
227:         if (seperator % 4 == 0 && i < NumDigits - 1)
228:         {
229:             pos --;
230:             tmp[pos] = ' ';
231:         }
232: 
233:         number >>= shift;
234:     }
235: 
236:     // copy result to callers buffer
237:     for (int i = 0; i < (BufSize - pos); i ++)
238:         buf[i] = tmp[pos + i];
239:     buf[BufSize - pos] = 0;
240: }
241: 
242: // output
243: ostream& operator<< (ostream& os, const Integer& i)
244: {
245:     os << i.m_value;
246:     return os;
247: }

Beispiel 2. Klasse Integer: Implementierung.


Verlagert man die Realisierung der einfachen Methoden direkt in die Klassenschnittstelle, kann man die Implementierung der Integer-Klasse erheblich kürzer gestalten. In Listing 3 finden Sie eine modifizierte Schnittstelle der Klasse Integer vor. Die Implementierung der Klasse müssen wir nicht noch einmal abdrucken, da sie – bis auf die bereits inline definierten Methoden – identisch mit Listing 2 ist.

01: class Integer
02: {
03: private:
04:     int m_value;
05: 
06: public:
07:     // c'tors
08:     Integer ()          { m_value = 0; }
09:     Integer (int value) { m_value = value; }
10:     Integer (char* s)   { Parse (s); }
11: 
12:     // unary arithmetic operators
13:     friend Integer operator+ (const Integer& i)
14:         { return Integer (i.m_value) ;}
15:     friend Integer operator- (const Integer& i)
16:         { return Integer (-i.m_value); }
17: 
18:     // binary arithmetic operators
19:     friend Integer operator+ (const Integer& i1, const Integer& i2)
20:         { return Integer (i1.m_value + i2.m_value); }
21:     friend Integer operator- (const Integer& i1, const Integer& i2)
22:         { return Integer (i1.m_value - i2.m_value); }
23:     friend Integer operator* (const Integer& i1, const Integer& i2)
24:         { return Integer (i1.m_value * i2.m_value);}
25:     friend Integer operator/ (const Integer& i1, const Integer& i2)
26:         { return Integer (i1.m_value / i2.m_value); }
27:     friend Integer operator% (const Integer& i1, const Integer& i2)
28:         { return Integer (i1.m_value % i2.m_value); }
29: 
30:     // arithmetic-assignment operators
31:     friend Integer& operator+= (Integer& i1, const Integer& i2)
32:         { i1.m_value += i2.m_value; return i1; }
33:     friend Integer& operator-= (Integer& i1, const Integer& i2)
34:         { i1.m_value -= i2.m_value; return i1; }
35:     friend Integer& operator*= (Integer& i1, const Integer& i2)
36:         { i1.m_value *= i2.m_value; return i1; }
37:     friend Integer& operator/= (Integer& i1, const Integer& i2)
38:         { i1.m_value /= i2.m_value; return i1; }
39:     friend Integer& operator%= (Integer& i1, const Integer& i2)
40:         { i1.m_value %= i2.m_value; return i1; }
41: 
42:     // increment / decrement operators
43:     friend Integer& operator++ (Integer&);           // prefix increment
44:     friend const Integer operator++ (Integer&, int); // postfix increment
45:     friend Integer& operator-- (Integer&);           // prefix decrement
46:     friend const Integer operator-- (Integer&, int); // postfix decrement
47: 
48:     // comparison operators
49:     friend bool operator== (const Integer& i1, const Integer& i2)
50:         { return i1.m_value == i2.m_value; }
51:     friend bool operator!= (const Integer& i1, const Integer& i2)
52:         { return i1.m_value != i2.m_value; }
53:     friend bool operator<  (const Integer& i1, const Integer& i2)
54:         { return i1.m_value < i2.m_value; }
55:     friend bool operator<= (const Integer& i1, const Integer& i2)
56:         { return i1.m_value <= i2.m_value; }
57:     friend bool operator>  (const Integer& i1, const Integer& i2)
58:         { return i1.m_value > i2.m_value; }
59:     friend bool operator>= (const Integer& i1, const Integer& i2)
60:         { return i1.m_value >= i2.m_value; }
61: 
62:     // conversion operator
63:     operator int () { return m_value; }
64:     
65:     // getter / setter
66:     int GetValue () { return m_value; }
67:     void SetValue (int value) { m_value = value; }
68:     static int MaxValue () { return ~Integer::MinValue (); }
69:     static int MinValue () { return 1 << (sizeof (int) * 8 - 1); }
70: 
71:     // public methods
72:     void Parse (char*);
73:     void ToHexString (char[]);
74:     void ToBinaryString (char[]);
75: 
76:     // private helper methods
77: private:
78:     static void ToUnsignedString (char[], int, int);
79: 
80:     // output
81:     friend ostream& operator<< (ostream&, const Integer&);
82: };

Beispiel 3. Klasse Integer: Schnittstelle – mit Inline-Code.


Eine Klasse Integer, die nur aus überladenen Operatoren besteht, wäre in ihrem Entwurf etwas schmal angelegt. Wir fügen drei weitere Methoden ToBinaryString, ToHexString und Parse hinzu, um zum einen ganzzahlige Wert in ihre Binär- und Hexadezimaldarstellung wandeln zu können und um zum anderen eine Zeichenkette wie z.B. "123" in ein korrespondierendes Integer-Objekt überführen zu können. Genaue Details zu diesen Methoden sind in Tabelle 1 zusammengestellt:

Schnittstelle und Beschreibung

void Parse (char* number);

Wandelt die Zeichenkette number in ein Integer-Objekt um. Entsprechend darf die Zeichenkette nur aus den Ziffern 0 bis 9 bestehen.

void ToBinaryString (char[] buf);

Darstellung eines Integer-Objekts in einer binären Zeichenkette. Der Parameter buf muss zur Laufzeit durch ein hinreichend großes char-Array versorgt werden.

void ToHexString (char[] buf);

Darstellung eines Integer-Objekts in einer hexadezimalen Zeichenkette. Der Parameter buf muss zur Laufzeit durch ein hinreichend großes char-Array versorgt werden.

Tabelle 1. Zusätzliche Methoden der Klasse Integer.


Das folgende Codefragment verdeutlicht den Aufruf und die Arbeitsweise der drei Methoden ToBinaryString, ToHexString und Parse:

01: void main()
02: {
03:     char buf[64];
04: 
05:     Integer i(0);
06:     i.ToBinaryString (buf);
07:     cout << "|" << buf << "|" << endl;
08: 
09:     i = 1;
10:     i.ToBinaryString (buf);
11:     cout << "|" << buf << "|" << endl;
12: 
13:     i = -1;
14:     i.ToBinaryString (buf);
15:     cout << "|" << buf << "|" << endl;
16: 
17:     i = 127;
18:     i.ToBinaryString (buf);
19:     cout << "|" << buf << "|" << endl;
20:     
21:     i = -128;
22:     i.ToBinaryString (buf);
23:     cout << "|" << buf << "|" << endl;
24: 
25:     i = 32767;
26:     i.ToBinaryString (buf);
27:     cout << "|" << buf << "|" << endl;
28:     
29:     i = -32768;
30:     i.ToBinaryString (buf);
31:     cout << "|" << buf << "|" << endl;
32: 
33:     i = 2147483647;
34:     i.ToBinaryString (buf);
35:     cout << "|" << buf << "|" << endl;
36: 
37:     i = (-2147483647 - 1);    // minimum *signed* integer value !
38:     i.ToBinaryString (buf);
39:     cout << "|" << buf << "|" << endl;
40: 
41:     i.ToHexString (buf);
42:     cout << "|" << buf << "|" << endl;
43: 
44:     i = 1;
45:     i.ToHexString (buf);
46:     cout << "|" << buf << "|" << endl;
47: 
48:     i = -1;
49:     i.ToHexString (buf);
50:     cout << "|" << buf << "|" << endl;
51: 
52:     i = 127;
53:     i.ToHexString (buf);
54:     cout << "|" << buf << "|" << endl;
55:     
56:     i = -128;
57:     i.ToHexString (buf);
58:     cout << "|" << buf << "|" << endl;
59: 
60:     i = 32767;
61:     i.ToHexString (buf);
62:     cout << "|" << buf << "|" << endl;
63: 
64:     i = -32768;
65:     i.ToHexString (buf);
66:     cout << "|" << buf << "|" << endl;
67: 
68:     i = 2147483647;
69:     i.ToHexString (buf);
70:     cout << "|" << buf << "|" << endl;
71: 
72:     i = (-2147483647 - 1);    // minimum *signed* integer value !
73:     i.ToHexString (buf);
74:     cout << "|" << buf << "|" << endl;
75: }

Ausgabe:

|0000 0000 0000 0000 0000 0000 0000 0000|
|0000 0000 0000 0000 0000 0000 0000 0001|
|1111 1111 1111 1111 1111 1111 1111 1111|
|0000 0000 0000 0000 0000 0000 0111 1111|
|1111 1111 1111 1111 1111 1111 1000 0000|
|0000 0000 0000 0000 0111 1111 1111 1111|
|1111 1111 1111 1111 1000 0000 0000 0000|
|0111 1111 1111 1111 1111 1111 1111 1111|
|1000 0000 0000 0000 0000 0000 0000 0000|
|8000 0000|
|0000 0001|
|FFFF FFFF|
|0000 007F|
|FFFF FF80|
|0000 7FFF|
|FFFF 8000|
|7FFF FFFF|
|8000 0000|

Achtung

Beachten Sie zu den Zeile 37 und 72 des Testrahmens: Vorzeichenlose Typen können ausschließlich nicht negative Werte enthalten, sodass eine auf einen vorzeichenlosen Typ angewendete Negation (unärer Minus-Operator) in der Regel keinen Sinn ergibt. Sowohl der Operand als auch das Ergebnis sind nicht negativ.

In der Praxis tritt diese Situation ein, wenn der Programmierer versucht, den minimalen Ganzzahlwert, d. h. -2147483648, auszudrücken. Dieser Wert kann nicht mit „-2147483648“ dargestellt werden, da der Ausdruck in zwei Schritten verarbeitet wird:

  • Die Zahl 2147483648 wird ausgewertet. Da größer als der maximale Ganzzahlwert 2147483647, hat 2147483648 nicht den Typ int, sondern unsigned int.

  • Der unäre Minus-Operator wird auf den Wert angewendet. Dies führt zu einem vorzeichenlosen Ergebnis, das zufällig auch den Wert 2147483648 hat.

Der vorzeichenlose Typ des Ergebnisses kann zu unerwartetem Verhalten führen. Um dieses zu vermeiden, sollte man den minimalen Ganzzahlwert im Quellcode immer durch

-2147483647 - 1 /* minimum (signed) int value */

ausdrücken! Der Gebrauch des Ausdrucks

int i = -2147483648;

wird vom Visual C++ Compiler mit der Warnung „Unsigned types can hold only non-negative values, so unary minus (negation) does not usually make sense when applied to an unsigned type.“ quittiert!