Chapter 10: Creating Custom Data Types with typedef

As we saw in the last two chapters, C allows you to define your own types from enumerations (enum types) and structures (struct types). C also allows you to redefine types for the convenience of naming and to provide clarity about how to use the redefined type. The redefined type becomes a synonym for the original type. The purpose of this chapter is to create a synonym of one type from another, which is extremely useful to express the purpose of variables, not only through their names but also through their redefined types.

The following topics will be covered in this chapter:

  • Creating custom named types from intrinsic types
  • Creating new synonyms from other custom named types
  • Simplifying the use of enumerations
  • Simplifying the use of structures
  • Exploring some important compiler options
  • Using header files for custom types and the typedef specifiers

Technical requirements

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

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

Renaming intrinsic types with typedef

In Chapter 3, Working with Basic Data Types, we looked at some of C's basic data types – whole numbers (integers), real numbers (floating point and complex numbers), characters, enumerations, and Boolean values. We called them intrinsic data types since they are built into the language and are always available. By referring to these types specifically as data types, we give focus to the content of variables of those types – containers of data that can be manipulated. However, the use of the data type term is not strictly accurate. In reality, these types are classified in C as arithmetic types. C has other types, some of which we have encountered – function typesstructure types, and void types – and some of which we have not yet encountered – array typespointer types, and union types. We will explore array types and pointer types in greater detail in later chapters. 

For all of these types, C provides a way to rename them. This is for convenience only. The underlying type that they are based on is unchanged. We use the typedef keyword to create a synonym for the base type. In this way, we supply additional context about the intent or purpose of a variable via a renamed type declaration. The syntax for using typedef is as follows:

typedef aType aNewType;

Here, aType is an intrinsic or custom type and aNewType is now a synonym for aType. Anywhere that aType is used after this declaration, aNewType can be used instead. 

Remember, when enumerations and structures are defined, no memory is allocated. Likewise, the typedef specifiers do not allocate any memory. Memory allocation does not happen until we declare a variable of the new type, regardless of whether it is an enum type, a struct type, or a type that has been defined via typedef.

Let's now see how and why this is useful.

Using synonyms

Up to this point, we have relied on the variable's name as the sole provider of the purpose, which is our intended use of that variable. The name tells us (humans) what we expect that variable to contain. The computer doesn't really care since a variable is just a location in memory somewhere that holds some value of the given type.

For instance, we might have three variables to hold measurement values – heightwidth, and length. We could simply declare them as integers, as follows:

int height, width, length;

Their use as measurements is fairly obvious, but what are the intended units of measurement? We cannot tell this from the int type. However, by using typedef, we can add more useful context, as follows:

typedef int meters;
meters height, width, length;
height = 4;
width  = height * 2;
length = 100;

meters has been defined as a synonym of the int type. Anywhere we use these, either as standalone variables or as arguments to functions, they are now known as values of meters and not simply as integers. We have added the context of units of measure via the synonym. We can assign integers to them and perform integer operations on them. If we assign real numbers to them, truncation will occur in the type conversion to integers.

There is another benefit to using synonyms for types. It is when we may need to change the underlying base type. When we declare variables using intrinsic or custom types, their type is set to and constrained to that type. In the preceding example (before typedef), all the variables are of the int type. However, what if we needed to change their type either to long long for a much larger range of whole numbers or to double to add greater fractional accuracy? Without using typedef, we'd have to find every declaration of those variables and functions that use them and change their type to the new type, which is tedious and error-prone. With typedef, we would need to change only one line, as follows:

typedef double meters;

After making the change in the typedef specifier, any variable of the meters type has an underlying type of double. Using typedef in this way allows us some flexibility to easily modify our program.

Note that creating a synonym with typedef is largely done as a matter of convenience. Doing so doesn't limit operations on the newly defined type; any operations that are valid on the underlying type are also valid on the synonym type.

Nevertheless, this is extremely useful and very convenient. For instance, in some situations, such as dealing with data to and from a network connection, is it not only useful but also essential to have a clear understanding of the sizes of the various data types and variables that are being used. One way to provide this clarity is through the use of typedef, as follows:

typedef          char      byte;    // 7-bits + 1 signed bit
typedef unsigned char      ubyte;   // 8-bits
typedef unsigned short     ushort;  // 16 bits
typedef unsigned long      ulong;   // 32 bits
typedef long long llong;            // 63 bits + 1 signed bit
typedef unsigned long long ullong;  // 64 bits

In a program that uses these redefined types, every time a variable of ubyte is declared, we know that we are dealing with 1 byte (8 bits) and that any value is positive only (unsigned). The ubyte synonym exactly matches the definition of the computer's byte unit, which is 8 bits. As an added benefit, ubyte uses far fewer keystrokes than unsigned char.

As stated earlier, anywhere where unsigned char would be used, ubyte can be used instead. This applies to additional typedef specifiers, too. We can redeclare a synonym type to be yet another synonym type, as follows:

typedef ubyte Month;
typedef ubyte Day;
typedef ulong Year;
struct Date {
  Month m;
  Day   d;
  Year  y;
};

In struct Date, each component is itself a typedef type based on ubyte (which is based on unsigned char). We have created a new custom type based on a synonym we have already created. In the preceding example, this makes sense because both Month and Day will never exceed a value in the range provided by unsigned char (0..256). For most practical purposes, Year could be represented as ushort, but here it is as ulong.

Use typedef to make the intended usage of the variables you declare clearer and to avoid ambiguity. You can also use typedef when you are uncertain about whether the chosen type is the final, correct type. Furthermore, you may develop for yourself a standard set of typedef specifiers that you use frequently and consistently in the programs you write. We will see, later in this chapter, that we can put them in a file so that they don't have to be repeatedly edited in our programs.

Simplifying the use of enum types with typedef

Before we examine the use of typedef with enum types, we must first complete the picture of using enum types. Remember that defining a new type does not require memory allocation. Only when we declare variables of a given type is memory allocated to the variables. In the last two chapters, we used enum types by first defining them and then separately declaring variables of that type, as follows:

  // First define some enumerated types.
enum Face { eOne , eTwo , eThree , ... };
enum Suit { eSpade , eHeart, ... };
  // Then declare variables of those types.
enum Face f1 , f2;
enum Suit s1 , s2;

In the preceding code fragment, we have defined two types – enum Face and enum Suit. Later, in separate statements, two variables of each type are declared – f1f2s1, and s2.

Another way to achieve the same result is to both define the enumerated type and to declare variables of that type in one statement, as follows: 

    // Defining an enumeration and declaring variables of 
    // that type at same time.
enum Face { eOne , eTwo , eThree , ... }  f1, f2;
enum Suit { eSpade , eHeart , ... }      s1 , s2;

In this code fragment, instead of four statements, we only have two. In each statement, a type is defined and then variables of that type are declared. This second method is handy if the enumerated type is only going to be used in a single function or within a single file. Otherwise, its use is somewhat limited and the previous method of two-step definition and declaration is preferred. We will see why when we put our custom type definitions in a header file later in this chapter.

The situation is quite different, however, when typedef is thrown into the mix. The syntax for using typedef in enumerations has three forms. The first form is a two-part definition where, in the first part, the enumeration is defined, and in the second part, the typedef specifier creates a synonym for it, as follows:

enum name { enumeration1, enumeration2, … , enumerationN };
typedef enum name synonym_name;

enum name is our custom type. We use that type, just as we did with intrinsic types, to create a new synonym. An example we have seen is as follows:

enum Face { eOne , eTwo , eThree , ... };
enum Suit { eSpade, eHeart , ... };
typedef enum Face Face;
typedef enum Suit Suit;

We now have two custom types – enum Face and enum Suit – and convenient short-form synonyms for them – Face and Suit. Anywhere that we need, say, to use enum Suit, we can now simply use Suit.

The second form defines both the enumeration and synonyms for it in one statement, as follows:

typedef enum name { enumeration1, enumeration2, … , enumerationN } synonym1, synonym2, …;

The custom type is enum name, with one or more synonyms in the same statement – synonym1, synonym2 ,. This is very different from what we saw earlier when an enum and variables were declared in the same statement. In this case, there is no allocation of memory and no variables are created. Using this form for Face and Suit would work as follows:

typedef enum Face { eOne , eTwo , eThree , ... } Face;
typedef enum Suit { eSpade , eHeart , ... }     Suit;

In each of the two statements, a custom type is defined and a synonym for it is created. While this is similar to the preceding statement where a custom type is defined and variables of that type are allocated, here, typedef makes all the difference.

There is an even shorter form, the third form, where name is omitted and we only have the synonyms for our unnamed or anonymous custom type. The following code block shows this:

typedef enum { eOne , eTwo , eThree , ... } Face;
typedef enum { eSpade , eHeart , ... }     Suit;

We have created two anonymous enumerations that are now only known as a type by their synonyms – Face and Suit. Regardless of which method we use to create synonyms for enum Face and enum Suit, we can now declare variables using our new synonyms, as follows:

Face face;
Suit suit;

We have declared two variables – face and suit – using a convention where the custom type identifier has the first letter in its name in uppercase and the variables are all lowercase identifiers.

Of the three ways given to create typedef enumerations, the last method is most common. Once the synonyms for a custom type are defined, there is rarely a need to use the enum name custom type; this, however, is not a strict rule, and all three forms can be used depending on what best fits the situation.

Simplifying the use of struct types with typedef

All of the considerations we explored for enumerations equally apply to structures. We will go through each of them as they apply to structures.

Before we examine the use of typedef with structures, we must first complete the picture of using structures. In the previous chapter, we used structures by first defining them and then separately declaring variables of that type, as follows:

  // First define a structured type.
struct Card { Face face; Suit suit; ... };
  // Then declare variables of that type.
struct Card c1 , c2 , c3 , c4 , c5;

In the preceding code fragment, we have defined one type, struct Card. In a separate statement, five variables of that type are declared – c1c2c3c4, and c5.

Another way to achieve the same result is to both define the structured type and declare variables of that type in one statement, as follows: 

// Defining an structure and declaring variables of that 
// type at the same time
struct Card { Face face; Suit suit; ... } c1 , c2 , c3 , c4 , c5;

In this code fragment, instead of two statements, we have only one. In that statement, a type is defined and then variables of that type are declared. This second method is handy if the structured type is only going to be used in a single function or within a single file. Otherwise, its use is somewhat limited, and the previous method of two-step definition and declaration is preferred. We will see why when we put our custom type definitions in a header file later in this chapter.

The situation is quite different, however, when typedef is thrown into the mix. The syntax for using typedef in structure definitions has three forms. The first form is a two-part definition, where first the structure is defined and then the typedef specifier creates a synonym for it, as follows:

struct name { type component1; type component2; … ; type componentN }; typedef struct name synonym_name;

struct name is our custom type. We use that type, just as we did with intrinsic types, to create a new synonym. An example we have seen is as follows:

struct Card { Face face; Suit suit; ... };
typedef struct Card Card;

We now have one custom type, struct Card, and a convenient short-form synonym for it, Card. Anywhere that we need, say, to use struct Card, we can now simply use Card.

The second form defines both the structure and synonyms for it in one statement, as follows:

 typedef struct name {
  type component1;
  type component2;
  … ;
  type componentN; 
} synonym1, synonym2, …;

The custom type is struct name, with one or more synonyms in the same statement – synonym1, synonym2,. This is very different from what we saw earlier when a struct and variables were declared in the same statement. In this case, there is no allocation of memory and no variables are created. Using this form for Card is as follows:

typedef struct Card { Face face; Suit suit; ... } Card;

In this statement, a custom type is defined and a synonym for it is created. While this is similar to the preceding statement, where a custom type is defined and variables of that type are allocated, here, typedef makes all the difference.

There is an even shorter form where name is omitted and we only have the synonyms for our unnamed or anonymous custom type. The following code snippet shows this:

typedef struct { Face face; Suit suit; ... } Card;

We have created an anonymous struct that is now only known as a type by its synonym, Card. Regardless of which method we use to create synonyms for struct Card, we can now declare variables using our new synonyms, as follows:

Card c1 , c2 , c3, c4, c5;

We have declared five variables – c1c2c3c4, and c5 – using a convention where the custom type identifier begins with an uppercase letter and the variables are all lowercase identifiers.

Of the three ways given to create structs defined via typedef, the last method is the most common. Once the synonyms for a custom type are defined, there is rarely a need to use the struct name custom type; this, however, is not a strict rule, and all three forms can be used depending on what best fits the situation.

Reading this section, did you feel as if you had read this before? If so, that is not coincidental. The considerations for using typedef for enumerations and structures are identical.

Let's put all of this into use with a working program. Let's alter the card3.c program from the previous chapter, making use of our knowledge of typedef. We'll modify a copy of this program to use enumerations (defined via typedef) and structures. Copy card3.c to card4.c and make the following changes in card4.c:

  • Use typedef for the enumerations for Suit and Face.
  • Use typedef for the structures for Card and Hand.
  • In the structs that use enum Suit and enum Face, replace these with their synonyms.
  • Replace each occurrence of struct Card and struct Hand with their synonyms wherever they are found (hint – with other structs, function prototypes, function parameters, and so on).

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

Figure 10.1 – Screenshot of card4.c output

Figure 10.1 – Screenshot of card4.c output

Finally, compare your edits on your local card4.c program to the card4.c program provided in the source repository for this chapter. You should have eliminated all but two enum keywords and all but two struct keywords in card4.c, and the output should be identical to that of card3.c from the preceding chapter. 

What we just did – that is, refactor a program to use more expressive C features – is a quite common programming skill. Note that we had a working program, modified it to use new language features, and then verified the program by using the program output. This is a skill we will reinforce throughout the rest of this book.

Before we move on to other C features in this chapter, let's preview some of the other uses of typedef.

Other uses of typedef

We began this chapter by discussing C's various types beyond arithmetic types and custom types. It should come as no surprise, then, that typedef can apply to more than just the types we have explored in this chapter. typedef can be applied to the following types:

  • Arrays (explored in the next chapter)
  • Pointer types (explored in Chapter 13Using Pointers)
  • Functions
  • Pointers to functions

We mention these here as a matter of completeness only. When we explore pointers, we will touch on using typedef on variables of pointer types. However, the use of typedef for the other types is somewhat advanced conceptually and beyond the scope of this book – nevertheless, we will touch on them with some simple examples in later chapters.

Exploring some more useful compiler options

Up until now, we have been compiling our programs with just the -o output_file option. Your compiler, whether gcc,  clang, or icc, probably unbeknownst to you before this point, has a bewildering array of options.

If you are curious, type cc -help into your command shell and see what spews out. Tread lightly! If you do this, just understand that you will never need the vast majority of those options. Some of these options are for the compiler only; others are passed on to the linker only. They are there for both very specialized system software configurations and for the compiler and linker writers. 

If you are using a Unix or Unix-like system, type man cc in the command shell to see a more reasonable list of options and their usage.

The most important of these options, and the one we will use from now on, is -std=c17 or -std=c11. The -std switch tells the compiler which version of the C standard to use. Some compilers default to older versions of the C standard, such as C89 or C99, for backward compatibility. Nearly all compilers now support the newest standard, C17, and even the next standard, C23. On the rather old system that I am using to write this, C17 is not supported. However, on another updated system I have, -std=c17 is supported; on that system, I will use that switch.

Another very important compiler switch is -Wall. The -W switch allows you to enable an individual warning or all warnings that the compiler encounters. Without that switch, the compiler may only report the most serious warnings. More often, especially when learning, we want to see all of them.

It is also a very good idea to treat all warnings as errors with the -Werror switch. Any warning conditions encountered by the compiler will then prevent further processing (the linker will not be invoked) and no executable output file will be created.

There are many reasons why using  -Wall with -Werror is always the best practice. As you encounter more C programs and more books about C, you will find that many rely on and are steeped in older versions of C with slightly different behaviors. Some older behaviors are good, while others are not as good. Newer compilers may or may not continue to allow or support those behaviors; without -Wall and -Werror, the compiler might issue warnings when they are encountered. In a worst-case scenario, no error or warning will be given but your program will behave in unexpected ways or will crash. Another reason is that you may be using C in a way that is not quite as it was intended to be used, according to Hoyle, and the compiler will provide warnings when that happens. When you get a warning, it is then important to do the following:

  • Understand why the warning was presented (what caused it or what did you do to cause it?).
  • Fix and remove the cause of the warning.
  • Repeat until there are no errors or warnings.

Compilers are notorious for spewing cryptic warnings. Thankfully, we now have the internet to help us out. When you get an error or warning you don't understand, copy it from your command shell and paste it into the search field of your favorite web browser. 

Given these considerations, our standard command line for compilation will be as follows:

cc source.c -std=c17 -Wall -Werror -o source

Here, source is the name of the C program to be compiled. Use c11 if your system does not support c17 (or consider upgrading your compiler/system). You will get tired of typing these switches. For now, I want you to do this manually.

Using a header file for custom types and the typedef specifiers

Because we have explored custom types (enumerations, structures, and the typedef specifiers), it is now appropriate to explore how to collect these custom types into our own header file and include them in our program.

We have seen the following statements:

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

These are predefined header files that provide standard library function prototypes – the typedef specifiers, enumerations, and structures – related to those function prototypes. When a header file is enclosed in < and >, the compiler looks in a predefined list of places for those files. It then opens them and inserts them into the source file just as if they had been copied and pasted into the source file.

We can now create our own header file, say card.h, and use it in our program. But where do we put it? We could find the location of the predefined header files and save ours there. That, however, is not a good idea since we may end up writing many, many programs; editing, saving, and updating program files in many locations is tedious and error-prone. Fortunately, there is a solution. 

When we enclose a header file in " and ", the compiler first looks in the same directory as the source .c file before it starts looking elsewhere for a given file. So, we would create and save our card.h file in the same directory location as, say, card5.c. We would then direct the compiler to use it as follows:

#include "card.h"

When you create header files for your programs, they will nearly always reside in the same directory location as the source files. We can then rely on this simple convention of using " and " to locate local header files. Local header files are those in the same directory as our source file(s).

For the time being, we will associate one header file with one source file. This is an extremely simple use of header files. As you might already expect, the usefulness of using a header file goes far beyond its inclusion in a single source file. We will explore the common uses of header files in multiple files and special considerations when doing so in Chapter 24Working with Multi-File Programs, in the Using the preprocessor effectively section.

We must now consider what belongs in header files and what doesn't belong there.

As a general convention, each C source file has an associated header file. In that file are the function prototypes and any custom types used within the source file that are specific to it. We will explore this simplified use of a header file in this section. Here are the basic guidelines:

  • Put anything in a header that does not allocate memory (variables) and does not define functions.
  • Put anything into the source file that allocates memory or defines functions.

To be clear, anything that could go into a header file doesn't have to go into a header file; as we have already seen, it can be put into the source file where it is used. Whether to put something in a header or not is a topic covered in Chapter 24Working with Multi-File Programs, and Chapter 25Understanding Scope. Complications arise when we use a header file in more than one place, and we will see how to do that in Chapter 24Working with Multi-File Programs. For now, we will use a single header file for our single C source file as a means to declutter the source file. 

Let's begin reworking card4.c into card.h and card5.c, which will use card.h but will otherwise remain unchanged. Depending on the editor you are using, copying and pasting between files may be easy or difficult. The approach we will take will avoid using the editor's copy and paste abilities. Our approach is as follows:

  1. Copy card4.c to card.h.
  2. Copy card4.c to card5.c.
  3. Then, pare down each of those two new files into what we want.

Open card.h and remove the #include statements, the main() function definition, and any other function definitions. You should be left with the following:

typedef enum { 
  eClub  = 1 , eDiamond , eHeart , eSpade 
} Suit;
typedef enum { 
  eOne = 1, eTwo, eThree, eFour, eFive, eSix, eSeven, 
         eEight, eNine, eTen, eJack, eQueen, eKing, eAce
} Face;
typedef struct {
  Suit suit;
  int  suitValue;
  Face face;
  int  faceValue;
  bool isWild;
} Card;
typedef struct {
  int  cardsDealt;
  Card c1, c2, c3, c4, c5;
} Hand;
Hand addCard(    Hand oldHand , Card card );
void printHand(  Hand h );
void printHand2( Hand h );
void printCard(  Card c );

Now, open card5.c and delete the typedef enumerations, the typedef structures, and the function prototypes. Replace them all with a single line, as follows:

#include "card.h"

Your card5.c file should now look like the following:

#include <stdio.h>
#include <stdbool.h>
#include "card.h"
int main( void)  {
 ...
 ...
}
// and all the rest of the function definitions.
...
...

What we have done is essentially split card4.c into two files – card.h and card5.c. Compile card5.c (with our new switches) and run it. You should see the following output (it should be the same as for card4.c):

Figure 10.2 – Screenshot of card5.c output

Figure 10.2 – Screenshot of card5.c output

Once again, we modified our program to use a C feature, a header file, and then verified that it gave the same output as before our modifications.

This header file as it is can only ever be included in a single C file. However, header files are most commonly used in multiple-file programs and included in two or more C source files. We will see how to properly use a header file in multiple C files in Chapter 24, Working with Multi-File Programs, in the Using the preprocessor effectively section.

Stop and consider this file organization – a source file with main() and function definitions, along with its header file, containing enumerations, structures, the typedef specifiers, and function prototypes. This is a core pattern of C source files. For now, this is a simple introduction to creating and using our own header files. Henceforth, you'll see custom header files being created and used. These will be simple header files and will typically consist of only a single header that we create. We will explore more complex header files, as well as multiple header files, in Chapter 24Working with Multi-File Programs

Summary

We have seen how to create alternative names, or synonyms, for intrinsic types and custom types declared with enumerations and structures. Using typedef, we have explored convenient ways to create synonyms for intrinsic types and how the typedef specifiers simplify the use of enumerations and structures. We have also seen how synonyms make your code clearer and provide added context for the intended use of variables of that synonym type.

We have seen how it is somewhat cumbersome to declare and manipulate multiple instances of structured data of the same type without a synonym for that type, especially when there are many instances of a single structure type, such as a deck of cards.

In the next chapter, we will see how to group, access, and manipulate collections of data types that are identical in type but differ only in values. These are called arrays. Arrays help us to further model and manipulate, for instance, a deck of cards consisting of 52 cards.

Questions

  1. What does the typedef keyword provide for intrinsic types?
  2. What does the typedef keyword provide for enumeration and structure types?
  3. How is using typedef for enumerations different from using typedef for structures?
  4. What are two compiler switches that help us prevent undefined program behavior?
  5. What goes into a header file? What does not go into a header file?
..................Content has been hidden....................

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