CS140 Lecture notes -- Unions

  • Jim Plank
  • Directory: /home/plank/cs140/Notes/Unions
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs140/Notes/Unions/
  • Tue Sep 18 15:43:30 EDT 2007

    Unions

    Unions are a modification of structs that let you use the same field for different types. This is only useful at very specific times, one of those being when you want to have a field in your struct that can be anything -- int, float, double, pointer, etc.

    In this class, you should not attempt to use unions in your code unless you are asked to do so. However, some of my code will have unions, so you'll have to know how to deal with them.

    A union is very simple -- it looks just like a struct:

      typedef union {
        int i;
        float f;
        double d;
      } Ifd;
    
    However, in a struct, you can use any and all of those fields. In a union, you can only use one at a time. Think if it as an either-or kind of thing. If I declare an Ifd above, then I can use it as an int, a float or a double, but I can't interchange them.

    Here is a trivial example, using the above struct (this is in union1.c):

    #include <stdio.h>
    
    typedef union {
      int i;
      float f;
      double d;
    } Ifd;
    
    main()
    {
      Ifd ifd;
      int i;
      float f;
    
      ifd.i = 1;
      i = 1;
    
      printf("%d %d\n", i, ifd.i);
    
      ifd.f = 5.55;
      f = ifd.f;
    
      printf("%f %f\n", ifd.f, f);
    
      printf("%d\n", ifd.i);
    }
    

    So, when we use ifd.i, this means we can use ifd as an integer. When we use ifd.f, we can use ifd as a floating point number. However, unlike a struct, we can't use both ifd.i and ifd.f at the same time. This is because ifd is not an aggregate data type with three fields -- it is a type that holds one value, which can be any of three types -- integer, float or double.

    So, when we look at the above code, the first three lines say:

    The next three lines say: Now, the last line is problemmatic, and a good example of where people get tripped up with unions. Here we print out ifd.i. The problem is that we're currently using ifd as a float -- therefore, the value of ifd.i is undetermined. We might expect the C compiler to automatically cast ifd.f to an integer, but it doesn't -- when you use a union incorrectly, as in this last line, you can get anything.

    Here's the output -- note the last line -- indeed, using ifd.i when you have set ifd.f gives you something that is not very useful:

    UNIX> union1
    1 1
    5.550000 5.550000
    1085381018
    UNIX> 
    

    When do we use unions?

    We use unions when we want to have our code work on any type, but we don't know what that type will be until run time. Again, we'll see useful examples of this later in class. Here is a slightly contrived one. Suppose we want to write code that reads in five items into an array, and then prints them out. The problem is that we don't know what an item will be -- it could be an int, a float, or a one-word character string. When we read an item, we'll make the user specify what kind of item it is. For example, here's some valid input:
    int 3
    float -4.33
    string Jim
    int -67
    float 40000.1
    
    Now, we could structure this code in one of two ways. First, lets write it without using unions. We can have a struct for each item, and this struct has four fields: type, i, f and s. The type field is a character that identifies the type as either 'i' for integer, 'f' for float, and 's' for string. Then if type is 'i', then the i field contains the value. If type is 'f', then the f field contains the value. If type is 's', then the s field contains the value.

    Here's the code (this is in union2.c):

    #include <stdio.h>
    #include <string.h>
    #include "fields.h"
    
    typedef struct {
      char type;
      int i;
      float f;
      char *s;
    } Item;
    
    main()
    {
      Item array[5];
      int i;
      IS is;
    
      is = new_inputstruct(NULL);
    
      /* Read in the items -- if "int", read it into array[i].i
                              if "float", read it into array[i].f
                              if "string", read it into array[i].s */
    
      for (i = 0; i < 5; i++) {
        if (get_line(is) != 2) exit(1); 
    
        if (strcmp(is->fields[0], "int") == 0) {
          array[i].type = 'i';
          if (sscanf(is->fields[1], "%d", &(array[i].i)) != 1) exit(1); 
    
        } else if (strcmp(is->fields[0], "float") == 0) {
          array[i].type = 'f';
          if (sscanf(is->fields[1], "%f", &(array[i].f)) != 1) exit(1); 
    
        } else if (strcmp(is->fields[0], "string") == 0) {
          array[i].type = 's';
          array[i].s = strdup(is->fields[1]);
    
        } else {
          exit(1);
        }
      }
    
      /* Write out the items. */
    
      for (i = 0; i < 5; i++) {
        printf("Item %d: Type %c -- ", i, array[i].type);
        if (array[i].type == 'i') {
          printf("Value: %d\n", array[i].i);
        } else if (array[i].type == 'f') {
          printf("Value: %f\n", array[i].f);
        } else if (array[i].type == 's') {
          printf("Value: %s\n", array[i].s);
        } else {
          exit(1);
        }
      }
     
      /* Print the size of the item struct */
    
      printf("\n");
      printf("Sizeof(Item): %d\n", sizeof(Item));
    }
    

    Now, when you give it the example input above, you get: (I've bold-faced the input):

    UNIX> union2
    int 3
    float -4.33
    string Jim
    int -67
    float 40000.1
    Item 0: Type i -- Value: 3
    Item 1: Type f -- Value: -4.330000
    Item 2: Type s -- Value: Jim
    Item 3: Type i -- Value: -67
    Item 4: Type f -- Value: 40000.101562
    
    Sizeof(Item): 16
    UNIX> 
    
    A little yucky, but certainly code that you all are capable of writing. One of the problems with this code is how wasteful it is. Every item contains two fields (8 bytes) that it does not use. Of course, in this example, that leads to a whopping 40 bytes, so no, it's not a problem here. But if we had a million items, then it would be 8 megabytes worth of wasted space.

    The solution is to use a union for the value of the item. The code is in union3.c. Note how similar it is to union2.c:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "fields.h"
    
    typedef struct {
      char type;
      union {
        int i;
        float f;
        char *s;
      } value;
    } Item;
    
    main()
    {
      Item array[5];
      int i;
      IS is;
    
      is = new_inputstruct(NULL);
    
      /* Read in the items -- if "int", read it into array[i].i
                              if "float", read it into array[i].f
                              if "string", read it into array[i].s */
    
      for (i = 0; i < 5; i++) {
        if (get_line(is) != 2) exit(1); 
    
        if (strcmp(is->fields[0], "int") == 0) {
          array[i].type = 'i';
          if (sscanf(is->fields[1], "%d", &(array[i].value.i)) != 1) exit(1); 
    
        } else if (strcmp(is->fields[0], "float") == 0) {
          array[i].type = 'f';
          if (sscanf(is->fields[1], "%f", &(array[i].value.f)) != 1) exit(1); 
    
        } else if (strcmp(is->fields[0], "string") == 0) {
          array[i].type = 's';
          array[i].value.s = strdup(is->fields[1]);
    
        } else {
          exit(1);
        }
      }
    
      /* Write out the items. */
    
      for (i = 0; i < 5; i++) {
        printf("Item %d: Type %c -- ", i, array[i].type);
        if (array[i].type == 'i') {
          printf("Value: %d\n", array[i].value.i);
        } else if (array[i].type == 'f') {
          printf("Value: %f\n", array[i].value.f);
        } else if (array[i].type == 's') {
          printf("Value: %s\n", array[i].value.s);
        } else {
          exit(1);
        }
      }
     
      /* Print the size of the item struct */
    
      printf("\n");
      printf("Sizeof(Item): %d\n", sizeof(Item));
    }
    

    The output is the same, only now you'll notice that the size of an item is only 8 bytes:

    UNIX> union3
    int 3
    float -4.33
    string Jim
    int -67
    float 40000.1
    Item 0: Type i -- Value: 3
    Item 1: Type f -- Value: -4.330000
    Item 2: Type s -- Value: Jim
    Item 3: Type i -- Value: -67
    Item 4: Type f -- Value: 40000.101562
    
    Sizeof(Item): 8
    UNIX> 
    
    This is because the union only allocates enough space for its biggest field -- here they are all 4 bytes, so the union is just four bytes.