C has been THE language for systems programming. It achieved its fame by being the first and yet the best language that "puts programmers close to the system". There are a few enabling fundamentals in the C programming language. I summarize four of the most major ones as the following:
These functionalities have been ingeneously designed and optimized, although they appear to be natural and straight-forward to use. However, not knowing the necessary intrinsics, codes tend to be very buggy and even a moderately sized system could fall flat on its face as a daily ritual.
Before dwelling on details, let's also point out some not so "good" things about C. One might notice that, given C's prevalent popularity, a lot scientific pracitioners still program in Fortran. There are obviously reasons. First, C does not have a built-in constructs like complex numbers, vectors and matrices, etc. It is not so easy to program sophisticated math routines using C. Second, C does not have a high quality scientific or numerical libraries. Indeed, until very recently, all of C's math libraries are in double precision only. While one might argue that there is nothing wrong with always using the highest precision, we all know perfectly well that great performance gains could result if we can afford to lose a few of the minor digits. Practically speaking, 30% to 50% performance gains are usually possible. For simulations that would take days of time, this difference carries great significance.
Of course, C was mainly designed as a systems programming language. It is unfair and unrealistic to expect C to perform equally well for other applications. Just that, for us, the pupils of computer science, we need to understand this distinction.
What's listed on the right are the size of each variable in the corresponding type on a common system. The proper way to obtain the size of each type is to use the macro sizeof, like in: sizeof(int).
These should all be familiar to you. 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.
#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 a statically declared aggregate type (a struct or an array), then you can NEVER say ``x = something''. It will always give you an error.
However, you can say ``something = x''. We'll discuss this below:
You can view memory as one huge array of bytes (chars). This array has 2147483648 (or some other huge number) elements. Usually, we consider the indices to this array in hexidecimal. In other words, the array goes from 0x0 to 0x7fffffff. What is this number? Why is this number important?
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%x. jp = 0x%x\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. When we run it, we get:
UNIX> p3 ip = 0xefffe924. jp = 0xefffe910 UNIX>What this means is that when we view memory as an array, elements 0xefffe924, 0xefffe925, 0xefffe926, and 0xefffe927 are allocated for the local variable i, and elements 0xefffe910 through 0xefffe91d 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 machines all pointers are 4 bytes. Thus, in p3.c, there are 26 bytes of local variables allocated in the main() procedure -- 4 for i, 14 for j, 4 for ip, and 4 for jp.
Here is an illustration of BSD style organization of an address space for a program.
BSD defines segments to be contiguous regions of virtual space (like everybody else). A process address space is composed of five primary segments: (i) the text segment, holding the executable code; (ii) the initialized data segment, containing those data that are initialized to specific non-zero values at process start-up; (iii) the bss segment, containing data initialized as zero at process start-up; (iv) the heap segment, containing uninitialized data and the process’s heap; and (v) the stack.
Beyond the stack is a region holding the kernel's stack (used when executing system calls on behalf of this process, for example) and the user struct, a kernel data structure holding a large quantity of process-specific information.
This is a illustration of BSD’s per process virtual memory space. Text, initialized data, bss and heap are contiguously put in memory address, starting from the lowest address. Stack, however, starts from the highest address space, expanding downward in address space. The empty ‘chunk’ between the top of heap and bottom of stack are allocated as needed.
Where and how a memory space is allocated affect which segment this memory allocation resides.
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.'').
For example, think about the procedure call: malloc(n). It allocates and returns n bytes of memory to the programmer. Look at the program p5.c:
main() { char *s; s = malloc(10); strcpy(s, "Jim"); printf("s = %s\n", s); }When you try to compile p5.c, you get a warning from the C compiler:
UNIX> gcc -o p5 p5.c p5.c: In function `main': p5.c:5: warning: assignment makes pointer from integer without a cast UNIX>What's going on? Well, all procedures in C are assumed to return integers unless they are specified otherwise. Thus, the statement ``s = malloc(10)'' is trying to set s, which is a pointer, to the return value of malloc, which is assumed to be an integer. The compiler actually does create p5, but it lets you know that you're doing something strange -- that is, assign a pointer to an integer.
What's the proper thing to do here? Well, you should really declare malloc() as returning a char *, as in p6.c:
extern char *malloc(); main() { char *s; s = malloc(10); strcpy(s, "Jim"); printf("s = %s\n", s); }This tells the compiler that you are using the procedure malloc() which returns a char *, and which is defined elsewhere. You'll note that p6.c compiles without any warnings, but that both p5 and p6 do the same thing when you run them:
UNIX> p5 s = Jim UNIX> p6 s = Jim UNIX>Most people do not write code like p6.c, though. Instead, they write it as in p7.c:
main() { char *s; s = (char *) malloc(10); strcpy(s, "Jim"); printf("s = %s\n", s); }This says to the compiler ``Yes, I know malloc() is returning an int, but I want it to be treated like a char *''. You'll notice that p7.c compiles without warning and runs just like p5 and p6.
Before we go on to other subjects, you need to understand that, however, p7.c is still not how you should write code. The following would be a lot more favorable:
#include < stdlib.h > main() { char *s; s = malloc(10); strcpy(s, "Jim"); printf("s = %s\n", s); }
In this piece of code, including stdlib.h effectively adds the line of code "void * malloc (size_t size);" before the main function. This makes sure the compiler knows the return value of malloc is actually not an integer, but instead a pointer of the type void *. Similar to how a char type can always be implicitly cast to an integer, a pointer of void * can always be coerced to be an char * type.
You should also notice that on our machines, 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:
main() { char s[4]; int i; char *s2; strcpy(s, "Jim"); i = (int) s; printf("i = %ld (0x%lx)\n", i, i); printf("s = %ld (0x%lx)\n", s, s); i++; s2 = (char *) i; printf("s = 0x%lx. s2 = 0x%lx, i = 0x%lx, s[0] = %c, s[1] = %c, *s2 = %c\n", s, s2, i, s[0], s[1], *s2); }This is a bad assumption, however, because on some machines, like the DEC alpha, ints are 4 bytes and pointers are 8. Thus, when you run p8.c on an alpha you get an error instead of a correct program run. This is because when we said ``i = (int) s'', we lost 4 bytes of the pointer s. Then when we said ``s2 = (char *) i'', the four extra bytes of s2 were set to zero, giving us different addresses for *s2 and s[1]. In fact, s2 becomes an illegal address, which results in a segmentation fault:
On our sparcs:
UNIX> p8 i = -268441312 (0xefffe920) s = -268441312 (0xefffe920) s = 0xefffe920. s2 = 0xefffe921, i = 0xefffe921, s[0] = J, s[1] = i, *s2 = iOn the alpha:
UNIX> p8 i = 536864720 (0x1fffe7d0) s = 4831832016 (0x11fffe7d0) Segmentation fault (core dumped)If we instead use a long for i instead of an int, everything works fine on the alpha, since longs and pointers are both 8 bytes:
On the alpha:
UNIX> p9 i = 4831832016 (0x11fffe7d0) s = 4831832016 (0x11fffe7d0) s = 0x11fffe7d0. s2 = 0x11fffe7d1, i = 0x11fffe7d1, s[0] = J, s[1] = i, *s2 = i UNIX>In this class we will assume that we are always working on a machine where ints and pointers are always 4 bytes. However, in general you should always be sure that your code will work when pointers and ints are different sizes.
main() { char * s; int array[2] = { 10000, 1000000}; s = array; s++; printf("my array has %d and %d\n",array[0], (int)(*s)); printf("array = %lx and s=%lx\n",array,s); }
The output looks like this:
my array has 10000 and 0 in address space, array = ffbef848, s= ffbef849
Since everything is in a giant array of bytes (so do executable texts), we can obviously get the entry address of each function. With this, let's introduce pointers to functions. These pointers can be passed to other functions as parameters, assigned, placed in arrays, returned by functions just like other variables.
You specify function pointers types by giving the return type, and type of each input parameters. Like this:int (*) (void *, void *)This is a function pointer that can point to any function that takes two void pointers as input and returns an integer. Note here, that the above specifies a type! Let's look at the following:
#include < stdio.h > int i = 0; int main(void) { int (*myfunc) (void); int (*foo) (void); myfunc = main; printf("main = %lx, myfunc = %lx, i = %d\n",main,myfunc,i); i ++; if (i == 5) return 0; foo = (i%2) ? myfunc : main; foo(); }The output would look like:
main = 106d8, myfunc = 106d8, i = 0 main = 106d8, myfunc = 106d8, i = 1 main = 106d8, myfunc = 106d8, i = 2 main = 106d8, myfunc = 106d8, i = 3 main = 106d8, myfunc = 106d8, i = 4
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]); }
main() { int *i; i = (int *) 1; printf("%d\n", *i); }
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:
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); }
The second bug is a typical one when you deal with math routines. If you say ``man log10,'' you'll see that it takes a double and returns a double:
double log10(double x);So
main() { double x; x = log10(100); printf("%lf\n", x); }
UNIX> pd -1035.000000Why? This is because you didn't include math.h in your C program, and therefore the compiler assumed that you were passing log10 an integer, and that it returned an integer. And the compiler doesn't worry about casting int's to double's. So you get the bug. C's linker does not worry about types of parameters at all, in fact, it only checks function name for matches. That's one of the main reasons that you cannot overload function names like in C++. Note here though, the comipler, once given a function declaration, checks for number of parameters you are passing too. So, that's some added security. You can fix this by including math.h, as in
#include < math.h > main() { double x; x = log10(100); printf("%lf\n", x); }
UNIX> pd 2.00000
In fact, this following program also compiles:
#include < stdio.h > main () { double x; x = log10(); printf("x = %lf\n",x); }This use of log10 does not even have an input parameter. But when you compile and supply the linker flag "-lm", it compiles. The output I get is:
x = -1036.000000But when I include math.h, i.e.:
#include < stdio.h > #include < math.h > main () { double x; x = log10(); printf("x = %lf\n",x); }This time I get a compile error:
prototype mismatch: 0 args passed, 1 expectedTo sum up, compiler and linker can do strange things, if you do not have a decent understanding of what is going on. Some weird errors may cost a lot of time to figure out. Actually, all three common bugs we are talking about here has to do with type casting, more specifically, whether an implicit type casting is triggered or not. Be very careful.
Finally
main() { double x; int y; int z; x = 4000.0; y = 20; z = -17; printf("%d %d %d\n", x, y, z); printf("%f %d %d\n", x, y, z); printf("%lf %d %d\n", x, y, z); printf("%lf %lf %lf\n", x, y, z); }
UNIX> pf 1085227008 0 20 4000.000000 20 -17 4000.000000 20 -17 4000.000000 0.000000 0.000000
Typically you see the first bug in line one. You try to print out a double as an int. Not only does it get the value wrong, but it gets x and y wrong as well. You'll learn why later. Lines two and three are fine, but line 4 is now wrong, because you try to print all three quantities as double's. Again, you'll see the reason why later, but you should be aware of this kind of bug now, since you may well see it again.
An even more interesting situation occurs in the following example pg.c.
#include < stdio.h > main() { double x; int y; char s[4] = "Jim"; x = 4000.0; y = 20; printf("%lf %lf %s\n", x, y, s); }
Running this code, the output is sometimes:
UNIX> pg Bus errorBut at other times, you may get:
4000.000000 0.00000Or even:
4000.000000 0.00000 ?Š???‡%??Š???‡&H?Š?ˇ?‡'H?Š???‡(H?Š?Ź?‡)H?Š
One way to catch many of such bugs is to always set the warning level of the compiler to the highest and truly treat each warning as a WARNING, and try any compilers as you get on your hand might help as well.