United States    
COMPAQ STORE | PRODUCTS |
SERVICES | SUPPORT | CONTACT US | SEARCH
Compaq C

Compaq C
Language Reference Manual


Previous Contents Index

3.4.5 Union Type

A union type can store objects of different types at the same location in memory. The different union members can occupy the same location at different times in the program. The declaration of a union includes all members of the union, and lists the possible object types the union can hold. The union can hold any one member at a time---subsequent assignments of other members to the union overwrite the existing object in the same storage area.

Unions can be named with any valid identifier. An empty union cannot be declared, nor can a union contain an instance of itself. A member of a union cannot have a void , function, or incomplete type. Unions can contain pointers to unions of their type.

Another way to look at a union is as a single object that can represent objects of different types at different times. Unions let you use objects whose type and size can change as the program progresses, without using machine-dependent constructions. Some other languages call this concept a variant record.

The syntax for defining unions is very similar to that for structures. Each union type definition creates a unique type. Names of union members must be unique within the union, but they can be duplicated in other nested or unnested unions or name spaces. For example:


union { 
  int a; 
  union { 
    int a;  /* This 'a' refers to a different object 
               than the previous 'a'                */ 
  }; 
}; 

The size of a union is the amount of storage necessary for its largest member, plus any padding needed to meet alignment requirements.

Once a union is defined, a value can be assigned to any of the objects declared in the union declaration. For example:


union name { 
  double dvalue; 
  struct x { int value1; int value2; }; 
  float fvalue; 
} alberta; 
alberta.dvalue = 3.141596; /* Assigns the value of pi to the union object */ 

Here, alberta can hold a double , struct , or float value. The programmer has responsibility for tracking the current type of object contained in the union. An assignment expression can be used to change the type of value held in the union.

Undefined behavior results when a union is used to store a value of one type, and then the value is accessed through another type. For example:


/* 
    Assume that `node' is a typedef_name for objects for which 
    information has been entered into a hash table; 
 
    `hash_entry' is a structure describing an entry in the hash table. 
    The member `hash_value' is a pointer to the relevant `node'. 
 */ 
typedef struct hash_entry 
{ 
   struct hash_entry *next_hash_entry; 
   node   *hash_value; 
   /* ... other information may be present ... */ 
} hash_entry; 
 
extern hash_entry *hash_table [512]; 
 
/* 
    `hash_pointer' is a union whose members are a pointer to a 
    `node' and a structure containing three bit fields that 
    overlay the pointer value.  Only the second bit field is 
    being used, to extract a value from the middle 
    of the pointer to be used as an index into the hash table.  
    Note that nine bits gives a range of values from 0 to 511;  
    hence, the size of `hash_table' above. 
 */ 
typedef union 
{ 
   node *node_pointer; 
   struct 
   { 
    unsigned : 4; 
    unsigned  index : 9; 
    unsigned :19; 
   } bits; 
} hash_pointer; 

3.5 void Type

The void type is an incomplete type that cannot be completed.

The void type has three important uses:

The following example shows how void is used to define a function, with no parameters, that does not return a value:


void message(void) 
{ 
  printf ("Stop making sense!"); 
} 

The next example shows a function prototype for a function that accepts a pointer to any object as its first and second argument:


void memcopy (void *dest, void *source, int length); 

A pointer to the void type has the same representation and alignment requirements as a pointer to a character type. The void * type is a derived type based on void .

The void type can also be used in a cast expression to explicitly discard or ignore a value. For example:


int tree(void); 
 
void main() 
{ 
  int i; 
 
  for (; ; (void)tree()){...}  /* void cast is valid                  */ 
 
  for (; (void)tree(); ;){...} /* void cast is NOT valid, because the */ 
                               /* value of the second expression in a */ 
                               /* for statement is used               */ 
 
  for ((void)tree(); ;) {...}  /* void cast is valid                  */ 
 
}  

A void expression has no value, and cannot be used in any context where a value is required.

3.6 Enumerated Types

An enumerated type is used to specify the possible values of an object from a predefined list. Elements of the list are called enumeration constants. The main use of enumerated types is to explicitly show the symbolic names, and therefore the intended purpose, of objects whose values can be represented with integer values.

Objects of enumerated type are interpreted as objects of type signed int , and are compatible with objects of other integral types.

The compiler automatically assigns integer values to each of the enumeration constants, beginning with 0. The following example declares an enumerated object background_color with a list of enumeration constants:


enum colors { black, red, blue, green, white } background_color; 

Later in the program, a value can be assigned to the object background_color :


background_color = white; 

In this example, the compiler automatically assigns the integer values as follows: black = 0, red = 1, blue = 2, green = 3, and white = 4. Alternatively, explicit values can be assigned during the enumerated type definition:


enum colors { black = 5, red = 10, blue, green = 7, white = green+2 }; 

Here, black equals the integer value 5, red = 10, blue = 11, green = 7, and white = 9. Note that blue equals the value of the previous constant ( red ) plus one, and green is allowed to be out of sequential order.

Because the ANSI C standard is not strict about assignment to enumerated types, any assigned value not in the predefined list is accepted without complaint.

3.7 Type Qualifiers

There are four type qualifiers:

Type qualifiers were introduced by the ANSI C standard to, in part, give you greater control over the compiler's optimizations. The const and volatile type qualifiers can be applied to any type. The __restrict type qualifier can be applied only to pointer types.

Note that because the __restrict type qualifier is not part of the 1989 ANSI C standard, this keyword has double leading underscores. The next version (9X) of the C standard is expected to adopt the keyword restrict with the same semantics described in this section.

The use of const gives you a method of controlling write access to an object, and eliminates potential side effects across function calls involving that object. This is because a side effect is an alteration of an object's storage and const prohibits such alteration.

Use volatile to qualify an object that can be changed by other processes or hardware. The use of volatile disables optimizations with respect to referencing the object. If an object is volatile qualified, it may be changed between the time it is initialized and any subsequent assignments. Therefore, it cannot be optimized.

Function parameters, however, do not all share the type qualification of one parameter. For example:


int f( const int a, int b)   /*  a is const qualified; b is not  */ 

When using a type qualifier with an array identifier, the elements of the array are qualified, not the array type itself.

The following declarations and expressions show the behavior when type qualifiers modify an array or structure type:


const struct s { int mem; } cs = { 1 }; 
struct s ncs;                        /* ncs is modifiable         */ 
typedef int A[2][3]; 
const A a = {{4, 5, 6}, {7, 8, 9}};  /*  array of array of const  */ 
                                     /*  int's                    */ 
int *pi; 
const int *pci; 
 
ncs = cs;            /*  Valid                                    */ 
cs = ncs;            /*  Invalid, cs is const-qualified           */ 
pi = &ncs.mem;       /*  Valid                                    */ 
pi = &cs.mem;        /*  Violates type constraints for = operator */ 
pci = &cs.mem;       /*  Valid                                    */ 
pi = a[0];           /*  Invalid; a[0] has type "const int *"     */ 

3.7.1 const Type Qualifier

Use the const type qualifier to qualify an object whose value cannot be changed. Objects qualified by the const keyword cannot be modified. This means that an object declared as const cannot serve as the operand in an operation that changes its value; for example, the ++ and -- operators are not allowed on objects qualified with const . Using the const qualifier on an object protects it from the side effects caused by operations that alter storage.

The declaration of const -qualified objects can be slightly more complicated than that for nonqualified types. Here are some examples, with explanatory comments:


const int x = 44;   /*  const qualification of int type  -- 
                        the value of x cannot be modified  */  
const int *z;       /*  Pointer to a constant integer    -- 
                        The value in the location pointed 
                        to by z cannot be modified         */ 
int * const ptr;    /*  A constant pointer -- a pointer  
                        which will always point to the 
                        same location                      */ 
const int *const p; /*  A constant pointer to a constant 
                        integer -- neither the pointer or 
                        the integer can be modified        */ 
const const int y;  /*  Illegal - redundant use of const   */ 

The following rules apply to the const type qualifier:

3.7.2 volatile Type Qualifier

Any object whose type includes the volatile type qualifier indicates that the object should not be subject to compiler optimizations altering references to, or modifications of, the object.

Note

volatile objects are especially prone to side effects. (See Section 2.5.)

Optimizations that are defeated by using the volatile specifier can be categorized as follows:

An object without the volatile specifier does not compel the compiler to perform these optimizations; it indicates that the compiler has the freedom to apply the optimizations depending on program context and compiler optimization level.

The volatile qualifier forces the compiler to allocate memory for the volatile object, and to always access the object from memory. This qualifier is often used to declare that an object can be accessed in some way not under the compiler's control. Therefore, an object qualified by the volatile keyword can be modified or accessed in ways by other processes or hardware, and is especially vulnerable to side effects.

The following rules apply to the use of the volatile qualifier:

3.7.3 __unaligned Type Qualifier

Use this data-type qualifier in pointer definitions to indicate to the compiler that the data pointed to is not properly aligned on a correct address. (To be properly aligned, the address of an object must be a multiple of the size of the type. For example, two-byte objects must be aligned on even addresses.)

When data is accessed through a pointer declared __unaligned , the compiler generates the additional code necessary to copy or store the data without causing alignment errors. It is best to avoid use of misaligned data altogether, but in some cases the usage may be justified by the need to access packed structures, or by other considerations.

Here is an example of a typical use of __unaligned :


typedef enum {int_kind, float_kind, double_kind} kind; 
void foo(void *ptr, kind k) { 
    switch (k) { 
    case int_kind: 
        printf("%d", *(__unaligned int *)ptr); 
        break; 
    case float_kind: 
        printf("%f", *(__unaligned float *)ptr); 
        break; 
    case double_kind: 
        printf("%f", *(__unaligned double *)ptr); 
        break; 
    } 
} 

3.7.4 __restrict Type Qualifier

Use the __restrict type qualifier on the declaration of a pointer type to indicate that the pointer is subject to compiler optimizations. Restricted pointers are expected to be an addition to the 9X revision of the ISO C Standard. Using restricted pointers judiciously can often improve the quality of code output by the compiler.

3.7.4.1 Rationale

The following sections describe the rationale for restricted-pointer support.

3.7.4.1.1 Aliasing

For many compiler optimizations, ranging from simply holding a value in a register to the parallel execution of a loop, it is necessary to determine whether two distinct lvalues designate distinct objects. If the objects are not distinct, the lvalues are said to be aliases. If the compiler cannot determine whether or not two lvalues are aliases, it must assume that they are aliases and suppresses various optimizations.

Aliasing through pointers presents the greatest difficulty, because there is often not enough information available within a single function, or even within a single compilation unit, to determine whether two pointers can point to the same object. Even when enough information is available, this analysis can require substantial time and space. For example, it could require an analysis of a whole program to determine the possible values of a pointer that is a function parameter.

3.7.4.1.2 Library Examples

Consider how potential aliasing enters into implementations in C of two Standard C library functions memmove and memcpy :

The following example contrasts sample implementations of the memcpy and memmove functions:


/* Sample implementation of memmove */ 
 
   void *memmove(void *s1, const void *s2, size_t n) { 
           char * t1 = s1; 
           const char * t2 = s2; 
           char * t3 = malloc(n); 
           size_t i; 
           for(i=0; i<n; i++) t3[i] = t2[i]; 
           for(i=0; i<n; i++) t1[i] = t3[i]; 
           free(t3); 
           return s1; 
   } 
 
 
/* Sample implementation of memcpy */ 
 
   void *memcpy(void *s1, const void *s2, size_t n); 
           char * t1 = s1; 
           const char * t2 = s2; 
           while(n-- > 0) *t1++ = *t2++; 
           return s1; 
   } 

The restriction on memcpy is expressed only in its description in the Standard, and cannot be expressed directly in its implementation in C. While this allows the source-level optimization of eliminating the temporary used in memmove , it does not provide for compiler optimization of the resulting single loop.

In many architectures, it is faster to copy bytes in blocks, rather than one at a time:


Previous Next Contents Index
  

1.800.AT.COMPAQ

privacy and legal statement