Creating and Using More Complex Structures

In the real world, objects that we may want to model with data types and then manipulate with our program code are often best expressed as collections—sometimes complex collections—of the various data types we have already encountered. We have seen how to make homogenous collections with arrays, where all of the values in the collection are of the same type and size. We have also seen how to make heterogeneous collections with structures, where the various types in the structure are simple intrinsic types, even if they are not all the same type in the real world.

In this chapter, we will explore more complex structures. These include the following:

  • Arrays of structures
  • Structures consisting of arrays
  • Structures consisting of other structures
  • Structures consisting of arrays of structures

This may sound bewilderingly complex at first, but it is not. In reality, this is a logical extension of the concepts we have already explored using intrinsic types to create combinations of structures and arrays. We are simply expanding the kinds of data types that we can group into structures and arrays to also include other structures and arrays. We will also explore some new syntax to access elements of these complex structures.

It is my hope that with this exploration, you will see how C establishes simple rules for basic concepts and then uniformly extends them to more complex topics. We have already seen this with how single-dimensional arrays are logically extended to multi-dimensional arrays. We have also seen this with how arrays can be extended to create strings. We will now see this with complex structures.

As we explore these complex structures, we will greatly expand our ability to model real-world objects. As we do that, we will also expand how we think about how to manipulate those complex objects with functions that operate specifically on a given complex structure. Furthermore, we will see how pointers make this manipulation both straightforward and efficient.

The following topics will be covered in this chapter:

  • Creating an array of structures
  • Accessing structure elements within an array
  • Manipulating an array of structures
  • Creating a structure consisting of other structures
  • Accessing structure elements within the structure
  • Manipulating a structure consisting of other structures
  • Creating a structure with arrays
  • Accessing array elements within a structure
  • Manipulating array elements within a structure
  • Creating a structure with an array of structures
  • Accessing individual structure elements of the array within a structure
  • Manipulating a structure with an array of structures

To illustrate these concepts, we will continue our development of the card4.c program that we encountered in Chapter 10, Creating Custom Data Types with typedef. By the end of this chapter, we will have a basic playing card program that will create a deck of cards, shuffle it into a random order, and then deal out four hands of five cards for each hand.

Technical requirements

Continue to use the tools you chose in the Technical requirements section of Chapter 1, Running Hello, World!.

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

Introducing the need for complex structures

We have explored C's intrinsic types—integers, floats/doubles, Booleans, and characters. We have also explored C's custom types—structures and enumerations. We have seen how a structure is a single instance of a grouping of intrinsic types to represent a set of related characteristics of something. Additionally, we have explored C's collection type—arrays, which are groups containing all of the same type of thing.

Each of the data types we have explored more or less represent real-worldobjects thatwe may wish to manipulate. More often, however, real-world things are far more complicated. Therefore, these types alone may not adequately represent the real-world object we want to model and manipulate. So, we need to learn how to combine structures with arrays and arrays of structures to be able to represent a much broader set of real-world things. In doing so, we can model them and then manipulate them with C programs. Complex representations of real-world things are often called data structures.

Creating a data structure adds significantly to the meaning and context of each of the values that comprise it. For example, a value that represents a length is not as meaningful alone as a group of values that represents not just the length, but also the width, height, and a set of angles that describes a solid object. With this grouping, each of those values is related in a way that might not be obvious if they were not otherwise grouped together. The data structure becomes a logical representation of the real-world object.

The core concept of programming real-world things involves two levels of abstraction:

  • To represent the thing in a minimal but essential manner
  • To manipulate the thing or things in meaningful ways for our purposes

We represent the essential characteristics of the thing with data structures. We are unlikely to represent every characteristic of the thing; we represent only the characteristics of the thing we care to manipulate. We also choose a set of manipulations on the thing—its data structure. Not all manipulations are worthwhile or valid. Some manipulations are simple and may only need to be included in a set—that is, is this like other things, or simple equality? (That is, does one thing have identical qualities to another?) Other manipulations can be much more complex depending on the thing being represented and can involve a single thing or multiple things, such as adding two objects together or determining whether one object is greater than another.

As we learn how to create complex data structures, we will explore how to access the various parts of each kind of data structure and then perform manipulations, treating each data structure as a whole, rather than as individual parts.

Revisiting card4.h

We left Chapter 10, Creating Custom Data Types with typedef, with the card.h program and card5.c, which split card4.c into a header file and an implementation file. However, instead of going further with multiple-file program development, we will return to card4.c and rework a few things to include the knowledge we've gained since Chapter 10, Creating Custom Data Types with typedef. We'll use it to create a series of programs, carddeck.c. carddeck.c will start out simple, but we will continue to modify it until we've added all the complex structures needed. This will remain a single file throughout this chapter. We will see how to logically split up our final carddeck.c program into multiple files and then build it in Chapter 24, Working with Multi-File Programs.

Before we begin adding complex structures to our carddeck.c series of programs, we need to rework card4.c to a simpler starting point. So, let's backtrack a little bit. Make a copy of the card4.cfile and rename the copy carddeck_0.c. If you have been creating each program file in the same directory, now might be a good time to create a folder for the files we will create in this chapter. You might then have a folder path such as the following:

          $HOME/PackT/LearnC/Chapter16
        

Here, $HOME is the home directory for the username you used to log in to your computer.

Instead of using $HOME, you may be accessing your folders with the tilde (~) symbol, as follows:

          ~/PackT/LearnC/Chapter16
        

Both file paths access the same file directory.

You might already have a file organization system with folders named Chapter01 through Chapter15 in your file hierarchy. This sort of organization mirrors the way files are stored in the source code repository. Now, you can move carddeck_0.c from where it was created to ../Chapter16.

carddeck_0.c has a number of structures and functions that we no longer need. Well, not really; we'll add them back later, but they will be different enough that we can delete them for now. Open carddeck_0.c in your editor and remove the struct Hand definition, the function prototypes, all the statements in main(), and all of the function definitions that follow main().

Your file should now consist of two #include statements, the enum Suit and enum Face definitions, a struct Card definition, and an empty main() function. It should look as follows:

#include <stdio.h>
#include <stdbool.h>

typedef enum {
club= 1,diamond,heart, spade
} Suit;

typedef enum {
one = 1,two,three,four,five,six,seven,
eight, nine,ten, jack, queen,king,ace
} Face;

typedef struct
{
Suit suit;
intsuitValue;
Face face;
intfaceValue;
bool isWild;
} Card;

int main( void )
{
}

Save this file and compile it. You shouldn't receive any error or warning messages and the compilation should succeed. Even though the program does nothing, it is still a valid program. We will call this our first known-goodprogram. As we add more to this program, we will move in a stepwise fashion from one known-good program to the next known-good version of our program. In this manner, we can build what will become a complex program step by step and limit the level of possible confusion that might occur between steps.

After decades of programming, my personal experience is that it is always easier, faster, and less painful to progress from one known-good program state to the next known-good program state, adding and changing any aspects of your program as needed until you finally arrive at the desired end. The number of changes made and the lines of code added between steps areusually not trivial (often, a dozen or two of lines of code are needed), but are alsonot so excessive that you have hundreds of lines of new, untested code. If you make too large a step and the compiler fails or the program returns a bad output, the problems arerarelysimple to find.

There is seldom a single issue in that huge mass of code changes. Therein lies the challenge—trying to work through a lot of untried, untested code, such that you don't have a good idea of where to start looking for the problems you've unwittingly introduced. So, we will employ a method of making small, stepwise changes throughout this chapter.

Let's add one more set of changes before we move on. Open carddeck_0.c and add the following to the main() function:

int main( void ) {
Card aCard;

aCard.suit = diamond;
aCard.suitValue = (int)diamond;
aCard.face = seven;
aCard.faceValue = (int)diamond;
aCard.isWile = true;

PrintCard( &aCard );
printf( " " );
}

This should be familiar from earlier chapters. We declare a Card structure and then assign values to each element of the aCard variable structure. We also print the values with the PrintCard() function. But wait—we need to add that function back in. Notice that when we call the function here, we are using the address of aCard, so the function declaration and definition should take a pointer parameter. Add the following function prototype before main():

void PrintCard( Card* pCard );

After the main() function body, add the following function definition:

void PrintCard( Card* pCard ) {
char cardStr[20] = {0};
CardToString( pCard , cardStr );
printf( "%18s" , cardStr );
}

This function is quite a bit different t0 the printCard() function from Chapter 10, Creating Custom Data Types with typedef. Instead of copying the structure via the function parameter, here, we are using a pointer to the structure. Only the address of the structure is copied into the function parameter, not the whole structure.

Before, we used multiple printf() statements in two switch()… statements to print each element of the card. Here, we onlyuseone printf()call.

In this version of PrintCard(), not only is the name slightly different from before, but we are also declaring a character array into which we'll create a string describing the card. The CardToString()function will do that. Then, we'll print the resulting string with printf(). The %18s format specifier constrains the string output to the 18 characters we actually need to see (rather than 20).

So now, with this approach, we need a function prototype for CardToString(). Add the following prototype before main():

void CardToString( Card* pCard , char pCardStr[20] );

Next, add the following function definition to the end of the file:

void CardToString( Card* pCard , char pCardStr[20] )  {
switch( pCard->face ){
case two: strcpy( pCardStr , "2 " ); break;
case three: strcpy( pCardStr , "3 " ); break;
case four:strcpy( pCardStr , "4 " ); break;
case five:strcpy( pCardStr , "5 " ); break;
case six: strcpy( pCardStr , "6 " ); break;
case seven: strcpy( pCardStr , "7 " ); break;
case eight: strcpy( pCardStr , "8 " ); break;
case nine:strcpy( pCardStr , "9 " ); break;
case ten: strcpy( pCardStr , " 10 " ); break;
case jack:strcpy( pCardStr , " Jack " ); break;
case queen: strcpy( pCardStr , "Queen " ); break;
case king:strcpy( pCardStr , " King " ); break;
case ace: strcpy( pCardStr , "Ace " ); break;
default:strcpy( pCardStr , "??? " ); break;
}
switch( pCard->suit ){
case spade: strcat( pCardStr , "of Spades"); break;
case heart: strcat( pCardStr , "of Hearts"); break;
case diamond: strcat( pCardStr , "of Diamonds"); break;
case club:strcat( pCardStr , "of Clubs "); break;
default:strcat( pCardStr , "of ???s"); break;
}
}

This function looks similar to the printCard() function. Instead of using the printf() calls, we are calling strcpy() in the first switch()… statement and strcat() in the second switch()… statement. We use a character array that is large enough to hold all of our card name characters (don't forget the NULL character at the end). In reality, we need a character array that is only 18 bytes in size, but we rounded it up to 20.

Recall how we access the two structure member elements with the -> notation. We are using a pointer to a Card structure in this function to avoid copying structures into function parameters. When structures are complex and/or very large, the use of pointers can be significantly more efficient for function calls, especially if the function may be called hundreds or thousands of times, for instance, or when we have large arrays of possibly large structures.

Note how we created a character array outside of our call to CardToString(), then filled the character array in CardToString(), and then used the constructed string before exiting PrintCard(). We did this because strcpy() and strcat() do not allocate memory for us; we must do it ourselves. We can't allocate memory in CardToString() because it would be deallocated when we leave the function and then unavailable to the printf() statement, where it is needed. So, we need to allocate the character array in the calling function and fill it in the called function so that we can then use it after we return from the called function. The character array is then deallocated after we have used it when we exit PrintCard(). This is a common C pattern for string creation and usage.

The reasons for making these changes to PrintCard() are as follows:

  • First, we get to use some common C string functions in CardToString().
  • Second, rather than print a single card on each line, later on, we will print four cards per line.

The preceding printCard() routine had the new line embedded within it; therefore, it did not allow us to do what we now want.

As a checkpoint, you may want to save this file and compile it. Did it compile? No, because we forgot to include the string.hheader file that makes strcpy() and strcat() visible to our program. Add the following to the top of your file:

#include <string.h>

Save, compile, and run the program. You should see the following output:

There is one final change to this version of carddeck_0.c to be made. Currently, we have two operations on a Card structure—PrintCard() and CardToString(). The last one to add is InitializeCard(). Change main() to the following:

int main( void )  {
Card aCard;
InitializeCard( &aCard, diamond , seven , true );
PrintCard( &aCard );
printf( " " );
}

Next, after main(), add the following function definition:

void InitializeCard( Card* pCard, Suit s , Face f , bool w )  {
pCard->suit = s;
pCard->suitValue = (int)s;
pCard->face = f;
pCard->faceValue = (int)f;
pCard->isWild = w;
}

Notice that we simply moved the statements from main() into the InitializeCard()function. This change mademain()both simpler and clearer. Also, the InitializeCard()function tells us exactly which values we need in order to populate aCardstructure properly.

Save, compile, and run the program. You should see the following output (which is the same as before this change):

To summarize this section, we added three operations to Card for the following reasons:

  • Each of these routines has specific knowledge of the Card structure. Most of the rest of the program doesn't need to know the details and can rely on these operations.
  • The caller of these routines needs to know very little about the Card structure.
  • By focusing the functionality on a single function, we, as programmers, can focus on a smaller set of operations.
  • The interaction between the caller and the called function is well-defined.
  • By using these operations on a given structure, the behavior is consistent and known.
  • If the structure changes, we would need to focus on changes to just the operations on that structure and can ignore large parts of the program. This leads to a more consistent and reliable program operation as the program evolves over time.

These motivations are compelling. As the objects that we model and our programs become more complex, we need to simplify our thinking about solving the larger problem. We can do this by solving a redefined yet smaller set of problems. Here, we are the ones who break down the larger problem into a set of smaller problems. The solution to the larger problem, then, is composed of the set of solutions we made for our smaller problem set. Furthermore, it is easier to modify a small set of routines for changes to a single structure than it is to make these changes in many places over a large program without these routines.

At this point, we have reviewed a wide variety of C concepts that were explored in Chapters 11,Working with Arrays, through Chapter 15,Working with Strings. We have also introduced some very important new programming concepts. In essence, we have taken card4.c and changed it into a more flexible version that does the same things but can more easilybe extended for new functionality. Now would be a good time to review the steps we performed to createcarddeck_0.c before we move on to the next section and look at carddeck_1.c, the next iteration of this program:

  1. Start with card4.c as a starting point for carddeck_0.c by eliminating a bunch of structures and functions. These will be added back later.
  2. Create a function to print a card using a pointer. This function creates a string to be populated by another function and then prints that string.
  3. Create a sub-function whose only purpose is to populate the string given to it with a description of the card, returning that string to the calling function.
  4. Create a function to initialize a card using a pointer. This function centralizes knowledge of the fields needed to create a card.
  5. Rework main() to verify our new functions.

You may notice that except for the new use of pointers in our functions, much of this code is similar to the statements found in card4.c. Pay particular attention to how these have been reworked into more general functions.

If you download the file in the repository, you may notice that the line spacing is different and comments are added. This is done to save space in the text while keeping the program listings valid.

Understanding an array of structures

Before we begin with the next set of changes, make a copy of carddeck_0.c and rename it carddeck_1.c. In this section, we will make changes to carddeck_1.c.

Probably the simplest of the complex structures we will explore in this chapter is an array of structures. Recall that all the elements of an array are of a single type and size. While before, we created arrays of one intrinsic type or another, we will now create an array of one custom type.

Creating an array of structures

In the carddeck_1.c program, we need to model a deck of cards. To do this, we will create an array of the Card structures, as follows:

Card deck[52];

With this statement, we have created an array of 52 cards.

Note how, in the preceding definition, 52 is a magic number; that is, it is a literal number that has a special meaning. However, there is no context associated with that number unless it is stated in the comments. One problem with magic numbers is that as a program evolves or is applied to different uses, the magic numbers aren't all always updated properly. To avoid this problem, we will define some convenience constants, as follows:

enum  {
kCardsinDeck = 52,
kCardsinHand = 5,
kCardsinSuit = 13,
kNumHands = 4
}

This enum statement declares four constants whose values we defined for each one, these are named literal constants. We would like to have used the following:

const int kCardsInDeck = 52;
const int kCardsInHand = 5;
...

We cannot use them since we will need them to declare arrays. C sees a const int as a variable even though it is a read-only one. So, when we declare an array with a constant size, C will not permit the use of aconst intbut will permit the use of a constant.

We can then use this constant, as follows:

Card deck[ kCardsInDeck ];

This statement declares a deck to have 52 cards, the same as before, but now if, for any reason, we add, say, 2 wildcards to our deck or, say, we want to play a game that uses 104 cards (two decks), we need to simply change our constant value and recompile. The benefit of using this constant will become even more obvious when we create methods to manipulate our deck.

While we're at it, let's create a few more convenience constants pertaining to a card, as follows:

const bool kWildCard    = true;
const bool kNotWildCard = false;

The last two Boolean constants make it clear whether a card is wild or not and provide more context than just true or false for the isWild property of a card. We can use the const bool type here because these will not be used in any array declarations.

To carddeck_1.c, add the four constant value definitions before the main() function. Next, add the deck array definition to the main() function. Save and compile the program. You should not see any errors.

Accessing structure elements within an array

Now that we have an array of 52 cards, how do we access, say, the fourth card?

We can do so with the following declaration:

Card aCard = deck[ 3 ];

Here, we've declared a new card variable (a structure)—aCard—and assigned (copied) the structure from the fourth element of deck to it. We now have two copies of the same structure with each member copied in each. If we make any modifications to aCard, they are not made to deck[3] because we are operating on a different structure address than that found at deck[3].

To modify the elements of the structure in the fourth array element directly, we use dot (.) notation, as follows:

deck[3].suit= spade;
deck[3].suitValue = (int)spade;
deck[3].face= five;
deck[3].faceValue = (int)five;
deck[3].isWild= kNotWildCard;

Because of operator precedence, deck[3] is evaluated first, which gives us a structure. The dot (.) notation is then evaluated, which gives us a specific element within the structure.

Modify main() to initialize a single card, as follows:

int main( void ) {
Card deck[ kCardsInDeck ];

deck[3].suit= spade;
deck[3].suitValue = (int)spade;
deck[3].face= five;
deck[3].faceValue = (int)five;
deck[3].isWild= kNotWildCard;

Sometimes, it is either convenient or necessary to use a pointer to access the structure elements in the array. We can do so as follows:

  Card* pCard = &deck[3];
pDeck->suit= spade;
pDeck->suitValue = (int)spade;
pDeck->face= five;
pDeck->faceValue = (int)five;
pDeck->isWild= kNotWildCard;

First, a pointer to a Card structure is created and is assigned the address of the fourth element of the deck. Remember that the target of a pointer must first exist before assigning its address to a pointer. In this case, deck[52] has already been created. Again, because of operator precedence, deck[3] is evaluated, which refers to a Card structure; then, & is evaluated to the address of that Card structure. Using the pointer variable with the arrow (->) notation, we then assign a value to each element of the structure.

We are using a pointer to refer to a single structure location, which is deck[3]. No copies of the structure nor any of its members are created.

This is identical to using a pointer to access a single structure. Using pointers to structures gives us the flexibility to modify the structure within a function call, thereby eliminating the need to copy the structure into the function and then copy it back out after it has been modified in the function.

Now, we can modify main() to initialize the fourth card and print it out, as follows:

int main( void ) {
Card deck[ kCardsInDeck ];

Card* pCard = &deck[3];
pCard->suit= spade;
pCard->suitValue = (int)spade;
pCard->face= five;
pCard->faceValue = (int)five;
pCard->isWild= kNotWildCard;

PrintCard( pCard );
printf( " " );
}

If you make this modification, save and compile the program. You should see the following output:

However, we created the InitializeCard() functionearlier. Let's use that instead, as follows:

int main( void ) {
Card deck[ kCardsInDeck ];
Card* pCard = *deck[3];

InitializeCard( pCard, spade , five , kNotWildCard );
PrintCard( pCard );
printf( " " );
}

As you can now see, the main() function not only has fewer lines of code, but it is also much clearer what is happening with each function call. The only oddity here is the final printf() function. Remember that PrintCard() does not include a new line, so now we have to supply that ourselves.

Save, compile, and run this version of carddeck_1.c. You should see the following output:

At this point, we have an array of cards, which is our deck, and some functions to manipulate individual cards. We can now think about manipulating the entire deck as a whole.

Manipulating an array of structures

Now that we have a deck of cards, what are some operations that we might need to perform on it? Two operations immediately come to mind—first, initializing the deck to the proper suit and face values, and second, printing out the deck of cards.

Let's add the following two function prototypes to the program, as follows:

void InitializeDeck( Card* pDeck );
void PrintDeck(Card* pDeck );

In each function, the function takes a pointer to a Card structure, which for now is an array of Card. These will operate on the entire deck, so no other parameters are needed for these functions.

To initialize our deck of cards, we will loop through the array, setting the structure member values. Before we show that, however, consider the patterns of repetition in an ordered card deck. Each suite has 13 cards. Within those 13 cards, the face value goes from two to ace (13). We now have some options for how to loop through the deck:

  • We could use one loop with 52 iterations and figure out when the suit and face values change, and then set suit and face as appropriate.
  • We could use four loops of 13 iterations each. In each loop, the suit is fixed and the face value is assigned as the loop is iterated.
  • We could use one loop, setting the suit and face values for four cards at a time.

The first option sounds like it might involve some tricky calculations. The second option is reasonable but involves more looping structures (more code). So, we will take the third approach. But how can we conveniently assign the face values without the need for the switch()… statement? The answer is to use a temporary lookup table that is set up so that the index of the card matches the lookup table's face value index.

The following function illustrates both the lookup table and the single loop:

void InitializeDeck( Card* pDeck )
{
Face f[] = { two , three , four , five , six , seven ,
eight , nine, ten, jack , queen , king, ace };
Card* pCard;
for( int i = 0 ; i < kCardsInSuit ; i++ ){
pCard = &(pDeck[ i + (0*kCardsInSuit) ]);
pCard->suit= spade;
pCard->suitValue = (int)spade;
pCard->face =f[ i ];
pCard->faceValue = (int) f[ i ];

pCard = &(pDeck[ i + (1*kCardsInSuit) ]);
pCard->suit= heart;
pCard->suitValue = (int)heart;
pCard->face = f[ i ];
pCard->faceValue = (int) f[ i ];

pCard = &(pDeck[ i + (2*kCardsInSuit) ]);
pCard->suit= diamond;
pCard->suitValue = (int)diamond;
pCard->face = f[ i ];
pCard->faceValue = (int) f[ i ];

pCard = &(pDeck[ i + (3*kCardsInSuit) ]);
pCard->suit= club;
pCard->suitValue = (int)club;
pCard->face = f[ i ];
pCard->faceValue = (int) f[ i ];
}

To understand the code, we need to do the following:

  1. First, we have already seen how arrays and pointers to arrays can be used interchangeably in function parameters. In this function, we use a pointer to the array.
  2. Next, we set up the lookup table for the face values. I would prefer to use the kCardsInSuit constant, but C does not permit that when initializing arrays in this manner. So, we leave it out.
  1. Finally, we create a loop of kCardsInSuititerations. Within this loop, four cards are configured in each iteration of the loop. To do this, the following five statements are executed for each card:
    1. The address of the card structure is calculated from the loop counter and a multiple of the number of cards in a suit. Note that the spades suit starts at index (0*kCardsInSuit); the hearts suit starts at index (1*kCardsInSuit); the diamonds suit starts at index (2 * kCardsInSuit); and clubs start at index (3 * kCardsInSuit). Notice how the pattern of (<suitNumber> * kCardsInDeck) is repeated to make the pattern a bit more explicit for all four suits.
    2. suit is assigned.
    3. suitValue is assigned based on the enumerated suit value.
    4. face is assigned based on the loop index, which corresponds to the lookup table's indexed value.
    5. faceValue is assigned based on the enumerated face value.

But hold on just a minute. Aren't we recreating what a function we've already created does? So, instead, let's use that function and see why doing so is far better. Add the following function to carddeck_1.c:

void InitializeDeck( Card* pDeck )
{
Face f[] = { two , three , four , five , six , seven ,
eight , nine, ten, jack , queen , king, ace };
Card* pC;
for( int i = 0 ; i < kCardsInSuit ; i++ ) {
pC = &(pDeck[ i + (0*kCardsInSuit) ]);
InitializeCard( pC , spade , f[i], kNotWildCard );

pC = &(pDeck[ i + (1*kCardsInSuit) ]);
InitializeCard( pC , heart , f[i], kNotWildCard );

pC = &(pDeck[ i + (2*kCardsInSuit) ]);
InitializeCard( pC , diamond , f[i], kNotWildCard );

pC = &(pDeck[ i + (3*kCardsInSuit) ]);
InitializeCard( pC , club , f[i], kNotWildCard );
}

As you can see, the overall approach is the same, but instead of five statements for each card in the loop, we onlyuse two statements.

We are still using the lookup table and the suit offsets to get the proper card. In this version, however, knowledge of a Card structure remains in the InitializeCard() function. Not only does this require quite a bit less typing, but what is happening should also be much clearer to any reader of this function.

If our deck ever changes, we only need to consider InitializeDeck()and the routines that manipulate it. We can largely ignore theCardmanipulation routines. If, on the other hand, our deck remains the same but we need to change the properties of ourCardstructure, we need only consider theCardmanipulation functions and possibly any changes to theCardfunction calls.

Our final change to carddeck_1.c is to add the PrintDeck() function. If we merely print one card per line, this routine will be a simple loop of kCardsInDeck iterations with a single call to PrintCard(). However, to print an ordered, unshuffled deck of cards in as few output lines as possible, we will print four cards per line, 13 lines in total.

As we do that, we'll order the suits to each be in a single column. To do that, our approach will be similar to InitializeDeck(), but without the need for a lookup table. Add the following function to carddeck_1.c:

void PrintDeck( Card* pDeck ) {
printf( "%d cards in the deck " ,
kCardsInDeck );
printf( "The ordered deck: " );
for( int i = 0 ; i < kCardsInSuit ; i++ ){
int index= i + (0*kCardsInSuit);
printf( "(%2d)" , index+1 );
PrintCard( &(pDeck[ index ] ) );

index = i + (1*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck[ index ] ) );

index = i + (2*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck[ i + (2*kCardsInSuit) ] ) );

index = i + (3*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck[ index ] ) );

printf( " " );
}
printf( " " );
}

The first two printf() calls provide some information about the deck. Then, we see the same kind of loop as we have in InitializeDeck(). For each suit, the following three statements are executed:

  1. Compute the index into the deck array from the loop index and a suit multiplier.
  2. Print the number of the card in the deck. Note here the one-off adjustment from a zero-based array index to a natural counting number (that darn one-off issue again).
  3. Call PrintCard() to print the card.

A <newline> character is printed after each of the four cards are printed. Finally, two new lines are printed after the loop iterations are complete.

Add this function to the end of carddeck_1.c. Save the file, and then compile and run it. You should see the following output:

The main reason we are printing out the entire deck along with each card's position in the deck array is to verify our deck initialization routine. Each routine proves the validity of the other. When this program was being developed, both routines were developed in tandem so that any errors could be caught and fixed as early as possible.

So, let's quickly review what we did to carddeck_1.c:

  1. We created a deck of cards from an array of Card structures.
  2. We added some convenience constants to name and thereby eliminate magic numbers from being sprinkled throughout our program.
  3. We created a function to initialize our deck of cards array.
  4. We created a function to print our deck of cards.
  5. We verified that we have a valid deck of properly initialized cards.

Now that we know that we have a properly initialized deck of cards, we can move on to do more interesting things with it. We are not done with our deck, though; it will become an even more complex structure. Before we get to that, however, let's expand on what we can put into structures.

Using a structure with other structures

Incard4.c from Chapter 10, Creating Custom Data Types with typedef, we saw a structure, Hand, that contains another structure, Card. However, in that program, we accessed the entire sub-structure. We assigned the hand.cardsub-structure by copying an entireCardstructure to it. While this is convenient if we are dealing with complete substructures, we also need to know how to access elements within the sub-structure of a structure.

Here, we are going to look at accessing sub-structure elements within a structure. Before we begin our exploration, copy the carddeck_1.cfiletocarddeck_2.c. Incarddeck_2.c, we'll add theHandstructure with sub-structures and operations onHand.

Creating a structure consisting of other structures

We have already seen how to create a Hand structure that consists of the Card structures, as follows:

typedef struct {
int cardsDealt;
Card card1;
Card card2;
Card card3;
Card card4;
Card card5;
} Hand;

Hand is a structure that represents a collection of cards dealt with that hand. In this case, Hand contains five individual instances of a Card structure, each of them named card1 through card5. The cardsDealtmember variable allows us to keep track of how many cards are in a hand.

Add the preceding structure definition to carddeck_2.c.

When you consider this structure, you might wonder why an array of Card structures is not used instead. Since an array is a collection of identical times, an array might be more appropriate than five named variables. In reality, using an array is an approach we will take later. For now, we want to explore accessing structures within structures. In the next section, after exploring structures within structures, we will then modify carddeck_2.c to use arrays of structures.

Accessing structure elements within the structure

We declare an instance of a Hand structure as follows:

Hand  h1;

We can then access its sub-structure member elements and structures, as follows:

h1.cardsDealt = 0;
Suit s;
Face f;

h1.card5.suit = club;
h1.card5.face = ace;

s = h1.card5.suit;
f = h1.card5.face;

Note that the card5sub-structure is accessed using dot (.) notation and that the elements of card5 are also accessed using another level of dot (.) notation. In the example given, the member values of card5 are first set to desired values. Then, those values are retrieved from card5 and stored in the s and fvariables, respectively.

Using a pointer to the Hand structure, h1, we access each substructure member element, as follows:

Hand* pHand = &h1;

pHand->card5.suit = club;
pHand->card5.face = ace.

s = pHand->card5.suit;
f = pHand->card5.face;

Note that when accessing the sub-structure elements in this manner, the pointer points to the structure and not the sub-structure. As before, in the example given, the member values of card5 are first set to the desired values. Then, those values are retrieved from card5 and stored in the s and fvariables, respectively.

Alternatively, we could use a pointer not to the structure, but directly to the Card sub-structure and access its elements, as follows:

Card* pCard = &h1.card5;

pCard->suit = club;
pCard->face = ace.

s = pCard->suit;
f = pCard->face;

Here, the pCardpointer points directly to the sub-structure. Note how the pointer to the sub-structure is assigned. The address of the & operator has lower precedence than the dot (.) operator so that h1.card5 is evaluated first and then the address of that Card sub-structure is assigned to the pointer. The card5 elements are then accessed directly via the pointer.

Using this method, we could reuse the Card function, InitializeCard(), as follows:

Card* pCard = &h1.card5;

InitializeCard( pCard , club , ace , kNotWildCard );

Note that by using a pointer directly to the sub-structure, we can use the functions we've already created to operate on that sub-structure as if it were a standalone structure. Here, card5 is a sub-structure of h1; by using a pointer to the sub-structure, we can call InitializeCard(), regardless of what structure contains it.

Of the two methods given—one using a pointer to the structure containing the sub-structure and the other using a pointer directly to the sub-structure—neither is necessarily better than the other. However, because we have already created functions to manipulate a Card structure, there is some benefit in reusing those methods that operate on a Card structure, rather than the structure that contains them. One advantage of using those methods is that they concentrate the knowledge about the internal members and their inter-relationship in a single location—the InitializeCard() function.

Manipulating a structure consisting of other structures

The operations we want to perform on a Hand structure include the following:

  • InitializeHand(): Sets the initial values of a Hand structure to a valid state
  • AddCardToHand(): Receives a dealt card from the deck to the hand
  • PrintHand(): Prints out the contents of the hand

With our current definition of Hand, we will need a way to determine which card in the hand we want to manipulate. To do this, we need a function:

  • GetCardInHand(): Gets a pointer to a specific card within the hand. This method is used by other Hand functions to both set card values within a hand and to retrieve card values.

You can add the following function prototypes to carddeck_2.c:

voidInitializeHand( Hand* pHand );
voidAddCardToHand(Hand* pHand , Card* pCard );
voidPrintHand(Hand* pHand , char* pHandStr , char* pLeadStr );
Card* GetCardInHand(Hand* pHand , int cardIndex );

Each of these methods takes a pointer to a Hand structure. We can now implement each of these functions. With the current definition of Hand, there is not much to initialize, only the number of cards dealt. So, InitializeHand() is as follows:

void InitializeHand( Hand* pHand ) {
pHand->cardsDealt = 0;
}

The cardsDealtstructure member is set to0to indicate that the given hand is empty. We should initialize the cards to some value, but we don't at this time. The cards will be initialized when a card is added to the hand. With this approach, we have to be extra cautious that we don't access a card in the hand that has not yet been initialized.

The GetCardInHand()functionis given a pointer to a hand and an index. It then returns a pointer to the desired card, as follows:

Card* GetCardInHand(Hand* pHand , int cardIndex ) {
Card* pC;
switch( cardIndex ) {
case 0:pC = &(pHand->card1); break;
case 1:pC = &(pHand->card2); break;
case 2:pC = &(pHand->card3); break;
case 3:pC = &(pHand->card4); break;
case 4:pC = &(pHand->card5); break;
}
return pC;
}

In this function, a switch()… statement is used to determine from the index which card is desired. Since the & and -> operators have the same precedence, we use () to make it clear in the &(pHand->card1)expression what type of pointer is being returned.pHand->card1gets theCardstructure and&gives the address of thatCardstructure.

Note that this is a zero-based index that works identically to the way we use indices in arrays. This is intentional. Every time we use an index, regardless of its data type (or array or element within a structure), we are using a zero-based index scheme. This is for mental consistency. We don't have to try to remember when and for which data type we have a zero- or one-based index scheme; they are all zero-based.

We can now implement the AddCardToHand() function. It takes a pointer to a Hand structure and a pointer to a Card structure to be added to the hand, as follows:

void AddCardToHand( Hand* pHand , Card* pCard ) {
int numInHand = pHand->cardsDealt;
if( numInHand == kCardsInHand ) return;

Card* pC = GetCardInHand( pHand , numInHand );
InitializeCard( pC , pCard->suit , pCard->face , pCard->isWild );

pHand->cardsDealt++;
}

The function first checks to see whether the hand is full by accessing the cardsDealt member and checking whether it is equal to the kCardsInHandconstant. If the hand is full, the function returns and nothing more is done; in effect, the card is ignored. If the hand is not full, we usenumInHandas the index to the card in the hand to be added. We then callInitializeCard()with the values from theCardpointer passed into the function.

Note that we are copying values from the deck into the hand. In essence, we have two of the same card—one in the deck and another in our hand. This is not a good design; ideally, we'd onlywant to ever have a single card that would be moved around from deck to hand. We'll soon rectify this condition.

Finally, the cardsDealt member variable is incremented to indicate that a card was added to the hand.

For PrintHand(), a string is passed to the function that will provide spacing before each card is printed. Like other Hand functions, a pointer of the hand to be printed is passed to the function, as follows:

void PrintHand( Hand* pHand , char* pHandStr , char* pLeadStr ) {
printf( "%s%s " , pLeadStr , pHandStr );
for( int i = 0; i < pHand->cardsDealt ; i++ ) {
Card* pCard = GetCardInHand( pHand , i );
printf("%s" , pLeadStr );
PrintCard( pCard );
printf(" ");
}

In this function, a heading line is printed that contains the leading string and the hand-name string. Then, a loop is used to print each Card structure that has been dealt with. Within this loop, a pointer to the card is fetched, the leading string is printed, PrintCard() is called to print the card, and finally, a new line is printed.

At this point, edit carddeck_2.c to include the Hand structure, as well as the four functions to manipulate the Hand structure. Save the file and compile it. You shouldn't get any compiler errors or warnings. We will make a few more changes to this file before we run the program.

The first change to make is to the Hand structure so that it contains pointers to cards, rather than copies of cards. The Hand structure is now as follows:

typedef struct {
int cardsDealt;
Card* pCard1;
Card* pCard2;
Card* pCard3;
Card* pCard4;
Card* pCard5;
} Hand;

Each card in a hand is now a pointer to a card created and initialized in the deck. With this modification, we will now only have one instance of each card; each hand will simply point to the appropriate card in our deck.

Since we have changed the Hand structure, we also need to change some aspects of the functions that manipulate a hand. First, we need to do a little more work in InitializeHand(), as follows:

void InitializeHand( Hand* pHand ) {
pHand->cardsDealt = 0;
pCard1 = NULL;
pCard2 = NULL;
pCard3 = NULL;
pCard4 = NULL;
pCard5 = NULL;
}

Each pointer value is initialized to NULL. This approach has the added benefit of being able to tell whether a card has been dealt just by checking whether the pointer is NULL.

Since we changed the structure member names from card<x> to pCard<x>, we need to make the corresponding name changes in GetCardInHand(), as follows:

Card** GetCardInHand(Hand* pHand , int cardIndex ) {
Card** ppC;
switch( cardIndex ) {
case 0: ppC = &(pHand->pCard1); break;
case 1: ppC = &(pHand->pCard2); break;
case 2: ppC = &(pHand->pCard3); break;
case 3:ppC = &(pHand->pCard4); break;
case 4:ppC = &(pHand->pCard5); break;
}
return ppC;
}

In this function, we don't return the pointer to a card, but rather the address of the variable that contains that pointer. Therefore, we don't need pCard<x>, a pointer to one of the cards in the hand; we need the address of that card pointer. We need a pointer to the pointer variable so that we can change that pointer variable. To see why, change the AddCardToHand()function, as follows:

void AddCardToHand( Hand* pHand , Card* pCard ) {
int numInHand = pHand->cardsDealt;
if( numInHand == kCardsInHand ) return;

Card** ppC = GetCardInHand( pHand , numInHand );
*ppC = pCard;
pHand->cardsDealt++;
}

In this function, we want the pointer variable in our hand to point to the desired card in the deck. In order to do this, we need the address of the pointer variable. This is double indirection at work.

A conceptually simpler way to achieve the same result would be to write this function as follows:

void AddCardToHand( Hand* pHand , Card* pCard ) {
int numInHand = pHand->cardsDealt;
if( numInHand == kCardsInHand ) return;

switch( numInHand ) {
case 0: pHand->pCard1 = pCard; break;
case 1: pHand->pCard2 = pCard; break;
case 2: pHand->pCard3 = pCard; break;
case 3: pHand->pCard4 = pCard; break;
case 4: pHand->pCard5 = pCard; break;
default: break;
}
pHand->cardsDealt++;
}

In this function, we do not need double indirection. Instead, we manipulate the appropriate pointer element of our Hand structure. This requires much more code than that used for double indirection. Remember, the main reason we are using the GetCardInHand() function is because of the way the Hand structure is currently defined. For this iteration of carddeck_2.c, we will use the double indirection method.

The next two changes to be made to carddeck_2.c will enable us to verify our current changes by providing some useful output. The first of these changes is to add a new method to our set of Deck functions—DealCardFromDeck()—as follows:

Card* DealCardFromDeck( Card deck[] , int index ) {
Card* pCard = &deck[ index ];
return pCard;
}

This function takes a deck array and an index for the desired card in the deck. Alternatively, we could have declared it as a pointer to Card, but we use array notation here instead to indicate the underlying array of our current deck representation. The function then returns a pointer to the requested Card structure in the given deck at the given index.

Our last change is to put these new methods to use. Modify main(), as follows:

int main( void ) {
Card deck[ kCardsInDeck ];
Card* pDeck = deck;
InitializeDeck( &deck[0] );

Hand h1 , h2 , h3 , h4;
InitializeHand( &h1 );
InitializeHand( &h2 );
InitializeHand( &h3 );
InitializeHand( &h4 );

for( int i = 0 ; i < kCardsInHand ; i++ ) {
AddCardToHand( &h1 , DealCardFromDeck( pDeck , i) );
AddCardToHand( &h2 , DealCardFromDeck( pDeck , i+13 ) );
AddCardToHand( &h3 , DealCardFromDeck( pDeck , i+26 ) );
AddCardToHand( &h4 , DealCardFromDeck( pDeck , i+39 ) );
}

PrintHand( &h1 , "Hand 1:" , "" );
PrintHand( &h2 , "Hand 2:" , "" );
PrintHand( &h3 , "Hand 3:" , "" );
PrintHand( &h4 , "Hand 4:" , " " );
}

Here, we declare and initialize our deck array as before. Then, four Hand structures are declared and initialized. Next, using a loop, each hand is dealt cards to fill each one by calling AddCardToHand(). Currently, the cards that are being dealt are not randomized. Instead, we use suit offsets and the loop index to deal with the first five cards of each suit to each hand. We will deal with random card selection in the next iteration of carddeck.c. Finally, each hand is printed with calls to PrintHand().

In carddeck_2.c, make the changes to the Hand structure and the functions that manipulate a hand. Add the function prototype and function definition for DealCardFromDeck(). Finally, modify main() to create four hands, initialize them, add cards to them, and print them out. Compile and run carddeck_2.c. You should see the following output:

You can now see how the PrintHand() function uses the various parameters it takes to print out each hand in a specific place on the console screen. Hand 1 holds the first five cards from the spades suit, just as the other hands hold the first five cards from each of the other suits.

In this iteration of carddeck.c, we have made the following changes:

  1. Create a Hand structure consisting of Card structures.
  2. Create functions to initialize a hand, add a card to a hand, print a hand, and get a pointer to a specific card in a hand. In these functions, the cards were manipulated as copies of cards in the deck.
  3. Modify the Hand structure to use pointers to Card structures, rather than copies of Card structures.
  4. Modify each of the Hand functions to use pointers to match the new structure definition.
  5. Add the DealCardFromDeck() function to get a pointer to Card from the deck.
  6. Modify main() to use the Hand structures and each of the Hand manipulation functions.

We now have a deck with properly initialized cards and four hands that we can populate with pointers to cards from our deck. The program is becoming more complex as well as more complete. We'll soon have a complete card-dealing program.

Using a structure with arrays

We currently use an array of cards to represent a deck of cards. However, this representation is not sufficient for things we still need to do to a deck of cards. Two operations that are common to a deck of cards are first to shuffle the deck into a random order, and second to properly deal out cards from the randomized deck. We'll need to keep track of how many cards have been dealt and whether the deck is shuffled.

Our model for a deck of cards has just got a bit more complex. A single array representation is no longer sufficient. We will create a new structure, Deck, to hold additional information about our deck of cards as well as its shuffled or random state.

Before we begin defining this structure and operations on it, let's consider the randomization (shuffling) of our deck of cards. We could randomize our deck array by copying the structures in it from one index to another. However, since we now know about pointers, we can put that knowledge to good use by employing another array to shuffle our deck. This array will consist of pointers to each card in our deck. We will initialize the array so that each pointer points to its ordered deck element; then, we will randomize the order of the pointers in this secondary array. In order to do that, we first need to understand a little about randomness and random number generators on computers.

Understanding randomness and random number generators

A computer is a deterministic machine. This means that when we run a program, we get the same result each time without variance. This consistent behavior is crucial to orderly computation. We depend on consistency regardless of the day of the week, the weather, or any other factor.

However, there are some cases where we need to simulate the random occurrence of events. One obvious example of this is shuffling a deck of cards. It would be of little interest to create a card game program that gives each player exactly the same cards each time they play the game. On the other hand, randomness is pervasive in the real world—the weather, rolling dice, even the fingerprints on our hands, are all unavoidable random events.

To get randomness on an otherwise non-random, deterministic machine, there are two ways to achieve this. The first is via hardware; the best source of this is the static generated by a purposely damaged sound-generating chip. This kind of device is truly random. Also, this device is neither practical nor readily available on common computer systems. The second way is to simulate randomness with a pseudorandom number generator (PRNG).

A PRNG is an algorithm that generates a very large sequence of numbers. This large sequence is called its period, or the periodicity of the PRNG. This is the length of the sequence of numbers it can generate before it repeats its sequence. The larger the periodicity, the better the PRNG. Each time we ask the PRNG for a random number, it actually gives us the next number in its sequence. Associated with a PRNGs periodicity is a seed, or a starting point within its sequence of numbers. A seed is itself some type of varying number that doesn't have to be nearly as random as the PRNG sequence. It could be the number of seconds since 1970, the number of microseconds within the current second, or the position of the disk head over a hard drive platter. The seed is our starting point in the PRNG's sequence of numbers. In fact, if we used the same seed each time, we would always get exactly the same sequence of numbers.

From this, we can surmise that there are two operations on a PRNG that are essential. First, we must initialize the PRNG with a seed, or starting point, and second, we make repeated calls to the PRNG to give us the next random number in its sequence.

Every computer system provides at least one PRNG that is readily available to the programmer. However, PRNGs have been, and are still being, studied extensively because of their importance in simulations of real-world events. There are many classes of PRNGs. Not every PRNG is equally random from one call to the next, nor does every PRNG have the same periodicity. Some PRNGs are simple while others are quite complex. The value a PRNG returns may be an integer between 0 and some maximum value or it may be a floating-point value between 0.0 and 1.0. We can then normalize that value into our desired range of numbers.

On the other hand, not every problem requires the same level of randomness. Very often, for simple games, a simple PRNG is adequate for the task. We will use the relatively simple PRNG supplied in the C standard library by including stdlib.h in our program. We can then initialize its PRNG with srand() and make subsequent calls to it with the rand() function. It is common to initialize srand() with the current time by calling time(). time() returns the number of seconds since 1970 on Unix systems; therefore, this will be a different number each time we run a program that uses it.

We will see how to use these functions in action when we shuffle our deck.

Creating a structure with an array

We will create a structure, Shuffled, which, for now, holds some information about the state of our deck and an array of pointers to Card, which is our shuffled deck. We define Shuffled as follows:

typedef struct {
Card* shuffled[ kCardsInDeck ];
int numDealt;
bool bIsShuffled;
} Shuffled;

This gives an array of pointers to Card, the number of cards that have been dealt, and a Boolean value to indicate whether the deck has been shuffled.

Accessing array elements within a structure

We have already seen how to access individual elements within a structure with dot (.) notation, or, if we have a pointer to the structure, with the arrow (->) notation. To access an array element with a structure, we would use the same notation for individual elements and simply add array notation ([]), as follows:

Shuffled  aShuffled;
Shuffled* pShuffled = &aShuffled;

aShuffled.numDealt = 0;
aShuffled.bIsShuffled = false;
for( int i = 0 , i < kCardsInDeck; i++ )
aShuffled.shuffled[i] = NULL;

pShuffled->numDealt = 0;
pShuffled->bIsShuffled = false;
pShuffled->shuffled[i] = NULL;

We have declared a deck structure,aShuffled, and a pointer to a deck structure,pShuffled, initializing it to the address ofaDeck. The next three statements access each element of aShuffledusing dot (.) notation. The last three statements access each element of pShuffledusing arrow (->) notation.

We can now consider operations executed on our Shuffled structure.

Manipulating array elements within a structure

We already have a Deck array and two operations in it InitializeDeck()andPrintDeck(). We now also have a Shuffled structure. We need to add operations to perform on it, such asInitializeShuffled()andPrintShuffled(). To this set of operations, we would add theShuffleDeck()function. The function prototypes for these would be as follows:

void InitializeShuffled( Shuffled* pShuffled , Deck[] pDeck ); 
void PrintShuffled( Shuffled* pShuffled );
void ShuffleDeck( Shuffled* pShuffled );

The InitializedShuffled()method isa bit different from InitializeDeck() because the function needs to know aboutDeckwhen we initialize our array of pointers. At this point, you might be wondering whetherDeckand its operations aresomehow very closely related toShuffledand its operations. The fact is, they are. We will combine both theDeckandShuffleddata structures as well as these operations in the final version of thecarddeck.cprogram. Before we do this, however, let's examine theShuffleDeck()function.

To shuffle our deck of cards, we will—for now—assume thatshuffled[] has been initialized such that the first element of shuffled[o] points to the first element of deck[0], the second element of shuffled[1] points to the second element of deck[1], and so on, such that shuffled[] is in the same order as deck[]. We will first initialize our PRNG and then loop through each element of the array. At each iteration, we will call rand() to get the next random number and normalize that number into a range between 0 and 51 (the number of cards in our deck). This will be a random index in our shuffled array. We'll then swap the pointer at the current index with the pointer at the random index. This function is as follows:

void ShuffleDeck( Shuffled* pDeck ) {
long randomIndex;
srand( time() );

Card* pTempCard;
for( int thisIndex = 0 ; thisIndex < kCardsInDeck ; thisIndex++ ) {
randomIndex = rand() % kCardsInDeck; // 0..51
// swap
pTmpCard = pDeck->shuffled[ thisIndex ];
pDeck->shuffled[ thisIndex ] = pDeck->shuffled[ randomIndex ];
pDeck->shuffled[ randomIndex ] = pTmpCard;
}
pDeck->bIsShuffled = true;
}

First, we declare randomIndex and then initialize our PRNG with a call to time(). In order to swap two values, we use a third value, pTempCard, as a placeholder. Upon each iteration through shuffled[], we get the next random number, use modulo to get a value between 0 and 51, and then perform the swap operation in three statements. Finally, we update the bIsShuffledstatus variable to reflect the fact that we now have a shuffled array of cards.

We can see that accessing an array of simple data types within a structure is very similar to accessing any member element within a structure. We simply add an array notation to the structure's array name as needed.

Copy carddeck_2.c into carddeck3.c. For the remainder of this chapter, we will modify carddeck_3.c. For now, add the stdlib.h and time.h header files to our program to access rand(), srand(), and time().

We will modify ourHandstructure to use an array of pointers toCard structures. This will simplify operations on aHandas well as eliminate the need for theGetCardInHand()function.

Revisiting the hand structure

In carddeck_2.c, we used named member variables for cards in the Hand structure. This meant we needed a function to get a pointer to a specific card. While possible, that turned out to be cumbersome. Recall that when all variables are of an identical type and you have a collection of them, an array should immediately come to mind. So, we will rework the definition of our Hand structure to use an array of pointers to Card, as follows:

typedef struct {
Card* hand[ kCardsInHand ];
int cardsDealt;
} Hand;

We still have five cards, but they are now contained in an array, and we still have cardsDealt to keep track of how many cards are in our hand.

As you will see, this will simplify our Hand operations.

Revisiting hand operations

Because we changed this structure definition, we will have to only slightly modify the Hand operations. Fortunately, none of the function prototypes will need to change:

  1. In carddeck_3.c, modify the InitializeHand() function, as follows:
void InitializeHand( Hand* pHand ) {
pHand->cardsDealt = 0;
for( int i = 0; i < kCardsInHand ; i++ ){
pHand->hand[i] = NULL;
}
}

We can now use a loop to set each of our Card pointers in the hand[] array to NULL.

  1. Next, we can simplify the AddCardToHand() function because we no longer need the GetCardInHand() method, as follows:
void AddCardToHand( Hand* pHand , Card* pCard ) {
if( pHand->cardsDealt == kCardsInHand ) return;
pHand->hand[ pHand->cardsDealt ] = pCard;
pHand->cardsDealt++;
}

As before, we first check whether our hand is full. Then, we simply set the pointer value to the given Card structure to the appropriate array element. As before, the cardsDealt member variable is incremented.

  1. Also, the PrintHand() function no longer needs to use the GetCardInHand() function, as follows:
void PrintHand( Hand* pHand , char* pHandStr , char* pLeadStr ) {
printf( "%s%s " , pLeadStr , pHandStr );
for( int i = 0; i < kCardsInHand ; i++ ) { // 1..5
printf("%s" , pLeadStr );
PrintCard( pHand->hand[i] );
printf(" ");
}
}

Because the cards in the hand are in an array, we can simply access pointers to them via array notation. Otherwise, this function is identical to its earlier version. GetCardInHand() is no longer needed. Both its prototype and definition can be deleted from carddeck_3.c.

  1. Finally, we will put our collection of four hands into an array of pointers to Hand. We can then create a PrintAllHands() function, as follows:
void PrintAllHands(Hand* hands[ kNumHands ] ) {
PrintHand( hands[0] , "Hand 1:" , "" );
PrintHand( hands[1] , "Hand 2:" , "" );
PrintHand( hands[2] , "Hand 3:" , "" );
PrintHand( hands[3] , "Hand 4:" , "");
}

Each element of the hands[]arraycontains a pointer to a Hand structure. We can, therefore, simply use the array and index to callPrintHand()for each of our hands. Remember to add thePrintAllHands()function prototype tocarddeck_3.c. Save and compile the program. You should get an identical output to that of carddeck_2.cin the previous section.

Using a structure with an array of structures

Because a deck of cards and a shuffled deck of cards are so similar, it makes somewhat more sense to combine them into a single structure, rather than have to declare and manipulate them separately. Our final Deck structure will consist of two arrays—one of an ordered set of cards and another of pointers to cards in that deck, which can then be shuffled as needed. We will add additional information to the Deck structure to keep track of whether the deck is shuffled and how many cards have been dealt.

As we enhance our Deck structure and create/modify operations on the new structure, you should notice how little any of the other structures and methods already created will need to be changed, if at all.

Creating a structure with an array of structures

In earlier versions of carddeck.c, we represented a deck of cards with a simple array of structures. Now that we need to shuffle the deck and keep track of other information about the deck, it makes sense to make our Deck structure a combination of an array of Card structures and an array of pointers to Card structures, as well as other information about the deck.

Consider the following definition of a new Deck structure:

typedef struct {
Cardordered[ kCardsInDeck ];
Card* shuffled[ kCardsInDeck ];
int numDealt;
boolbIsShuffled;
} Deck;

The orderedmember array will contain the initialized and orderedCardstructures. Once initialized, the orderedarray elements will not be modified. Instead of moving cards around in theorderedarray, we will use another array,shuffled, which is a collection of pointers to cards in theorderedarray. We will rely on the bIsShuffledmember variable to indicate when the deck has been shuffled.

This new definition collects various types of complex information into a single, somewhat more complex structure. We will see how this makes our program organization and logic more cohesive.

Before we delve into modifying operations on the Deck structure, let's explore how to access the various elements and sub-elements within the Deck structure.

Accessing individual structure elements of the array within a structure

Accessing structure elements and sub-elements is a matter of layering access from the topmost, or outer structure elements, to the bottommost, or inner sub-structure elements. We have already seen simpler versions of this in the earlier sections of this chapter.

We must, however, be mindful of the data type of the sub-element being accessed. In particular, we must pay attention to whether the member element is a direct intrinsic type or structure or whether it is a pointer to another intrinsic type or structure. Being clear about differentiating member elements and pointer elements determines which notation is required—dot (.) or arrow (->) notation.

Given the preceding definition of Deck, consider the following access to each of its various elements and sub-elements:

Deck deck;

deck.cardsDealt = 0;
deck.bIsShuffled = false;
deck.shuffled[0] = NULL;
deck.ordered[3].suit = spade;
deck.ordered[3].face = four;

deck.shuffled[14] = &(deck.ordered[35]);
(deck.shuffled[4])->suit = heart;
(deck.shuffled[14])->face = two;

Suit s = deck.ordered[3].suit;
Face f = deck.ordered[3].face;

s = (deck.shuffled[14])->suit;
f = (deck.shuffled[14])->face;

The deckvariableis declared as aDeckstructure. The next two statements set two elements of the structure. Next, the zeroth element of theshuffledarray is set toNULL. Then, the sub-elements of the fourth Cardstructure element are set.

The next statement sets the 14th pointer element of shuffled to the address of the 35th structure element of the ordered array. The next two statements access, via the pointer contained in the 14th element of shuffled, the suit and face elements, which are actually the sub-elements of the 35th array structure in ordered.

The last four statements show how to retrieve sub-element values.

Now, consider how to access the sub-elements of the Deck structure via a pointer to the Deck structure in the following statements:

Deck  anotherDeck
Deck* pDeck = &anotherDeck;

pDeck->cardsDealt = 0;
pDeck->bIsShuffled = false;
pDeck->shuffled[3] = pDeck

pDeck->shuffled[14] = &(deck.ordered[31]);
(pDeck->shuffled[14])->suit = heart;
(pDeck->shuffled[14])->face = two;

Suit s = pDeck->ordered[3].suit;
Face f = pDeck->ordered[3].face;

s = (pDeck->shuffled[14])->suit;
f = (pDeck->shuffled[14])->face;

Each of these statements—which are indirect references via a pointer to a Deck structure—has the identical effect of accessing sub-elements as the previous set of direct references to the same sub-elements.

Manipulating a structure with an array of structures

So, a question regarding when to use direct references versus using an indirect reference may come to mind. This is a very pertinent question. Unfortunately, there is no obvious answer or one that must be strictly obeyed.

In general, however, whenever a structure is declared in a function block and its elements are accessed within that function block, direct references typically make the most sense. On the other hand, when structures are declared in one function block and then manipulated by another function block, it is typically best to use indirect references (pointers) to them in the manipulating function called.

With this in mind, we can now redefine the prototypes to the Deck manipulation operations, as follows:

voidInitializeDeck( Deck* pDeck );
voidShuffleDeck(Deck* pDeck );
Card* DealCardFromDeck( Deck* pDeck );
voidPrintDeck(Deck* pDeck );

In each case, the Deck structure is not copied into and back out of the functions. Instead, a pointer to the already existing structure variable is passed into each manipulating function.

We will revisit each of these functions in the remainder of this chapter.

Completing carddeck.c

In this section and its sub-sections, we will complete carddeck.c so that it creates a deck of cards using our new Deck structure, shuffle the deck, create four hands, and deal five shuffled cards to each hand. To verify our work, we will print the deck and the hands out at various stages of the program's execution.

Revisiting the deck structure

To carddeck_3.c, add the definition for the Deck structure, as follows:

typedef struct {
Cardordered[ kCardsInDeck ];
Card* shuffled[ kCardsInDeck ];
int numDealt;
boolbIsShuffled;
} Deck;

This is the complex Deck structure that we described earlier.

Revisiting deck operations

Since we now have a complex Deck structure, we must revisit each of the functions that operate on a deck:

  1. The first of these is InitializeDeck(). Modify InitializeDeck(), as follows:
void InitializeDeck( Deck* pDeck ) {
Face f[] = { two , three , four , five , six , seven ,
eight , nine, ten, jack , queen , king, ace };
Card* pC;
for( int i = 0 ; i < kCardsInSuit ; i++ ){
pC = &(pDeck->ordered[ i + (0*kCardsInSuit) ]);
InitializeCard( pC , spade , f[i], kNotWildCard );

pC = &(pDeck->ordered[ i + (1*kCardsInSuit) ]);
InitializeCard( pC , heart , f[i], kNotWildCard );

pC = &(pDeck->ordered[ i + (2*kCardsInSuit) ]);
InitializeCard( pC , diamond , f[i], kNotWildCard );

pC = &(pDeck->ordered[ i + (3*kCardsInSuit) ]);
InitializeCard( pC , club , f[i], kNotWildCard );
}

for( int i = 0 ; i < kCardsInDeck ; i++ ){
pDeck->shuffled[i] = &(pDeck->ordered[i]);
}
pDeck->bIsShuffled = false;
pDeck->numDealt= 0;
}

We use the same lookup array as we did before. We use the same loop that initialized four cards for each iteration through the loop. The only difference in this loop is how we use the card in the ordered array, with &(pDeck->ordered[ i + (0*kCardsInSuit) ]).

  1. Next, we initialize the shuffled array to have pointers to cards in the same order as in ordered. The deck is not yet shuffled, but it is initialized. Lastly, we set bIsShuffled to false and numDealt to 0. The deck is now properly initialized.

To shuffle our deck, we are actuallyonly going to change the order of the pointers inshuffled. Create theShuffleDeck()function, as follows:

void ShuffleDeck( Deck* pDeck ) {
long randIndex;
srand( time(NULL) ); // Seed our PRNG using time() function.
// Because time() ever increases, we'll get a
// different series each time we run the
// program.

Card* pTmpCard;

// Now, walk through the shuffled array, swapping the pointer
// at a random card index in shuffuled with the pointer at the
// current card index.

for( int thisIndex = 0 ; thisIndex < kCardsInDeck ; thisIndex++ ) {
// get a random index
randIndex = rand() % kCardsInDeck;// get next random number
// between 0..52
// swap card pointers between thisIndex and randIndex
pTmpCard = pDeck->shuffled[ thisIndex ];
pDeck->shuffled[ thisIndex ] = pDeck->shuffled[ randIndex ];
pDeck->shuffled[ randIndex ] = pTmpCard;
}
pDeck->bIsShuffled = true;
}

In this, the randIndex function will be a randomly generated number between 0 and 51. After initializing our PRNG with srand(), we loop through all 52 cards. At each iteration, we get the index of a random card pointer and swap that pointer value with the current index of the loop counter. As you will see, this does exactly what we need. Finally, we set bIsShuffled to true.

  1. Modify DealCardFromDeck(), as follows:
Card* DealCardFromDeck( Deck* pDeck ) {
Card* pCard = pDeck->shuffled[ pDeck->numDealt ];
pDeck->shuffled[ pDeck->numDealt ] = NULL;
pDeck->numDealt++;
return pCard;
}

In this version of DealCardFromDeck(), we use the numDealt member variable as the index of the next card pointer to be dealt or returned from the function. Notice that the pointer at that index is set to NULL to ensure we don't deal that card again. Next, numDealt is incremented to the top of the deck or the next available card. Finally, pCard, the pointer to the dealt card, is returned to the caller.

  1. Finally, modify the PrintDeck() function, as follows:
void PrintDeck( Deck* pDeck )  {
printf( "%d cards in the deck " , kCardsInDeck );
printf( "Deck %s shuffled ", pDeck->bIsShuffled ? "is" : "is not" );
printf( "%d cards dealt into %d hands ",pDeck->numDealt,kNumHands );

if( pDeck->bIsShuffled == true ) {// Deck is shuffled.
if( pDeck->numDealt > 0 ){
printf( "The remaining shuffled deck: " );
} else {
printf( "The full shuffled deck: ");
}
for( int i=pDeck->numDealt , j=0 ; i < kCardsInDeck ; i++ , j++ ) {
printf( "(%2d)" , i+1 );
PrintCard( pDeck->shuffled[ i ] );
if( j == 3) {
printf( " " );
j = -1;
} else {
printf( " ");
}
}
} else {// Deck is not shuffled.
printf( "The ordered deck: " );
for( int i = 0 ; i < kCardsInSuit ; i++ ) {
int index= i + (0*kCardsInSuit);
printf( "(%2d)" , index+1 );
PrintCard( &(pDeck->ordered[ index ] ) );
index = i + (1*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck->ordered[ index ] ) );

index = i + (2*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck->ordered[ i + (2*kCardsInSuit) ] ) );

index = i + (3*kCardsInSuit);
printf( " (%2d)" , index+1 );
PrintCard( &(pDeck->ordered[ index ] ) );
printf( " " );
}
}
printf( " " );
}

The PrintDeck() function is now used to print the deck in a few different ways. We have already seen various parts of this function. First, it prints out some information about the current state of the deck. Next, it determines whether the deck is shuffled. If the deck is shuffled, it prints out either the full deck (no cards dealt) or the remaining, undealt portion of the deck. If the deck is not shuffled, it prints out the ordered deck of cards.

Congratulations! You should now be familiar with every syntax element in this rather complex function. You may consider this a complete review of everything you have learned up to this point.

This function will be called several times to verify all parts of our program.

A basic card program

Now that we have all of our new structures and modifications to the functions that operate on them, we are now ready to put everything into play, so to speak. Modify the main() function routine in carddeck_3.c, as follows:

int main( void ) {
Deckdeck;
Deck* pDeck = &deck;

InitializeDeck( pDeck );
PrintDeck(pDeck );

ShuffleDeck( pDeck );
PrintDeck( pDeck );

Hand h1 , h2 , h3 , h4;
Hand* hands[] = { &h1 , &h2 , &h3 , &h4 };
for( int i = 0 ; i < kNumHands ; i++ ) {
InitializeHand( hands[i] );
}

for( int i = 0 ; i < kCardsInHand ; i++ ){
for( int j = 0 ; j < kNumHands ; j++ )
{
AddCardToHand( hands[j] , DealCardFromDeck( pDeck ) );
}
}
PrintAllHands( hands );
PrintDeck( pDeck );
}

Does it come as a surprise how few lines of code in main() are needed to express all of the work that the program is doing? This was achieved through the use of our manipulation functions. Let's walk through it:

  1. First, we declare a deck and a pointer to that deck. Next, the deck is initialized with a call to InitializeDeck() and the deck is printed. When you edit, save, compile, and run this program, you should see the first deck print out as follows:

  1. Then, the deck is shuffled with a call to ShuffleDeck() and printed again. You should see the second deck print out something like the following:

The order of your cards will be different because of our use of a PRNG. Examine the cards closely to be certain that they are all there, that there are no duplicates, and that they are, in fact, shuffled.

  1. Next, four hands are declared and then grouped into an array of pointers to each hand. With a simple loop, InitializeHand() is called for each of them. Next, a nested loop is used to deal cards to each hand. The i index will deal five cards to each hand and the j index of the inner loop distributes the card to the proper hand, in turn. Notice how the return value from DealCardsFromDeck() is used as the input parameter to AddCardToHand().
  1. Finally, all the hands are printed with a call to PrintAllHands() and the final call to PrintDeck(). You should now see something like the following:

Just as in earlier versions of carddeck.c, this version consisted of a number of additions and modifications:

  1. Modify the Hand structure using an array of pointers to Card.
  2. Modify the Hand manipulation functions.
  3. Add a new function to print all hands in an array of pointers to hands.
  4. Create a complex Deck structure.
  5. Modify the Deck manipulation functions.
  6. Use all of our structures and manipulation functions to shuffle a deck and deal to our four hands.

At each stage of the program's development, we went from a known-goodstate to the next known-good state with relevant output at each stage to verify our program's validity. In this way, we were not only able to build our understanding of complex data structures and operations on them, but we also gained some experience and insight into how program development typically occurs. This approach is also referred to as stepwise refinement.

Summary

This chapter not only explored complex structures, but also reviewed nearly all the concepts we've explored in previous chapters.

We learned about various ways to access arrays of structures, sub-structures within structures, and arrays of structures within a structure. Each version of our carddeck.c program included a review of what was changed in that version.

We also learned about PRNGs and used a system-supplied PRNG to shuffle our deck of cards.

Throughout this chapter, we developed a complex program using stepwise refinement as we added structures and operations to these structures. More significantly, we got an in-depth view of how a program might change over its development life cycle. When we add or change structures, we also need to add or change the routines that manipulate those structures. This chapter demonstrated the software development process first described in Chapter 1,Running Hello, World!.

In the next two chapters, we will explore C's various memory allocation mechanisms. Chapter 17, Understanding Memory Allocationand Lifetime, will provide a review of, and a moderate expansion on, the methods we've used so far. It also prepares us for the following chapter, Chapter 18, Using Dynamic Memory Allocation. This chapter while conceptually challenging, paves the way for much more interesting and useful programming algorithms.

..................Content has been hidden....................

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