Chapter 8: Creating and Using Enumerations

The real world is complicated – far more complicated than just whole numbers, numbers with fractions, Boolean values, and characters. In order to model it, C provides various mechanisms for custom and complex data types. For the next eight chapters, we are going to explore various ways that our intrinsic data types can be extended and combined to more closely match the real world.

The first of these extensible data types is enumerated types. These are named values that are grouped because of some conceptual relationship we want to give them; we don't really care about their values – we differentiate each item in the group by its name. There is a value corresponding to each name, but usually, although not always, that value is irrelevant to us; the significance lies in its unique name within the group of enumerated items. However, a specific value for each item in the group can be specified by us otherwise it will be automatically assigned by the compiler. We can then use the names instead of values – the compiler replaces the name with their value – or we can convert the name to its value. The switch statement is particularly handy when dealing with enumerated items, also simply called enumerations. Finally, we can name values that are loosely related or not at all related by using a special kind of enumeration; doing so allows us to use these named values in places where const variables cannot be used.

By using enumerated types, we can express conceptual relationships between different values via their enumerated names. Enumeration also allows us to give a name to a meaningful value; we can then use that name instead of the value. Both of these make the intent of our code clear and, ideally, less error-prone.

The following topics will be covered in this chapter:

  • Understanding how enumerations limit values to a specified range
  • Declaring various enumerations
  • Writing a function to use the enumerations declared
  • Using the switch statement to select an enumeration to perform actions specific to it
  • Using enumerated item values as literal integer constants

Technical requirements

As detailed in the Technical requirements section of Chapter 1, Running Hello, World!, continue to use the tools you have chosen.

The source code for this chapter can be found at https://github.com/PacktPublishing/Learn-C-Programming-Second-Edition/tree/main/Chapter08.

Introducing enumerations

There are times when we want a program or function variable to take an integer value that is from a set of related values. For convenience, and to make the relationship between each value clear, each value in the set of values is given a name. We can think of this set as a grouping of conceptually related values.

Let's say we want a variable to represent the suits of a deck of cards. Naturally, we know each suit by its name – spades, hearts, clubs, and diamonds. But C doesn't know about card names or card suits. If we wanted to represent each of these suits with a value, we could pick any value for each, say, 4 for spades, 3 for hearts, 2 for diamonds, and 1 for clubs. Our program, using this simple scheme, might look as follows:

int card;
...
card = 3; // Heart.

But we would have to do the work of remembering the conceptual relationship. We would have to do the work remembering which value corresponds to which suit. This is an error-prone way of solving this problem.

We could, however, improve that solution by either using the preprocessor or by using constants, as follows:

#define SPADE   4
#define HEART   3
#define DIAMOND 2
#define CLUB    1
int card = heart;

This gives each value a name but relies upon the preprocessor. It also does not really convey any conceptual relationship between them.

Or, even better, we could do it by using constants for each value, as follows:

const int kSpade   = 4;
const int kHeart   = 3;
const int kDiamond = 2;
const int kClub    = 1;
int card = heart;

Again, this gives each value a name but, as we will see in Chapter 11, Working with Arrays, does not allow us to use these names as we might like.

While each of these values accurately represents the four suits, they are still unrelated. Rather, they are only related by our intention. The data type of the card variable is an integer; this tells us nothing about the valid values for card. Furthermore, card could be assigned with a value that is not one of the four and therefore would make no sense.

C provides a way to group values by name so that their conceptual relationship is clear.

Defining enumerations

The enumeration type allows us to specify a set, or group, of values a variable is intended to have. These values are constant and may not be changed. Note, however, that a variable of an enumerated type may still be given a value not in the list of enumerated items. The advantage of the enumeration is that we make our intention of the relationship between the enumerated items explicit.

There are two kinds of enumerations, named and unnamed, or anonymous. For each kind of enumeration, the item specified by it is always named. The syntax for defining a named enumerated type is as follows:

enum name { enumerationItem1,enumerationItem2, … ,enumerationItemN };

The type consists of two words, enum, and name. The group of enumerated items is contained within { and }. Each named item in the group of enumerated items is separated by , and the definition is concluded with ;. Each enumerated item represents a constant integer value. So, the definition of our enumerated type named suit would be as follows:

enum suit { eSpade , eHeart , eDiamond , eClub };

Or, we might want to make each item appear on its own line, as follows:

enum suit { 
  eSpade , 
  eHeart , 
  eDiamond , 
  eClub
 };

We have just created a new data type – enum suit. We will see later, in Chapter 10, Creating Custom Data Types with typedef, that with typedef, we can abbreviate this type even further.

Any variable of the enum suit type may take one of those four possible enumerated constant values. In this definition, we don't know what the values actually are; the values are hidden by their item names. The convenience of this new type is that we no longer have to remember what each numerical value means; the enumerated item name tells us that.

When we define an enumerated type as we have done previously, the compiler automatically assigns values to each enumerated item.

What if we wanted each enumerated item to have a specific constant value? We could be completely explicit and assign each named item a value, as follows:

enum suit { 
  eSpade   = 4,
  eHeart   = 3, 
  eDiamond = 2, 
  eClub    = 1
 };

Here, the 4 value is named eSpade, the value 3 value is named eHeart, and so on. These could be any values. However, we assign these values intentionally to give each suit a specific value, which we will use in later chapters. This models the fact that in most card games, spades have a higher value than hearts, hearts have a higher value than diamonds, and clubs have the lowest suit value.

Experiment One

Use the following program as your "test bed":

#include <stdio.h>

enum suit {

eSpade = 4,

eHeart = 3,

eDiamond = 2,

eClub = 1

};

int main( void ) {

enum suit s = eClub;

printf( "%d " , s);

return 0;

}

Create this file, giving it any name you like, say, testenum.c. Save it and be certain it successfully compiles. Now, change the value of each enumerated constant to the same value; it could be any value you choose. Does this test program still compile? It should. This tells us that while item names must be unique within an enumeration list, the values themselves do not have to be unique.

We will use this file for the next experiment in a moment.

In Chapter 5, Exploring Operators and Expressions, we briefly examined bitwise operators using constants to declare single-bit flags. We can do this more clearly with enumeration. However, when we use enumeration in this manner, we actually do care about the value of each enumerated item. An enumerated definition for textStyles would be as follows:

/* flag name       binary value */ 
enum textStyle {
   lowercase     = 0b00000001,
   bold          = 0b00000010,
   italic        = 0b00000100,
   underline     = 0b00001000
}

We set each item in the enumeration to be a specific bit pattern for its value or, put another way, a specific bit in its bit field. 0b00000001 is the binary value for 1. Knowing this, we could have alternatively defined enum textStyle as follows:

/* flag name binary value */ 
enum textStyle {
   lowercase     = 1,
   bold          = 2,
   italic        = 4,
   underline     = 8
}

You need to be careful when doing this, because the importance of the bit pattern may not be obvious in the second example, and an unsuspecting programmer may add an additional item with a clashing bit pattern, such as the following:

  strikethrough = 5;

In our original intended scheme, the added item would be 4 (italic) + 1 (lowercase) and might later give unpredictable results at runtime since tests for italic or lowercase would evaluate to true if strikethrough was intended. To properly add the strikethrough item, it should be defined as having a power of 2 so as not to clash with the other enumerated values, as follows:

enum textStyle {
   lowercase     =  1,
   bold          =  2,
   italic        =  4,
   underline     =  8,
   strikethrough = 16,
}

Using enumerations in this way is possible and sometimes necessary. By carefully assigning values to each name, we can then combine these textStyle enums in an integer variable and extract them, as follows:

int style      = bold | italic;      // Style has bold and 
                                     // italic turned on.
int otherStyle = italic + underline; // OtherStyle has 
                        // italic and underline turned on.
if( style & bold ) ...              // bold is on 
if( !(otherStyle & bold) )          // bold is off

style is an integer that can be assigned enumerated values via bitwise operators or simple arithmetic operators. We can't change the value of bold, italic, or any of the other enumerated items, but we can use their values to assign them to another variable. Those variables' values can be combined and/or changed.

Using bit patterns is but one case where we do care about the values of the named items. In this case, each bit signifies a flag or setting. Later, we will use the values of enum suit to determine not just the suit name but also its relative value compared to the others.

Using enumerations

We have defined a new type with a specified set of values. To assign values of that type to a variable, we now have to define a variable using our new enumerated type.

Declaring a variable of enum suit type would be done as follows:

enum suit card;
...
card = spade;
...
if(      card == eClub )    ...
else if( card == eDiamond ) ...
else if( card == eHeart )   ...
else if( card == eSpade )   ...
else
    printf( "Unknown enumerated value
" );

Since card is an enumerated type, we know that its intended values are those named in the enumeration. You will notice in the preceding code snippet that we check for any value of card outside of our known set of values. This is actually not required for a simple card suit example; is there a deck of cards with more than four suits? Possibly, but I have yet to see one. Is it likely then, that this set of enumerations would change? Also not likely. However, card may be assigned a value that is not in the enumerated list. This would be a programming error, which is mitigated by checking the value of card against the intended set of named values.

Experiment Two

Keeping the enumerated items as before (all having the same value), change the main() function to the following:

int main( void ) {

card = spade;

if( card == eClub ) printf( "club " );

else if( card == eDiamond ) printf( "diamond " );

else if( card == eHeart ) printf( "heart " );

else if( card == eSpade ) printf( "spade " );

else

printf( "Unknown enumerated value " );

return 0;

}

Save, compile, and run your test program. Does your program print spade? No, because the eClub named value has the same actual value as the eSpade named value and the first if()... condition evaluates to TRUE. This means that when we need to compare enumerated items, their values should be unique.

Experiment Three

Now change the value of each enumerated item in enum suit so that each are unique. Save, compile, and run the program. Does your program now print spade? It should do.

Now assign other named values (enumerated items) as well as bogus values to card. Recompile each time and run. The program should run as expected for each value of card.

The practice of checking for unknown enumerated items is recommended because the number of items could expand or even just the names of the items could change at some later time. Such a simple check will ensure that the new values are properly handled (or not), but the program behavior will not be unexpected, thereby avoiding possible mayhem. When an unhandled or unknown enumeration appears, it means we either have a new enumeration that we are not checking or that the new enumeration is not properly handled in our program. We will see this later. Imagine if, in the initial version of the program, the enum shape enumerated type was the following:

enum shape { triangle, rectangle , circle }; 

Later, it may be found that enum shape needs to be extended to deal with more shapes, as follows:

enum shape { triangle, square, rectangle, trapezoid, pentagon, hexagon, octagon, decagon, circle ); 

All of the existing code that checks for triangle, square, and circle will work as before. However, as we have added new enumerated values, we must also add code to handle each one. It may not be obvious at all whether we remembered to change every place in our program that uses these new types. The unknown type check is a fail-safe plan to help us root out all instances where the handling of new enumerated items may have been missed.

As we saw at the end of the preceding chapter, if our program was large and involved many files, we might even want to perform error processing and exit gracefully, as follows:

int shapeFunc( enum shape )
{
  ...
  if(      shape == triangle ) ...
  else if( shape == rectangle ) ...
  else if( shape == circle ) ...
  else
    goto error:
  }
  ...
  return 0;  // Normal end.
error: 
  ...        // Error: unhandled enumerated type. 
             // Clean up, alert user, exit.
   return -1; // Some error value.
}

In the preceding code block, we may have been pressed for time and have forgotten to handle our new enumerated values – trapezoid, pentagon, hexagon, and octagon. Or, as more often happens, we may have failed to have updated code for just one or two of them. By including a check as shown, while we may not completely eliminate unexpected runtime behavior, we can limit the possible negative effects of not handling one of the added enumerated values.

In the next section, we will see how to replace this if()… else if()… else… code with a switch()… statement, thereby eliminating the need for a goto statement.

Indeed, we could even make our return type an enumerated type and let the caller handle the error, as follows:

enum result_code
{
    noError = 0,
    unHandledEnumeration,
    ...
    unknownError
};

Here, noError is specifically assigned the 0 value. Any subsequent items are automatically assigned unique values for that enumerated group by the compiler. Then, our function would become as follows:

enum result_code shapeFunc( enum shape )  {
  ...
  if( shape == triangle ) ...
  else if( shape == square ) ...
  else if( shape == circle ) ...
  else
    return unHandledEnumeration;
  }
  ...
  return noError;
}

In this code block, we have removed the goto: section. By removing the "clean-up" processing, responsibility is then placed on the caller of shapeFunc(). The caller of this function should then do a check and handle result_code, as follows:

enum result_code result;
enum shape       aShape;
...
result = shapeFunc( aShape );
if( noError != result )  {
  ... // An error condition occurred; do error processing.
}
...

In the preceding if condition, as it is evaluated, a call is made to shapeFunc() and enum_result is returned. Note that the enumerated value, which is constant and immutable, is placed first. In this way, we can avoid the somewhat common error of assigning a function result to one element of a conditional expression. Let's explore this a bit further.

In the following code, the variable is given first:

if( result == noError ) ...  // continue.

This could be mistakenly written (or later altered) as follows:

if( result = noError ) ...  // continue.

In the latter case, noError is assigned to the result variable. Its value is actually 0 by definition, which would then be interpreted as false. The code would behave as if an error condition had occurred when, in fact, no error was encountered.

The preceding code illustrates a defensive coding tactic – in a conditional expression, strive to put the invariant conditional first. In the preceding example, the value of noError cannot be changed by definition, whereas result can. Therefore, it is safer to put noError first in the conditional expression, to avoid any confusion between comparison (==) and assignment (=) operators.

A more concise version of the call and error check would be as follows:

if( noError != shapeFunc( aShape ) )  {
  ... // An error condition occurred; do error processing.
}

The returned function value is used immediately. This approach has some advantages and disadvantages. While we avoid the use of the result variable, what if, in the error processing code, we want to test for several error conditions and process each differently? This concise method does not allow us to do that. We would have to reintroduce our result variable and then use it to check for various values.

The next section will show how we can avoid this possibility even further with enumerated types.

The switch()… statement revisited

The switch()… statement is best used when we have a single value that can only have a specified set of values. Doesn't that sound like an enumerated type? It should and, happily, it does.

Using the switch()… statement to evaluate an enumerated type simplifies the intent of our code and helps to prevent some troublesome situations. Here is the shapeFunc() function revisited using the switch()… statement:

enum result_code shapeFunc( enum shape aShape)  {
  ...
  switch( aShape )
  { 
    case triangle: 
      ...
      break;
    case rectangle:
      ...
      break;
    case circle:
      ...
      break;
    default:
      ...        // Error: unhandled enumerated type. Clean 
                // up, alert user, return.
      return unHandledEnumeration;
      break;
  }
  ...
  return noError; // Normal end.
}

By using the switch()… statement, it is clear we are considering only the value of shape. Recall that using if()…else… is more general; other conditions may be introduced into the processing, which would make the intent of processing the value of shape less straightforward. The switch()… statement also allows us to remove the need for goto and handle the unknown shape enumerated type in the default: branch of the switch()… statement. Note that even though our default: branch has a return statement, we also add the break statement as a matter of safety. As a rule, any default: branch should always include a break statement because other case: branches may occur after the default: branch.

Let's put this very function into a working program. In sides.c, we define an enumerated list of shapes and then call PrintShapeInfo() to tell us how many sides each shape has. We'll also demonstrate fall-through logic in the switch()… statement. The PrintShapeInfo() function is as follows:

void PrintShapeInfo( enum shape aShape)  {
  int nSides = 0;
  switch( aShape )  {
    case triangle: 
      nSides = 3;
      break;
    case square:
    case rectangle:
    case trapezoid:
      nSides = 4;
      break;
    // missing something?
    case hexagon:
      nSides = 6;
      break;
    case  octagon:
      nSides = 8;
      break; 
    // missing something?
    case circle:
      printf( "A circle has an infinite number of sides
" );
      return;
      break;
    default:
      printf( "UNKNOWN SHAPE TYPE: %s
" , 
             GetShapeName( aShape ) );
      return;
      break;
  }
  printf( "A %s has %d sides
" , GetShapeName( aShape) , nSides );
}

The purpose of the function is to determine and print the number of sides of a given shape. Notice that square, rectangle, and trapezoid all have four sides. When one of them is encountered, the logic is the same – assign 4 to the number of sides. For a circle, no number is possible to represent infinity; in that case, we simply print that and return from the function. For all the other cases, we call another function, getShapeName(), to return the name of the shape.

Note that, in this switch()… statement, we have forgotten to handle some of our shapes. The function handles this gracefully. We do not need to return a function result status because we are simply printing out the name and number of sides of a given shape.

The getShapeName() function is as follows:

const char* GetShapeName( enum shape aShape)  {
  const char* name = nameUnknown;
  switch( aShape )  {
    case triangle:  name = nameTriangle;  break;
    case square:    name = nameSquare;    break;
    case rectangle: name = nameRectangle; break;
    case trapezoid: name = nameTrapezoid; break;
    case pentagon:  name = namePentagon;  break;
    case hexagon:   name = nameHexagon;   break;
    case octagon:   name = nameOctagon;   break;
    case circle:    name = nameCircle;    break;
    // missing something?
    default:        name = nameUnknown;   break;
  }
  return name;
}

This function takes a shape enumerated type and returns the name of that shape. Here, we put each branch of the switch()… statement on a single line consisting of case:, an assignment, and a break statement. But where do these names come from? We will explore strings in Chapter 15, Working with Strings. For now, let's just take these as given. To use them in our function, we must define them as global constants. We cannot define them in the function itself because they will be destroyed in the function block when the function returns. So, by the time the caller needs those values, they are gone! By making them global constants, they exist for the life of the program, and they do not change (they don't need to change). We will explore this concept in greater detail in Chapter 25, Understanding Scope. The shape names must be defined in our program, as follows:

 #include <stdio.h>
const char* nameTriangle  = "triangle";
const char* nameSquare    = "square";
const char* nameRectangle = "rectangle";
const char* nameTrapezoid = "trapezoid";
const char* namePentagon  = "pentagon";
const char* nameHexagon   = "hexagon";
const char* nameOctagon   = "octagon";
const char* nameDecagon   = "decagon";
const char* nameCircle    = "circle";
const char* nameUnknown   = "unknown_name";
enum shape  {
  triangle, 
  square, 
  rectangle, 
  trapezoid, 
  pentagon, 
  hexagon, 
  octagon,
  decagon,
  circle
};
void        PrintShapeInfo( enum shape aShape );
const char* GetShapeName(   enum shape aShape );
int main( void )  {
  PrintShapeInfo( triangle );
  PrintShapeInfo( square );
  PrintShapeInfo( rectangle );
  PrintShapeInfo( trapezoid );
  PrintShapeInfo( pentagon );
  PrintShapeInfo( hexagon );
  PrintShapeInfo( octagon );
  PrintShapeInfo( decagon );
  PrintShapeInfo( circle );
  return 0;
}

Unfortunately, in C, we cannot use an enumerated type to define string values. We define them as constants because we do not want the names to change.

Create the shapes.c file, enter the main function and the two functions, and save your program. Compile and run the program. You should see the following output:

Figure 8.1 – Screenshot of shapes.c output

Figure 8.1 – Screenshot of shapes.c output

Note that there are two unknown shape types. Perhaps we forgot to handle them somewhere in our program. Edit the program so that all the shape types are handled. Also, note that there is an unknown shape name. Perhaps we forgot to add that name to our list of shapes. You can find the source code in the GitHub repository for shapes.c and the completed version, shapes2.c, which handles all of the enumerated items.

We may not always have values that are closely related, yet we might want to still give them names. To do that, we use the second kind of enumerated type.

Using enumerated item values as literal integer constants

There is one other form of enumerated types. It is when we want to give an identifier to a literal constant value and we want to avoid the use of #define. Early versions of C relied upon the preprocessor heavily – later versions, much less so. The pros and cons of using the preprocessor will be discussed in Chapter 24, Working with Multi-File Programs.

Instead of using #define, we can declare an anonymous enumerated type that contains enumerated items that act identically to literal constants, as follows:

enum {
  inchesPerFoot = 12,
  FeetPerYard   = 3,
  feetPerMile   = 5280,
  yardsPerMile  = 1760,
  ...
}

Note that there is no name associated with the enum type; this is what makes it anonymous. Each of these enumerated values can be used wherever we need that value; we simply use its name. This will become important when we want to declare arrays with predefined sizes. Because enumerated item values are integers, this method cannot completely replace the need for #define preprocessors or for non-integer values; in those cases, the use of const is preferred.

Summary

An enumerated type is a set of conceptually related named values. An enumerated item in an enumerated type is a way to give a name to a literal value. The value of an enumerated item is constant. Most of the time, the values are not significant, but items that are in the set themselves add meaning to the type. We can use enumerated types to create natural collections or groups of values, as we have seen with card suits and shapes. The switch()… statement is ideally suited to select and process items within an enumerated type. We can also use an anonymous enumerated type to name unrelated literal integer values instead of using #define for them.

An enumerated type, unfortunately, doesn't provide everything we might need to model the real world. For instance, in a deck of cards, each card has both a suit and a face value, two different enumerations. To combine them into a single card object that represents reality more closely, we need another custom data type – structures. We will explore them in the next chapter.

Questions

  1. Why might you want to use an enumerated type?
  2. Do you have to give each enumerated constant a value?
  3. Do enumerated constants have to be unique with a group?
  4. Do the values of each case: condition in a switch()… statement have to be unique?
  5. Enumerated constants are limited to which intrinsic data type?
..................Content has been hidden....................

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