Menu

Class 4: More C


Reading
These notes.

Homework
Printout the Homework and answer the questions on that paper.

Reviewing Dynamic Memory Allocation (Review from IC210)
In IC210 you talked a fair bit about dynamic memory allocation, which in C++ means creating objects with the new operator. The way we do this in C is different from C++, but the ideas are all the same. So please review your IC210 online course notes, e.g.
MUST UPDATE!
IC210 L22, IC210 L23 and IC210 L24.

To give the briefest of reviews:

stack-allocated objects
The most common way to create an object (i.e. something with a type and value) in a C/C++ program is to declare a variable. Variables defined within a function (or as function parameters) are called stack allocated objects because, as you recall, we view an executing program as having a stack of function-call records, the topmost of which is the currently executing function, where each function-call record contains its own copies of the function's local variables. The key thing here is that stack allocated objects die when the function they belong to returns.
statically-allocated objects
Global variables, i.e. variables declared outside of any function definition, are called statically allocated. They are created when the program starts and only die when the program ends.
heap-allocated objects (i.e. dynamically allocated objects)
Objects created by the new operator in C++ are different than stack-allocated and statically-allocated objects: they are not variables, meaning that they are not bound to a name, and they don't die until explicitly destroyed with the delete operator. Objects created by new are called heap-allocated. When you need arrays whose size isn't determined until run-time (recall the distinctions between compil time, link-time and run-time), or when you need nodes to build a linked list, you need heap-allocated objects. Since heap-allocated objects don't have names, you can only refer to these objects via pointers.

When you use new in C++, what's really happening is that you are requesting a chunk of memory, in an area called "the heap", to store objects in --- objects that live indefinitely, unlike local variables. So int *p = new int; causes a four-byte chunk (assuming 32-bit ints) of memory in the heap to be allocated to your program, and sets the pointer variable p to point to that chunk. When you later call delete p;, it tells the memory management system that it can recycle those four-bytes ... which means your program better not try to derefence p afterwards. The pictures we draw to represent this kind of thing are very explicit about showing the heap-allocated objects as living off in space, away from the function-call records on the stack.

Dynamic memory allocation in C
In C, heap-allocated objects are not created with the new operator and destroyed with the delete operator. Instead there are function calls that accomplish the same things. Functions malloc and calloc allocate memory, and free gives it back to the memory management system. The primary difference is that malloc/calloc simply return a pointer to a chunk of memory, i.e. a sequence of consecutive bytes. The programmer must explicitly cast that pointer to one of the proper type, e.g. int* or char**. Part and parcel with this is that the programmer must figure out how many bytes are required to store the object(s) they plan to put in that chunk of memory. Fortunately, there is an operator sizeof to help.

From the man page:
SYNOPSIS
     #include <stdlib.h>

     void *malloc(size_t size);

     void *calloc(size_t nelem, size_t elsize);

     void free(void *ptr);
		
struct Node
{
  int data;
  struct Node *link;
};

int main(int argc, char **argv)
{
  /* Create array of 10 double's, uninitialized */
  double *A = (double*)malloc(10*sizeof(double));

  /* Create array of argc ints, initialized to zero */
  int *B = (int*)calloc(argc,sizeof(int));

  /* Create one node for a linked list, unitialized */
  struct Node *L = (struct Node*)malloc(sizeof(struct Node));
  
  /* Create a string that is a copy if argv[0] */
  char *s = (char*)malloc((strlen(argv[0]) + 1)*sizeof(char)); 
  strcpy(s,argv[0]);
  /* NOTE:
     0) It's not enough to say "char *s".  That gives a pointer,
        but it doesn't yet point to anything. We need to allocate
        an actual array of char's to copy argv[0] into.
     1) We need 1 + length-or-argv[0] characters so we can
        store the terminating '\0'
     2) The "sizeof(char)" is actually not needed, since char's
        have size one.
  */
  
  /* Free up all that memory! */
  free(A);
  free(B);
  free(L);
  free(s);

  return 0;
}

Please look at this example, which uses dynamic memory allocation to produce histograms.

NOTE: malloc and calloc return NULL if they are unsuccessful. How might that happen? Well you might run out of memory! Like this program, which keeps allocating huge chunks of memory, but never freeing them.
mem2.cSample run:
/* This program is made to run out of memory! */
#include <stdlib.h>
#include <stdio.h>

int main()
{
  int *p, i = 0;;

  while(1)
  {
    ++i;
    p = malloc(10000000*sizeof(int));
    if (p == NULL) /* I.e. malloc failed */
    {
      fprintf(stderr,"Failed on iteration %i\n",i);
      return 0;
    }    
  }
}
bear[1] [~/]> gcc -o mem2 mem2.c
bear[2] [~/]> ./mem2
Failed on iteration 92

Pointer notation (Review from IC210)
Basically we have the following notation with pointers: But there is some scope for confusion in this. The primary source of confusion is this: * means three distinct things in C, and you have to understand the context to know which meaning a particular * has.
  1. In an arithmetic expression, * means multiplication, as in 3*x + 1.
  2. In a variable declaration or function prototype, or any other context in which you are naming a type, * means "pointer"; as in int *p, which declares variable p to be of type pointer-to-an-int, or "int*".
  3. In other expression contexts, * means "dereference a pointer", i.e. to get the object the pointer points to. So assuming that p pointed to an int with value 5, *p + 1 would evaluate to 6, while ++*p would evaluate to 6 and change the value of the int p points to to 6.
So, assuming we have linked lists whose data field is an array of ints and a variable L that is a pointer to a Node, you might have wonderful expressions like this:
(*L).data = (int*)malloc(n*sizeof(int)); /* Wow this is ugly! */
in which the first * dereferences L, i.e. gets the node object that L points to, the second * indicates declares a pointer data type, and the third * is good old multiplication! Of course the fourth and fith *'s serve yet another purpose by delimiting comments. By the way, recall that this could also be written as
L->data = (int*)malloc(n*sizeof(int)); /* A bit less ugly. */
because L->data is just a shorthand for (*L).data. You always need to keep the context in mind to know what's what.

Pointers give you the ability to write different expressions that refer to the same object, which are called aliases.

int x = 5;
int *p = &x;
/* Now x and *p refer to the same object! */

Arrays vs. pointers
In essence, arrays in C/C++ are simply pointers -- pointers to a chunk of memory. So
int *A = (int*)malloc(10*sizeof(int));  /* Dynamic array */
/* now A is the same as &A[0] */

int B[10];                              /* Static array */
/* now B is the same as &B[0] */
However, C/C++ does distinguish between dynamic arrays and static arrays. Static arrays don't require calls to malloc, but their size is hardcoded in, i.e. determined at compile time, and even though the array gets treated like a pointer, it's a pointer you can never change. So with int A[5];, which defines a static array of five ints, you can modify the ints that A points to, but the pointer A itself cannot change, i.e. A = NULL is not allowed. In fact, in this example A has type int * const, which makes sense reading right-to-left. It gives: "constant pointer to ints". So the pointer is constant, the ints in the chunk of memory it points to can be modified. There are lots of subtlties, unfortunately, with how arrays and pointers and their initilizations interact. I'll just give one example, because it might very easily get you:
char *s = "the";
s[0] = 'T';  /* this crashes the program, because s points to a constant string "the"


char r[] = "the";
r[0] = 'T';  /* this is OK, because the constant string "the" is actually copied into 
                a new static array r, which you may then modify. */

Pointers as function arguments
Of course in IC210 you defined functions that passed pointers as arguments and/or returned pointers as results. In C you see a lot more passing of pointers, because there is no pass-by-reference. So if you want a function to modify some variable x, you must pass a pointer to x (i.e. &x) rather than x itself.

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

void swap(int *a, int *b)
{
  int t = *a;
  *a = *b;
  *b = t;
}

int main()
{
  int x, y;
  scanf("%i %i",&x,&y);
  if (x > y)
    swap(&x,&y);
  printf("|y - x| = %i\n",y-x);
}
To define a function "swap" that swaps two integers, we need to pass pointers to the integers, not the integers themselves, because we want to modify the integers. That means the arguments are int* rather than int in the prototype, and when the function is called you pass pointers to x and y (i.e. &x and &y) rather than x and y. Notice that in the function swap, we have to dereference parameters a and b, i.e. *a and *b to refer to the integer values.

File I/O
Every C program automatically has three streams open: stdin, stdout, stderr. All three of these have type FILE* --- note that the definition for type FILE comes from stdio.h. These streams are part of the C language, not the Unix operating system. However, one thing Unix and C share is a vision of these streams as nothing more than a sequence of bytes. If you want to, you can read and write to and from streams in this simple stream-of-bytes way with the functions fread and fwrite. This may not be so attractive for text files, but for reading/writing binary data it's the way to go. To read or write a single char at a time, you can use fgetc (to read) and fputc (to write).

Opening a file, whether for reading or writing, is done with the function fopen.

SYNOPSIS
     #include <stdio.h>

     FILE *fopen(const char *filename, const char *mode);
The first argument is the a string (char *) giving the filename (possibly including the path), and the second argument is a string (char *) giving the mode which for us will simply be "r" for read or "w" for write. P. 138 of APUE describes the several other modes that are available. The return value of fopen is the FILE* that references the newly opened stream. If the file cannot be opened, NULL is returned instead. Closing a file is done with fclose.

#include <stdio.h>

int main()
{
  char fname[64] = { '\0' };
  while(fscanf(stdin,"%s",fname) == 1 && strlen(fname) != 0)
  {
    FILE* fs = fopen(fname,"r");
    if (fs == NULL)
      fprintf(stderr,"Error: File \"%s\" could not be opened!\n",fname);
    else
    {
      char c;
      while((c = fgetc(fs)) != EOF)
	fputc(c,stdout);
      fclose(fs);
    }
  }
  return 0;
}
This program reads filenames from stdin and echoes the contents of those files to standard out (can you tell me why this is fundamentally different than cat?). If a file cannot be opened, it prints an error message, but continues. Notice that I'm using fgetc and fputc to read and write a byte at a time. The FILE* fs could just as well be used with fscanf, by the way.