Chapter 9: Creating and Using Structures

When a number of different kinds of values all pertain to a single thing, we can keep them organized with structures. A structure is a user-defined type. There may be multiple values in a structure and they may be of the same type or different types. A structure, then, is a collection of information representing a complex object.

With structures, not only can we represent complex objects more realistically, but we can also create functions that manipulate the structure in relevant ways. Just as data within a structure is grouped together in a meaningful manner, we can also group functions that manipulate the structure together in meaningful ways.

C is not an object-oriented programming (OOP) language. However, OOP has been a primary focus of programming languages and programming since the early 1990s. It is extremely likely that after you learn C, you will, at the very least, be exposed to OOP concepts. Therefore, after we learn about C structures and the operations we can use on them, we will learn how C structures are a logical transition to OOP. Thinking about C in a special manner then becomes a stepping stone to learning OOP.

The following topics will be covered in this chapter:

  • Understanding structures
  • Performing operations on structures – functions
  • Structures of structures
  • Understanding how C structures and functions are similar to and different from objects in other OOP languages

Let's get started!

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/Chapter09.

Understanding structures

It would be extremely easy for C programmers if the world were simply made up of objects that were only numbers or names. Imagine if everything were expressed as only a name or a series of numbers and nothing else. For example, an automobile vehicle identification number (VIN) precisely describes various attributes of that car and uniquely identifies it. On the other hand, humans are far more complex than automobiles. If not, perhaps the world would be a rather uninteresting place.

For C programs to solve real-world problems, they have to be able to model real-world complex objects. C allows various aspects of real-world objects to be abstracted and modeled via C structures. In the previous chapter, in a very basic way, we explored two such objects—playing cards and two-dimensional (2D) shapes. Did we explore every aspect of them? No.

In fact, we barely scratched the surface. For playing cards, we need to be able to describe all 52 cards in a deck uniquely. To do this, we need both the card's suit and its face value. We might also need the card's relative value (spades have a higher value than hearts, an ace might have a high value or low value, and so on). Later, we will learn how to put them in a collection so that we can represent a full deck of 52 cards. For now, we will see how to accurately represent each card individually using structures.

Likewise, with 2D shapes, we only represented a single, basic aspect of them—the number of corners or sides that they have. Depending on the requirements of the model we need to manipulate, we may need to additionally consider the lengths of each side, the angles of each corner, and other aspects of the shape, such as its surface area.

If we were drawing a shape in a field, we would need to be concerned about its x and y positions, line thickness, line color, fill color, vertical ordering in relation to other shapes, and possibly other aspects of the shape in the field. Our shape structure might also need to contain other structures representing position and colors. A position would be a structure of two values, x and y, representing the Cartesian coordinate (x,y). A color would be a structure of four values representing red, green, blue, and transparency levels (this is just one representation of color).

In Chapter 6, Exploring Conditional Program Flow, we considered the calculations for a leap year. The year is just one small part of a structure representing a date. Actually, some of the most complex structures are those that accurately represent date and time and then convert between all of Earth's various calendars and time zones.

Lastly, when we consider a structure consisting of multiple aspects, we must also consider the operations we can perform on that structure. This is similar to our basic data types and the operations we perform on those data types. However, because structures are custom to our specific program and problem domain, we must realize that we have to also fashion the necessary operations on those structures—how to set and get information within them; how to compare one to another; and which other operations might be possible, such as adding a duration to time or blending two colors. Later, when we have collections of structures, we might want to order them, find a specific structure in a collection of structures, and perform other operations on them.

Declaring structures

The structure type allows us to specify a group of related variables, each representing an aspect, or component, of the thing being modeled. There may be just a few components in the modeled thing; more often than not, there are many components. Each component can be of any intrinsic C data type (integer, real, Boolean, char, complex, and so on) or any previously defined custom type (a structure within a structure). The components in a structure do not have to be of the same type. Therein lies the power of a structure—it allows us to group various aspects of the thing we are modeling into a single custom data type; in this case, a C structure. We can then use that data type much like we use any other data type.

The syntax for defining a structured type is shown here:

struct name  { 
  type componentName1; 
  type componentName2; 
  … ; 
  type componentNameN;
};

The type consists of two words, struct and name. The components of the structure are contained within { and }. Each named component in the structure is separated by ;, and the definition of the structure is concluded with ;. Unlike intrinsic data types, components can't be initialized within a structure when the structure is defined. The initialization of components is done when a variable of that structure type is declared.

So, the definition of our enumerated type of card would look like this:

enum Suit  {
  eClub    = 1, 
  eDiamond, 
  eHeart,
  eSpade 
};
enum Face  { 
  eOne   = 1, 
  eTwo, 
  eThree, 
  eFour, 
  eFive, 
  eSix, 
  eSeven, 
  eEight, 
  eNine, 
  eTen, 
  eJack, 
  eQueen, 
  eKing, 
  eAce
};
struct Card {
  enum Suit suit;
  int       suitValue;
  enum Face face;
  int       faceValue;
  bool      isWild;
};
struct Card card;

Notice that we must define enumerations of enum Suit and enum Face before using them in our structure. Also, note that enum Suit and enum Face (custom types) are similarly named but are still different from the suit and face variables. Remember that uppercase letters are different from lowercase letters in identifiers (IDs)/variable names. We are adopting an arbitrary convention here that custom data types begin with an uppercase letter and variables of that type begin with a lowercase letter of the same name.

Our struct Card enumeration now has enough information to accurately reflect a playing card's suit and face. We have added integer variables to hold the card's relative suit and face value. We might have chosen to use the values contained within enum Suit and enum Face, but that might cause issues later on, so we have a separate component for each. In some card games, the ace is either high or low. In most card games, the suits have an ordered value, with spades being the highest and clubs being the lowest. For instance, in the card game Blackjack, suit value doesn't matter and aces are the highest-value card, whereas in some poker games, suit value does matter and the ace may be high or low. Our struct Card enumeration is general enough that it can be used for each of these scenarios.

Finally, we declare a card variable of struct Card, again using the convention that the custom data type name has an uppercase name while an instance of that type has a lowercase name.

The card variable is the overall name for a variable of five components—suit, suitValue, face, faceValue, and isWild. When the card variable is declared, enough space must be allocated to hold all the values of the components within it. If we assume that the enum types are 4 bytes and bool is 1 byte, then the result of sizeof( card ) would be 17. So, each time a variable of the struct Card type is declared, 17 bytes would be allocated for it.

Note that, here, we assume that enum types are the same size as int and that a bool is 1 byte. The compiler actually determines the size of each enum type based on the range of values it contains. Therefore, in reality, we cannot always rely on our assumptions. We can verify this, of course, with the following code:

  printf( "  enum Suit is %lu bytes
" , sizeof( 
         enum Suit ) );
  printf( "  enum Face is %lu bytes
" , sizeof( 
         enum Face ) );
  printf( "        int is %lu bytes
" , sizeof( int ) );
  printf( "       bool is %lu bytes
" , sizeof( bool ) );

Note that we use the type as the parameter of sizeof(). The return type of sizeof() is size_t, which is equivalent to unsigned long. To print the result of size(), we need to specify %lu in print(). This will be thoroughly explained in Chapter 19, Exploring Formatted Output. We could have also used any declared variable, as follows:

  printf( "struct Card is %lu bytes
" , sizeof( 
         struct Card ) );
  printf( "       card is %lu bytes
" , sizeof( card ) );

Let's verify our assertions about the size of struct card with the following code:

// add necessary includes
// add definitions for enum Suit, enum Face, and struct 
// Card
int main( void )  {
  struct Card card;
  printf( " enum Suit is %lu bytes
" , sizeof( 
         enum Suit ) );
  printf( " enum Face is %lu bytes
" , sizeof( 
         enum Face ) );
  printf( " int is %lu bytes
" , sizeof( int ) );
  printf( " bool is %lu bytes
" , sizeof( bool ) );
  printf( "struct Card is %lu bytes
" , sizeof( 
         struct Card ) );
  printf( " card is %lu bytes
" , sizeof( card ) ); 
  return 0; 
}

Create a card.c file and enter the preceding code, adding the necessary header file(s) and enum and struct definitions. Save the file, compile it, and run it. You might see the following output:

Figure 9.1 – card.c output

Figure 9.1 – card.c output

We previously calculated that struct Card would be 17 bytes, but our test program shows that it is 20 bytes. What's going on?

It turns out that something else is going on behind the scenes. That something is called structure alignment, where a given structure is padded with enough space so that it contains an even multiple of the size of its largest component. In the case of struct Card, it is padded with 3 bytes so that it will occupy an even multiple of 4, with 4 bytes being the largest size of any component in struct Card.

Let's try two different tests.

Experiment 1

First, add another bool variable to struct Card. Run the program again. Notice that the structure takes the same number of bytes—20. The 1-byte bool reduced the amount of padding needed, but the structure is still 20 bytes.

Experiment 2

Now, add a double variable to struct Card and run the program. Notice that the size of struct Card is now 32, or an even multiple of 8, which is the size of a double.

Padding within a structure can occur at the end or even in between components. Holes may appear between two consecutive components or after the last component. For the most part, we don't need to and shouldn't concern ourselves with how padding occurs within a structure.

However, because of the padding that's used to align structures, we cannot compare two structures as whole entities for comparison. If padding is present in a structure, the contents of that padding may be undefined, depending on how it is initialized. Therefore, even if two structures have identical component values, the values in the padding are highly unlikely to be equal.

Instead, if an equality test is required, a function must be written to compare two structures component by component. We'll look at this in more detail later in the Performing operations on structures – functions section.

Initializing structures and accessing structure elements

Once a structure is defined, we can declare variables of that type. The variables of that structure type must be initialized before use. C gives us a number of ways to do this, depending on the needs of the situation.

Given the definition of struct Card, we can initialize a variable of that type in one of three ways, as outlined here:

  • At the time of declaration: The first way of initializing a structure variable is at declaration time, as follows:

    struct Card c1 = { eHeart , (int) eHeart , eKing, (int)eKing , false };

The structure component values are enclosed between { and }, separated by commas. The order is significant. c1 is a card of the eHeart suit with suitValue of the eHeart enumeration and face of eKing with faceValue of the eKing enumeration, which is not a wildcard. In this form of initialization, we must be careful to get the order exactly correct within the definition of struct Card.

If we wanted a completely zero initial state, we could initialize all bytes in the structure with the following code:

struct Card card3 = {0};  // Entire structure is zero-d.

Zeroing the structure in this manner can only be done at the same time the variable is declared.

  • After declaration, the entire structure, in toto: The second way of initializing a structure variable is by assigning it the values of another structure variable in toto, or a complete and whole copy. This can be done as follows:

    struct Card card2 = card1;

As card2 is being declared, every component of card1 is assigned to the corresponding component of card2. Care must be taken that these structures are of the same type, otherwise unpredictable results/assignments may occur. This is because it is a bitwise assignment that assumes all of the components of each structure are identically positioned (with padding) with the structure type.

  • After declaration, component by component: Lastly, a structure variable can have its components assigned explicitly, component by component. When doing this, it is a good idea to first nullify, or zero, the entire structure at its definition. Each component is accessed using . notation, which specifies a given component of the structure. This initialization would look like this:

    struct Card card3 = {0}; // Entire structure is

                             // zero-d.

    card3.suit = eSpade;

    card3.suitValue = (int)eSpade;

    card3.face = eAce;

    card3.faceValue = (int)eAce;

    card3.isWile = true;

In this way, each component is accessed directly. The components do not have to be assigned in the same order that they are defined in the structure, but it is a good practice to do so. This type of initialization is done via component initialization. While tedious, it is often the most error-free approach since the structure can change; as long as the component names are the same, the code will not fail. When using this approach, it is also a good idea to initialize to some default state, which can either be all zeros or a default value.

You might need to create a default structure constant that is assigned to any structure variable of that type when declared, as follows:

const struct Card defaultCard = { eClub , (int)eClub , eTwo , (int)eTwo , false };
struct Card c4 = defaultCard;
c4.suit = ... /* some other suit */
...

Clearly, for our card example, having defaultCard does not make sense. In other scenarios, especially when the structure type is complex with many components, a default structure constant can provide highly consistent program behavior since all structure variables will begin in a known state of either zeroed values or otherwise valid values.

Performing operations on structures – functions

Except for assignment, there are no intrinsic operations for structures. To perform any operation on a single structure or with two structures, a function must be written to perform the desired operation.

For example, earlier, we mentioned a function that can be used to compare two structures for equality. This must be done component by component, as follows:

bool isEqual( struct Card c1 , struct Card c2 )  {
  if( c1.suit != c2.suit ) return false;
  if( c1.face != c2.face ) return false;
  return true;
}

Notice that we did not compare every component of struct Card in this function. We'd only have to do that for absolute comparison and when we need to compare each and every component of both structures. This just isn't necessary for our card example.

Does it make sense to perform any or all mathematical operations on two structures? In general, no, but this answer is completely dependent on the nature of the structures we are using.

For the card game Blackjack, we need to add up the face values of our hand. First, we'd have to set up our faceValue component with the proper values for Blackjack. Then, we need to create an operation to add faceValue for two cards. We could do this in a couple of ways. The first way would be to simply add the faceValue component of each card, accessing that component directly, as follows:

int handValue = card1.faceValue + card2.faceValue;
if( handValue > 21 ) {
  // you lose
} else {
  // decide if you want another card
}

Alternatively, we could write a function that adds two cards, as follows:

int sumCards( struct Card c1 , struct Card c2 )  {
  int faceValue = c1.faceFalue + c2.faceValue;
  return faceValue;
}

Then, we can get the sum of two cards with the following code:

int cardsValue = sumCards( card1 , card2 );
if( cardsValue > 21 ) ...

Given these two approaches, is one preferred over the other? It really depends on a number of factors. First, if we need to add the faceValue component of cards in just one or possibly two places in our program, the former approach is acceptable. If, however, we must add faceValue in many places, it is better to consolidate that operation into a single function. Any changes or enhancements that must be made to the operation can then be made in a single place—the one function. All calls to that function would remain unchanged. Each call would reflect the new changes to the function and consistent results would be provided.

For example, what if aces could have either a high value—say, 14—or a low value say, 1? If we used the former approach, we'd have to be certain our ace faceValue component was properly set before performing the addition. If the ace value could change or be either value, depending on the situation, then we'd have to add code to take that into consideration in every place where we added faceValue components. In the latter approach, we would have to take the ace's value into account in only one place: the add function.

Notice in the preceding function that the faceValue variable is different than both c1.faceValue and c2.faceValue. You might think that these variable names conflict because they have the ID name, but they don't. faceValue is a local variable to the sumCards() function, while c1.faceValue is a component of c1, and c2.faceValue is a component of c2. Each of them is actually a different location in memory capable of holding different values.

Now, let's put these concepts into a simplified yet working program.

Copy the card.c file to card2.c. Add the isEqual() and sumCards() functions to it, along with their function prototypes. Delete the body of main() and replace it with the following code:

 
int main( void )  {
  struct Card card1 = { eHeart , (int)eHeart , eKing, 
    (int)eKing , false };
  struct Card card2 = card1;  // card 2 is now identical 
                              // to card 1
  struct Card card3 = {0};
  card3.suit      = eSpade;
  card3.suitValue = (int)eSpade;
  card3.face      = eAce;
  card3.faceValue = (int)eAce;
  card3.isWild    = true;
  bool cardsEqual = isEqual( card1 , card2 );
  printf( "card1 is%s equal to card2
" , 
         cardsEqual? "" : " not" );
  cardsEqual = isEqual( card2 , card3 ); 
  printf( "card2 is%s equal to card3
" , 
         cardsEqual? "" : " not" ); 
  printf( "The combined faceValue of card2(%d) + card3(%d) is %d
" , card2.faceValue , card3.faceValue , sumCards( card2 , card3 ) );
  return 0;  
}

Save your work. Compile and run cards2.c. You should see the following output:

Figure 9.2 – card2.c output

Figure 9.2 – card2.c output

After initializing three cards with enum and int values, we then use the function we wrote to compare two structures for equality. We also use the sumCards() function to add up the face value of two cards. Note that while we can copy one structure to another with the = operator, for any other types of comparisons or operations on structures, we need to create and call our own functions.

So far, we have created a structure that is composed of intrinsic types (int), as well as custom types (enum). We can also compose a structure out of other structures.

Structures of structures

A structure can contain components of any type, even other structures.

Let's say we want to represent a hand of five cards. We'll define a structure that contains five struct Card components, as follows:

struct Hand {
  int cardsDealt;
  struct Card c1; 
  struct Card c2;
  struct Card c3;
  struct Card c4;
  struct Card c5;
}

We could have just as easily written this as follows:

struct Hand {
  int cardsDealt;
  struct Card c1, c2, c3, c4, c5;
];

Both definitions are functionally identical. Do you see how this is similar to how variables of the same type were declared in Chapter 4, Using Variables and Assignments? As you work with C more and more, you will begin to see patterns being used repeatedly throughout the language. These repeated patterns make the language concise, consistent (for the most part), and easier to understand.

As we will see in Chapter 16, Creating and Using More Complex Structures, there is a somewhat more appropriate way to express our hand of cards; that is, by using a structure that contains an array of cards. For now, we'll stick with our current method.

In the struct Hand structure, we use a counter, cardsDealt, to store how many cards are currently in our hand.

Given the structure definition we have seen, each component would be accessed as before, like this:

struct Hand h = {0};

In the preceding code snippet, struct Hand h is initialized to 0 for all its components and subcomponents.

Then, we can access each of the card components directly, as follows:

h.c1.suit      = eSpade;
h.c1.suitValue = (int)eSpade;
h.c1.face      = eTwo;
h.c1.faceValue = (int)eTwo;
h.c1.isWild    = false;
h.cardsDealt++;

The preceding code is equivalent to adding a card to our hand. Writing these lines out is tedious. A common way to do this without things getting cumbersome is to use the following code:

  struct Card c1 = { eSpade   , (int)eSpade   , 
                     eTen      , (int)eTen   , false };
  struct Card c2 = { eHeart    , (int)eHeart   , 
                     eQueen    , (int)eQueen , false };
  struct Card c3 = { eDiamond  , (int)eDiamond , 
                     eFive     , (int)eFive   , false };
  struct Card c4 = { eClub     , (int)eClub    , 
                     eAce      , (int)eAce   , false };
  struct Card c5 = { eHeart    , (int)eHeart   , 
                     eJack     , (int)eJack  , false };
  struct Card c6 = { eClub     , (int)eClub    , 
                     eTwo      , (int)eTwo   , false };

While somewhat tedious, it is less tedious than the preceding method and enables us to see the patterns of the values that have been assigned to each card.

Initializing structures with functions

Another way to initialize our hand is with a function. When given a card as an input parameter to an addCard() function, we need to make sure we put it in the correct place in our hand. The function is shown here:

struct Hand addCard( struct Hand oldHand , struct Card card )  {
  struct Hand newHand = oldHand;
  switch( newHand.cardsDealt )  {
    case 0:
      newHand.c1 = card;  newHand.cardsDealt++;  break;
    case 1:
      newHand.c2 = card;  newHand.cardsDealt++;  break;
    case 2:
      hewHand.c3 = card;  newHand.cardsDealt++;  break;
    case 3:
      hewHand.c4 = card;  newHand.cardsDealt++;  break;
    case 4:
      hewHand.c5 = card;  newHand.cardsDealt++;  break;
    default:
      // Hand is full, what to do now?
      // ERROR --> Ignore new card.
      newHand = oldHand;
      break;
  }
  return newHand;
}

Note that in this switch()... statement, three statements are placed on one line instead of three lines. This method is sometimes preferable as it highlights both similarities and differences in each case clause. In this function, addCard(), oldHand, and card are inputs to the function. Remember that variables, including structures, are passed by copy (a copy of them is made). Therefore, we have to give the function our struct Hand structure in its current or old state, add a card to it, and then return an updated or a new version of it back to the caller, again via a copy. The call to this function would, therefore, look like this:

struct Card aCard;
struct Hand myHand;
...
aCard  = getCard( ... );
myHand = addCard( myHand , aCard );
...

We have not defined getCard() yet and will defer that until Chapter 16, Creating and Using More Complex Structures.

In this function, rather than copy each struct Card subcomponent of hand, we simply assign one struct Card component in toto to the hand's appropriate card component.

Also, in this function, we pass in a copy of our current hand, create a new hand based on the current hand, modify it, and then return the modified version. This is not necessarily the best way to perform this operation, but it is a common function pattern—that is, copy in the structure, modify a copy of it in the function, and then overwrite the original with the modified structure. In Chapter 13, Using Pointers, we will look at an alternate way of doing this that avoids all of the structure copying.

Notice that, in the function, we have not considered what to do if the hand is already full. Do we expect our function to handle it or perhaps handle it with another function, discardCard(), before calling addCard()? The code to do that would look like this:

...
card = getCard();
if( myHand.cardsDealt >= 5 )  {  // should never be 
                                 // greater than 5
  myHand = discardCard( myHand, ... );
}
myHand = addCard( myHand, card );
...

To keep things simple, for now, we will assume that more than 5 cards is a programming mistake and that our addCard() function handles the card presented to it by simply ignoring it.

Printing a structure of structures – reusing functions

Let's create a function to print the contents of the hand. This function will use a function that we will create to print the structures it contains. In this way, the minimum amount of code is used since printing an individual card exists in only one function. The following function takes our struct Hand structure as an input parameter, determines which card we are dealing with, and calls printCard() with that card as a parameter, as follows:

void printHand( struct Hand h )  {
  for( int i = 1; i < h.cardsDealt+1 ; i++ )  {  // 1..5
    struct Card c;
    switch( i )  {
      case 1: c = h.c1; break;
      case 2: c = h.c2; break;
      case 3: c = h.c3; break;
      case 4: c = h.c4; break;
      case 5: c = h.c5; break;
      default:  return; break;
    }
    printCard( c );
  }
}

In this function, printHand(), we iterate over the number of cards in our hand. At each iteration, we figure out which card we are looking at and copy it to a temporary variable so that all subsequent accesses are to the temporary structure variable. We can then call printCard() for each card (shown in the following code snippet), which deals with the face and suit of the given card, even though it is a copy of a different card at each iteration. Alternatively, we could have written a printHand() function, as follows:

void printHand2( struct Hand h )  {
  int dealt = h.cardsDealt;
  if( d == 0 ) return;
  printCard( h.c1 ); if( dealt == 1 ) return;  
  printCard( h.c2 ); if( dealt == 2 ) return;
  printCard( h.c3 ); if( dealt == 3 ) return;
  printCard( h.c4 ); if( dealt == 4 ) return;
  printCard( h.c5 ); return;
}

In the preceding function, we use fall-through logic to print the cards in our hand.

The printHand() function contains two switch statements to print an individual card—one to print the face and one to print the suit, as follows:

void printCard( struct Card c )  {
  switch( c.face )  {
    case eTwo:   printf( "    2 " ); break;
    case eThree: printf( "    3 " ); break;
    case eFour:  printf( "    4 " ); break;
    case eFive:  printf( "    5 " ); break;
    case eSix:   printf( "    6 " ); break;
    case eSeven: printf( "    7 " ); break;
    case eEight: printf( "    8 " ); break;
    case eNine:  printf( "    9 " ); break;
    case eTen:   printf( "   10 " ); break;
    case eJack:  printf( " Jack " ); break;
    case eQueen: printf( "Queen " ); break;
    case eKing:  printf( " King " ); break;
    case eAce:   printf( "  Ace " ); break;
    default:    printf( "  ??? " ); break;
  }
  switch( c.suit )  {
    case eSpade:   printf( "of Spades
");   break;
    case eHeart:   printf( "of Hearts
");   break;
    case eDiamond: printf( "of Diamonds
"); break;
    case eClub:    printf( "of Clubs
");    break;
    default:      printf( "of ???s
");     break;
  }
}

Let's put these concepts into a simplified yet working program.

Copy card2.c to card3.c and remove the function prototypes and functions from card3.c. Add the addCard(), printHand(), printHand2(), and printCard() functions and their prototypes. Then, replace main() with the following code:

int main( void )  {
  struct Hand h = {0};
  struct Card c1 = { eSpade   , (int)eSpade   , 
                     eTen     , (int)eTen   , false };
  struct Card c2 = { eHeart   , (int)eJeart   , 
                     eQueen   , (int)eQueen , false };
  struct Card c3 = { eDiamond , (int)eDiamond , 
                     eFive    , (int)eFive   , false };
  struct Card c4 = { eClub    , (int)eClub    , 
                     eAce     , (int)eAce   , false };
  struct Card c5 = { eHeart   , (int)eHeart   , 
                     eJack    , (int)eJack  , false };
  struct Card c6 = { eClub    , (int)eClub    , 
                     eTwo     , (int)eTwo   , false };
  h = addCard( h , c1 );
  h = addCard( h , c2 );
  h = addCard( h , c3 );
  h = addCard( h , c4 );
  h = addCard( h , c5 );
  h = addCard( h , c6 );
  
  printHand( h );
  printf("
");
  printHand2( h );
  
  return 0; 
}

Compile card3.c and run it. You should see the following output:

Figure 9.3 – card3.c output

Figure 9.3 – card3.c output

Here, you can see that both printHand() functions match our card initializations.

Lastly, we must mention that a structure may not contain a component that is its own type. For example, a struct Hand structure may not contain a component of struct Hand. A structure may contain a pointer reference to a component that is its own type. For example, take a look at the following code:

struct Hand {
    int cardCount
    struct Hand myHand;   /* NOT VALID */
};

This code is not valid because a structure cannot contain a component that is itself. Now, take a look at the following code:

struct Hand {
  int cardCount;
  struct Hand * myHand;   /* OK */
}

This is valid because the component is a pointer reference to the same type of structure. The type of myHand is struct Hand *, which points to a different variable of the struct Hand type. We will explore this feature in more detail in Chapter 13, Using Pointers.

Now that we have seen some simple uses of C structures and functions that manipulate them, we can look at how these facilities give us a preview of OOP.

The stepping stone to OOP

OOP has been defined in many ways. At the core of all OO languages are objects. Objects are collections of data, much like C structures, and also operations on that data that are specific to that object, similar to C functions that operate on a structure. So, an object contains both its data and the set of operations that can be performed on it. Sometimes, the internals of the object are completely hidden to the outside program and its components are only available through functions with access to them, called accessors. This is a more self-contained version of C where functions are somewhat independent of the data or structures they operate on and must be passed the data that they manipulate. In C, a function—or a manipulator of data—is loosely tied or coupled to the data it manipulates.

In this chapter, we have used a set of data structures and enumerations to represent real-world cards. We also made functions specific to that data, such as addCard(), printCard(), and printHand(), which are meant to manipulate those data structures.

So, data structures and functions that manipulate them become the basis for OOP. Data and operations being used on that data, when combined into a self-contained cohesive unit, are called a class. A data object may be a derivative of a more general classification of objects, much like a square is a derivative of the more general shape classification. In this case, the square class derives certain general properties from the shape class. Such a derivation is called inheritance and is also common to all OOP languages. Methods, also called member functions, are functions that only operate on the data contained within the class, also called member data.

Later, in Chapter 24, Working with Multi-File Programs, we will see that we can approximate OO thinking in C where a single file would contain a set of data structures and constants, as well as a set of functions that are meaningful for those data structures. Such a single C file data and functions approach would be a stepping stone to how you might make the transition from C, a function-oriented programming language, to an OOP language.

Summary

In this chapter, we learned about user-defined C structures. This is one of the most powerful ways of representing real-world objects in a cohesive and clear manner. First, we learned how to declare structures of our basic intrinsic types and custom types (enum). Then, we explored how to directly access and manipulate the components of C structures. The only simple operation that we can perform on structures in toto is the assignment operator.

We also explored how to manipulate structures via functions that access structure components and manipulate either the individual components, the entire structure, or multiple structures at the same time. We then expanded on the concept of what can be in a structure by defining structures of other structures. Finally, we learned that while C is not an OO language, we saw how C structures are the stepping stone to languages that are OO.

You may have found that, while using enums and structs, having to remember to add those keywords is somewhat cumbersome. In the next chapter, we will learn how to make using enums and structs a bit less cumbersome with the typedef type specifier. This will allow us to make the names of our data types more expressive, in a similar way to how we make variable IDs express their intended purpose.

Questions

  1. How do you initialize a variable of struct someName to zero?
  2. How do you do a bitwise copy of one structure variable to another?
  3. What are the advantages of initializing the components of a structure variable at the time of declaration?
  4. What are the disadvantages of initializing the components of a structure variable at the time of declaration?
  5. Which data types may make up the components of a structure?
..................Content has been hidden....................

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