"C Stuff 2": Pointers, Casting, Malloc, Segmentation Violations and Bus Errors


Pointers

Pointers are where most people mess up in C. A pointer is simply a index 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):

#include <stdio.h>
#include <stdlib.h>

int main()
{
  int i;
  char j[14];
  int *ip;
  char *jp;

  ip = &i;
  jp = j;

  printf("ip = 0x%lx.  jp = 0x%lx\n", ip, jp);
  exit(0);
}

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 the following (this was on my Mac in 2015)

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. When you run this on your own machine, you will get different pointer values. Regardless of the pointer values, ip will point to the first of the four bytes of i, and jp will point to the first of the fourteen bytes of 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.

One of the nice things about 32-bit machines is that their pointers don't seem so unwieldy. On my Pi, the output is:

pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ UNIX> .p3
ip = 0x7eeb050c.  jp = 0x7eeb04fc
pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ 
That feels more manageable, doesn't it?

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

There are times when you would like to take a variable that is stored in x bytes, and assign them to a variable that is stored in y bytes. 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:

int 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);
  exit(0);
}

The statement `i = c' is a type cast, as is the statement `f = i'. There are no surprises when we run this:

UNIX> ./p4
c = 97 (a).   i = 97 (a).  f = 97.000000
UNIX> 

Some type castings, like the ones above, are very natural. The C compiler will do these for you without complaining. For most others, the C compiler will spit out a warning, 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  -o p3 p3.c
p3.c:14:39: warning: format specifies type 'unsigned long' but the argument has type 'int *' [-Wformat]
  printf("ip = 0x%lx.  jp = 0x%lx\n", ip, jp);
                 ~~~                  ^~
p3.c:14:43: warning: format specifies type 'unsigned long' but the argument has type 'char *' [-Wformat]
  printf("ip = 0x%lx.  jp = 0x%lx\n", ip, jp);
                              ~~~         ^~
                              %s
2 warnings generated.
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. To the right, I've used a typedef to make those typecasts a little less unweidly. It's a good trick to know:

p5.c
#include <stdio.h>
#include <stdlib.h>

int 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);
  exit(0);
}
p5a.c
#include <stdio.h>
#include <stdlib.h>

typedef long unsigned int LU;

int main()
{
  int i;
  char j[14];
  int *ip;
  char *jp;

  ip = &i;
  jp = j;

  printf("ip = 0x%lx.  jp = 0x%lx\n", (LU) ip, (LU) jp);
  exit(0);
}

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

UNIX> make p5 p5a
gcc  -o p5 p5.c
gcc  -o p5a p5a.c
UNIX> ./p5
ip = 0x7fff57350268.  jp = 0x7fff5735025a
UNIX> ./p5a
ip = 0x7fff52ba1268.  jp = 0x7fff52ba125a
UNIX> 
As an aside, some compilers won't print out those warnings. For example, on my Pi, there is no warning on p3.c. So it goes.

On some machines (not ours), both pointers and ints are 4 bytes. This has led many people to treat pointers and ints as interchangeable. For example, look at the code in p8.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef long unsigned int LUI;

int main()
{
  char s[4];
  int i;
  char *s2;

  /* Copy the string "Jim" to s, then turn the pointer into an integer i.
     Print out the pointer's value, and i's value. */

  strcpy(s, "Jim"); 
  i = (int) s;
  printf("i = %d (0x%x)\n", i, i);
  printf("s = %ld (0x%lx)\n", (LUI) s, (LUI) s);

  /* Now increment i, and turn it back into a pointer.  
     Print out the pointers, and then attempt to print out what they point to. */

  i++;
  s2 = (char *) i;
  printf("s = 0x%lx.  s2 = 0x%lx, i = 0x%x\n", (LUI) s, (LUI) s2, i);
  printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
  exit(0);
}

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 the four bytes that i is missing, typically with zeros, but sometimes with -1's. In either case, it will be an illegal address, and you will get a segmentation violation:

UNIX> ./p8
Before incrementing i.
i = -206846176 (0xf3abc720)
s = 140737281509152 (0x7ffff3abc720)

After incrementing i.
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  -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. The compiler will not complain either:
UNIX> gcc -m32 -o p8-32 p8.c
UNIX> p8-32
Before incrementing i.
i = -4643812 (0xffb9241c)
s = -4643812 (0xffb9241c)

After incrementing i.
s = 0xffb9241c.  s2 = 0xffb9241d, i = 0xffb9241d
s[0] = J, s[1] = i, *s2 = i
UNIX> 
Same thing on my Pi:
pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ make p8
gcc -o p8 p8.c
pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ ./p8
Before incrementing i.
i = 2126570764 (0x7ec0e50c)
s = 2126570764 (0x7ec0e50c)

After incrementing i.
s = 0x7ec0e50c.  s2 = 0x7ec0e50d, i = 0x7ec0e50d
s[0] = J, s[1] = i, *s2 = i
pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ 
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:

typedef long unsigned int LUI;

int main()
{
  char s[4];
  long i;
  char *s2;

  /* This is the same as p8.c, but we've changed i to a long. */

  strcpy(s, "Jim"); 
  i = (long) s;
  printf("Before incrementing i.\n");
  printf("i = %ld (0x%lx)\n", i, i);
  printf("s = %ld (0x%lx)\n", (LUI) s, (LUI) s);

  i++;
  s2 = (char *) i;
  printf("\n");
  printf("After incrementing i.\n");
  printf("s = 0x%lx.  s2 = 0x%lx, i = 0x%lx\n", (LUI) s, (LUI) s2, i);
  printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
  exit(0);
}

UNIX> make p9
gcc  -o p9 p9.c
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. My philosophy is to try to program so that none of them have warnings. That can be a challenge, but you should strive to do the same.

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>

/* This allocates n integers, error checks and returns a pointer to them. */

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;
}

/* This takes a pointer to n integers and assigns them to random numbers. */

void fill_in_the_ints(int *a, int n)
{
  int i;

  for (i = 0; i < n; i++) a[i] = lrand48();
}

/* This reads the command line, allocates, assigns and prints n integers. */

int 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.

Note the difference between C and C++ here:

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>
#include <stdlib.h>

int main()
{
  char *s;

  s = NULL;

  printf("%d\n", s[0]);
  exit(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 The Little Endian Representation

(I owe a debt of graditude to Jay Pickens, a CS360 student in 2015, who wrote the first version of this program and these lecture notes. I have since mangled Jay's code beyond repair, so I don't want to attribute it to him, but he has my appreciation for taking the initiative to spearhead some extra illuminating code to explore little endianness and pointers.)

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.

One such example program that we're going explore is the program endian.c. We're going to walk through this program slowly, looking at code and then what happens when it runs. To make it easier to understand, I'm compiling it in 32-bit mode:

UNIX> gcc -m32 -o endian endian.c
UNIX> ./endian
In this code, I declare an array, called array, which is four unsigned integers. I also declare a pointer to an unsigned integer (called ip), and I also declare a pointer to an unsigned char (called cp). I set the four integers to four different values in hexadecimal:

#include <stdio.h>

typedef unsigned long UL;

int main() 
{
  unsigned int array[4]; /* An array of four integers. */
  unsigned int *ip;      /* An integer pointer that we're going to set to one byte beyond array */
  unsigned char *cp;     /* An unsigned char pointer for exploring the individual bytes in array */
  unsigned short *sp;
  int i;

  /* Set array to equal four integers, which we specify in hexadecimal. */

  array[0] = 0x12345678;
  array[1] = 0x9abcdef0;
  array[2] = 0x13579bdf;
  array[3] = 0x2468ace0;

Now, I print the four values, preceded by their locations in memory. I print everything in hexadecimal:

  /* For each value of array, print it out in hexadecimal.  Also print out its location in memory. */

  for (i = 0; i < 4; i++) {
    printf("Array[%d]'s location in memory is 0x%lx.  Its value is 0x%x\n", 
            i, (UL) (array+i), array[i]);
  }

When you run this, you may get different pointer values. However, their interrelationships will be the same as in this lecture. Here's what I get when I run it. You'll note that the four numbers look exactly the same as in the source code.

Array[0]'s location in memory is 0xbff344a8.  Its value is 0x12345678
Array[1]'s location in memory is 0xbff344ac.  Its value is 0x9abcdef0
Array[2]'s location in memory is 0xbff344b0.  Its value is 0x13579bdf
Array[3]'s location in memory is 0xbff344b4.  Its value is 0x2468ace0
Next, I set the unsigned char pointer, cp to equal array, and then I print the 16 bytes in hexadecimal, along with their memory locations. Here's the code:

  /* Now, print out the sixteen bytes as bytes, printing each byte's location first. */

  printf("\n");
  printf("Viewing the values of array as bytes:\n");
  printf("\n");

  cp = (unsigned char *) array;

  for (i = 0; i < 16; i++) {
    printf("Byte %2d. Pointer: 0x%lx - Value: 0x%02x\n", i, (UL) (cp+i), cp[i]);
  }

Pay careful attention to the output, especially the values of the bytes. You may find them confusing at first:

Viewing the values of array as bytes:

Byte  0. Pointer: 0xbff344a8 - Value: 0x78
Byte  1. Pointer: 0xbff344a9 - Value: 0x56
Byte  2. Pointer: 0xbff344aa - Value: 0x34
Byte  3. Pointer: 0xbff344ab - Value: 0x12
Byte  4. Pointer: 0xbff344ac - Value: 0xf0
Byte  5. Pointer: 0xbff344ad - Value: 0xde
Byte  6. Pointer: 0xbff344ae - Value: 0xbc
Byte  7. Pointer: 0xbff344af - Value: 0x9a
Byte  8. Pointer: 0xbff344b0 - Value: 0xdf
Byte  9. Pointer: 0xbff344b1 - Value: 0x9b
Byte 10. Pointer: 0xbff344b2 - Value: 0x57
Byte 11. Pointer: 0xbff344b3 - Value: 0x13
Byte 12. Pointer: 0xbff344b4 - Value: 0xe0
Byte 13. Pointer: 0xbff344b5 - Value: 0xac
Byte 14. Pointer: 0xbff344b6 - Value: 0x68
Byte 15. Pointer: 0xbff344b7 - Value: 0x24
Didn't you expect the first byte to be 0x12? You aren't alone. This is a feature of a "little endian" architecture, which nearly all machines are these days. When an integer is stored in four bytes, the smallest of the bytes is stored first, then the next, the next and the next. The smallest byte in 0x12345678 is 0x78. So that's what goes into the first byte of the integer. The next byte i s0x56, etc.

Let's look at a picture of memory:

I have listed each byte's address and its value in hex. I have then labeled the four pointers, array, (array+1), (array+2) and (array+3). At the bottom, I have grouped together the four bytes of each integer, and what their integer representation is. Study the picture, and make sure you understand how it matches the output of the code above.

Now, the next snippet of code increments cp by one, and sets ip to equal it. ip's value is now 0xbff344a9. We now print out the four integers pointed to by ip, (ip+1), (ip+2) and (ip+3).

  /* Finally, set the pointer ip to be one byte greater than array,
     and then print out locations and integers. */

  printf("\n");
  printf("Setting the pointer ip to be one byte greater than array:\n");
  printf("\n");

  cp++;
  ip = (unsigned int *) cp;
  for (i = 0; i < 4; i++) {
    printf("(ip+%d) is 0x%lx.  *(ip+%d) is 0x%x\n", i, (UL) (ip+i), i, *(ip+i));
  }

On some machines, this code will have a Bus Error, because ip is not a multiple of four. However, it works on my Mac and on the Pi. You may find the output confusing, but don't worry, we'll go through it:

Setting the pointer ip to be one byte greater than array:

(ip+0) is 0xbff344a9.  *(ip+0) is 0xf0123456
(ip+1) is 0xbff344ad.  *(ip+1) is 0xdf9abcde
(ip+2) is 0xbff344b1.  *(ip+2) is 0xe013579b
(ip+3) is 0xbff344b5.  *(ip+3) is 0x612468ac
This is best explained by modifying the picture above to reflect ip:

Now you can see why each value is as it is. Also, the "61" in 0x612468ac is a byte that is not from the original array. It can has some other value -- we don't really know what it should be. It just so happens that when we run it, it is 0x61. Finally, we print out the first four bytes of array, but as two shorts:

  /* Now, set sp to equal array.  Sp is a pointer to shorts.  We print out sp[0] and sp[0]. */

  printf("\n");
  printf("Finally printing the first four bytes of array as two shorts.\n");
  printf("\n");

  sp = (unsigned short *) array;
  printf("Location: 0x%lx - Value as a short: 0x%04x\n", (UL) sp, sp[0]);
  printf("Location: 0x%lx - Value as a short: 0x%04x\n", (UL) (sp+1), sp[1]);
  printf("\n");

  return 0;
}

Here's the output. Like integers, shorts are represented in little endian as well, so the smallest byte of the first short is at address 0xbff344ac with a value of 0x56, and the largest byte of the first short is at address 0xbff3444a8 with a value of 0x78:

Finally printing the first four bytes of array as two shorts.

Location: 0xbff344a8 - Value as a short: 0x5678
Location: 0xbff344aa - Value as a short: 0x1234

Alignment within structs

As I mentioned above, some machines require pointers to be aligned. That means that pointers to integers must be multiples of four, pointers to doubles must be multiples of eight, and pointers to shorts must be multiples of two.

In order to meet this requirement, compilers and runtime libraries have been designed with two features:

  1. Malloc() always returns pointers that are multiples of 8. Remember that malloc() does not know the type of the data that will be using the memory that it allocates. So, it always returns a multiple of 8, just to be safe.

  2. The compiler lays out structs so that its variables are in order in memory, and they will be aligned if the base pointer for the struct itself is a multiple of eight. That means that the compiler may put some padding into a struct, and make it bigger than you think it should be.
To explore this, take a look at the program pd.c. This program defines four structs:

typedef struct {
  char b;
  int i;
} Char_Int;
typedef struct {
  char b1;
  char b2;
  char b3;
  char b4;
  int i1;
} CCCC_Int;
typedef struct {
  char b1;
  int i1;
  char b2;
  int i2;
} C_I_C_I;
typedef struct {
  int i;
  char b;
} Int_Char;

For each of these structs, I print the struct's size. Then, I allocate an array composed of two structs, and then I look at the pointers to each variable. Here's the code for the Char_Int struct:

  Char_Int *ci;

  ci = (Char_Int *) malloc(sizeof(Char_Int)*2);
  printf("The size of a Char_Int is %ld\n", sizeof(Char_Int));
  printf("I have allocated an array, ci, of two Char_Int's at location 0x%lx\n", (UL) ci);
  printf("&(ci[0].b) = 0x%lx\n", (UL) &(ci[0].b));
  printf("&(ci[0].i) = 0x%lx\n", (UL) &(ci[0].i));
  printf("&(ci[1].b) = 0x%lx\n", (UL) &(ci[1].b));
  printf("&(ci[1].i) = 0x%lx\n", (UL) &(ci[1].i));
  printf("\n");

I'm not going to include the code for the other structs, because it is very similar to the code above. Let's look at the output for each struct in turn:

UNIX> ./pd
The size of a Char_Int is 8
I have allocated an array, ci, of two Char_Int's at location 0x7f81aac03260
&(ci[0].b) = 0x7f81aac03260
&(ci[0].i) = 0x7f81aac03264
&(ci[1].b) = 0x7f81aac03268
&(ci[1].i) = 0x7f81aac0326c
Although the struct only uses five bytes (one for b and four for i), the size of the struct is 8 bytes. When we look at the pointers, we see that the first byte of the struct is where you find b, and i is four bytes after b. The three bytes between b and i are unused. Why is this so? The reason is that the pointer for i must be a multiple of four, and b's address has to come before i's address (this is a compiler standard). The only way to make that so is to have the three bytes after b be unused.

Let's look at the output for the CCCC_Int struct:

The size of a CCCC_Int is 8
I have allocated an array, cccci, of two CCCC_Int's at location 0x7f81aac03270
&(cccci[0].b1) = 0x7f81aac03270
&(cccci[0].b2) = 0x7f81aac03271
&(cccci[0].b3) = 0x7f81aac03272
&(cccci[0].b4) = 0x7f81aac03273
&(cccci[0].i1) = 0x7f81aac03274
&(cccci[1].b1) = 0x7f81aac03278
&(cccci[1].b2) = 0x7f81aac03279
&(cccci[1].b3) = 0x7f81aac0327a
&(cccci[1].b4) = 0x7f81aac0327b
&(cccci[1].i1) = 0x7f81aac0327c
As you can see, this struct makes optimal use of memory -- the variables take up 8 bytes, and the size of the data structure is 8. The pointer for i is aligned - Perfect!

The C_I_C_I struct is less optimally arranged. Here's its output:

The size of a C_I_C_I is 16
I have allocated an array, cici, of two C_I_C_I's at location 0x7f81aac03280
&(cici[0].b1) = 0x7f81aac03280
&(cici[0].i1) = 0x7f81aac03284
&(cici[0].b2) = 0x7f81aac03288
&(cici[0].i2) = 0x7f81aac0328c
&(cici[1].b1) = 0x7f81aac03290
&(cici[1].i1) = 0x7f81aac03294
&(cici[1].b2) = 0x7f81aac03298
&(cici[1].i2) = 0x7f81aac0329c
In order to make sure that the pointers for i1 and i2 are aligned, we have to waste the three bytes after b1 and the three bytes after b2. If we defined b2 right after b1, the size of the data structure would be 12 rather than 16.

Now, look at the last lines of output, for Int_Char. It may surprise you:

The size of a Int_Char is 8
I have allocated an array, ic, of two Int_Char's at location 0x7f81aac03290
&(ic[0].i) = 0x7f81aac03290
&(ic[0].b) = 0x7f81aac03294
&(ic[1].i) = 0x7f81aac03298
&(ic[1].b) = 0x7f81aac0329c
Most students think that sizeof(Int_Char) should be five, because the i pointer is the beginning of the struct, and should be aligned, and the b pointer doesn't have to worry about alignment. That would be true if we only allocated one of these structs. However, if we allocate an array of two structs, then the second one needs to be aligned too -- if the size of the struct were 5, then ic[1].i would not be aligned. To fix that, the size of the struct is 8, and the three bytes after b are wasted.

The bottom line is that if you care about memory, you should lay your structs out so that you don't waste too much memory. If you don't care about memory, don't worry about it (or just keep writing programs in Python.....)


A Common Type Bug

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

#include <stdio.h>
#include <stdlib.h>

int main()
{
  char c;
  int i;
  int j;

  i = 10000;
  c = i;             /* You are losing information here. */
  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);
  exit(0);
}

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>