Laboratory 10 Abstract Data Types Introduction An abstract data type (ADT) is a data type whose implementation details are hidden from the user. Only the user interface is visible. This ability is provided by the class construct with its public and private parts. The public part is the user interface; the private part contains the implementation details. This helps the user maintain a high-level (abstract) view of the data type. He may concentrate on the functionality of the ADT without concerning himself with its implementation. In this lab we will work entirely with the Rational ADT, which is described in the text in Chapter 8, Sections 8.2 - 8.4. The authors present only a portion of the ADT. It is enough for us to see how things are done. Much of this lab will be devoted to adding functionality to the class. For example, we will add subtraction and division operators, a less-than operator, and various improvements of existing functions. Once we are satisfied with our Rational ADT, we will make it into a library. Key Concepts Abstract data type Rational class Operators Object integrity Library Before the Lab Read Chapter 8 in Cohoon and Davidson. Preliminary Turn the PC on, if necessary. Access the network. Copy the source files from the Lab 10 Source Files folder. Start up CodeWarrior. Open the LabWork project. The Rational Class Observation The program RationalTest.cpp will perform various calculations with Rational objects to see if the results are correct. If everything checks out, then we will begin to add new features to the class definition. Open the file RationalTest.cpp. Read the source code to see what the program is supposed to do. It will construct three rational numbers, 0/1, 3/1, and 3/4. Then it will read a rational number (an integer, followed by /, followed by another integer) and print it, to test the extraction and insertion operators. It then tests the arithmetic operators of addition and multiplication. Notice that it adds an int to a Rational and it multiplies an int by a Rational. Finally, it adds 1/2 and 1/3 and prints the result. Add the files rational.cpp and RationalTest.cpp to the LabWork project. Run RationalTest.cpp. Check the results. Are they correct? Run RationalTest.cpp again, entering an unreduced fraction, such as 3/6. Check the results. What do you notice? We should pay special attention to the add() function, since it is representative of the other Rational arithmetic functions. Locate the function add() in the file rational.cpp. Read the code in the function to see how it works. Notice that add() gets the numerator and denominator of the “invoking object” and names them a and b and then it gets the numerator and denominator of the parameter Rational object and names them c and d. These short names are used strictly for convenience. Now it must add a/b and c/d. Of course, we cannot write simply a/b + c/d, for that would invoke integer division. We must compute the numerator as an integer and the denominator as an integer using the rule a/b + c/d = (ad + bc)/bd. Furthermore, the computed numerator and denominator must be combined to form a Rational object. All of this is accomplished by the expression Rational(a * d + b * c, b * d) This expression computes a * d + b * c and b * d and then passes them to the Rational constructor, which returns a Rational object. Will the sum be reduced? That is, if the add() function adds 1/3 and 1/3, the above formula will compute 6 and 9 for the numerator and denominator. Will add() return 6/9 or will it return 2/3? It will return 2/3. Why? Read the code carefully and determine at what point the fraction is reduced. Experimentation Now we will add two new member functions to the Rational ADT: subtract() divide() Open the file rational.h. Add the subtract() and divide() prototypes to the Rational class definition in the file rational.h. Write the subtract() and divide() function definitions in the file rational.cpp. Use the addition and multiplication member functions as models. When dividing Rationals, be sure to check that the divisor is not zero. If it is zero, write an appropriate error message to cerr and return Rational(0). Now we will add two operators, operator-() and operator/(), which will do subtraction and division. These operators should call on the functions subtract() and divide(). Add the operator-() and operator/() prototypes in the file rational.h. Add the operator-() and operator/() function definitions in the file rational.cpp. Use the operators + and * as models. We will test these changes once we have modified RationalTest.cpp. Add code to RationalTest.cpp to do the tasks listed below. Add this code immediately after the lines r = 2 * r; cout << "Multiply by 2: " << r << endl; Add lines that will subtract OneHalf from r and print the result. Add lines that will divide r by Two and print the result. Now test your changes. Run RationalTest.cpp to verify that everything works. Check the results carefully. Everything should look fine. Observation There is a potential problem, which we will not attempt to fix. Change the lines that compute and print 1/2 + 1/3 so that they compute and print 1/1000000 + 1/1000000 (one millionth plus one millionth). Run the program again. We would expect the result to be 2/1000000, or 1/500000. What is the output? Why? The problem is that the add() function computed a denominator of 1000000*1000000, which is too large to store as an int. This is a problem of which the client programmer, who uses the Rational ADT, must be aware (since we are not going to do anything about it). The Constructor and the Extraction Operator Observation The constructor and the extraction operator currently have a minor shortcoming, which we will now demonstrate. Run RationalTest.cpp. When you enter a fraction, enter one with a negative denominator, such as 13/-4. What happens? Is this acceptable? Of course, it is mathematically possible to divide 13 by -4, but it is customary to write the fraction as -13/4, not 13/-4. Experimentation Modify the input() function so that if the user enters a negative denominator, the function will make it positive and reverse the sign of the numerator. (Of course, the user could have entered a negative numerator as well as a negative denominator.) Make a similar improvement in the Rational(int, int) constructor. Modify the definition of t in RationalTest.cpp from Rational t(3, 4); to Rational t(3, -4); in RationalTest.cpp. Run RationalTest.cpp. Enter the fraction 13/-4. Does it work better now? Run RationalTest.cpp again. This time, enter the fraction -13/-4. Does it still work? Open the input.cpp file and notice in the code that if the user wants to enter a whole number as a Rational, we should allow him to enter only the numerator. After reading the numerator, how will the program determine that a denominator will not follow? We must use in.get() to get the very next character following the numerator and then determine whether it is a '/'. If it is, then we will also read the denominator; if not, then we will set the denominator to 1 and then put the character back in the input stream using in.putback(). Comment out all of the code for the input() member function in rational.cpp. Copy and paste the input() function from input.cpp into rational.cpp. Now, member function calls to input() by a Rational object will use the new version. Run RationalTest.cpp to see if this improvement works. Enter the number 3. Run RationalTest.cpp again, entering 3/2 to be sure the old method still works. The Insertion Operator Experimentation As the output() function is currently written, it will always print the numerator and the denominator, even when the denominator is 1. For example, it printed The value of r is 0/1 The value of s is 3/1 when we ran RationalTest.cpp. We would like to modify output() so that it will print only the numerator when the denominator is 1. Modify the definition of output() so that when the denominator is 1, only the numerator is printed. Run RationalTest.cpp to test this improvement. Enter the fraction 3/1. The Less-Than Operator Experimentation We want to provide one more operator, the less-than operator (<). To do this, we will first write a lessThan() facilitator function. Then it will be a simple matter to write the operator<(). Since we know that the denominators of our fractions are positive, the inequality a/b < c/d is equivalent to the inequality ad < bc. In other words, an inequality involving rationals may be rewritten as an equivalent inequality involving integers. Thus, the lessThan() function should make its determination by evaluating the expression a * d < b * c. Write the prototype for the lessThan() member function in the Rational class definition in the file rational.h. Be sure that lessThan() is a public member function. Write the prototype for operator<() in the file rational.h. Be sure that operator<() is defined outside of the class definition, with the other operators. Write the definition of lessThan() in the file rational.cpp. Write the definition of operator<() in the file rational.cpp. In a similar way we could define the operators >, <=, >=, ==, and !=, but we won’t. We will test our implementation of less than. In the file RationalTest.cpp, Remove the comment delimiters /* and */ which enclose some code near the bottom of the program RationalTest.cpp. Remove only the delimiters, not the code. This will admit the lines cout << endl << r = Rational(2, s = Rational(3, if (r < s) cout << r else if (s < r) cout << s else cout << s "Test the less-than operator" << endl; 3); 5); << " is less than " << s << endl; << " is less than " << r << endl; << " equals " << r << endl; into the program, so that we can test the less-than operator. Run RationalTest.cpp. Did the less-than operator work? Application Open the MS Word document Lab 10 App.doc, found in the Lab 10 App subfolder, and follow the instructions. Summary The user interface of an ADT should provide the user with access to the objects in a way that feels natural. We saw that using + feels more natural than a function add() when adding rational numbers. It requires considerably more work on the part of the programmer. In fact, you may as well know that C++ is a language designed for large projects (programs of 1,000,000+ lines of code). For such programs, a good user interface will reward the programmer many times over. On the other hand, for relatively small programs, it can seem like more trouble than it is worth. We also touched on the topic of object integrity, i.e., guaranteeing that the object is stored in a valid form (in this case, the denominator must be positive). By reducing our fractions, we could further ensure that the fractions stored are valid. Of course, there is a limit to everything. To improve our Rational class further, we could add lines of code that would warn the user whenever a numerator or denominator exceeded INT_MAX, the largest legal int. We learned how to compile a library. Once the library is compiled, then it may be used in place of the “.cpp” file, which contains the source code. In this way a programmer may protect his proprietary rights to his source code, yet still share his programs with others. Also, a smart linker will extract from the library and put into the application code only those library functions that the application requires. This feature minimizes the size of the application code.