Martin Kalin - Modern C Up and Running
Martin Kalin - Modern C Up and Running
Running
A Programmer’s Guide
to Finding Fluency
and Bypassing the Quirks
Martin Kalin
Modern C Up and Running: A Programmer’s Guide to Finding Fluency and
Bypassing the Quirks
Martin Kalin
Chicago, IL, USA
v
Table of Contents
vi
Table of Contents
Chapter 6: Networking����������������������������������������������������������������������189
6.1. Overview����������������������������������������������������������������������������������������������������189
6.2. A Web Client�����������������������������������������������������������������������������������������������190
6.2.1. Utility Functions for the Web Client���������������������������������������������������196
6.3. An Event-Driven Web Server����������������������������������������������������������������������199
6.3.1. The webserver Program��������������������������������������������������������������������203
6.3.2. Utility Functions for the Web Server��������������������������������������������������204
6.3.3. Testing the Web Server with curl�������������������������������������������������������212
vii
Table of Contents
viii
Table of Contents
8.6. Signals�������������������������������������������������������������������������������������������������������323
8.7. Software Libraries��������������������������������������������������������������������������������������328
8.7.1. The Library Functions������������������������������������������������������������������������330
8.7.2. Library Source Code and Header File������������������������������������������������331
8.7.3. Steps for Building the Libraries���������������������������������������������������������334
8.7.4. A Sample C Client������������������������������������������������������������������������������336
8.7.5. A Sample Python Client���������������������������������������������������������������������341
8.8. What’s Next?����������������������������������������������������������������������������������������������342
Index�������������������������������������������������������������������������������������������������345
ix
About the Author
Martin Kalin has a Ph.D. from Northwestern University and is a professor
in the College of Computing and Digital Media at DePaul University. He
has cowritten a series of books on C and C++ and written a book on Java
web services. He enjoys commercial programming and has codeveloped,
in C, large distributed systems in process scheduling and product
configuration. He can be reached at http://condor.depaul.edu/mkalin.
xi
About the Technical Reviewer
Germán González-Morris is a polyglot Software Architect/Engineer with
20+ years in the field, with knowledge in Java, Spring, C, Julia, Python,
Haskell, and JavaScript, among others. He works for cloud, web distributed
applications. Germán loves math puzzles (including reading Knuth),
swimming, and table tennis. Also, he has reviewed several books including
an application container book (Weblogic) and some books on languages
(Haskell, TypeScript, WebAssembly, Math for coders, regexp, Julia,
Algorithms). For more details, visit his blog (https://devwebcl.blogspot.
com/) or Twitter account: @devwebcl.
xiii
Acknowledgments
My hearty thanks go to the Apress people who made this book possible.
I would like to thank, in particular, the following: Steve Anglin, Associate
Editorial Director of Acquisitions; Jill Balzano, Coordinating Editor; and
James Markham, Development Editor. Thanks as well to the technical
reviewers who made the book better than it otherwise would have been.
xv
Preface
1. Why C?
C is a small but extensible language, with software libraries (standard and
third party) extending the core language. Among high-level languages, C
still sets the mark for performance; hence, C is well suited for applications,
especially ones such as database systems and web servers that must
perform at a high level. The syntax for C is straightforward, but with an
oddity here and there. Anyone who programs in a contemporary high-
level language already knows much of C syntax, as other languages have
borrowed widely from C.
C is also the dominant systems language: modern operating systems
are written mostly in C, with assembly language accounting for the rest.
Other programming languages routinely and transparently use standard
library routines written in C. For example, when an application written
in any other high-level language prints the Hello, world! greeting, it is a
C library function that ultimately writes the message to the screen. The
standard system libraries for input/output, networking, string processing,
mathematics, security, cryptography, data encoding, and so on are
likewise written mainly in C. To write a program in C is to write in the
system’s native language.
xvii
Preface
% gcc -v
C has been a modern language from the start. The familiar function,
which can take arguments and return a value, is the primary code module
in C. C exhibits a separation of concerns by distinguishing between
interfaces, which describe how functions are called, and implementations,
which provide the operational details. As noted, C is naturally and easily
extended through software libraries, whether standard or third party. As
these libraries become better and richer, so does C. C programmers can
create arbitrarily rich data types and data structures and package their
own code modules as reusable libraries. C supports higher-order functions
(functions that can take functions as arguments) without any special, fussy
syntax. This book covers C’s modern features, but always with an eye on
C’s close-to-the-metal features.
To understand C is to understand the underlying architecture of
a modern computing machine, from an embedded device through a
handheld up to a node in a server cluster. C sits atop assembly language,
which is symbolic (human-understandable) machine language. Every
assembly language is specific to a computer architecture. The assembly
language for an Intel device differs from that of an ARM device. Even
within an architectural family such as Intel, changes in the architecture are
xviii
Preface
xix
Preface
xx
Preface
xxi
CHAPTER 1
Program Structure
1.1. Overview
This chapter focuses on how C programs are built out of functions, which
are a construct in just about all modern program languages. The chapter
uses short code segments and full programs to explain topics such
as these:
1.2. The Function
A C program consists of one or more functions, with a function as a
program module that takes zero or more arguments and can return a
value. To declare a function is to describe how the function should be
invoked, whereas to define a function is to implement it by providing
the statements that make up the function’s body. A function’s body
provides the operational details for whatever task the function performs.
A declaration is a function’s interface, whereas a definition is a function’s
implementation. The following is an example of the declaration and
the definition for a very simple function that takes two integer values as
arguments and returns their sum.
int add2(int n1, int n2) { /* definition: the body is enclosed
in braces */
int sum = n1 + n2; /* could avoid this step, here for
clarity */
return sum; /* could just return n1 + n2 */
} /* end of block that implements the
function */
2
Chapter 1 Program Structure
int add2(int n1, int n2) { /* definition: the body is enclosed
in braces */
int sum = n1 + n2; /* could avoid this step, here for
clarity */
return sum; /* could just return n1 + n2 */
} /* end of block that implements the
function */
int main() {
return add2(123, 987); /* ok: add2 is visible to main */
}
3
Chapter 1 Program Structure
In a program, each function must be defined exactly once and with its
own name, which rules out the name overloading (popular in languages
such as Java) in which different functions share a name but differ in
how they are invoked. A function can be declared as often as needed. As
promised, an easy way of handling declared functions is forthcoming.
In the current example, the declaration shows that function add2 takes
two integer (int) arguments and returns an integer value (likewise an
int). The definition of function add2 provides the familiar details, and this
definition could be shortened to a single statement:
return n1 + n2;
4
Chapter 1 Program Structure
#include <stdio.h>
5
Chapter 1 Program Structure
int main() {
int k = -26, m = 44;
int sum = add2(k, m); /* call the add2 function, save the
returned value */
/* %i means: format as an integer */
printf("%i + %i = %i\n", k, m, sum); /* output: -26 +
44 = 18 */
return 0; /* 0 signals normal termination, < 0 signals some
error */
}
The revised add2 example (see Listing 1-3) can be compiled and then
run at the command line as shown in the following, assuming that the file
with the two functions is named add2.c. These commands are issued in
the very directory that holds the source file add2.c. My comments begin
with two ## symbols:
The flag -o stands for output. Were this flag omitted, the executable
would be named a.out (A.exe on Windows) by default. On some systems,
the C compiler may be invoked as cc instead of gcc. If both commands are
available, then cc likely invokes a native compiler—a compiler designed
specifically for that system. On Unix-like systems, this command typically
is a shortcut:
The add2 program begins with an include directive. Here is the line:
#include <stdio.h>
6
Chapter 1 Program Structure
Header files are the natural way to handle function declarations—but not
function definitions. A header file such as stdio.h can be included wherever
needed, and even multiple includes of the same header file, although
inefficient, will work. However, if a header file contains function definitions,
there is a danger. If such a file were included more than once in a program’s
source files, this would break the rule that every function must be defined
exactly once in a program. The sound practice is to use header files for
function declarations, but never for function definitions.
What is the point of having the main function return an int value?
Which function gets the integer value that main returns? When the
user enters
% ./add2
at the command-line prompt and then hits the Return key, a system
function in the exec family (e.g., execv) executes. This exec function then
calls the main function in the add2 program, and main returns 0 to the exec
function to signal normal termination (EXIT_SUCCESS). Were the add2
7
Chapter 1 Program Structure
8
Chapter 1 Program Structure
#include <stdio.h>
int main() {
/* msg is a pointer to a char, the H in Hello, world! */
char* msg = "Hello, world!"; /* the string is implemented
as an array of characters */
printf("%s\n", msg); /* %s formats the argument as
a string */
return 0; /* main must return an int
value */
}
The hi program (see Listing 1-4) has three points of interest for
comparing C and assembly code. First, the program initializes a variable
msg whose data type is char*, which is a pointer to a character. The star
could be flush against the data type, in between char and msg, or flush
against msg:
9
Chapter 1 Program Structure
+---+---+---+---+---+ +---+---+---+
msg--->| H | e | l | l | o |...| l | d | \0| ## \0 is a char
+---+---+---+---+---+ +---+---+---+
In the array to which msg points, the last character \0 is called the
null terminator because its role is to mark where the string ends. As a
nonprinting character, the null terminator is perfect for the job. Of interest
now is how the assembly code represents a string literal.
The second point of interest is the call to the printf function. In this
version of printf, two arguments are passed to the function: the first
argument is a format string, which specifies string (%s) as the formatting
type; the second argument is the pointer variable msg, which points to the
greeting by pointing to the first character H. The third and final point of
interest is the value 0 (EXIT_SUCCESS) that main returns to its caller, some
function in the exec family.
The C code for the hi program can be translated into assembly. In this
example, the following command was used:
10
Chapter 1 Program Structure
file hi.s, which contains the corresponding assembly code. The file hi.s
could be compiled in the usual way:
11
Chapter 1 Program Structure
The essentials of this assembly code example begin with two labels.
The first, .LC0:, locates the string greeting “Hello, world!”. This label thus
12
Chapter 1 Program Structure
serves the same purpose as the pointer variable msg in the C program. The
label main: locates the program’s entry point and, in this way, the callable
code block that makes up the body of the main: routine.
Two other parts of the main: routine deserve a look. The first is the call
to the library routine puts, where the s indicates a string. In C code, the call
would look like this:
movl $0, %eax ## copy 0 into %eax, which holds return value
13
Chapter 1 Program Structure
been translated first into assembly language and then into machine code
(see the sidebar).
There are flags for the gcc utility, as well as separately named utilities (e.g.,
cpp for preprocess only), for carrying out the process only to a particular stage.
The preprocess stage handles directives such as #include, which start with
a sharp sign. The compile stage generates assembly language code, which the
assemble stage then translates into machine code. The link stage connects the
machine code to the appropriate libraries. The command
would compile the code but also save the temporary files: net.i (text, from
preprocess stage), net.s (text, from compile stage), and net.o (binary, from
assemble stage).
14
Chapter 1 Program Structure
## hello program
.data # data versus code section
.globl hello # global scope for label hello
hello: # label == symbolic address
.string "Hello, world!" # a character string
The hiAssem program (see Listing 1-6) prints the traditional greeting,
but using assembly code rather than C. The program can be compiled and
executed in the usual way except for the added flag -static:
15
Chapter 1 Program Structure
4. Call puts.
The function main also can take arguments from the command line:
int main(int argc, char* argv[ ]); /* with two arguments, also
could return void */
16
Chapter 1 Program Structure
% ./hi
17
Chapter 1 Program Structure
#include <stdio.h>
int i;
for (i = 1; i < argc; i++)
puts(argv[i]); /* additional command-line arguments */
return 0; /** 0 is EXIT_SUCCESS **/
}
The cline program (see Listing 1-7) first checks whether there are
at least two command-line arguments—at least one in addition to the
program’s name. If not, the usage section introduced by the if clause
explains how the program should be run. Otherwise, the program uses the
library function puts (put string) to print the program’s name (argv[0])
and the other command-line argument(s). (The for loop used in the
program is clarified in the next section.) Here is a sample run:
% ./cline A 1 B2
18
Chapter 1 Program Structure
./cline
A
1
B2
Later examples put the command-line arguments to use. The point for
now is that even main can have arguments passed to it. Both of the control
structures used in this program, the if test and the for loop, now need
clarification.
1.6. Control Structures
A block is a group of expressions (e.g., integer values to initialize an array)
or statements (e.g., the body of a loop). In either case, a block starts with
the left curly brace { and ends with a matching right curly brace }. Blocks
can be nested to any level, and the body of a function—its definition—is a
block. Within a block of statements, the default flow of control is straight-
line execution.
#include <stdio.h>
int main() {
int n = 27; /** 1 **/
int k = 43; /** 2 **/
printf("%i * %i = %i\n", n, k, n * k); /** 3 **/
return 0; /** 4 **/
}
19
Chapter 1 Program Structure
#include <stdio.h>
int main() {
int n = 111, k = 98;
20
Chapter 1 Program Structure
case 3:
puts("case 3");
break;
default:
puts("none of the above");
} /* end of switch */
}
The tests program (see Listing 1-9) shows three ways in which to test in
a C program. The first way uses the conditional operator in an assignment
statement. The conditional expression has three parts:
21
Chapter 1 Program Structure
parentheses, in this example, (n > k). The same syntax applies to if-tests
and to loop-tests. Parentheses always can be used to enhance readability,
as later examples emphasize, but parentheses are required for test
expressions.
The middle part of the tests program introduces the syntax for if-else
constructs, which can be nested to any level. For instance, the body of an
else clause could itself contain an if else construct. In an if and an else
if clause, the test is enclosed in parentheses. There can be an if without
either an else if or an else, but any else clause must be tied to a prior if
or else if, and every else if must be tied to an if. In this example, the
conditions and results (in this case, puts calls) are on the same line. Here
is a more readable version:
if (n < k)
puts("if");
else if (r > k)
puts("else if"); /** prints **/
else
puts("else");
In this example, the body of the if, the else if, and the else is a
single statement; hence, braces are not needed. The bodies are indented
for readability, but indentation has no impact on flow of control. If a body
has more than one statement, the body must be enclosed in braces:
22
Chapter 1 Program Structure
while (<condition>) {
/* body */
}
If the condition is true (nonzero), the body executes, after which the
condition is tested again. If the condition is false (zero), control jumps to
the first statement beyond the loop’s body. (If the loop’s body consists of
23
Chapter 1 Program Structure
do {
/* body */
} while (<condition>);
The break statement in loop2 breaks out of this loop only, and control
resumes within loop1. C does have goto statement whose target is a label,
but this control construct should be mentioned just once and avoided
thereafter.
#include <stdio.h>
int main() {
int n = -1;
while (1) { /* 1 == true */
printf("A non-negative integer, please: ");
scanf("%i", &n);
24
Chapter 1 Program Structure
The whiling program (see Listing 1-10) prompts the user for a
nonnegative integer and then prints its value. The program does not
otherwise validate the input but rather assumes that only decimal
numerals and, perhaps, the minus sign are entered. The focus is on
contrasting a while and a do while for the same task.
The condition for the while loop is 1, the default value for true:
25
Chapter 1 Program Structure
Among the looping constructs, the for loop has the most complicated
syntax. Its general form is
for (<init>;<condition>;<post-body>) {
/* body */
}
A common example is
The init section executes exactly once, before anything else. Then
the condition is evaluated: if true, the loop’s body is executed; otherwise,
control goes to the first statement beyond the loop’s body. The post-body
expression is evaluated per iteration after the body executes; then the
condition is evaluated again; and so on. Any part of the for loop can be
empty. The construct
26
Chapter 1 Program Structure
the function returns after executing the last statement in the block that
makes up the function’s body.
The normal return-to-caller behavior takes advantage of how modern
systems provide scratchpad for called functions. This scratchpad is a
mix of general-purpose CPU registers and stack storage. As functions are
called, the call frames on the stack are allocated automatically; as functions
return, these call frames can be freed up for future use. The underlying
system bookkeeping is simple, and the mechanism itself is efficient in that
registers and stack call frames are reused across consecutive function calls.
#include <stdio.h>
#include <stdlib.h> /* rand() */
int g() {
return rand() % 100; /* % is modulus; hence, a number 0
through 99 */
}
int main() {
int n = 72;
int r = f(n);
printf("Calling f with %i resulted in %i.\n", n, r);
/* 5976 on sample run */
return r; /* not usual, but all that's required is a
returned int */
}
27
Chapter 1 Program Structure
The calling program (see Example 1-1) illustrates the basics of normal
return-to-caller behavior. When the calling program is launched from the
command line, recall that a system function in the exec family invokes the
calling program’s main function. In this example, main then calls function
f with an int argument, which function f uses a multiplier. The number
to be multiplied comes from function g, which f calls. Function g, in
turn, invokes the library function rand, which returns a pseudorandomly
generated integer value. Here is a summary of the calls and returns, which
seem so natural in modern programming languages:
calls calls calls calls
exec-function------->main()------->f(int)------->g()------->rand()
exec-function<-------main()<-------f(int)<-------g()<-------rand()
returns returns returns returns
28
Chapter 1 Program Structure
The first argument is the format string, and the optional remaining
arguments—represented by the ellipsis—are the values to be formatted.
The printf function requires the first argument, but the number of
additional arguments depends on the number of values to be formatted.
There are many other library functions that take a variable number of
arguments, and programmer-defined functions can do the same. Two
examples illustrate.
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main() {
/* 0755: owner has read/write/execute permissions, others
read/execute permissions */
int perms = 0755; /* 0 indicates base-8, octal */
int status = syscall(SYS_chmod, "/usr/local/website", perms);
if (-1 == status) perror(NULL);
return 0;
}
The sysCall program (see Example 1-2) invokes the library function
syscall, which takes a variable number of arguments; the first argument,
in this case the symbolic constant SYS_chmod, is required. SYS_chmod is
clarified shortly.
The syscall function is an indirect way to make system calls, that is, to
invoke functions that execute in kernel space, the address space reserved
for those privileged operating system routines that manage shared
system resources: processors, memory, and input/output devices. This
indirect approach allows for fine-tuning that the direct approach might
not provide. This example is contrived in that the function chmod (change
29
Chapter 1 Program Structure
mode) could be called directly with the same effect. The mode refers to
various permissions (e.g., read and write permissions) on the target, in this
case a directory on the local file system.
As noted, the first argument to syscall is required. The argument is
an integer value that identifies the system function to call. In this case, the
argument is SYS_chmod, which is defined as 90 in the header file syscall.h
and identifies the system function chmod. The variable arguments to
function syscall are as follows:
The header file stdarg.h has a data type va_list (list of variable
arguments) together with utilities to help programmers write functions
with a variable number of arguments. These utilities allocate and
deallocate storage for the variable arguments, support iteration over
these arguments, and convert each argument to whatever data type is
appropriate. The utilities are well designed and worth using. As a popular
illustration of a function with a variable number of arguments, the next
code example sums up and then averages the arguments. In the example,
the required argument and the others happen to be of the same data
type, in the current case int, but this is not a requirement. Recall again
the printf function, whose first argument is a string but whose optional,
variable arguments all could be of different types.
#include <stdio.h>
30
Chapter 1 Program Structure
double avg(int count, ...) { /* count is how many, ellipses are
the other args */
double sum = 0.0;
va_list args;
va_start(args, count); /* allocate storage for the
additional args */
int i;
for (i = 0; i < count; i++) sum += va_arg(args, int);
/* compute the running sum */
va_end(args); /* deallocate the storage for
the list */
void main() {
printf("%f\n", avg(4, 1, 2, 3, 4));
printf("%f\n", avg(9, 9, 8, 7, 6, 5, 4, 3, 2, 1));
printf("%f\n", avg(0));
}
The varArgs program (see Example 1-3) defines a function avg with
one named argument count and then an ellipsis that represents the
variable number of other arguments. In this example, the int parameter
count is a placeholder for the required argument, which specifies how
many other arguments there are. In the first call from main to the function
avg, the first 4 in the list become count, and the remaining four values
make up the variable arguments.
31
Chapter 1 Program Structure
1.9. What’s Next?
C has basic or primitive data types such as char (8 bits), int (typically 32
bits), float (typically 32 bits), and double (typically 64 bits) together with
mechanisms to create arbitrarily rich, programmer-defined types such
as Employee and digital_certificate. Names for the primitive types
are in lowercase. Data type names, like identifiers in general, start with a
letter or an underscore, and the names can contain any mix of uppercase
and lowercase characters together with decimal numerals. Most modern
languages have naming conventions similar to those in C. The basic types
in C deliberately match the ones on the underlying system, which is one
way that C serves as a portable assembly language. The next chapter
focuses on data types, built-in and programmer-defined.
32
CHAPTER 2
The sizeof operator gives the size in bytes for any data type or value of
that type. Here is a code segment to illustrate:
#include <stdio.h>
#include <wchar.h> /* wchar_t type */
void main() {
printf("char size: %lu\n", sizeof(char));
/* 1 (long unsigned) */
printf("wchar_t size: %lu\n", sizeof(wchar_t)); /* 4 */
34
Chapter 2 Basic Data Types
The dataTypes (see Listing 2-1) program prints the byte sizes for the
basic C data types. These sizes are the usual ones on modern devices.
The following sections focus on C’s built-in data types and built-in
operations on these types. Technical matters such as the 2’s complement
representation of integers and the IEEE 754 standard for floating-point
formats is covered in detail.
2.2. Integer Types
All of C’s integer types come in signed and unsigned flavors. The unsigned
types have a one-field implementation: all of the bits are magnitude bits.
By contrast, signed types have a two-field implementation:
35
Chapter 2 Basic Data Types
Table 2-1 lists the basic integer types in C, which have the very bit sizes
as their machine-level counterparts. C also has a long long type, which
must be at least 8 bytes in size and typically is the same size as long: 8.
C does not have a distinct boolean type but instead uses integer values
to represent true and false: 0 represents false, and any nonzero value (e.g.,
-999 and 403) represents true. The default value for true is 1. For example, a
potentially infinite loop might start out like this:
36
Chapter 2 Basic Data Types
37
Chapter 2 Basic Data Types
38
Chapter 2 Basic Data Types
39
Chapter 2 Basic Data Types
An earlier example showed that C’s signed char and unsigned char
are likewise integer types. As the name char indicates, the char type is
designed to store single-byte character codes (e.g., ASCII and Latin-1);
the more recent wchar_t type also is an integer type, but one designed for
multibyte character codes (e.g., Unicode). For historical reasons, the char
type is shorthand for either signed char or unsigned char, but which
is platform dependent. For the remaining types, this is not the case. For
example, short is definitely an abbreviation for signed short.
40
Chapter 2 Basic Data Types
The same recipe yields -1 from 1: invert the bits in 1 (yielding 11111…0)
and then add 1 (yielding 11111…1), which again is all 1s in binary and -1 in
decimal.
In the case of INT_MIN, the peculiarity becomes obvious:
In C, the unary minus operator is shorthand for (a) inverting the bits
and (b) adding 1. This code segment illustrates
int n = 7;
int k = -n; /* unary-minus operator */
int m = ~n + 1; /* complement operator and addition by 1 */
The value of k and of m is the same: -7. In the case of INT_MIN, however,
the peculiarity is that
INT_MIN == -INT_MIN
2.2.2. Integer Overflow
A programmer who uses any of the primitive C types needs to stay alert
when it comes to sizeof and the potential for overflow. The next code
example illustrates with the int type.
41
Chapter 2 Basic Data Types
#include <stdio.h>
#include <limits.h> /* INT_MAX */
int main() {
printf("Max int in %lu bytes %i.\n", sizeof(int), INT_MAX);
/* 4 bytes 2,147,483,647 */
int n = 81;
while (n > 0) {
printf("%12i %12x\n", n, n);
n *= n; /* n = n * n */
}
printf("%12i %12x\n", n, n); /* -501334399 e21e3e81 */
return 0;
}
/* 81 51
6561 19a1
43046721 290d741
-501334399 e21e3e81 ## e is 1101 in binary */
42
Chapter 2 Basic Data Types
2.3. Floating-Point Types
C has the floating-point types appropriate in a modern, general-purpose
language. Computers as a rule implement the IEEE 754 specification
(https://standards.ieee.org/ieee/754/6210/) in their floating-point
hardware, so C implementations follow this specification as well.
Table 2-2 lists C’s basic floating-point types. Floating-point types are
signed only, and their values have a three-field representation under IEEE
754: sign bits, exponent bits, and significand (magnitude) bits. A floating-
point constant such as 3.1 is of type double in C, whereas 3.1F and 3.1f
are of type float. Recall that a double is 8 bytes in size, but a float is only
4 bytes in size.
43
Chapter 2 Basic Data Types
2.3.1. Floating-Point Challenges
Floating-point types pose challenges that make these types unsuitable for
certain applications. For instance, there are decimal values such as 0.1 that
have no exact binary representation, as this short code segment shows:
float n = 0.1f;
printf("%.24f\n", n); /* 0.100000001490116119384766 */
WHAT’S A MACRO?
Although macros are often named in uppercase, this is convention only. Here
are two parameterized macros for computing the max and min of two integer
arguments:
44
Chapter 2 Basic Data Types
#define max(x, y) (x) ^ ((x ^ y) & -(x < y)) /* ^ bitwise xor,
& bitwise and */
These macros look like functions, but the compiler does no type-checking on
the arguments. Here are two sample uses:
A look at the hex values for the two expressions confirms that they are
not equal:
The two differ in the least significant digit: hex a is 1010 in binary,
whereas hex b is 1011 in binary. The two values differ ever so slightly, in the
least significant (rightmost) bit of their binary representations. In close-to-
the-metal C, the equality operator compares bits; at the bit level, the two
expressions differ.
45
Chapter 2 Basic Data Types
The comp code segment (see Listing 2-4) shows how a comparison
can be made using FLT_EPSILON. The library function fabs returns the
absolute value of the difference between f1 and f2. This value is less than
FLT_EPSILON; hence, the two values might be considered equal because
their difference is less than FLT_EPSILON.
The next two examples reinforce the risks that come with floating-
point types. The goal is to show various familiar programming contexts
in which floating-point issues arise. Following each example is a short
discussion.
46
Chapter 2 Basic Data Types
/* 1.010000
2.020000
...
7.070001 ;; rounding up now evident
...
10.100001
*/
float incr = 1.01f;
float num = incr;
int i = 0;
while (i++ < 10) { /* i++ is the post-increment operator */
printf("%12f\n", num); /* %12f is field width, not
precision */
num += incr;
}
47
Chapter 2 Basic Data Types
int i = 1;
printf("%i\n", i++); /* 1 (evaluate, then increment) */
printf("%i\n", i); /* 2 (i has been incremented above) */
printf("%i\n", ++i); /* 3 (increment, then evaluate) */
#include <stdio.h>
#include <math.h> /* pi and e as macros, M_PI and M_E,
respectively */
void main() {
printf("%0.50f\n", 10.12);
/* 10.11999999999999921840299066388979554176330566406250 */
48
Chapter 2 Basic Data Types
/* e and pi */
double ans = pow(M_E, M_PI) - M_PI; /* e and pi, respectively */
printf("%lf\n", ans); /* 19.999100 prints: expected is
19.99909997 */
}
The d2bconvert program (see Listing 2-6) shows yet again how
information may be lost in converting from decimal to binary. In these
isolated examples, of course, no harm is done; but these cases underscore
that floating-point types such as float and double are not suited for
applications involving, for instance, currency.
+-+--------+-----------------------+
|s|exponent| magnitude | 32 bits
+-+--------+-----------------------+
1 8 23
49
Chapter 2 Basic Data Types
50
Chapter 2 Basic Data Types
The middle field alone, the 8-bit exponent, indicates that this value is
indeed normalized: the written exponent contains a mix of 0s and 1s.
Denormalized values cover two representations of zero and evenly
spaced values in the vicinity of zero. Zero can represented as either a
negative or a nonnegative value under the IEEE specification, which the C
compiler honors:
51
Chapter 2 Basic Data Types
The value in the middle row has all 0s in the exponent, which makes
the value denormalized. This value is the largest denormalized value in
32 bits, but this value is still smaller than the very small normalized value
above it. The smallest denormalized value, the bottom row in the table,
has a single 1 as the least significant bit: all the rest are 0s. Between the
smallest and the largest denormalized values are many more, all differing
in the bit pattern of the written magnitude. Although the denormalized
values shown so far are positive, there are negative ones as well: the sign
bit is 1 for such values.
In summary, denormalized values cover the two representations
of zero, as well as evenly spaced values that are close to zero. The
preceding examples show that the gap between the smallest positive
normalized value and positive zero is considerable and filled with
denormalized values.
The third IEEE category covers special values, three in particular:
NaN (Not a Number), positive infinity, and negative infinity. A written
exponent of all 1s signals a special value. If the written magnitude contains
all 0s, then the value is either negative or positive infinity, with the sign bit
determining the difference. If the written magnitude contains at least one
1, the value is NaN. A short code segment clarifies.
52
Chapter 2 Basic Data Types
#include <stdio.h>
#include <math.h>
The specVal program (see Listing 2-7) has the following output, with
comments introduced by ##:
53
Chapter 2 Basic Data Types
Compiling the specVal program into an executable requires an explicit link flag:
In the flag -lm (lowercase L followed by m), the -l stands for link, and the m
identifies the standard mathematics library libm, which resides in a file such
as libm.so on the compiler/linker search path (e.g., in a directory such as /usr/
lib or /usr/local/lib). Note that the prefix lib and the file extension so fall away
in a link specification, leaving only the m for the mathematics library.
The linking is needed because the specVal program calls the sqrt function
from the mathematics library. A compilation command may contain several
explicit link flags in same style shown previously: -l followed by the name of
the library without the prefix lib and without the library extension such as so.
During compilation, libraries such as the standard C library and the input/
output library are linked in automatically. Other libraries, such as the
mathematics and cryptography libraries, must be linked in explicitly. In
Chapter 8, the section on building libraries goes into more detail on linking.
54
Chapter 2 Basic Data Types
Recall the layout for a 32-bit floating-point value under IEEE 754:
+-+--------+-----------------------+
|s|exponent| magnitude |
+-+--------+-----------------------+
1 8 23
float f = 123.456f;
f = (int) f << 2; /* ERROR without the cast operation (int) */
printf("%f\n", f); /* 492.000000 */
55
Chapter 2 Basic Data Types
A cast is not an assignment: in the second example shown previously, the cast
(float) does not change what is stored in n but rather creates a new value
then assigned to variable f. A cast is thus an explicit conversion of one type to
another. The compiler regularly does such conversions automatically:
2.4.1. Arithmetic Operators
C has the usual unary and binary arithmetic operators, and C uses the
standard symbols to represent these operators. For operations such as
exponentiation and square roots, C relies upon library routines, in this
case the pow and sqrt functions, respectively. Table 2-4 clarifies the binary
arithmetic operators with sample expressions.
56
Chapter 2 Basic Data Types
Addition + 12 + 3
Subtraction - 12 - 3
Multiplication * 12 * 3
Division / 12 / 3
Modulus % 12 % 3
The plus and minus signs also designate the unary plus and unary
minus operators, respectively:
int k = 5;
printf("%i %i\n", +k, -k); /* 5 -5 */
8 + 2 * 3
#include <stdio.h>
void main() {
int n1 = 4, n2 = 11, n3 = 7;
printf("%i\n", n1 + n2 * n3); /* 81 */
printf("%i\n", (n1 + n2) * n3); /* 105 */
57
Chapter 2 Basic Data Types
printf("%i\n", n3 * n2 % n1); /* 1 */
printf("%i\n", n3 * (n2 % n1)); /* 21 */
}
The assoc program (see Listing 2-8) shows how expressions can
be parenthesized in order to get the desired association when mixed
operations are in play. The use of parentheses seems easier than trying to
recall precedence details, and parenthesized expressions are, in any case,
easier to read.
C has variants of the assignment operator (=) that mix in arithmetic
and bitwise operators. A few examples should clarify the syntax:
int n = 3;
n += 4; /* n = n + 4 */
n /= 2; /* n = n / 2 */
n <<= 1; /* n = n << 1 */
2.4.2. Boolean Operators
The boolean or relational operators are so named because the expressions
in which they occur evaluate to the boolean values true or false. Although
any integer value other than zero is true in C, true boolean expressions in C
evaluate to the default value for true, 1. Here are some sample expressions
to illustrate the boolean operators:
58
Chapter 2 Basic Data Types
A few cautionary notes are in order. Note that the operators for equality
(==) and inequality (!=) both have two symbols in them. The equality
operator can be tricky because it is so close to the assignment operator (=).
Consider this code segment, the stuff of legend among C programmers
whose code has gone awry because of some variation of the problem:
int n = 2;
if (n = 1)
printf("yep\n"); /** prints: presumably meant n == 1 **/
The logical and and logical or operators are efficient because they
short-circuit. For example, in the expression
(3 < 2) && (4 > 2) /* only (3 < 2), the 1st conjunct, is
evaluated */
59
Chapter 2 Basic Data Types
int i = 0;
while (i < 10) { /* loop while i is less than 10 */
/* ... */
i += 1; /* increment loop counter: i++ or ++i would
work, too */
}
2.4.3. Bitwise Operators
As the name suggests, the bitwise operators work on the underlying
bit-string representation of data. These operators thus deserve caution,
as it may be hard to visualize the outcome of bit manipulation. Bitwise
operations are fast, usually requiring but a single clock tick to execute.
For example, an optimizing compiler might transform a source-code
expression such as
60
Chapter 2 Basic Data Types
int n = 5;
if (-n == (~n + 1))
printf("yep\n"); /* prints */
61
Chapter 2 Basic Data Types
Right shifts can be even trickier. Consider the signed integer value
0xffffffff in hex, which is all 1s in binary; in decimal, this is -1. Even in a
1-bit right shift, the sign could change to 0—if the shift is logical, that is,
if the vacated bit is filled with a 0. If the shift is sign preserving, it is an
arithmetic shift: the sign bit becomes the filler for the vacated positions.
Whether a right is logical or arithmetic is platform dependent. In general,
it is best to shift only unsigned integer values. Even in this case, of course,
overshifting is possible; but at least the issue of sign preservation does
not arise.
The endian code segment (see Listing 2-9) uses bitwise operators in a
utility function that reverses the endian-ness of a 4-byte integer. Modern
machines are still byte addressable in that an address is that of a single
byte. For multibyte entities such as a 4-byte integer, an address thus points
to a byte at one end or the other in the sequence of 4 bytes. Given this 4-
byte integer
62
Chapter 2 Basic Data Types
+----+----+----+----+
| B1 | B2 | B3 | B4 | ## B1 is high-order byte, B4 is low-order byte
+----+----+----+----+
+----+----+----+----+
| B4 | B3 | B2 | B1 | ## B4 is high-order byte, B1 is low-order byte
+----+----+----+----+
unsigned n = 0x1234abcd;
printf("%x %x\n", n, endian_reverse(n)); /*
1234abcd cdab3412 */
Recall that each hex digit is 4 bits. Accordingly, the leftmost byte in
variable n is 12, and the rightmost is cd.
C has a header file endian.h that declares various functions for
transforming little-endian formats to big-endian formats, and vice versa.
These functions specify the bit sizes on which they work: 16 (2 bytes), 32 (4
bytes), and 64 (8 bytes).
63
Chapter 2 Basic Data Types
An rvalue is one that does not persist. For example, in the statement
int n = 444;
/* 444 persists in n beyond the
assignment */
The variable n is the symbolic name of a memory location or CPU register, and
a value assigned to n is thus an lvalue.
2.5. What’s Next?
The examples so far have focused mostly on scalar variables: there is
an identifier for a single variable, not a collection of variables. A typical
example is
64
Chapter 2 Basic Data Types
The pointer variable str identifies a collection (in this case, an array)
of five characters: the ones shown and the null terminator. The expression
str[0] refers to the first of the variables that hold a character, lowercase a
in this example. Pointer str thus identifies an aggregate rather than just a
single variable.
Arrays and structures are the primary aggregates in C. Pointers also
deserve a closer look because they dominate in efficient, production-grade
programming. The next chapter focuses on aggregates and pointers.
65
CHAPTER 3
Aggregates and
Pointers
3.1. Overview
This chapter focuses on arrays and structures, which are C’s primary
aggregate types. Arrays aggregate variables of the same type, whereas
structures can do the same for variables of different types. Structures can
be array elements, and a structure may embed arrays. Together these
aggregate types make it possible for programmers to define arbitrarily rich
data types (e.g., Employee, Game, Species) that meet application needs.
Pointers—address constants and variables—come into play naturally
with both arrays and structures, and the code examples throughout the
chapter get into the details. Among modern general-purpose languages, C
(together with C++) stands out by giving the programmer so much control
over—and, therefore, responsibility for—memory addresses and the items
stored at these addresses. All of the chapters after this one have examples
that, in one way or another, illustrate the power of pointers.
3.2. Arrays
An array in C is a fixed-size collection of variables—of the same type—
accessible under a single name, the array’s identifier. A code example
illustrates.
#define Size 8
void main() {
int arr[Size]; /* storage from the stack --
uninitialized */
int i;
for (i = 0; i < Size; i++) /* iterate over the array */
arr[i] = i + 1; /* assign a value to each
element */
}
The array program (see Listing 3-1) shows the basic syntax for declaring
an array and then uses a for loop to populate the array with values. An array
has a fixed size, in this case specified by the macro Size. The array’s name,
in this case arr, is a pointer constant that holds the address of the array’s first
element, in this case the element that the for loop initializes to 1:
+---+---+---+---+---+---+---+---+
arr--->| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## array elements
+---+---+---+---+---+---+---+---+
[0] [1] [2] [3] [4] [5] [6] [7] ## indexes
68
Chapter 3 Aggregates and Pointers
No. The programmer is responsible for ensuring that array indexes are in
bounds at runtime. The following code segment compiles without warning and
likely blows up when executed because of the out-of-bounds index -9876.
69
Chapter 3 Aggregates and Pointers
70
Chapter 3 Aggregates and Pointers
The arrayPtr program (see Listing 3-2), which revises the original array
program, has three pointers at work:
The following is a depiction of where ptr and end point before the
looping begins:
+---+---+---+---+---+---+---+---+---+
ptr--->| ? | ? | ? | ? | ? | ? | ? | ? | ? |<---end
+---+---+---+---+---+---+---+---+---+
[0] [1] [2] [3] [4] [5] [6] [7] ## indexes
A pointer is allowed to point one element beyond the end of the array,
although nothing should be stored at that location. In this example, the
array’s initialization now uses a while rather than a for loop, and the loop’s
condition compares the two pointers, ptr and end: looping continues so
long as ptr < end. At the bottom of the loop, ptr is incremented by 1—by
one int, which is 4 bytes. The pointer ptr is a variable, unlike the pointer
constant arr, and so can have its value changed. Eventually ptr points to
the same location as does end, which makes the loop condition false.
The initialization of each array element uses the dereference operator,
the star:
71
Chapter 3 Aggregates and Pointers
In the declaration of ptr, the star comes after the data type int. In the
dereferencing of ptr, the star comes before the variable’s name. It would be
an error to change the code to
because ptr then would take on values such as 1,2,3,…,8, which almost
surely are not addresses within the program’s address space. The aim is to
initialize the array element to which ptr points, not ptr itself.
3.4. More on the Address
and Dereference Operators
In the addPtr example, the pointer variable ptr is initialized to the array’s
name arr so that both arr and ptr point to the array’s first element. An
equivalent but less concise initialization uses the address operator &:
The examples so far have shown pointers that hold the addresses of
char and int cells, but not pointers to other pointers. In principle, there
can be pointer to pointer to…, although in practice, it is unusual to see
more than two levels of indirection. The next example illustrates the case
of a pointer to a pointer, and later examples motivate such a construct.
72
Chapter 3 Aggregates and Pointers
#include <stdio.h>
void main() {
int n = 1234;
int* ptr1 = &n; /* ptr1--->n */
The ptr2ptr program (see Listing 3-3) has an int variable n that stores
1234, a pointer ptr1 that points to n, and a second pointer ptr2 that points
to ptr1. Here is a depiction, with fictional addresses written in hex above
the storage cells and variable names below these cells:
0xAB 0xEF ## addresses
+------+ +------+ +------+
| 0xAB |--->| OxEF |--->| 1234 | ## contents
+------+ +------+ +------+
ptr2 ptr1 n ## variable names
Given this storage layout, any of the variables n, ptr1, and ptr2 can
be used to access (including to update) the value stored in variable n. For
example, each of these statements updates n by one:
73
Chapter 3 Aggregates and Pointers
The index syntax used with arrays can be seen as syntactic sugar, as a
short example shows:
In the printf statement, the usual syntax for array access would be
arr[i], not i[arr]. Yet either works, and the compiler does not wince at
the second form. The reason can summarized as follows:
74
Chapter 3 Aggregates and Pointers
3.5. Multidimensional Arrays
An array declared with a single pair of square brackets is one-dimensional
and sometimes called a vector. An array declared with more than one pair
of square brackets is multidimensional:
Arrays of any dimension are possible, but more than three dimensions
is unusual. The array nums_table is two-dimensional. The arrays nums
and nums_table hold the same number of integer values (128), but they
do not have the same number of elements: array nums has 128 elements,
each an int value; by contrast, array nums_table has four elements, each a
subarray of 32 int values. The sizeof operator, when applied to an array’s
name, does the sensible thing: it gives the number of bytes required for all
of the array elements, not the size in bytes of the array’s name as pointer.
In this case, for example, the sizeof array nums is the same as the sizeof
array nums_table: 512 because there are 128 int values in each array and
each int is 4 bytes.
Multidimensional arrays are yet another example of syntactic sugar
in C. All arrays are implemented as one-dimensional, as the next code
example illustrates.
#include <stdio.h>
void main() {
int table[3][4] = {{1, 2, 3, 4}, /* row 1 */
{9, 8, 7, 6}, /* row 2 */
{3, 5, 7, 9}}; /* row 3 */
75
Chapter 3 Aggregates and Pointers
int i, j;
for (i = 0; i < 3; i++) /** outer loop: 3 rows **/
for (j = 0; j < 4; j++) /** inner loop: 4 cols per row **/
printf("%i ", table[i][j]);
printf("\n");
The table program (see Listing 3-4) highlights critical features about
how pointers work in C. The array name table is, as usual, a pointer
constant, and this name points to the first byte of the first int in the first
element in the array, where the first array element is a subarray of four int
values, in this case 1, 2, 3, and 4:
76
Chapter 3 Aggregates and Pointers
columns, then the second row with all of its columns, and so on. A Fortran
compiler, by contrast, would lay out a multidimensional array in column-
major order.
The second traversal of array table uses only a single for loop. The
variable ptr is assigned the value of table, but with a cast: the cast (int*)
is required because ptr is of type int*, whereas table is not. A revision to
the table example goes into the details.
To get a better sense of the table data type, imagine breaking out a
print function for printing the two-dimensional table (see Listing 3-5).
The first parameter in the print function could be written in different
ways, including the one shown. Another way is this:
77
Chapter 3 Aggregates and Pointers
The parameter arr in function print then points to the first row, and
second parameter n gives the number of rows. The cast of table to int* in
the assignment
The first and second addresses differ by 16 bytes, as do the third and
fourth. The variable table[0] points to the first of the three rows in the
table, and each row has four int values of 4 bytes apiece; hence, table[1]
points 16 bytes beyond where table[0] points.
The syntax of multidimensional arrays gives a hint about how various
pointer expressions are to be used. The table array, which holds int
values, is declared with two sets of square brackets:
78
Chapter 3 Aggregates and Pointers
If index syntax is used to read or write an int value, then two square
brackets must be used:
The first index picks out the row, and the second index picks out the
column in the row. Any expressions involving table, but with fewer than
two pairs of brackets, are pointers rather than int values. In particular,
table points to the first subarray, as does table[0]; pointer table[1]
points to the second subarray; and pointer table[2] points to the third
subarray. A quick review exercise is to explain, in plain terms or through a
code segment, the difference between the data type of table and the data
type of table[0]. Both pointer expressions point to the same byte, but the
two differ in type.
C arrays promote efficient modular programming. Consider again a
function to print one-dimensional integer arrays of arbitrary sizes. As the
table program shows, it is straightforward to treat an n-dimensional array
as if it were one-dimensional. The print_array function might be declared
as follows:
The obvious way to call print_array is to pass it, as the first argument,
the array’s name—a pointer:
int arr[100000];
/* fill the array */
print_array(arr, 100000); /* passing a pointer as the
1st arg */
79
Chapter 3 Aggregates and Pointers
80
Chapter 3 Aggregates and Pointers
#include <stdio.h>
#include <limits.h>
• The string
81
Chapter 3 Aggregates and Pointers
When the program executes (see the main function in the following),
the output is
82
Chapter 3 Aggregates and Pointers
/* no overflow */
int flag = safe_mult(16, 48, &n);
msg = (!flag) ? "Overflow on 16 * 48" : "No overflow on
16 * 48";
printf("%s: returned product == %i\n", msg, n);
83
Chapter 3 Aggregates and Pointers
/* overflow */
flag = safe_mult(INT_MAX, INT_MAX, &n);
msg = (!flag) ? "Overflow on INT_MAX * INT_MAX" : "No
overflow on INT_MAX * INT_MAX";
printf("%s: returned product == %i\n", msg, n);
}
The main function for the safeMult program (see Listing 3-7) makes
two calls against the function. The first, with 16 and 48 as the values to
be multiplied, does not cause overflow. The second call, however, passes
INT_MAX as both arguments, with overflow as the expected and, because of
safe_mult, the now detected overflow.
This definition of main suggests that the function returns a void in the
same way that another version of main returns an int; but the suggestion
is misleading. The void is really shorthand for returns no value and so is
not a data type in the technical sense. For instance, a variable cannot be
declared with void as the type:
84
Chapter 3 Aggregates and Pointers
There is a very important data type in C that has void in its name:
void*, or pointer to void. This type is a generic pointer type: any other
pointer type can be converted to and from void* without explicit casting.
Why is this useful? A short example provides one answer, and the next
section provides another.
Consider this array of strings:
The array happens to hold six strings, each of which is a char* in C. For
example, the first array element is a pointer to the “e” in “eins”. To write
a loop that traverses this array without going beyond the end requires a
count of how many elements are in the array; in this case, there are six.
There is a better, more robust, and more programmer-friendly way to
build an array of strings:
85
Chapter 3 Aggregates and Pointers
int i = 0;
while (strings[i]) /* short for: while
(strings[i] != NULL) */
printf("%s\n", strings[i++]); /* print current string, then
increment i */
+---+---+---+---+--+
| e | i | n | s |\0| ## \0 is 8-bit zero
----+---+---+---+--+
By contrast, the NULL that terminates the strings array is either a 32-bit
zero or a 64-bit zero, depending on whether the machine uses 32-bit or 64-
bit addresses. To be sure, the comparison
evaluates to true, but only because the compiler converts the 8-bit null
terminator (zero) to the 32-bit or 64-bit zero.
In summary, zero has three specific uses in C beyond 0 as a
numeric value:
86
Chapter 3 Aggregates and Pointers
• In a string context, the 8-bit zero (\0) is the code for the
nonprinting character that marks the end of a string:
the null terminator.
while (strings[i])
87
Chapter 3 Aggregates and Pointers
A full sorting example fleshes out the details of the remaining three
arguments.
#include <stdio.h>
#include <stdlib.h> /* rand, qsort */
#define Size 12
void main() {
int arr[Size], i;
for (i = 0; i < Size; i++) arr[i] = rand() % 100; /* values
< 100 */
print_array(arr, Size); /* 83 86 77 15 93 35 84 92 49 21
62 27 */
88
Chapter 3 Aggregates and Pointers
89
Chapter 3 Aggregates and Pointers
90
Chapter 3 Aggregates and Pointers
n2 - n1
is then 79, a positive value signaling that 99 should precede 20 in the
sorted order. The sort is thus in descending order. If the returned value
were changed to
n1 - n2
then the resulting sort would be in ascending order. If the int array
had the same values throughout, then 0 would be returned for every
comparison, leaving the array unchanged by the sort.
The usefulness of void* is undoubtedly evident to programmers from
object-oriented languages such as Java and C#. In these languages, a
reference (pointer) to Object can point to anything. Here is a segment of
Java to illustrate:
91
Chapter 3 Aggregates and Pointers
Pointers to functions, like other C pointers, have data types, and the
typedef is useful in defining the appropriate type, a type that will satisfy
the compiler. It is easy to get a pointer to a function; the function’s name is
just such a pointer. It can be challenging to pass an appropriate function
pointer as an argument in another function.
WHAT’S AN ENUM?
92
Chapter 3 Aggregates and Pointers
The enumerated values start at 0 and continue in series unless explicit values
are given, as in the second example shown previously. In the second example,
false would default to 2 if not explicitly assigned 0 as its value.
Constructs such as typedef and enum promote readable code:
93
Chapter 3 Aggregates and Pointers
The reducer program (see Listing 3-9) has two functions, sum and
product, that reduce a list of integers to a single value, in this case a sum
and product, respectively. The third function is higher order and named
reduce. This function takes a (pointer to a) function as its first argument,
an array of values as its second, and the array’s length as its third.
The typedef in the reducer program is the tricky part:
The data type alias is reducer, and it can point to any function that
meets these conditions:
The declaration of the reduce function uses the typedef data type in
the first argument position:
94
Chapter 3 Aggregates and Pointers
#include <stdio.h>
#define Size 30
int main() {
unsigned nums[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30};
The main function in the reducer program (see Listing 3-10) shows two
calls to the reduce function: the first using sum as its first argument and the
second using product as this argument.
The reducer program illustrates that higher-order functions are
routine in C. Such functions, used judiciously, make programs easier
to understand. The reduce function maps a list of integers to a single
value, and the first argument—the function pointer—specifies the kind
of mapping involved, in this case reducing the list to either a sum or a
product.
95
Chapter 3 Aggregates and Pointers
3.8. Structures
Arrays aggregate variables of the same data type, whereas structures can
aggregate variables of different types. The variables in a structure are
known as its fields. There can be arrays of structures, and structures that
embed arrays and even other structures. As a result, programmer-defined
data structures can be arbitrarily rich.
The syntax of structures can be introduced in short code examples.
Here’s a start:
struct {
int n;
double k;
} s1;
s1.n = -999;
s1.k = 44.4;
96
Chapter 3 Aggregates and Pointers
struct TwoNums s2; /* the data type is struct TwoNums: struct
plus the tag */
#include <stdio.h>
#define Size 100000
97
Chapter 3 Aggregates and Pointers
void main() {
BigNumsStruct bns;
bns.n = -9876;
bad(bns); /** CAUTION **/
good(&bns); /* right approach: pass an address */
}
(*ptr).n
98
Chapter 3 Aggregates and Pointers
ptr->n
struct {
int n; /* sizeof(int) == 4 */
char c; /* sizeof(char) == 1 */
double d; /* sizeof(double) == 8 */
} test;
99
Chapter 3 Aggregates and Pointers
+--------+--------+--------+
| 0x0004 | 0x1f44 | 0x3e84 |...## index array for Employee array
+--------+--------+--------+
In this depiction, the elements in the top or data array are Employee
instances, whereas the elements in the bottom or index array are
Employee* pointers. In short, each index element points to an Employee
element. The addresses in the index array are 8KB (kilobytes) apart
because sizeof(Employee) is 8,000 bytes, and addresses are of bytes.
Given the significant difference in size between elements in the Employee
100
Chapter 3 Aggregates and Pointers
array and the index array, it would be more efficient to sort the index than
the Employee array. Indeed, several index arrays might be created and then
sorted to obtain various orders: employees sorted by ID, by salary, by years
in service, and so on. To print or otherwise process the Employee elements
in the desired order, a program would traverse one of the indexes. The
Employee elements would remain in their initial positions.
This approach does bring a challenge to the programmer, however.
Consider the arguments passed to the qsort comparison function when
an index is sorted on some Employee feature such as ID or years in service.
Each such argument is of type const void*, which in this case is really of
type Employee**: a pointer to a pointer to an Employee. The arguments
to the comparison function thus must be dereferenced twice in order to
access the Employee feature to be used in the comparison. A full code
example goes into the details.
#include <stdio.h>
#include <stdlib.h> /* rand */
#define SizeS 1000
#define SizeA 100
typedef struct {
double nums[SizeS]; /* 8 bytes per */
int n; /* for demo purposes */
} BigNumsStruct;
101
Chapter 3 Aggregates and Pointers
void main() {
BigNumsStruct big_nums[SizeA];
BigNumsStruct* pointers[SizeA];
int i;
for (i = 0; i < SizeA; i++) {
big_nums[i].n = rand();
pointers[i] = big_nums + i; /* base address (big_nums)
plus offset (index i) */
}
The sortPtrs program (see Listing 3-12) revises the earlier example of
the BigNumsStruct. The size of this structure is reduced to a more realistic
number, and a local array of such structures is declared, which means that
storage for the array comes from the stack. The int field named n remains
and now is initialized to a random value.
Although a BigNumsStruct is slimmer than before, its sizeof remains
an impressive 8,008 bytes on my machine. By contrast, a pointer to such a
structure instance requires only 8 bytes on the same machine. In the sortPtrs
program, sorting the big_nums array would require moving 8KB (kilobytes)
chunks, whereas sorting pointers to the elements in this array would require
moving only 8-byte chunks. The resulting gain in efficiency is compelling.
The printf loop at the end confirms that the pointers array has been
sorted as desired, in ascending order by the BigNumsStruct field named n.
102
Chapter 3 Aggregates and Pointers
3.8.2. Unions
There is a specialized type of structure called a union, which is designed
for memory efficiency. A short example highlights the difference between
a struct and a union.
The following structure has two fields: a double and a long. The
sizeof(v1)
struct {
double d;
long l;
} v1;
is 16: both the double and the long are 8 bytes in size.
103
Chapter 3 Aggregates and Pointers
By contrast, a union with exactly the same fields would require only
half the bytes. The sizeof(v2)
union {
double d;
long l;
} v2;
is 8 bytes. A union provides enough storage for the largest of its fields,
and all of the fields then share this storage. For example, the struct
variable v1 can store both a double and a long at the same time:
v1.d = 44.44;
v1.l = 1234L;
By contrast, the union variable v2 stores either the one or the other:
involves a conversion: from the 32-bit int constant 65 to the 8-bit char
value stored in variable c. Converting from one single value to another
is routine in C: an explicit cast can be used for clarity, but in general, the
compiler can be counted on to do the converting without complaint. For
example, the compiler does not even warn against this conversion:
104
Chapter 3 Aggregates and Pointers
The const qualifier signals that the pointer argument is not used to
change the string itself, only to convert the string to a numeric value. The a
in atoi and the others is for ASCII, the default character encoding in C.
None of the ato functions are especially helpful in determining why
an attempted conversion failed. To that end, the stdlib.h header file also
includes functions with names that start out with strto, for example, strtol
(string to long integer) and strtod (string to double). The strto functions
check the string for inappropriate characters and have a mechanism for
separating out the converted part of the source string, if any, from the rest.
A code example clarifies.
105
Chapter 3 Aggregates and Pointers
#include <stdio.h>
#include <stdlib.h> /* atoi, etc. */
void main() {
const char* s1 = "27";
const char* s2 = "27.99";
const char* s3 = " 123"; /* whitespace to begin */
const char* e1 = "1z2q"; /* bad characters */
const char* e2 = "4m3.abc!#"; /* ditto */
The str2num program (see Listing 3-13) has three examples of strings
that convert straightforwardly. The pointers to these are s1, s2, and s3. The
string to which s3 points is the most interesting in that it begins with blanks;
106
Chapter 3 Aggregates and Pointers
but the atoi functions ignore the leading whitespace. The challenging
cases are the strings to which e1 and e2 point, as these strings contain
nonnumeric characters other than whitespace. (Numeric characters include
the numerals, the plus and minus signs, and the decimal point.)
For strings with nonnumeric characters such as the sharp sign, the ato
functions convert until the first such character is encountered and then
stop. This is why function atoi converts the string “1z2q” to 1: the function
converts as long as it can and then halts abruptly on the first inappropriate
character. If a string starts with a nonnumeric character, then the ato
functions return 0:
The strto functions are more powerful than their ato counterparts,
and they use a pointer-to-pointer type to gain this power. Here is the
declaration for strtol:
The first argument is again a pointer to the source string, and the return
value is a long. The last argument specifies the base to be used in the
conversion: 2 for binary, 10 for decimal, and so on. The middle argument
is the tricky one, as its type is pointer-to-pointer-to-char. Here, for review,
is the code segment in the str2num program that sets up and then calls the
strtol function:
107
Chapter 3 Aggregates and Pointers
#include <stdio.h>
void main() {
char* s1 = "123456";
char* s2 = "123.45";
int n1;
float n2;
108
Chapter 3 Aggregates and Pointers
109
Chapter 3 Aggregates and Pointers
The examples so far have not covered the third category, the heap. The
compiler figures out how much storage is required for the read-only area
and the stack; hence, the details about such storage are determined at
compile time—no extra programmer intervention is required. By contrast,
the programmer uses designated operators (e.g., new in many modern
languages) or functions (e.g., malloc and its relatives in C) to allocate
storage from the heap, an allocation traditionally described as dynamic
because it is done explicitly at runtime.
The programmer plays a more active role with heap as opposed to
stack storage. The compiler determines the mix of stack and CPU registers
required for program execution, thereby off-loading this responsibility
from the programmer. By contrast, the programmer manages heap storage
through system calls to allocate and, in the case of C, to deallocate this
storage. A review of stack storage through a code example sets the scene
for a code-based analysis of heap storage.
110
Chapter 3 Aggregates and Pointers
void main() {
int nums[ ] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int n = sum_array(nums, Size);
printf("The sum is: %i\n", n); /* The sum is: 45 */
}
The sumArray program (see Listing 3-15) has two functions, main and
sum_array, each of which needs stack storage for scratchpad. The main
function has a local array of nine elements, each an int; these elements
are stored on the stack. This function also has a local variable n to store the
value returned from a call to the sum_array function. Depending on how
optimizing the compiler happens to be, variable n could be implemented
as a CPU register instead of as a stack location.
The sum_array function works with a pointer to the array declared and
populated in main, but sum_array does need some local storage of its own:
the integers sum and i, the loop counter. Both sum and i are scalars rather
than aggregates, and so CPU registers would be ideal; but the stack is the
fallback for the compiler.
The assembly code for the sumArray program is generated in the
usual way:
111
Chapter 3 Aggregates and Pointers
For readability, the resulting assembly code has been pared down; for
instance, most of the directives are omitted. The first code display is the
assembly code for main, and the following display is the assembly code for
sum_array. To begin, however, a look at the syntax for pointers in assembly
code will be useful.
Recall the assembly opcode movq, which copies 64 bits (a quadword)
from a source to a destination:
A comparable C statement is
112
Chapter 3 Aggregates and Pointers
With this background, the assembly code for the function main in the
sumArray program should make sense.
Listing 3-16. The assembly code for main in the sumArray program
113
Chapter 3 Aggregates and Pointers
The high points of the assembly code for the main block (see
Listing 3-16), the assembly-language counterpart of the main function in C,
can be summarized as follows:
• After shrinking the stack back to its size before the call
to main, the main routine returns to its caller. Recall that
main in the C source does not return a value; hence,
the assembly routine does not place a value in %eax
immediately before returning.
sum_array:
testl %esi, %esi ## is the array size 0?
je .L4 ## if so, return to caller
movl $0, %edx ## otherwise, set loop counter to 0
movl $0, %eax ## initialize sum to 0
.L3:
addl (%rdi,%rdx,4), %eax ## increment the running sum by
the next value (sum += arr[i])
addq $1, %rdx ## increment loop counter by 1
(integer)
cmpl %edx, %esi ## compare loop counter with
array size
ja .L3 ## keep looping if size is bigger
(ja = jump if above)
115
Chapter 3 Aggregates and Pointers
Several points about the code deserve mention. For one thing, the code
sometimes references the 64-bit register %rdx but sometimes references
only the lower 32 bits of this register under the name %edx. This can be
confusing but works just fine because the upper-order bits in register %rdx
have been zeroed out.
Another point of interest is the most complicated instruction in the
sum_array routine:
116
Chapter 3 Aggregates and Pointers
This instruction updates the loop counter %rdx by one integer, not by
4 bytes. Accordingly, the addl instruction’s first operand is the expression
(%rdi,%rdx,4). Register %rdi points to the start of the array; in the C code,
this is the parameter arr in the function sum_array. The offset from this
base address is %rdx × 4, where %rdx is the loop counter (in C, the index i)
and 4 is sizeof(int).
The assembly code confirms that the stack requirements for the
sumArray program are determined at compile time. The stack management
is thus automatic from the programmer’s perspective: the programmer
declares local variables and parameters, makes a function call, executes
a print statement, and so on. The compiler manages the details when it
comes to providing scratchpad storage on the stack and, in this example, in
CPU registers as well.
This analysis of the sumArray program sets up a contrast between
stack and heap storage. C has functions for allocating heap storage,
with the malloc and the calloc functions as the primary ones. There is
also a realloc function for growing or shrinking previously allocated
heap memory. The free function deallocates the memory allocated by
any of these functions. The general rule for avoiding memory leaks is
this: for every malloc or calloc, there should be a matching free. The
programmer is fully responsible for the calls to these functions. A first code
example covers the basics.
#include <stdio.h>
#include <stdlib.h> /* malloc, calloc, realloc */
#include <string.h> /* memset */
#define Size 20
117
Chapter 3 Aggregates and Pointers
void main() {
/* allocate */
int* mptr = malloc(Size * sizeof(int)); /* 20 ints, 80
bytes */
if (mptr) /* malloc returns NULL (0) if it cannot allocate
the storage */
memset(mptr, -1, Size * sizeof(int)); /* set each byte
to -1 */
dump(mptr, Size);
/* realloc */
mptr = realloc(mptr, (Size + 8) * sizeof(int)); /* request
8 more */
if (mptr) dump(mptr, Size + 8);
/* deallocate */
free(mptr);
/* calloc */
mptr = calloc(Size, sizeof(int)); /* calloc initializes the
storage to zero */
if (mptr) {
dump(mptr, Size);
free(mptr);
}
}
118
Chapter 3 Aggregates and Pointers
The program memalloc (see Listing 3-18) shows the basic API for
allocating and deallocating memory from the heap. The simplest and most
basic function is malloc, which tries to allocate the number of bytes given
as its single argument. The return type from malloc is the same for calloc
and realloc:
119
Chapter 3 Aggregates and Pointers
No. Library functions such as malloc and calloc allocate specified amounts
of storage from the heap, but the programmer then is responsible for explicitly
deallocating (freeing) this heap storage. Allocation without deallocation causes
memory leaks, which can dramatically degrade system performance. Freeing
no longer needed heap storage is, indeed, one of the major challenges in
writing sound C programs.
120
Chapter 3 Aggregates and Pointers
Two code examples get into the details of the challenge. The first
example focuses on how to return an aggregate to a caller, and the second
focuses on nested freeing.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BuffSize 128
121
Chapter 3 Aggregates and Pointers
The getname program (see Listing 3-19) contrasts three ways to return
a string—an aggregate—from a function. The compiler generates an
apt warning about one of the ways. Three functions represent the three
different approaches. For each approach, imagine that a user is prompted
for, and then enters, a name. To keep the code short, the example hard-
wires the names. Here is a summary of the three approaches, with
recommendations:
to
122
Chapter 3 Aggregates and Pointers
The getname program still compiles because the compiler treats these
two data types as being equivalent:
The main function for the getname program (see Listing 3-20) declares
a char buffer, which is used in the call to function get_name1. The
responsibility falls squarely on the caller to provide enough storage for the
string to be stored. The second argument, BuffSize, guards against buffer
overflow because the char array is of size BuffSize + 1, with the added
byte for the null terminator.
The call to get_name2 returns a pointer to the heap storage provided
for the name. In this case, the main function does call free, but the logic is
complicated: one function allocates, another function frees. The approach
works, but it is error-prone.
The last call, to get_name3, provokes a compiler warning because a
pointer to local storage is being returned to main. In this case, the storage
for the name is local to the call frame for get_name3. Once the function
get_name3 returns to main, the call frame for get_name3 should not be
accessed. It is unpredictable whether this third approach works.
124
Chapter 3 Aggregates and Pointers
This code segment allocates heap storage for 5,000 int values, does
whatever logic is appropriate, and then frees the storage. The challenge
increases when, for example, structure instances are allocated from the
heap—and such instances contain fields that are themselves pointers to heap
storage. If the heap allocation is nested, the freeing must be nested as well.
As a common example of the challenge, C has various library functions
that return a pointer to heap storage. Here is a typical scenario:
125
Chapter 3 Aggregates and Pointers
The next code example (see Listing 3-21) illustrates the problem and
focuses on how to design a library that safely provides heap-allocated
storage to clients.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
unsigned id;
unsigned len;
float* heap_nums;
} HeapStruct;
unsigned structId = 1;
HeapStruct* get_heap_struct(unsigned n) {
/* Try to allocate a HeapStruct. */
HeapStruct* heap_struct = malloc(sizeof(HeapStruct));
if (NULL == heap_struct) /* failure? */
return NULL; /* if so, return NULL */
126
Chapter 3 Aggregates and Pointers
int main() {
const unsigned n = 100;
HeapStruct* hs = get_heap_struct(n); /* get structure with N
floats */
return 0;
}
127
Chapter 3 Aggregates and Pointers
typedef struct {
unsigned id;
unsigned len;
float* heap_nums; /** pointer **/
} HeapStruct;
hs-->HeapStruct instance
id
len
heap_nums-->N contiguous float elements
128
Chapter 3 Aggregates and Pointers
129
Chapter 3 Aggregates and Pointers
After checking that the argument heap_struct is not NULL, the function
first frees the heap_nums array, which requires that the heap_struct
pointer is still valid. It would be an error to free the heap_struct first.
Once the heap_nums have been deallocated, the heap_struct can be
freed as well. If heap_struct were freed but heap_nums were not, then the
float elements in the array would be leakage: still allocated bytes but
with no possibility of access—hence, of deallocation. The leakage would
persist until the nestedHeap program exited and the system reclaimed the
leaked bytes.
A few cautionary notes on the free library function are in order. Recall
the earlier sample calls:
These calls free the allocated storage—but they do not set their
arguments to NULL. (The free function gets a copy of an address as an
argument; hence, changing the copy to NULL would leave the original
unchanged.) For example, after a successful call to free, the pointer
heap_struct still holds a heap address of some heap-allocated bytes, but
using this address now would be an error because the call to free gives the
system the right to reclaim and then reuse the allocated bytes.
130
Chapter 3 Aggregates and Pointers
3.12.1. Memory Leakage
and Heap Fragmentation
As the previous code examples illustrate, the phrase memory leakage”
refers to dynamically allocated heap storage that is no longer accessible.
Here is a refresher code segment:
Assume that the first malloc succeeds. The second malloc resets the
nums pointer, either to NULL (allocation failure) or to the address of the
first float among newly allocated 25. Heap storage for the initial ten
float elements remains allocated but is now inaccessible because the
nums pointer either points elsewhere or is NULL. The result is 40 bytes
(sizeof(float) * 10) of leakage.
Before the second call to malloc, the initially allocated storage should
be freed:
131
Chapter 3 Aggregates and Pointers
Even without leakage, the heap can fragment over time, which then
requires system defragmentation. For example, suppose that the two
biggest heap chunks are currently of sizes 200MB and 100MB. However,
the two chunks are not contiguous, and process P needs to allocate
250MB of contiguous heap storage. Before the allocation can be made, the
system must defragment the heap to provide 250MB contiguous bytes for
P. Defragmentation is complicated and, therefore, time-consuming.
Memory leakage promotes fragmentation by creating allocated but
inaccessible heap chunks. Freeing no-longer-needed heap storage is,
therefore, one way that a programmer can help to reduce the need for
defragmentation.
#include <stdio.h>
#include <stdlib.h>
int* get_ints(unsigned n) {
int* ptr = malloc(n * sizeof(int));
if (ptr != NULL) {
unsigned i;
for (i = 0; i < n; i++) ptr[i] = i + 1;
}
return ptr;
}
132
Chapter 3 Aggregates and Pointers
int main() {
const unsigned n = 32;
int* arr = get_ints(n);
if (arr != NULL) print_ints(arr, n);
% valgrind --leak-check=full ./leaky
In the following code segment, most of the output is shown. The number
of the left, 207683, is the process identifier of the executing leaky program.
The report provides details of where the leak occurs, in this case, from the
call to malloc within the get_ints function that main calls.
133
Chapter 3 Aggregates and Pointers
If function main is revised to include a call to free right after the one to
print_ints, then valgrind gives the leaky program a clean bill of health:
3.13. What’s Next?
C variables can be defined inside and outside of blocks, where a block is
the by-now-familiar construct that begins with a left brace { and ends with
a matching right brace }. Where a variable is defined determines, within
options, where its value is stored, how long the variable persists, and where
the variable is visible. Every variable has a storage class (with auto and
extern as examples) that determines the variable’s persistence and scope.
Functions too have a storage class: either extern (the default) or static.
The next chapter gets into the details of storage classes.
134
CHAPTER 4
Storage Classes
4.1. Overview
In C, a storage class determines where functions and variables are stored,
how long variables persist, and where functions and variables can be made
visible. For functions, the key issue is visibility (scope) because the lifetime
of a function is the lifetime of the program that contains the function. For
variables, both lifetime and scope are of interest to the programmer.
Storage classes also shed further light on the distinction between
declarations and definitions in C. In large programs, with the constituent
functions typically residing in different files, the distinction is especially
important. Once again, code examples illustrate the basics and advanced
features. The chapter ends with a discussion of type qualifier volatile, yet
another aspect of C’s close-to-the-metal personality.
C has four storage class specifiers: extern, static, auto, and register.
It is rare for the last two to be used explicitly in modern C because the
compiler, on its own, does what the specifiers call for. The first two
specifiers, extern and static, remain relevant. A function can be either
extern or static only; a variable can be any one of the four. A storage class
also impacts the following:
136
Chapter 4 Storage Classes
137
Chapter 4 Storage Classes
#include <stdio.h>
#include <stdlib.h> /* rand() */
int main() {
/* i and n are visible from their declaration to the end
of main */
auto int i; /* auto is the default in any case */
The autoreg program (see Listing 4-1) shows how the auto and
register specifiers could be used. Recall that these specifiers are used
for variables only, and only for variables declared inside a block. In this
example, there are two blocks:
138
Chapter 4 Storage Classes
139
Chapter 4 Storage Classes
Running the program produces the file gmon.out, and the utility gprof then
can be executed from the command line:
% gprof
#include <stdio.h>
void foo() {
static unsigned n = 0; /* initialized only once */
if (SizeF == ++n) printf("foo: %i\n", n);
}
140
Chapter 4 Storage Classes
void bar() {
static unsigned n; /* initialized automatically to zero */
if (SizeB == ++n) printf("bar: %i\n", n);
}
void main() {
unsigned i = 0, limit_foo = SizeF, limit_bar = SizeB;
while (i++ < limit_foo) foo(); /* call foo() a bunch of
times */
i = 0;
while (i++ < limit_bar) bar(); /* call bar() a bunch of
times */
}
The profile program (see Listing 4-2) tracks the number of times that
main calls two other functions, foo and bar. Each of the called functions
has a local static variable named n. The compiler initializes a static
variable to zero unless the program provides an initial value. Two points
about these static variables are important in this example:
141
Chapter 4 Storage Classes
142
Chapter 4 Storage Classes
The rule of thumb for making life easy on the programmer is to avoid
the explicit extern in a definition (in particular for variables) and to use
the explicit extern only in a declaration.
A code example should help to clarify the details. The example consists
of two files, prog2files1.c and prog2files2.c. These will be considered
in order.
#include <stdio.h>
143
Chapter 4 Storage Classes
144
Chapter 4 Storage Classes
145
Chapter 4 Storage Classes
For review, here again is the rule of thumb that sidesteps the legalese
surrounding the specifier extern. This rule can be spelled out as two
related recommendations:
146
Chapter 4 Storage Classes
The qualifier const for constant originated in C++ and was brought into C. A
few code segments clarify.
Recall that the parameters to the qsort comparison function are const
void*, in effect a promise that such pointers will not be used to modify the
values pointed to.
147
Chapter 4 Storage Classes
148
Chapter 4 Storage Classes
The standard compilers have a debugger with the usual support: breakpoints,
stepping, viewing and resetting variables, and so on. Here is an example with
the fpoint.c as the source file:
1. Compile with the -g flag:
% gcc -g -o fpoint point.c
149
Chapter 4 Storage Classes
4.7. What’s Next?
Every program in execution requires at least one processor (CPU) to
execute its instructions and memory to store these instructions and
the data that together make up the program. Except for special cases,
a program uses I/O devices as well, which are accessible to a program
as files of one sort or another. A file in this generic, abstract sense is just
a collection of words, and a word is just a formatted collection of bits.
For example, a camera in a smartphone and the lowly keyboard on a
desktop machine are both files in this sense. The role of input and output
operations is, of course, to allow a program to interact with the outside
world. The next chapter gets into the details by highlighting C’s flexible
approach to input/output operations.
150
CHAPTER 5
Input and Output
5.1. Overview
Programs of all sorts regularly perform input/output (I/O) operations, and
programmers soon learn the pitfalls of these operations: trying to open a
nonexistent file, having too many files open at the same time, accidentally
overwriting a file and thereby losing its data, and so on. Nonetheless, I/O
operations remain at the core of programming.
C has two APIs for I/O operations: a low-level or system-level API,
which is byte-oriented, and a high-level API, which deals with multibyte
data types such as integers, floating-point types, and strings. The system-
level functions are ideal for fine-grained control, and the high-level
functions are there to hide the byte-level details. Although the two APIs
can be mixed, as various code examples show, this must be done with
caution. This chapter covers both APIs and examines options such as
nonblocking and nonsequential for I/O operations.
Files and I/O operations are one way to support interprocess
communication (IPC). Recall that separate processes have separate
address spaces by default, which means that shared memory, although
possible, requires setup for processes to communicate with one another.
Local files, by contrast, can be used readily for IPC: one process can
produce data that is streamed to a file, while another process can
consume the data streamed from this file. A later section examines how to
synchronize process access to shared files.
5.2. System-Level I/O
A short review of some basic concepts should be helpful in clarifying
system-level I/O in C. A process, as a program in execution, requires shared
system resources from at least two but typically from three categories:
152
Chapter 5 Input and Output
At the command line on modern systems, the less-than sign < redirects
the standard input; the greater-than sign > redirects the standard output;
and the combined symbols 2> redirect the standard error. Examples are
forthcoming, together with a clarification of why the numeral in 2> is 2.
In system-level I/O, nonnegative integer values called file descriptors
are used to identify, within a process, the files that the process has opened.
Recall that files can be used for interprocess communication (IPC). If
two processes were to open a file to share data using system-level I/O,
then each process would have a file descriptor identifying the file; the file
descriptor values would not have to be the same because the operating
system maintains a global file table that tracks which processes have
opened which files.
Table 5-1 summarizes the basics about the three files to which
a normal process automatically gets access. For other files, access is
achieved through a successful call to an open function: in low-level I/O,
the basic function is named open, and in high-level I/O, the basic function
is named fopen. The table now can be clarified further:
153
Chapter 5 Input and Output
Listing 5-1. Some basic I/O operations using the system-level API
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BuffSize 4
void main() {
const char* prompt = "Four characters, please: ";
char buffer[BuffSize]; /* 4-byte buffer */
if (flag < 0)
perror("Ooops..."); /* this string + a system msg
explaining errno */
154
Chapter 5 Input and Output
else
write(1, buffer, sizeof(buffer));
/* 1 == stdout */
putchar('\n');
}
The ioLL program (see Listing 5-1) is a first look at low-level or byte-
oriented I/O. The program uses two of the three automatically supplied
file descriptors: 0 for the standard input (keyboard) and 1 for the standard
output (screen). The key features of the program can be summarized as
follows:
155
Chapter 5 Input and Output
Like many of the low-level I/O functions, read returns an int value:
the number of bytes read, on success, and -1, on error. If an error occurs,
an error code is available in the global variable errno, which is declared
in the header file errno.h. The perror function prints a human-readable
description of this error. This function takes a single string argument
so that the user can add a customized error message to which perror
appends a system error message. If only the system error message is of
interest, perror can be called with NULL as its argument.
The program concludes with another call to write, this time using 1
to designate the standard output. The bytes to be written come from the
array buffer, and the number of bytes is computed as sizeof(buffer),
which returns the number of bytes in the array, not the size of the pointer
constant buffer.
The buffer does not include extra space for a null terminator: the
program does not treat the input from keyboard as a string, but rather as
four independent bytes. The write function takes the same approach: no
string terminator is needed because the last argument to write specifies
exactly how many bytes should be written, in this case four.
A short experiment underscores the level at which the functions read
and write work. The experiment is to replace
char buffer[BuffSize];
with
156
Chapter 5 Input and Output
or, indeed, with a variable of any data type whose size is at least 4 bytes.
The read call now changes to
The 4 bytes are to be put into a single int variable, which now acts
like a 4-byte buffer. The write statement requires only a minor but
critical change:
% ./ioLL
Four characters, please: !$ef
!$ef
These characters are not numerals, of course. The low-level read and
write functions treat these simply as 8-bit bytes stored together in a 4-byte
variable named buffer.
157
Chapter 5 Input and Output
the program to call open on these three. For other files, however, a call to
open is required, and a matching call to close is sound practice. (When a
program terminates, the system closes any files that the program may have
opened.) The open function, like so many in the standard libraries, takes a
variable number of arguments.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void main() {
/* Open a file for reading and writing. */
int fd = open(FILE_NAME, /* name */
O_CREAT | O_RDWR, /* create, read/
write */
S_IRUSR | S_IWUSR | S_IXUSR | /* owner's
rights */
S_IROTH | S_IWOTH | S_IXOTH); /* others'
rights */
158
Chapter 5 Input and Output
perror(NULL);
return;
}
The sysWrite program (see Listing 5-2) tries to open a file on the local
disk, creating this file if necessary. The program sets the access rights for
the file’s owner and for others. The program then writes five integers to
the file and closes the file. There is error-checking on all three of these I/O
operations.
In this example, the call to the open function has three arguments, but
the open function also can be called with only the first two arguments. The
arguments in this case are as follows:
O_CREAT | O_RDWR
159
Chapter 5 Input and Output
whereas
160
Chapter 5 Input and Output
161
Chapter 5 Input and Output
are harmless.
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
void main() {
int fd = open(FILE_NAME, O_RDONLY); /* open for
reading only */
if (fd < 0) { /* -1 on error, > 2 on success */
perror(NULL); /* "No such file or directory" if nums.dat
doesn't exist */
return;
}
int i;
int n = how_many / sizeof(int); /* from byte count to number
of ints */
for (i = 0; i < n; i++) printf("%i\n", read_in[i] * 10);
/* 90 70 50 30 10 */
}
162
Chapter 5 Input and Output
The sysRead program (see Listing 5-3) reads five 4-byte int values from
the same file that the sysWrite program populates with these integers. In
the sysRead program, the file is opened for read-only. The available macro
flags for a call to open, together with their values, are
The source code documentation shows the perror message if the file
nums.dat does not exist.
Once the file is opened, the read function requires a buffer in which
to place the bytes, in this case the read_in array that can hold five int
elements, or 20 bytes in all. The read function, like the others seen so far,
returns -1 in case of error; 0 on end of file; and otherwise the number of
bytes read.
A read operation is the inverse of a write operation, and the arguments
passed to read and write reflect this relationship. The first argument to
read is a file descriptor for the source of bytes, whereas this argument
specifies the destination in the case of write. The second argument to
read is the destination buffer, whereas this argument specifies the source
in a write. The last argument is the same in both: the number of bytes
involved.
The sysRead program uses the high-level printf function to print the
int values. Each value is multiplied by 10 to confirm that int instances
have been read into memory from the source file. Recall that a successful
read returns the number of bytes, in this case stored in the local variable
how_many; hence, how_many is divided by sizeof(int) to get the number of
4-byte integers, in this case five.
Together the sysWrite and sysRead programs illustrate how local disk
files can support basic interprocess communication. The programs would
163
Chapter 5 Input and Output
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BuffSize 8
void main() {
char buffer[BuffSize]; /* 8-byte buffer */
ssize_t flag = read(0, buffer, sizeof(buffer)); /* 0 ==
stdin */
if (flag < 0) {
perror("Ooops...");
return;
}
char ws = '\t';
164
Chapter 5 Input and Output
The ioRedirect program (see Listing 5-4) expects to read 8 bytes from
the standard input and then echoes these bytes to the standard output and
the standard error. If the bytes are ASCII character codes, the program is
easy to follow. Here is a screen capture of a sample run; my comments start
with ##:
abcdefgh
abcdefgh abcdefgh
165
Chapter 5 Input and Output
The eight characters entered on the keyboard now appear once on the
screen (default for the standard error) and once in the local disk file outfile.
By the way, if outfile already exists, then the redirection purges this file and
then repopulates it; hence, caution is in order.
Redirection to the standard error differs only slightly. Recall that 2 is
the file descriptor for the standard error:
Assuming that infile is the same as before, the contents of logfile are
abcdefgh abcdefgh
5.4. Nonsequential I/O
The examples so far have dealt with sequential I/O: bytes are read in
sequence and written in sequence. It is convenient at times, however, to
have random or nonsequential access to a file’s contents. A short code
example illustrates the basic API.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
166
Chapter 5 Input and Output
void main() {
const char* bytes = "abcdefghijklmnopqrstuvwxyz";
int len = strlen(bytes);
char buffer[len / 2];
char big_N = 'N';
The nonseq program (see Listing 5-5) skips the error checking to
minimize the clutter, thereby keeping the focus on the nonsequential file
access. The program first writes 26 bytes (the lowercase characters in the
English alphabet) to a file. After closing the file, the program reopens the
file to do an lseek operation that sets up another write operation, this
167
Chapter 5 Input and Output
time a write of just one byte. As the name indicates, the function lseek
performs a seeking operation, which can change the current file-position
marker. A closer look at lseek clarifies.
The library function lseek takes three arguments. They are, in order:
• A file descriptor
168
Chapter 5 Input and Output
5.5. High-Level I/O
System-level I/O is low level because it works with bytes, the char type in
C; by contrast, high-level I/O can work with multibyte data types such as
integers, floating-point numbers, and strings. To take but one convenient
example, the API for the high-level I/O makes it straightforward to convert
between, for example, integers and strings. High-level I/O can work at
the byte (char) level, but this kind of I/O is especially useful above the
byte level.
The names are similar for some functions in the high-level and the
low-level API. For example, there is a low-level open function and the
high-level fopen function, as well as the low-level close and the high-
level fclose functions. There is an fread function in the high-level API
that matches up with the read function in the low-level API. The functions
differ in syntax, of course, but also in how they work at the byte level. The
low-level functions work only at the byte level, whereas the high-level API
can work directly with multibyte types such as int and double.
There is crossover. For example, the high-level fdopen function takes
a low-level file descriptor as an argument but returns the high-level type
FILE*, the return type for various high-level library functions. Consider
this contrast for opening and closing a file on the local disk:
169
Chapter 5 Input and Output
In general, a file opened with the low-level open function is closed with
the low-level close function. In a similar fashion, a file opened with fopen
is closed with the fclose function. By the way, there is a limit on how
many files a process can have open at a time; hence, it is critical to close
files when keeping them open is no longer important.
In the low-level API, the integer values 0, 1, and 2 identify the standard
input, the standard output, and the standard error, respectively. In the
high-level API, the FILE* pointers stdin, stdout, and stderr do the same.
The data type of interest in high-level I/O is FILE*, not FILE. It would be
highly unusual for a program to declare a variable of type FILE, but typical
for a program to assign the value returned from a high-level I/O function to
a variable of type FILE*.
The following code segment summarizes the contrast between low-
level and high-level I/O, with variable fd as a file descriptor and variable
fptr as a pointer to FILE:
170
Chapter 5 Input and Output
To study the API for the high-level I/O is, in effect, to study various
ways of managing I/O streams. The forthcoming examples do so.
#include <stdio.h>
#define FILE_NAME "data.in"
void main() {
float num;
printf("A floating-point value, please: ");
int how_many_floats = fscanf(stdin, "%f", &num);
/* last arg must be an address */
if (how_many_floats < 1)
fprintf(stderr, "Bad scan -- probably bad characters\n");
else
fprintf(stdout, "%f times 2.1 is %f\n", num, num * 2.1);
171
Chapter 5 Input and Output
The scanPrint program (see Listing 5-6) covers some basics of high-
level I/O, beginning with scanning a file for input. The statement
172
Chapter 5 Input and Output
If the address operator & were missing from &num in the scanf call, the
contents of num would be interpreted as an address, and it is highly unlikely
that these random bits make up an address within the executing program’s
address space. If num is a local variable, for example, its contents are random
bits from the stack or a register.
if (how_many_floats < 1)
fprintf(stderr, "Bad scan -- probably bad characters\n");
173
Chapter 5 Input and Output
The last loop in the program is a while loop, and the loop’s condition is
a common one in programs that use high-level I/O to read from files:
The value returned from fscanf in particular, and the related scanning
functions in general, is tricky:
174
Chapter 5 Input and Output
A final point about EOF is in order. The EOF value (32 1s in binary)
marks the end of a stream, and streams can differ in their sources. If the
source is a file on a local disk, then the EOF is generated when a read
operation tries to read beyond the last byte stored in the file. If the source
is a pipe, a one-way channel between two processes, then the EOF is
generated when the pipe is closed on the sending side. An EOF thus should
be treated as a condition, rather than as just another data item. To be sure,
a program recognizes the EOF condition by reading the 32 bits that make
up the EOF value; but these 32 bits differ in meaning from whatever else
happens to be read from the stream.
High-level I/O is appropriately named, for this level focuses on
the multibyte data types that are dominant in high-level programming
languages. There may be times at which any program must drop down to
the byte level, but the usual level is awash with integers, strings, floating-
point values, and other instances of multibyte types. C works well at either
I/O level. Other technical aspects of high-level I/O will be explored in
forthcoming examples, which provide context for exploring this API.
char byte;
read(fd, &byte, 1); /* fd identifies a local disk file */
175
Chapter 5 Input and Output
char byte;
read(0, &byte, 1); /* one byte from standard input */
fread(&byte, 1, 1, stdin); /* ditto */
There are also high-level functions such as fgetc that seem to read a
single byte, as the c for char in the function’s name suggests. But the return
type for fgetc and related high-level functions is int, not char. The fgetc
function, like its high-level cousins, returns EOF to signal the end-of-stream
condition, and EOF is a 4-byte int value. In situations other than EOF, the
fgetc function returns a byte packaged in an int whose high-order 24 bits
are zeroed out; the byte of interest occupies the low-order 8 bits.
176
Chapter 5 Input and Output
#include <unistd.h>
#include <stdio.h>
void main() {
int i = 0, n = 8;
char byte;
/* unbuffered */
while (i++ < n) {
read(0, &byte, 1); /* read a single byte */
write(1, &byte, 1); /* write it */
}
/* buffered */
i = 0;
while (i++ < n) {
int next = fgetc(stdin); /* char read in a 4-byte int */
fputc(next, stdout); /* char written as a 4-byte int */
}
putchar('\n');
}
/* stdin is: 12345678abcdefgh */
177
Chapter 5 Input and Output
5.7. Nonblocking I/O
Nonblocking I/O has become a popular technique for boosting
performance. For example, a production-grade web server is likely to
include nonblocking I/O in the mix of acceleration techniques. The
potential boost in performance is likewise a challenge to the programmer:
nonblocking I/O is simply trickier to manage than its blocking counterpart.
As the name indicates, nonblocking I/O operations do not block—that
is, wait—until a read, write, or other I/O operation completes. Consider
this code segment in system-level I/O:
The file descriptor fd might identify a local file on the disk but also
a less reliable source of bytes such as a network connection. If the read
operation in the second statement blocks, then the printf statement
immediately thereafter does not execute until the read call returns,
perhaps because of an error.
178
Chapter 5 Input and Output
If the read call were nonblocking, the code segment would need a
more complicated approach. A nonblocking call returns immediately, and
there are now various possibilities to consider, including the following:
• The read call got only some of the expected bytes and
perhaps none at all.
The program now needs logic to handle such cases. Consider the
second case. If one call to a nonblocking read gets only some of the
expected bytes, then these bytes need to be saved, and another read
attempted to get the rest. Perhaps a loop becomes part of the read logic:
loop until all of the expected bytes arrive or an error occurs. At the very
least, it seems that the printf statement would need to occur inside
an if test that checks whether enough bytes were received to go on with
the printf.
179
Chapter 5 Input and Output
180
Chapter 5 Input and Output
% rm tester
As the name mkfifo suggests, a named pipe also is called a fifo for first
in, first out (FIFO). A named pipe implements the FIFO discipline so that
the pipe acts like a normal queue: the first byte into the pipe is the first byte
out, and so on. There is also a library function named mkfifo, which is
used in the next code example.
181
Chapter 5 Input and Output
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
void main() {
const char* pipeName = "./fifoChannel";
mkfifo(pipeName, 0666); /* read/write for user/group/
others */
int fd = open(pipeName, O_CREAT | O_WRONLY); /* open as
write-only */
182
Chapter 5 Input and Output
The fifoWriter program (see Listing 5-8) creates and then writes
sporadically to the named pipe called fifoChannel. Two statements at the
start do the setup:
The first statement calls the library function mkfifo with two
arguments: the name of the implementing file and the access permissions
in octal. The second statement invokes the by-now-familiar open function,
specifying that the file underlying the named pipe be created if necessary;
the fifoWriter is restricted to write operations because of the O_WRONLY flag.
The fifoWriter then pauses for two seconds to give the user a chance
to start the other program, the fifoReader. The fifoWriter needs to start
first because it creates and opens the named pipe; but the two-second
pause is there only for convenience. The fifoWriter program then loops
183
Chapter 5 Input and Output
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
unsigned i;
for (i = 5; (i * i) <= n; i += 6)
if (0 == (n % i) || 0 == (n % (i + 2))) return 0;
return 1;
}
184
Chapter 5 Input and Output
void main() {
const char* file = "./fifoChannel";
int fd = open(file, O_RDONLY | O_NONBLOCK); /* non-
blocking */
if (fd < 0) return; /* no point in continuing */
unsigned primes_count = 0, success = 0, failure = 0;
while (1) {
int next;
int i;
185
Chapter 5 Input and Output
The fifoReader program (see Listing 5-9) reads from the named pipe
that the fifoWriter creates and then populates with chunks of int values.
The program configures the pipe for nonblocking read operations with the
O_NONBLOCK flag passed as an argument to the open function:
The utility function fcntl also could be used to set the nonblocking
status, as illustrated shortly. The program tries to read int values from
the pipe:
Recall that the fifoWriter writes an array of four int values at a time
and does so sporadically. Because the read operation in the fifoReader is
nonblocking, three cases are singled out for application logic:
186
Chapter 5 Input and Output
187
Chapter 5 Input and Output
The setNonBlock example (see Listing 5-10) shows how a file descriptor
can be used to change the status from blocking to nonblocking. The set_
nonblock function takes a file descriptor as its only argument and returns
either true (1) or false (0) to signal whether the attempt succeeded. The
function first gets the flags currently set (e.g., O_CREAT and O_RDONLY); if
an error occurs here, false is returned. Otherwise, the function adds the O_
NONBLOCK flag and then uses the fcntl function for updating. If the update
succeeds, set_nonblock returns true, and false otherwise.
5.8. What’s Next?
Network programming centers on the socket API, where a socket is an
endpoint in a point-to-point connection between two processes. If the
processes are running on physically distinct hosts (machines), a network
socket is in play. If the processes are running on the same host, a domain
socket could be used instead. (Domain sockets are a popular way for large
systems, such as database systems, to interact with clients.) The very same
I/O API used to interact with disk files works with sockets as well. Sockets,
unlike pipes, are bidirectional.
This chapter has focused on I/O operations on a single machine.
The next chapter broadens the study to include I/O operations across
machines, and the chapter also explores an event-driven alternative to the
nonblocking I/O introduced in this chapter.
188
CHAPTER 6
Networking
6.1. Overview
Network programming brings challenges beyond the details of yet
another API. Networks can be brittle, as connections go down for reasons
that may be hard to determine. Performance can vary widely because
of network load. Programs must be sufficiently robust to deal with such
issues and to anticipate the many others that come with the territory.
Debugging network applications is harder, in general, than debugging
ones that involve only a single machine. Given the challenges of network
programming, it is no surprise that library functions in its support can
seem subtle, complicated, and even overwhelming. This chapter uses
relatively short but realistic examples to illustrate the challenges and
sound ways to address them. After a few more introductory points, the
discussion moves to a series of code examples.
HTTP Hyper Text Transport Protocol Web servers and their clients
TCP Transmission Control Protocol Connection-oriented, reliable
UDP User Datagram Protocol Connectionless, best-try
IP Internet Protocol Addressing: symbolic and numeric
190
Chapter 6 Networking
void main() {
const char* host = "www.google.com"; /* symbolic IP
address */
const char* port = "80"; /* standard port for
HTTP connections */
const char* request = "GET / HTTP/1.1\nHost: www.google.
com\r\n\r\n";
ssize_t count;
char buffer[BuffSize];
/* connect */
int sock_fd = get_connection(host, port);
if (sock_fd < 0) {
fprintf(stderr, "Can't connect\n");
exit(-1);
}
191
Chapter 6 Networking
while (1) {
count = read(sock_fd, buffer, sizeof(buffer));
The file web_client.c (see Listing 6-1), one of the two source files in
the webclient program, uses the familiar read and write functions to
communicate with a web server, in this case a Google HTTP server. A
socket, just like a file on the local disk, has a file descriptor as its identifier.
In addition to the read and write functions, the socket API also has send
and recv functions, which take four arguments instead of the three in read
and write. The fourth argument, in both cases, allows for configuration
through various flags.
192
Chapter 6 Networking
The webclient program initializes two strings, host and port, which
specify the symbolic IP address for the Google server and the port number:
www.google.com and 80, respectively. The port number 80 is the default for
HTTP connections, just as 443 is the default port for HTTPS connections.
Instead of the symbolic IP address, the program could have used the IPv4
dotted-decimal address 216.58.192.132, each of whose four parts is 8 bits
in size. The IP address and port number are sent as arguments to the get_
connection function, which returns either the file descriptor for a socket
(success) or -1 (failure). In case of failure, the webclient program exits after
an error message.
WHAT’S A MAKEFILE?
The first line lists the target (webclient) and its dependencies, with a colon as
the separator. The dependencies consist of the two source files in any order.
The second line begins with a single tab character, not blanks. This line is
the command to be executed, in this case a familiar gcc command. At the
command line, invoke the make utility:
193
Chapter 6 Networking
The webclient program has a third string literal, which holds the
request. In more readable form, the request is
The first line is the HTTP start line, consisting of the HTTP method
(verb) named GET: a GET request is a read request, whereas a POST
request is a create request (e.g., a POSTed order form is a request to create
an order). After the start line come arbitrarily many header elements, or
headers for short. These are key/value pairs, with a colon as the separator.
Under HTTP 1.1, the host header, which specifies the device address
to which the request is being sent, is required; but a half-dozen or so
headers is typical. The headers section ends with two carriage-return/
newline combinations. (Two newlines are likely to work.) A GET request
has no HTTP body, and so is complete as shown. In the start line, the
URI (Uniform Resource Identifier) is the single slash, which web servers
typically interpret as the identifier for their home page. In effect, then,
the start line and the host header make up a read request for Google’s
home page.
The write function, with the socket’s descriptor as its first argument, is
used to send the request to the server. As usual in network programming,
there is a check for an error condition: the write could fail for any number
of reasons; if it does so, there is no point in continuing. Next comes a loop
to read the server’s response. There are some subtle issues to consider, as a
closer analysis of client’s connection to the web server will indicate.
Web servers typically keep client connections alive so that repeated
request/response pairs can use the original connection. The motive, of
course, is efficiency. Also, a web server is likely to chunk its response, that
is, break the requested document (in this case, Google’s HTML home
page) into parts, transmitting each of these separately. The webclient
194
Chapter 6 Networking
program has a read buffer of about 2KB (kilobytes). On a sample run, the
program printed out this report:
The Google home page is a hefty 48K bytes, and these were fetched
in 34 separate read operations. The chunks of data from the various read
operations vary in size.
How much time should be allowed between responses from the
server? This is a question without an obvious answer. Whatever the answer,
the socket API supports a timeout on a blocking read operation, which is in
use here. For review, here are the three critical lines in the while loop that
reads the Google response:
If the blocking read operation times out, there is a signal with an aptly
named error code EWOULDBLOCK, which says that the read operation would
have continued to block except for the interrupting signal. If the blocking
times out, the program assumes that no further response bytes are coming.
Recall that read returns 0 on an end-of-byte-stream condition. In this
case too, there is a break out of the while loop. If any other nonfatal error
should occur (the -1 test), then execution of the while loop continues: the
continue statement moves control directly to the loop condition, in this
case bypassing the write operation to the standard output.
195
Chapter 6 Networking
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
196
Chapter 6 Networking
if (!next) {
fprintf(stderr, "can't find an address\n");
exit(-1);
}
freeaddrinfo(result); /* clean up storage no longer needed */
197
Chapter 6 Networking
The code in the file get_connection.c (see Listing 6-2) handles the
networking details. This code also illustrates various points made
throughout earlier chapters. At the center is the data type struct
addrinfo, which encapsulates information about an IP address. The
program declares a variable hints of this type and then initializes the
structure’s fields with information that provides hints to the library
function getaddrinfo. One hint is that the program could deal with either
an IPv4 or an IPv6 address (AF_UNSPEC for address family unspecified),
and a second hint is that the program wants a reliable connection (SOCK_
STREAM vs. SOCK_DGRAM), which is typically TCP based. Two other fields
are initialized to zero, indicating that the webclient program defers to the
library function to make the default choices.
A pointer to the hints structure is one of the arguments to library
function getaddrinfo. Here is a summary of the four arguments passed to
this function:
• The host argument is www.google.com, the symbolic IP
address.
• The port argument is 80 as a string, the standard
server-side port number for accepting HTTP
connections.
• The third argument is &hints: a pointer to the hints
structure, rather than a copy of it.
• The last argument is the pointer results of type struct
addrinfo*: the library function sets this pointer to the
address of a structure that contains the information
about available addresses for the Google server.
198
Chapter 6 Networking
199
Chapter 6 Networking
failure rate of 98%! The approach taken in the code example was crude
and inefficient and designed only to introduce the nonblocking API. The
fifoReader tried, on every loop iteration, to read whatever happened to be
available in the named pipe. But the fifoWriter paused a random amount
of time between write operations so that there was a discontinuous byte
stream from the writer to the reader. The odds were overwhelmingly
against successful nonblocking read operations by the fifoReader.
A different approach can improve the efficiency of read operations
and also make application logic easier to follow. The approach involves a
division of labor:
• A library function monitors a channel to detect whether
there are bytes to read.
• The application can query the monitor function before
even attempting a read operation: if the monitor
detects nothing to read, the application does not bother
to attempt a read operation.
200
Chapter 6 Networking
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
void main() {
fd_set fds; /* set of file descriptors */
struct timeval tv;
int flag;
char byte;
if (flag)
printf("The byte value is: %c\n", byte);
}
201
Chapter 6 Networking
202
Chapter 6 Networking
% make
% ./web_server ## on Windows: % web_server
203
Chapter 6 Networking
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>
204
Chapter 6 Networking
The servutils2.c file (see Listing 6-4) contains two utility functions. The
log_client function has one argument, a pointer to a struct in_addr
(Internet address). This structure contains information about the client,
including the client’s IP address. The log_client function calls the
library function inet_ntop (Internet name to protocol) with the structure
pointer as an argument; the library function generates a human-readable
string and puts the string in the caller-supplied buffer. If the web server
is running on localhost (127.0.0.1), and a request comes from this same
machine, then the message would be
205
Chapter 6 Networking
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define Backlog 100
206
Chapter 6 Networking
207
Chapter 6 Networking
WHAT’S CURL?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include "servutils.h" /* function declarations */
int main() {
const int port = 3000;
char request[BuffSize + 1];
memset(request, 0, sizeof(request));
struct sockaddr_in client_addr;
socklen_t len = sizeof(struct sockaddr_in);
208
Chapter 6 Networking
while (1) {
temp_set = active_set; /* make a working copy, as active_
set changes */
if (select(FD_SETSIZE, &temp_set, NULL, NULL, NULL) < 0)
/* activity? */
report_and_exit("select(...)");
int i;
for (i = 0; i < FD_SETSIZE; i++) { /* handle the
current fds */
if (!FD_ISSET(i, &temp_set)) continue; /* member of
the set? */
209
Chapter 6 Networking
The webserver program (see Listing 6-6) uses the select function and
its supporting macros such as FD_SET and FD_CLR to read client requests
and to write back responses. The salient points can be summarized as
follows:
210
Chapter 6 Networking
211
Chapter 6 Networking
% curl localhost:3000?msg=Hello,world!
212
Chapter 6 Networking
Echoing request:
GET /?msg=Hello,world! HTTP/1.1 ## GET request with a
query string
User-Agent: curl/7.35.0 ## user program is curl
Host: localhost:3000 ## localhost on port 3000
Accept: */* ## accept any MIME type/
subtype combination
Echoing request:
POST / HTTP/1.1 ## POST, not GET
User-Agent: curl/7.35.0
Host: localhost:3000
Accept: */*
Content-Length: 40 ## in bytes for
HTTP body
Content-Type: application/x-www-form-urlencoded
## POSTed form
## two newlines end the headers
name=Fred Flintstone&occupation=handyman ## body of
POST request
213
Chapter 6 Networking
214
Chapter 6 Networking
messages
Alice<------------>Bob
How does Alice know that it is Bob, and not an impostor, at the other
end? The same goes for Bob. The eavesdropper Eve might be in the
middle (man-in-the-middle attack), pretending to be both Alice and Bob,
thereby intercepting all of the messages sent in one direction or the other.
Alice and Bob need a procedure (peer authentication) so that each can
authenticate the other’s identity before any significant messages are sent
between them.
Peer authentication, as used in HTTPS, requires a key pair apiece for
Alice and Bob: a digital public key (distributable to anyone) and a digital
private key (secret to its owner). The public key is an identity. For example,
Amazon’s public key identifies Amazon, and Alice’s public key identifies
her. A public key can be embedded in a digital certificate, with a certificate
authority (CA) vouching for this key through the CA’s own digital signature
on the same certificate. For example, a CA such as VeriSign or RSA
vouches with its own digital signature that the public key on Alice’s digital
certificate indeed identifies Alice. The vouching may come with a fee,
of course.
Here is a scenario for peer authentication between Alice and Bob:
215
Chapter 6 Networking
There is, of course, a fly in this ointment. If Eve manages to get a copy
of Alice’s digital certificate and also manages to intercept an authentication
request from Bob to Alice, then Eve becomes indistinguishable from Alice.
To guard against this possibility, Bob might request from Alice several
digital certificates, each with a different signer and with different validity
dates. There also are certificates with more than one CA as a signer.
When it comes to peer authentication, there are precautions rather than
guarantees.
+----------------+
N input bits--->| message digest |--->fixed-length digest
+----------------+
216
Chapter 6 Networking
+--------------+
Alice's public key---------->| verification |--->yes or no
Alice's digital signature--->| engine |
+--------------+
Validating a CA’s digital signature requires the CA’s public key: a CA’s
public key is available on the CA’s own digital certificate, which in turn
has a digital signature as a voucher. Thus begins the verification regress.
At some point, of course, the regress stops because a digital signature is
accepted as valid.
217
Chapter 6 Networking
Certificate:
Data:
Signature Algorithm: md5WithRSAEncryption
Issuer: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte
Consulting cc,
...
CN=Thawte Server CA/[email protected]
Validity
Not Before: Aug 1 00:00:00 1996 GMT
Not After : Dec 31 23:59:59 2028 GMT
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (1024 bit)
Modulus (1024 bit):
00:d3:a4:50:6e:c8:ff:56:6b:e6:cf:5d:b6:ea:0c:
...
3a:c2:b5:66:22:12:d6:87:0d
Exponent: 65537 (0x10001)
...
Signature Algorithm: md5WithRSAEncryption
07:fa:4c:69:5c:fb:95:cc:46:ee:85:83:4d:21:30:8e:ca:d9:
...
b2:75:1b:f6:42:f2:ef:c7:f2:18:f9:89:bc:a3:ff:8a:2
3:2e:70:47
The dcert display (see Listing 6-7) shows parts from a sample digital
certificate, with Thawte as the CA. The public key algorithm is RSA, the
industry standard. The certificate also gives details about the digital
signature.
218
Chapter 6 Networking
219
Chapter 6 Networking
220
Chapter 6 Networking
Recall that the client and the server have settled on a cryptographic
suite, which includes a message digest (hash) algorithm. The sender
computes a hash of the message to be sent and sends the hash as well.
The receiver recomputes the hash locally, using the same algorithm, and
then checks whether the received hash matches the locally computed one.
Assume that the locally computed hash is correct. If the two hashes do not
match, then something in the sent message (the original message and/or
the sender’s hash) has been corrupted in transit; the message and a hash
need to be sent again.
The wcSSL program is an HTTPS client that exhibits the security
features discussed previously. The OpenSSL libraries do a nice job of
wrapping the usual HTTP client functions—create a socket, open a
connection, engage in a conversation, close the connection—within
security-enabled counterparts. The resulting flow of control is easy to
follow. For readability, the source code for wcSSL program is divided
among three files. A Makefile is included.
The three source files in the wcSSL program are as follows:
221
Chapter 6 Networking
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
The five functions declared in the header file wcSSL.h (see Listing 6-8)
can be clarified as follows:
222
Chapter 6 Networking
#include "wcSSL.h"
223
Chapter 6 Networking
224
Chapter 6 Networking
The load_SSL function in the wcSSLutils.c file (see Listing 6-9) calls
four functions from the OpenSSL API in order to load various SSL modules.
The load_SSL then calls a fifth OpenSSL function SSL_library_init to
do whatever SSL initialization is required. Any error in the initialization
would make it impossible to continue; hence, the wcSSL client exits if an
error occurs.
The view_cert function gets the X509-formatted certificate from
Google, extracts some information, and then prints this information. X509
is versioned and remains the dominant format for digital certificates;
hence, OpenSSL includes many functions with X509 in the name. Once
information about the certificate is printed, in this case only the subject
line, the heap storage for the certificate is freed. The X509_free utility
function does whatever nested freeing is required; hence, this function and
not the library function free should be called.
Throughout the wcSSL program, there are calls to various OpenSSL
functions with BIO (Basic Input/Output) in the name. The BIO library is
roughly a wrapper around the standard FILE type, and the BIO API mimics
the FILE API. However, the BIO functions have access to the all-important
SSL context, which is discussed shortly.
In working with the OpenSSL libraries, it is best practice to use the
BIO functions for any input/output operations that involve web content.
Accordingly, the wcSSL program uses the standard puts function in
report_exit but otherwise sticks with the BIO input/output functions.
For instance, the BIO_puts function is used to send the request, over an
encrypted channel, to the Google web server.
#include "wcSSL.h"
#define BuffSize 2048
int main() {
const char* host_port = "www.google.com:443";
225
Chapter 6 Networking
load_SSL();
const SSL_METHOD* method = SSLv23_method(); /* protocol
version */
if (NULL == method) report_exit("SSLv23_method()");
SSL_CTX* ctx = SSL_CTX_new(method);
/* global context for client/server */
if (NULL == ctx) report_exit("SSL_CTX_new(...)");
226
Chapter 6 Networking
int len = 0;
do { /* read chunks from Google server */
char buff[BuffSize] = { };
len = BIO_read(web, buff, sizeof(buff));
if (len > 0) BIO_write(out, buff, len);
} while (len > 0 || BIO_should_retry(web));
return 0;
}
The main file for the wcSSL program is wcSSL.c (see Listing 6-10).
Rather than analyze each OpenSSL function call separately, it may be
more useful to group the calls, focusing on what each group is meant to
accomplish. The following describes three groups in turn:
227
Chapter 6 Networking
Certificate subject:
C=US, ST=California, L=Mountain View, O=Google Inc, CN=www.
google.com
228
Chapter 6 Networking
The two link libraries (the two -l flags) are the OpenSSL library and
the standard cryptography library, respectively. In the flag at the end -I.,
the I is for include files, and the period represents the current working
directory, which means that only this directory should be searched for any
include files. In general, any search path could be specified for include files.
6.5. What’s Next?
Concurrency and parallelism are distinct but related concepts. A
concurrent program handles multiple tasks within the same time span.
For example, a concurrent web server might handle, say, 20 client
requests within a second or so. Concurrency is possible even on an old-
fashioned, single-CPU machine through time-sharing: one task gets the
CPU for a certain amount of time, and then its processing is preempted
so that another task can have a turn, and so on. A concurrent program
becomes a truly parallel one if the tasks are delegated to separate
processors so that all of tasks can be processed literally at the same time.
There is also instruction-level parallelism on modern machines; this
parallelism involves the execution of instructions that perform machine-
level operations in parallel. The next chapter fleshes out the details of
concurrency and parallelism with code examples.
229
CHAPTER 7
Concurrency and
Parallelism
7.1. Overview
A concurrent program handles more than one task at a time. A familiar
example is a web server that handles multiple client requests at the same
time. Although concurrent programs can run even on a single-processor
machine of bygone days, these programs should show a marked gain in
performance by running on a multiprocessor machine: different tasks can
be delegated to different processors. A parallel program in this sense is a
concurrent program whose tasks can be handled literally at the same time
because multiple processors are at hand.
The two traditional and still relevant approaches to concurrency are
multiprocessing and multithreading. Applications such as web servers
and database systems may mix the approaches and throw in acceleration
techniques such as nonblocking I/O. Multiprocessing has a relatively long
history and is still widespread. For example, early web servers supported
concurrency through multiprocessing; but even state-of-the-art web
servers such as Nginx are multiprocessing systems.
Recall that a process is a program in execution and that each process
has its own address space. Two processes could share a memory location,
but this requires setup: shared memory is not the default. Separate address
spaces are appealing to the programmer, who does need to worry about
memory-based race conditions when writing a multiprocessing program.
A typical race condition arises when two or more operations, at least one of
which is a write, could access the same memory location at the same time.
Of interest now is that separate processes, by default, do not share access
to a memory location, which is requisite for such a race condition.
What is the downside of multiprocessing? When the operating system
preempts a not-yet-finished process, a process-level context switch
occurs: the operating system gives the processor to another process for its
execution. The preempted process must be scheduled again to complete
its execution. A process-level context switch is expensive because the
operating system may have to swap data structures such as page tables
(virtual-to-physical address translators) between memory and disk; in
any case, there is nontrivial bookkeeping to track the state of both the
preempted and the newly executing process. It is hard to come up with an
exact figure, but a process-level context switch takes about 5ms to 15ms
(milliseconds), time that is not available for other tasks.
Recall too that a thread (short for thread of execution) is a sequence of
executable instructions. Every process has at least one thread; a process
with only one thread is single threaded, and a process with more than one
thread is multithreaded. Operating systems schedule threads to processors;
to schedule a process is, in effect, to schedule one of its threads. On a
multiprocessor machine, multiple threads from the same process can
execute at the very same time. A thread-level context switch—preempting
one thread in a process for another in the same process—is not free, but
the cost is very low: nanoseconds rather than milliseconds. Multithreading
is efficient.
In a simplifying move, Linux systems turn process scheduling into
thread scheduling by treating even a multithreaded process as if it were
single threaded. A multithreaded process with N threads then requires N
scheduling actions to cover the threads. Threads within a multithreaded
process remain related in that they share resources such as memory
232
Chapter 7 Concurrency and Parallelism
7.2. Multiprocessing Through
Process Forking
The standard library functions provide options for multiprocessing, but
the fork function is the most explicit. The first code example covers the
basics of a fork call using unnamed pipes; an earlier example (recall
Listings 5-8 and 5-9) covered named pipes. A look at unnamed pipes from
the command line serves as preparation.
At the command line, the vertical bar | represents an unnamed pipe:
to the left is the pipe writer and to the right is the pipe reader. Each is a
process. Here is a contrived example using the sleep and echo utilities
available on Unix-like systems and through Cygwin:
The greeting Hello, world! appears on the screen; then, after about five
seconds, the command-line prompt returns, signaling that both the sleep
and echo processes have exited. The pipe is closed automatically when the
233
Chapter 7 Concurrency and Parallelism
void main() {
signal(SIGCHLD, SIG_IGN); /* prevents zombie */
int n = 777; /* both parent and child have
a copy */
234
Chapter 7 Concurrency and Parallelism
The basicFork program (see Listing 7-1) opens with a call to the signal
function. This is a precaution to prevent zombie processes, as clarified in
an upcoming section. The int variable n is declared and initialized to 777.
If the subsequent call to the library function fork succeeds, both the child
and the parent process get their own separate copy of variable n; hence,
each process manages different variables with the same name.
The library function fork tries to create a new process. If the attempt
succeeds, the newly created process becomes the child of the original
process, which is now a parent. The fork function returns an integer
value; for portability, the recommended type is pid_t, where pid stands
for process identifier. The tricky part of the fork call is that, if successful, it
returns one value to the parent—but a different value to the child. A short
digression into the process id explains.
235
Chapter 7 Concurrency and Parallelism
• 0 to the child
Once forked, the child process executes a copy of the very same code
as the parent—the code that comes after the call to fork. Accordingly, a
test is typically used (in this case, the if test) to distinguish between code
intended for the child and code intended for the parent. In this example,
the child executes the if block, printing 787; the parent executes the else
block, printing 7770. The order in which the prints occur is indeterminate.
If the program runs on a multiprocessor machine, this concurrent program
can execute in a truly parallel fashion.
The second code example uses an unnamed pipe for interprocess
communication. The parent again calls fork to spawn a child process,
and the two processes then communicate through the pipe: the parent as
the writer process and the child as the reader process. The discussion also
explains zombie processes and how to reap them.
#define ReadEnd 0
#define WriteEnd 1
236
Chapter 7 Concurrency and Parallelism
void report_and_die() {
perror(NULL);
exit(-1); /** failure **/
}
void main() {
int pipeFDs[2]; /* two file descriptors */
char buf; /* 1-byte buffer */
const char* msg = "This is the winter of our discontent\n";
/* bytes to write */
The pipeUN program (see Listing 7-2) uses the fork function for
multiprocessing and the pipe function for creating an unnamed pipe so
that the processes can communicate. To begin, here is an overview of the
library function pipe:
The fork function is used to create the reader process, although this
spawned process could have been the writer. The process that does the
forking is the parent, and the forked process is the child. The child process,
an almost exact duplicate of the parent, is said to inherit from the parent.
For example, a forked child process inherits open file descriptors from the
parent. Recall that once forked, the child process executes the very same
code as the parent process, unless an if test or the equivalent is used to
divide the code that each process executes. A closer look at the example
clarifies.
238
Chapter 7 Concurrency and Parallelism
Here, for quick review and with added detail, are the values that the
fork function can return:
The else clause is thus for the parent to execute. Because the child
process is the reader, it immediately closes the WriteEnd of the pipe; in a
similar fashion, the parent process as the writer immediately closes the
ReadEnd of the pipe. Both file descriptors are open because of the call to
pipe. By closing one end of the pipe, each process exhibits the separation-
of-concerns pattern.
The writer process then writes bytes to the pipe, and the reader process
reads these bytes one at a time. When the writer process closes the pipe’s
write end, an end-of-stream marker is sent to the reader, which responds
by closing the pipe’s read end. At this point, the pipe closes down.
239
Chapter 7 Concurrency and Parallelism
240
Chapter 7 Concurrency and Parallelism
The effect of this call is to automate the reaping of a zombie. Were this
approach taken in the current example, the parent’s call to wait would not
be needed to safeguard against a zombie.
241
Chapter 7 Concurrency and Parallelism
int main() {
pid_t pid = fork(); /* try to create a child process */
if (-1 == pid) { /* did the fork() work? */
perror("fork()"); /* if not, error message and exit */
exit(-1);
}
242
Chapter 7 Concurrency and Parallelism
}
else
printf("This should not print!\n"); /* never
executes */
}
return 0;
}
The execing program (see Listing 7-3) forks a child process, which then
calls execv to execute the cline program (recall Listing 1-7). Each function
in the exec family does the following:
• The new process, in this case cline, runs with the same
pid as the original process, in this case execing.
243
Chapter 7 Concurrency and Parallelism
does not execute. Only the newly executed cline program runs to
completion: the process image for the forked child indeed has been
overlaid.
There is a short experiment that can confirm the overlay in the execing
program:
The two printed pid values should be the same, thereby confirming
that the execed program cline is executing under the forked child’s pid. The
code available on GitHub includes this experiment.
244
Chapter 7 Concurrency and Parallelism
void main() {
int status; /* parent captures child's status here */
int cret = 0xaa11bb22; /* child returns this value */
245
Chapter 7 Concurrency and Parallelism
In the exiting program (see Listing 7-4), one process forks another
in the by-now-familiar way. The parent waits for the child with a call to
waitpid, which expects three arguments:
246
Chapter 7 Concurrency and Parallelism
The first argument to waitpid (-1) means, in effect, any child of mine;
the second argument is NULL instead of a pointer to an int variable to store
the child’s exit status; and the third argument is NULL for no flags.
For the child process, there are various possibilities that a waiter such
as the parent needs to consider. Three of these possibilities are considered
in the exiting program:
In this example, the child exits normally with a call to _exit. The
WEXITSTATUS macro returns the low-order 8 bits of the child’s 32-bit
explicitly returned value, 0xaa11bb22 in hex. The macro thus extracts 22.
The exiting program also confirms that a child’s ppid is the same as
its parent’s pid. In a sample run, this value was 2613, and the child’s pid
was 2614. These values are not guaranteed to be consecutive, but it is a
common pattern: the child’s pid is one greater than the parent’s.
247
Chapter 7 Concurrency and Parallelism
There are two separate libraries and APIs for shared memory: the
legacy System V library and API, and the more recent POSIX pair. These
APIs should never be mixed in a single application, however. The POSIX
pair is still in development and dependent upon the version of the
operating system kernel, which impacts code portability. By default,
the POSIX API implements shared memory as a memory-mapped file:
for a shared memory segment, the system maintains a backing file with
corresponding contents. Shared memory under POSIX can be configured
without a backing file, but this may impact portability. My example uses
the POSIX API with a backing file, which combines the benefits of memory
access (speed) and file storage (persistence).
The shared memory example has two programs, named memwriter and
memreader, and uses a semaphore to coordinate their access to the shared
memory. Whenever shared memory comes into the picture with a writer, so
does the risk of a memory-based race condition with indeterminate results;
hence, the semaphore is used to coordinate (synchronize) access to the
shared memory so that the writer and the reader operations do not overlap.
The memwriter program, which creates the shared memory segment,
should be started first in its own terminal. The memreader program then
can be started (within a dozen seconds) in its own terminal. The output
from the memreader is
248
Chapter 7 Concurrency and Parallelism
int main() {
int fd = shm_open(BackingFile, /* name from smem.h */
O_RDWR | O_CREAT, /* read/write, create if
needed */
AccessPerms); /* access permissions
(0644) */
249
Chapter 7 Concurrency and Parallelism
250
Chapter 7 Concurrency and Parallelism
/* clean up */
munmap(memptr, ByteSize); /* unmap the storage */
close(fd);
sem_close(semptr);
shm_unlink(BackingFile); /* unlink from the backing file */
return 0;
}
allocates ByteSize bytes, in this case, a modest 512 bytes. The memwriter
and memreader programs access the shared memory only, not the backing
file. The system is responsible for synchronizing the shared memory and
the backing file.
The memwriter then calls the mmap library function
251
Chapter 7 Concurrency and Parallelism
then the writing can proceed. The SemaphoreName (any unique nonempty
name will do) identifies the semaphore in both the memwriter and the
memreader. The initial value of zero gives the semaphore’s creator (in
this case, the memwriter) the right to proceed (in this case, to the write
operation).
After writing, the memwriter increments the semaphore value to 1:
if (sem_post(semptr) < 0)
252
Chapter 7 Concurrency and Parallelism
This bars the memwriter from further access to the shared memory.
int main() {
int fd = shm_open(BackingFile, O_RDWR, AccessPerms);
/* empty to begin */
if (fd < 0) report_and_exit("Can't get file descriptor...");
253
Chapter 7 Concurrency and Parallelism
/* cleanup */
munmap(memptr, ByteSize);
close(fd);
sem_close(semptr);
unlink(BackingFile);
return 0;
}
254
Chapter 7 Concurrency and Parallelism
In both the memwriter and memreader (see Listing 7-6) programs, the
shared memory functions of primary interest are shm_open and mmap: on
success, the first call returns a file descriptor for the backing file, which
the second call then uses to get a pointer to the shared memory segment.
The calls to shm_open are similar in the two programs except that the
memwriter program creates the shared memory, whereas the memreader
only accesses this already allocated memory:
With a file descriptor in hand, the calls to mmap are the same:
The first argument to mmap is NULL, which means that the system
determines where to allocate the memory in virtual address space. It is
possible (but tricky) to specify an address instead. The MAP_SHARED flag
indicates that the allocated memory is shareable among processes, and
the last argument (in this case, zero) means that the offset for the shared
memory should be the first byte. The size argument specifies the number
of bytes to be allocated (in this case, 512), and the protection argument
indicates that the shared memory can be written and read.
When the memwriter program executes successfully, the system
creates and maintains the backing file; on my system, the file is /dev/shm/
shMemEx, with shMemEx as my name (given in the header file shmem.h)
for the shared storage. In the current version of the memwriter and
memreader programs, the statement
255
Chapter 7 Concurrency and Parallelism
removes the backing file. If the unlink statement is omitted, then the
backing file persists after the program terminates.
The memreader, like the memwriter, accesses the semaphore through
its name in a call to sem_open. But the memreader then goes into a wait
state until the memwriter increments the semaphore, whose initial
value is 0:
Once the wait is over, the memreader reads the ASCII bytes from the
shared memory, cleans up, and terminates.
The shared memory API includes operations explicitly to synchronize
the shared memory segment and the backing file. These operations have
been omitted from the example to reduce clutter and keep the focus on the
memory-sharing and semaphore code.
The memwriter and memreader programs are likely to execute without
inducing a race condition even if the semaphore code is removed: the
memwriter creates the shared memory segment and writes immediately
to it; the memreader cannot even access the shared memory until this
has been created. However, best practice requires that shared memory
access is synchronized whenever a write operation is in the mix, and the
semaphore API is important enough to be highlighted in a code example.
256
Chapter 7 Concurrency and Parallelism
simple case in which one process (producer) creates and writes to a file
and another process (consumer) reads from this same file:
writes +-----------+ reads
producer-------->| disk file |<-------consumer
+-----------+
257
Chapter 7 Concurrency and Parallelism
The standard I/O library includes a utility function named fcntl that
can be used to inspect and manipulate both exclusive and shared locks
on a file. The function works through the by-now-familiar file descriptor,
a nonnegative integer value that, within a process, identifies a file. (Recall
that different file descriptors in different processes may identify the same
physical file.) For file locking, Linux provides the library function flock,
which is a thin wrapper around fcntl. The code examples use the fcntl
function to expose API details.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
struct flock lock;
lock.l_type = F_WRLCK; /* read/write (exclusive versus
shared) lock */
lock.l_whence = SEEK_SET; /* base for seek offsets */
lock.l_start = 0; /* 1st byte in file */
lock.l_len = 0; /* 0 here means 'until EOF' */
lock.l_pid = getpid(); /* process id */
258
Chapter 7 Concurrency and Parallelism
The main steps in the producer program (see Listing 7-7) can be
summarized as follows. The program declares a variable of type struct
flock, which represents a lock, and initializes the structure’s five fields.
The first initialization
259
Chapter 7 Concurrency and Parallelism
tries to lock the file exclusively, checking whether the call succeeded. In
general, the fcntl function returns -1 (hence, less than zero) to indicate
failure. The second argument F_SETLK means that the call to fcntl does
not block: the function returns immediately, either granting the lock or
indicating failure. If the flag F_SETLKW (the W at the end is for wait) were
used instead, the call to fcntl would block until gaining the lock was
possible. In the calls to fcntl, the first argument fd is the file descriptor,
the second argument specifies the action to be taken (in this case, F_SETLK
for setting the lock), and the third argument is the address of the lock
structure (in this case, &lock).
If the producer gains the lock, the program writes two text records to
the file. After writing to the file, the producer changes the lock structure’s
l_type field to the unlock value:
lock.l_type = F_UNLCK;
and calls fcntl to perform the unlocking operation. The program finishes
up by closing the file and exiting.
260
Chapter 7 Concurrency and Parallelism
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
struct flock lock;
lock.l_type = F_WRLCK; /* read/write (exclusive) lock */
lock.l_whence = SEEK_SET; /* base for seek offsets */
lock.l_start = 0; /* 1st byte in file */
lock.l_len = 0; /* 0 here means 'until EOF' */
lock.l_pid = getpid(); /* process id */
261
Chapter 7 Concurrency and Parallelism
close(fd);
return 0;
}
lock.l_type = F_WRLCK;
...
fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no
write lock */
if (lock.l_type != F_UNLCK)
report_and_exit("file is still write locked...");
262
Chapter 7 Concurrency and Parallelism
The F_GETLK operation specified in the fcntl call checks for a lock, in
this case, an exclusive lock given as F_WRLCK in the first statement earlier. If
the specified lock does not exist, then the fcntl call automatically changes
the lock type field to F_UNLCK to indicate this fact. If the file is exclusively
locked, the consumer terminates. (A more robust version of the program
might have the consumer sleep a bit and try again several times.)
If the file is not currently locked, then the consumer tries to gain a
shared (read-only) lock (F_RDLCK). To shorten the program, the F_GETLK
call to fcntl could be dropped because the F_RDLCK call would fail if a
read-write lock already were held by some other process. Recall that a
read-only lock does prevent any other process from writing to the file but
allows other processes to read from the file. In short, a shared lock can
be held by multiple processes. After gaining a shared lock, the consumer
program reads the bytes one at a time from the file, prints the bytes to the
standard output, releases the lock, closes the file, and terminates.
Here is the output from the two programs launched from the same
terminal:
% ./producer
Process 29255 has written to data file...
% ./consumer
Now is the winter of our discontent
Made glorious summer by this sun of York
263
Chapter 7 Concurrency and Parallelism
+-+ +-+ +-+ +-+
sender--->|3|--->|2|--->|2|--->|1|--->receiver
+-+ +-+ +-+ +-+
Of the four messages shown, the one labeled 1 is at the front, that is,
closest to the receiver. Next come two messages with label 2, and finally, a
message labeled 3 at the back. If strict FIFO behavior were in play, then the
messages would be received in the order 1-2-2-3. However, the message
queue allows other retrieval orders. For example, the messages could be
retrieved by the receiver in the order 3-2-1-2.
The mqueue example consists of two programs: the sender that writes
to the message queue and the receiver that reads from this queue. Both
programs include the header file queue.h shown in Listing 7-9.
264
Chapter 7 Concurrency and Parallelism
typedef struct {
long type; /* must be of type long */
char payload[MsgLen + 1]; /* bytes in the message */
} queuedMessage;
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
265
Chapter 7 Concurrency and Parallelism
#include <string.h>
#include "queue.h"
int main() {
key_t key = ftok(PathName, ProjectId);
if (key < 0) report_and_exit("couldn't get key...");
266
Chapter 7 Concurrency and Parallelism
The preceding sender program sends out six messages, two each of a
specified type: the first messages are of type 1, the next two of type 2, and
the last two of type 3. The sending statement
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include "queue.h"
int main() {
key_t key= ftok(PathName, ProjectId); /* key to identify the
queue */
if (key < 0) report_and_exit("key not gotten...");
267
Chapter 7 Concurrency and Parallelism
int i;
for (i = 0; i < MsgCount; i++) {
queuedMessage msg; /* defined in queue.h */
if (msgrcv(qid, &msg, MsgLen + 1, types[i], MSG_NOERROR |
IPC_NOWAIT) < 0)
puts("msgrcv trouble...");
printf("%s received as type %i\n", msg.payload, (int)
msg.type);
}
return 0;
}
The receiver program does not create the message queue, although the
API suggests as much. In the receiver, the call
is misleading because of the IPC_CREAT flag, but this flag really means
create if needed, otherwise access. The sender program calls msgsnd to
send messages, whereas the receiver calls msgrcv to retrieve them. In this
example, the sender sends the messages in the order 1-1-2-2-3-3, but the
receiver then retrieves them in the order 3-1-2-1-3-2, showing that message
queues are not bound to strict FIFO behavior:
% ./sender
msg1 sent as type 1
msg2 sent as type 1
msg3 sent as type 2
msg4 sent as type 2
268
Chapter 7 Concurrency and Parallelism
% ./receiver
msg5 received as type 3
msg1 received as type 1
msg3 received as type 2
msg2 received as type 1
msg6 received as type 3
msg4 received as type 2
The preceding output shows that the sender and the receiver can be
launched from the same terminal. The output also shows that the message
queue persists even after the sender process creates the queue, writes to
it, and exits. The queue goes away only after the receiver process explicitly
removes the queue with the call to msgctl:
7.7. Multithreading
Recall that a multithreaded process has multiple threads (sequences) of
executable instructions, which can be executed concurrently and, on a
multiprocessor machine, in parallel. Multithreading, like multiprocessing,
is a way to multitask. Multithreading has the upside of efficiency
because thread-level context switches are quite fast but the downside of
challenging the programmer with the twin perils of race conditions and
deadlock. Code examples go into detail. To begin, an example of pthread
(the standard thread library) basics should be helpful.
269
Chapter 7 Concurrency and Parallelism
#define ThreadCount 4
void main() {
pthread_t threads[ThreadCount];
unsigned long i;
for (i = 0; i < ThreadCount; i++) {
/* four args: pointer to pthread_t instance, attributes,
start function,
and argument passed to start function */
int flag = pthread_create(threads + i, /* 0 on success */
NULL,
greet,
(void*) i + 1);
if (flag < 0) {
perror(NULL);
270
Chapter 7 Concurrency and Parallelism
exit(-1);
}
}
puts("main exiting...");
pthread_exit(NULL); /* allows other threads to continue
execution */
}
The multiT program (see Listing 7-12) has five threads in all: the main
thread, which executes the body of main, and four additional threads that
main creates through calls to the library function pthread_create. The
pthread_create function takes four arguments:
271
Chapter 7 Concurrency and Parallelism
All four of the created threads execute the same code, the body of
the greet function, but no race condition arises. Arguments passed to a
function, and local (auto or register) variables within the function, are
thereby thread-safe because each thread gets its own copies. If a variable
is neither extern nor static, then it represents a thread-safe memory
location.
The pthread_create function returns -1 to signal an error and 0 to
signal success. A successfully created thread is ready to be scheduled for
execution on a processor.
At the end of main, the multiT program calls the library function
pthread_exit with an argument of NULL. The address of an int exit-status
variable also could be used as the argument. This call from main allows
other threads to continue executing. On a sample run, for instance, the
output began:
272
Chapter 7 Concurrency and Parallelism
WHAT’S POSIX?
The Portable Operating System Interface is a family of standards from the IEEE
Computer Society meant to encourage compatibility among operating systems.
The multithreading examples use pthreads, where the p stands for POSIX.
273
Chapter 7 Concurrency and Parallelism
274
Chapter 7 Concurrency and Parallelism
shared bank account: both threads access the same account. A memory-
based race condition requires contention for a shared memory location. Of
course, the account variable could be extern rather than static without
changing the program’s behavior.
The miser (saver) and the spendthrift (spender) are implemented as
two separate threads, each with uncoordinated access to the account.
To highlight the race condition, the miser and the spendthrift update the
balance the same number of times, given as a command-line argument.
Here is a depiction of what goes on in the miserSpend program:
increment +---------+ decrement
miser----------->| account |<-----------spendthrift ## updates are done many times
+---------+
275
Chapter 7 Concurrency and Parallelism
The code for the saveSpend program is divided into two parts for
readability. The first part (see Listing 7-13) has the main thread create and
then start two other threads: the miser and the spendthrift threads. Each
created thread is of type pthread_t, and the pthread_create function can
be reviewed as follows:
276
Chapter 7 Concurrency and Parallelism
If the miser already has exited, the first call to pthread_join returns
immediately; if not, the call returns when the miser does exit. The second
argument to pthread_join can be used to get the exit status of the thread
given as the second argument; in this case, the status is ignored with NULL
as the second argument. The two calls to pthread_join ensure that the
main thread prints the final balance—the balance after the other two
threads have terminated.
277
Chapter 7 Concurrency and Parallelism
void update(int n) {
account += n; /** critical section **/
}
The second part of the saveSpend program (see Listing 7-14) has the
two start functions for the created threads: deposit (miser) and w ithdraw
(spendthrift). Each of these functions takes, as its single argument, the
number of times to perform an account update, implemented as the
update function: the deposit function calls update with 1 as the argument,
whereas the withdraw function calls update with -1 as the argument.
278
Chapter 7 Concurrency and Parallelism
279
Chapter 7 Concurrency and Parallelism
void update(int n) {
if (0 == pthread_mutex_lock(&lock)) {
account += n; /** critical section **/
pthread_mutex_unlock(&lock);
}
}
...
pthread_join(spendt, NULL);
pthread_mutex_destroy(&lock); /** added **/
280
Chapter 7 Concurrency and Parallelism
7.8. Deadlock in Multithreading
Deadlock can occur in either a multiprocessing or multithreading. In the
multithreading context, deadlock can occur with just two threads: T1 and
T2. To access a shared resource R, either T1 or T2 must hold two locks (L1
and L2) at the same time. Suppose the two threads try to access R, with T1
managing to grab lock L1 and T2 managing to grab lock L2. Each thread
281
Chapter 7 Concurrency and Parallelism
now waits indefinitely for the other to release its held lock—and deadlock
results. Deadlock is usually inadvertent, of course, but the next code
example tries to cause deadlock.
282
Chapter 7 Concurrency and Parallelism
void* thread1() {
grab_locks("thread1", "lock1", "lock2", &lock1, &lock2);
/* lock1...lock2 */
return NULL;
}
void* thread2() {
grab_locks("thread2", "lock2", "lock1", &lock2, &lock1);
/* lock2...lock1 */
return NULL;
}
void main(){
pthread_t t1, t2;
pthread_create(&t1, NULL, thread1, NULL); /* start
thread 1 */
pthread_create(&t2, NULL, thread2, NULL); /* start
thread 2 */
pthread_join(t1, NULL); /* wait for thread 1 */
pthread_join(t2, NULL); /* wait for thread 2 */
printf("Number: %i (Unlikely to print...)\n", resource);
}
The deadlock program (see Listing 7-16) is likely but not certain to
deadlock. Although deadlock is intended, the code still might execute in
such a way that deadlock does not occur. On a sample run, however, the
deadlock program produced this output:
283
Chapter 7 Concurrency and Parallelism
284
Chapter 7 Concurrency and Parallelism
7.9. SIMD Parallelism
The acronym SIMD was introduced in the mid-1960s as part of Flynn’s
taxonomy for parallel computing. SIMD stands for single instruction,
multiple data stream. Flynn’s taxonomy introduces other acronyms
(e.g., MIMD for multiple instruction, multiple data stream) to describe
additional approaches to parallel computation. This section focuses on
SIMD parallelism.
285
Chapter 7 Concurrency and Parallelism
286
Chapter 7 Concurrency and Parallelism
#include <stdio.h>
#define Length 8
typedef double doubleV8 __attribute__ ((vector_size (Length *
sizeof(double)))); /** critical **/
void main() {
doubleV8 dataV1 = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8};
/* no square brackets on dataV1 */
doubleV8 dataV2 = {4.4, 6.6, 1.1, 3.3, 5.5, 2.2, 3.3, 5.5};
/* no square brackets on dataV2 */
int i;
for (i = 0; i < Length; i++)
printf("%f ", add[i]); /* 5.500000 8.800000 4.400000
7.700000 11.000000 8.800000 11.000000 14.300000 */
287
Chapter 7 Concurrency and Parallelism
putchar('\n');
for (i = 0; i < Length; i++)
printf("%f ", mul[i]); /* 4.840000 14.520000 3.630000
14.520000 30.250000 14.520000
25.410000 48.400000 */
putchar('\n');
for (i = 0; i < Length; i++)
printf("%f ", div[i]); /* 0.250000 0.333333 3.000000
1.333333 1.000000 3.000000 2.333333
1.600000 */
putchar('\n');
}
The simd program (see Listing 7-17) has a typedef that triggers the C
compiler to use native SIMD instructions and the supporting architectural
components, in particular SIMD registers. The typedef makes doubleV8
an alias for a double vector by using a special attribute:
doubleV8 dataV1 = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8};
288
Chapter 7 Concurrency and Parallelism
The vectors dataV1 and dataV2 can be used with indexes, as the three
loops near the end of the code illustrate:
7.10. What’s Next?
The next chapter covers miscellaneous topics to provide a better sense of
the libraries available in C, both standard and third party. There is also
a section on building software libraries from scratch. As usual, the code
examples highlight the power and flexibility of C.
The forthcoming code examples cover regular expressions for pattern
matching and data validation; assertions for enforcing conditions in code
modules; locale management for internationalization; the compilation of
C code into WebAssembly for high-performance web modules; signals for
interprocess communication; and the building, deployment, and use (by
both C and Python clients) of software libraries.
289
CHAPTER 8
Miscellaneous Topics
8.1. Overview
This chapter introduces libraries and topics not seen so far, but it also
extends and refines the coverage of earlier material. For example, the
flexible library function system, for quick multiprocessing, is introduced;
the input function scanf is examined more closely.
The chapter begins with regular expressions, a language designed for
pattern matching, which makes the language well suited for verifying
input. Indeed, professional data validation relies on regular expressions
as a base level. The chapter then moves to assertions, which allow the
programmer to express and enforce constraints in a program. A section
on locales and internationalization follows. Short code examples and full
programs get into the details.
WebAssembly is a language designed for high-performance web
modules, for example, ones that do serious number crunching. C is among
the earliest languages (the others are C++ and Rust) to compile into
WebAssembly. This section goes into detail with an full code example.
A signal is a low-level but still powerful way for one process to
communicate with another, and C has an API for generating and handling
signals. The section on signals is code oriented as usual.
8.2. Regular Expressions
The regular expression language, or regex for short, is used to match
strings against patterns and even for editing strings. Users of command-
line utilities such as grep (short for grab regular expression) or rename
already have experience with regex. In web and other applications, regex
verification of user input is best practice; modern programming languages
typically support regex. The first code example prompts a user for an
employee ID and then checks whether the entered string matches a
pattern that validates IDs.
#include <stdio.h>
#include <regex.h>
#define MaxBuffer 64
void main() {
char input[MaxBuffer];
char error[MaxBuffer + 1]; /* null terminator */
printf("Employee Id: ");
scanf("%7s", input); /* read only 7 chars */
292
Chapter 8 Miscellaneous Topics
int flag;
if ((flag = regcomp(®ex_comp, regex, REG_EXTENDED)) < 0) {
/* compile regex */
regerror(flag, ®ex_comp, error, MaxBuffer);
fprintf(stderr, "Error compiling '%s': %s\n", regex, error);
return;
}
The empId program (see Listing 8-1) prompts the user for an employee
ID and then reads the entered ID using scanf:
The 7 in the format string %7s ensures that no more than seven
characters are scanned into the buffer named input, which has room for
64 in any case.
The program then compiles a regex pattern given as a string. This
pattern is the most complicated part of the program and so deserves
careful analysis. The pattern consists of three parts, and each part consists
of a set and a count. For now, ignore the start character ^ and the end
character $; these are covered shortly.
The first set/count pair is
[A-Z]{2}
293
Chapter 8 Miscellaneous Topics
[1234]
[2143]
In the empId program, the members of the first set are the uppercase
letters A,B,…,Z. These letters could be enumerated in the square brackets
and in any order—a tedious undertaking. The regex language thus has a
shortcut: [A-Z] means the uppercase letters A through Z.
Immediately after the set [A-Z] comes the count (quantifier) of how
many characters from the set are required. The count occurs in braces:
The third part of the pattern requires two lowercase letters, but in the
range of a through k:
294
Chapter 8 Miscellaneous Topics
foobarAB123bb9876
would pass muster because the substring AB123bb matches the pattern
without the anchors. The anchored expression requires that the ID start
with an uppercase letter and end with a lowercase one.
The employee ID pattern as a string is compiled using the library
function regcomp, which creates a regex_t instance if successful. The
compiled pattern is used in matches. The last argument to regcomp is REG_
EXTENDED, which enables various POSIX extensions to the original regex
library. There is also a C library that supports Perl syntax and features (see
www.pcre.org/), which has become the de facto standard for regex syntax.
Once the pattern is compiled, it can be used in a call to regexec, which
matches the pattern against an input string. The call takes five arguments:
The first two arguments are the address of the compiled pattern and
the string to test against the pattern, which in this case is the user input.
The next two arguments, 0 and NULL, are for capture groups: parts of the
string to be tested can be captured for later reference. In this example, the
capture option is not needed; hence, the number of capture groups is 0,
and then there is NULL instead of an array in which to save the captures. A
later example illustrates captures. The last argument consists of optional
295
Chapter 8 Miscellaneous Topics
integer flags, for example, a flag to ignore case when matching letters. In
this example, there are no flags, which 0 represents.
The empId program works as advertised. For example, it accepts
AQ431af as an employee ID but rejects AQ431mf (m is not between a and
k, inclusive) and AQ444kk7 (ends with a digit, not a letter).
A first experience with regex syntax may seem daunting, but a
rhetorical question puts the challenge into perspective: Would it be easier
to learn regex, or to write a program from scratch that does what the empId
example requires? Regular expressions are not always intuitive, but they
make up for this shortcoming with their power and flexibility.
#include <stdio.h>
#include <unistd.h>
#include <regex.h>
#define MaxBuffer 128
#define GroupCount 4 /* entire expression counts as one group
by default */
void main() {
char error[MaxBuffer + 1];
char* inputs[ ] = {"AABC123dd95", "Az4321jb81", "QQ987ii4",
"QQ98ii4", "YTE987ef4", "ARNQ999kk6", NULL};
296
Chapter 8 Miscellaneous Topics
unsigned i = 0, j;
while (inputs[i]) { /* iterate over the inputs */
regmatch_t groups[GroupCount]; /* for extracting
substrings */
if (REG_NOMATCH == regexec(®ex_comp, inputs[i],
GroupCount, groups, 0))
fprintf(stderr, "\t%s is not a valid employee ID.\n",
inputs[i]);
else {
fprintf(stdout, "\nValid employee ID. %i parts
follow:\n", GroupCount);
for (j = 0; j < GroupCount; j++) {
if (groups[j].rm_so < 0) break;
write(1, inputs[i] + groups[j].rm_so, groups[j].rm_eo -
groups[j].rm_so);
write(1, "\n", 1);
}
printf("-----");
}
i++; /* loop counter */
}
regfree(®ex_comp); /* good idea to clean up */
}
The empId2 program (see Listing 8-2) adds features to the original
empId program. The new features can be summarized as follows:
297
Chapter 8 Miscellaneous Topics
The anchors remain, but the end requirement for one or more
decimal digits is new. The other major change is the use of parenthesized
subexpressions, each of which represents a group that is captured for later
analysis.
The major change in the rest of the code has to do with group captures.
The code declares an array:
typedef struct {
regoff_t rm_so; /* start offset */
regoff_t rm_eo; /* end offset */
} regmatch_t;
298
Chapter 8 Miscellaneous Topics
write(1, /* stdout */
inputs[i] + groups[j].rm_so, /* start */
groups[j].rm_eo - groups[j].rm_so); /* length */
The first argument to write is, of course, the standard output. The
second argument takes the base address of a test string (for instance,
inputs[0] is the string AABC123dd95) and adds the start offset (rm_so,
which is 0, 4, or 7). The third argument to write is the captured part’s
length: the end index (one beyond the end of the part) minus the start
index. The output for parsing the first two candidate IDs is
The standard C library for regex covers the basics but does not include
newer features such as lookaheads. These features make it easier or more
efficient to do pattern matching that still can be done without them. The
previously mentioned PCRE (Perl Compatible Regular Expressions) library
is an option for such newer features.
299
Chapter 8 Miscellaneous Topics
8.3. Assertions
An assertion checks whether a program satisfies a condition at a specified
point in its execution. There are three traditional types of assertion that
can be used to check a program module such as a C block:
• An assertion expressing a precondition, which must
hold at the start of a block
#include <stdio.h>
#include <regex.h>
#include <assert.h>
#define MaxBuffer 64
#define MaxTries 3
void main() {
const char* regex_s = "^[A-Z]{2,4}[1-9]{3}[a-k]{2}[0-1]?$";
regex_t regex_c;
300
Chapter 8 Miscellaneous Topics
char id[MaxBuffer];
unsigned tries = 0, flag = 0;
assert(0 == tries); /* precondition */
do {
assert(tries < MaxTries); /* invariant */
printf("Employee Id: ");
scanf("%10s", id);
if (check_id(id, ®ex_c)) {
flag = 1;
break;
}
tries++;
} while (tries < MaxTries);
The verifyEmp program (see Listing 8-3) builds on the earlier empId
program, in particular by using a regex to verify an employee’s ID. The
regex itself has changed a little in order to show more aspects of the
language:
301
Chapter 8 Miscellaneous Topics
and the user failed to provide a valid ID, the program would abort, and the
error message from the failed assertion would be
The 24 represents line 24 in the source code, the assertion immediately
after the do:
The verifyEmp program has three assertions, each with a different test:
302
Chapter 8 Miscellaneous Topics
303
Chapter 8 Miscellaneous Topics
8.4. Locales and i18n
Date, currency, and other information should be formatted in a locale-
aware way as part of i18n programming, where i18n abbreviates
internationalization. (The skeptic should count the letters between the i
and the n.) Consider, for example, this large number formatted in a way
familiar to North Americans:
1,234,567,891.234
1 234 567.891,234
304
Chapter 8 Miscellaneous Topics
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void main () {
int i = 0;
while (environ[i]) printf("%s\n", environ[i++]);
char cmd[32];
strcpy(cmd, "locale -a");
int status = system(cmd);
printf("\n%s exited with %i\n", cmd, status);
}
The environ program (see Listing 8-4) shows two ways to access
environment information. The first way uses the extern variable environ,
an array of strings each with a key=value format. Here, for example, are
two entries from my desktop system: the first key/value pair provides
information about the terminal and the second about the shell language.
TERM=xterm
SHELL=/bin/bash
305
Chapter 8 Miscellaneous Topics
C
C.UTF-8
en_AG.utf8
en_AU.utf8
...
306
Chapter 8 Miscellaneous Topics
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main () {
setlocale(LC_ALL, ""); /* set current locale for library
functions */
char* prev_locale = setlocale(LC_ALL, NULL);
/* with NULL, a getter, not a setter */
char* saved_locale = strdup(prev_locale);
/* get a separate copy */
307
Chapter 8 Miscellaneous Topics
The localeBasics program (see Listing 8-5) opens with two calls to
library function setlocale, but the calls are quite different. The first call
has the empty string, hence non-NULL, as its second argument:
308
Chapter 8 Miscellaneous Topics
The program then uses the strdup function (string duplicate) to make
an altogether separate copy of this string just in case there are further calls
to setlocale. Note that setlocale returns a pointer to a string, not a copy
of this string.
The program ends by resetting the locale to the saved_locale. The
save/restore pattern is common in locale-aware programs.
In the middle, the localeBasics program calls the library function
localeconv to get a pointer to a structure that contains information in
all of the locale categories. This structure is displayed shortly. For now,
the pointer loc is used to access the currency symbol, first for the United
States and then for Great Britain. The output is
At the end, the program resets the locale to the original one:
typedef struct {
char *decimal_point;
char *thousands_sep;
char *grouping;
char *int_curr_symbol;
char *currency_symbol;
char *mon_decimal_point;
char *mon_thousands_sep;
char *mon_grouping;
309
Chapter 8 Miscellaneous Topics
char *positive_sign;
char *negative_sign;
char int_frac_digits;
char frac_digits;
char p_cs_precedes;
char p_sep_by_space;
char n_cs_precedes;
char n_sep_by_space;
char p_sign_posn;
char n_sign_posn;
} lconv;
310
Chapter 8 Miscellaneous Topics
The fields in the lconv structure are numerous, and there are
connections among many of them. The connections may not be evident.
Accordingly, these fields are divided into seven categories, with macros to
define each category (see Table 8-1). The categories make it easier to set
related pieces of locale information.
A typical call to function setlocale uses the LC_ALL category as the
first argument:
The next code example puts the LC_MONETARY category to use. The
program first sets all locale categories (LC_ALL) to local settings. The
program then resets LC_MONETARY only to get locale-specific currency
information from six English-speaking regions around the world.
void main () {
setlocale(LC_ALL, ""); /* set all categories to default
locale */
char* regions[ ] = {"en_AU.utf-8", "en_CA.utf-8",
"en_GB.utf-8", "en_US.utf-8", "en_NZ.utf-8",
"en_ZM.utf-8", NULL};
311
Chapter 8 Miscellaneous Topics
int i = 0;
while (regions[i]) {
setlocale(LC_MONETARY, regions[i]); /* change the locale */
const struct lconv* loc = localeconv();
printf("Region: %s Currency symbol: %s International
currency symbol: %s\n",
regions[i], loc->currency_symbol, loc->int_curr_
symbol);
i++;
}
}
The output from the locMonetary program (see Listing 8-8) shows the
region, currency symbol, and international currency acronym for the six
regions.
8.5. C and WebAssembly
WebAssembly is a language well-suited for compute-bound tasks (e.g.,
number crunching) executed on a browser. All rumors to the contrary, the
WebAssembly language is not meant to replace JavaScript, but rather to
supplement JavaScript by providing better performance on CPU-intensive
tasks that JavaScript otherwise might perform. JavaScript remains the glue
that ties together HTML pages and WebAssembly modules:
download +-------+ translate
wasm module---------->|browser|----------->fast machine code
+-------+
313
Chapter 8 Miscellaneous Topics
Both the parameter n and the returned value are explicitly typed as
int. The equivalent function is asm.js would be
JavaScript, in general, does not have explicit data types, but a bitwise-
OR operation in JavaScript yields an integer value. This explains the
otherwise pointless bitwise-OR operation:
314
Chapter 8 Miscellaneous Topics
3N + 1 if N is odd
hstone(N) =
N/2 if N is even
24,12,6,3,10,5,16,8,4,2,1,4,2,1,...
315
Chapter 8 Miscellaneous Topics
It takes ten calls for the sequence to converge to 1, at which point the
sequence of 4,2,1 repeats indefinitely: (3x1)+1 is 4, which is halved to yield
2, which is halved to yield 1, and so on. The Wikipedia page (https://
en.wikipedia.org/wiki/Collatz_conjecture) goes into technical detail
on the hailstone function, including a clarification of the name hailstone.
Note that powers of two (2N) converge quickly to 1, requiring just N
divisions by two to reach 1. For example, 32 (25) has a convergence length
of five, and 64 (26) has a convergence length of six. A hailstone sequence
converges to 1 if and only if the sequence generates a power of two. At issue,
therefore, is whether a hailstone sequence inevitably generates a power of two.
The Collatz conjecture is that a hailstone sequence converges to 1 no
matter what the initial argument N > 0 happens to be. No one has found a
counterexample to the Collatz conjecture, nor has anyone come up with
a proof to elevate the conjecture to a theorem. The conjecture, simple as
it is to test with a program, remains a profoundly challenging problem
in mathematics. My hstone example generates hailstone sequences and
counts the number of steps required for a sequence to hit the first 1.
316
Chapter 8 Miscellaneous Topics
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int hstone(int n) {
int len = 0;
while (1) {
if (1 == n) break; /* halt on 1 */
if (0 == (n & 1)) n = n / 2; /* if n is even */
else n = (3 * n) + 1; /* if n is odd */
len++; /* increment counter */
}
return len;
}
#define HowMany 8
int main() {
srand(time(NULL)); /* seed random number generator */
int i;
puts(" Num Steps to 1");
Num Steps to 1
64 6
317
Chapter 8 Miscellaneous Topics
40 8
86 30
16 4
30 18
47 104
12 9
60 19
However, the WebAssembly module does not require the main function
because JavaScript could invoke the hstone function directly. The hstone
program can be simplified by dropping the main function in the hstoneCL
version.
The hstoneWA revision (see Listing 8-10) drops main and adds the
directive EMSCRIPTEN_KEEPALIVE to the hstone function. This directive
informs the compiler that the C function named hstone should be
exposed, under the same name, as a WebAssembly function.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emscripten/emscripten.h>
318
Chapter 8 Miscellaneous Topics
int len = 0;
while (1) {
if (1 == n) break; /* halt on 1 */
if (0 == (n & 1)) n = n / 2; /* if n is even */
else n = (3 * n) + 1; /* if n is odd */
len++; /* increment counter */
}
return len;
}
The flag --no-entry indicates that the file hstoneWA.c does not
contain the function main, and the -o flag stands for output: the resulting
WebAssembly file is named hstone.wasm. On my desktop machine, this file
is a trim 662 bytes in size.
For testing, the next requirement is an HTML page that, when
downloaded to a browser, fetches the WebAssembly module. A
production-grade version of the HTML page would include embedded
JavaScript calls to appropriate WebAssembly functions. A handcrafted
version of the HTML page reveals details that otherwise remain hidden.
Here is an HTML page that downloads and prepares the WebAssembly
module stored in the hstone.wasm file:
319
Chapter 8 Miscellaneous Topics
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script>
fetch('hstone.wasm').then(response => <!-- Line 1 -->
response.arrayBuffer() <!-- Line 2 -->
).then(bytes => <!-- Line 3 -->
WebAssembly.instantiate(bytes, {imports: {}})
<!-- Line 4 -->
).then(results => { <!-- Line 5 -->
window.hstone = results.instance.exports.hstone;
<!-- Line 6 -->
});
</script>
</head>
<body/>
</html>
320
Chapter 8 Miscellaneous Topics
The script’s Line 6 exports the original C function hstone under the
same name. This WebAssembly function is available now to any JavaScript
code, as a session in the browser’s JavaScript console confirms. Here is part
of my test session in Chrome’s JavaScript console:
The outputs are the steps required to reach 1 from the input (e.g.,
hstone(27) requires 111 steps to reach 1).
WebAssembly now has a more concise API for fetching and
instantiating a module; the new API reduces the preceding script to only
the fetch and instantiate operations. The longer version shown previously
has the benefit of exhibiting details, in particular the representation of a
WebAssembly module as a byte array that gets instantiated as an object
with exported functions.
Emscripten comes with a test server, which can be invoked as follows
to host the handcrafted HTML file hstone.html and the WebAssembly file
hstone.wasm:
321
Chapter 8 Miscellaneous Topics
322
Chapter 8 Miscellaneous Topics
8.6. Signals
A signal interrupts an executing program (process) to notify it of some
exceptional event:
interrupt +---------+
signal from outside the program----------->| process |
/ +---------+
e.g., Control-C from the keyboard
323
Chapter 8 Miscellaneous Topics
At the core of the signal library is the legacy signal function, but
best practice now favors the newer sigaction function. The signal
function may behave differently across platforms and even operating
system versions. The forthcoming code example uses the better-behaved
sigaction function, introduced as a POSIX replacement for signal.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
324
Chapter 8 Miscellaneous Topics
void main() {
/** Set up a signal handler. **/
struct sigaction current;
sigemptyset(¤t.sa_mask); /* clear the
signal set */
current.sa_flags = 0; /* enables setting
sa_handler, not sa_
action */
current.sa_handler = cntrlC_handler; /* specify a handler */
sigaction(SIGINT, ¤t, NULL); /* control-C is a
SIGINT */
int i;
for (i = 0; i < MaxLoops; i++) {
printf("Counting sheep %i...\n", i + 1);
sleep(1);
}
}
The signals program (see Listing 8-11) introduces the basic signal
API. Here is an overview of how the program handles SIGINT and why the
program does so:
325
Chapter 8 Miscellaneous Topics
If the sa_action field were used instead, then the sa_flags field would
indicate which pieces of signal information were of interest.
The sigaction function, which sets the desired signal-handling action,
takes three arguments:
326
Chapter 8 Miscellaneous Topics
The first argument is the signal number, in this case SIGINT. The
second argument is a pointer to the new signal-handling action, and the
last argument is a pointer to the previous action, which can be saved with
a non-NULL pointer for later retrieval. In this example, the old action is not
saved: the third argument is NULL. Each action is specified by setting a field
in an instance of the struct sigaction type.
327
Chapter 8 Miscellaneous Topics
8.7. Software Libraries
Software libraries are a long-standing, easy, and sensible way to reuse code
and to extend C by providing new functionalities. This section explains
how to build such libraries from scratch and to make them easily available
to clients. Although the two sample libraries target Linux, the steps for
creating, publishing, and using these libraries apply in essentials to other
Unix-like systems.
There are two sample clients (one in C, the other in Python) to access
the libraries. It is no surprise that a C client can access a library written in
C, but the Python client underscores that a library written in C can serve
clients from other languages.
Computer systems in general and Linux in particular have two types of
library:
328
Chapter 8 Miscellaneous Topics
329
Chapter 8 Miscellaneous Topics
Detailed steps for building and publishing each type of library are
coming shortly. First, however, the C functions in the two libraries should
be introduced.
330
Chapter 8 Miscellaneous Topics
331
Chapter 8 Miscellaneous Topics
#include <stdio.h>
#include <math.h>
332
Chapter 8 Miscellaneous Topics
333
Chapter 8 Miscellaneous Topics
The five functions (see Listing 8-13) serve as grist for the library mill.
The two libraries derive from exactly the same source code, and the header
file primes.h is the C interface for both libraries.
This produces the binary file primes.o, the object module. The flag -c
means compile only. The next step is to archive the object module(s) by
using the Linux ar utility:
The three flags -cvq are short for “create,” “verbose,” and “quick
append” in case new files must be added to an archive. The prefix lib is
standard, but the library name is arbitrary. Of course, the file name for a
library must be unique to avoid conflicts.
334
Chapter 8 Miscellaneous Topics
The flag -shared indicates that the library is shared (dynamic) rather
than static. The -Wl flag introduces a list of compiler options, the first of
which sets the dynamic library’s soname, which is required. The soname
first specifies the library’s logical name (libshprimes.so) and then, following
the -o flag, the library’s physical file name (libshprimes.so.1). The goal is
to keep the logical name constant while allowing the physical file name to
change with new versions. In this example, the 1 at the end of the physical
file name libshprimes.so.1 represents the first version of the library. The
logical and physical file names could be the same, but best practice is to
have separate names. A client accesses the library through its logical name
(in this case, libshprimes.so), as clarified shortly.
335
Chapter 8 Miscellaneous Topics
The next step is to make the shared library easily accessible to clients
by copying it to the appropriate directory, for example, /usr/local/lib again:
A symbolic link is now set up between the shared library’s logical name
(libshprimes.so) and its full physical file name (/usr/local/lib/libshprimes.
so.1). Here is the command with /usr/local/lib as the working directory:
The logical name libshprimes.so should not change, but the target of
the symbolic link (libshprimes.so.1) can be updated as needed for new
library implementations that fix bugs, boost performance, and so on.
The final step (a precautionary one) is to invoke the ldconfig utility,
which configures the system’s dynamic loader. This configuration ensures
that the loader will find the newly published library:
The dynamic library is now ready for clients, including the two sample
ones that follow.
Both header files are to be found on the compiler’s search path (in the
case of primes.h, the directory /usr/local/include). Without this #include,
the compiler would complain as usual about missing declarations for
336
Chapter 8 Miscellaneous Topics
#include <stdio.h>
#include <math.h>
The header file math.h is required because the library function prime_
factors calls the mathematics function sqrt from the standard library
libm.so.
For reference, Listing 8-14 is the source code for the tester program.
#include <stdio.h>
#include <primes.h>
int main() {
/* is_prime */
printf("\nis_prime\n");
unsigned i, count = 0, n = 1000;
for (i = 1; i <= n; i++) {
if (is_prime(i)) {
count++;
if (1 == (i % 100)) printf("Sample prime ending in 1:
%i\n", i);
}
}
printf("%i primes in range of 1 to a thousand.\n", count);
/* prime_factors */
printf("\nprime_factors\n");
337
Chapter 8 Miscellaneous Topics
/* are_coprimes */
printf("\nare_coprime\n");
printf("Are %i and %i coprime? %s\n",
21, 22, are_coprimes(21, 22) ? "yes" : "no");
printf("Are %i and %i coprime? %s\n",
21, 24, are_coprimes(21, 24) ? "yes" : "no");
/* goldbach */
printf("\ngoldbach\n");
goldbach(11); /* error */
goldbach(4); /* small one */
goldbach(6); /* another */
for (i = 100; i <= 150; i += 2) goldbach(i);
return 0;
}
338
Chapter 8 Miscellaneous Topics
The first link flag identifies the library libshprimes.so, and the second
link flag identifies the standard mathematics library libm.so.
The linker is lazy, which means that the order of the link flags matters.
For example, reversing the order of the link specifications generates a
compile-time error:
The flag that links to libm.so comes first, but no function from this
library is invoked explicitly in the tester program; hence, the linker does
not link to the math.so library. The call to the sqrt library function occurs
only in the prime_factors function from the libshprimes.so library. The
resulting error in compiling the tester program is
Accordingly, the order of the link flags should notify the linker that the
sqrt function is needed:
The linker picks up the call to the library function sqrt in the
libshprimes.so library and, therefore, does the appropriate link to the
mathematics library libm.so. There is a more complicated option for
linking that supports either link-flag order; in this case, however, it is just
as easy to arrange the link flags appropriately.
Here is some output from a run of the tester client:
is_prime
Sample prime ending in 1: 101
339
Chapter 8 Miscellaneous Topics
prime_factors
prime factors of 12: 2 2 3
prime factors of 13: 13
prime factors of 876,512,779: 211 4154089
are_coprime
Are 21 and 22 coprime? yes
Are 21 and 24 coprime? no
goldbach
Number must be > 2 and even: 11 is not.
4 = 2 + 2
6 = 3 + 3
...
32 = 3 + 29
32 = 13 + 19
...
100 = 3 + 97
100 = 11 + 89
...
For the goldbach function, even a relatively small even value (e.g., 18)
may have multiple pairs of primes that sum to it (in this case, 5 + 13 and 7
+ 11). Such multiple prime pairs are among the factors that complicate an
attempted proof of Goldbach’s conjecture.
340
Chapter 8 Miscellaneous Topics
primes.prime_factors.restype = None
>>> primes.is_prime(13)
1
341
Chapter 8 Miscellaneous Topics
>>> primes.is_prime(12)
0
>>> primes.prime_factors(72)
2 2 2 3 3
>>> primes.goldbach(32)
32 = 3 + 29
32 = 13 + 19
The functions in the primes library use only a simple data type,
unsigned int. If this C library used complicated types such as structures,
and if pointers to structures were passed to and returned from library
functions, then an FFI more powerful than ctypes might be better for a
smooth interface between Python and C. Nonetheless, the ctypes example
shows that a Python client can use a library written in C. Indeed, the
popular NumPy library for scientific computing is written in C and then
exposed in a high-level Python API.
8.8. What’s Next?
This is a small book about a big language—not big in size, but in its impact
throughout computing. C is a very small language with easy access to an
expanse of standard and third-party libraries. As the libraries get better, C
gets better.
342
Chapter 8 Miscellaneous Topics
343
Chapter 8 Miscellaneous Topics
What, then, is next? The code examples are available from GitHub
(https://github.com/mkalin/cbook.git). They are short enough to
explore, to tweak, and to improve.
344
Index
A integer value, 13
labels, 13
Access permissions, 159, 161, 183
null terminator, 10
Address operator, 72–74, 157, 173
optimization, 11
Amazon’s public key, 215
printf function, 10
API, 291, 299, 321, 325, 326,
puts argument, 13
342, 343
slash, 10
Applications, 231
translation, 10
Arguments
Assembly languages, 1, 9, 13–16,
avg function, 31, 32
114, 315
format string, 29
Assertions, 291, 300–304
printf function, 28, 30
Asymmetric approach, 220
syscall function, 29, 30
Autoreg program, 138, 139
SYS_chmod function, 30
utilities, 30, 32
varArgs program, 31 B
Arithmetic operators, 56–58 BasicFork program, 235, 241, 323
Arrays Binary semaphore, 249
array program, 68 BIO library, 225
for loop, 68 Bitwise operators, 61
square brackets, 68 arithmetic shift, 62
ASCII bytes, 256 bit-level representation, 61
asm.js, 314 4-byte integer, 62
Assembly callable blocks compiler, 60
AT&T version, 12 complement operator, 61
char and msg, 9 endian program, 62, 63
code segment, 13 functions, 63
hi program, 9, 11 integer’s address, 63
346
INDEX
puts statement, 23 E
scanf function, 25
empId program, 293, 294, 296,
straight-line execution, 20
297, 301
switch construct, 22
empId2 program, 297, 298
test program, 20, 21
Emscripten toolchains, 316–321
while loop, 23–25
Encryption/decryption, 214, 219
Critical section, 274, 279, 281
Enum, 92–93
Cryptographic suite, 220, 221
Environ program, 305, 306
Curl command-line tool, 208
Event-driven web server
Curl utility, 212
fifoReader, 199, 200
C vs. C++, 65
read operations, 200
cwSSLutils.c, 223–224
select function, 200–202
selectStdin program, 202
D webserver program, 203, 204
Exec family functions
Data types, 33–37, 47, 175, 314
argument formats, 241
Deadlock
child process, 243
code analysis, 284
cline program, 243
concurrent program, 285
execing program, 243
experimentation, 285
execle, 241
grab_locks function, 284, 285
execv, 241, 243
main thread, 284
printed pid values, 244
multithreading, 281
return value, 244
output, 283
Execing program, 243, 244
threads, 282
Exiting program, 246, 247
Deadlock-detection module, 285
Debugging network applications, 189
Dereference operator, 71–74, 112
Digital certificates, 214, 216, 219, F
222, 223, 225 fcntl function, 186–188, 258, 260
Digital signature, 214, 215, 217, fifoReader program, 183, 186, 187,
218, 223 199, 200
Dynamic library, 329, 334–336, fifoWriter program, 183, 184, 186,
338, 339 187, 199, 200
347
INDEX
348
INDEX
349
INDEX
350
INDEX
351
INDEX
352
INDEX
353
INDEX
354
INDEX
355
INDEX
356
INDEX
357