Programming Language Concepts Using C and C++/Introduction to Programming in C

From Wikibooks, open books for an open world
Jump to navigation Jump to search

What follows is an assortment of simple C programs presented to provide an exposure to the basics of the language. In order to better achieve this purpose, examples are kept simple and short. Unfortunately, for programs of such scale certain criteria become less important than they normally should be. One such criterion is source code portability, which can be ensured by checking programs against standards compliance. This criterion, if not pursued diligently from the start, is very difficult to fulfill.

For easier tackling of the problem, many compilers offer some assistance. Although the degree of support and its form may vary, it’s worth taking a look at the compiler switches. For the compilers we will be using in the context of this class an incomplete list of applicable options are given below.

Option* Meaning Valid in
-Wall Provide warnings for questionable programming practices, such as missing function return types, unused identifiers, switch statements without a default, and so on. Both
-std Check for compliance witha particular standard. gcc
-pedantic Check programs for strict standards compliance and reject all programs using non-standard extensions. gcc
-Tc Similar to -pedantic MS Visual C/C++
*: Options supported by MS Visual C/C++ can also be prefixed with '/'.
Example: Compiler option usage.

Using gcc, provide the required compilation command needed to ensure any sloppy programming does not go unnoticed. Also make sure your source code can be easily ported to other development environments.

First requirement is met by passing -Wall, while the second one is fulfilled by using the -pedantic option. So, the required compilation command looks like below.

gcc -Wall -pedantic [other options] filename

Same purpose can be achieved in MS Visual C/ C++ by the following command:

cl [other options] /Wall /Tc filename

C Preprocessor[edit]

The C preprocessor is a simple macro processor—a C-to-C translator—that conceptually processes the source text of a C program before the compiler proper reads the source program. Generally, the preprocessor is actually a separate program that reads the original source file and writes out a new "preprocessed" source file that can then be used as input to the C compiler. In other implementations, a single program performs the preprocessing and compilation in a single pass over the source file. The advantage of the former scheme is, apart from its more modular structure, the possibility of translators for other programming languages using the preprocessor.

The preprocessor is controlled by special preprocessor command lines, which are lines of the source file beginning with the character #.

The preprocessor removes all preprocessor command lines from the source file and makes additional transformations on the source file as directed by the commands.

The name of the command must follow the # character.[1] A line whose only non-whitespace character is a # is termed null directive in ISO C and is treated the same as blank line.

Defining a Macro[edit]

#define
#define preprocessor command causes a name to become defined as a macro to the preprocessor. A sequence of tokens, called the body of the macro, is associated with the name. When the name of the macro is recognized in the program source text or in the arguments of certain other preprocessor commands, it is treated as a call to that macro; the name is effectively replaced by a copy of the body. If the macro is defined to accept arguments, then the actual arguments following the macro name are substituted for formal parameters in the macro body. Argument passing mechanism used is akin to call-by-name. However, one should not forget the fact that text replacement is carried out by the preprocessor, not the compiler.

This macro directive can be used in two different ways.

  • Object-like macro definitions: #define name sequence-of-tokens?, where ? stands for zero or one occurrence of the preceding entity. Put another way, the body of the macro may be empty.
Example: Object-like macro definition

#define BOOL char /* Not #define BOOL=char */ #define FALSE 0 #define TRUE 1 #define CRAY

Note that there is no equal sign. Neither is a semicolon required to terminate the lines. Leading and trailing white space around the token sequence is discarded.

Redefinition of a macro is allowed in ISO C provided that the new definition is the same, token for token, as the existing definition. Redefining a macro with a different definition is possible only if an undef directive is issued before the second definition.

  • Defining Macros with Parameters: #define name(name1, name2, ..., namen) sequence-of-tokens?. The left parenthesis must immediately follow the name of the macro with no intervening whitespace. Such a macro definition would be interpreted as an object-like macro that starts with a left parenthesis. The names of the formal parameters must be identifiers, no two the same. A function-like macro can have an empty formal parameter list. This is used to simulate a function with no arguments.
Example: Macro with a parameter
#define increment(n) (n + 1)

When a function-like macro call is encountered, the entire macro call is replaced, after parameter processing, by a processed copy of the body. The entire process of replacing a macro call with the processed copy of its body is called macro expansion; the processed copy of the body is called the expansion of the macro call. Therefore, with the above definition, a = 3 * increment(a); would be expanded to a = 3 * (a + 1);. Now that preprocessing takes place prior to compilation, the compiler proper does not even see the identifier increment in the source code. All it sees is the expanded form of the macro.

Example:
#define product(x, y) (x * y)
The above innocent looking code is unfortunately wrong. It won’t produce the correct result for result = product(a + 3, b);. This line, after preprocessing, will be transformed into result = (a + 3 * b);. Not exactly what we wanted!
Instead, we must write
#define product(x, y) ((x) * (y))
With this definition in place, the previous example will be expanded as result = ((a + 3) * (b));. Note that the programmer does not see the superfluous parentheses. She sees the un-preprocessed form of the program text.
Example: An incorrect macro definition
#define SQUARE(x) x * x
This definition would fail to produce the correct result for a + 3. It would yield a + 3 * a + 3 instead of (a + 3) * (a + 3). This can be taken care of by putting parentheses around x’s.
#define SQUARE(x) (x) * (x)
However, this second version fails to produce the correct result for result = (long) SQUARE(a + 3);, which is expanded to result = (long) (a + 3) * (a + 3);
In the example above, only the first subexpression would be cast into a long. In order to apply the cast operator to the entire expression, we should further put parentheses around the entire body of the macro. So, the correct version of the macro is:
#define SQUARE(x) ((x) * (x))
Is this an end to the troubles? Unfortunately, no! What if we use the macro in the following way? This would be expanded to result = ((x++) * (x++));
result = SQUARE(x++);
The problem with this expression is that the side effect due to the increment operator takes place twice and x is multiplied by x + 1, not itself. This [once again] goes to show that one has to avoid writing side effect producing expressions.

The problems we faced in the above example may be attributed to the textual nature of the parameter passing mechanism used: call-by-name: Each time the parameter name appears in the text, it is replaced by the exact text of the argument; each time it is replaced, it is evaluated once again.

Example:

#define swap(x, y) \ { unsigned long temp = x; x = y; y = temp; }

\ at the end of the line is used for line continuation. For

if (x < y) swap(x, y); else x = y;

we get

if (x < y) { unsigned long temp = x; x = y; y = temp; }; else x = y;

The semicolon after the closing brace is extraneous and causes a compile-time error. A way out of this is given below.

#define FALSE 0 #define swap(x, y) \ do { unsigned long temp = x; x = y; y = temp; } while(FALSE)

After expansion we will have

if (x < y) do { unsigned long temp = x; x = y; y = temp; } while(FALSE); else x = y;

Predefined Macros[edit]

Macro Value
__LINE__ The line number of the current source program line, expressed as a decimal integer constant
__FILE__ The name of the current source file, expressed as a string constant
__DATE__ The calendar date of the translation, expressed as a string constant of the form "Mmm dd yyyy"
__TIME__ The time of the translation, expressed as a string constant of the form "hh:mm:ss"
__STDC__ The decimal constant, if and only if the compiler is an ISO conforming implementation
__STDC_VERSION__ If the implementation conforms to Amendment 1 of ISO C, then this macro is defined and has the value 199409L

Undefining a Macro[edit]

#undef name
#undef causes the preprocessor to forget any macro definition of name. It is not an error to undefine a name that is currently not defined. Once a name has been undefined, it may then be given a completely new definition without error.
Example: Undefining a macro.

#define square(x) ((x) * (x)) ... a = square(b); ... ... #undef square ... ... c = square(d++); ...

The first application of square will use the macro definition, while the second one will call the function.

Macros vs. Functions[edit]

Choosing macro definition over function makes sense when

  • efficiency is a concern,
  • the macro definition is simple and short, or used infrequently in the source, and
  • the parameters are evaluated only once.

Macro definition is efficient because preprocessing takes place before runtime. It actually takes place even before the compiler proper starts. On the other hand, function call is a rather expensive instruction and it is executed in the runtime. In this sense, a macro can be used to simulate inlining of a function.

Macro definitions are expected to be simple and short because replacing large bodies of text in the source will give rise to code-bloat, especially when it is used frequently.

Justification of the third requirement was given previously.

On the down side, it should be kept in mind that the preprocessor does not do any type-checking on macro parameters.

Code Inclusion[edit]

#include <filename>
#include "filename"
#include preprocessor-tokens
The #include preprocessor command causes the entire contents of a specified source file to be processed as if those contents had appeared in place of the #include command.

The general intent is that the "filename" form is used to refer to the header files written by the programmer, whereas the <filename> form is used to refer to standard implementation files. The third form undergoes normal macro expansion and the result must match one of the first two forms.

Conditional Preprocessing[edit]

#if constant-expression
group-of-lines1
#elif
group-of-lines2
...
#elif
group-of-linesn
#endif
These commands are used together to allow lines of source text to be conditionally included or excluded from the compilation. It comes handy when we need to produce code for different architectures (e.g., Vax and Intel) or in different modes (e.g., debugging mode and production mode).
#ifdef name
#ifndef name
These two commands are used to test whether a name is defined as a preprocessor macro. They are equivalent to #if defined(name) and #if !defined(name), respectively.

Notice that #if name and #ifdef name are not equivalent. Although they work exactly the same way when name is undefined, the parity breaks when name is defined to be 0. In that case, the former will be false while the latter will be true.

defined name
defined(name)
The defined operator can be used only in #if and #elif expressions; it is not a preprocessor command but a preprocessor operator.

Emitting Error Messages[edit]

#error preprocessor-tokens
The #error directive produces a compile-time error message that will include the argument tokens, which are subject to macro expansion.
Example: #error macro

#if (TABLESIZE > 1000) #error "Table size cannot exceed 1000" #endif

Compile-Time Switches[edit]

In addition to the use of the #define command; we can use a compile-time switch to define a macro.

Example: Defining a macro on the command line.
gcc –DTABLESIZE=500 prog_name↵
will treat the program as if #define TABLESIZE 500 were the first line of the source.
Example: Conditional preprocessing via a compiler switch.
gcc –DVAX prog_name↵
The above command will define a macro named VAX and (preprocess and) compile a program named prog_name.

#ifdef VAX
VAX-dependent code
#endif #ifdef PDP11
PDP11-dependent code
#endif #ifdef CRAY2
CRAY2-dependent code
#endif

With VAX defined by a compile-time switch and these definitions in the source, only the VAX-dependent code will be processed. Code related to other architectures will be ignored. When we want to compile our program for a different architecture, all we have to do is to change the compile-time switch to the relevant architecture.
A novice (or malicious) programmer might define more than one architecture macro at command line.
gcc –DVAX –DCRAY2 prog_name↵
In this case, the compiled program will contain code meant for two different architectures. This can be avoided by the following preprocessor commands.

#if defined(VAX)
VAX-dependent code
#elif defined(PDP-11)
PDP11-dependent code
#elif defined(CRAY2)
CRAY2-dependent code
#else #error "Target architecture not specified/known! Aborting compilation..." #endif

Example: Debugging mode compilation
In prog_name,

... #if defined(DEBUG)
Debugging mode code
#endif ...

The command gcc –DDEBUG prog_name↵ will produce (object) code that contains some extra statements for debugging. Once debugging phase is over, you can recompile the program with gcc prog_name↵ and debugging mode code will not be included in the object code. Nice thing about this is that you do not need to remove the debugging mode code from the source!

char Data Type[edit]

Problem: Write a program that prints out the characters 'a'..'z', 'A'..'Z', and '0'..'9' along with their encoding.


Encoding.c

The following preprocessor directive pulls the contents of the file named stdio.h into the current file. Once this file is included, its contents are interpreted by the preprocessor.

For each function we use we need to bring in its prototype, whose location can be found out by means of the man command found in UNIX-based environments: man function_name will provide you with information about funtion_name, including the header file it's declared in.

Included files generally contain declarations shared among different applications. Constant declarations and function prototypes are examples to these. In this case, we must include stdio.h in order to pull in the prototype of the printf function.

A function prototype consists of the function return type, the name of the function, and the parameter list. It describes the interface of the function: it details the number, order, and types of parameters that must be provided when the function is called and the type of value that the function returns.

A function prototype helps the compiler ensure correct usage of a particular function. For instance, given the declaration in stdio.h, one cannot pass an int as the first argument to the printf function.

Time and again you will see the words prototype and signature used interchangeably. This however is not correct. Signature of a function is its list of argument types; it does not include the function return type.

1 #include <stdio.h>

All runnable C programs must contain a function named main. This function serves as the entry point to the program and is called from within the C runtime, which is executed after the executable is loaded, as part of the initialization code.

The system software needed to copy a program from secondary storage into the main memory so that it’s ready to run is called a loader. In addition to bringing the program into the memory, it can also set protection bits, arrange for virtual memory to map virtual addresses to disk pages, and so on.

The formal parameter list of the following main function consists of the single keyword void, which means that it does not take any parameters at all. You will at times see C codes with int main() or main() written instead of :int main(void). In this context all three serve the same purpose although the first two should be avoided. A function with no return type in its declaration/ definition is assumed to return a value of type int. A function prototype with an empty formal parameter list is an old style declaration that tells the compiler about the presence of a function, which can take any number of arguments of any type. It also has the unasked for side effect of turning off prototype checking from that point on. So, one should avoid such usages.

3 int main(void) {

Although we will manipulate characters, the variable used to hold the characters, ch, is declared as an int. The reasoning for this is as follows: Absence of the sign qualifier (signed or unsigned) in the original language design gave compiler implementers the freedom to interpret char as a type comprising values in [0..255]—because characters can be indexed by nonnegative values only—or as a type comprising values in [–128..127]—because it is an integer type represented in a single byte. These two views basically limit the range of char to [0..127].

Some information on ASCII

Due to the lack of exceptions in C, most functions dealing with characters and character strings need to represent exceptional situations, such as end-of file, as unlikely return values. This means, in addition to the legitimate characters, we should be able to encode the exceptional situations as different values. ASCII, this implies a representation that can hold 128 + n signed values, where n is the number of exceptional conditions to be dealt with. When taken together with the conclusion drawn in the previous paragraph, it can be seen that we need a representation that is larger than type char. Therefore, you'll often see an int variable being used to hold values of type char.

Encountering a character constant the C compiler replaces it with an integer constant that corresponds to the order of the character in the encoding table. For instance, 'a' in ASCII is replaced with 97, which is an integer constant of type int.

Had we used char instead of int our program would still be working correctly. This is because we did not have to deal with any exceptional conditions in the program and all int constants used are within the limits of char representation. That is, all narrowing implicit conversions, as that would take place in the initialization statement of the for loop, are value preserving.

4   int ch;
5   for(ch = 'a'; ch <= 'z'; ch++)

printf function is used to format and send the formatted output on the standard output file stdout, which by default is the screen. It is a variadic function: that is, it takes a variable length argument list. The first argument is taken to be a format control string, which is used to figure out the type and number of arguments. This is accomplished through the use of special character sequences starting with %. When encountered, such a sequence is replaced with the corresponding actual parameter, if the actual parameter can be used in the context implied by the character sequence. For instance, '%c' in the following line means that the corresponding argument must be interpretable as a character. Likewise, '%d' is a placeholder for a decimal number.

Note that printf actually returns an int. Not assigning this return value to some variable means that it is ignored.

 6     printf("Encoding of '%c' is %d\n", ch, ch);
 7   printf("Press any key to continue\n"); getchar();
 8 
 9   for(ch = 'A'; ch <= 'Z'; ch++)
10     printf("Encoding of '%c' is %d\n", ch, ch);
11   printf("Press any key to continue\n"); getchar();
12   for(ch = '0'; ch <= '9'; ch++)
13     printf("Encoding of '%c' is %d\n", ch, ch);
14 
15   return(0);
16 } /* end of int main(void) */

Compiling and Running a Program in Linux Command Line[edit]

Given that this program is saved under the name Encoding.c, it can be compiled (and linked) using

gcc –o AlphaNumerics Encoding.c↵
GCC (GNU Compiler Collection) is, as its name implies, a collection of compilers for various programming languages. gcc, the C/C++ frontend to GCC, performs the preprocessing, compilation, assembly, and link phases required for creating an execuable from C source files.

gcc invokes the GNU C/C++ compiler driver, which first gets the C-preprocessor process Encoding.c and passes the transformed source file to the compiler proper. The output of the compiler proper, an assembly program, is later passed to the assembler. The object code file assembled by the assembler is finally handed to the linker, which links it with the standard C library and stores the executable in a disk file whose name is provided with -o option. Note that you do not have to tell the driver to link with the standard C library. This is a special case, though. With other libraries you must tell the compiler driver the libraries and object files to be linked with. This whole scheme basically creates a new external command named AlphaNumerics. Issuing the command

./AlphaNumerics

at the command line will eventually cause the loader to load the program from the secondary storage to memory and run it.

Compiling and Running a Program Using Emacs[edit]

emacs &

This command will put you in the Emacs development environment. Click on File→Open... and pick Encoding.c from the file list. This will open up a new C-mode buffer within the current window. Note that the second line from the bottom shows (C Abbrev), which means Emacs has identified your source code as a C program. Next, click on Tools→Compile►→Compile.... This will prompt you to enter the command needed to compile (and link) the program. This prompt will be printed in an area called the minibuffer, which is normally the very last line of the frame. Erase the default selection and write

gcc –o AlphaNumerics.exe Encoding.c↵

As you hit the enter key, you will see a *compilation* buffer pop up that lets you know about how the compilation process is proceeding. Hoping you don’t make any typos and everything goes smoothly, next thing we will do is to run the executable. In order to do this, click on Tools→Shell►→Shell. This will open a restricted shell inside a Shell mode buffer, from where you can run your executables. Type in

./AlphaNumerics↵

within this buffer and you will see the same output as you saw in the previous section.

In case you end up with compilation errors, clicking on an error line in the *compilation* buffer takes you to the relevant source line.

If you want to go back to the source code and make some changes, click on Buffers→Encoding.c. When you are through with the changes, you can compile the source code once again by clicking on Tools→Compile►→Repeat Compilation. This will recompile Encoding.c using the command entered above. However, in case you may want to modify the command, click on Tools→Compile►→Compile... and proceed as you did before.

Non-portable Version[edit]

Assuming ASCII, you may be tempted to replace all character constants in the program with the corresponding integer values. This is strongly discouraged since it will make the program non-portable.


ASCII_Encoding.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 
4 int main(void) {
5   int ch;

The following line contains embedded constants, which cause the code to be non-portable. What if we wanted to move our code to some environment where EBCDIC is used to encode characters? So, one should avoid embedding such implementation dependent features into the programs and let the compiler do the dirty work.

Note also, since the same action is taken by the compiler, probable motivation—speeding up the program—for replacing character literals with integer constants is not well-founded, either.

 6   for(ch = 97; ch <= 122; ch++)
 7     printf(Encoding of %c is %d\n, ch, ch);
 8   printf(Press any key to continue\n);
 9   getchar();
10 
11   for(ch = 65; ch <= 90; ch++)
12     printf(Encoding of %c is %d\n, ch, ch);
13   printf(Press any key to continue\n);
14   getchar();
15 
16   for(ch = 48; ch <= 57; ch++)
17     printf(Encoding of %c is %d\n, ch, ch);

The exit function causes the program to terminate, returning the value passed to it as the result of executing the program. Same effect can be achieved by returning an integer value from the main function. By convention, a value of 0 signifies successful termination while a nonzero value is used to signify abnormal termination.

18   exit(0);
19 } /* end of int main(void) */

Compiling and Running a Program Using MS Visual C/C++[edit]

First of all, make sure you execute vcvars32.bat, which you can find in the bin subdirectory of the MS Visual C/C++ directory. This will set some environment variables you need for correct operation of the command line tools.

cl /FeAscii_Enc.exe Ascii_Encoding.c↵

Similar to gcc, this command will go through preprocess, compile, assemble, and link phases. Upon successful completion, we can run our program simply by issuing the name of the executable filename at the command line. The operating system shell will recognize the resultant executable as an external command and get the loader to load the Ascii_Enc.exe into memory and eventually run it.

Ascii_Enc↵

Compiling and Running a Program Using DGJPP-rhide[edit]

Start a new DOS box and enter the following command.

rhide Ascii_Encoding.c↵

This will start a DOS-based IDE that you can use to develop projects in different programming languages. Choose Compile→Make or Compile→Build All or Compile→Compile followed by Compile→Link. This will compile the source code and link the resulting object module with the C runtime. You can now run the executable by clicking on Run→Run or by exiting to DOS by choosing File→DOS Shell and entering the filename at the prompt. (In case you may see unexpected behavior from rhide, make sure the file is not too deep inside the directory hierarchy and the names do not contain special characters such as space.) If you choose the second option you can return to rhide by typing exit at the command prompt.

Macros[edit]

Problem: Write a program that prints a greeting in English or Turkish. The language should be chosen by a compile-time switch. The name(s) of the person(s) to be greeted is passed to the program through a command-line argument.


Greeting.c
1 #include <stdio.h>

The following line checks to see whether a macro named TURKISH has already been defined or not. Definition of this macro, or any macro for that matter, can be made within a file or at the command line prompt as a compiler switch. In this example, there is no such definition in the file or any included file. So, absence of such a macro as a compiler switch will cause the control jump to the #else part and the statements between #else and #endif are included in the source file. Given the definition is made at the command line prompt, the statements between #if and #else are included in the source file. Whichever section of code is included, one thing is certain: either the part between #if and #else or the part between #else and #endif is included, not both; there is no danger of duplicate definition.

Notice the peculiar way of naming the variables. This is the so-called Hungarian notation. By prefixing specially interpreted characters to the identifier name, this method aims at providing the fellow programmers/maintainers as much context information as possible. Without any reference to the definition of an identifier, which can be pages apart or even in a different source file that is inaccessible to us, we can now garner the required information simply by interpreting the prefix. For instance, szGreeting is meant to be a string ending with zero (that is, a C-style string).

 3 #if defined(TURKISH)
 4   char szGreeting[] = "Gunaydin,";
 5   char szGreetStranger[] = "Her kimsen gunaydin!";
 6   char szGreetAll[] = "Herkese gunaydin!";
 7 #else
 8   char szGreeting[] = "Good morning,";
 9   char szGreetStranger[] = "Good morning, whoever you might be!";
10   char szGreetAll[] = "Good morning to you all!";
11 #endif

C does not allow the programmer to overload function names. main function is an exception to this: it comes in two flavors. The first one, which we have already seen, does not take any arguments. The second one permits the user to pass command line arguments to the application. These arguments are passed in a vector of pointers to character. If the user wants the arguments to be interpreted as belonging to some other data type, the application code has to do some extra processing.

In C, there is no standard way of telling the size of an array. One should either use a convention or hold the size in another variable. Character strings in C, which may be considered as an array of characters, are an example to the former. Here, a sentinel value (NULL character) is used to signify the end of the string. For most instances, such a scheme turns out to be impossible or infeasible. In that case we need to hold the size (or length) information in a separate variable. Hence is the need for a second variable.

The program name is the first component of the argument vector. So, if the argument count is one it means the user has not passed any arguments at all.

13 int main(int argc, char *argv[]) {
14   switch (argc) {

Execution of a break statement will terminate the innermost enclosing loop (while, for, and do while) or switch statement. That is, it will basically jump to the next line following the loop or switch statement. In our case, control will be transferred to the return statement.

Newcomers to C from a Pascal-based background must be careful about the nature of the switch statement: Unlike the case statement, where each branch is executed mutually, switch lets more than one branch be executed. If that’s not what you want you must delimit the branches with break statements, as was done below. Without the break statements in place an argument count of one would cause all three messages to be printed. Similarly, an argument count of two would print anonymous message together with the appropriate one.

15     case 1: printf("%s\n", szGreetStranger); break;
16     case 2: printf("%s %s\n", szGreeting, argv[1]); break;
17     default: printf("%s\n", szGreetAll);
18   } /* end of switch (argc) */
19 
20   return(0);
21 } /* end of int main(int, char**) */

Using Compiler Switches[edit]

Saving this C program as Greeting.c and issuing

gcc Greeting.c –DTURKISH –o Gunaydin↵ # In Linux

at the command line will produce an executable named Gunaydin. This executable will not include object code for any of the statements between #else and #endif. Similarly, if we compile the program using

gcc Greeting.c –DENGLISH –o GoodMorning↵

we will get an executable named GoodMorning with the statements between #if defined and #else excluded. Note that with no macros defined at the command line, the English version of the greetings will be included.

One should not mistake compile-time switches for command-line arguments. The former is passed to the preprocessor and used to alter the code to be compiled, while the latter is passed to the running program to alter its behavior. Provided that we build our executables as shown above

./Gunaydin Tevfik↵

will produce as output

Gunaydin, Tevfik

whereas

./GoodMorning Tevfik Ugur↵

will produce

Good morning to you all!

Pointer Arithmetic[edit]

Problem: Write a program to demonstrate the relationship between pointers and addresses.


Pointer_Arithmetic.c
1 #include <stdio.h>
2 
3 int string_length(char *str) {
4   int len = 0;

The third part in the following for loop increments the variable str, which is a pointer to char. If we fail to realize that address and pointer are two different things, we might be tempted to think that what we do is simply incrementing the address value. But this would be utterly wrong. Although, for pedagogical purposes, we may assume a pointer to be an address, they are not one and the same thing. When we increment a pointer, the address value held in it is incremented by as much as the size of the type of value pointed to by the pointer. However, as far as pointer to char is concerned, incrementing a pointer and address has the same effect. This is due to the fact that a char value is stored in a single byte.

Definition: A pointer is a variable that contains the address of a variable, content of which is interpreted to belong to a certain type.[2]

Effect of incrementing a char*

Note that although a handle on an object may be regarded as a "kind of" pointer, they are two different concepts. Along with other differences such as support for inheritance, unlike pointers and addresses, handles do not take part in arithmetic operations.

 5   for(; *str != '\0'; str++) len++;
 6 
 7   return len;
 8 } /* end of int string_length(char *) */
 9 
10 long sum_ints(int *seq) {
11   long sum = 0;

Next line is an example showing the difference between a pointer and an address. Here, incrementing seq will increase the value of the address held in it by the size of an int.

Effect of incrementing a int*
12   for (; *seq > 0; seq++) sum += *seq;
13 
14   return sum;
15 } /* end of long sum_ints(int *) */
16 
17 int main(void) {

Next line creates and initializes an array of chars. The compiler computes the size of this array. What the compiler does is basically count the number of characters in between double quotes and reserve memory accordingly. Note that the compiler automatically appends a NULL character too. However, if we choose to use aggregate initialization, we have to be a bit more cautious.

char string[] = {G, o, k, c , e};

will create an array of 5 characters, not 6. There won’t be any NULL character at the end of the character array. If that’s not what we really want we should either add the NULL character in initialization as in

char string[] = {G, o, k, c, e, ‘\0};

or revert back to the former style. In both cases, the array is allocated in the runtime stack.

Note also the use of escape sequences for embedding double quotes in the character string. Since it is used to flag the end it's not possible to insert '”' in a string literal. The way out of this is by means of escaping '”', which tells the compiler that the following '”' does not end the character string, it should be literally embedded in the string.

18   char string[] = "Kind of \"long\" string";
19   int i;
20   int sequence[] = {9, 1, 3, 102, 7, 5, 0};

Third argument of the next printf statement is a call to a function that returns an int. What’s more interesting with this call is the way its argument is passed. Although we seem to be passing an array, what is really passed behind the scenes is the address of the first component of the array; that is, &string[0]. This is done regardless of the array size. Advantages of such a scheme are:

  1. All we need to pass is a single pointer, not an entire array. As the array size grows, we save more on memory.
  2. Now that we pass a single pointer we avoid copying the entire array. This means saving both on time and memory.
  3. The changes we make in the array are permanent; the caller sees the changes made by the callee. This is due to the fact that it is the pointer that is passed by value, not the array. So, although we cannot change the address of the first component of the array, we can modify the contents of the array.

The down side of it is that you need to make a local copy of the array if you want the array to remain the same between calls.

21   printf("Length of \"%s\": %d\n", string, string_length(string));
22   printf("Sum of ints ( ");
23   for (i = 0; sequence[i] != 0; i++)
24     printf("%d ", sequence[i]);
25   printf("): %ld\n", sum_ints(sequence));
26 
27   return(0);
28 } /* end of int main(void) */

Bit Manipulation[edit]

Originally a systems-programming language, C offers assistance for manipulation of data on a bit-by-bit basis. This comprises bitwise operations and a facility to define structure with bit fields.

Not surprisingly, this aspect of the language is utilized in machine-dependent applications such as real-time systems, system programs [device drivers, for instance] where running speed is a primary concern. Given the sophisticated optimizations done by today's compilers and the non-portable nature of bit manipulation, in the presence of an alternative their use in general-purpose programming should be avoided.

Bitwise Operations[edit]

Problem: Write functions to extract exponent and mantissa of a float argument.

Extract_Fields.c
1 #include <limits.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 
5 #define TOO_FEW_ARGUMENTS 1
6 #define TOO_MANY_ARGUMENTS 2

Specification of C does not standardize (with the exception of type char) the size of integer types. The only guarantee given by the language specification is the following relation:

sizeof(short)sizeof(int)sizeof(long)sizeof(long long)

On most machines an int takes up four bytes, whereas on some it takes up two bytes. A menifestation of C's system programming orientation, this variance is due to the fact that size of int was meant to be equal to the word size of the underlying architecture. As a consequence of this, if we take it for granted that four bytes are used to represent an int value, we may occasionally see our programs producing insensible output. This will be due to the fact that an int value representable in four bytes will probably give rise to an overflow when represented in two bytes.

The following #if-#else directive is used to circumvent this problem. UINT_MAX, defined in limits.h, holds the largest possible unsigned int value. On machines where an int value is stored in two bytes, this will be equal to 65535. Otherwise, it will be something else. So, if UINT_MAX happens to be 65535 we can say an int is represented in two bytes. If not, it is represented in four bytes.

 6 #if UINT_MAX==65535
 7 typedef unsigned long BIG_INT;
 8 #else
 9 typedef unsigned int BIG_INT;
10 #endif
11 
12 char *usage = USAGE: Extract_Fields float_number;

Next function pulls the exponent part of a particular float variable. This is done by isolating the exponent part and shifting the resulting value to right. We use bitwise-and operator (binary &) to isolate the number and shift-right (>>) this isolated exponent (in a way right-adjust the number).

Note that result of right-shifting a negative integer—that is, a number with a bit pattern having 1 in the most significant bit—is undefined. In other words, the behavior is implementation dependent. While in some implementations the sign bit is preserved and therefore right-shifting is effectively sign-extending the number, in others this bit is replaced with a 0.[3]

12 BIG_INT exponent(float num) {
13   float *fnump = &num;
14   BIG_INT *lnump = (BIG_INT *) fnump;

Partial image of memory at this point is given in Figure 3. If, say, num has -1.5 as its value, it will be interpreted as -1.5 when seen through fnump. That is, *fnump will be -1.5. However, if seen through lnump, it will be interpreted to contain 3,217,031,168! The difference is due to different ways of looking at the same thing: While *fnump sees num as a float value compliant with the IEEE 754/854, *lnump sees the same four bytes of memory as an integer value (of type unsigned long or unsigned int, depending on the machine the program is running on) encoded using 2’s complement notation.

(insert figure here)

Question
What would be the value of *fnump after we increment *lnump by 1?

The following line first uses a bit mask to isolate the exponent part and then shifts it to right so that the exponent bits occupy the least significant numbers.

(insert figure here)

15   return((*lnump & 0x7F800000) >> 23);
16 } /* end of BIG_INT exponent(float) */

Next function extracts the mantissa part of the number. It does this simply by masking out the sign and exponent parts of the number. This is accomplished by the bitwise-and operator in the return expression. Note that we do not need to shift the number since its mantissa is made up of the lowest 23 digits of the representation.

18 BIG_INT mantissa(float num) {
19   float *fnump = &num;
20   BIG_INT *lnump = (BIG_INT*) fnump;
21 
22   return(*lnump & 0x007FFFFF);
23 } /* end of BIG_INT long mantissa(float) */

The following function tries to make sense of the command line arguments. Number of such arguments being one means the user has not passed any numbers. We display an appropriate message and exit the program. If the argument count is two the second one is taken to be the number whose components we will extract. Otherwise, the user has passed more arguments than we could deal with; we simply let her know about it and exit the program.

In case of failure it’s a recommended practice to exit with a nonzero value. This may turn out to be of great help when programs are run in coordination by means of a shell script. If a program depends on the successful completion of another, the script needs to have a reliable way of checking the result of the previous programs. A program exiting with non-descriptive values is of no help in such situations. We have to make sure that when we return with zero, it really means successful execution. Otherwise there is something wrong, which may further be elucidated by different exit codes.

strtod function is used to convert a numeric string, which may also contain -/+ and e/E, to a floating-point number of type double. While it’s easy to figure out the function of the first argument, which is a pointer to the string to be converted, one cannot say the same thing about the second argument. Upon return from strtod the second argument will hold a pointer to the character following the converted part of the input string. Thanks to this, we can process the rest of the string using strtod and its friends.

Friends of strtod
Declaration Description In Case of Error
long strtol(const char* str, char **ptr, int base): Converts a character string to a long int. Returns 0L.
unsigned long strtoul(const char* str, char **ptr, int base): Converts a string to an unsigned long int. Returns 0L.
double atof(const char* str): Converts str to a floating point number and returns the result of this conversion as a double. Returns 0.0.
int atoi(const char* str): Converts str to an integer and returns the result of this conversion as an int. Returns 0.
unsigned long atol(const char* str): Converts str to an integer and returns the result of this conversion as an unsigned long. Returns 0L.
char* strtok(char* str, const char* set): Tokenizes str using characters in set as separators. Passing NULL for the first argument tells this function to pick up from where it left off the last time.
Parse.c

#include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char *name, *midterm, *final, *assignment; // Assume argv[1] is “Selin Yardimci: 80, 90, 100”. name = strtok(argv[1], ":"); // // printf("Name: %s\n", name); midterm = strtok(NULL, ","); // // printf("Midterm: %lu\t", strtoul(midterm, NULL, 10)); final = strtok(NULL, ","); // // printf("Final: %lu\t", strtoul(final, NULL, 10)); assignment = strtok(NULL, "\0"); // // printf("Assignment: %lu\n", strtoul(assignment, NULL, 10)); return(0); } /* end of int main(int, char**) */

gcc -o Parse.exe Parse.c↵
&num; Double-quotes are needed to treat the string as a single argument. It is not a part of the argument string and will be stripped before passing to main!
Parse "Selin Yardimci: 80, 90, 100"↵
Name: Selin Yardimci
Midterm: 80 Final: 90 Assignment: 100
25 float number(int argc, char *argv[]) {
26   switch (argc) {
27     case 1: 
28       printf(Too few arguments. %s\n, usage);
29       exit(TOO_FEW_ARGUMENTS);
30     case 2:
31       return((float) strtod(argv[1], NULL));
32     default:
33       printf(Too many arguments. %s\n, usage);
34       exit(TOO_MANY_ARGUMENTS):
35   } /* end of switch (argc) */
36 } /* end of float number(int, char **) */

Observe the simplicity of the main function. We just state what the program does; we do not delve in the details of how it does that. Reading the main function, the maintainer of this code can easily figure out what it claims to do. In case she may need more detail, she has to check the function bodies. Depending on the complexity of the program, these functions will provide full details of the implementation or defer this provision to another function. In a complex program this deferment can be extended to multiple levels.

However simple or complex the problem at hand might be and whichever paradigm we use, we start by answering the question "What" and then move (probably in degrees) on to answering the question "How". In other words, we first analyze the problem and come up with a design for the solution and then provide an implementation.[4] Our code should reflect this: it should first expose the answer to "what" (interface) and then (to the interested parties) the answer to "how" (implementation).

38 int main(int argc, char *argv[]) {
39   float num;
40 
41   printf(Exponent of the number is: %x\n, 
42             exponent(num = number(argc, argv)));
43   printf(Mantissa of the number is: %x\n, mantissa(num));
44 
45   return(0);
46 } /* end of int main(int, char**) */

Bit Fields[edit]

Problem: Using bit fields, implement the previous problem.

Extract_Fields_BF.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

...

Bit fields are defined similar to ordinary record fields. The only difference between the two is the width specification following the bit field. According to the definition below fraction, exponent, and sign occupy twenty-three, eight, and one bit, respectively. However, rest is pretty much up to the compiler implementation.

To start with, as is manifested by the preprocessor directive, memory layout of a variable of type struct SINGLE_FP depends on the processor endianness. Manner of bit packing is also not guaranteed to be the same among different hardware. These two factors effectively limit the use of bit fields to machine-dependent programs.

struct SINGLE_FP {
#ifdef BIG_ENDIAN /* e.g. Motorola */
  unsigned int sign : 1;
  unsigned int exponent : 8;
  unsigned int fraction : 23;
#else /* if LITTLE_ENDIAN, e.g. Intel */
  unsigned int fraction : 23;
  unsigned int exponent : 8;
  unsigned int sign : 1;
#endif
};

BIG_INT exponent(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;

There are two field access operators in C: . and ->. The former is used to access fields of a structure, whereas the latter is used to access fields of a structure through a pointer to the structure. Now that lnump is defined to be a pointer to the SINGLE_FP structure, all variables declared to be of type SINGLE_FP can access the bit fields through the use of ->.

Observe structure->field is equivalent to (*structure).field. For example, lnump->exponent is equivalent to (*lnump).exponent

  return(lnump->exponent);
} /* end of BIG_INT exponent(float) */

BIG_INT mantissa(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;  

  return(lnump->fraction);
} /* end of BIG_INT mantissa(float) */

float number(int argc, char *argv[]) { ... }

int main(int argc, char *argv[]) { ... }

Static Local Variables (Memoization)[edit]

Problem: Using memoization write a low cost recursive factorial function.

Reminiscent of caching memoization can be used to speed up programs by saving results of computations that have already been made. The difference between the two lies in their scopes. When we speak of caching we speak of a system- or application-wide optimization technique. On the other hand, memoization is a function-specific technique. When a request is received we first check to see whether we can avoid computing the result from scratch. Otherwise, computation is carried out from scratch and the result is added to our database. In other words, we prefer computation-in-space over computation-in-time and save some precious computer time.

Applying this technique to the problem at hand we will store the set of already computed factorials in a static local array. Now that any changes made to this array will be persistent between different calls, basis condition for recursion will be changed to reaching a computed factorial, not an argument value of 1 or 0. That is, we have

Mathematical definition of the factorial function


Memoize.c
1 #include <stdio.h>
2 
3 #define MAX_COMPUTABLE_N 20
4 #define TRUE 1
5 
6 unsigned long long factorial(unsigned char n) {

As soon as the program starts running the following initializations will have taken effect. As a matter of fact, since static local variables are allocated in the static data region initial values for them are present in the disk image of the executable.

Mark the initializer of the array used for storing the already made computations. Although it has MAX_COMPUTABLE_N components only two values are provided in the initializer. The remaining slots are filled with the default initial value 0. In other words, it is equivalent to

static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1, 0, 0, , .., 0};

Note that we could provide more initial values to avoid more of the initial cost.

static unsigned char largest_computed = 1;
static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1};

If we have already computed the factorial of a number that is equal to or greater than the current argument value, we retrieve this value and return it to the caller.

Note that receiver of the returned value can be either the main function or another invocation of the factorial function. In the second case, we do part of the computation and use some partial result from the previous computations.

7   if (n <= largest_computed) return computed_factorials[n];
8 
9   printf("N: %d\t", n);

Once new values are computed it is stored in our array and the largest argument value for which factorial has been computed is appropriately updated to reflect this fact.

10   computed_factorials[n] = n * factorial(n - 1);
11   if (largest_computed < n) largest_computed = n;
12 
13   return computed_factorials[n];
14 } /* end of unsigned long long factorial(unsigned char) */
15 
16 int main(void) {
17   short n;
18 
19   printf("Enter a negative value for termination of the program...\n");
20   do {
21     printf("Enter an integer in [0..20]: ");

h preceding the conversion letter (d) specifies that input is expected to be a short int. Similarly, one can use l to specify a long int.

22     scanf("%hd", &n);
23     if (n < 0) break;
24     if (n > 20) {
25       printf("Value out of range!!! No computations done.\n");

Like break continue is used to alter the flow of control inside loops; it terminates the execution of the innermost enclosing while, do while, and for statements. In our case, control will be transferred to the beginning of the do-while statement.

26       continue;
27     } /* end of if (n > 20) */
28     printf("%d! is %Lu\n", n, factorial((unsigned char) n));
29   } while (TRUE);
30 
31   return(0);
32 } /* end of int main(void) */
gcc –o MemoizedFactorial.exe Memoize.c↵
MemoizedFactorial↵
Enter a negative value for termination of the program
Enter an integer in [0..20]: 1
1! is 1
Enter an integer in [0..20]: 5
N:5 N:4 N:3 N:2 5! is 120
Enter an integer in [0..20]: 5
5! is 120
Enter an integer in [0..20]: 10
N:10 N:9 N:8 N:7 N:6 10! is 3628800
Enter an integer in [0..20]: -1

File Manipulation[edit]

Problem: Write a program that strips comments of a C program.


Strip_Comments.c
 1 #include <ctype.h>
 2 #include <errno.h>
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 #include <string.h>
 6 
 7 #define BOOL char
 8 #define FALSE 0
 9 #define TRUE 1
10 
11 #define MAX_LINE_LENGTH 500
12 
13 #define NORMAL_COMPLETION 0
14 #define CANNOT_OPEN_INPUT_FILE 11
15 #define CANNOT_OPEN_OUTPUT_FILE 12
16 #define TOO_FEW_ARGUMENTS 21
17 #define TOO_MANY_ARGUMENTS 22

When an identifier definition is qualified with const, it is taken to be immutable. Such an identifier cannot appear as an lvalue. This means we cannot declare an identifier to be constant and subsequently assign a value to it; a constant must be provided with a value at its point of declaration. In other words, a constant must be initialized.

Initializer of a global constant can contain nothing but subexpressions that can be evaluated at the compile-time. A local constant on the other hand can contain run-time values. For instance,

#include <stdio.h> #include <stdlib.h> void f(int ipar) { const int i = ipar * 3; printf(i: %d\n, i); } /* end of void f(int) */ int main(void) { int i = 6; f(5); f(10); f(i); f(i + 7); exit(0); } /* end of int main(void) */

will produce the following output:

i: 15
i: 30
i: 18
i: 39

This goes to show that constants are created for each function invocation and they can get different values. But they still cannot be modified throughout the function call.

19 const char file_ext[] = .noc;
20 const char temp_file[] = TempFile;
21 const char usage[] = "USAGE: StripComments filename";

In C, all identifiers must be declared before they are used! This includes file names, variables, and structure tags. The corresponding definition can be provided after the declaration in the same file or in a different one. While there can be more than one declaration, there can be only one definition.

Following declarations are prototypes of functions whose definitions are provided later in the program. Note the parameter names do not agree with the parameter names provided in the definition. As a matter of fact, one does not even have to provide the names. But, you still have to list the parameter types so as to facilitate type checking: It is an error if the definition of a function or any uses of it do not agree with its prototype.

The use of prototypes can be avoided by rearranging the order of definitions. For this example, putting the main function at the end would remove the need for the prototypes.

23 char* filename(int argumentCount, char* argumentVector[]);
24 void trimWS(const char *filename);
25 void strip(const char *filename);

Comma-separated expressions are considered a single expression result of which is the result returned by the last expression. Evaluation of a comma-separated expression is strictly from left to right; the compiler cannot change this order.

27 int main(int argc, char* argv[]) {
28   char *fname = filename(argc, argv);
29   (strip(fname), trimWS(fname));
30 
31   return(NORMAL_COMPLETION);
32 } /* end of int main(int, char**) */
33 
34 char* filename(int argc, char* argv[]) {

A more general version of printf, fprintf performs output formatting and sends the output to the stream specified as the first argument. We can therefore see printf as an equivalent form of the following:

fprintf(stdin, "...", ...); /* read it as "file printf..." */

Another friend of printf that you may want to consider in output formatting is the sprintf function. This function, instead of writing the output to some media, causes it to be stored in a string of characters.

typedef double currency; currency expenditure = 234.0; ... char *str = (char*) malloc(); ... sprintf(str, %f.2$”, expenditure); printf(Total spending: %s, str); /* will print “Total spending: 234.00$” */ ... free(str); ...

35   switch (argc) {
36     case 1: 
37       fprintf(stderr, "No file name passed!\n %s\n", usage);
38       exit(TOO_FEW_ARGUMENTS);
39     case 2: return(argv[1]);
40     default: 
41       fprintf(stderr, "Too many arguments!\n %s\n",usage);
42       exit(TOO_MANY_ARGUMENTS);
43   } /* end of switch(argc) */
44 } /* end of char* filename(int, char**) */
45 
46 void trimRight(char *line) {
47   int i = strlen(line) - 1;
48 
49   do 
50     i--; 
51   while (i >= 0 && isspace(line[i])) ;
52 
53   line[i + 1] = '\n';
54   line[i + 2] = '\0';
55 } /* end of void trimRight(char*) */
56 
57 void trimWS(const char *infilename) {
58   char next_line[MAX_LINE_LENGTH];
59   char outfilename[strlen(infilename) + strlen(file_ext) + 1];
60   FILE *infile, *outfile;
61   BOOL empty_line = FALSE;
MS Visual C/C++ compiler will emit a compilation error for the highlighted line. However, according to the ISO C, automatic—that is, local—arrays may have an unknown size and this size is determined by the initializer in the run-time. In order for this program to compile using MS Visual C/C++, you need to change this array definition to a pointer definition and allocate/deallocate space for the array in the heap by using the malloc/free functions.


The following statement tries to open a file in reading mode. It maps the physical file, whose name is held in variable temp_file, to the logical file named infile. If this attempt results in success every operation you apply on the logical file will be executed on the physical file. infile variable can be thought of as a handle on the real file. The mapping between the handle and the physical file is not one-to-one. Just like more than one handle can show the same object, one can have more than one handle on the same physical file. There is no problem as long as all handles are used in read mode. But things get ugly if different handles simultaneously try to modify the same file. [The keywords are operating systems, concurrency, and synchronization.]

If the open operation fails we cannot get a handle on the physical file. That is reflected in the value returned by the fopen function: NULL. A pointer having a NULL value means that we cannot use it for further manipulation. All we can do is to test it against NULL. So, we check for this condition first. Unless it is NULL we proceed; otherwise we write something about the nature of the exceptional condition to the standard error file stderr and exit the program.

Just like the standard output, the standard error file is by default mapped to the screen. So, why do we write to stderr instead of stdio? The answer is, we may choose to re-map these standard files to different physical units. In such a case if we kept writing everything to the same logical file, say stdio, errors would clutter valid output data; we would have difficulty telling which one is which.

 63   infile = fopen(temp_file, "r");
 64   if (infile == NULL) {
 65     fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
 66     exit(CANNOT_OPEN_INPUT_FILE);
 67   } /* end of if (infile == NULL) */
 68 
 69   strcpy(outfilename, infilename); strcat(outfilename, file_ext);
 70   outfile = fopen(outfilename, "w");
 71   if (outfile == NULL) {
 72     fprintf(stderr, "Error in opening file %s: %s\n", outfilename, strerror(errno));
 73     fclose(infile);
 74     exit(CANNOT_OPEN_OUTPUT_FILE);
 75   } /* end of if (outfile == NULL) */
 76 
 77   while (fgets(next_line, MAX_LINE_LENGTH + 1, infile)) {
 78     trimRight(next_line);
 79     if (strlen(next_line) == 1) {
 80       if (!empty_line) fputs(next_line, outfile);
 81       empty_line = TRUE;
 82       } else {
 83         fputs(next_line, outfile);
 84         empty_line = FALSE;
 85       } /* end of else */
 86   } /* end of while (fgets(next_line, …) */
 87 
 88   fclose(infile); fclose(outfile); remove(temp_file);
 89 
 90   return;
 91 } /* end of void trimWS(const char*) */
 92 
 93 void strip(const char *filename) {
 94   int next_ch;
 95   BOOL inside_comment = FALSE;
 96   FILE *infile, *outfile;
 97 
 98   infile = fopen(filename, "r");
 99   if (infile == NULL) {
100     fprintf(stderr, "Error in opening file %s: %s\n", filename, strerror(errno));
101     exit(CANNOT_OPEN_INPUT_FILE);
102   } /* end of if (infile == NULL) */
103 
104   outfile = fopen(temp_file, "w");
105   if (outfile == NULL) {
106     fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
107     fclose(infile);
108     exit(CANNOT_OPEN_OUTPUT_FILE);
109   } /* end of if (outfile == NULL) */

The solution to the problem can be modeled using the following finite automaton.

Finite automaton providing the solution

Note the ease of transforming the FA-based solution to C code. This is yet another example of how useful, however useless and boring it might look at first, such a theoretic model might be.

A problem is solved by transforming its problem domain representation to the corresponding solution domain representation. The more one knows about models to represent a problem the more easily she will come up with the solution of the problem.

111   while ((next_ch = fgetc(infile)) != EOF) {
112     switch (inside_comment) {
113       case FALSE:
114         if (next_ch != '/') { fputc(next_ch, outfile); break; }
115         if ((next_ch = fgetc(infile)) == '*') inside_comment = TRUE;
116           else { fputc('/', outfile); ungetc(next_ch, infile); }
117       break;
118       case TRUE:
119         if (next_ch != '*') break; 
120         if ((next_ch = fgetc(infile)) == '/') inside_comment = FALSE;
121     } /* end of switch(inside_comment) */
122   } /* end of while ((next_ch = fgetc(infile)) != EOF) */

fclose disconnects the logical file(handle) from the physical file. If not done by the programmer, code found in the exit sequence of the C runtime guarantees that all opened files are closed at the end of the program. Leaving it to the exit sequence has two disadvantages, though.

  1. There is a maximum number of files that an application can have open at the same time. If we defer closing of the files to the exit sequence, we may reach this limit more frequently and quickly.
  2. Unless you explicitly flush using fflush, data you write to a file is actually written to a buffer in memory, not to disk. It is flushed automatically whenever you write a newline to the file or the buffer area becomes full. It means that if there occurs a catastrophic failure, such as an outage, between the earliest time you could close the file and the time exit sequence closes it at the end of the program, the data left in the buffer area will not have been committed to the disk. Not a perfect success story!

So you should either flush explicitly or close the file as early as possible.

124   fclose(infile); 
125   fclose(outfile);
126 
127   return;
128 } /* end of void strip(const char *) */

Heap Memory Allocation[edit]

Problem: Write an encryption program that reads from a file and writes the encoded characters to some other file. Use this simple encryption scheme: The encrypted form of a character c is c ^ key[i], where key is a string passed as a command line argument. The program uses the characters in key in a cyclic manner until all the input has been read. Re-encrypting encoded text with the same key produces the original text. If no key or a null string is passed, then no encryption is done.

Cyclic_Encryption.c
 1 #include <errno.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 #include <string.h>
 5 
 6 #define CANNOT_OPEN_INPUT_FILE 11
 7 #define CANNOT_OPEN_OUTPUT_FILE 12
 8 #define TOO_FEW_ARGS 21
 9 #define TOO_MANY_ARGS 22
10 
11 const char *usage = "USAGE: CyclicEncryption inputfilename [key [outputfilename]]";
12 const char file_ext[] = ".enc";
13 
14 struct FILES_AND_KEY {
15   char *infname;
16   char *outfname;
17   char *key;
18 };
19 
20 typedef struct FILES_AND_KEY f_and_k;
21 
22 f_and_k getfilenameandkey(int argc, char **argv) { 
23   f_and_k retValue = { "\0", "\0", "\0" };
24 
25   switch (argc) {
26     case 1:
27       fprintf(stderr, "No file name passed!\n %s", usage);
28       exit(TOO_FEW_ARGS);

malloc is used to allocate storage from the heap, the area of memory that is managed by the programmer herself. This implies that it is the responsibility of the programmer to return every byte of memory allocated through malloc to the pool of available memory.

Such memory is indirectly manipulated through the medium of a pointer. Different pointers can point to the same region of memory. In other words, they can share the same object.

Using pointers to manipulate heap objects does not mean that pointers can point only to heap objects. Nor does it mean that pointers themselves are allocated in the heap. Pointers can point to non-heap objects and they can reside in the static area or the run-time stack. Indeed, such was the case in the previous examples.

29     case 2:
30       retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
31       strcpy(retValue.infname, argv[1]);
32       retValue.outfname = (char *) malloc(
33       strlen(argv[1]) + strlen(file_ext) + 1);
34       strcpy(retValue.outfname, argv[1]);
35       strcat(retValue.outfname, file_ext);
36       return(retValue);
37     case 3:
38       retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
39       strcpy(retValue.infname, argv[1]);
40       retValue.key= (char *) malloc(strlen(argv[2]) + 1);
41       strcpy(retValue.key, argv[2]);
42       retValue.outfname = (char *) malloc(
43       strlen(argv[1]) + strlen(file_ext) + 1);
44       strcpy(retValue.outfname, argv[1]);
45       strcat(retValue.outfname, file_ext);
46       break;
47     case 4:
48       retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
49       strcpy(retValue.infname, argv[1]);
50       retValue.key = (char *) malloc(strlen(argv[2]) + 1);
51       strcpy(retValue.key, argv[2]);
52       retValue.outfname = (char *) malloc(strlen(argv[3]) + 1);
53       strcpy(retValue.outfname, argv[3]);
54       break;
55     default:
56       fprintf(stderr, "Too many arguments!\n %s", usage);
57       exit(TOO_MANY_ARGS);
58   } /* end of switch(argc) */
59 
60   return retValue;
61 } /* end of f_and_k getfilenameandkey(int, char**) */
62 
63 void encrypt(f_and_k fileandkey) {
64   int i = 0, keylength = strlen(fileandkey.key);
65   int next_ch;
66   FILE *infile;
67   FILE *outfile;
68 
69   if (keylength == 0) return;

The following line opens a file that can be read byte-by-byte (binary mode), not character-by-character (text mode). In an operating system like UNIX, where each and every character is mapped to a single entry in the code table, there is no difference between the two. But in MS Windows, where newline is mapped to carriage return followed by linefeed, there is a difference, which may escape incautious programmers. The following C program demonstrates this. Run it with a multi-lined file and you will see that the number of fgetc's needed will be different. The number of characters you read will be less than the number of bytes you read. This discrepency, which is a consequence of the difference in the treatment of the newline character, is likely to cause a great headache when you move your working code from Linux to MS Windows.

Newline.c

#include <errno.h> #include <stdio.h> #include <string.h> #define CANNOT_OPEN_INPUT_FILE 1 int main(void) { int counter; int next_ch; FILE *in1, *in2; in1 = fopen("Text.txt", "r"); if (!in1) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in1) */ counter = 1; while((next_ch = fgetc(in1)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); printf("\n"); fclose(in1); in2 = fopen("Text.txt", "rb"); if (!in2) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in2) */ counter = 1; while((next_ch = fgetc(in2)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); fclose(in2); return(0); } /* end of int main(void) */

Text.txt

First line Second line Third Line Fourth and last line

If you compile and run the program listed on the previous page in a Unix-based environment, it will produce identical outputs for both (text and binary) modes. For MS Windows and DOS environments they will produce the following output.

gcc –o Newline Newline.c↵
Newline↵
1.ch: 70 ...
...
11.ch: 10 12.ch: 83 ...
...
... 54.ch: 101
1.ch: 70 ...
...
11.ch: 13 12.ch: 10 13.ch: 83 ...
...
...
... 57.ch: 101
 71   infile = fopen(fileandkey.infname, "rb");
 72   if (!infile) {
 73     fprintf(stderr, "Error in opening file %s: %s\n",
 74     fileandkey.infname, strerror(errno));
 75     exit(CANNOT_OPEN_INPUT_FILE);
 76   } /* end of if (!infile) */
 77 
 78   outfile = fopen(fileandkey.outfname, "wb");
 79   if (!outfile) {
 80     fprintf(stderr, "Error in opening output file %s: %s\n", 
 81     fileandkey.outfname, strerror(errno));
 82     fclose(infile);
 83     exit(CANNOT_OPEN_OUTPUT_FILE);
 84   } /* end of if (!outfile) */
 85 
 86   while ((next_ch = fgetc(infile)) != EOF) {
 87     fprintf(outfile, "%c",(char) (next_ch ^ fileandkey.key[i++]));
 88     if (keylength == i) i = 0;
 89   } /* end of while ((next_ch = fgetc(infile)) != EOF) */
 90 
 91   fclose(outfile); fclose(infile);
 92 
 93   return;
 94 } /* end of void encrypt(f_and_k) */
 95 
 96 int main(int argc, char *argv[]) {
 97   f_and_k fandk = getfilenameandkey(argc, argv);
 98 
 99   encrypt(fandk);
100   free(fandk.infname);
101   fandk.infname = fandk.outfname; 
102   fandk.outfname = (char *) 
103   malloc(strlen(fandk.infname) + strlen(file_ext) + 1);
104   strcpy(fandk.outfname, fandk.infname);
105   strcat(fandk.outfname, file_ext);
106   encrypt(fandk);
107 
108   return(0);
109 } /* end of int main(int, char **) */

Pointer to Function (Callback)[edit]

Problem: Write a generic bubble sort routine. Test your code to sort an array of ints and character strings.

General.h
#ifndef GENERAL_H
#define GENERAL_H

...

#define BOOL char
#define FALSE 0
#define TRUE 1
...
typedef void* Object;

The following typedef statement defines COMPARISON_FUNC to be a pointer to a function that takes two arguments of Object (that is void*) type and returns an int. After this definition, in this header file or any file that includes this header file, we can use COMPARISON_FUNC just like any other data type. That actually is what we do in Bubble_Sort.h and Bubble_Sort.c. Third argument of the bubble_sort function is defined to be of type COMPARISON_FUNC. That is, we can pass address of any function conforming to the prototype stated below.

void* is used as a generic pointer. In other words, data pointed to by the pointer is not assumed to belong to a particular type. In a sense, it serves the same purpose the Object class at the top of the Java class hierarchy does. Just as any object can be treated to belong in the Object class, any value, be that something as simple as a char or something as complex as a database, can be pointed to by this pointer. But, such a pointer cannot be dereferenced with the * or subscripting operators. Before using it one must first cast the pointer to an appropriate type.

typedef int (*COMPARISON_FUNC)(Object, Object);
...
#endif


Bubble_Sort.h
1 #ifndef BUBBLE_SORT_H
2 #define BUBBLE_SORT_H
3 
4 #include "General.h"

bubble_sort function sorts an array of Objects (first argument), whose size is provided as the second argument. Now that there is no universal way of comparing two components and we want our implementation to be generic, we must be able to dynamically determine the function that compares two items of any type. Dynamic determination of the function is made possible by using a pointer to a function. Assigning different values to this pointer enables using different functions, which means different behavior for different types. Coming up with parameter types that are common to all possible data types is the key to making this pointer to function work for all. For this reason, comp_func takes two arguments of type Object, that is void*, which can be interpreted to belong to any type.

6 extern void bubble_sort (Object arr[], int sz, COMPARISON_FUNC comp_func);
7 
8 #endif


Bubble_Sort.c
1 #include <stdio.h>
2 
3 #include "General.h"
4 #include "algorithms\sorting\Bubble_Sort.h"

The static qualifier in the following definition limits the visibility of swap to this file. In a sense, it is what private qualifier is to a class in OOPL’s. The difference lies in the fact that a file is an operating system concept (that is, it is managed by OS), while a class is a programming language one (that is, it is managed by the compiler). The latter is certainly a higher-level abstraction. In the absence of a higher-level abstraction, it (higher-level abstraction) can be simulated using lower level one(s). Intervention of an external agent is required to provide this simulation, which gives rise to a more fragile solution. Such is the case in this solution: the programmer, the intervening agent, has to simulate this higher-level of abstraction through using some programming conventions.

 6 static void swap(Object arr[], int lhs_index, int rhs_index) {
 7   Object temp = arr[lhs_index];
 8   arr[lhs_index] = arr[rhs_index];
 9   arr[rhs_index] = temp;
10 
11   return;
12 } /* end of void swap(Object[], int, int) */
13 
14 void bubble_sort (Object arr[], int sz, COMPARISON_FUNC cmp) {
15   int pass, j;
16 
17   for(pass = 1; pass < sz; pass++) {
18     BOOL swapped = FALSE;
19 
20     for (j = 0; j < sz - pass; j++)

We could have written the following line as follows:

if (cmp(arr[j], arr[j + 1]) > 0) {

and it would still do the same thing. So, a pointer-to-function variable can be used just like a function: simply use its name and enclose the arguments in parentheses. This will cause the function pointed to by the variable to be called. Nice thing about calling a function through a pointer is the fact that you can call different functions by simply changing the value of the pointer variable. If you pass the address of compare_ints to the bubble_sort function, it will call compare_ints; if you pass the address of compare_strings, it will call compare_strings; if ...

21       if ((*cmp)(arr[j], arr[j + 1]) > 0) {
22         swap(arr, j, j + 1);
23         swapped = TRUE;
24       } /* end of if ((*cmp)(arr[j], arr[j + 1]) > 0) */
25 
26     if (!swapped) return; 
27   } /* end of outer for loop */
28 } /* void bubble_sort(Object[], int, COMPARISON_FUNC) */


Pointer2Function.c
1 #include <stdio.h>
2 #include <string.h>
3 
4 #include “General.h”

The following line brings in the prototype of the bubble_sort function, not the source code or the object code for it.

The compiler utilizes this prototype in type-checking the function use. This involves checking the number of parameters, their types, whether it [the function] is used in the right context. As soon as this is confirmed, the linker takes over and [if it is in a separate source file] brings in the object code for bubble_sort. So,

  1. Preprocessor preprocesses the source code by replacing directives with C source code.
  2. Compiler, using the provided meta-information (such things as variable declarations/definitions and function prototypes), checks the syntax and semantics of the program. Note that by the time the compiler takes over, all preprocessor directives will have been replaced with C source. That is, the compiler does not know anything about the preprocessor.
  3. Linker combines object files to a single executable file. This executable is later loaded into memory by the loader and run under the supervision of the operating system.
 5 #include "algorithms\sorting\Bubble_Sort.h"
 6 
 7 typedef int* Integer;
 8 
 9 void print_args(char *argv[], int sz) {
10   int i = 0;
11 
12   if (sz == 0) { 
13     printf("No command line args passed!!! Unable to test strings...\n");
14     return;
15   }
16   for (; i < sz; i++)
17     printf("Arg#%d: %s\t", i + 1, argv[i]);
18   printf("\n");
19 } /* end of void print_args(char **, int) */
20 
21 void print_ints(Integer seq[], int sz) {
22   int i = 0;
23 
24   for (; i < sz; i++) 
25   printf("Item#%d: %d\t", i + 1, *(seq[i]));
26   printf("\n");
27 } /* end of void print_ints(Integer[], int) */

Next two functions are needed for making comparisons in the implementation of the sorting algorithm. They compare two values of the same type.

Implementer of the bubble_sort function cannot know in advance the countless number of object types to be sorted and there is no universal way of comparing that works for all types. So, user of the sorting algorithm must implement the comparison function and let the implementer know about this function. Implementer makes use of this function to successfully provide the service. In doing this it calls “back” to the user’s code. This type of a call is therefore named a callback.

29 int compare_strings(Object lhs, Object rhs) {
30   return(strcmp((char *) lhs, (char *) rhs));
31 } /* end of int compare_strings(Object, Object) */

Looks like a very complicated way of comparing two ints? That’s right! But remember: we have to be able to compare two objects (values) of any type and we must do it with one single function signature. Comparison of two ints is straightforward: just compare the values. But, how about character strings? Comparing the pointers will not yield an accurate result; we must compare the character strings pointed to by the pointers. It gets even more complex as we compare two structures that have nested structures in themselves. The solution to this is to pass the ball to the person who knows it best (the user of the algorithm) and while doing so pass a generic pointer (void *) to the data, not the data itself. In the process, the user will cast it into an appropriate type and make the comparison accordingly.

32 int compare_ints(Object lhs, Object rhs) {
33   Integer ip1 = (Integer) lhs;
34   Integer ip2 = (Integer) rhs;
35 
36   if (*ip1 > *ip2) return 1;
37   else if (*ip1 < *ip2) return -1;
38     else return 0;
39 } /* end of int compare_ints(Object, Object) */
40 
41 int main(int argc, char *argv[]) {
42   int seq[] = {1, 3, 102, 6, 34, 12, 35}, i;
43   Integer int_seq[7];
44 
45   for(i = 0; i< 7; i++) int_seq[i] = &seq[i];
46 
47   printf("\nTESTING FOR INTEGERS\n\nBefore Sorting\n");
48   print_ints(int_seq, 7);

The third argument passed to bubble_sort function is a pointer-to-function. This pointer contains a value that can be interpreted as the entry point of a function. Conceptually, there is not much of a difference between such a pointer and pointer to some data type. The only difference is in the memory region they point to. The latter points to some address in the data segment while the former to some address in the code segment. But one thing remains the same: the address value is always interpreted [in a particular way].

Note that you do not have to apply the address-of operator when you send a function as an argument.

49   bubble_sort((Object*) int_seq, 7, &compare_ints);
50   printf("\nAfter Sorting\n");	print_ints(int_seq, 7);
51 
52   printf("\nTESTING FOR STRINGS\n\nBefore Sorting\n");
53   print_args(&argv[1], argc - 1);
54   bubble_sort((Object*) &argv[1], argc - 1, compare_strings);
55   printf("After Sorting\n");	print_args(&argv[1], argc - 1);
56 
57   return(0);
58 } /* end of int main(int, char **) */

Linking to an Object File[edit]

cl /c /ID:\include Bubble_Sort.c↵

/I adds D:\include to the head of the list of directories to be searched for header files, which initially include the directory of the source file and the directories found in %INCLUDE%. Analogous to CLASSPATH of Java, it is used in organizing header files. Using this information, the preprocessor brings D:\include\algorithms\sorting\Bubble_Sort.h into the current file (Bubble_Sort.c). Once the preprocessor is through with its job, compiler will try to compile the resulting file and output an object file, named Bubble_Sort.obj.

cl /FeTest_Sort.exe /ID:\include Bubble_Sort.obj Pointer2Function.c↵

The above command compiles Pointer2Function.c as was explained in the previous paragraph. Once Pointer2Function.obj is created it is linked with Bubble_Sort.obj to form the executable named Test_Sort.exe. This linking is required to bring in the object code of bubble_sort function. Remember: inclusion of Bubble_Sort.h brought in the prototype, not the object code!

Modular Programming[edit]

Problem: Write a (pseudo) random number generator in C. It is guaranteed that only one generator is used by a particular application and different applications will be using it over and over again.

Now that our generator will be used by different applications, we had better put it in a separate file of its own so that, by linking with the client program, we can use it from different sources. This is similar to, if not same with, the notion of module supported in languages like Modula-2. The difference has to do with their abstraction levels: the module is an entity provided by the programming language and known to all of its users, while file is an entity provided by the operating system and known to all of its users. Programming language compiler (that is, an implementation of the programming language specification) being a user of OS-provided services and concepts means module concept is a higher abstraction.

Now that the module concept (higher abstraction) is not present in C, we need to simulate it using other, probably lower-level, abstraction(s). In this case, we use a file (lower abstraction). By doing so, we cannot regain all of what comes with a module, though. The notion of module remains unknown to the compiler; certain rules reinforced by the compiler in a modular programming language must be checked by the programmer herself/ himself. For example, interface and implementation of a module must be synchronized by the programmer, which is certainly an error-prone process.

None of the applications being expected to use more than one generator means that we can create the data fields related to the generator in the static data region; in order to identify which generator the function is acting on, we do not need to pass a separate, unique handle. This implies we do not need any creation or destruction functions. All we need to have is a function for initializing the generator and another for returning the next (pseudo) random number.

RNGMod.h
1 #ifndef RNGMOD_H
2 #define RNGMOD_H
3 
4 extern void RNGMod_InitializeSeed(long int);
5 extern double RNGMod_Next(void);
6 
7 #endif


RNGMod.c
1 #include "math\RNGMod.h"
2 
3 static long int seed = 314159;
4 const int a = 16807;
5 const long int m = 2147483647;

Generating a (pseudo) random number is similar to iterating through a list of values: we basically start from some point and move through the values one by one. The difference lies in the fact that the values generated are to be computed using a function rather than retrieved from some memory location. In other words, a generator iterates through a list using computation in time while an iterator does this using computation in space.

Seen from this perspective, initializing a (pseudo) random number generator with a seed is analogous to creating an iterator over a list. By using different values for the seed we can guarantee the generator returns a different value. Using the same seed value will give the same sequence of (pseudo) random values. Such a use may be wanted for replaying the same simulation scenarios for different parameters.

 7 void RNGMod_InitializeSeed(long int s) {
 8   seed = s;
 9 
10   return;
11 } /* end of void RNGMod_InitializeSeed(long int) */

The following function computes the next number in the sequence. The number being computed using a well-known formula means that the sequence is actually not random. That is, it can be known beforehand. This is why such a number is usually qualified with the word ‘pseudo’.

What makes the sequence look random is the number of different values it produces in a row. Formula used in the following function will produce all values between 1 and m – 1 and then repeat the sequence. More than two billion values!

In practice, it doesn’t make sense to use these large numbers. We therefore choose // to normalize the value into [0..1).

12 double RNGMod_Next(void) {
13   long int gamma, q = m / a;
14   int r = m % a;
15 
16   gamma = a * (seed % q) - r * (seed / q);
17   if (gamma > 0) seed = gamma;
18     else seed = gamma + m;
19 
20   return((double) seed / m);
21 } /* end of double RNGMod_Next(void) */


RNGMod_Test.c
 1 #include <stdio.h>
 2 
 3 #include "math\RNGMod.h"
 4 
 5 int main(void) {
 6 printf("TESTING RNGMod…\n");
 7 printf("Before initialization: %g\n", RNGMod_Next());
 8 RNGMod_InitializeSeed(35000);
 9 printf("After initialization: %g\t", RNGMod_Next());
10 printf("%g\t", RNGMod_Next());
11 printf("%g\t", RNGMod_Next());
12 printf("%g\n", RNGMod_Next());
13 
14 return(0);
15 } /* end of int main(void) */

Building a Program Using make[edit]

As the number of files needed to compile/link a program increases, it becomes more and more difficult to track the interdependency between the files. One tool to tackle with such situations is the make utility found in UNIX environments. This utility automatically determines which pieces of a large program need to be recompiled and issues commands to recompile them.

Input to make is a file consisting of rules telling which files are dependent on which files. These rules generally take the following shape:

target : prerequisites
TAB command
TAB command
TAB ...

The preceding rule is interpreted as follows: if the target is out of date use the following commands to bring it up to date. A target is out of date if it does not exist or if it is older than any of the prerequisites (by comparison of last-modification times).

Makefile

The following rule tells the make utility that RNGMod_Test.exe depends on RNGMod.o and RNGMod_Test.c. RNGMod_Test.exe is out of date if it does not exist or if it is older than RNGMod.o and RNGMod_Test.c. If any one of these two files is modified we have to update RNGMod_Test.exe by means of the command supplied in the next line.

Note the tab preceding the command is not there to make the file more readable; commands used to update the target must be preceded by a tab.

$@ is a special variable used to denote the target filename.

1 RNGMod_Test.exe : RNGMod.o RNGMod_Test.c
2   gcc -o $@ -ID:\include RNGMod.o RNGMod_Test.c
3 
4 RNGMod.o : RNGMod.c D:\include\math\RNGMod.h
5   gcc -c -ID:\include RNGMod.c

.PHONY is a predefined target used to define fake goals. It ensures the make utility that the target is not actually a file to be updated but rather an entry point. In this example, we use clean as an entry point enabling us to delete all relevant object files in the current directory.

7 .PHONY : clean
8 clean:
9   del RNGMod.o RNGMod_Test.exe

As soon as we save this file, all we have to do is to issue the make command. This command will look in the current directory for a file named makefile or Makefile. If you are using GNU version of make, GNUmakefile will also be tried. Once it finds such a file, make tries to update the target file of the first rule from the top.

make↵
gcc –c –ID:\include RNGMod.c
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 2.263 seconds
RNGMod_Test↵
Testing RNGMod...
Before initialization: 0.458724
After initialization: 0.273923 0.822585 0.186277 0.754617

Note that the time it takes the make utility to complete its task may vary depending on the processor speed and its load. In case only RNGMod_Test.c may have been modified we will see the following output.

make↵
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 1.673 seconds

We may at times want the make utility to start from some other target. In such a case we have to provide the name of the target as a command line argument. For instance, if we need to delete the relevant object files found in the current directory, issuing the following command will do the job.

make clean↵
del RNGMod.o RNGMod_Test.exe
Time: 0.171 seconds

Above presentation is a rather limited introduction to the make utility. For more, see Manual of GNU make utility.

Object-Based Programming (Data Abstraction)[edit]

Problem: Write a (pseudo) random number generator in C. Your solution should enable a single application to use more than one generator (with no upper bound to this number) at the same time. We should also meet the requirement of the generator being used from different applications.

As was mentioned in the [#Modular Programmingmodular|programming section], second requirement can be met by providing the generator in a separate file of its own.

In order to enable the use of more than one generator, where the exact number is not known, we must devise a method to create the generators dynamically as needed (similar to what constructors do in OOPL’s). This creation scheme must also give us something to uniquely identify each generator (similar to handles returned by constructors). We should also be able to return generators that are not needed anymore (similar to what destructors do in OOPL’s without automatic garbage collection).

RNGDA.h
1 #ifndef RNGDA_H
2 #define RNGDA_H

As stated in the requirements we have to come up with a scheme that lets users have more than one generator coexisting in the same program. We should in some way be able to identify each and every one of these generators and tell it from the others. This means that we cannot utilize the same method we used in the previous example. We have to get related functions to behave differently depending on the state of the particular generator. This difference in behavior can be achieved by passing an extra argument, a handle on the present state of the generator. This handle should hide the implementation details of the generator from the user; it should have immutable characteristics that we can use to hide the mutable properties of the generator. Sounds like the notion of handle in Java? That’s right! Unfortunately, C does not have direct support for handles. We need some other, probably less abstract, notion to simulate it. This less abstract notion we will use is the notion of pointer: whatever the size of the data it is a handle on, its size does not change.

In the following lines we first make a forward declaration of a struct named _RNG and then define a new type as a pointer to this not-yet-defined struct. By using forward declarations, we do not betray any of the implementation details; we just let the compiler know that we have the intention of using such a type. By defining a pointer to this type, we put a barrier between the user and the implementer: the user has something (pointer, something size of which does not change) to access a generator (underlying object, the size of which can be changed by implementation decisions) indirectly. Freedom of change means the generator can be used without any reference to the underlying object, which is why we call this approach data abstraction any type defined in such a way abstract data type. Note the pointer argument passed to the function. Function behavior changes depending on the underlying object, which is indirectly accessed by means of this pointer. That’s why this style of programming is called object-based programming.

 4 struct _RNG;
 5 typedef struct _RNG* RNG;
 6 
 7 extern RNG RNGDA_Create(long int);
 8 extern void RNGDA_Destroy(RNG);
 9 extern double RNGDA_Next(RNG);
10 
11 #endif


RNGDA.c
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 #include "math\RNGDA.h"
 5 
 6 struct _RNG { long int seed; };
 7 
 8 const int a = 16807;
 9 const long int m = 2147483647;
10 
11 RNG RNGDA_Create(long int s) {
12   RNG newRNG = (RNG) malloc(sizeof(struct _RNG));
13   if (!newRNG) {
14     fprintf(stderr, "Out of memory...\n");
15     return(NULL);
16   }
17   newRNG->seed = s;
18 
19   return(newRNG);
20 } /* end of RNG RNGDA_Create(long int) */
21 
22 void RNGDA_Destroy(RNG rng) { free(rng); }
23 
24 double RNGDA_Next(RNG rng) {
25   long int gamma;
26   long int q = m / a;
27   int r = m % a;
28 
29   gamma = a * (rng->seed % q) - r * (rng->seed / q);
30   if (gamma > 0) rng->seed = gamma;
31     else rng->seed = gamma + m;
32 
33   return((double) rng->seed / m);
34 } /* end of double RNGDA_Next(RNG) */


RNGDA_Test.c
 1 #include <stdio.h>
 2 #include "math\RNGDA.h"
 3 
 4 int main(void) {
 5   int i;
 6   RNG rng[3]; 
 7 
 8   printf("TESTING RNGDA\n");
 9   rng[0] = RNGDA_Create(1245L);
10   rng[1] = RNGDA_Create(1245L);
11   rng[2] = RNGDA_Create(2345L);
12   for (i = 0; i < 5; i++) {
13     printf("1st RNG, %d number: %f\n", i, RNGDA_Next(rng[0]));
14     printf("2nd RNG, %d number: %f\n", i, RNGDA_Next(rng[1]));
15     printf("3rd RNG, %d number: %f\n", i, RNGDA_Next(rng[2]));
16   } /* end of for (i = 0; i < 5; i++)*/
17   RNGDA_Destroy(rng[0]);
18   RNGDA_Destroy(rng[1]);
19   RNGDA_Destroy(rng[2]);
20 
21   return(0);
22 } /* end of int main(void) */

Notes[edit]

  1. ISO C permits whitespace to precede and follow the # character on the same source line, but older compilers do not.
  2. As we shall later see, void * is an exception to this.
  3. In Java, there are two right-shift operators: >> and >>>: While the former sign-extends its operand the latter does its job by zero-extending.
  4. This should not lead you to think that these phases cannot take place concurrently. What it tells is that phases must start in a certain order. For instance, once the preliminary design is ready, implementers can start implementation, but not before. But both teams must be in contact with each other and provide feedback. As designers make changes these are relayed to the implementation team; as problems surface in the implementation these are passed back to the design team. Note that same sort of relationship may be present between other teams in the software production cycle.