Chapter 2. Types

Programs have to store and process different kinds of data, such as integers and floating-point numbers, in different ways. To this end, the compiler needs to know what kind of data a given value represents.

In C, the term object refers to a location in memory whose contents can represent values. Objects that have names are also called variables. An object’s type determines how much space the object occupies in memory, and how its possible values are encoded. For example, the same pattern of bits can represent completely different integers depending on whether the data object is interpreted as signed (that is, either positive or negative) or unsigned (and hence unable to represent negative values).

Typology

The types in C can be classified as follows:

  • Basic types

    • Standard and extended integer types

    • Real and complex floating-point types

  • Enumerated types

  • The type void

  • Derived types

    • Pointer types

    • Array types

    • Structure types

  • Union types

  • Function types

The basic types and the enumerated types together make up the arithmetic types. The arithmetic types and the pointer types together are called the scalar types. Finally, array types and structure types are referred to collectively as the aggregate types. (Union types are not considered aggregate because only one of their members can store a value at any given time.)

A function type describes the interface to a function; that is, it specifies the type of the function’s return value, and may also specify the types of all the parameters that are passed to the function when it is called.

All other types describe objects. This description may or may not include the object’s storage size. If it does, the type is properly called an object type; if not, it is an incomplete type. An example of an incomplete type might be an externally defined array variable:

extern float fArr[ ];     // External declaration

This line declares fArr as an array whose elements have type float. However, because the array’s size is not specified here, fArr’s type is incomplete. As long as the global array fArr is defined with a specified size at another location in the program—in another source file, for example—this declaration is sufficient to let you use the array in its present scope. (For more details on external declarations, see Chapter 11.)

Tip

This chapter describes the basic types, enumerations, and the type void. The derived types are described in Chapters 7 through 10.

Some types are designated by a sequence of more than one keyword, such as unsigned short. In such cases, the keywords can be written in any order. However, there is a conventional keyword order, which we use in this book.

Integer Types

There are five signed integer types. Most of these types can be designated by several synonyms, which are listed in Table 2-1.

Table 2-1. Standard signed integer types
Type Synonyms

signed char

int

signed, signed int

short

short int, signed short, signed short int

long

long int, signed long, signed long int

long long (C99)

long long int, signed long long, signed long long int

For each of the five signed integer types in Table 2-1, there is also a corresponding unsigned type that occupies the same amount of memory, with the same alignment. In other words, if the compiler aligns signed int objects on even-numbered byte addresses, then unsigned int objects are also aligned on even addresses. These unsigned types are listed in Table 2-2.

Table 2-2. Unsigned standard integer types
Type Synonyms

_Bool

bool (defined in stdbool.h )

unsigned char

unsigned int

unsigned

unsigned short

unsigned short int

unsigned long

unsigned long int

unsigned long long

unsigned long long int

C99 introduced the unsigned integer type _Bool to represent Boolean truth values. The Boolean value true is coded as 1, and false is coded as 0. If you include the header file stdbool.h in a program, you can also use the identifiers bool, true, and false, which are familiar to C++ programmers. The macro bool is a synonym for the type _Bool, and true and false are symbolic constants equal to 1 and 0.

The type char is also one of the standard integer types. However, the one-word type name char is synonymous either with signed char or with unsigned char, depending on the compiler. Because this choice is left up to the implementation, char, signed char, and unsigned char are formally three different types.

Tip

If your program relies on char being able to hold values less than zero or greater than 127, you should be using either signed char or unsigned char instead.

You can do arithmetic with character variables. It’s up to you to decide whether your program interprets the number in a char variable as a character code or as something else. For example, the following short program treats the char value in ch as both an integer and a character, but at different times:

char ch = 'A';               // A variable with type char
printf("The character %c has the character code %d.\n", ch, ch);
for ( ; ch <= 'Z'; ++ch )
  printf("%2c", ch);

In the printf() statement, ch is first treated as a character that gets displayed, and then as numeric code value of the character. Likewise, the for loop treats ch as an integer in the instruction ++ch, and as a character in the printf() function call. On systems that use the 7-bit ASCII code or an extension of it, the code produces the following output:

The character A has the character code 65.
 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

A value of type char always occupies one byte—in other words, sizeof(char) always yields 1—and a byte is at least eight bits wide. Every character in the basic character set can be represented in a char object as a positive value.

C defines only the minimum storage sizes of the other standard types: the size of type short is at least two bytes, long is at least four bytes, and long long is at least eight bytes. Furthermore, although the integer types may be larger than their minimum sizes, the sizes implemented must be in the order:

sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)

The type int is the integer type best adapted to the target system’s architecture, with the size and bit format of a CPU register.

The internal representation of integer types is binary. Signed types may be represented in binary as sign and magnitude, as a one’s complement, or as a two’s complement. The most common representation is the two’s complement. The non-negative values of a signed type are within the value range of the corresponding unsigned type, and the binary representation of a non-negative value is the same in both the signed and unsigned types. Table 2-3 shows the different interpretations of bit patterns as signed and unsigned integer types.

Table 2-3. Binary representations of signed and unsigned 16-bit integers
Binary Decimal value as unsigned int Decimal value as signed int, one’s complement Decimal value as signed int, two’s complement

00000000 00000000

0

0

0

00000000 00000001

1

1

1

00000000 00000010

2

2

2

01111111 11111111

32,767

32,767

32,767

10000000 00000000

32,768

-32,767

-32,768

10000000 00000001

32,769

-32,766

-32,767

11111111 11111110

65,534

-1

-2

11111111 11111111

65,535

-0

-1

Table 2-4 lists the sizes and value ranges of the standard integer types.

Table 2-4. Common storage sizes and value ranges of standard integer types
Type Storage size Minimum value Maximum value
char (Same as either signed char or unsigned char)
unsigned char One byte 0 255
signed char One byte -128 127
int Two bytes or four bytes -32,768 or -2,147,483,648 32,767 or 2,147,483,647
unsigned int Two bytes or four bytes 0 65,535 or 4,294,967,295
short Two bytes -32,768 32,767
unsigned short Two bytes 0 65,535
long Four bytes -2,147,483,648 2,147,483,647
unsigned long Four bytes 0 4,294,967,295
long long (C99) Eight bytes -9,223,372,036, 854,775,808 9,223,372,036, 854,775,807
unsigned long long (C99) Eight bytes 0 18,446,744,073, 709,551,615

In the following example, each of the int variables iIndex and iLimit occupies four bytes on a 32-bit computer:

int iIndex,            // Define two int variables and
    iLimit = 1000;     // initialize the second one.

To obtain the exact size of a type or variable, use the sizeof operator. The expression

sizeof(type)

yields the size of the type named, and

sizeof expression

yields the size of the given expression’s type, as a number of bytes with the type size_t. The type size_t is defined in stddef.h, stdio.h, and other header files as an unsigned integer type (such as unsigned long, for example). If the operand is an expression, the size is that of the expression’s type. In the previous example, the value of sizeof(int) would be the same as sizeof(iIndex)—namely, 4. The parentheses around the expression iIndex can be omitted because iIndex is an expression, not a type.

You can find the value ranges of the integer types for your C compiler in the header file limits.h, which defines macros such as INT_MIN, INT_MAX, UINT_MAX, and so on (see Chapter 16). The program in Example 2-1 uses these macros to display the minimum and maximum values for the types char and int.

Example 2-1. Value ranges of the types char and int
// limits.c: Display the value ranges of char and int.
// ---------------------------------------------------
#include <stdio.h>
#include <limits.h>     // Contains the macros CHAR_MIN, INT_MIN, etc.

int main()
{
  printf("Storage sizes and value ranges of the types char and int\n\n");
  printf("The type char is %s.\n\n", CHAR_MIN < 0 ? "signed" :"unsigned");

  printf(" Type   Size (in bytes)   Minimum         Maximum\n"
         "---------------------------------------------------\n");
  printf(" char %8zu %20d %15d\n", sizeof(char), CHAR_MIN, CHAR_MAX );
  printf(" int  %8zu %20d %15d\n", sizeof(int), INT_MIN, INT_MAX );
  return 0;
}

In arithmetic operations with integers, overflows can occur. An overflow happens when the result of an operation is no longer within the range of values that the type being used can represent. In arithmetic with unsigned integer types, overflows are ignored. In mathematical terms, that means that the effective result of an unsigned integer operation is equal to the remainder of a division by UTYPE_MAX + 1, where UTYPE_MAX is the unsigned type’s maximum representable value. For example, the following addition causes the variable to overflow:

unsigned int ui = UINT_MAX;
ui += 2;                       // Result: 1

C specifies this behavior only for the unsigned integer types. For all other types, the result of an overflow is undefined. For example, the overflow may be ignored, or it may raise a signal that aborts the program if it is not caught.

Integer Types Defined in Standard Headers

The headers of the standard library define numerous integer types for specific uses, such as the type wchar_t to represent wide characters. These types are typedef names—that is, synonyms for standard integer types (see “typedef Declarations”).

The types ptrdiff_t, size_t, and wchar_t are defined in the header stddef.h (and in other headers); the types char16_t and char32_t are defined in the header uchar.h. For special requirements, integer types with specifed bit widths, in signed and unsigned variants, are defined in the header stdint.h. These are described in the following subsection.

Furthermore, the header stdint.h also defines macros that supply the maximum and minimum representable values of all the integer types defined in the standard library. For example, SIZE_MAX equals the largest value you can store in a variable of the type size_t. For all details on the types listed here, and the corresponding macros, see Chapter 16.

Integer types with exact width (C99)

The width of an integer type is defined as the number of bits used to represent a value, including the sign bit. Typical widths are 8, 16, 32, and 64 bits. For example, the type int is at least 16 bits wide.

In C99, the header file stdint.h defines integer types to fulfill the need for known widths. These types are listed in Table 2-5. Those types whose names begin with u are unsigned. C99 implementations are not required to provide the types marked as “optional” in the table.

Table 2-5. Integer types with defined width
Type Meaning Implementation

intN_t
uintN_t

An integer type whose width is exactly N bits

Optional

int_leastN_t
uint_leastN_t

An integer type whose width is at least N bits

Required for N = 8, 16, 32, 64

int_fastN_t
uint_fastN_t

The fastest type to process whose width is at least N bits

Required for N = 8, 16, 32, 64

intmax_t
uintmax_t

The widest integer type implemented

Required

intptr_t
uintptr_t

An integer type wide enough to store the value of a pointer

Optional

For example, int_least64_t and uint_least64_t are integer types with a width of at least 64 bits. If an optional signed type (without the prefix u) is defined, then the corresponding unsigned type (with the initial u) is required, and vice versa. The following example defines and initializes an array whose elements have the type int_fast32_t:

#define ARR_SIZE 100
int_fast32_t arr[ARR_SIZE];       // Define an array arr
                                  // with elements of type int_fast32_t
for ( int i = 0; i < ARR_SIZE; ++i )
    arr[i] = (int_fast32_t)i;     // Initialize each element

The types listed in Table 2-5 are usually defined as synonyms for existing standard types. For example, the stdint.h file supplied with one C compiler contains the line:

typedef signed char    int_fast8_t;

This declaration simply defines the new type int_fast8_t (the fastest 8-bit signed integer type) as being equivalent with signed char.

Furthermore, an implementation may also define extended integer types such as int24_t or uint_least128_t.

The signed intN_t types have a special feature: they must use the two’s complement binary representation. As a result, their minimum value is −2N−1, and their maximum value is 2N−1 − 1.

The value ranges of the types defined in stdint.h are also easy to obtain: macros for the greatest and least representable values are defined in the same header file. The names of the macros are the uppercased type names, with the suffix _t (for type) replaced by _MAX or _MIN (see Chapter 16). For example, the following definition initializes the variable i64 with its smallest possible value:

int_least64_t i64 = INT_LEAST64_MIN;

The header file inttypes.h includes the header file stdint.h, and provides other features such as extended integer type specifiers for use in printf() and scanf() function calls (see Chapter 16).

Floating-Point Types

C also includes special numeric types that can represent nonintegers with a decimal point in any position. The standard floating-point types for calculations with real numbers are as follows:

float

For variables with single precision

double

For variables with double precision

long double

For variables with extended precision

A floating-point value can be stored only with a limited precision, which is determined by the binary format used to represent it and the amount of memory used to store it. The precision is expressed as a number of significant digits. For example, a “precision of six decimal digits” or “six-digit precision” means that the type’s binary representation is precise enough to store a real number of six decimal digits, so that its conversion back into a six-digit decimal number yields the original six digits. The position of the decimal point does not matter, and leading and trailing zeros are not counted in the six digits. The numbers 123,456,000 and 0.00123456 can both be stored in a type with six-digit precision.

In C, arithmetic operations with floating-point numbers are usually performed with double or greater precision. The floating-point precision used internally by the given implementation is indicated by the value of the macro FLT_EVAL_METHOD, defined in the header float.h. For example, if the macro FLT_EVAL_METHOD has the value 1, the following product is calculated using the double type:

float height = 1.2345, width = 2.3456;  // Float variables have
                                        // single precision.
double area = height * width;           // The actual calculation
                                        // is performed with
                                        // double precision.

If you assign the result to a float variable, the value is rounded as necessary. For more details on floating-point math, see “math.h”.

C defines only minimal requirements for the storage size and binary format of the floating-point types. However, the format commonly used is the one defined by the International Electrotechnical Commission (IEC) in the 1989 standard for binary floating-point arithmetic, IEC 60559. This standard is based in turn on the Institute of Electrical and Electronics Engineers’ 1985 standard, IEEE 754. Compilers can indicate that they support the IEC floating-point standard by defining the macro __STDC_IEC_559__. Table 2-6 shows the value ranges and the precision of the real floating-point types in accordance with IEC 60559, using decimal notation.

Table 2-6. Real floating-point types
Type Storage size Value range Smallest positive value Precision

float

4 bytes

±3.4E+38

1.2E-38

6 digits

double

8 bytes

±1.7E+308

2.3E-308

15 digits

long double

10 bytes

±1.1E+4932

3.4E-4932

19 digits

The header file float.h defines macros that allow you to use these values and other details about the binary representation of real numbers in your programs. The macros FLT_MIN, FLT_MAX, and FLT_DIG indicate the value range and the precision of the float type. The corresponding macros for double and long double begin with the prefixes DBL_ and LDBL_. These macros, and the binary representation of floating-point numbers, are described in “float.h”.

The program in Example 2-2 starts by printing the typical values for the type float, and then illustrates the rounding error that results from storing a floating-point number in a float variable.

Example 2-2. Illustrating the precision of type float
#include <stdio.h>
#include <float.h>

int main()
{
  puts("\nCharacteristics of the type float\n");

  printf("Storage size: %d bytes\n"
         "Smallest positive value: %E\n"
         "Greatest positive value: %E\n"
         "Precision: %d decimal digits\n",
         sizeof(float), FLT_MIN, FLT_MAX, FLT_DIG);

  puts("\nAn example of float precision:\n");
  double d_var = 12345.6;       // A variable of type double.
  float f_var = (float)d_var;   // Initializes the float
                                // variable with the value of d_var.
  printf("The floating-point number    "
         "%18.10f\n", d_var);
  printf("has been stored in a variable\n"
         "of type float as the value   "
         "%18.10f\n", f_var);
  printf("The rounding error is        "
         "%18.10f\n", d_var - f_var);

  return 0;
}

The last part of this program typically generates the following output:

The floating-point number    12345.6000000000
has been stored in a variable
of type float as the value   12345.5996093750
The rounding error is            0.0003906250

In this example, the nearest representable value to the decimal 12,345.6 is 12,345.5996093750. This may not look like a round number in decimal notation, but in the internal binary representation of the floating-point type, it is exactly representable, while 12,345.60 is not.

Complex Floating-Point Types

C99 supports mathematical calculations with complex numbers. The 1999 standard introduced complex floating-point types and extended the mathematical library to include complex arithmetic functions. These functions are declared in the header file complex.h, and include the trigonometric functions csin(), ctan(), and so on (see Chapter 16).

In the C11 standard, support for complex numbers is optional. The macro__STDC_NO_COMPLEX__ can be defined to indicate that the implementation does not include the header file complex.h.

A complex number z can be represented in Cartesian coordinates as z = x + y × i, where x and y are real numbers, and i is the imaginary unit, defined by the equation i2 = -1. The number x is called the real part, and y the imaginary part, of z.

In C, a complex number is represented by a pair of floating-point values for the real and imaginary parts. Both parts have the same type, whether float, double, or long double. Accordingly, these are the three complex floating-point types:

  • float _Complex

  • double _Complex

  • long double _Complex

Each of these types has the same size and alignment as an array of two float, double, or long double elements.

The header file complex.h defines the macros complex and I. The macro complex is a synonym for the keyword _Complex. The macro I represents the imaginary unit i, and has the type const float _Complex:

#include <complex.h>
// ...
double complex z = 1.0 + 2.0 * I;
z *= I;      // Rotate z through 90° counterclockwise around the origin

To compose a complex number from its real and imaginary parts, C11 also provides the macros CMPLX, CMPLXF, and CMPLXL. For example, the complex number CMPLX(1.0, 2.0) is equal to the number z defined in the preceding example, and has the type double complex. Similarly, the macros CMPLXF and CMPLXL yield a complex number of the type float complex and long double complex. An implementation may also include the following types to represent pure imaginary numbers: float imaginary, double imaginary, and long double imaginary.

Enumerated Types

Enumerations are integer types that you define in a program. The definition of an enumeration begins with the keyword enum, possibly followed by an identifier for the enumeration, and contains a list of the type’s possible values, with a name for each value:

enum [identifier] { enumerator-list };

The following example defines the enumerated type enum color:

enum color { black, red, green, yellow, blue, white=7, gray };

The identifier color is the tag of this enumeration. The identifiers in the list—black, red, and so on—are the enumeration constants, and have the type int. You can use these constants anywhere within their scope—as case constants in a switch statement, for example.

Each enumeration constant of a given enumerated type represents a certain value, which is determined either implicitly by its position in the list, or explicitly by initialization with a constant expression. A constant without an initialization has the value 0 if it is the first constant in the list, or the value of the preceding constant plus one. Thus, in the previous example, the constants listed have the values 0, 1, 2, 3, 4, 7, 8.

Within an enumerated type’s scope, you can use the type in declarations:

enum color bgColor = blue,         // Define two variables
           fgColor = yellow;       // of type enum color.
void setFgColor( enum color fgc ); // Declare a function with a
                                   // parameter of type enum color.

An enumerated type always corresponds to one of the standard integer types. Thus, your C programs may perform ordinary arithmetic operations with variables of enumerated types. The compiler may select the appropriate integer type depending on the defined values of the enumeration constants. In the previous example, the type char would be sufficient to represent all the values of the enumerated type enum color.

Different constants in an enumeration may have the same value:

enum { OFF, ON, STOP = 0, GO = 1, CLOSED = 0, OPEN = 1 };

As the preceding example also illustrates, the definition of an enumerated type does not necessarily have to include a tag. Omitting the tag makes sense if you only want to define constants and not declare any variables of the given type. Defining integer constants in this way is generally preferable to using a long list of #define directives, as the enumeration provides the compiler with the names of the constants as well as their numeric values. These names are a great advantage in a debugger’s display, for example.

The Type void

The type specifier void indicates that no value is available. Consequently, you cannot declare variables or constants with this type. You can use the type void for the purposes described in the following sections.

void in Function Declarations

A function with no return value has the type void. For example, the standard function perror() is declared by the prototype:

void perror( const char * );

The keyword void in the parameter list of a function prototype indicates that the function has no parameters:

FILE *tmpfile( void );

As a result, the compiler issues an error message if you try to use a function call such as tmpfile("name.tmp"). If the function were declared without void in the parameter list, the C compiler would have no information about the function’s parameters, and hence would be unable to determine whether the function call is correct.

Expressions of Type void

A void expression is one that has no value. For example, a call to a function with no return value is an expression of type void:

char filename[ ] = "memo.txt";
if ( fopen( filename, "r" ) == NULL )
  perror( filename );             // A void expression

The cast operation (void)expression explicitly discards the value of an expression, such as the return value of a function:

(void)printf("I don't need this function's return value!\n");

Pointers to void

A pointer of type void * represents the address of an object, but not its type. You can use such quasi-typeless pointers mainly to declare functions that can operate on various types of pointer arguments, or that return a “multipurpose” pointer. The standard memory management functions are a simple example:

void *malloc( size_t size );
void *realloc( void *ptr, size_t size );
void free( void *ptr );

As Example 2-3 illustrates, you can assign a void pointer value to another object pointer type, or vice versa, without explicit type conversion.

Example 2-3. Using the type void
// usingvoid.c: Demonstrates uses of the type void
// -------------------------------------------------------
#include <stdio.h>
#include <time.h>
#include <stdlib.h>  // Provides the following function prototypes:
                     // void srand( unsigned int seed );
                     // int rand( void );
                     // void *malloc( size_t size );
                     // void free( void *ptr );
                     // void exit( int status );

enum { ARR_LEN = 100 };

int main()
{
  int i,             // Obtain some storage space.
      *pNumbers = malloc(ARR_LEN * sizeof(int));

  if ( pNumbers == NULL )
  {
    fprintf(stderr, "Insufficient memory.\n");
    exit(1);
  }

  srand( (unsigned)time(NULL) );        // Initialize the
                                        // random number generator.

  for ( i=0; i < ARR_LEN; ++i )
    pNumbers[i] = rand() % 10000;       // Store some random numbers.

  printf("\n%d random numbers between 0 and 9999:\n", ARR_LEN );
  for ( i=0; i < ARR_LEN; ++i )         // Output loop:
  {
    printf("%6d", pNumbers[i]);         // Print one number per loop
    if ( i % 10 == 9 ) putchar('\n');   // iteration and a newline
  }                                     // after every 10 numbers.
  free( pNumbers );                     // Release the storage space.
  return 0;
}

The Alignment of Objects in Memory

Every complete object type imposes a certain alignment on objects of that type. In other words, the type specifies the kind of memory addresses at which objects of that type can be stored: all addresses, only even addresses, only addresses divisible by four, and so on. The alignment of a type is expressed as a number of bytes equal to the minimum distance between two objects of that type in storage. The specific values of the types’ alignments can vary from one implementation to another, but they are always positive integer powers of 2: that is, 1, 2, 4, 8, and so on. An alignment with a greater value than another type’s alignment is said to be stricter than the other.

C11 provides the operator _Alignof to determine a type’s alignment, and the specifier _Alignas to specify the alignment in an object definition.

The _Alignof operator, like the sizeof operator, yields a constant value with the type size_t, an unsigned integer type defined in stddef.h and other header files. For example, the following expression yields the alignment of the type int, which is typically 4:

_Alignof(int)

An alignment value less than or equal to _Alignof(max_align_t) is called a fundamental alignment. All the fundamental types—that is, the basic types and pointer types—have a fundamental alignment. The type max_align_t is defined in the header stddef.h, and its alignment is supported in every context, including dynamic memory allocation, for example. In addition, the implementation may also support alignments greater than _Alignof(max_align_t), which are known as extended alignments.

When an object is defined with the specifier _Alignas, it can have a stricter alignment than its type requires. The argument of _Alignas can be a constant integer expression whose value is a valid alignment, or a type, as in the following examples:

_Alignas(4) short var;  // Defines var with the type short
                        // and four-byte alignment.
_Alignas(double) float x;  // Defines x with the type float
                           // and the alignment of double.

The form _Alignas(type) is synonymous with _Alignas (_Alignof(type)). The header file stdalign.h defines alignof and alignas as synonyms for _Alignof and _Alignas. Thus, if your program includes stdalign.h, you can write alignas(int) instead of _Alignas(int).