10. More on Variables and Data Types

In this chapter, we'll go into more detail about variable scope, initialization methods for objects, and data types.

The initialization of an object needs some special attention, which we'll give it here.

We talked briefly about the scope of instance variables and also static and local variables in Chapter 7, “More on Classes.” We'll talk more about static variables here and introduce the concept of global and external ones as well. In addition, certain directives can be given to the Objective-C compiler to more precisely control the scope of your instance variables. These are covered in this chapter as well.

An enumerated data type enables you to define the name for a data type that will only be used to store a specified list of values. The Objective-C language's typedef statement lets you assign your own name to a built-in or derived data type. Finally, in this chapter, we'll describe in more detail the precise steps the Objective-C compiler follows when converting data types in the evaluation of expressions.

Initializing Classes

You've seen the pattern before: You allocate a new instance of an object and then initialize it, using a familiar sequence, like so:

Fraction *myFract = [[Fraction alloc] init];

After these two methods are invoked, you typically assign some values to the new object, like this:

[myFract setTo: 1 over: 3];

The process of initializing an object followed by setting it to some initial values is often combined into a single method. For example, you could define an initWith:: method that initializes a fraction and sets its numerator and denominator to the two (unnamed) supplied arguments.

A class that contains many methods and instance variables in it commonly has several initialization methods as well. For example, the Foundation framework's NSArray class contains the following six initialization methods:

initWithArray:
initWithArray:copyItems:
initWithContentsOfFile:
initWithContentsOfURL:
initWithObjects:
initWithObjects:count:

An array might be allocated and then initialized with a sequence like this:

myArray = [[NSArray alloc] initWithArray: myOtherArray];

It's common practice for all the initializers in a class to begin with init.... As you can see, the NSArray's initializers follow that convention. You should adhere to the following two strategies when writing initializers.

If your class contains more than one initializer, one of them should be your designated initializer and all the other initialization methods should use it. Typically, that is your most complex initialization method (usually the one that takes the most arguments). By creating a designated initializer, your main initialization code will be centralized in a single method. Anyone subclassing your class can then override your designated initializer to ensure that new instances are properly initialized.

Ensure that any inherited instance variables get properly initialized. The easiest way to do that is to first invoke the parent's designated initialization method, which is most often init. After that, you can initialize your own instance variables.

Based on that discussion, your initialization method initWith:: for your Fraction class might look like this:

-(Fraction *) initWith: (int) n: (int) d
{
  self = [super init];

  if (self)
     [self setTo: n over: d];
  return self;
}

This method invokes the parent initializer first, which is Object's init method (you'll recall that that is Fraction's parent). You need to assign the result back to self because an initializer has the right to change or move the object in memory.

Following super's initialization (and its success as indicated by the return of a nonzero value) you use the setTo:over: method to set the numerator and denominator of your Fraction. As with other initialization methods, you are expected to return it yourself, which is what you do here.

Program 10.1 tests your new initWith:: initialization method.

Program 10.1.


#import "Fraction.h"

int main (int argc, char *argv[])
{
  Fraction *a, *b;

  a = [[Fraction alloc] initWith: 1: 3];
  b = [[Fraction alloc] initWith: 3: 7];

  [a print]; printf (" ");
  [b print]; printf (" ");
  [a free];
  [b free];

  return 0;
}


Program 10.1. Output


1/3
3/7


Initializing Classes: The initialize Method

When your program begins execution, it sends the initialize call method to all your classes. If you have a class and associated subclasses, the parent class gets the message first. This message is sent only once to each class, and it is guaranteed to be sent before any other messages are sent to the class. The purpose is for you to perform any class initialization at that point. For example, you might want to initialize some static variables associated with that class at that time.

Scope Revisited

You can influence the scope of the variables in your program in several ways. This can be done with instance variables as well as with normal variables defined either outside or inside functions. In the discussion that follows, we use the term module to refer to any number of method or function definitions contained within a single source file.

Directives for Controlling Instance Variable Scope

You know by now that instance variables have scope that is limited to the instance methods defined for the class. So, any instance method can access its instance variables directly by name, without having to do anything special.

And you also know that instance variables are inherited by a subclass. Inherited instance variables can also be accessed directly by name from within any method defined in that subclass. Once again, this is without having to do anything special.

You can put three directives in front of your instance variables when they are declared in the interface section to more precisely control their scope; these are

@protectedThe instance variables that follow can be directly accessed by methods defined in the class and by any subclasses. This is the default case.

@privateThe instance variables that follow can be directly accessed by methods defined in the class but not by any subclasses.

@publicThe instance variables that follow can be directly accessed by methods defined in the class and by any other classes or modules.

If you wanted to define a class called Printer that kept two instance variables called pageCount and tonerLevel private and accessible only by methods in the Printer class, you might use an interface section that looked like this:

@interface Printer: Object
{
@private
  int  pageCount;
  int  tonerLevel;
@protected
  // other instance variables
}
...
@end

Anyone subclassing Printer would be incapable of accessing these two instance variables because they were made private.

These special directives act like “switches”; all variables that appear after one of these directives (until the right curly brace that marks the end of the variable declarations) has the specified scope unless another directive is used. In the previous example, the @protected directive ensures that instance variables that follow up to the } will be accessible by subclasses as well as by the Printer class methods.

The @public directive makes instance variables accessible by other methods or functions through the use of the pointer operator (->), which is covered in Chapter 13, “Underlying C Language Features.” Making an instance variable public is not considered good programming practice because it defeats the concept of data encapsulation (that is, a class hiding its instance variables).

External Variables: extern and static

If you write the statement

int gMoveNumber = 0;

at the beginning of your program—outside any method, class definition, or function—its value can be referenced from anywhere in that module. In such a case, we say that gMoveNumber is defined as a global variable. By convention, a lowercase g is commonly used as the first letter of a global variable to indicate its scope to the program's reader.

Actually, this very same definition of the variable gMoveNumber also makes its value accessible from other files. Specifically, the preceding statement defines the variable gMoveNumber not just as a global variable, but in fact as an external global variable.

An external variable is one whose value can be accessed and changed by any other methods or functions. Inside the module that wants to access the external variable, the variable is declared in the normal fashion and the keyword extern is placed before the declaration. This signals to the system that a globally defined variable from another file is to be accessed. The following is an example of how to declare the variable gMoveNumber as an external variable:

extern int gMoveNumber;

The value of gMoveNumber can now be accessed and modified by the module in which the preceding declaration appeared. Other modules can also access the value of gMoveNumber by using a similar extern declaration in the file.

Here is an important rule you must follow when working with external variables: The variable must be defined in some place among your source files. This is done by declaring the variable outside any method or function, not preceded by the keyword extern, like so:

int gMoveNumber;

Here, an initial value can be optionally assigned to the variable, as was shown previously.

The second way to define an external variable is to declare the variable outside any function, placing the keyword extern in front of the declaration, and explicitly assigning an initial value to it, like this:

extern int gMoveNumber = 0;

This, however, is not the preferred way to do things, and the compiler will give you a warning to the effect that you've declared the variable extern and assigned it a value at the same time. That's because using the word extern makes it a declaration for the variable and not a definition. Remember, a declaration doesn't cause storage for a variable to be allocated, but a definition does. So, the previous example violates this rule by forcing a declaration to be treated as a definition (by assigning it an initial value).

When dealing with external variables, a variable can be declared as extern in many places but can be defined only once.

Let's take a look at a small program example to illustrate the use of external variables. Suppose we have defined a class called Foo and we type the following code into a file called main.m:

#import "Foo.h"
int gGlobalVar = 5;

int main (int argc, char *argc[])
{
   Foo *myFoo = [[Foo alloc] init];
   printf ("%i ", gGlobalVar);

   [myFoo setgGlobalVar: 100];

   printf ("%i ", gGlobalVar);
   [myFoo free];
   return 0;
}

The definition of the global variable gGlobalVar in the previous program makes its value accessible by any method (or function) that uses an appropriate extern declaration. Suppose your Foo method setgGlobalVar: looks like this:

-(void) setgGlobalVar: (int) val
{
  extern int gGlobalVar;
  gGlobalVar = val;
}

This program would produce the following output at the terminal:

100

This would verify that the method setgGlobalVar: is capable of accessing and changing the value of the external variable gGlobalVar.

If many methods needed to access the value of gGlobalVar, making the extern declaration just once at the front of the file would be easier. However, if only one method or a small number of methods needed to access this variable, there would be something to be said for making separate extern declarations in each such method; it would make the program more organized and would isolate the use of the particular variable to those functions that actually used it. Note that if the variable is defined inside the file containing the code that accesses the variable, the individual extern declarations are not required.

Static Versus Extern Variables

The example just shown goes against the notion of data encapsulation and good object-oriented programming techniques. However, you might need to work with variables whose values are shared across different method invocations. Even though it might not make sense to make gGlobalVar an instance variable in the Foo class, a better approach than that shown might be to “hide” it within the Foo class by restricting its access to setter and getter methods defined for that class.

You now know that any variable defined outside a method is not only a global variable, but an external one as well. Many situations arise in which you want to define a variable to be global but not external. In other words, you want to define a global variable to be local to a particular module (file). It would make sense to want to define a variable this way if no methods other than those contained inside a particular class definition needed access to the particular variable. This can be accomplished by defining the variable to be static inside the file that contains the implementation for the particular class.

The statement

static int gGlobalVar = 0;

if made outside any method (or function), makes the value of gGlobalVar accessible from any subsequent point in the file in which the definition appears but not from methods or functions contained in other files.

You'll recall that class methods do not have access to instance variables (you might want to think about why that's the case again). However, you might want a class method to be capable of setting and accessing variables. A simple example would be a class allocator method that wanted to keep track of the number of objects it had allocated. The way to accomplish this task would be to set up a static variable inside the implementation file for the class. The allocation method could then access this variable directly because it would not be an instance variable. The users of the class do not need to know about this variable. Because it's defined as a static variable in the implementation file, its scope would be restricted to that file. So, users wouldn't have direct access to it and the concept of data encapsulation would not be violated. A method can be written to retrieve the value of this variable if access is needed from outside the class.

Program 10.2 extends the Fraction class definition with the addition of two new methods. The allocF class method allocates a new Fraction and keeps track of how many Fractions it has allocated, whereas the count method returns that count. Note that this latter method is also a class method. It could have been implemented as an instance method as well, but it makes more sense to ask the class how many instances it has allocated rather than sending the message to a particular instance of the class.

Here are the declarations for the two new class methods to be added to the Fraction.h header file:

+(Fraction *) allocF;
+(int)    count;

You might have noticed that the inherited alloc method wasn't overridden here; that's risky business. Instead, you defined your own allocator method. Your method will take advantage of the inherited alloc method. Here's the code to be placed into your Fraction.m implementation file:

static int gCounter;

@implementation Fraction;

+(Fraction *) allocF
{
   extern int gCounter;
   ++gCounter;

   return [Fraction alloc];
}

+(int) count
{
   extern int gCounter;

   return gCounter;
}
// other methods from Fraction class go here
  ...
@end

The static declaration of counter makes it accessible to any method defined in the implementation section yet does not make it accessible from outside the file. The allocF method simply increments the gCounter variable and then uses the alloc method to create a new Fraction, returning the result. The count method simply returns the value of the counter, thus isolating its direct access from the user.

Recall that the extern declarations are not required in the two methods because the gCounter variable is defined within the file. It simply helps the reader of the method understand that a variable defined outside the method is being accessed. The g prefix for the variable name also serves the same purpose for the reader; for that reason, most programmers typically do not include the extern declarations.

Program 10.2 tests the new methods.

Program 10.2.


#import "Fraction.h"

int main (int argc, char *argv[])
{
  Fraction *a, *b, *c;

  printf ("Fractions allocated: %i ", [Fraction count]);

  a = [Fraction allocF];
  b = [[Fraction allocF] init];
  c = [Fraction allocF];

  printf ("Fractions allocated: %i ", [Fraction count]);
  [a free];
  [b free];
  [c free];

  return 0;
}


Program 10.2. Output


Fractions allocated: 0
Fractions allocated: 3


When the program begins execution, the value of counter is automatically set to 0 (you'll recall that you could override the inherited class initialize method if you wanted to perform any special initialization of the class as a whole, such as set the value of other static variables to some nonzero values). After allocating three Fractions using the allocF method, the count method retrieves the counter variable, which is correctly set to 3. You could also add a setter method to the class if you wanted to reset the counter or set it to a particular value. You don't need that for this application, though.

Storage Class Specifiers

You've already encountered storage class specifiers you can place in front of variable names, such as extern and static. Here are some more.

auto

This keyword is used to declare an automatic local variable, as opposed to a static one. It is the default for a variable declared inside a function or method, and you'll never see anyone using it. Here's an example:

auto int index;

This declares index to be an automatic local variable, meaning it will automatically be allocated when the block (which can be a curly-braced sequence of statements, a method, or a function) is entered and automatically deallocated when the block is exited. Because this is the default inside a block, the statement

int index;

is equivalent to

auto int index;

Unlike static variables, which have default initial values of 0, automatic variables have no default initial values; their values are undefined unless you explicitly assign them values.

register

If a local variable is used heavily, you can request that the value of that variable be stored in one of the machine's registers whenever the method or function is executed. This is done by prefixing the declaration of the variable with the keyword register, like so:

register int  index;

Both local variables and parameters can be declared as register variables. The types of variables that can be assigned to registers vary among machines. The basic data types can usually be assigned to registers, as well as objects.

Even if you declare a variable as a register variable, it is still not guaranteed that it will in fact be assigned to a register. In fact, the compiler is free to ignore the presence of this keyword. This keyword was more important years ago when compilers needed hints in producing optimized code. As such, it is not used much any more.

Additionally, a restrict modifier is available that you can use with pointer variables to help optimization (such as the register modifier). Consult Appendix B, “Objective-C Language Summary,” for more information.

const

The compiler enables you to associate the const attribute to variables whose values will not be changed by the program. That is, this tells the compiler that the specified variables have a const ant value throughout the program's execution. If you try to assign a value to a const variable after initializing it, or try to increment or decrement it, the compiler issues a warning message. As an example of the const attribute, the line

const double pi = 3.141592654;

declares the const variables pi. This tells the compiler that this variable will not be modified by the program. Of course, because the value of a const variable cannot be subsequently modified, you must initialize it when it is defined.

Defining a variable as a const variable aids in the self-documentation process and tells the reader of the program that the variable's value will not be changed by the program.

volatile

This is sort of the inverse to const. It tells the compiler explicitly that the specified variable will change its value. It's included in the language to prevent the compiler from optimizing away seemingly redundant assignments to a variable or repeated examination of a variable without its value seemingly changing. A good example to consider is an I/O port and involves an understanding of pointers (see Chapter 13).

Let's say you have the address of an output port stored in a variable in your program called outPort. If you wanted to write two characters to the port—let's say an O followed by an N—you might write the following code:

*outPort = 'O';
*outPort = 'N';

This first line says to store the character O at the memory address specified by outPort. The second says to then store the character N at the same location. A smart compiler might notice two successive assignments to the same location and, because outPort isn't being modified in between, simply remove the first assignment from the program. To prevent this from happening, you declare outPort to be a volatile variable, like so:

volatile char *outPort;

Enumerated Data Types

The Objective-C language enables you to specify a range of values that can be assigned to a variable. An enumerated data type definition is initiated by the keyword enum. Immediately following this keyword is the name of the enumerated data type, followed by a list of identifiers (enclosed in a set of curly braces) that define the permissible values that can be assigned to the type. For example, the statement

enum flag { false, true };

defines a data type flag. In theory, this data type can be assigned the values true and false inside the program, and no other values. Unfortunately, the Objective-C compiler does not generate warning messages if this rule is violated.

To declare a variable to be of type enum flag, you again use the keyword enum, followed by the enumerated type name, followed by the variable list. So the statement

enum flag endOfData, matchFound;

defines the two variables endOfData and matchFound to be of type flag. The only values (in theory, that is) that can be assigned to these variables are the names true and false. So, statements such as

endOfData = true;

and

if ( matchFound == false )
  ...

are valid.

If you want to have a specific integer value associated with an enumeration identifier, the integer can be assigned to the identifier when the data type is defined. Enumeration identifiers that subsequently appear in the list are assigned sequential integer values beginning with the specified integer value plus one.

In the definition

enum direction { up, down, left = 10, right };

an enumerated data type, direction, is defined with the values up, down, left, and right. The compiler assigns the value 0 to up because it appears first in the list, assigns 1 to down because it appears next, assigns 10 to left because it is explicitly assigned this value, and assigns 11 to right because it is the incremented value of the preceding enum in the list.

Enumeration identifiers can share the same value. For example, in

enum boolean { no = 0, false = 0, yes = 1, true = 1 };

assigning either the value no or false to an enum boolean variable assigns it the value 0; assigning either yes or true assigns it the value 1.

As another example of an enumerated data type definition, the following defines the type enum month, with permissible values that can be assigned to a variable of this type being the names of the months of the year:

enum month { january = 1, february, march, april, may, june, july,
        august, september, october, november, december };

The Objective-C compiler actually treats enumeration identifiers as integer constants. If your program contains these two lines

enum month thisMonth;
  ...
thisMonth = february;

the value 2 would be assigned to thisMonth (and not the name february).

Program 10.3 shows a simple program using enumerated data types. The program reads a month number and then enters a switch statement to see which month was entered. Recall that enumeration values are treated as integer constants by the compiler, so they're valid case values. The variable days is assigned the number of days in the specified month, and its value is displayed after the switch is exited. A special test is included to see whether the month is February.

Program 10.3.


// print the number of days in a month
int main (int argc, char *argv[])
{
  enum month { january = 1, february, march, april, may, june,
               july, august, september, october, november,
               december };
  enum month amonth;
  int     days;

  printf ("Enter month number: ");
  scanf ("%i", &amonth);

  switch (amonth) {
    case january:
    case march:
    case may:
    case july:
    case august:
    case october:
    case december:
                days = 31;
                break;
    case april:
    case june:
    case september:
    case november:
                days = 30;
                break;
    case february:
                days = 28;
                break;
    default:
                printf ("bad month number ");
                days = 0;
                break;
  }

  if ( days != 0 )
    printf ("Number of days is %i ", days);

  if ( amonth == february )
     printf ("...or 29 if it's a leap year ");
     return 0;
}


Program 10.3. Output


Enter month number: 5
Number of days is 31


Program 10.3. Output (Rerun)


Enter month number: 2
Number of days is 28
...or 29 if it's a leap year


You can explicitly assign an integer value to an enumerated data type variable; this should be done using the type cast operator. Therefore, if monthValue were an integer variable that had the value 6, for example, the expression

lastMonth = (enum month) (monthValue - 1);

would be permissible. If you don't use the type cast operator, the compiler (unfortunately) won't complain about it.

When using programs with enumerated data types, try not to rely on the fact that the enumerated values are treated as integers. Instead, try to treat them as distinct data types. The enumerated data type gives you a way to associate a symbolic name with an integer number. If you subsequently need to change the value of that number, you must change it only in the place where the enumeration is defined. If you make assumptions based on the actual value of the enumerated data type, you defeat this benefit of using an enumeration.

Some variations are permitted when defining an enumerated data type: The name of the data type can be omitted, and variables can be declared to be of the particular enumerated data type when the type is defined. As an example showing both of these options, the statement

enum { east, west, south, north } direction;

defines an (unnamed) enumerated data type with values east, west, south, or north and declares a variable (direction) to be of that type.

Defining an enumerated data type within a block limits the scope of that definition to the block. On the other hand, defining an enumerated data type at the beginning of the program, outside any block, makes the definition global to the file.

When defining an enumerated data type, you must make certain that the enumeration identifiers are unique with respect to other variable names and enumeration identifiers defined within the same scope.

The typedef Statement

Objective-C provides a capability that enables the programmer to assign an alternative name to a data type. This is done with a statement known as typedef. The statement

typedef int Counter;

defines the name Counter to be equivalent to the Objective-C data type int. Variables can subsequently be declared to be of type Counter, as in the following statement:

Counter  j, n;

The Objective-C compiler actually treats the declaration of the variables j and n, shown previously, as normal integer variables. The main advantage of the use of the typedef in this case is in the added readability it lends to the definition of the variables. It is clear from the definition of j and n what the intended purpose of these variables is in the program. Declaring them to be of type int in the traditional fashion would not have made the intended use of these variables at all clear.

The following typedef defines a type named NumberObject to be a Number object:

typedef Number *NumberObject;

Variables subsequently declared to be of type NumberObject, as in

NumberObject myValue1, myValue2, myResult;

are treated as if they were declared in the normal way in your program, like so:

Number *myValue1, *myValue2, *myResult;

To define a new type name with typedef, follow this procedure:

  1. Write the statement as if a variable of the desired type were being declared.
  2. Where the name of the declared variable would normally appear, substitute the new type name.
  3. In front of everything, place the keyword typedef.

As an example of this procedure, to define a type called Direction to be an enumerated data type that consists of the directions east, west, north, and south, write out the enumerated type definition and substitute the name Direction where the variable name would normally appear. Before everything, place the keyword typedef:

typedef enum { east, west, south, north } Direction;

With this typedef in place, you can subsequently declare variables to be of type Direction, as in the following:

Direction step1, step2;

The Foundation framework, which is covered in Part II, “The Foundation Framework,” has the following typedef definition for NSComparisonResult in one of its header files:

typedef enum _NSComparisonResult {
      NSOrderedAscending = -1, NSOrderedSame, NSOrderedDescending
} NSComparisonResult;

Some of the methods in the Foundation framework that perform comparisons return a value of this type. For example, Foundation's string comparison method, called compare:, returns a value of type NSComparisonResult after comparing two strings that are NSString objects. The method is declared like this:

-(NSComparisonResult) compare: (NSString *) string;

To test whether two NSString objects called userName and savedName are equal, you might include a line like this in your program:

if ([userName compare: savedName] == NSOrderedSame)
  // The names match
    ...

This actually tests whether the result from the compare: method is zero.

Data Type Conversions

Chapter 4, “Data Types and Expressions,” briefly addressed the fact that sometimes conversions are implicitly made by the system when expressions are evaluated. The case you examined was with the data types float and int. You saw how an operation that involves a float and an int was carried out as a floating-point operation, the integer data item being automatically converted to floating point.

You have also seen how the type cast operator can be used to explicitly dictate a conversion. So, given that total and n are both integer variables

average = (float) total / n;

the value of the variable total is converted to type float before the operation is performed, thereby guaranteeing that the division will be carried out as a floating-point operation.

The Objective-C compiler adheres to very strict rules when it comes to evaluating expressions that consist of different data types.

The following summarizes the order in which conversions take place in the evaluation of two operands in an expression:

  1. If either operand is of type long double, the other is converted to long double, and that is the type of the result.
  2. If either operand is of type double, the other is converted to double, and that is the type of the result.
  3. If either operand is of type float, the other is converted to float, and that is the type of the result.
  4. If either operand is of type _Bool, char, short int, bit field,1 or of an enumerated data type, it is converted to int.
  5. If either operand is of type long long int, the other is converted to long long int, and that is the type of the result.
  6. If either operand is of type long int, the other is converted to long int, and that is the type of the result.
  7. If this step is reached, both operands are of type int, and that is the type of the result.

This is actually a simplified version of the steps involved in converting operands in an expression. The rules get more complicated when unsigned operands are involved. For the complete set of rules, see Appendix B, “Objective-C Language Summary.”

Realize from this series of steps that, whenever you reach a step that says “that is the type of the result,” you're done with the conversion process.

As an example of how to follow these steps, let's see how the following expression would be evaluated, where f is defined to be a float, i an int, l a long int, and s a short int variable:

f * i + l / s

Consider first the multiplication of f by i, which is the multiplication of a float by an int. From step 3, you know that, because f is of type float, the other operand (i) will also be converted to type float and that will be the type of the result of the multiplication.

Next, the division of l by s occurs, which is the division of a long int by a short int. Step 4 tells you that the short int will be promoted to an int. Continuing, step 6 shows that, because one of the operands (l) is a long int, the other operand will be converted to a long int, which will also be the type of the result. This division will therefore produce a value of type long int, with any fractional part resulting from the division truncated.

Finally, step 3 indicates that, if one of the operands in an expression is of type float (as is the result of multiplying f * i), the other operand will be converted to type float, which will be the type of the result. Therefore, after the division of l by s has been performed, the result of the operation will be converted to type float and then added into the product of f and i. The final result of the preceding expression will therefore be a value of type float.

Remember, the type cast operator can always be used to explicitly force conversions and thereby control the way in which a particular expression is evaluated.

Thus, if you didn't want the result of dividing l by s to be truncated in the preceding expression evaluation, you could have type cast one of the operands to type float, thereby forcing the evaluation to be performed as a floating-point division, like so:

f * i + (float) l / s

In this expression, l would be converted to float before the division operation was performed because the type cast operator has higher precedence than the division operator. Because one of the operands of the division would then be of type float, the other (s) would be automatically converted to type float, and that would be the type of the result.

Sign Extension

Whenever a signed int or signed short int is converted into an integer of a larger size, the sign is extended to the left when the conversion is performed. This ensures that a short int having a value of -5, for example, will also have the value -5 when converted to a long int. Whenever an unsigned integer is converted to an integer of a larger size, no sign extension occurs, as you would expect.

On some machines (such as a Mac G4/G5 and Pentium processors) characters are treated as signed quantities. This means that when a character is converted to an integer, sign extension occurs. As long as characters are used from the standard ASCII character set, this never poses a problem. However, if a character value is used that is not part of the standard character set, its sign can be extended when converted to an integer. For example, on a Mac, the character constant '377' is converted to the value -1 because its value is negative when treated as a signed 8-bit quantity.

Recall that the Objective-C language permits character variables to be declared unsigned, thus avoiding this potential problem. That is, an unsigned char variable never has its sign extended when converted to an integer; its value always is greater than or equal to zero. For the typical 8-bit character, a signed character variable therefore has the range of values from –128 to +127, inclusive. An unsigned character variable can range in value from 0 to 255, inclusive.

If you want to force sign extension on your character variables, you can declare such variables to be of type signed char. This ensures that sign extension occurs when the character value is converted to an integer, even on machines that don't do so by default.

In Chapter 15, “Numbers, Strings, and Collections,” you'll learn about dealing with multibyte Unicode characters. This is the preferred way to deal with strings that can contain characters from character sets containing millions of characters.

Exercises

  1. Using the Rectangle class from Chapter 8, “Inheritance,” add an initializer method according to the following declaration:

    -(Rectangle *) initWithWidth: (int) w: andHeight: (int) h;

  2. Given that you label the method developed in exercise 1 the designated initializer for the Rectangle class, and based on the Square and Rectangle class definitions from Chapter 8, add an initializer method to the Square class according to the following declaration:

    -(Square *) initWithSide: (int) side;

  3. Add a counter to the Fraction class's add: method to count the number of times it is invoked. How can you retrieve the value of the counter?
  4. Using typedef and enumerated data types, define a type called Day with the possible values Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, and Saturday.
  5. Using typedef, define a type called FractionObj that enables you to write the statements such as the following:

    FractionObj f1 = [[Fraction alloc] init],
                f2 = [[Fraction alloc] init];

  6. Based on the following definitions

    float     f = 1.00;
    short int i = 100;
    long int  l = 500L;
    double    d = 15.00;

    and the seven steps outlined in this chapter for the conversion of operands in expressions, determine the type and value of the following expressions:

    f + i
    l / d
    i / l + f
    l * i
    f / 2
    i / (d + f)
    l / (i * 2.0)
    l + i / (double) l

  7. Write a program to ascertain whether sign extension is performed on signed char variables on your machine.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.61.179