Let’s dive right into this lesson by extending our Fraction class and covering some miscellaneous OOP concepts.
The last post, which described methods in detail, mentioned a method called setNumerator:overDenominator:
. As you might have guessed, we are going to implement this functionality into our Fraction
class, and see how we use a method with multiple arguments.
Open up Fraction.h
from the project we’ve been working on; alternatively, you can download the files by clicking here. Add the following line to the list of methods (it doesn’t matter exactly where you place it, as long as it’s between the @property
s and the final @end
):
- (void)setNumerator:(NSInteger)num overDenominator:(NSInteger)denom;
In Fraction.m
, add the following implementation for the method:
- (void)setNumerator:(NSInteger)num overDenominator:(NSInteger)denom {
self.numerator = num;
self.denominator = denom;
}
Note that the names of the arguments in this method are num and denom are not the same as the instance variables numerator and denominator. This is because every instance method has access to the class’s instance variables (remember that a class is just an object that gets created at runtime—hypothetically, it would be weird if a car did not have access to its current speed (speedometer) or fuel level). Because of this, if your argument names are the same name as an instance variable, and you try to reference the argument or the instance variable, the compiler will get confused. At best, you’ll have runtime issues; at worst, the program will refuse to compile.
Update the main
function (main.m) to have the following code:
#import "Fraction.h"
int main (int argc, const char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Fraction *myFraction = [[Fraction alloc] init];
// Set myFraction to value of 2/5
/* [myFraction setNumerator: 2];
[myFraction setDenominator: 5]; */
/* myFraction.numerator = 2;
myFraction.denominator = 5; */
[myFraction setNumerator:2 overDenominator:5];
// Display the value of myFraction
NSLog(@"myFraction has a value of: ");
[myFraction display];
[myFraction release];
[pool drain];
return 0;
}
Just note that the main()
function is where any program starts executing. Here, we are placing our actual program (where something is done) inside the main()
routine; when we begin building iPhone apps, we will almost never modify main()
.
Build and run your program. The output should be exactly as expected:
myFraction has a value of:
2/5
Here, we will implement an instance method to add two fractions. As an exercise to you, try to implement subtraction, multiplication, and division! The code will be available for download at the end of this post, but try to do it on your own.
(a/b) + (c/d) = (ad + bc)/(bd)
Our method looks like this:
- (void)add:(Fraction *)newFraction;
Note that we are passing in another Fraction
object (technically, the asterisk symbolizes that we are passing in a pointer, or reference to a Fraction
object). Also note that although we are only passing in one value to be added, the other (implied) value in the addition is the instance of Fraction that we’re calling this method on. Every method has access to its own instance variables, and as we’ll discover later in this post, every method can reference self
.
The implementation of the method is as follows:
- (void)add:(Fraction *)newFraction {
// a/b + c/d = ((a * d) + (b * c)) / (b * d)
self.numerator = self.numerator * newFraction.denominator + self.denominator * newFraction.numerator;
self.denominator = self.denominator * newFraction.denominator;
}
The parentheses were not strictly necessary, but made it easier to read and understand.
Let’s test this new method:
#import "Fraction.h"
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Fraction *aFraction = [[Fraction alloc] init];
Fraction *bFraction = [[Fraction alloc] init];
[aFraction setNumerator:1 overDenominator:2];
[bFraction setNumerator:1 overDenominator:3];
[aFraction display]; NSLog(@" + "); [bFraction display]; NSLog(@" = ");
[aFraction add:bFraction];
[aFraction display];
[aFraction release];
[bFraction release];
[pool drain];
return 0;
}
The result:
1/2
+
1/3
=
5/6
This is exactly correct.
If we try our program with different numbers (say, 2/4 and 1/3, which do happen to have the same values), we get a result of 10/12. Our current add method simply adds, without reducing the value. Let’s implement a method that reduces any Fraction:
- (void)reduce {
int u = self.numerator;
int v = self.denominator;
int temp = 0;
// Euclid's procedure to find GCD (Greatest Common Denominator)
// Don't worry about how this works, exactly.
while (v != 0) {
temp = u % v;
u = v;
v = temp;
}
self.numerator /= u;
self.denominator /= u;
}
This method declares three integer values called u
, v
, and temp
. These are local variables- they only exist during the execution of the reduce method, they can only be accessed from the same method, and get cleared once the method is done. In more formal parlance, these variables’ scope is limited to this method. These variables must be initialized (given a value) before they are used; every time the method is invoked (called), these values are set to the default initial values.
Incidentally, this scope also applies to variables defined within loops and if() blocks—those variables would only be accessible within the same block. As a general rule of thumb, a variable is always and only accessible in the same block as the declaration itself. A block is delineated by curly braces ({ }
).
The same scope is applied to method arguments. The arguments contain a copy of the value that you pass in, not the original. For primitive types, such as ints or floats, this means that you cannot modify the original value that you passed in, only the copy. Whenever you pass in an object (more precisely, an object pointer—more on that in the next Extension), you can, in fact, modify the original object, because a copy is made of the memory address, not of the object itself. Don’t worry if that last line was confusing; it’ll be quickly cleared up.
Let’s test our reduce method:
#import "Fraction.h"
#import <Foundation/Foundation.h>
int main (int argc, const char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Fraction *aFraction = [[Fraction alloc] init];
Fraction *bFraction = [[Fraction alloc] init];
[aFraction setNumerator:2 overDenominator:4];
[bFraction setNumerator:1 overDenominator:3];
[aFraction display]; NSLog(@" + "); [bFraction display]; NSLog(@" = ");
[aFraction add:bFraction];
[aFraction reduce];
[aFraction display];
[aFraction release];
[bFraction release];
[pool drain];
return 0;
}
The resulting fraction is the expected 5/6, not 10/12.
A static variable is one that retains its value through multiple method calls to the same variable. Unlike a regular variable declared in a method, which gets cleared after the method is finished, a static variable does not get cleared. A static variable is automatically set to 0 at creation:
static int counter;
You do not need to set the variable to 0; that is already done.
Although static
variables retain their value through successive method calls, they are still only accessible in that method. Some static variables are placed outside any method, sometimes even before the @implementation
line, so that they can accessed in any method.
If a variable is a local static
variable, it could be used anytime that method in which it was defined is called. If it were an instance variable, it would be able to be changed from any method. This makes it very similar to a static variable outside any method. The difference at this point is a matter of conventions. If you need to keep track of a value which does not necessary seem to “belong” to an object (for instance, the number of potholes a Car
object has metaphorically gone over probably won’t be an instance variable of Car
(because it is not a property of Cars
), but could still be a static variable).
The self keyword is used to access the receiver of the method—the same object that sent the message in the first place. For instance, in our add: method, we could have reduced the value directly in the method:
- (void)add:(Fraction *)newFraction {
// a/b + c/d = ((a * d) + (b * c)) / (b * d)
self.numerator = self.numerator * newFraction.denominator + self.denominator * newFraction.numerator;
self.denominator = self.denominator * newFraction.denominator;
[self reduce];
}
This invokes the reduce method on the same object that sent the add: method (in this case, aFraction). We can remove the reduce line from the main() routine.
We can also write a class method that takes two Fractions, adds them, and returns a new Fraction. Look through the following code, and see if it makes sense.
Fraction.h
+ (Fraction *)addFraction:(Fraction *)frac1 toFraction:(Fraction *)frac2;
Fraction.m
+ (Fraction *)addFraction:(Fraction *)frac1 toFraction:(Fraction *)frac2 {
Fraction *result = [[[Fraction alloc] init] autorelease]; // Store result of addition
// Autorelease is memory management—
// Don't worry about it now.
NSInteger resultNum = frac1.numerator * frac2.denominator + frac1.denominator * frac2.numerator;
NSInteger resultDenom = frac1.denominator * frac2.denominator;
[result setNumerator:resultNum overDenominator:resultDenom];
[result reduce];
return result;
}
main.m
// Reset fractions
[aFraction setNumerator:2 overDenominator:4];
[bFraction setNumerator:1 overDenominator:3];
NSLog(@"Using class method:");
Fraction *classAddition = [Fraction addFraction:aFraction toFraction:bFraction];
[classAddition display];
The result is the same: 5/6.
In this version, in addition to adding everything mentioned above, the Fraction class has been cleaned up. Our old setNumerator:
and setDenominator:
methods have been removed; the synthesized properties fill in their role. Download here. For bonus points, try implementing the commented out method (making a Fraction out of a decimal value). Good luck!
This post is part of the Learn Objective-C in 24 Days course.