C Types, Casting, Malloc, Segementation Violations and Bus Errors


TYPES in C

In C, there are three kinds of types that variables can have -- scalars, aggregates, and pointers. Half of the game in getting things right in C is keeping yourself from being confused about types. This lecture tries to elaborate on this a little.

Scalar Types

There are 7 scalar types in C:
  • char -- 1 byte
  • short -- 2 bytes
  • int -- 4 bytes
  • long -- 4 or 8 bytes, depending on the system and compiler
  • float -- 4 bytes
  • double -- 8 bytes
  • (pointer -- 4 or 8 bytes, depending on the system and compiler)

    These should all be familiar to you (ok, maybe not a short, but the rest should). If you want to verify or use the size of a type in C, you use the macro sizeof(). For example, sizeof(long) will return either 4 or 8, depending on how big a long is in your system.

    You can declare a scalar variable in one of three places: As a global variable, as a procedure parameter, and as a local variable. For example, look at the program below in p1.c:

    (In this and all other lecture notes, you can copy the programs and the makefile into your own directory, and then compile them by using make. E.g. to make the program p1, you say ``make p1'').

    #include <stdio.h>
    
    int i;
    
    main(int argc, char **argv)
    {
      int j;
    
      j = argc;
      i = j;
      printf("%d\n", i);
    }
    

    There are three scalar int variables here -- i, j, and argc. I is a global variable. J is a local variable, and argc is a parameter. Scalars are pretty straightforward. You can pass them as parameters to procedures, and return them from procedures without worrying about anything going awry.


    Aggregate Types

    Arrays and structs are aggregate types in C. They are more complex than scalars. You can statically declare an array as a global or local variable -- I do both below in p2.c:

    #include <stdio.h>
    
    char s1[15];
    
    main(int argc, char **argv)
    {
      char s2[4];
    
    ...
    

    S1 is a global array of 15 chars and s2 is a local array of 4 chars.

    If an array has been statically declared, then you cannot assign it to another array. For example, look at p2.c:

    #include <stdio.h>
    
    char s1[15];
    
    main(int argc, char **argv)
    {
      char s2[4];
      
      s2 = "Jim";
    }
    

    The statement ``s2 = "Jim"'' is illegal in C, because s2 has been statically declared. If you try to compile this program, gcc will give you an error:

    UNIX> gcc -o p2 p2.c
    p2.c: In function `main':
    p2.c:10: incompatible types in assignment
    UNIX>
    
    This is a good rule to bear in mind -- if x is an array, then you should NEVER say ``x = something''. It will usually give you an error. However, if it doesn't (because they change the compiler yet again), you are writing bad C code.

    However, you can always say ``something = x''. We'll discuss later in the lecture.


    Structs

    The second way to aggregate data is with a struct. A struct looks a little like a C++ class with some notable omissions: Suppose we want to aggregate an int and a double. We can do that as in id1.c

    #include <stdio.h>
    #include <stdlib.h>
    
    struct intdouble {
      int i;
      double d;
    };
    
    main()
    {
      struct intdouble id1;
    
      id1.i = 5;
      id1.d = 3.14;
    
      printf("%d %.2lf\n", id1.i, id1.d);
    }
    

    UNIX> id1
    5 3.14
    UNIX> 
    
    You can use a typedef to make it a little more readable (id2.c):

    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct intdouble {
      int i;
      double d;
    } ID;
    
    main()
    {
      ID id1;
    
      id1.i = 5;
      id1.d = 3.14;
    
      printf("1: %d %.2lf\n", id1.i, id1.d);
    }
    

    You may have some confusion with structs, because they exist in C++ with different semantics. Let's take a look at the following code in C++, which declares two intdouble's, sets one's variables, and then copies one to the other (id3.cpp):

    #include <cstdio>
    #include <iostream>
    using namespace std;
    
    struct intdouble {
      int i;
      double d;
    };
    
    main()
    {
      intdouble id1, id2;
    
      id1.i = 5;
      id1.d = 3.14;
    
      id2 = id1;
      id2.i += 5;
      id2.d += 5;
    
      printf("1: %d %.2lf\n", id1.i, id1.d);
      printf("2: %d %.2lf\n", id2.i, id2.d);
    }
    

    Straightforward when it runs:

    UNIX> id3
    1: 5 3.14
    2: 10 8.14
    UNIX> 
    
    Let's change this to C. If we simply fix the headers, this will not compile. That is because C++ creates "intdouble" as a type, and C does not. When we try to compile, it fails:
    UNIX> gcc -o id4 id4.c
    id4.c: In function 'main':
    id4.c:11: error: 'intdouble' undeclared (first use in this function)
    id4.c:11: error: (Each undeclared identifier is reported only once
    id4.c:11: error: for each function it appears in.)
    id4.c:11: error: expected ';' before 'id1'
    id4.c:13: error: 'id1' undeclared (first use in this function)
    id4.c:16: error: 'id2' undeclared (first use in this function)
    UNIX> 
    
    Now, if you put "struct" in front of "intdouble," that will fix the problem. It is in id5.c:

    #include <stdio.h>
    #include <stdlib.h>
    
    struct intdouble {
      int i;
      double d;
    };
    
    main()
    {
      struct intdouble id1, id2;
    
      id1.i = 5;
      id1.d = 3.14;
    
      id2 = id1;
    
      id2.i += 5;
      id2.d += 5;
    
      printf("1: %d %.2lf\n", id1.i, id1.d);
      printf("2: %d %.2lf\n", id2.i, id2.d);
    }
    

    This runs identically to the C++ version. I can tell you that I disapprove of this code. Why, because the statement ``id2 = id1'' offends me. It is the only part of C where you can copy an unspecified number of bytes with an assignment statement. It is a weakness of the language. For example, take a look at id5a.c:

    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
      int a[1000];
    } SID;
    
    main()
    {
      SID s1, s2;
      int i;
    
      for (i = 0; i < 1000; i++) s1.a[i] = i;
    
      s2 = s1;  /* THE OFFENDING LINE */
    
      for (i = 0; i < 1000; i++) printf("%4d %4d\n", s1.a[i], s2.a[i]);
     
      exit(1);
    }
    

    The ``s2 = s1'' line copies 4000 bytes. While I expect such garbage in C++, I am surprised that it's legal in C. Why does it allow you to copy the struct, but not to copy the array? Who knows -- anyway, I want you to be aware of it. You will never see me use that feature of the language because I don't approve. As a corollary, you can pass a struct as an argument to a procedure, and in C++ fashion, it makes a copy of the entire thing. You'll note that in id5b.c, I define a procedure a(), which changes the last element of the array, and when we run it, s1.a[999] is unchanged. Make a mental note of it.

    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
      int a[1000];
    } SID;
    
    void a(SID s)
    {
      s.a[999] = -1;
    }
    
    
    main()
    {
      SID s1, s2;
      int i;
    
      for (i = 0; i < 1000; i++) s1.a[i] = i;
      s2 = s1;
    
      a(s1);
      for (i = 0; i < 1000; i++) printf("%4d %4d\n", s1.a[i], s2.a[i]);
     
      exit(1);
    }
    

    UNIX> id5b | tail
     990  990
     991  991
     992  992
     993  993
     994  994
     995  995
     996  996
     997  997
     998  998
     999  999
    UNIX> 
    

    A final note about C++ structs. They are basically stripped down classes -- you can put methods in them, and then implement the methods using the struct's variables, as in id6.cpp:

    #include <cstdio>
    #include <iostream>
    using namespace std;
    
    struct intdouble {
      int i;
      double d;
      void Print();
    };
    
    void intdouble::Print()
    {
      printf("   %d %.2lf\n", i, d);
    }
      
    main()
    {
      intdouble id1, id2;
    
      id1.i = 5;
      id1.d = 3.14;
    
      id2 = id1;
      id2.i += 5;
      id2.d += 5;
    
      id1.Print();
      id2.Print();
    }
    

    Whoever designed structs in C++ did the world a disservice, because you have the same constructs in two fairly similar languages that have completely different semantics. Which means that tons of people are going to be confused going from C++ to C, or even C to C++ when their struct semantics are off. It is for this reason that I don't teach structs as a C++ construct in either CS140 or CS302. Just use a class.


    Pointers

    Pointers are where most people mess up in C. A pointer is simply a pointer to memory. Memory can be allocated in one of two ways -- by declaring variables, or by calling malloc() (there is no new in C). Whenever memory has been allocated, you can set a pointer to it.

    You can view memory as one huge array of bytes (chars). This array has 232 or 264 elements. Usually, we consider the indices to this array in hexadecimal. In other words, the array goes from 0x0 to 0xffffffff (or 0xffffffffffffffff).

    A pointer is simply an index of this array. Whenever we allocate x bytes of memory, we are reserving x contiguous elements from the memory array. If we set a pointer to these bytes, then that pointer will be the index of the first allocated byte in memory.

    For example, look at the following program (in p3.c):

    main()
    {
      int i;
      char j[14];
      int *ip;
      char *jp;
    
      ip = &i;
      jp = j;
    
      printf("ip = 0x%lx.  jp = 0x%lx\n", ip, jp);
    }
    

    This program allocates one integer (i), an array of 14 characters (j), and two pointers (ip and jp). It then sets the pointers so that they point to the memory allocated for i and j. Finally, it prints out the values of those pointers -- these are indices into the memory array.

    Unfortunately, when we try to compile this, we get warnings. Don't worry about them yet -- we'll get to that. It still compiles correctly.

    When we run it, we get:

    UNIX> p3
    ip = 0x7fff2efcdd9c.  jp = 0x7fff2efcdda0
    UNIX> 
    
    What this means is that when we view memory as an array, elements 0x7fff2efcdd9c, 0x7fff2efcdd9d, 0x7fff2efcdd9e, and 0x7fff2efcdd9f are allocated for the local variable i, and elements 0x7fff2efcdda0 through 0x7fff2efcddad are allocated for the array j.

    Note that I said ``jp = j'' and not ``jp = &j''. This is because when treated as an expression, an array is equivalent to a pointer. The only difference is that you cannot assign a value to an array variable. Thus, you can say ``jp = j'', but you cannot say ``j = jp''. Moreover, you cannot take the address of an array variable -- saying ``&j'' is illegal.

    Pointers are a little like scalars -- they too can be declared as globals, locals or parameters, and can be assigned values, passed as parameters, and returned from procedures. On our lab machines, pointers are 8 bytes. Thus, in p3.c, there are 34 bytes of local variables allocated in the main() procedure -- 4 for i, 14 for j, 8 for ip, and 8 for jp.


    Type Casting (sometimes called ``type coercion'')

    There are times when you would like to take x bytes of memory of a certain type, and assign them to y bytes of memory of another type. This is called ``type casting''. A simple example is when you want to turn a char into an int, or an int into a float as in p4.c:

    main()
    {
      char c;
      int i;
      float f;
    
      c = 'a';
      i = c;
      f = i;
      printf("c = %d (%c).   i = %d (%c).  f = %f\n", c, c, i, i, f);
    }
    
    

    The statement `i = c' is a type cast, as is the statement `f = i'.

    Some type castings, like the one above, are very natural. The C compiler will do these for you without complaining. Most others, however, the C compiler will complain about, unless you specifically tell it that you are doing a type cast (this is a way of telling the compiler ``Yes, I know what I'm doing.'').

    An example is program p3.c above -- as mentioned, when we compile it, we get warnings:

    UNIX> make p3
    gcc -g -c p3.c
    p3.c: In function 'main':
    p3.c:13: warning: format '%lx' expects type 'long unsigned int', but argument 2 has type 'int *'
    p3.c:13: warning: format '%lx' expects type 'long unsigned int', but argument 3 has type 'char *'
    gcc -g -o p3 p3.o
    UNIX> 
    
    What's going on is that the compiler parses the format string of printf() and gleans that "%lx" desires a long unsigned int, but it's getting an (int *). You go ahead and perform a type cast on the argument to tell the compiler "Yes, this is an (int *) but treat it like a (long unsigned int), please. I know what I'm doing." That's in p5.c:

    #include <stdio.h>
    
    main()
    {
      int i;
      char j[14];
      int *ip;
      char *jp;
    
      ip = &i;
      jp = j;
    
      printf("ip = 0x%lx.  jp = 0x%lx\n", (long unsigned int) ip, (long unsigned int) jp);
    }
    

    The compiler, happy that you have taken responsibility for using mixmatched types, compiles it without any warnings:

    UNIX> make p5
    gcc -g -c p5.c
    gcc -g -o p5 p5.o
    UNIX> p5
    ip = 0x7fff6ad53dfc.  jp = 0x7fff6ad53e00
    UNIX> 
    
    On some machines (not ours), both pointers and ints are 4 bytes. This has led many people to treat pointers and ints as interchangable. For example, look at the code in p8.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    main()
    {
      char s[4];
      int i;
      char *s2;
    
      strcpy(s, "Jim");
      i = (int) s;
      printf("i = %d (0x%x)\n", i, i);
      printf("s = %ld (0x%lx)\n", (long unsigned int) s, (long unsigned int) s);
    
      i++;
      s2 = (char *) i;
      printf("s = 0x%lx.  s2 = 0x%lx, i = 0x%x\n", 
               (long unsigned int) s, 
               (long unsigned int) s2, i);
      printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
    }
    

    When you set i equal to s, you are losing 4 bytes of information, because ints are four bytes, and pointers are eight. When you set s2 back to i, it fills in those four bytes that i is missing. Thus it will be an illegal address, and you will get a segmentation violation:

    UNIX> p8
    i = -206846176 (0xf3abc720)
    s = 140737281509152 (0x7ffff3abc720)
    s = 0x7ffff3abc720.  s2 = 0xfffffffff3abc721, i = 0xf3abc721
    Segmentation fault
    UNIX> 
    
    Why does s2 have all of those f's? Because the sign bit of i is negative. Thus, when we set s2 to i, it fills in the missing four bytes with ones, making s2 negative.

    The compilers on our lab machines are happy to warn you about your potential problems, as evidenced by the warnings here:

    UNIX> make p8
    gcc -g -o p8 p8.c
    p8.c: In function 'main':
    p8.c:12: warning: cast from pointer to integer of different size
    p8.c:17: warning: cast to pointer from integer of different size
    UNIX> 
    
    On a machine with 32-bit pointers, this code will work fine, because now integers and pointers are the same size. You can force our machines to do that with the compilation flag "-m32", and the program works well. The compiler doesn't complain either:
    UNIX> gcc -m32 -o p8-32 p8.c
    UNIX> p8-32
    i = -4643812 (0xffb9241c)
    s = -4643812 (0xffb9241c)
    s = 0xffb9241c.  s2 = 0xffb9241d, i = 0xffb9241d
    s[0] = J, s[1] = i, *s2 = i
    UNIX> 
    
    If we instead use a long for i instead of an int, everything works fine, since longs and pointers are guaranteed to be the same size, be that 4 or 8 bytes. The program p9.c makes the requisite changes:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    main()
    {
      char s[4];
      long i;
      char *s2;
    
      strcpy(s, "Jim");
      i = (long) s;
      printf("i = %ld (0x%lx)\n", i, i);
      printf("s = %ld (0x%lx)\n", (long unsigned int) s, (long unsigned int) s);
    
      i++;
      s2 = (char *) i;
      printf("s = 0x%lx.  s2 = 0x%lx, i = 0x%lx\n", 
               (long unsigned int) s, 
               (long unsigned int) s2, i);
      printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
    }
    

    UNIX> make p9
    gcc -g -c p9.c
    gcc -g -o p9 p9.o
    UNIX> p9
    i = 140733481930528 (0x7fff1132cf20)
    s = 140733481930528 (0x7fff1132cf20)
    s = 0x7fff1132cf20.  s2 = 0x7fff1132cf21, i = 0x7fff1132cf21
    s[0] = J, s[1] = i, *s2 = i
    UNIX> 
    
    Compilers and machines all differ. Some machines (like my old macintosh) have 32-bit pointers and rather laconic compilers that don't give you many warnings. Others, like our lab machines, have 64-bit pointers and downright chatty compilers. Be ready for all of them.

    Malloc and Free

    There is no new or delete in C. Their functionality is taken by the library calls malloc() and free(). Read their man pages to see their prototypes and include statements. This one is from my Linux box in 2015:
    SYNOPSIS
    
           #include 
    
           void *malloc(size_t size);
           void free(void *ptr);
    
    Like new, malloc() allocates bytes of memory from the operating system. Unlike new, which requires you to give it information about the data type that it is allocating, malloc() simply asks for the number of bytes, and if it is successful, it will return a pointer to at least that many bytes, allocated for you by the operating system. It returns a void *, which means it's a pointer, but malloc() doesn't know what it's pointing to. Fortunately, you do know what it's pointing to, and that is what you set its return value to.

    To figure out how many bytes you need from malloc(), you call sizeof(type). For example, to allocate one integer, you would call malloc(sizeof(int)). Often you want to allocate an array of a data type. To do that, you multiply sizeof(type) by the number of elements. Your pointer will point to the first of these elements. The next element will be sizeof(type) bytes after the pointer. And so on. We'll explore this more soon.

    For now, take a look at pm.c

    #include <stdio.h>
    #include <stdlib.h>
    
    int *give_me_some_ints(int n)
    {
      int *p;
      int i;
    
      p = (int *) malloc(sizeof(int) * n);
      if (p == NULL) { fprintf(stderr, "malloc(%d) failed.\n", n); exit(1); }
      return p;
    }
    
    void fill_in_the_ints(int *a, int n)
    {
      int i;
    
      for (i = 0; i < n; i++) a[i] = lrand48();
    }
    
    main(int argc, char **argv)
    {
      int *array;
      int size;
      int i;
    
      if (argc != 2) { fprintf(stderr, "usage: pm size\n"); exit(1); }
      size = atoi(argv[1]);
    
      array = give_me_some_ints(size);
      fill_in_the_ints(array, size);
    
      for (i = 0; i < size; i++) printf("%4d %10d\n", i, array[i]);
      exit(0);
    }
    

    The procedure give_me_some_ints() allocates an array of n integers and returns a pointer to the array. fill_in_the_ints() takes a pointer to the array, plus its size, and fills it in. Because we are passing pointers, no copies of the array are made. In other words, fill_in_the_ints() fills in the array that was created by the malloc() call. Finally, we print out the array.

    free() returns the memory so that it may be reused. It is analogous to delete in C++. You simply pass it the pointer that malloc() returned. Don't pass it any other pointer, or a pointer that has already been freed, or really ugly things can happen (you'll see this in detail later in the class).

    Although some people disagree with this, I am of the opinion that you should only free memory that you are going to need for reuse, or perhaps that the system may want to use for other programs. We'll see an example of that in the next lecture. If you are simply allocating memory and then exiting your program, don't bother freeing the memory. The operating system will reclaim it when the program exits.


    Segmentation Violations and Bus Errors

    Memory can be viewed as a giant array of bytes. However, certain parts of this array are not accessible. For example, typically, elements 0 to 0x1000 are inaccessible. When you try to access an inaccessible element, you generate a segmentation violation. (In the old days, it would also store the contents of memory to a file, called a core dump. These days, memory is so big that we don't generate core dumps, although we could if we had to.)

    The reason element 0 is inaccessible is that it's a common bug to forget to initialize a pointer. When that happens, the pointer's value is zero, and when you try to dereference it, you'll generate a segmentation violation, which helps you find the bug.

    The program pa.c generates a segmentation violation by trying to dereference NULL:

    #include <stdio.h>
    
    main()
    {
      char *s;
    
      s = NULL;
    
      printf("%d\n", s[0]);
    }
    

    UNIX> pa
    Segmentation fault
    UNIX> 
    
    On many machines. Whenever you access a scalar type, its value in memory must be aligned. What this means is that if the type is 4 bytes, its location in memory must start at a memory index that is a multiple of 4. For example, if i is an (int *), then if i is not a multiple of 4, dereferencing i will be an error. This error is manifested by a bus error. Unfortunately, our machines don't demonstrate this problem, so I won't write code for it.

    More Type Casting, Memory and Representations

    With C, you can do "dangerous" things, like treat memory generically. In other words, suppose you have a region of bytes. You can treat that region as if it's holding any type you want -- integers, chars, doubles, structs, whatever. Typically, you don't want to leverage this flexibility, because it can get you into a lot of trouble. However, when you're writing systems programs, this flexbility is often essential. It also helps you understand memory and what is going on with your machine while programs are running.

    Let's take an example. This was written by a student, Jay Pickens, while he took CS360 in 2015:

    "One little, two little, three little-endians."

    By Jay Pickens (with a little editing from Dr. Plank):

    This is primarily intended to be a visual aide dealing with memory, integers, little-endian format, and partial offsets. If all you want is pics, skip to the end.

    Take a look at endian.c

    #include <stdio.h>
    
    typedef unsigned long UL;
    
    int main () {
    
      /*
        I used unsigned variables to shut up the compiler
        and to prevent preservation of negative integers when
        examining the contents of the array when viewed as
        individual bytes.
      */
      unsigned int arr[4];  /* An array of 4 integers */
      unsigned char* bp;    /* Pointer to be used to examine byte order */
      unsigned int* arrp;   /* Pointer to offset array */
    
      int i;  /* generic */
    
      /*
        Each byte in the array will be uniquely identified by a sequence
        two hex digits.  This was arbitrarily chosen as to ease visual
        identification of individual bytes;
      */
      arr[0] = 0x00112233;
      arr[1] = 0x44556677;
      arr[2] = 0x8899AABB;
      arr[3] = 0xCCDDEEFF;
    
      printf( "\nINT array: 0x%08lX\n", (UL) arr );
      for ( i = 0; i < 4; i++ ) {
        printf( "Index %d - Location: 0x%08lX  Size:%2d  Contents: 0x%08lX\n",
                i, (UL) (arr+i), (int) sizeof(int), (UL) arr[i] );
      }
    
      /*
        This section sets bp equal to arr.
        We may now use bp to access the array as individual bytes.
      */
    
      bp = (unsigned char *) arr;
      printf( "\nBYTE array: 0x%08lX\n", (UL) bp );
      for ( i = 0; i < 16; i++ ) {
        printf( "Index %2d - Location: 0x%08lX  Contents: 0x%02X\n", i, (UL) (bp+i), bp[i] );
      }
      printf( "\n\n" );
    
      /*
        Here we first increment the character pointer by one.
      */
      arrp = (unsigned int *) ( bp + 1 );
      printf( "Original array begin: 0x%08lX\n", (UL) arr );
      printf( "Offset array begin:   0x%08lX\n", (UL) arrp );
      printf( "Byte offset:          %ld\n", ((UL)arrp)-((UL)arr) );
      for ( i = 0; i < 4; i++ ) {
        printf( "Index %d - Location: 0x%08lX  Size:%2d  Contents: 0x%08lX\n",
                i, (UL) (arrp+i), (int) sizeof(int), (UL) arrp[i] );
      }
      printf( "\n" );
    
      return 0;
    }
    

    What this code does is declare an array of four integers, which will be a total of 16 bytes of memory. I then assigned a unique identifier to each of them, so they may be easily visually distinguished.

    Once the bytes are tagged, I then iterate over the array, displaying the memory location and contents of each. This part is basic enough.

    Next, I use the char* to examine each individual byte of the array, printing out its address and contents. I can use this to piece together the actual layout of the memory.

    Finally, I use the char* to offset another int*. I then iterate over this "array" to see how the system can interpret any four adjacent bytes as an integer. I do not encounter a bus error.

    This was run on my toybox. It is a 32-bit pentium 4 with 786MB of RAM running Debian.

    Output:
    INT array: 0xBFF33344
    Index 0 - Location: 0xBFF33344  Size: 4  Contents: 0x00112233
    Index 1 - Location: 0xBFF33348  Size: 4  Contents: 0x44556677
    Index 2 - Location: 0xBFF3334C  Size: 4  Contents: 0x8899AABB
    Index 3 - Location: 0xBFF33350  Size: 4  Contents: 0xCCDDEEFF
    
    BYTE array: 0xBFF33344
    Index  0 - Location: 0xBFF33344  Contents: 0x33
    Index  1 - Location: 0xBFF33345  Contents: 0x22
    Index  2 - Location: 0xBFF33346  Contents: 0x11
    Index  3 - Location: 0xBFF33347  Contents: 0x00
    Index  4 - Location: 0xBFF33348  Contents: 0x77
    Index  5 - Location: 0xBFF33349  Contents: 0x66
    Index  6 - Location: 0xBFF3334A  Contents: 0x55
    Index  7 - Location: 0xBFF3334B  Contents: 0x44
    Index  8 - Location: 0xBFF3334C  Contents: 0xBB
    Index  9 - Location: 0xBFF3334D  Contents: 0xAA
    Index 10 - Location: 0xBFF3334E  Contents: 0x99
    Index 11 - Location: 0xBFF3334F  Contents: 0x88
    Index 12 - Location: 0xBFF33350  Contents: 0xFF
    Index 13 - Location: 0xBFF33351  Contents: 0xEE
    Index 14 - Location: 0xBFF33352  Contents: 0xDD
    Index 15 - Location: 0xBFF33353  Contents: 0xCC
    
    
    Original array begin: 0xBFF33344
    Offset array begin:   0xBFF33345
    Byte offset:          1
    Index 0 - Location: 0xBFF33345  Size: 4  Contents: 0x77001122
    Index 1 - Location: 0xBFF33349  Size: 4  Contents: 0xBB445566
    Index 2 - Location: 0xBFF3334D  Size: 4  Contents: 0xFF8899AA
    Index 3 - Location: 0xBFF33351  Size: 4  Contents: 0x45CCDDEE
    
    Analyzing this output I can draw the following diagram:

    Each column represents an 8-bit cell in the memory. The actual memory address of each cell is in the top column, and multiple of 4 are in bold. Note: The gray areas my not be system memory, but it is not memory that has been declared.

    First of note would be the middle column, in blue. This is the array of bytes as it was initially declared and set.

    Second, we have in green the actual bytes as they lay in memory. This is a little endian system.

    Finally, in yellow we have our 1-Byte offset array. As you can see form the output, I neither got a seg-fault nor a bus error. Ths single block in red is where the shifted array goes out of declared memory. Likewise, since it does not match the tag of any declared byte, we have no idea where it came form or why it contains 0x45."

    End quote -- thanks, Jay!


    Alignment within structs

    Malloc() always returns pointers that are multiples of 8 -- therefore using malloc() to allocate any scalar type or array of scalar types will generate pointers that are correctly aligned.

    Moreover, the compiler always lays out structs so that the fields are aligned. Thus, in the following struct:

    struct {
      char b;
      int i;
    }
    
    The whole struct will be 8 bytes -- 1 for b, 3 unused, and 4 for i. The 3 bytes are necessary so that i will be aligned. The compiler does not shuffle around the fields so that they pack into memory better. So, for example, if you have:
    struct {
      char b1;
      int i1;
      char b2;
      int i2;
    }
    
    The struct will be 16 bytes:

    However, if you order them differently, you can get all of those fields into 12 bytes:

    struct {
      char b1;
      char b2;
      int i1;
      int i2;
    }
    
    Now the struct will have: Finally, the following struct's size is also 8 bytes, even though you may initially think it should only be five:
    typedef struct {
      int i;
      char b;
    } S1B;
    
    Why? Because if you wanted to allocate an array of ten of these using malloc(), you'd do the following:

    S1B *s;
    
    s = (S1B *) malloc(sizeof(S1B)*10);
    

    Now, suppose sizeof(S1B) is only 5. We know that s will be a multiple of 8 because malloc() guarantees that. What about &(s[1])? That will be five bytes greater than s, which means it won't be a multiple of 8 -- of a multiple of 4. It is unaligned, and importantly s[1].i is unaligned. By making sizeof(S1B) be 8, we have no alignment problems when we use the type in an array.

    The program pd.c confirms all of these assertions:

    UNIX> pd
    8 16 12 8
    UNIX> 
    

    A Common Type Bug

    This looks idiotic, but it is at the heart of most type bugs. Look at pc.c:

    #include <stdio.h>
    
    main()
    {
      char c;
      int i;
      int j;
    
      i = 10000;
      c = i;
      j = c;
    
      printf("I: %d,   J: %d,       C: %d\n", i, j, c);
      printf("I: 0x%04x,  J: 0x%04x,   C: 0x%04x\n", i, j, c);
    }
    

    Since c is a char, it cannot hold the value 10000. It will instead hold the lowest order byte of i, which is 16 (0x10). Then when you set j to c, you'll see that j becomes 16.

    Also, even our chatty compiler doesn't complain about it. Make sure you understand this bug and the output below:

    UNIX> make pc
    gcc -g -c pc.c
    gcc -g -o pc pc.o
    UNIX> pc
    I: 10000,   J: 16,       C: 16
    I: 0x2710,  J: 0x0010,   C: 0x0010
    UNIX>