Yung-Hsiang Lu - Intermediate C Programming - 2015
Yung-Hsiang Lu - Intermediate C Programming - 2015
Intermediate C Programming
“… an excellent entryway into practical software development practices … I
wished I had this book some 20 years ago … the hands-on examples … are eye
opening. I recommend this book to anyone who needs to write software beyond
the tinkering level.”
—From the Foreword by Gerhard Klimeck, Reilly Director of the Center for Predic-
tive Materials and Devices and the Network for Computational Nanotechnology
and Professor of Electrical and Computer Engineering, Purdue University; Fellow
of the IOP, APS, and IEEE
“This well-written book provides the necessary tools and practical skills to turn
students into seasoned programmers. It not only teaches students how to write
good programs but, more uniquely, also teaches them how to avoid writing bad
programs. The inclusion of Linux operations and Versioning control as well as the
coverage of applications and IDE build students’ confidence in taking control over
large-scale software developments.”
—Siau Cheng Khoo, Ph.D., National University of Singapore
“This book is unique in that it covers the C programming language from a bottom-
up perspective, which is rare in programming books. … students immediately
understand how the language works from a very practical and pragmatic per-
spective.”
—Niklas Elmqvist, Ph.D., Associate Professor and Program Director, Master of
Science in Human–Computer Interaction, University of Maryland
Intermediate C Programming provides a stepping-stone for intermediate-lev-
el students to go from writing short programs to writing real programs well. It
shows students how to identify and eliminate bugs, write clean code, share code
with others, and use standard Linux-based tools, such as ddd and valgrind. The
text enhances their programming skills by explaining programming concepts and
comparing common mistakes with correct programs. It also discusses how to use
debuggers and the strategies for debugging as well as studies the connection
Lu
between programming and discrete mathematics.
K25074
w w w. c rc p r e s s . c o m
Yung-Hsiang Lu
Purdue University
West Lafayette, IN, USA
CRC Press
Taylor & Francis Group
6000 Broken Sound Parkway NW, Suite 300
Boca Raton, FL 33487-2742
© 2015 by Taylor & Francis Group, LLC
CRC Press is an imprint of Taylor & Francis Group, an Informa business
This book contains information obtained from authentic and highly regarded sources. Reasonable efforts have been
made to publish reliable data and information, but the author and publisher cannot assume responsibility for the valid-
ity of all materials or the consequences of their use. The authors and publishers have attempted to trace the copyright
holders of all material reproduced in this publication and apologize to copyright holders if permission to publish in this
form has not been obtained. If any copyright material has not been acknowledged please write and let us know so we may
rectify in any future reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted, reproduced, transmitted, or uti-
lized in any form by any electronic, mechanical, or other means, now known or hereafter invented, including photocopy-
ing, microfilming, and recording, or in any information storage or retrieval system, without written permission from the
publishers.
For permission to photocopy or use material electronically from this work, please access www.copyright.com (http://
www.copyright.com/) or contact the Copyright Clearance Center, Inc. (CCC), 222 Rosewood Drive, Danvers, MA 01923,
978-750-8400. CCC is a not-for-profit organization that provides licenses and registration for a variety of users. For
organizations that have been granted a photocopy license by the CCC, a separate system of payment has been arranged.
Trademark Notice: Product or corporate names may be trademarks or registered trademarks, and are used only for
identification and explanation without intent to infringe.
Visit the Taylor & Francis Web site at
http://www.taylorandfrancis.com
and the CRC Press Web site at
http://www.crcpress.com
Contents
Foreword xxi
Preface xxiii
2 Stack Memory 9
2.1 Values and Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 The Call Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.1 The Return Location . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.2 Function Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.3 Local Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.4 Value Address . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3.5 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3.6 Retrieving Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4 Visibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.5.1 Draw Call Stack I . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.5.2 Draw Call Stack II . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.5.3 Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.6 Answers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.1 Draw Call Stack I . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.2 Draw Call Stack II . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.3 Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.7 Examine the Call Stack with DDD . . . . . . . . . . . . . . . . . . . . . . . 27
v
vi Contents
4 Pointers 39
4.1 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 The Swap Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.4 The Swap Function Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.5 Type Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.6 Arrays and Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.7 Type Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.8 Pointer Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.1 Swap Function 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.2 Swap Function 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.3 Swap Function 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.9.4 Swap Function 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.9.5 Swap Function 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.9.6 15,552 Variations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.10 Answers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.10.1 Swap Function 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.10.2 Swap Function 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.3 Swap Function 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.4 Swap Function 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.5 Swap Function 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6 Strings 85
6.1 Array of Characters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.2 String Functions in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.2.1 Copy: strcpy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.2.2 Compare: strcmp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
6.2.3 Finding Substrings: strstr . . . . . . . . . . . . . . . . . . . . . . . 90
6.2.4 Finding Characters: strchr . . . . . . . . . . . . . . . . . . . . . . . 90
6.3 Understanding argv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
6.4 Counting Substrings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
II Recursion 163
12 Recursion 165
12.1 Selecting Balls with Restrictions . . . . . . . . . . . . . . . . . . . . . . . . 166
12.1.1 Balls of Two Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
12.1.2 Balls of Three Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
12.1.3 A Further Restriction . . . . . . . . . . . . . . . . . . . . . . . . . . 168
12.2 One-Way Streets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
12.3 The Tower of Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
12.4 Calculating Integer Partitions . . . . . . . . . . . . . . . . . . . . . . . . . 174
12.4.1 Count the Number of “1”s . . . . . . . . . . . . . . . . . . . . . . . . 175
12.4.2 Odd Numbers Only . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
12.4.3 Increasing Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
12.4.4 Alternating Odd and Even Numbers . . . . . . . . . . . . . . . . . . 179
12.4.5 Generalizing the Integer Partition Problem . . . . . . . . . . . . . . 180
12.4.6 How Not to Solve the Integer Partition Problem . . . . . . . . . . . 181
IV Applications 357
22 Finding the Exit of a Maze 359
22.1 Maze File Format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
22.2 Reading the Maze File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
22.3 The Maze Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
22.4 An Escape Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
22.5 Implementing the Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
22.5.1 canMove Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
22.5.2 getOut Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
22.5.3 Printing Visited Locations . . . . . . . . . . . . . . . . . . . . . . . . 378
A Linux 443
A.1 Options for Installing Linux . . . . . . . . . . . . . . . . . . . . . . . . . . 443
A.2 Getting Ubuntu Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444
A.3 Downloading and Installing VirtualBox . . . . . . . . . . . . . . . . . . . . 445
A.4 Install and Update Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
A.5 Install Programming Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
Contents xi
Index 461
This page intentionally left blank
List of Figures
2.1 Pushing and popping data on a stack. (a) Originally, the top of the stack
stores the number 720. The number 653 is pushed onto the top of the stack.
(b) Data are retrieved (popped) from the stack. Pushes and pops can only
occur at the top of the stack. Although this figure illustrates the idea with
integers, a stack is a general concept and can manage any type of data. . 12
2.2 The flow of the program as indicated by the numbers 1, 2, and 3. . . . . . 12
2.3 The return location is the place where the program continues after the
function f1 returns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 The flow of the program with the three functions. . . . . . . . . . . . . . 14
2.5 The return locations (RLs) are marked at the lines after calling f2 (RL A)
and f1 (RL B). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6 Enter the commands at the bottom of DDD. . . . . . . . . . . . . . . . . . 29
2.7 Use the mouse to select a inside g1. Click the right mouse button and select
“Display a”. Do the same for b. . . . . . . . . . . . . . . . . . . . . . . . . 30
2.8 The values of a and b in function g1 are shown. . . . . . . . . . . . . . . . 30
2.9 Use the f command to see different frames. . . . . . . . . . . . . . . . . . 31
9.1 (a) Execution time for selection sort and quick sort. (b) The ratio of the
execution time. Please note that both axes use a logarithmic scale. . . . . 140
10.1 A file is a stream. This example uses the program source code as the input
file. (a) After calling fopen, the stream starts from the very first character of
the file and ends with EOF. EOF is a special character that does not actually
exist in the file, but signifies that there is no data left in the stream. (b),(c)
Each time fgetc is called, one character is taken out of the stream. (d)
After calling fgetc enough times, all the characters in the file are retrieved.
We have not yet attempted to read past the end of the file. (e) Finally, the
end of file character EOF is returned because there are no more characters
in the file. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
12.1 To decide f (n), consider the possibilities for the first ball. If the first ball
is B, the remaining n − 1 balls have f (n − 1) possibilities. If the first ball
is R, then the second ball must be B and the remaining n − 2 balls have
f (n − 2) possibilities. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
12.2 To decide f (n), consider the possibilities for the first ball. If the first ball is
G or B, the remaining n − 1 balls have f (n − 1) possibilities. If the first ball
is R, the second ball must be G or B and the remaining n − 2 balls have
f (n − 2) possibilities. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
xiii
xiv List of Figures
12.3 A city’s streets form a grid, and are either east–west bound or north–south
bound. A car can move only east or north. . . . . . . . . . . . . . . . . . 170
12.4 (a) A driver cannot turn anywhere when traversing from A to B. (b) Like-
wise, a driver cannot turn anywhere when traversing from C to D. (c) There
are some turning options when traversing from E to F. At E, the driver can
go northbound first or eastbound first, as indicated by the two arrows. . . 170
12.5 The Tower of Hanoi. (a) Some disks are on pole A and the goal is to move
all the disks to pole B, as shown in (b). A larger disk can never be placed
on top of a smaller disk. A third pole, C, can be used when necessary. . . 171
12.6 Moving one disk from A to B requires only one step. . . . . . . . . . . . . 172
12.7 Moving two disks from A to B requires three steps. . . . . . . . . . . . . . 172
12.8 Moving three disks from A to B requires seven steps. . . . . . . . . . . . . 173
12.9 Count the occurrences of “1” when partitioning n. . . . . . . . . . . . . . 176
15.1 In each step, the binary search reduces the number of elements to search
across by half. In the first step, key is compared with the element at the
center. If key is smaller, then it is impossible to find key in the upper half
of the array. If key is greater than the element at the center, then it is
impossible to find key in the lower half of the array. The array must have
been sorted before performing a binary search. . . . . . . . . . . . . . . . 224
15.2 Determine the values of fv and count using a graphical illustration of the
calling relations. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
18.1 A linked list starts empty, i.e., without any Node. This figure shows three
views: (a) the source code view, (b) a diagram, and (c) the stack memory. 295
18.2 Creating a Node object whose value is 917. Please note that head is a pointer. 295
18.3 Replacing the three lines by using List insert. . . . . . . . . . . . . . . 296
18.4 Calling List insert again to insert another list node. . . . . . . . . . . . 296
18.5 Insert the third object by calling List insert again. . . . . . . . . . . . . 297
18.6 Delete the node whose value is −504. . . . . . . . . . . . . . . . . . . . . 299
18.7 To delete a list node, first create a pointer p that points to the same memory
address as head. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
18.8 The function creates another pointer q. Its value is the same as p->next. 300
List of Figures xv
18.9 Modify p->next to bypass the node that is about to be deleted. . . . . . 300
18.10 Release the memory pointed to by q. . . . . . . . . . . . . . . . . . . . . . 301
19.1 (a) The original linked list. The list’s head points to A. (b) The reversed
linked list. The list’s head points to E. . . . . . . . . . . . . . . . . . . . . 314
19.2 (a) Three pointers are used. (b) Change orighead -> next and make it
point to revhead. (c) Update revhead to the new head of the reversed list.
(d) Update orighead to the new head of the remaining list. (e) Update
origsec to the second node of the remaining list. . . . . . . . . . . . . . 315
22.1 Coordinates (row, column). The upper left corner is (0, 0). Moving right
increases the column; moving down increases the row. . . . . . . . . . . . . 360
22.2 Strategy to get out of a maze. Suppose ↑ is north and → is east. A gray
square is a brick. (a) If moving east in step 1 does not reach a dead end,
then keep moving east in step 2. (b) If the corridor has a turn, then follow
the turn and keep moving forward. (c) After encountering a dead end, turn
around (i.e., backtrack) and move back along the corridor. . . . . . . . . . 370
xvi List of Figures
23.1 Example of metadata: the exposure time, the focal length, the time and the
date, etc. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
23.2 (a) The RGB color space, showing the primary colors and their mixtures.
White is produced by mixing all three primary colors together. Color filters.
(b) original images. (c) red only, (d) green only, (e) blue only. . . . . . . . 392
23.3 Color filters. (a)–(c) original images. (d) red only, (e) green only, (f) blue
only. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392
23.4 Color inversion. (a),(c): original. (b),(d): inverted. . . . . . . . . . . . . . . 393
23.5 Detecting vertical edges. (a) The original image. (b) Gray-scale image. The
detected edges use different threshold values. (c) 120. (d) 100. (e) 80. (f)
is 60. Many vertical edges are not detected when the threshold is too high.
When the threshold is too low, there are many false-positive edges (like the
sky). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
23.6 Equalization. (a),(b): original images. (c),(d): processed images. . . . . . . 393
23.7 Equalization. (a),(b): original images. (c),(d): processed images. . . . . . . 393
24.7 Now the linked list has only one node. The tree has been built, and it is in
the only remaining list node. . . . . . . . . . . . . . . . . . . . . . . . . . . 406
24.8 A list of tree nodes. This figure shows the list as it is being built. The tree
is the same as the one shown in Fig. 24.4 (c). . . . . . . . . . . . . . . . . 410
24.9 Display the tree in DDD. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
24.10 If there are n leaf nodes on the left side, zeros should be filled in n rows.
The column is determined by the distance from the root. . . . . . . . . . . 412
24.11 The expressions for the code trees are (a) 1a1b00, (b) 1a1b1c000, (c)
1a1b01c1d000, (d) 1a1b01c1d1e0000. For each tree, the number of 1s is
the same as the number of leaf nodes. The number of 0s is one plus the
number of non-leaf nodes. . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
24.12 (a) One tree node is added after reading the first command and the first
character. (b) After reading two bytes. (c) After reading six bytes. (d) The
first bit in the seventh byte is a command and it is 0. (e) The next command
bit (the second bit in the seventh byte) is 1. . . . . . . . . . . . . . . . . . 424
24.13 (a) The next command (the second bit in the eighth byte) is 0. This will
create a common parent for the first two tree nodes. (b) The next command
(the third bit in the eighth byte) is also 0. This will create a common parent
for the first two tree nodes. (c) The next command (the fourth bit in the
eighth byte) is 1. This will create a tree node to store the value g. . . . . 425
24.14 The remaining commands are 0. Continue building the tree. . . . . . . . . 426
24.15 Finish building the tree. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
C.10 To solve the build problem, we add another source file called addsub.c and
in this file we define the two functions. . . . . . . . . . . . . . . . . . . . . 456
C.11 When you build the project, Eclipse should say that the project is built
successfully. A valid Makefile is automatically generated by Eclipse. . . . . 457
C.12 Running: Click Run in the menubar and then select Run. . . . . . . . . . 457
C.13 The program’s output is shown in the Console. . . . . . . . . . . . . . . . 458
C.14 Eclipse uses gdb to debug programs, and also provides a convenient user
interface. To debug a program, click Run and select Debug. . . . . . . . . 458
C.15 Eclipse starts the program and stops at the first statement in main. This is
denoted by the arrow that is shown at line 13. . . . . . . . . . . . . . . . . 459
C.16 Eclipse knows how to communicate with gdb, and provides a convenient
method for common debugging commands such as step over, step into, and
toggle breakpoint. Move the mouse cursor to line 18 in the source code, and
toggle line breakpoint. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459
C.17 Click Window, Show View, and Variables. Here you can see the values of
variables as the code executes. Note that the value of c is 96. . . . . . . . 460
List of Tables
4.1 Different usages of * in C programs. Please notice that ptr = and * ptr =
have different meanings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
16.1 Functions for opening, writing to, and reading from text and binary files. . 270
20.1 The numbers of shapes for binary trees of different sizes. . . . . . . . . . . 333
xix
This page intentionally left blank
Foreword
Imagine you run a research or development group where writing software is the means
to examine new physics or new designs. You look for students or employees who have a
technical background in that specific physics or science, yet you also look for some software
experience. You will typically find that students will have taken a programming class or
have tinkered around with some small programs. But in general they have never written
software with any serious complexity, they have never worked in a team of people, and they
are scared to dive into an existing piece of scientific software.
Well, that is my situation. My research group studies electron flow at the nanometer
scale in the very transistors that power your future computer. As a faculty member I have
found that most of today’s graduated bachelor students in engineering or physical sciences
are used to writing small programs in scripting languages and are not even familiar with
compiling, practical debugging, or good programming practices.
I believe my situation is not unique but quite common in academia and industry. How
can you bring these novices up to speed? How can you give them the day-to-day practical
insights fast, that I had to learn through years of slow cut and try experiences?
Most advanced programming books explain complex or larger programs that are correct
and beautiful. There is an analogy here between reading a well-written book and composing
a new novel yourself. Literature analysis helps the reader to appreciate the content or the
context of a novel. While many people can read a correctly formulated algorithm in C,
few people would be able to write this code even if they were given the pseudocode (the
storyline). This book provides an entry into writing your own real code in C.
I believe that this new book provides an excellent entry way into practical software
development practices that will enable my beginning and even advanced students to be
more productive in their day-to-day work, by avoiding typical mistakes and by writing
cleaner code, since they understand the underlying implications better. This book will also
facilitate the collaborations within the group through exemplary coding styles and practices.
This book explains the importance of detecting hidden problems. A common mistake
among most students is that they pay attention to only the surface: the outputs of their
programs. Their programs may have serious problems beneath the surface. If the programs
generate correct outputs in a few cases, the students believe that the programs are correct.
This is dangerous in this connected world: A small careless mistake may become a security
threat to large complex systems. Creating a secure and reliable system starts from paying
attention to details. This book certainly covers many details where careless mistakes may
cause serious problems.
I wished I had this book some 20 years ago after I had read through Kernighan and
Richie. Back then I began writing a large code basis in C after my coding experience in
FORTRAN. Passing by reference, passing by value—simple concepts, but this book plays
out these concepts in a pedagogically sound approach. I truly like the hands-on examples
that are eye opening.
I recommend this book to anyone who needs to write software beyond the tinkering level.
You will learn how to program well. You will learn how to identify and eliminate bugs. You
will learn how to write clean code, that cleans up after itself, so it can be called millions of
xxi
xxii Foreword
times without crashing your own or someone else’s computer. You will learn how to share
code with others. All along you will begin to use standard LINUX-based tools such as ddd,
valgrind, and others.
Gerhard Klimeck
Reilly Director of the Center for Predictive Materials and Devices (c-PRIMED) and the
NCN (Network for Computational Nanotechnology)
Professor of Electrical and Computer Engineering at Purdue.
Fellow of the Institute of Physics (IOP), the American Physical Society, (APS) and
Institute of Electrical and Electronics Engineers (IEEE).
Preface
xxiii
xxiv Preface
take them from being capable of writing short programs to being capable of writing real
programs.
Currently, the gap is partially filled by books that cover Data Structures and Algo-
rithms. These books provide programs that implement the data structures or algorithms.
However, this is not an ideal solution. These books focus on the subjects, Data Structures
and Algorithms, but rarely provide information that helps students write correct code. In
fact, they usually include the programs without much explanation. They do not explain
programming concepts—for example, why a function needs a pointer as an argument or
the difference between deep and shallow copy. As a result, students have to learn these
programming skills by themselves.
To fill this need, I am writing this book for intermediate-level students. This book is
ideal as a second book on programming.
has ensured that it remains the foundation of computing on almost all modern platforms.
Like many operating systems, Linux is written in C. Android is mostly written in Java but
has an interface with C, called JNI (Java Native Interface). Most computer languages can
communicate with and through C. In fact, this is generally required for a programming
language to be useful, since most operating system interfaces use C. When a brand new
system is designed, C is often the first (in many cases, the only) programming language
supported by the system.
C is a good choice for intermediate-level students because learning C requires knowing
many concepts about computers. The web site langpop.com compares the popularity of
programming languages and C is the most popular language, followed by Java. A report
in IEEE Spectrum [1] ranks popular programming languages. This report considers four
types of software: mobile, enterprise, embedded, and web. C is the most popular language
for embedded systems. When all four types are considered, the top five are:
1. Java (100%)
2. C (99.3%)
3. C++ (95.5%)
4. Python (93.4%)
5. C# (92.4%)
As you can see, three (C, C++, C#) of the top five languages are based on C. Java is
influenced by C++.
[1] Stephen Cass, Nick Diakopoulos, Joshua J. Romero, “Interactive: The Top Programming
Languages IEEE Spectrums 2014 Ranking”, July 1, 2014, http://spectrum.ieee.org/
static/interactive-the-top-programming-languages.
This page intentionally left blank
Author, Reviewers, and Artist
Author
Yung-Hsiang Lu is an associate professor at the School of Electrical and Computer
Engineering in Purdue University, West Lafayette, Indiana, U.S.A. He is an ACM (Associ-
ation for Computing Machinery) Distinguished Scientist and ACM Distinguished Speaker.
In August-December 2011, he was a visiting associate professor at the Department of Com-
puter Science in the National University of Singapore. He received the Ph.D. degree from
the Department of Electrical Engineering in Stanford University, California, U.S.A.
Reviewers
Aaron Michaux is a graduate student at the School of Electrical and Computer Engi-
neering in Purdue University, West Lafayette, Indiana, U.S.A. He was awarded a BSc in
computer science from the University of Queensland, Australia, and a BA in psychology
from Saint Thomas University, New Brunswick, Canada. Aaron worked as a professional
programmer for 10 years before heading back to school to work on a Ph.D. His research
focuses mainly around computer vision and human visual perception.
Artist
The book’s cover is painted by Kyong Jo Yoon. Yoon is a Korean artist and often places
heroic figures in natural settings. He is an adviser of the Korean Fine Arts Association and
his work is on display in the Ann Nathan Gallery in Chicago, Illinois, U.S.A.
xxvii
This page intentionally left blank
Rules in Software Development
Would you be satisfied with a bank’s service if the bank lost 0.1% of your money every
day due to a software mistake? Would you accept a wrist watch that lost 40 minutes
every month? Both of these are cases of “99.9% success” but are nonetheless unacceptable.
Computers are now being used in many applications, some of which could affect human
safety. If your program works correctly 99.9% of the time, then your program could kill
people during the remaining 0.1%. This is totally unacceptable, and such a program is a
failure. Thus, 99.9% success is failure.
If you live in Pasadena, California, and want to go to New York, which route should you
take? Perhaps you could go to the Los Angeles Airport and take a flight. New York is at the
east side of Pasadena but the airport is at the west side of Pasadena. Why don’t you drive
(or even walk) east from Pasadena right away? Why do you travel farther than necessary
and go west of your destination to the airport? After one hour of travel, you would be close
to New York if you drove rather than waiting in lines at an airport. The answer is simple: An
airplane is a better tool than a car for long-distance travel. In program development, there
are many tools designed for managing larger programs. You need to learn these tools. Yes,
learning these tools takes time but you spend much more time when using inappropriate
tools, or not using any tool at all. Spending time learning programming tools can save time
in development and debugging.
Despite decades of effort, computers are still pretty “dumb”. Computers cannot guess
what is on your mind. If your programs tell a computer to do the wrong thing, then the
computers will do the wrong thing. If your program is wrong, it is your fault. Computers
cannot read your mind. There are many instances in which “small” mistakes in computer
programs cause significant financial damages, injuries, or loss of lives. Missing a single
semicolon (;) can make a C program unusable. Replacing . by , can also make a C program
fail. Computer programs cannot tolerate “small” mistakes.
Passing test cases does not guarantee a program is correct. Testing can only tell you
that a program is wrong. Testing cannot tell you that a program is correct. Why? Can test
cases cover every possible scenario? Covering all scenarios is difficult and, in many cases,
impossible. Problems can be hidden inside your programs because it is difficult for test cases
to detect idiosyncratic behavior.
Producing correct outputs does not mean a program is correct. Would you consider a
plane safe if the plane has taken off and landed without any injuries? If the plane leaks fuel,
would you demand the airline fix the plane before boarding? Would you accept the airline’s
response “Nobody was hurt so this means that plane is safe.”? If a driver runs a red light
without an accident, does that mean running a red light is safe? A program that produces
correct outputs is like a plane that lands without injury. There may be problems beneath the
surface. Many tools are available to detect hidden problems in human health, for example
X-ray, MRI, and ultrasonic scan. To detect hidden problems in computer programs, we need
good tools. We need to fix programs even though they produce correct outputs.
You have to assume that your programs will fail and develop a strategy to detect and
correct mistakes. When writing a program, focus on one small part each time. Check it
carefully and ensure that it is correct before working on other small parts. For most pro-
xxix
xxx Rules in Software Development
grams, you need to write additional code for testing these small parts. You will save a lot
of time if you write additional testing code, even though the testing code is not included in
the final program. Sometimes, the testing code is more than the programs themselves. My
own experience suggests 1:3 ratio—for every line in the final program, about three lines of
testing code are needed.
No tools can replace a clear mind. Tools can help but nothing can replace deep and
thorough understanding of the concepts. If you want to be a good software developer, then
you need to fully understand every detail. Do not rely on tools to think for you: They
cannot.
Source Code
The sample programs in this book are available at github.com. Please use the following
command to retrieve the files:
xxxi
This page intentionally left blank
Part I
1
This page intentionally left blank
Chapter 1
Program Execution
1.1 Compile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Redirect Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.1 Compile
This chapter explains how to write, compile, and execute programs in Linux. We use
a Linux terminal and explain the commands you need to type. Why do you learn how
to use the terminal? First, the terminal is a flexible and convenient interface for working
with a computer. It may take some experience to realize this, but learning how to use the
terminal may improve your productivity. Second, many cloud computing or web services
offer terminal access. This is a natural method of providing computing resources, especially
when working with many computers (like in a data center). A graphical user interface (GUI)
is nice when working with one computer. However, when dealing with many computers, GUI
can become a distraction. Also, using the terminal helps you understand how UNIX systems
work. After becoming familiar with terminal commands, you may understand integrated
development environments (IDEs), and what they can do for you. The Eclipse IDE is
explained later in this book.
Start a terminal in Linux and type
$ cd
$ pwd
$ mkdir cprogram
$ cd cprogram
In this book, $ is used as the terminal prompt.
The first command cd means “change directory”. If no argument is added after cd, as
in the first command, then it will return to your home directory (also called “folder”).
The second command pwd means “print the current working directory”. It will be some-
thing like /home/yourname/.
The third command mkdir means “make a directory”. The command mkdir cprogram
means “make a directory whose name is cprogram”.
You should not create a directory or a file whose name includes spaces. The reason is very
simple: The international standard for directory names and file names (called International
Standard Organization or ISO 9660) disallows spaces. If a directory’s or a file’s name has
spaces, then some programs may not work.
The last command cd cprogram means “change directory to (i.e., enter) cprogram”.
This is the directory that was just created.
In the terminal, type
$ which emacs
If nothing appears in the terminal or it says “Command not found”, then please install
3
4 Intermediate C Programming
Emacs first. If you do not know how to install software in Linux, please read Section A.5.
In the terminal, type
This command starts Emacs to edit a file called prog1.c. Adding & allows you to use the
terminal as well as the Emacs editor at the same time. Without the trailing &, the terminal
will force you to wait until Emacs quits. Inside Emacs, type the following code:
1 // prog1 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t main ( i n t argc , char * * argv )
5 {
6 i n t a = 5;
7 i n t b = 17;
8 printf ( " main : a = %d , b = %d , argc = % d \ n " , a , b , argc ) ;
9 return EXIT_SUCCESS ;
10 }
Save the file. You can probably guess that this program prints something like
This is the first complete program shown in this book and requires some explanation. This
program prints something by calling printf. This is a function provided by the C language
but you need to include stdio.h before you can use this function. This is a header file for
standard input and output functions. In a C program, the starting point is the main function.
The program returns EXIT SUCCESS after it successfully prints the addresses. As you can
guess, if a program can return EXIT SUCCESS, another program can return EXIT FAILURE.
Why should a program return either EXIT SUCCESS or EXIT FAILURE? In today’s complex
computer systems, many programs are called by other computer programs. Thus, it is
important that your programs inform the calling programs whether your programs have
successfully accomplished what they are designed to do. This information allows the calling
programs to decide what actions to take next. EXIT SUCCESS and EXIT FAILURE are symbols
defined in stdlib.h so it is included at the second line.
In this book, source code is listed with line numbers, starting from 1. Sometimes, the
code refers to a previously mentioned example and the line number corrsponds to the value
in the earlier example.
The main function is the starting point of a C program but this is not always true for
a C++ program. If a C++ program has a static object, the object’s constructor will be
called before the main function is called. Since this book is about C programming, it is safe
to assume that the main function is the starting point of all the programs.
What is argc? It is easier to answer this question by running the program. First, we
need to explain how to convert this program from a human-readable format to a computer-
readable format.
What is typed into Emacs is a “C source file”. It is vaguely similar to English and
consists of Latin alphabet letters. However, since the computer does not understand this
format, the “source file” needs to be converted into a computer-readable format called an
executable. A compiler is the tool needed for this conversion and gcc is a popular compiler
on Linux. In the terminal, type
NAME
gcc - GNU project C and C++ compiler
SYNOPSIS
gcc [-c|-S|-E] [-std=standard]
[-g] [-pg] [-Olevel]
[-Wwarn...] [-pedantic]
[-Idir...] [-Ldir...]
[-Dmacro[=defn]...] [-Umacro]
[-foption...] [-mmachine-option...]
[-o outfile] [@file] infile...
Only the most useful options are listed here; see below for the
remainder. g++ accepts mostly the same options as gcc.
DESCRIPTION
Most of the command line options that you can use with GCC are
useful for C programs; when an option is only useful with another
language
The document is also called the “man page”, where “man” means manual. These manual
pages are typically well written but terse. Early computers were very expensive and the
designers tried to keep everything as short as possible. We have seen a few Linux commands
already:
6 Intermediate C Programming
$ file prog
The output should be similar to the following, but the specifics will vary depending on
the computer the program is compiled on:
The most important thing to pay attention to is the word “executable”. This word
means that the file “prog” is a program. By convention, executable files in Linux have no
extension, unlike “.exe” used in Windows. How do you execute the program? Type this
command:
$ ./prog
Here, prog is the name of the program; ./ means the current directory. Why is it
necessary to add ./ in front of the program? It is necessary because it is possible to have
files of the same name in different directories. By adding ./, the terminal knows that the
desired program is in this directory. Some people like to call their programs “test”. This is
a bad name for your program because “test” is also a built-in command in Linux. If you
type
$ test
$ ./test
$ ./prog
main: a = 5, b = 17, argc = 1
$ ./prog abc
main: a = 5, b = 17, argc = 2
Do you notice the changes in argc? When the program is executed without anything
else, argc is 1. If some words are added after the program, then argc becomes larger.
The more words (i.e., arguments) that are added, the larger argc becomes. This illustrates
that arguments can be given to programs when they are run. Consecutive arguments are
separated by one or more spaces. The terminal tells your program (specifically, the main
function) the number of arguments. As can be seen in the examples below, adding extra
spaces between words makes no difference. One space has the same effect as several spaces.
The answer is 4.
1. gcc
2. prog1.c
3. -o
4. prog
The arguments themselves are strings, and are stored in argv. This will be covered when
explaining strings in Chapter 6.
• You need to write a program that produces correct outputs based on given inputs.
This is frequently the case when taking programming courses. The correctness of the
program is evaluated by whether your program produces correct outputs. In many
cases, the programs are graded by computer programs based on the input-output
pairs. In this case, nobody reads the information on a computer screen.
If > and a file name is added after the command, then the output is saved in that file.
Nothing appears on the computer screen because the information is redirected to the
file whose name is output. You can use a text editor to see the contents of this file. You
can also use the Linux command more or less or cat to see the file’s content. If you type
more output in the terminal, this is what appears on the computer screen:
main: a = 5, b = 17, argc = 5
Since the output is saved in a file, you can use the diff command to check whether
that output is the same as the correct output, assuming you have the correct output saved
in another file. The diff command requires the names of two files and determines whether
these files are the same or not. If they are different, the command shows the line-by-line
differences. The diff program will compare the files exactly. It is often useful to ignore
whitespace and this can be done by adding -w after diff. Adding -q after diff shows only
whether the files are different or not, without showing the line-by-line differences. Although
the diff command is useful, sometimes we want to see the differences side-by-side. The
meld program in Linux does precisely that.
Chapter 2
Stack Memory
9
10 Intermediate C Programming
Address Family
One Jones
Two Smith
Three Brown
Four Taylor
Five Clark
In a computer’s memory, each location stores either a zero or a one—something like the
following:
• Zero is stored at the first location.
• Zero is stored at the second location.
• One is stored at the third location.
• Zero is stored at the fourth location.
• One is stored at the fifth location.
We can also express this as a table:
Address Value
First Zero
Second Zero
Third One
Fourth Zero
Fifth One
Programmers usually consider more than one bit at a time. For the time being, let us
set aside the size of data. Instead, assume that each piece of data occupies one unit of
memory. Operating systems guarantee that everything has a unique and positive address.
The address is never zero or negative. The symbol NULL is defined as the zero address
and indicates an invalid address. It would be impossible to remember the addresses of
all of the bits of memory that a computer program manipulates. Early computer science
pioneers found an elegant solution: Create symbols, such as counter or sum to refer to the
relevant bits of memory. If the value stored corresponding to a symbol may change during
the program’s execution, this symbol is called a variable. The symbols have meaning to
humans writing computer programs, and compilers (such as gcc) convert these symbols
into addresses. The final computer program manipulates the values, and does not see the
symbols. Inside a computer’s memory, there are only addresses and values. This was a major
early innovation in easing the task of writing computer programs. The following figure shows
the relationships between symbols and addresses:
source code .c or .h files executable program
human readable −→ computer readable
symbols compiler addresses
A programmer has no control over the addresses—that is the job of the operating system
(e.g., Linux) and the compiler. Programmers do not need to know the addresses of a, b, or
z as long as the following rules are observed:
• Each piece of data has a unique address.
• The address cannot be zero (NULL) or negative.
• The compiler can convert symbols to addresses.
2.2 Stack
Modern computers usually organize volatile memory into three types:
1. stack memory
2. heap memory
3. program memory
The first two store data and the last stores the machine code of computer programs.
This chapter focuses on stack memory. Heap memory will be explained in a later chapter.
Before talking about stack memory, we must first introduce the concept of a stack.
Technical terms in computing are often related to the everyday meanings of the words
that they comprise. “Stack” is no exception. Ever heard of a “stack of books”? The easiest
way to add a book to a stack of books is to place it on top of the stack. The easiest way to
remove will be from the top. Thus, the first book to be removed from the stack will be the
last book previously placed on the stack. Computer scientists refer to this arrangement as
“last in, first out” (or “first in, last out”). Placing an item is called push, and removing an
item is called pop.
The stack concept is used in everyday life. To wear both socks and shoes, the socks must
go on before the shoes—push the socks, push the shoes. Then, to remove both the socks
and the shoes, the shoes come off before the socks—pop the shoes, pop the socks. The order
is reversed and this is characteristic of “last in, first out”.
Stack memory strictly follows first in, last out. New data enters the stack memory at the
top, and data are always removed from the top. It would be equivalent to add and remove
data from the bottom (i.e., it is still first in, last out); however, by convention, we use the
top instead of the bottom. The concept is the same. Data are pushed onto the top of the
stack and, later, popped from the top of the stack. Fig. 2.1 illustrates these two operations
of stack memory.
653 653
push
top
653 653
720 720 720 720
386 386 386 386
pop
-15 -15 -15 -15
46 46 bottom
46 46
945 945 945 945
(a) (b)
FIGURE 2.1: Pushing and popping data on a stack. (a) Originally, the top of the stack
stores the number 720. The number 653 is pushed onto the top of the stack. (b) Data are
retrieved (popped) from the stack. Pushes and pops can only occur at the top of the stack.
Although this figure illustrates the idea with integers, a stack is a general concept and can
manage any type of data.
6 }
7 void f2 ( void )
8 {
9 f1 () ;
10 // program continues from here after f1 finishes
11 }
The function f2 calls f1 at line 10. After f1 finishes its work, the program continues
running f2 from the line after f1. Fig. 2.2 illustrates the flow of the program.
FIGURE 2.2: The flow of the program as indicated by the numbers 1, 2, and 3.
Imagine that a mark is inserted right below the place where f1 is called, as shown in
Fig. 2.3. This mark tells the program where it should continue after f1 finishes. It is called
the “return location”, meaning that this is the place where the program should continue
after the function f1 returns (i.e., after f1 finishes its work).
A function is finished when it executes the return statement—anything below this
statement is ignored. Consider the following example:
1 void f ( void )
2 {
3 i f (...)
Stack Memory 13
FIGURE 2.3: The return location is the place where the program continues after the
function f1 returns.
4 {
5 // ...
6 return ;
7 // the program will never reach here
8 }
9 // else not needed
10 // ...
11 return ;
12 // the program will never reach here
13 }
1 In this function, if the condition at line 3 is true, then the function will execute the
2 return at line 6. In this case, anything at line 7 is ignored and the program continues
3 from the return location. However, if the condition at line 3 is false, then the function will
4 execute the code at line 9. Note that it is not necessary to have an else at line 9. When the
5 function reaches line 11, a return is executed, and the function stops—line 12 is ignored.
6 Here, “ignored” means that the code is not executed when the program runs. Even though
7 lines 7 and 12 are never executed, if they contain any syntax errors, the source code will
8 not compile. Next, let’s consider three functions:
9 void f1 ( void )
10 {
11 // ...
12 }
13
14 void f2 ( void )
15 {
16 f1 () ;
17 // line after calling f1 , return location B
18 // ...
19 }
20
21 void f3 ( void )
22 {
23 f2 () ;
24 // line after calling f2 , return location A
25 // ...
26 }
14 Intermediate C Programming
Function f3 calls f2 at line 15, and f2 calls f1 at line 8. When f1 finishes, the program
continues from the line after calling f1 (line 9). When f2 finishes, the program continues
from the line after calling f2 (line 16). How does the program know where to continue after
a function finishes? When f3 calls f2, the machine-code equivalent to “line number 16” is
pushed to the stack memory. Fig. 2.4 shows the flow of function calls when running this
program.
FIGURE 2.4: The flow of the program with the three functions.
Imagine that the line after each function call is marked as a return location (RL), as
shown in Fig. 2.5. This book uses line numbers as the return locations. The call stack in
this book is a simplified conceptual model and does not reflect any specific processor. Real
processors use program counters instead of line numbers.
FIGURE 2.5: The return locations (RLs) are marked at the lines after calling f2 (RL A)
and f1 (RL B).
Why is the last in, first out nature of stack memory important? The stack memory
stores the reverse order of function calls. This is how the program knows that it should
continue from RL B instead of RL A after f1 finishes. The program uses the stack memory to
remember the return locations. This stack memory is also called the call stack (or callstack),
and every C program has one to control the flow of execution of its functions. Almost all
computer programming languages employ this scheme.
As our three-function program executes, the call stack may appear as follows: When f3
calls f2, the line number after calling f2 (RL A) is pushed to the call stack.
When f2 calls f1, the line number after calling f1 (RL B) is pushed to the call stack.
line number (9) after calling f1, i.e., RL B
line number (16) after calling f2, i.e., RL A
When f1 finishes, the line number 9 is popped and the program continues at this line
number (9). The call stack now has line number 16.
When f2 finishes, the line number is popped and the program continues at this line num-
ber (16). Programmers do not need to worry about marking return locations; the compiler
takes care of inserting the appropriate code to do this.
It is instructive to note why the stack must store the return locations. Consider this
example:
1 void f1 ( void )
2 {
3 // ...
4 }
5
6 void f2 ( void )
7 {
8 f1 () ;
9 // RL A
10 // some statements ...
11 f1 () ;
12 // RL B
13 // ...
14 }
Function f1 is called in two different locations (line 8 and line 11). When f1 is called
the first time at line 8, the program continues from line 9 (RL A) after f1 finishes. When
f1 is called the second time at line 11, the program continues from line 12 (RL B) after f1
finishes. A call stack is a simple scheme to manage the fact that, since the same function
(f1) can be called from multiple places, something must track the next line of code to
execute.
The rules for the call stack can be summarized as follows:
• When a function is called, the line number after this call is pushed onto the call stack.
This line number is the “return location” (RL). This is the place from which the
program will continue after the called function finishes (i.e., returns).
• If the same function is called from multiple lines, then each call has a corresponding
return location (the line after each function call).
• When a function finishes, the program continues from the line number stored at the
top of the call stack. The top of the call stack is then popped.
the variables x, y, and z are the arguments of the function f . In C programs, functions have
a similar syntax. Consider the following example:
1 void f1 ( i n t a , char b , double c )
2 {
3 // ...
4 }
5
6 void f2 ( void )
7 {
8 f1 (5 , ‘m ’ , 3.7) ;
9 // RL A
10 // ...
11 }
The inputs a, b, and c are the arguments for f1. When f1 is called, f2 must provide
three arguments and this information is pushed onto the call stack. The call stack stores
the arguments and their values above the return location.
Symbol Value
c 3.7
b ‘m’
a 5
Return Location line 9
Remember that there are no symbols inside of a computer program. Instead, as pre-
viously discussed, the computer’s memory has only addresses and values. Thus, the table
above is extended with another column to show the addresses. Every value has a unique
address—the arguments are stored in different physical parts of the computer’s circuitry—
and this property is guaranteed by the operating system and the hardware. A programmer
has no control over the precise addresses used. The addresses can vary widely on different
types of computers. This book uses 100, 101, ... for these addresses. By convention, the
addresses start from a smaller number at the bottom and increase upward.
Symbol Address Value
c 103 3.7
b 102 ‘m’
a 101 5
Return Location 100 line 9
The return location and the arguments together form a frame for the called function f1.
A frame occupies a contiguous chunk of memory. The above table can now be extended to
show the frame that the symbols, addresses, and values belong to.
Frame Symbol Address Value
c 103 3.7
b 102 ‘m’
f1
a 101 5
Return Location 100 line 9
What happens when there is another function call? Consider the following example:
1 void f1 ( i n t t , i n t u )
2 {
3 // ...
Stack Memory 17
4 }
5
6 void f2 ( i n t a , i n t b )
7 {
8 f1 ( a - b , a + b ) ;
9 // RL B
10 // ...
11 }
12
13 void f3 ( void )
14 {
15 f2 (5 , -17) ;
16 // RL A
17 // ...
18 }
Function f3 calls f2 so f2’s frame is pushed to the call stack. Argument a’s value is 5
because that is the value given to a when f3 calls f2 at line 15. Similarly, argument b’s
value is −17 because that is the value given to b when f3 calls f2 at line 15.
Frame Symbol Address Value
b 102 −17
f2 a 101 5
Return Location 100 line 16
Function f2 calls f1 and f1’s frame is pushed onto the call stack. Argument t’s value is
22 because that is the value of a−b at line 8. Similarly, argument u’s value is −12 because
that is the value of a + b at line 8.
Frame Symbol Address Value
u 105 −12
f1 t 104 22
Return Location 103 line 9
b 102 −17
f2 a 101 5
Return Location 100 line 16
Please remember that frames and symbols are for humans only. Computers do not under-
stand frames and symbols. Instead, they only work with addresses and values. Previously,
in Section 2.3.1, we listed the rules of the call stack; now we add some more.
• If a function has arguments, then the arguments are stored above the return location.
• The arguments and the return location together form the frame of the called function.
• When a function is called, the line number after this call is pushed onto the call stack.
This line number is the “return location” (RL). This is the place from which the
program will continue after the called function finishes (i.e., returns).
• If the same function is called from multiple lines, then each call has a corresponding
return location (the line after each function call).
• When a function finishes, the program continues from the line number stored at the
top of the call stack. The top of the call stack is then popped.
18 Intermediate C Programming
7 void f2 ( void )
8 {
9 f2 (5 , 11 , -8) ;
10 // RL A
11 }
The arguments k, m, and p are stored above the return location A. The local variables
t and u are stored on the call stack above the arguments.
Frame Symbol Address Value
u 105 -88
t 104 16
p 103 -8
f1
m 102 11
k 101 5
Return Location 100 line 12
are strongly discouraged, global constants are acceptable and commonly used because they
cannot change.
6 void f2 ( void )
7 {
8 int u;
9 u = f1 (7 , 2) ;
10 // RL A
11 }
The local variable u is inside f2 so it is in f2’s frame. The value of u is undefined because
it has not yet been assigned to anything. Remember that C does not initialize variables,
so uninitialized variables could store any values (i.e., garbage). The frame for f2 contains
the variable u whose value is undefined yet.
Frame Symbol Address Value
f2 u 100 garbage
The address of u is stored in the call stack before f1 is called. This address is called the
value address because it is the address where the return value of function f1 will be stored.
Thus, when the frame for f1 is constructed, one more row is added for the value address,
and its value is the address of u.
Frame Symbol Address Value
m 104 2
k 103 7
f1
Value Address 102 100
Return Location 101 line 10
f2 u 100 garbage
When function f1 executes, it adds the values of k and m, producing the value 9. The
number 9 is then written to (i.e., replaces) the original garbage value at address 100. After
f1 finishes, and its frame has been popped, the call stack will be as follows:
Frame Symbol Address Value
f2 u 100 9
This rule can be incorporated into the previous rules of the call stack.
• If a function returns a value, the value is written to a local variable in the caller’s
frame. This variable’s address (called the value address) is stored in the call stack.
• If a function has local variables, then the local variables are stored above the argu-
ments.
• If a function has arguments, then the arguments are stored above the return location.
20 Intermediate C Programming
• The arguments and the return location together form the frame of the called function.
• When a function is called, the line number after this call is pushed onto the call stack.
This line number is the “return location” (RL). This is the place from which the
program will continue after the called function finishes (i.e., returns).
• If the same function is called from multiple lines, then each call has a corresponding
return location (the line after each function call).
• When a function finishes, the program continues from the line number stored at the
top of the call stack. The top of the call stack is then popped.
Note that the caller (f2) is not obliged to store the return value of the callee (f1), and
line 9 in the example above can be written as:
9 f1 (7 , 2) ;
In this case, function f1 is called but the returned value is discarded. Since there is no need
to store the return value, the value address is not pushed onto the call stack.
The keyword return can be used for two different purposes:
• If void is in front of the function’s name, the function does not return any value. The
word return stops the function and the program continues from the return location
in the caller.
• If the function is not void, the word return assigns a value to the variable given by
the value address in the call stack.
Please remember that if a function executes a return statement, anything after the
return is ignored and will not be executed. Executing a return statement stops the func-
tion, and its frame is popped from the call stack. The program then continues from the
return location.
2.3.5 Arrays
The following example creates an array of five elements. Each element contains one
integer, which will be uninitialized.
1 i n t arr [5];
If an array has five elements, the valid indexes are 0, 1, 2, 3, and 4. The first index is
0, not 1; the last index is 4, not 5. The array is said to be “zero indexed”. In general, if an
array has n elements, the valid indexes are 0, 1, 2, ..., n − 1. Please remember that n is
not a valid index. This is a common mistake among students.
Programmers have no control over addresses and this is still true for arrays. The ad-
dresses of an array’s elements are, however, always contiguous. Suppose i < j < k and all
of them are valid indexes for an array called arr. Then the address of arr[j] is between
the addresses of arr[i] and arr[k]. If an array’s elements are not initialized (like in the
example above), then the values are garbage.
The following example illustrates C’s facility to initialize arrays:
1 i n t arr [5] = { -31 , 52 , 65 , 49 , -18};
Stack Memory 21
The output will probably be different when the program is run again:
As you can see, the addresses change. If you execute the same program, you will likely
see different addresses.
2.4 Visibility
Every time a function is called, a new frame is pushed to the call stack. A function
can see only its own frame. Consider these two examples:
22 Intermediate C Programming
1 i n t f1 ( i n t k , i n t m ) 1 i n t f1 ( i n t a , i n t b )
2 { 2 {
3 return ( k + m ) ; 3 return ( a + b ) ;
4 } 4 }
5 5
These two programs are identical. Renaming the arguments of f1 from k and m to a and
b has no effect. What about the call stack? This is the call stack when f1 is called in the
first example:
Frame Symbol Address Value
m 106 2
k 105 8
f1
Value Address 104 102
Return Location 103 line 14
u 102 garbage
f2 b 101 6
a 100 5
The call stack in the second example is the same, except that the arguments in frame f1
have different symbols. Note that the addresses are the same. The second example highlights
the fact that the a and b in f1 refer to different address–value pairs than the a and b in f2.
This is the call stack:
Frame Symbol Address Value
b 106 2
a 105 8
f1
Value Address 104 102
Return Location 103 line 14
u 102 garbage
f2 b 101 6
a 100 5
The a in f1’s frame has nothing to do with the a in f2’s frame. Renaming a to k makes
no difference to the behavior of the program. The same rule applies to b. Remember that
computers do not know about symbols. Computers only use addresses and values. Symbols
are only useful for any humans that are reading the code, and are discarded when a program
is compiled into machine-readable format.
This can be a source of confusion among students. It may seem intuitive that the a in
f1’s frame and the a in f2’s frame are related. In fact, they occupy different locations in
the call stack and are unrelated. The following example offers a further explanation:
1 i n t f1 ( i n t a , i n t b )
2 {
3 a = a + 9;
Stack Memory 23
4 b = b * 2;
5 return ( a + b ) ;
6 }
7
8 void f2 ( void )
9 {
10 i n t a = 5;
11 i n t b = 6;
12 int u;
13 u = f1 ( a + 3 , b - 4) ;
14 // some additional code
15 }
The following table shows the call stack when the program has entered f1 but has not
yet executed line 3:
Frame Symbol Address Value
b 106 2
a 105 8
f1
Value Address 104 102
Return Location 103 line 14
u 102 garbage
f2 b 101 6
a 100 5
After line 3 has been executed, the call stack will appear as in the table below. Note
that function f1 only modifies the variable a that is in its frame, since a function can only
see arguments and variables in its own frame.
Frame Symbol Address Value
b 106 2
a 105 8 → 17
f1
Value Address 104 102
Return Location 103 line 14
u 102 garbage
f2 b 101 6
a 100 5
The following table shows the call stack after the program has executed line 4:
Frame Symbol Address Value
b 106 2→4
a 105 8 → 17
f1
Value Address 104 102
Return Location 103 line 14
u 102 garbage
f2 b 101 6
a 100 5
10 void f2 ( void )
11 {
12 i n t a = 5;
13 i n t b = 6;
14 int u;
15 u = f1 ( a + 3 , b - 4) ;
16 // some additional code
17 }
In review, this chapter explains the concept of the call stack, which is used whenever a
function is called. The call stack stores the return location, the value address, the arguments,
and the local variables for each function.
2.5 Exercises
This book has two types of homework: exercises and programming problems. Exercises
are problems that do not require writing programs—they are “paper-and-pencil” problems.
Programming problems, obviously, are done on a computer.
Understanding the call stack is one of the most essential skills for programmers. If you
want to understand C programs (and many other programming languages), then a solid
understanding about the call stack is necessary.
1 i n t f1 ( i n t k , i n t m )
2 {
3 int y;
4 y = k + m;
5 return y ;
Stack Memory 25
6 }
7
8 void f2 ( void )
9 {
10 i n t a = 83;
11 i n t c = -74;
12 c = f1 (a , c ) ;
13 /* RL */
14 }
Draw the call stack
• before f1 is called.
• when the program has finished line 4.
• when the program has finished f1 and the top frame has been popped.
1 void f1 ( i n t k , i n t m )
2 {
3 int y;
4 y = k;
5 k = m;
6 m = y;
7 }
8
9 void f2 ( void )
10 {
11 i n t a = 83;
12 i n t c = -74;
13 f1 (a , c ) ;
14 /* RL */
15 }
Draw the call stack
• when the program has entered f1 and finished line 4. What are the values of k and m?
• when the program has finished line 6, and before f1’s frame is popped. What are the
values of k and m?
• when the program has finished f1 and f1’s frame has been popped. What are the
values of a and c?
2.5.3 Addresses
• How can a programmer control the address of a variable?
• If the same program runs multiple times, will the address of the same variable be the
same?
• Are the addresses of an array’s elements contiguous or scattered?
26 Intermediate C Programming
2.6 Answers
2.6.1 Draw Call Stack I
• before calling f1
Frame Symbol Address Value
c 101 −74
f2
a 100 83
• finished line 4
Frame Symbol Address Value
y 106 9
m 105 −74
f1 k 104 83
Value Address 103 101
Return Location 102 line 13
c 101 −74
f2
a 100 83
• finished line 6
Frame Symbol Address Value
y 105 83
m 104 83
f1
k 103 −74
Return Location 102 line 14
c 101 −74
f2
a 100 83
2.6.3 Addresses
• A programmer cannot control the address of a variable.
• If the same program runs multiple times, the address of the same variable will likely
be different.
• The addresses of an array’s elements are contiguous.
11 i n t g2 ( i n t a , i n t b )
12 {
13 i n t c = g1 ( a + 3 , b - 11) ;
14 printf ( " g2 : a = %d , b = %d , c = % d \ n " , a , b , c ) ;
15 return c - b ;
16 }
17
This uses gcc to convert the source file of the C program (p1.c), into an executable file that
the computer can understand. Adding -g enables debugging so that we can examine the
call stack. Adding -Wall and -Wshadow enables warning messages. Shadow variables will be
explained in Section 4.1. Warning messages are sometimes benign, but they usually indicate
deeper problems in the code. It is good practice to always enable warning messages, and to
act on gcc’s advice. The name of the output file (i.e., the executable file) is specified by -o.
In this example, p1 is the output of the gcc command and, thus, is the executable file (i.e.,
the program). It can be run in the terminal by typing:
$ ./p1
To view the call stack, we will need to start the debugger. In this example, we will use
DDD (Data Display Debugger). DDD is a graphical user interface for the GDB debugger.
Start DDD, go to the menu and click
File - Open Program - select p1 - Open
Here, we have selected the executable program, not the .c file. The debugger will automat-
ically find the .c file based on information that gcc leaves in the executable. This is useful
when debugging a program that uses multiple source files.
Set breakpoints at the two functions g1 and g2 with the following commands after the
(gdb) prompt in the bottom window:
(gdb) b g1
(gdb) b g2
The command b g1 instructs DDD to set a breakpoint when the function g1 starts.
When the program reaches the first line of g1, the program will stop and you will get a
chance to check the status of the program. The command b g2 instructs DDD to similarly
set a breakpoint when the function g2 starts.
Execute the program by typing the following command at the (gdb) prompt:
(gdb) run
The program will start, and then pause at the breakpoint of function g2. Why does the
program stop at g2, not g1? Because main calls g2, so g2 is encountered before g1. If
several breakpoints are set, the program will pause at the breakpoints based on the order
in which they are executed, not the order in which they are set. In this example, although
the breakpoint at g1 is set first, the program executes g2 first. Thus, the program pauses
at the breakpoint g2 first.
To continue the program, type the following command:
(gdb) continue
The program will continue executing and then pause at the next breakpoint, located at
function g1. The call stack can be viewed by asking for the backtrace. This is done with
the following command:
(gdb) bt
Stack Memory 29
(gdb) bt
#0 g1 (a=7, b=23) at p6.c:6
#1 0x0000000000400554 in g2 (a=4, b=34) at p6.c:13
#2 0x00000000004005b2 in main (argc=1, argv=0x7fffffffe4f8) at p1.c:22
The values of a and b are shown in the top frame. The beginning of each line shows the
frames (0, 1, and 2) of the call stack, corresponding to the functions g1, g2, and main. You
can use the f command to see different frames: for example, type
(gdb) f 1
to go to frame 1, i.e., the frame of function g2. The values of a and b can be displayed again.
What are their values? The digits after 0x are likely different on your computer; these are
the addresses. In g2’s frame, the values of a and b are different from the values in the top
frame. Fig. 2.6 to Fig. 2.9 show some screenshots of DDD.
FIGURE 2.7: Use the mouse to select a inside g1. Click the right mouse button and select
“Display a”. Do the same for b.
Some books suggest that software should be well-designed, carefully written, and never
debugged. These books do not say anything about debugging. From my experience writing
programs, working with students, and talking to people in the software industry, debugging
is difficult to avoid completely, even when software is planned and written carefully. In some
ways, debugging is like editing an article. It is very difficult to write a good article without
any editing. Even though debugging is difficult to avoid completely, it should not be relied
upon. Experienced programmers carefully prevent bugs from happening and detect them
as early as possible.
Many people learn software development by writing small programs (tens of lines for
each program). This is good because learning should progress in stages. The problem is that
many people hold onto habits acceptable for small programs when they attempt to write
larger programs. Writing a program of 400 lines requires different strategies than writing a
program of 40 lines. This book is written for people learning how to write programs that are
between 100 and 1,000 “lines of code” (LoC). Although LoC is not a particularly good way
of measuring software complexity, it does serve as a very basic yardstick for how complex a
program might be. Finding a good way to measure software complexity is beyond the scope
of this book. Instead, this book gives some suggestions on how to write correct programs.
33
34 Intermediate C Programming
this case, you should ask the purposes of these assignments. In particular, there should
be some learning objectives. Without knowing the purposes, it is impossible to understand
how to evaluate software. This is increasingly important as software becomes more complex.
Complex software has many parts and you need to understand why these parts are needed
and how they affect each other. Developing software requires many steps before, during,
and after coding. The following gives a few principles for you.
1 i f ( a > 0) ;
2 {
3 ... // always runs , not controlled by the if condition
4 }
The semicolon ; ends the if condition. As a result, the code inside { and } is not
controlled by the if condition and always runs.
• Run some simple test cases in your head. If you do not understand what your program
does, the computer will not be able to do what you want.
• Write code to test whether certain conditions are met, before proceeding. Suppose
sorting is part of a program: Check whether the data is sorted before the program
does anything else.
• Avoid copying and pasting code; instead, refactor the code by creating a function
and, thus, avoiding duplication. If you need to make slight changes to the copied
code, use the function’s argument(s) to handle the differences. This is a tried-and-
true principle: Similar code invites mistakes. You will soon lose track of the number of
copies and the differences among similar code. It is difficult to maintain the consistency
of multiple copies of the code. You will likely find that your program is correct in some
situations and wrong in others. Finding and removing this type of bug can be very
time-consuming. Your best strategy is to avoid it in the first place. It is better to write
a program that is always wrong than a program that is sometimes right. If it is always
wrong, and the problems come from only a single place, you can focus on that place.
If the problems do not consistently appear and come from many possible places, it is
more difficult to identify and remove the mistakes.
• Use version control. Have you ever had an experience like this: “Some parts of the
program worked yesterday. I made some changes and nothing works now. I changed
so many places that I don’t remember exactly what I have changed.”? Version control
allows you to see the changes from the previous commit.
• Resolve all compiler warnings. Many studies have shown that warnings are likely to
be serious errors, even though they are not syntax errors. Some people ignore warning
messages, thinking that they can handle the warnings after they get their programs
to work. However, the warning messages frequently indicate the problems preventing
their programs from working.
think. This section considers only coding mistakes, not design mistakes. Design mistakes
require a different book on the subject of designing software.
scenario is that your program interfaces with the physical world (e.g., controlling a robot).
The physical world does not wait for your program and it cannot slow down too much.
Logging also slows down a program; thus, do not add excessive amounts of logging.
In many other cases, you can slow down your programs and debug the programs
interactively—run some parts of the programs, see the intermediate results, change the
programs, run them again, continue the process until you are convinced the programs are
correct. For interactive debugging, printing debugging messages is usually ineffective and
time-wasting. There are several problems with printing debugging messages for interactive
debugging:
• Code needs to be inserted for printing debugging messages . This can be a considerable
amount of effort. In most cases, the debugging messages must be removed later because
debugging messages should appear in neither the final code nor its output.
• If there are too few messages, there is insufficient information to help you determine
what is wrong.
• If there are too many messages, some messages may be irrelevant and should be
ignored. Getting the right amount of messages, not too few and not too many, can be
difficult.
• Worst of all, problems are likely to occur at unexpected places where no debugging
messages have been inserted. As a result, more and more debugging messages must
be added. This can be time-consuming and frustrating.
Instead of using debugging messages in interactive debugging, gdb (or DDD) is a better
tool in most cases. I have shown you some gdb commands. I will describe more commands
later in this book.
func(arguments) test_func(arguments)
{ {
/* do work to get result */ /* create arguments */
/* test to check result */ result = func(arguments);
} /* check the result */
}
What is the difference between these two approaches? The first (located on the left) calls
the testing code inside a function of your program. In the second (located on the right),
the testing code is outside your program and the testing code calls func. This difference
is important because the first mixes the testing code with the actual code needed for your
38 Intermediate C Programming
program (sometimes called “production code”). As a result, it will be difficult for you to re-
move the testing code. The second approach separates the testing code from the production
code, so that you can easily remove the testing code later on. You should take the second
approach whenever you test your program.
Chapter 4
Pointers
4.1 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 The Swap Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.4 The Swap Function Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.5 Type Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.6 Arrays and Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.7 Type Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.8 Pointer Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.1 Swap Function 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.2 Swap Function 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.3 Swap Function 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.9.4 Swap Function 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.9.5 Swap Function 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.9.6 15,552 Variations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.10 Answers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.10.1 Swap Function 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.10.2 Swap Function 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.3 Swap Function 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.4 Swap Function 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.10.5 Swap Function 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.1 Scope
Chapter 2 described several rules. One of those rules was that each function can see only
its own frame. This is called scope. A new scope is created every time a pair of { and } is
used. This could be inside of a function body, for example, an if statement, or a while loop.
The following example shows two scopes:
1 void f ( i n t a , i n t b )
2 {
3 /* this is a scope , call it X */
4 int i;
5 f o r ( i = 0; i < a + b ; i ++)
6 {
7 /* this is another scope , call it Y */
8 int j;
9 }
10 }
39
40 Intermediate C Programming
In the scope marked X (in the comments), a and b are arguments and i is a local
variable. Another scope called Y is created inside of X. Scopes are always nested inside of
each other like this. Variables from outer scopes are still accessible, so scope Y can “see” a,
b, and i. A local variable j is created inside of scope Y and is accessible only inside scope
Y; scope X cannot “see” j.
The following example has three scopes: X, Y, and Z. The arguments a and b in f1
have nothing to do with the arguments of a and b in f2 because they are in different and
non-overlapping scopes: f1 is not nested in f2 or vice versa.
The variable j is in the inner scope, and can only be “seen” between the { and } of the
for loop.
1 void f1 ( i n t a , i n t b )
2 {
3 /* this is a scope , call it X */
4 int i;
5 f o r ( i = 0; i < a + b ; i ++)
6 {
7 /* this is another scope , call it Y */
8 int j;
9 }
10 }
11
12 void f2 ( i n t a , i n t b )
13 {
14 /* this is a scope , call it Z */
15 f1 ( a + b , a - b ) ;
16 /* RL A */
17 }
It is legal to create another variable of the same name in an inner scope, like u.
1 void f1 ( i n t u , i n t v )
2 {
3 /* this is a scope , call it X */
4 int i;
5 f o r ( i = 0; i < u + v ; i ++)
6 {
7 /* this is another scope , call it Y */
8 int j;
9 i n t u ; /* shadow variable */
10 }
11 }
In the inner scope Y, we create a new variable called u. Please note that Y already has
an argument called u. By adding the type int in front of u, a new variable is created. This
function now has two variables both called u in the overlapping scopes (Y is nested inside
of X). This makes the u in Y a shadow variable of the u in X. These two variables have two
different memory addresses. Modifying the u in scope Y does not change the u in scope
X. Shadow variables are considered bad programming style because they make programs
difficult to understand, and can introduce subtle errors. Consider the following example:
1 void f1 ( i n t u , i n t v )
2 {
3 /* this is a scope , call it X */
Pointers 41
4 int i;
5 u = 27;
6 f o r ( i = 0; i < u + v ; i ++)
7 {
8 /* this is another scope , call it Y */
9 i n t u ; /* shadow variable because of int */
10 u = 5;
11 }
12 /* u is 27 even though it was assigned to 5 two lines
13 earlier */
14 }
The value of u is 5 just above the closing brace (line 11) that encloses scope Y. After
leaving Y, the value of u is 27 because the outer u was never changed. This can make the
program confusing, and confusing programs are error-prone. Fortunately most compilers
make it is easy to detect shadow variables. For example, gcc compiler warns about shadow
variables when you add -Wshadow to gcc.
6 void f2 ( void )
7 {
8 int s;
9 s = f1 (2 , 37) ;
10 /* RL A */
11 }
In the function f2, we see s becomes the sum of 2 and 37. By calling f1 we are able
to change one variable in the caller f2. What can we do if we want to change two or more
variables in the caller? Suppose we want to write a “swap function”,
1 void swap ( i n t x , i n t y )
2 {
3
6 }
7
8 void f2 ( void )
9 {
10 i n t a = 2;
11 i n t b = 37;
42 Intermediate C Programming
12 swap (a , b ) ;
13 /* RL A */
14 }
Can this swap function work?
1 void swap ( i n t x , i n t y )
2 {
3 int z = x;
4 x = y;
5 y = z;
6 }
When the swap function is called, the values of a and b are copied to the arguments x and
y. The call stack is shown below.
Frame Symbol Address Value
z 106 -
y 105 37
swap
x 104 2
Return Location 103 line 13
b 102 37
f2
a 101 2
The value of x is stored in a temporary variable z. Then y’s value is assigned to x and
z’s value is assigned to y. After these three steps, x has y’s old value and y has x’s old value
(through z). After finishing line 5 in swap and before the top frame is popped, this is the
call stack:
Frame Symbol Address Value
z 106 2
y 105 2
swap
x 104 37
Return Location 103 line 13
b 102 37
f2
a 101 2
Inside swap, the values of x and y have been swapped. As explained in Chapter 2, when
swap finishes, the top frame is popped. After the top frame is popped, the call stack becomes
Frame Symbol Address Value
b 102 37
f2
a 101 2
The swap function was called and finished, and the values of a and b have not changed.
This swap function does not work. C programs use “call-by-value” when calling functions
That means that values are copied from the caller to the arguments of the called function
(i.e., callee). This is the only way to call functions in C. Java and C++ have call-by-value
and call-by-reference but C only uses call-by-value.
Does this mean it is impossible to write a swap function?
Pointers 43
4.3 Pointers
C solves this problem by creating the concept of pointers. A pointer is a variable (or an
argument) whose value is a memory address. To create a pointer, add * after the type.
1 type * ptr ; // ptr is a pointer , meaning its value is a
2 memory address
This creates a pointer called ptr. Its value is an address. At that address is stored a
value of the given type. This may seem abstract so let us see some concrete examples:
1 int * iptr ;
2 char * cptr ;
3 double * dptr ;
In each case, the pointer stores a memory address. Chapter 2 said programmers cannot
control addresses. How can a program obtain valid addresses? C provides special syntax for
precisely that purpose: by adding an & in front of a variable. For example,
1 i n t a = -61; // a is an integer
2 i n t * iptr ; // iptr is a pointer
3 iptr = & a ; // iptr ’s value is a ’s address
Section 2.3.6 prints the addresses of two variables a and c. It shows that the addresses
change when the same program runs again. By using an ampersand (&) in front of a, iptr’s
value changes every time the program runs. Please remember that a programmer can
change variables’ values but a programmer cannot change variables’ addresses.
You may want to ask why this example uses 100 for a’s address but the addresses in
Section 2.3.6 are much larger values. In order to make the book easier to read, the book
uses small addresses instead of the much larger addresses that are common on modern
computers.
How are pointers useful? First, just like any other variable type, two pointers can have
the same value.
1 i n t a = 632;
2 int c; /* c ’s value is garbage now */
3 c = a; /* c ’s value is the same as a ’s value */
4 i n t * iptr1 ; /* iptr1 ’s value is garbage now */
5 i n t * iptr2 ; /* iptr2 ’s value is garbage now */
6 iptr1 = & a ; /* iptr1 ’s value is a ’s address */
7 iptr2 = iptr1 ; /* iptr2 and iptr1 have the same value */
44 Intermediate C Programming
After executing the first line, an integer called a has been created and its value is 632.
The second line creates another integer variable called c and its value is not defined yet.
This is the snapshot of the call stack after finishing line 2.
Symbol Address Value
c 101 garbage
a 100 632
The third line makes c’s value the same as a’s value.
Symbol Address Value
c 101 632
a 100 632
The fourth and the fifth lines create two pointers. Their values are currently undefined.
Symbol Address Value
iptr2 103 garbage
iptr1 102 garbage
c 101 632
a 100 632
The third and the seventh lines are similar: The third line assigns a’s value to c’s value;
the seventh line assigns the value of iptr1 to the value of iptr2.
A second way to use pointers is to retrieve the value stored at their addresses.
1 i n t a = 632;
2 i n t * iptr ;
3 int c;
4 iptr = & a ;
5 c = * iptr ;
6 /* read iptr ’s value as an address , go to that address ,
7 read the value at that address , assign the value to c */
8 printf ( " % d " , * iptr ) ;
9 * iptr = -84;
Pointers 45
Shown below is the call stack after the program finishes the fourth line.
Symbol Address Value
iptr 102 100
c 101 garbage
a 100 632
Many students find pointers confusing at first. This confusion is well justified. The same
symbol * has different meanings. The symbol also means multiplication when it is between
two numeric values (integer, float, double). The following table summarizes the different
meanings:
It is time to test your understanding of the different usages of *. Draw the call stack for
the following code snippet:
46 Intermediate C Programming
Example Meaning
1. int * iptr; Create a pointer variable. ptr’s value is an address. An integer is
stored at that address.
* is after the type (int in this case)
2. iptr = & val Assign val’s address to ptr’s value.
This is how to assign a valid address to ptr; note that * is not
used.
3. = * ptr (right hand side of assignment, RHS) Take ptr’s value as an address
and read the value at that address.
= is not always necessary, for example, when printing or calling a
function.
4. * ptr = (left hand side of assignment, LHS) Take ptr’s value as an address
and modify the value at that address.
5. 5 * 17 Multiplication: 5 * 17 is 85.
In this case, * is between two numbers
TABLE 4.1: Different usages of * in C programs. Please notice that ptr = and * ptr =
have different meanings.
1 int a = 21;
2 int c = -4;
3 int * ptr ;
4 ptr = & a;
5 * ptr = 7;
6 c = * ptr ;
7 * ptr = a * c;
After executing the first three lines, the call stack is shown below.
Symbol Address Value
ptr 102 garbage
c 101 −4
a 100 21
The fifth line has * ptr at the left hand side of the assignment sign. This assigns value
7 to the address 100.
Symbol Address Value
ptr 102 100
c 101 −4
a 100 21 → 7
The sixth line reads the value at address 100; the value is 7. This value is assigned to c.
The call stack is shown below.
Pointers 47
The seventh line reads the values of a and c; both are 7. The symbol * is used twice.
At the right hand side, * means multiplication and the result is 49. Then, 49 is assigned to
the value at address 100. This changes a’s value to 49.
Symbol Address Value
ptr 102 100
c 101 7
a 100 7 → 49
6 }
7
8 void f ( void )
9 {
10 i n t a = 83;
11 i n t c = -74;
12 swap ( /* the addresses of a and c */ ) ;
13 /* RL */
14 }
Since the function f must provide the addresses of a and c, the swap function’s arguments
must be pointers that store these addresses.
1 void swap ( i n t * k , i n t * m )
2 {
48 Intermediate C Programming
6 }
7
8 void f ( void )
9 {
10 i n t a = 83;
11 i n t c = -74;
12 swap (& a , & c ) ;
13 /* RL */
14 }
This is the call stack when starting the swap function.
Frame Symbol Address Value
m 104 101
swap k 103 100
Return Location 102 line 13
c 101 −74
f
a 100 83
9 void f ( void )
10 {
11 i n t a = 83;
12 i n t c = -74;
13 swap (& a , & c ) ;
14 }
The third line reads the value at the address of 100 and stores the value in s.
Frame Symbol Address Value
s 105 83
swap m 104 101
k 103 100
Return Location 102 line 13
c 101 −74
f
a 100 83
The fourth line reads the value stored at address 101; the value is −74. This value is
stored at the address 100.
Pointers 49
Note that the values of a and c have been changed. Pointers are a central feature of C
programming, and they must be handled carefully. The swap function should be understood
thoroughly, since it is a simple example of using pointers. You should understand how to
call swap, how it is implemented, and why it is implemented in the way that it is.
Section 2.4 says a function can see only its own frame. However, the swap function
modifies the values of a and c even though a and c are in a different frame. Does this mean
the rule in Section 2.4 is violated? The answer is no. The swap function still cannot access
a or c directly. The swap function can access a or c indirectly because k and m store the
addresses of a and c. Through pointers, a function can access (i.e., read or write)
the values of variables in another frame.
By using pointers, the swap function can read or write the values in f’s frame. Is it
possible for f to use pointers to read or write variables in swap’s frame? We can illustrate
this question with a simple example. Will the following code change m’s value from 0 to 7?
1 i n t * f1 ( void )
2 {
3 i n t m = 0;
4 return & m ;
5 }
6
7 void f2 ( void )
8 {
9 i n t * iptr = f1 () ;
10 /* RL */
11 * iptr = 7;
12 }
The answer is no: m exists only inside of f1’s frame.
• Before calling f1, m does not exist.
• When running the code in f1, the program executes the statements in f1, not in f2.
50 Intermediate C Programming
• After f1 finishes, the program continues from the return location (line 10). The top
frame has been popped and m no longer exists.
Hence, it is impossible for f2 to modify m. In fact, most compilers will warn you that the
fourth line is likely a mistake. Using pointers to read or write only works in one direction.
If f2 calls f1, f1 can read or write values in f2’s frame but f2 cannot read or write values
in f1’s frame. This rule can be generalized: Through pointers, a function can read or write
values stored in the function’s frame or the stack frames below it. It is impossible to read
or write values in a frame that is above the function’s frame.
Function f calls function sumarr with two arguments: arr and 5. The former is the
address of arr[0]. This is the call stack after starting function sumarr before the fifth line.
Frame Symbol Address Value
sum2 111 0
ind 110 garbage
len 109 5
sumarr
intarr 108 101
value address 107 100
return location 106 line 21
arr[4] 105 9
arr[3] 104 3
arr[2] 103 2
f
arr[1] 102 −7
arr[0] 101 4
sum 100 0
Please pay special attention to the value of intarr at address 108. The value is 101
because the address of the first element, i.e., & arr[0], is 101. In C programs, an array
itself does not provide information about the number of elements. As a result, when calling
sumarr, another argument is needed for the number of elements. Because intarr has the
address of the array’s first element, function sumarr can read the array’s elements even
Pointers 53
though the array is stored in a different frame. The for loop adds the elements’ values and
stores the result in sum2. This is the call stack after finishing the for loop.
Frame Symbol Address Value
sum2 111 0 → 11
ind 110 5
len 109 5
sumarr
intarr 108 101
value address 107 100
return location 106 line 21
arr[4] 105 9
arr[3] 104 3
arr[2] 103 2
f
arr[1] 102 −7
arr[0] 101 4
sum 100 0
The value of sum2 is then written to the value at address 100 (sum’s address). This is
the call stack after function sumarr has finished.
Frame Symbol Address Value
arr[4] 105 9
arr[3] 104 3
arr[2] 103 2
f
arr[1] 102 −7
arr[0] 101 4
sum 100 0 → 11
Because an array is passed as a pointer to the first element, a function can modify the
values of an array in another frame. Consider this example:
1 void incrarr ( i n t * intarr , i n t len )
2 {
3 i n t ind ;
4 f o r ( ind = 0; ind < len ; ind ++)
5 {
6 intarr [ ind ] ++;
7 }
8 }
9 void f ( void )
10 {
11 i n t arr [5];
12 arr [0] = 4;
13 arr [1] = -7;
14 arr [2] = 2;
15 arr [3] = 3;
16 arr [4] = 9;
17 incrarr ( arr , 5) ;
18 /* RL */
19 }
This is the call stack after entering incrarr before executing the for loop.
54 Intermediate C Programming
• Pointers are not necessarily arrays. For example, t * ptr creates a pointer of type t
and it is not related to any array. Hence, arr = ptr can be a dangerous assignment
because operations like arr[1] may read from (or write to) an invalid address.
giving the size, by putting nothing between [ and ]. The compiler will automatically cal-
culate the array’s size. Lines 9 to 11 calculate the lengths of the three arrays. So far we
use the same size for different types (int, char, double) but different types actually take
up different amounts of memory, and therefore have different sizes. Thus, these three lines
divide the array sizes by the types’ sizes in order to get the numbers of the elements. Line
12 prints the lengths. As you can see, the program prints the correct lengths. This method
for calculating an array’s size is valid only for constant arrays. If an array is created using
malloc, this method will not work. A later chapter will explain malloc.
Lines 13 to 15 assign the addresses of the first element in each array to the pointers.
These three lines are equivalent to
1 int * iptr = & arr1 [0];
2 char * cptr = & arr2 [0];
3 double * dptr = & arr3 [0];
Do not mix the pointer types. For example, the following statements are wrong:
1 i n t * iptr = arr2 ;
2 i n t * iptr = arr1 [1];
The first is wrong because arr2 is an array of char. The second is wrong because arr1[1]
is an int, and is not an address.
Line 16 prints the values stored at the corresponding addresses. The printed values are
the first elements. Lines 17 to 19 are called pointer arithmetic. Each pointer is advanced by
one. This means specifically, that each pointer now points to the next element of the array.
Line 20 prints the values stored at the corresponding addresses. The printed values are the
second elements. Lines 21 to 23 make each pointer point to the next element. Line 24 prints
the values stored at the corresponding addresses. The printed values are the third elements.
Even though different types have different sizes in memory, the compiler will automatically
move the pointers to correctly point to the next elements.
The sizes of types are not fixed by the C language, and can vary depending on the
computer, operating system, and specific compiler options chosen to compile the code. The
following program prints the sizes of various types:
1 // arithmetic2 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t main ( i n t argc , char * * argv )
5 {
6 int arr1 [] = {7 , 2 , 5 , 3 , 1 , 6 , -8 , 16 , 4};
7 char arr2 [] = { ’m ’ , ’q ’ , ’k ’ , ’z ’ , ’% ’ , ’ > ’ };
8 double arr3 [] = {3.14 , -2.718 , 6.626 , 0.529};
9 long i n t addr10 = ( long i n t ) (& arr1 [0]) ;
10 long i n t addr11 = ( long i n t ) (& arr1 [1]) ;
11 long i n t addr12 = ( long i n t ) (& arr1 [2]) ;
12 printf ( " % ld , % ld , % ld \ n " , addr12 , addr11 , addr10 ) ;
13 printf ( " % ld , % ld \ n " , addr12 - addr11 , addr11 - addr10 ) ;
14 long i n t addr20 = ( long i n t ) (& arr2 [0]) ;
15 long i n t addr21 = ( long i n t ) (& arr2 [1]) ;
16 long i n t addr22 = ( long i n t ) (& arr2 [2]) ;
17 printf ( " % ld , % ld , % ld \ n " , addr22 , addr21 , addr20 ) ;
18 printf ( " % ld , % ld \ n " , addr22 - addr21 , addr21 - addr20 ) ;
19 long i n t addr30 = ( long i n t ) (& arr3 [0]) ;
20 long i n t addr31 = ( long i n t ) (& arr3 [1]) ;
Pointers 57
We have already discussed lines 6 to 8, but what do lines 9 to 11 do? At the right side
of the assignment, & arr1[0] gets the address of the first element of arr1. This address
is assigned to addr10. Because this code is compiled on a 64-bit computer, the memory
addresses use 64 bits, and require the long int type to store the addresses. We need to use
some special syntax (called a type cast) to tell the compiler to store the memory address
inside an integer. This is why we have (long int) after =. In general, storing memory
addresses in integers is a very bad idea, because it can lead to subtle problems when the
code is compiled under different circumstances. Using (long int) is telling the compiler “I
know this is wrong, but trust me, I want to do it.” The purpose of this program is to show
you that the sizes of different types can be different.
Lines 10 and 11 get the addresses of the second, and the third elements of the array. Line
12 prints the values of these long integers. In printf, %ld is used to print a longer integer.
The value changes if you execute the program again. However, line 13 always prints 4, 4
meaning that the addresses of two adjacent elements differ by 4. This means each integer
uses 4 bytes of memory. Line 17 prints some addresses and they change when the program
is executed again. Line 18 always prints 1, 1 meaning that the addresses of two adjacent
elements differ by 1. Thus, each character needs 1 byte of memory. Line 23 always prints
8, 8 meaning that the addresses of two adjacent elements differ by 8. Thus, each double
needs 8 bytes of memory.
The next example combines these two programs.
1 // arithmetic3 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t main ( i n t argc , char * * argv )
5 {
6 int arr1 [] = {7 , 2 , 5 , 3 , 1 , 6 , -8 , 16 , 4};
7 char arr2 [] = { ’m ’ , ’q ’ , ’k ’ , ’z ’ , ’% ’ , ’ > ’ };
8 double arr3 [] = {3.14 , -2.718 , 6.626 , 0.529};
9 int * iptr = & arr1 [3];
10 printf ( " % d \ n " , * iptr ) ;
11 long i n t addr13 = ( long i n t ) iptr ;
12 iptr - -;
13 printf ( " % d \ n " , * iptr ) ;
14 long i n t addr12 = ( long i n t ) iptr ;
15 printf ( " addr13 - addr12 = % ld \ n " , addr13 - addr12 ) ;
16 printf ( " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = \ n " ) ;
58 Intermediate C Programming
17
3
5
addr13 - addr12 = 4
=====================================
q
k
addr22 - addr21 = 1
=====================================
6.626000
-2.718000
addr32 - addr31 = 8
Line 9 assigns the address of arr1[3] to iptr and line 10 prints the value stored at that
address. As you can see in this example, iptr does not have to start from the first element
of the array. Line 11 stores iptr’s value in addr13. Please remember that iptr’s value is
an address. Line 12 decrements iptr’s value and line 13 prints the value at address. The
value is 5, the same as arr1[2]. Line 14 stores iptr’s value in addr12. Line 15 shows the
differences of the two addresses stored in addr13 and addr12 and the difference is 4, not 1.
What does this mean? Even though line 12 decrements iptr by one, the compiler ac-
tually decreases iptr’s value by 4 because the size of an integer is 4. In other words, the
specific change in iptr’s value depends on the size of the type being pointed to. The outputs
for the other two arrays further illustrate this point. Line 24 prints 1 and line 33 prints 8
because of the sizes of the types being pointed to. This explains why mixing types can be
problematic. For example,
1 i n t * iptr = arr2 ; // arr2 is a char array
2 i n t * iptr = arr3 ; // arr3 is a double array
Programs have odd behavior when the types are mixed like this.
Pointers 59
4.9 Exercises
4.9.1 Swap Function 1
Does this program have any syntax problems because of wrong types (such as assigning
an integer to a pointer’s value)? Will this function actually swap the values of u and t?
What is the program’s output? Please draw the call stack and explain.
1 // swap1 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 void swap1 ( i n t a , i n t b )
5 {
6 int k = a;
7 a = b;
8 b = k;
9 }
10 i n t main ( i n t argc , char * * argv )
11 {
12 int u;
13 int t;
14 u = 17;
15 t = -96;
16 printf ( " before swap1 : u = % d , t = % d \ n " , u , t ) ;
17 swap1 ( u , t ) ;
18 printf ( " after swap1 : u = % d , t = % d \ n " , u , t ) ;
19 return EXIT_SUCCESS ;
20 }
5 void swap3 ( i n t * a , i n t * b )
6 {
7 int k = * a;
8 a = b;
9 * b = k;
10 }
11
12
5 void swap4 ( i n t * a , i n t * b )
6 {
7 int k = * a;
8 * a = * b;
9 * b = * k;
10 }
11
12
15 int u;
16 int t;
17 u = 17;
18 t = -96;
19 printf ( " before swap4 : u = % d , t = % d \ n " , u , t ) ;
20 swap4 (& u , & t ) ;
21 printf ( " after swap4 : u = % d , t = % d \ n " , u , t ) ;
22 return EXIT_SUCCESS ;
23 }
5 void swap5 ( i n t * a , i n t * b )
6 {
7 int k = a;
8 a = b;
9 b = k;
10 }
11
9 void f ( void )
10 {
11 i n t a = 83;
12 i n t c = -74;
13 swap (& a , & c ) ;
14 }
How do we get 15,552 variations? In the first line, there are two options for k:
1. int k
2. int * k
int & k is illegal so it is not considered.
Similarly, there are two options for m and two options for s. So far, there are 8 variations
of the function up to the third line. Next, consider the number of options for s = k at the
fourth line; there are six options:
1. s = * k;
2. s = & k;
3. s = k;
4. * s = * k;
5. * s = & k;
6. * s = k;
& s = is illegal so it is not considered.
Similarly, there are also six options for k = m and another six options for m = s. So far
there are 8 × 6 × 6 × 6 = 1,728 variations for swap function.
From the main function, calling swap has three options for using a in the thirteenth line:
1. a
2. & a
3. * a
Similarly, there are another three options in using c. Thus, in total, there are 1,728 × 3
× 3 = 15,552 variations.
Among all these variations, if the swap function is called without using addresses, the
changes are lost when the swap function finishes. In other words, regardless what happens
inside swap, calling swap in this way,
1 swap (a , c ) ;
is always wrong.
4.10 Answers
4.10.1 Swap Function 1
There are no syntax errors or warnings but this function does not swap u and k. This is
the output of the program:
This is the output of the program. Both u and t are 17, and the value −96 has been
discarded.
This chapter uses programming problems to illustrate how to use pointers and how to test
programs for correctness.
65
66 Intermediate C Programming
The condition at line 8 is used to ensure that the program has two arguments. If two
arguments are not supplied to the program, then it will not proceed to line 12. The first
argument is always the name of the program. By requiring two arguments, one additional
argument can specify the name of a file that contains data that we want to process. If the pro-
gram does not have exactly two arguments, the program stops by returning EXIT FAILURE.
This symbol is defined in stdlib.h. When the main function returns, this program ter-
minates. EXIT FAILURE means that the program failed to accomplish what the program is
supposed to do. For now, we can ignore the program between lines 12 and line 30 and also
line 38. This part of the program is about reading data from a file, and will be explained
in detail in a later chapter.
Line 31 calls the areDistinct function. Dependent on the result of line 31, this program
prints either “The elements are distinct.” or “The elements are not distinct.” Finally, the
program returns EXIT SUCCESS because it successfully determined whether or not the values
are distinct.
The fifth line declares the areDistinct function so that main knows about it. This
declaration says that the areDistinct function returns an integer and takes two arguments.
The first argument is a pointer to integer, and the second argument is an integer. Without
this declaration, the gcc compiler would not know anything about areDistinct. If the fifth
line is removed, then gcc will give the following warning message
4 /* definition */
5 i n t areDistinct ( i n t * arr , i n t len )
6 {
7 // some code
8 }
Below is the code listing for the definition of the areDistinct function. It goes through
the elements in the input array one by one, and checks whether any element after this current
one has the same value. Checking the elements before the current element is unnecessary,
68 Intermediate C Programming
because they have already been checked in earlier iterations. If two elements have the same
value, the function returns 0. If no match is found after going through all of the array
elements, then this function returns 1. If len is zero, the function does not enter the for-
loop at the sixth line, goes directly to line 18, and then returns 1.
1 // aredistinct . c
2 i n t areDistinct ( i n t * arr , i n t len )
3 {
4 i n t ind1 ;
5 i n t ind2 ;
6 f o r ( ind1 = 0; ind1 < len ; ind1 ++)
7 {
8 f o r ( ind2 = ind1 + 1; ind2 < len ; ind2 ++)
9 {
10 i f ( arr [ ind1 ] == arr [ ind2 ])
11 {
12 // found two elements with the same value
13 return 0;
14 }
15 }
16 }
17 // have not found two elements of the same value
18 return 1;
19 }
This command creates an executable file called prog. Section 2.7 suggests that gcc should
always be run with -Wall -Wshadow. Furthermore, if you want to run gdb or ddd, then you
must also add -g after gcc. The new command is
As more and more files are added, this command becomes too long to type. Running
gcc may take a rather long time because every source (.c) file is recompiled every time.
Writing and Testing Programs 69
This seems acceptable for two files, but becomes a serious problem for larger projects.
Recompiling every file can take minutes, or even hours.
Fortunately it is possible to compile individual files separately. When a source file is
compiled, an intermediate file is created. This intermediate file is called an object file and it
has the .o extension. Once an object file has been created for the corresponding source file,
gcc has a special procedure, called linking, for creating an executable file. The following
shows the commands.
$ gcc -g -Wall -Wshadow -c aredistinct.c
$ gcc -g -Wall -Wshadow -c main.c
$ gcc -g -Wall -Wshadow ardistinct.o main.o -o prog
The first gcc command compiles aredistinct.c and creates the object file whose name
is aredistinct.o. Adding -c after gcc tells gcc to create an object file. The object file has
the same name as the source file, except the extension is changed from .c to .o. Similarly,
the second command compiles main.c and creates main.o. The third command takes the
two object files and creates the executable file. This command links the two files because
the input files are object files and uses -o for the name of the executable output file. Please
notice that the last command has no -c.
To see how this saves time, note that aredistinct.o only needs to be updated if
aredistinct.c is changed. Similarly, main.o only needs to be updated if main.c changes.
If either of the object files change, then the link command (the third command above) needs
to be rerun to generate the updated executable. Avoiding the unnecessary compilation saves
time. This is called separate compilation. Even if the advantages of separate compilation
are compelling, typing the three commands is even more awkward and tedious than typing
one command. It certainly is inefficient to type
$ gcc -g -Wall -Wshadow -c main.c
$ gcc -g -Wall -Wshadow ardistinct.o main.o -o prog
whenever main.c is modified. These commands are too long to type over and over again.
Moreover, it is necessary to keep track of which files have been changed and need recom-
pilation. Fortunately, special build tools have been developed to take care of these issues.
The make program in Linux is one popular tool for this purpose.
5.1.4 make
The make program in Linux takes a special input file whose name is Makefile. The main
purpose of the Makefile is to decide which files need to be recompiled. The decisions are
based on the modification time of the object files and the relevant .c files. The object file
aredistinct.o depends on aredistinct.c. If aredistinct.c has a newer modification
date (or time) than aredistinct.o, then make recompiles aredistinct.c. This is expressed
below in the Makefile.
1 aredistinct . o : aredistinct . c
2 gcc -g - Wall - Wshadow -c aredistinct . c
The first line uses : to indicate dependence—aredistinct.o depends on aredistinct.c.
If aredistinct.o does not exist or aredistinct.c is newer than aredistinct.o, then
the command in the next line will be executed. This command uses gcc to recompile
aredistinct.c and to generate aredistinct.o. A Tab key is needed before gcc at the
second line. In make Tab cannot be replaced by spaces.
Note that Makefile is the name of a file that make looks for when it runs. You can tell
make to use any file by adding -f name:
70 Intermediate C Programming
$ make -f name
In this case, the make program uses name as the input, instead of Makefile. Most people
just use Makefile because it is the default, and everyone understands what the file is for.
The following Makefile includes the dependence of main.o and main.c.
1 aredistinct . o : aredistinct . c
2 gcc -g - Wall - Wshadow -c aredistinct . c
3
4 main . o : main . c
5 gcc -g - Wall - Wshadow -c main . c
6
7 # This is a comment
If a line is blank (e.g., line 3), it is discarded by the make command. In Makefile,
anything after # is treated as a comment and ignored. You can use symbols in Makefile.
Symbols are usually uppercase letters. After creating a symbol, it can be expressed by using
$() to enclose the symbol. The following Makefile replaces gcc -g -Wall -Wshadow using
two symbols GCC and CFLAGS.
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3
4 aredistinct . o : aredistinct . c
5 $ ( GCC ) $ ( CFLAGS ) -c aredistinct . c # another comment
6
7 main . o : main . c
8 $ ( GCC ) $ ( CFLAGS ) -c main . c
Why are symbols useful? A general principle in software design is to use symbols to
express some common things. If changes are needed later, these modifications can be made
in only one place. For example, the Makefile could be modified to use another compiler,
and only the first line needs to be updated. There is another common reason for updating a
Makefile. When a program has been completed and is ready for customers. In this case, we
want to replace -g with -O. Please notice that the letter is the uppercase O for optimization,
not zero. The former adds debugging information to the program. The latter optimizes the
program and makes it faster. Replacing -g by -O can make a program noticeably faster.
We only need to update the CFLAGS symbol. By using a single symbol, we ensure that the
change is consistent throughout the entire Makefile.
1 GCC = gcc
2 CFLAGS = -O - Wall - Wshadow # replace -g by -O
3
4 # This is a comment
5 aredistinct . o : aredistinct . c
6 $ ( GCC ) $ ( CFLAGS ) -c aredistinct . c # another comment
7
8 main . o : main . c
9 $ ( GCC ) $ ( CFLAGS ) -c main . c
The Makefile still needs the command to link the two object files together. This is
placed below the symbols in the Makefile.
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
Writing and Testing Programs 71
7 aredistinct . o : aredistinct . c
8 $ ( GCC ) $ ( CFLAGS ) -c aredistinct . c
9
10 main . o : main . c
11 $ ( GCC ) $ ( CFLAGS ) -c main . c
The fourth line says the executable prog depends on both aredistinct.o and main.o.
If either object file is newer than prog, then the executable needs to be rebuilt by linking
the two object files. Line 7 determines whether aredistinct.o needs to be regenerated.
Line 10 determines whether main.o needs to be regenerated.
In a Linux Terminal, type
$ make
The output is
If you change main.c (add a comment somewhere) and type make, the output is
$ make prog
If these two files are identical, then nothing appears on the computer screen. This means
that the program generates the correct output for this test case. If these two files are
different, then the difference is shown on the screen. We can add -w after diff to ignore
differences caused only by spaces.
$ make test0
This dependence follows the same rule mentioned earlier even though test0 is not a file.
Because test0 is not a file, its time can never be later than the time of prog. As a result,
the following two commands (./prog and diff) will always be executed. Before executing
these two commands, make checks the dependence of prog because it is at the right side of
the colon. The make program finds this rule in the Makefile,
1 prog : aredistinct . o main . o
and make compares the time of these three files. If prog is older, then the executable file
prog will be regenerated. Before regenerating the executable file, make finds another two
rules in the Makefile:
1 aredistinct . o : aredistinct . c
2 main . o : main . c
Each object file will be regenerated if it is older than the corresponding .c file. Because
of these dependences, when you type:
$ make test0
the executable file prog will be regenerated if either .c file has changed since the last time
make was invoked. More rules can be added to the Makefile for running different cases:
1 test1 : prog
2 ./ prog inputs / input1 > outputs / output1
3 diff expected / expected1 outputs / output1
4
5 test2 : prog
6 ./ prog inputs / input2 > outputs / output2
74 Intermediate C Programming
9 test3 : prog
10 ./ prog inputs / input3 > outputs / output3
11 diff expected / expected3 outputs / output3
12
13 test4 : prog
14 ./ prog inputs / input4 > outputs / output4
15 diff expected / expected4 outputs / output4
To test each case, type
$ make test1
$ make test2
$ make test3
$ make test4
Another rule can be used to test all test cases at once:
1 testall : test0 test1 test2 test3 test4
Finally, developers usually add a special rule that deletes computer-generated files:
1 clean :
2 / bin / rm -f *. o prog outputs /*
When we type
$ make clean
all of the object files (*.o), the executable prog, and the output files outputs/* are deleted.
This is the full Makefile after adding all of these rules:
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3
7 aredistinct . o : aredistinct . c
8 $ ( GCC ) $ ( CFLAGS ) -c aredistinct . c
9
10 main . o : main . c
11 $ ( GCC ) $ ( CFLAGS ) -c main . c
12
15 test0 : prog
16 ./ prog inputs / input0 > outputs / output0
17 diff expected / expected0 outputs / output0
18
19 test1 : prog
20 ./ prog inputs / input1 > outputs / output1
21 diff expected / expected1 outputs / output1
22
23 test2 : prog
Writing and Testing Programs 75
27 test3 : prog
28 ./ prog inputs / input3 > outputs / output3
29 diff expected / expected3 outputs / output3
30
31 test4 : prog
32 ./ prog inputs / input4 > outputs / output4
33 diff expected / expected4 outputs / output4
34
35 clean :
36 / bin / rm -f *. o prog outputs /*
An array is created at line 10 and it has 5 elements. The valid indexes are 0, 1, 2, 3,
and 4. Lines 12 and 13 print the addresses of x, y, and the array. Lines 16, 17, 19, and 21
use incorrect indexes. If we compile, link, and execute this program, we may find that the
values of x or y are changed because we are using incorrect indexes. This is not guaranteed,
and the results will depend on the specific compiler. This is the output when I run this
program:
compile, link, and run the program again. We will very likely see “Segmentation fault (core
dumped)”. The index 7000 is too large and probably outside the page given by the operating
system.
This message says that something is wrong at the 20th line of the program. Sometimes
valgrind prints a lot to the computer screen. It is useful to direct valgrind’s output to a
log file, like so:
Typing this long command repeatedly is too much work, and the command can be put
in the Makefile. This is the new Makefile for the program that determines whether an
array has distinct elements:
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3 VALGRIND = valgrind -- tool = memcheck -- verbose -- log - file
4
9 aredistinct . o : aredistinct . c
10 $ ( GCC ) $ ( CFLAGS ) -c aredistinct . c
11
12 main . o : main . c
13 $ ( GCC ) $ ( CFLAGS ) -c main . c
14
78 Intermediate C Programming
17 test0 : prog
18 ./ prog inputs / input0 > outputs / output0
19 diff expected / expected0 outputs / output0
20 $ ( VALGRIND ) = log0 ./ prog inputs / input0 > / dev / null
21
22 test1 : prog
23 ./ prog inputs / input1 > outputs / output1
24 diff expected / expected1 outputs / output1
25 $ ( VALGRIND ) = log1 ./ prog inputs / input0 > / dev / null
26
27 test2 : prog
28 ./ prog inputs / input2 > outputs / output2
29 diff expected / expected2 outputs / output2
30 $ ( VALGRIND ) = log2 ./ prog inputs / input0 > / dev / null
31
32 test3 : prog
33 ./ prog inputs / input3 > outputs / output3
34 diff expected / expected3 outputs / output3
35 $ ( VALGRIND ) = log3 ./ prog inputs / input0 > / dev / null
36
37 test4 : prog
38 ./ prog inputs / input4 > outputs / output4
39 diff expected / expected4 outputs / output4
40 $ ( VALGRIND ) = log4 ./ prog inputs / input0 > / dev / null
41
42 clean :
43 / bin / rm -f *. o prog outputs /* log *
The third line creates a symbol for the valgrind command. What is /dev/null in the
20th line? Running prog will produce the output “The elements are distinct.” or “The
elements are not distinct.” This output has already been stored in outputs/output0 in
line 18. Thus, in line 20, the output is discarded. In Linux, /dev/null is a special file that
simply discards everything put into this special file. It is the “black hole” in Linux. Line 20
says “ignore any output produced by running prog”. After making these changes we can
type
$ make testall
and a lot of commands will run. The outputs of valgrind are stored in the log files. We
can use the grep command to check whether any error has been detected by valgrind:
If the result is
certain system calls to talk directly to hardware. Please read the valgrind document for
more information on its limitations. For example, On x86 and amd64, there is no support
for 3DNow! instructions. ... Valgrind’s signal simulation is not as robust as it could be.
... Please understand that valgrind is another tool that can help, but not replace, good
software developers. In many cases, valgrind can detect memory problems. When valgrind
says that a program has no invalid memory accesses, it is still possible that the program
has problems not tested by the specific test cases. It is also possible that valgrind misses
an error because of its limitations. How can you prevent memory access errors? When you
write programs, be careful how the indexes are calculated. It is important to read your code
before testing it, because testing can only determine if a program is wrong.
Some programming languages, such as Java, check the index every time an array element
is read or written. If a wrong index is detected, an exception is thrown. This guarantees
that every invalid index is detected. However, checking indexes slows down the program.
C’s design principle is to do only what a program says and nothing more. This is a trade-off
in the designs of programming languages.
The executable file is called cov. Next, run the ./cov program:
$ ./cov
Two output files are generated: coverage.gcda and coverage.gcno. We can now run the
gcov command.
$ gcov coverage.c
The output is
File ’coverage.c’
Lines executed:71.43% of 7
coverage.c:creating ’coverage.c.gcov’
Another new file called coverage.c.gcov is generated. Here is the content of this file:
1 -: 0: Source : coverage . c
2 -: 0: Graph : coverage . gcno
3 -: 0: Data : coverage . gcda
4 -: 0: Runs :1
5 -: 0: Programs :1
6 -: 1: /*
7 -: 2: file : coverage . c
8 -: 3: purpose : a condition that can never be true
9 -: 4: */
Writing and Testing Programs 81
• The value to search is an element of the array, somewhere in the middle of the array.
• The value is not an element of the array but between some elements in the array.
• The value is the same as the first element.
• The value is the same as the last element.
• The value is smaller than all elements.
• The value is larger than all elements.
• The array has only one element and the value is the same as this only element.
• The array is empty and the value is irrelevant.
Why is it necessary to test these different cases? Depending on the search algorithm and
how the algorithm is implemented, one test case may fail to detect any problem. Creating
good test inputs is not trivial. A different approach is called formal verification by proving a
program is correct regardless of inputs. This is an advanced topic and will not be discussed
here.
It is important to understand the limitation of test coverage. Low coverage means that
the test inputs need improvement. However, high coverage is not necessarily better. A good
test input is one that can detect problems in your programs. A simple program like the
following can get 100% coverage:
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdlib .h >
3 i n t main ( i n t argc , char * argv [])
4 {
5 return EXIT_SUCCESS ;
6 }
This program does not do anything. Pursuing high coverage should not be a goal in
itself. The goal should be detecting and fixing problems.
It is necessary to further explain the limitations of testing. Some students believe that
their programs are correct if the programs pass all test cases given by their professors. This is
wrong for a very simple reason: It is difficult, almost impossible, to test all possible scenarios.
Every if condition in a program creates two possible scenarios. Studies show that an if
condition appears approximately every 10 to 15 lines of code (excluding comments). If your
program has 15,000 lines, there are approximately 1,000 if conditions and 21000 possible
scenarios. How large is this number? The fastest computer in the world can perform about
50 × 1015 (255 ) operations per second. Testing 21000 scenarios would simply be impossible.
The command rm means remove. The earlier command find . -name "core" is now
enclosed by single back-quotes. This is the quote mark ‘ sharing the key with ∼. This is
not the single quote ’ sharing the same key with the double quote ". The system settings
can be modified to eliminate cores. If you use the C shell, you can type
$ limit coredumpsize 0
Limiting the core size prevents the generation of a core file. This does not prevent
programs from making invalid memory accesses and having segmentation faults. We still
have to correct our programs and remove invalid memory accesses.
Strings can be created by putting characters between double quotations. For example,
• “Hello”
• “The C language”
• “write 2 programs”
• “symbols $%# can be part of a string”
A string can include alphabet characters, digits, spaces, and symbols. The examples
above are string constants, which means that their data cannot be edited. In most cases,
however, string variables are preferable to store strings whose values may change. For ex-
ample, a program may ask a user to enter a name. The program cannot know the user’s
name in advance, and thus cannot be compiled with the name. From the program’s point
of view, the name is a string variable that gets initialized when it receives the name from
the keyboard input.
85
86 Intermediate C Programming
The string in arr3 is “2nd st”. The character ’M’ is an array element but it is not part
of the string. Similarly, for arr4, the string is “C P @-”. The trailing characters ’1’ and ’8’
are elements of the array but they are not part of the string. We do not need to put any
number between [ and ] because gcc calculates the size for each array.
What is the difference between single quotation marks and double quotation marks?
Single quotations enclose a single letter, such as ’M’ and ’@’, and represent a character type.
Double quotations enclose a string and the null terminator, ’\0’, is automatically added to
the end of the string. Thus, the string stored in arr3 is “2nd st” (no ’\0’) but it actually
contains the element ’\0’. Note that “W” is different from ’W’. The former uses double quotes
and means a string, ending with a null terminator even though it is not shown. Hence, “W”
actually means two characters. In contrast, ’W’ is a character without a null terminator.
To explain this in another way, when storing a string of n characters, the array needs
space for n + 1 characters. The additional character is used to store the terminating ’\0’.
For example, to store the string “Hello” (5 characters), we need to create an array of 6
elements:
1 char arr [6]; /* create an array with 6 characters */
2 arr [0] = ’H ’;
3 arr [1] = ’e ’;
4 arr [2] = ’l ’;
5 arr [3] = ’l ’;
6 arr [4] = ’o ’;
7 arr [5] = ’ \0 ’; /* remember to add ’\0 ’ */
Forgetting the null terminator ’\0’ is a common mistake. The null terminator
is important because it indicates the end (and thus length) of the string. In the earlier
examples, arr3 and arr4 were two arrays; arr3 had 8 elements and arr4 had 10 elements.
However, if they are treated as strings, the length of each is only 6. The null terminator is
not counted at part of the length. C provides a function strlen for calculating the length
of strings. Before calling strlen, the program needs to include the file string.h because
strlen and many string-related functions are declared in string.h.
1 // strlen . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 i n t main ( i n t argc , char * * argv )
6 {
7 char str1 [] = { ’T ’ , ’h ’ , ’i ’ , ’s ’ , ’ ’ , ’n ’ , ’v ’ , ’t ’ };
8 char str2 [] = { ’T ’ , ’h ’ , ’i ’ , ’s ’ , ’ ’ , ’s ’ , ’t ’ , ’r ’ , ’0 ’ };
9 char str3 [] = { ’2 ’ , ’n ’ , ’d ’ , ’ ’ , ’s ’ , ’t ’ , ’ \0 ’ , ’M ’ };
10 char str4 [] = { ’C ’ , ’ ’ , ’P ’ , ’ ’ , ’@ ’ , ’ - ’ , ’ \0 ’ , ’1 ’ , ’8 ’ , ’k ’ };
11 char str5 [6];
12 i n t len3 ;
13 i n t len4 ;
14 i n t len5 ;
15 str5 [0] = ’H ’;
16 str5 [1] = ’e ’;
17 str5 [2] = ’l ’;
18 str5 [3] = ’l ’;
19 str5 [4] = ’o ’;
20 str5 [5] = ’ \0 ’;
21 len3 = strlen ( str3 ) ;
Strings 87
Calling strlen pushes a new frame onto the call stack with the return location, the
value address, the argument str, and the local variable length:
Frame Symbol Address Value
length 110 0
str 109 100
strlen
value address 108 106
return location 107 line 23
len5 106 garbage
str5[5] 105 ’\0’
str5[4] 104 ’o’
main
str5[3] 103 ’l’
str5[2] 102 ’l’
str5[1] 101 ’e’
str5[0] 100 ’H’
88 Intermediate C Programming
The argument str stores the address of the first array element and that address is 100.
The fourth line of strlen reads the value stored at the address and it is the character ’H’.
Since this is not a ’\0’, both length and str increment.
Frame Symbol Address Value
length 110 1
str 109 101
strlen
value address 108 106
return location 107 line 23
len5 106 garbage
str5[5] 105 ’\0’
str5[4] 104 ’o’
main
str5[3] 103 ’l’
str5[2] 102 ’l’
str5[1] 101 ’e’
str5[0] 100 ’H’
The value of str is the address of the second element and it is 101. The fourth line *
str reads the value at address 101 and the value is ’e’. Since this is not ’\0’, both length
and str increment again. Both length and str increment until str becomes 105, and the
condition at the fourth line is false. The function returns 5, without counting ’\0’.
Frame Symbol Address Value
len5 106 garbage → 5
str5[5] 105 ’\0’
str5[4] 104 ’o’
main
str5[3] 103 ’l’
str5[2] 102 ’l’
str5[1] 101 ’e’
str5[0] 100 ’H’
The strlen function ignores everything after ’\0’, and thus the string lengths of len3
and len4 are 6 even though they have 8 and 10 elements.
check whether the destination has enough space. You must ensure that there is enough
space at the destination. The manual for strcpy says: “The strcpy() function copies the
string pointed to by src, including the terminating null byte (’\0’), to the buffer pointed to
by dest. The strings may not overlap, and the destination string dest must be large enough
to receive the copy.”
Moreover, the manual says: “If the destination string of a strcpy() is not large enough,
then anything might happen. Overflowing fixed-length string buffers is a favorite cracker
technique for taking complete control of the machine. Any time a program reads or copies
data into a buffer, the program first needs to check that there’s enough space. This may be
unnecessary if you can show that overflow is impossible, but be careful: Programs can get
changed over time, in ways that may make the impossible possible.”
What does this mean? When writing a program that uses strcpy, the programmer must
ensure that the destination has enough space. If sufficient space is not made available, then
the program has a serious and unpredictable flaw. Consider a situation where a program
reads data from the keyboard. For example, it asks a user to enter the name. To handle this
situation correctly, the program must be careful about an extremely long input. If sufficient
memory is not allocated and strcpy is called, then the program has a serious security flaw,
vulnerable to “buffer overflow attacks”.
Why does C not check the memory of the destination? To improve speed. Checking would
slow down programs. When C was designed in the late 1960s, computers were expensive
and slow. To make C programs fast, programmers had to take the responsibility of ensuring
that the destination has enough space.
55
7
Why do the two lines print different values, even though both use v? The first printf
treats v as an integer by using %d. Hence, the printed value is 55. The second printf treats
v as a character by using %c. Since 55 is the ASCII value of character ’7’, 7 is printed on
screen. Note that in this case, using %c in printf causes the number 55 to be interpreted
as a character. C has different types, including char for characters and int for integers. A
char is an integer of a smaller range and can store one ASCII character. The character ’7’
has the value of 55. Even though they are both integers, the interpretations (%c or %d) are
different.
There are three arguments so argc is 3, including ./prog itself. The value of argv is the
address of the first element, i.e., & argv[0]. Where is argv[0] stored? Before the main
function is called, the C runtime places it somewhere on the call stack. As usual, we do not
need to know where it is stored. We just need to know how to get the information: by using
argv[0] to get the first string.
The table below shows the call stack. The value of argv is the address of argv[0]. As
with all arrays, the addresses of argv[0], argv[1], and argv[2] are contiguous. For the
sake of explanation, we will use “-” for the values of argv[0], argv[1], and argv[2] for
the time being.
Frame Symbol Address Value
argv[2] 104 -
argv[1] 103 -
main argv[0] 102 -
argv 101 102
argc 100 3
Since argv[0], argv[1], and argv[2] are strings, each of them is also a pointer storing
the starting address of the first letter in each of those strings. The value of argv[0] is the
address of argv[0][0]. To make it clearer a horizontal line separates the strings. Everything
still belongs to the same frame.
Frame Symbol Address Value
argv[0][6] 111 ’\0’
argv[0][5] 110 g
argv[0][4] 109 o
argv[0][3] 108 r
argv[0][2] 107 p
argv[0][1] 106 /
argv[0][0] 105 .
main
argv[2] 104 -
argv[1] 103 -
argv[0] 102 105
argv 101 102
argc 100 3
In this example, the address of argv[0][0] is right above the address of argv[2].
However, it does not necessarily have to be this way. As previously mentioned, the value
of argv[0] is the address of argv[0][0]. Similarly, the value of argv[1] is the address of
argv[1][0]. The lower part of the call stack is skipped since it is the same as shown earlier.
Strings 93
Finally, here is the full frame on the call stack, showing all the arguments:
Frame Symbol Address Value
argv[2][3] 126 ’\0’
argv[2][2] 125 s
argv[2][2] 124 t
argv[2][2] 123 n
argv[2][2] 122 e
argv[2][2] 121 m
argv[2][1] 120 u
argv[2][0] 119 g
argv[1][4] 118 r
main argv[1][3] 117 a
argv[1][4] 116 ’\0’
argv[1][3] 115 e
argv[1][2] 114 m
argv[1][1] 113 o
argv[1][0] 112 s
argv[0][6] 111 ’\0’
argv[0][5] 110 g
argv[0][4] 109 o
argv[0][3] 108 r
argv[0][2] 107 p
argv[0][1] 106 /
argv[0][0] 105 .
argv[2] 104 117
argv[1] 103 112
argv[0] 102 105
argv 101 102
argc 100 3
program combines what we have learned about strstr and argv to count the occurrences
of a substring.
1 /*
2 * countsubstr . c
3 * count the occurrence of a substring
4 * argv [1] is the longer string
5 * argv [2] is the shorter string
6 * argv [1] may contain space if the string enclosed by " "
7 */
8
Earlier we noted that spaces separate the command line arguments. If we enclose a
sentence in double quotation marks, the whole sentence is treated as a single argument,
as shown in this example. Lines 21 prints argv[1] and it is the whole sentence. Without
the quotation marks, “This” is argv[1] and “is” becomes argv[2], etc. We must use two
quotation marks; otherwise, the Terminal says:
Unmatched "
Below is the call stack for this example. It has been formatted to fit onto one page.
We have skipped the column for the frame because only one function is displayed. Line 25
assigns the value of argv[1] to ptr. This value is the address of argv[1][0], namely 116.
Line 28 finds “is” in “This is his history book.” The first occurrence of “is” is at address
118. This line changes ptr’s value to 118. Since it is not NULL, counter increments. Line
31 prints the string starting at the address where “is” is found. Recall that strings are
terminated by a null byte, and thus the entire string is printed starting from address 118.
Line 33 increments ptr to 119. This allows us to continue to search for “is” in the rest of
96 Intermediate C Programming
the argv[1]. Without this increment, strstr will search “is” from the address of 118, i.e.,
“is is his history book.”, and the first occurrence will be 118 again. The program would
enter an infinite loop if line 33 were removed. Instead, line 33 makes the program continue
searching from the address 119, i.e., “s is his history book.” The next occurrence of “is”
is at address 121. As ptr increments, it gradually moves toward the end of argv[1]. This
is evident from the output of line 31. After finding four occurrences, strstr cannot find
“is” any more and returns NULL. Note that count was incremented once every time strstr
returned a result other than NULL. Thus count now contains the number of times that “is”
was found in argv[1].
C provides many more functions for processing strings. You can find a list of the C’s
string processing functions by typing into the Terminal:
$ man string
man stands for “manual”. The manual displays a list of functions related to processing
strings, for example strcat.
Chapter 7
Programming Problems and Debugging
97
98 Intermediate C Programming
The original C language does not support image processing or any specific computer
vision functionality. OpenCV provides these features as an extension to the language.
If a library is not from the original C language, you need to tell gcc to link to the
library. For example, if a program uses mathematical functions declared in math.h,
-lm needs to be added when linking the object files into an executable program.
• Enhancing performance. Libraries are usually well optimized so that programs can
have better performance.
• If a C file uses the printf function, the file needs to include stdio.h. This is the
header file for standard input and output functions for reading and writing to the
Terminal and files.
• If a C file uses EXIT SUCCESS, then the file needs to include stdlib.h because the
symbol EXIT SUCCESS is defined in that header file.
• If a C file calls mathematical functions, such as sin or log, then the file needs to
include math.h.
Header files have a .h extension. A header may be used for the following purposes:
• Define symbolic constants, for example:
1 # define MATH_PI 3.14159
2 # define MAX_LENGTH 50
• Declare functions, for example:
1 i n t areDistinct ( i n t * arr , i n t len ) ;
Header files must not contain function implementations (definition).
• Define programmer-created data types. We will explore this feature further in Chap-
ter 16.
• Include other header files, for example
1 # include < stdio .h >
It is very important that header files do not contain any function definitions. That means
that all functions listed in a header file must end in a ; character. If there is a block of code
(statements between { and }), then you will likely run into problems while linking code.
The implementations of functions should be kept in .c files (or libraries), and not in header
files. Header files are included; .c files are compiled and linked. Never include .c files. Pro-
grammers generally write .h and .c files in pairs: The .c file contains the implementation,
and the .h file contains only the function declarations. The first two lines and the very last
line of a header file are usually something like:
Programming Problems and Debugging 99
1 #i f n d e f FILENAME_H
2 #d e f i n e FILENAME_H
3 // The rest of the header file
4 #e n d i f // do not add FILENAME_H
The #ifdef is matched with an #endif that appears at the very end of the file.
It is important to replace FILENAME H with the actual name of the file. FILENAME H is a
symbol and can only contain alphanumeric characters and the underscore. Thus program-
mers generally replace the “.” that appears before a file extension with “ ” in the symbol.
As a matter of style, symbols are always typed in upper case. The purpose of #ifndef
... #define ... #endif is to prevent multiple inclusion. Sometimes, the same header file is
included multiple times, for example included by two different header files, and both of
them are included by the same .c file. Without this three lines at the very top and the
very bottom, gcc will report an error when the same header file is included multiple times.
Some other languages have no such problems; for example, in Java, the same package can
be imported multiple times.
When a header file is included, if this header file is from the standard C library, or some
library that is installed on the system, then < and > are used to enclose the file name, for
example:
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdilib .h >
3 #i n c l u d e < math .h >
When a programmer-defined header file is included, the file name is enclosed by double
quotations, such as:
1 #i n c l u d e " myheader . h "
7.1.3 mystring.h
This programming problem has a header file called mystring.h and it declares several
string functions:
1 // mystring . h
2 #i f n d e f MYSTRING_H
3 #d e f i n e MYSTRING_H
4 // Count the number of characters in a string .
5 // Example : my_strlen (" foo ") should be 3.
6 i n t my_strlen ( const char * str ) ;
7 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
8 // Count the number of occurrences of a particular
9 // character c in a string .
10 // Example : my_countchar (" foo " , ’o ’) should be 2.
11 //
12 i n t my_countchar ( const char * str , char c ) ;
13 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
14 // Convert a string to uppercase . Only alphabetical
15 // characters should be converted ; numbers and symbols
16 // should not be affected . Hint : toupper ( c ) is a macro
17 // that is the uppercase version of a character c .
18 // Example : char * str = " foobar ";
19 // my_strupper ( foobar ) is " FOOBAR ".
100 Intermediate C Programming
25 fclose ( infptr ) ;
26 return EXIT_FAILURE ;
27 }
28
29 i n t num_lines = 0;
30 char buffer [ LINE_SIZE ];
31 // count the number of lines in the file
32 while ( fgets ( buffer , LINE_SIZE , infptr ) != NULL )
33 {
34 num_lines ++;
35 }
36
55 i n t total_length = 0;
56 f o r ( i = 0; i < num_lines ; i ++)
57 {
58 total_length += my_strlen ( lines [ i ]) ;
59 }
60 // count the length of each line
61 i f ( strcmp ( argv [1] , " strlen " ) == 0)
62 {
63 f o r ( i = 0; i < num_lines ; i ++)
64 {
65 fprintf ( outfptr , " length : % d \ n " ,
66 my_strlen ( lines [ i ]) ) ;
67 }
68 }
69 /* for each line , count the occurrence of the first
70 letter in the line */
71 i f ( strcmp ( argv [1] , " countchar " ) == 0)
72 {
73 f o r ( i = 0; i < num_lines ; i ++)
74 {
75 fprintf ( outfptr , " count (% c ) : % d \ n " , lines [ i ][0] ,
Programming Problems and Debugging 103
the output is stored in the file called output strlen. The first four lines of the file are
1 length : 63
2 length : 70
3 length : 70
4 length : 69
The original article has a blank line at the eleventh line. A blank line means that it only
has the new line character ’\n’. Thus, this line actually has one character. Similarly, line
21 also has one character. When running the program with this command
the output is stored in the file called output countchar. The first four lines of the file are
1 count ( I ) : 1
2 count ( n ) : 6
3 count ( n ) : 3
4 count ( d ) : 1
This is the final command
7.1.5 Makefile
The Makefile should look familiar:
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3 OBJS = mystring . o main . o
4 HDRS = mystring . h
5 VAL = valgrind -- tool = memcheck -- leak - check = full
6 VAL += -- verbose -- log - file =
7
11 .c.o:
12 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
13
14 clean :
15 rm -f mystring $ ( OBJS ) out_ * log *
16
19 test0 : mystring
20 $ ( VAL ) log0 ./ mystring strlen input out_len
21 diff -q out_len expected_strlen
22
23 test1 : mystring
24 $ ( VAL ) log1 ./ mystring countchar input output_countchar
25 diff -q output_countchar exp ec te d_ co un tc ha r
26
27 test2 : mystring
28 $ ( VAL ) log2 ./ mystring strupper input out_upper
29 diff -q out_upper ex pected _strup per
This Makefile introduces several new concepts:
• Line 6 appends more options to the symbol VAL. This approach makes it easy to add
a symbol with many options.
• The symbol $@ is used on line 9. It means the symbol before the : at line 8. In this
case, the $@ means mystring. Using $@ is a convenient way to manage rules.
• Lines 11 and 12 mean “If an object file is needed, compile the corresponding .c file.”
It determines the object files on an as-needed basis. In this case, mystring depends on
OBJS and it depends on mystring.o and main.o. To invoke the mystring rule, make
will ensure that both mystring.o and main.o are up to date. If they need updating,
then the rule on lines 11 and 12 is invoked in order to generate the object files from
the corresponding .c file. Lines 11 and 12 are equivalent to the following:
Programming Problems and Debugging 105
mystring.o: mystring.c
$(GCC) $(CFLAGS) -c mystring.c
main.o: main.c
$(GCC) $(CFLAGS) -c main.c
If a program requires many object files, lines 11 and 12 can shorten Makefile signifi-
cantly.
7.1.6 mystring.c
The following is a reference solution for mystring.c:
1 // mystring . c
2 #i n c l u d e " mystring . h "
3 #i n c l u d e < ctype .h >
4 i n t my_strlen ( const char * str )
5 {
6 i n t len = 0;
7 while ( str [ len ] != ’ \0 ’)
8 {
9 len ++;
10 }
11 return len ;
12 }
13
Lines 11 and 12 make chptr1 and chptr2 store the address of str1[0]. By putting
const in front of char * at line 11, we do not want to change the value at the memory
that is pointed by chptr1. Line 13 is not allowed because this line attempts to change the
value at address 100, i.e., & str1[0], through chptr1. Please note the word through. It
is still possible to change str1[0] as long as the change is not through chptr1. Line 15 does
not prevent us from changing chptr1 itself because chptr1 is not a constant. Hence, we
can change the value of chptr1 at line 15.
Symbol Address Value
chptr1 113 100 → 106 by line 15
In contrast, line 12 says chptr2 is a constant and so it cannot be changed in line 16.
However, it is possible to change the value of str1[0] through chptr2 at line 14.
Symbol Address Value
chptr2 114 100 (cannot be changed)
chptr1 113 100 → 106 by line 15
str1[0] 100 F → C by line 14
In my countchar, str itself is not a constant (similar to chptr1) so changing str itself
at line 23 is allowed. In fact, const can be used twice for the same pointer. In this exam-
ple, chptr3 stores the address of & str1[0]. Changing the value of chptr3 (line 17) and
changing the value at the address (line 18) are disallowed.
1 // const2 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t main ( i n t argc , char * argv [])
5 {
6 char str1 [20];
7 char str2 [20];
8 strcpy ( str1 , " First " ) ;
9 strcpy ( str2 , " Second " ) ;
10 const char * chptr1 = & str1 [0];
11 char * const chptr2 = & str1 [0];
12 const char * const chptr3 = & str1 [0];
13 // * chptr1 = ’C ’; // not allowed
108 Intermediate C Programming
14 * chptr2 = ’C ’; // OK
15 chptr1 = & str2 [0]; // OK
16 // chptr2 = & str2 [0]; // not allowed
17 // chptr3 = & str2 [0]; // not allowed
18 // * chptr3 = ’C ’; // not allowed
19 return EXIT_SUCCESS ;
20 }
7.2 Debugging
To write good programs, we must abandon the habit of “coding-testing-debugging”.
Testing does not magically reveal what is wrong with a program and how to fix it. Instead,
we must have a plan before writing code. This includes having a testing and debugging
strategy. After writing each line of code, read it carefully. This saves time. It will help you
find simple mistakes, but reading code will also help reveal more subtle problems.
A common problem among learners is forgetting to initialize variables. C does not ini-
tialize variables. It is your responsibility. This can lead to apparently inexplicable bugs and
worse code that seems to work but then mysteriously fails. It is hard to find these types
of bugs by “testing” and “debugging” because the program behavior can be unexpected,
or sometimes appear correct. In this case, there is no substitute to reading the code, and
asking yourself if every variable has been initialized.
Another common mistake is putting ; in the wrong place. In the code listing for
mystring.c, if an ; is placed at the end of line 7, 17, or 30 then the program is incor-
rect because the block of code between { and } is no longer related to the while or if
conditions. Putting ; at the end of line 7, 17, or 30 makes the program enter infinite loops.
This problem can be difficult to find by testing alone because the program does not stop
and the output will probably be incomplete. Putting ; at the end of line 19 increments
the count regardless of whether * str and ch match. These problems are easy to find by
reading code line by line carefully. Unfortunately, gcc cannot offer much help because the
program is syntactically correct.
What is the fastest way to diagnose this problem? Start gdb in a Linux Terminal.
$ gdb mystring
Please remember that gdb takes the executable file as the input, and not a .c file. Inside
gdb, type
The first letter r means “run” the program. It replaces ./mystring in the command
line. Add the normal command-line arguments after r. The program now starts and enters
the aforementioned infinite loop. Press Ctrl-c to interrupt the normal execution of the
program and gdb will display something like:
This means that the program has stopped at line 7 of the file mystring.c. The message
tells us where the infinite loop is. At the (gdb) prompt, type list to show the code:
4 i n t my_strlen ( const char * str )
5 {
6 i n t len = 0;
7 while ( str [ len ] != ’ \0 ’) ;
8 {
9 len ++;
As explained earlier, infinite loops may occur in many places (lines 7, 17, and 30). Using
gdb to identify an infinite loop is easy and takes only a few seconds. Moreover, we do not
need to modify any line before using gdb.
we get
Segmentation fault means the program tries to access (read or write) memory at an
invalid address. The operating system reacts by stopping the program. It is similar to
attempting to enter someone else’s house. If you do not own the house, entering the house is
illegal. We can determine where segmentation fault occurs by using either gdb or valgrind.
If we run the program using gdb, we will see something like:
Program received signal SIGSEGV, Segmentation fault.
0x00000000004008cd in my_countchar (str=0x624000 $<$Address 0x624000
out of bounds$>$, ch=73 ’I’) at mystring.c:17
Type the bt (backtrace) command at the gdb prompt, to see the call stack.
#0 0x00000000004008cd in my_countchar (str=0x624000
$<$Address 0x624000 out of bounds$>$, ch=73 ’I’) at mystring.c:17
#1 0x0000000000400cc9 in main (argc=4, argv=0x7fffffffe418) at main.c:74
The call stack shows two frames. The top frame is frame 0 and the next is frame 1. In
gdb, you can see a specific frame. Type
(gdb) f 1
after the (gdb) prompt to enter frame 1. Type list to show the code around line 74 in
main.c. Type print i to print the value of i. This is the line number of the input file. Its
value is 0 meaning that the my countchar is processing the first line of input.
The first line of the input is “If we consider that part of the theory of relativity which
may\n” gdb can tell us that if we type the command print lines[i]:
(gdb) print lines[i]
$3 = 0x603590 “If we consider that part of the theory of relativity which may\n”
The starting address of this string is 0x603590. Something is wrong inside my countchar
but at this stage, it is unclear precisely what is wrong. Let’s go back to the frame of
my countchar by typing f 0:
(gdb) f 0
#0 0x00000000004008cd in my countchar (str=0x624000
<Address 0x624000 out of bounds>, ch=73 ’I’) at mystring.c:17
The segmentation fault occurs at line 17 and this line reads the value at the address stored
in str. Print the value of str in gdb:
(gdb) print str
$4 = 0x624000 <Address 0x624000 out of bounds>
Compare the value of str (0x624000) and the starting address of the string in main
(0x603590). The difference is quite large but the string is not very long, only 63 characters.
This means that str kept increasing far beyond the end of the string.
You may ask, “Why does the segmentation fault occur when str is so large? Doesn’t
the program start accessing invalid addresses after str is larger than 0x603590 + 63?”
It is correct that the program starts accessing invalid addresses after str is larger than
0x603590 + 63. However, Linux stops the program only when it reads memory that it is
not authorized to use. Since the memory is given in chunks, as explained in Section 5.3,
the segmentation fault occurs when the program accesses a memory address beyond the
currently authorized segment. A program may access all the addresses inside the segments
given to the program, and the operating system will not stop it. This does not mean that
the program is correct. We need to correct the program as soon as possible.
Programming Problems and Debugging 111
Do not be too concerned about the number of errors detected. Fixing one error will Often
fix many of the other detected errors. This is because a single error can be hit many times
as a program executes. Go to the very top of the log file and start looking for anything
related to the source files, i.e., mystring.c, mystring.h, and main.c. The first detected
problem related to mystring.c is:
This is related to the problem at line 17. If we fix line 17, the problem at line 19
disappears. Some students think that accessing invalid memory is harmless as long as the
programs do not have segmentation faults. This is wrong. Allowing invalid addresses is
one of the most common security problems in software. It can allow a malicious program to
“hijack” another program. If a program accesses invalid addresses, the program’s behavior is
not defined. That means it may work a hundred or a thousand times, and then mysteriously
fail. The same program may fail when using a different compiler, or run on a different
computer.
Please remember that testing can demonstrate that something is wrong; however, testing
cannot demonstrate that everything is right. As we see, Linux stops the program when str
is already very far away from valid addresses. Thus, we cannot rely on testing exclusively.
Instead, we need to use many methods to prevent and detect mistakes.
“Which one should I use, gdb or valgrind?”, you may ask. The answer is both. These
two tools serve different purposes. Choosing gdb vs. valgrind is like choosing a hammer vs.
a screw driver. Use the right tool for the job: gdb is interactive, and allows a programmer
to see the program’s execution line by line. In contrast, valgrind runs the program until it
stops (or crashes). In general it is a good idea to use valgrind first to detect whether there
are any memory problems and then use gdb to pinpoint the problem. You should always use
valgrind to check whether your programs have invalid memory accesses. The command is:
What is the difference between gcc and valgrind? Doesn’t gcc also check whether a
program has problems? The gcc compiler checks the source code: i.e., it finds syntax errors.
This is a very rudimentary form of error checking. It is like a spell-checker in a document
editor. An article without any spelling error does not mean that the article makes any sense.
In similar fashion, gcc does not check what happens when the program runs. It is impossible
for gcc to check what the program does when it is running.
In contrast, valgrind is a run-time checker. The program must be run for valgrind to
check anything. This implies the following: If the program does not execute the parts of code
112 Intermediate C Programming
that have problems, then valgrind will not detect any problems. This is not a limitation in
valgrind, but more an indication of what type of tool it is. This is a limitation in how you
write test cases. You need to think how your program may fail and then test the potential
problems. Good tests can make valgrind invaluable. Note that this is another reason why
passing test cases does not guarantee that the program is correct. If the test cases do not
test the problematic parts, then the problems are undetected.
How could it be possible that some parts of the program are not executed? This is
because most programs have many if conditions. It is extremely difficult—practically
impossible—to design test cases that can check every combination of these conditions. For
each if condition, the condition can be true or false. Hence, there are two possibilities. If
there are n if conditions and they are independent, there are 2n possibilities. As a point of
reference, in one weekly homework for my class at Purdue University, the sample solution
has 25 if conditions, about 225 = 34 million possibilities. Can you create 34 million test
cases? Obviously a brute-force approach is impossible, but the task is significantly eased by
examining the logic of the program. Even though there are 25 if conditions in the sample
solution mentioned, there are not 34 million paths through the code because some condi-
tions are related. Careful reasoning is required to make this type of analysis. It is certainly
possible that some problems are not detected when this homework assignment is graded.
Chapter 8
Heap Memory
Chapter 2 describes one type of memory: stack memory (also called the call stack). Stack
memory follows a simple rule: first in, last out. The call stack is automatically managed by
the code generated by the compiler. Every time a function is called, a new frame is pushed.
Every time a function ends, the frame is popped. Programmers have no direct control
over this process. One consequence is that a function may read from or write to memory
addresses in lower frames; however, a function can never “look upward” into memory above
its frame. This is because whenever a function is actively executing, there are not valid
memory addresses “above” it.
This is natural and convenient for a lot of programming tasks; however, sometimes
programmers need more control over memory. They want to be able to allocate memory
when necessary and release memory when the allocated memory is no longer needed. For
this purpose, computers have another type of memory: heap memory.
Computers also have the third type of memory where the compiled code resides. In
general, this memory cannot be modified. Programs can access only stack memory and
heap memory. Thus, the rest of this book does not explain the memory where programs are
stored.
113
114 Intermediate C Programming
possible input size. This is very inefficient. What we need is a way to allocate memory as
needed while the program is running.
Before talking about how to use heap memory, let’s review how to create a fixed-size
array. The following example creates an array of six integers and assigns the first three
elements to 11, −29, and 74.
1 i n t arr1 [6];
2 arr1 [0] = 11;
3 arr1 [1] = -29;
4 arr1 [2] = 74;
This array is stored inside a frame in the call stack. The values of the other three elements
(arr[3], arr[4], and arr[5]) are still garbage. To create a fixed-size array, the array’s size
must be specified in the source code. This is problematic because the size may be unknown
at the time the code is written. For example,
1 i n t num ;
2 printf ( " Please enter a number : " ) ;
3 scanf ( " % d " , & num ) ;
Note that the number is given after the program starts running. We can use this number
to create an integer array with num elements. The program uses malloc to create the array:
1 i n t * arr2 ;
2 arr2 = malloc ( num * s i z e o f ( i n t ) ) ; // no * inside sizeof
Notice * in front of arr2. The value of num is entered by a user. To create an array
of integers, an integer pointer is needed. This allocation must use sizeof(int) because
the size of an integer can be different on different machines. If sizeof(int) is 4 (typical
among computers), then the program allocates num * 4 bytes of memory. If sizeof(int)
is 2 (common in some types of micro controller), then the program allocates num * 2 bytes
of memory. The type of the pointer must match what is in sizeof(...). The following
example has mistakes.
1 i n t * arr3 ;
2 arr3 = malloc ( length * s i z e o f ( char ) ) ; /* WRONG */
3 /* types do not match , one is int , the other is char */
4 i n t * arr4 ;
5 arr4 = malloc ( length * s i z e o f ( double ) ) ; /* WRONG */
6 /* types do not match , one is int , the other is double */
The program will behave strangely when types do not match. Programs should check
whether malloc succeeds. If it fails, then it is NULL. Programs should handle this prob-
lem before doing anything else. Why would malloc fail? This happens when the system
cannot provide the requested memory. Perhaps the request is unreasonably large. C uses
NULL to indicate an invalid value for a memory address.
1 i n t * arr5 ;
2 arr5 = malloc ( length * s i z e o f ( i n t ) ) ;
3 i f ( arr5 == NULL )
4 {
5 // malloc has failed , handle the problem here .
6 }
If a program allocates memory successfully, then the program can assign values to the
elements in the same way as an array:
Heap Memory 115
The address is arbitrarily chosen to be 200. If you prefer, you can choose 400, 5000,
or whatever number. Even though the pointer is on the stack, we can create an array on
the heap. Remember that the pointer’s address and value are independent. This example
allocates memory for an array of 6 integers. This is how to create the array on the heap:
2 arr2 = malloc (6 * s i z e o f ( i n t ) ) ;
Section 4.8 mentioned that different types have different sizes. That is the reason why
malloc is used in conjunction with sizeof. From now on, let us assume that each integer
requires 4 bytes and the addresses of two adjacent array elements is different by 4. Calling
malloc returns a valid heap address to a piece of memory large enough to store 6 integers,
and arr2’s value stores that address. Heap memory is pretty far away from the stack
memory. In this example, we use 10000 for the heap address. The memory is uninitialized,
so we use “?” for each integer’s value.
116 Intermediate C Programming
How does this work? What happens when the program executes this line?
4 arr2 [2] = 74;
This is what this statement does precisely:
1. Takes arr2’s value as an address. In this example the value is 10000.
2. The index is 2, so sizeof(int) × 2 = 4 × 2 = 8 is added to 10000 and the new
address is 10008.
3. Since this is at the left of the assignment, the value at the address of 10008 is changed
to 74.
The following example creates an array whose length is determined by argc. The pro-
gram converts the command line arguments—the elements of argv—into integers. This is
necessary because each element of argv is a string. Then, the program adds up the integers’
values and prints the sum.
Heap Memory 117
1 // malloc . c
2 // create an array whose size is specified at run time .
3 // The array ’s elements are the command line arguments .
4 // The program adds the elements and prints the sum .
5 #i n c l u d e < stdio .h >
6 #i n c l u d e < stdlib .h >
7 i n t main ( i n t argc , char * argv [])
8 {
9 i n t * arr2 ;
10 i n t iter ;
11 i n t sum = 0;
12 i f ( argc < 2)
13 {
14 printf ( " Need to provide some integers .\ n " ) ;
15 return EXIT_FAILURE ;
16 }
17 arr2 = malloc ( argc * s i z e o f ( i n t ) ) ;
18 i f ( arr2 == NULL )
19 {
20 printf ( " malloc fails .\ n " ) ;
21 return EXIT_FAILURE ;
22 }
23 /* iter starts at 1 because argv [0] is the program ’s name */
24 f o r ( iter = 1; iter < argc ; iter ++)
25 {
26 arr2 [ iter ] = ( i n t ) strtol ( argv [ iter ] , NULL , 10) ;
27 }
28 printf ( " The sum of " ) ;
29 f o r ( iter = 1; iter < argc ; iter ++)
30 {
31 printf ( " % d " , arr2 [ iter ]) ;
32 sum += arr2 [ iter ];
33 }
34 printf ( " is % d .\ n " , sum ) ;
35
36 free ( arr2 ) ;
37 return EXIT_SUCCESS ;
38 }
The program uses strtol to convert the strings into integers. Some books suggest using
atoi but strtol is preferred for two reasons: (i) strtol is more general because it is not
limited to decimal bases. For example, strtol can be used to convert binary numbers, or
hexadecimal (base 16) numbers. (ii) More important, strtol allows the calling program to
check whether the conversion fails. The conversion fails when the string contains no number.
In contrast, atoi provides no information about whether the conversion fails. Use gcc to
convert the program into an executable file called malloc:
$ gcc -Wall -Wshadow malloc.c -o malloc
The following shows two examples running this program. If an argument is not an integer
(“hello” and “C” in the second example), then the value of that argument is zero.
$ ./malloc 5 8 −11 4 3 27
118 Intermediate C Programming
$ ./malloc 7 9 hello 1 6 C 2 4 8
The sum of 7 9 0 1 6 0 2 4 8 is 37.
After f1 returns, this is what is in the call stack and heap memory:
Heap Memory 119
The allocated heap memory is still available because the program has not called free
yet. The stack variable ptr, declared on line 3, is destroyed when f1 returns, because ptr
is on the stack. However, the allocated heap memory is available until it is freed. This is
a fundamental difference between stack and heap memory. Heap memory is more flexible.
The statement,
1 arr [4] = 872;
changes an element in the array. Now the stack and heap memory look as follows:
Symbol Address Value Address Value
arr 100 10000 10020 ?
10016 872
10012 ?
10008 ?
10004 ?
10000 ?
Before f2 finishes, it must call free. Otherwise, the program leaks memory. The pur-
pose of this example is to show that memory allocated by malloc can be passed between
functions. Please be aware that this example does not follow the principle mentioned in
Section 8.1. In this example, malloc and free are called in two different functions. This is
sometimes necessary, but also error-prone. It is easy to forget calling free in f2 because f2
does not call malloc. We have used 10000 as the memory address returned by malloc. In
a real computer, the address can change every time the program runs and the address will
likely be a very large number.
5 arr2d [3][1] = 6;
6 // assign 6 to the second column of the fourth row
In this example, the first dimension has eight rows and the indexes are between zero
and seven (inclusively). The second dimension has three columns; the indexes are between
zero and two. A two-dimensional array is like a matrix.
It is a little more complicated creating a two-dimensional array whose size is known only
at run time by calling malloc. We first create a one-dimensional array of integer pointers
(int *) and then each pointer is used to create an integer array. Fig. 8.1 illustrates this
concept. The first step creates an array of integer pointers. In the second step, each pointer
stores the address of the first element in a one-dimensional integer array. The addresses of
&arr2d[0] and &arr2d[1] are adjacent. However, the values of arr2d[0] (corresponding
to &arr2d[0][0]) and arr2d[1] (corresponding to &arr2d[1][0]) are likely far apart.
arr2d
arr2d[0] add2d[0][0] add2d[0][1] add2d[0][2]
arr2d[1] add2d[1][0] add2d[1][1] add2d[1][2]
arr2d[2] add2d[2][0] add2d[2][1] add2d[2][2]
arr2d[3] add2d[3][0] add2d[3][1] add2d[3][2]
arr2d[4] add2d[4][0] add2d[4][1] add2d[4][2]
arr2d[5] add2d[5][0] add2d[5][1] add2d[5][2]
arr2d[6] add2d[6][0] add2d[6][1] add2d[6][2]
arr2d[7] add2d[7][0] add2d[7][1] add2d[7][2]
FIGURE 8.1: A two-dimensional array has an array of pointers as the first dimension.
Each element points to an array.
An array’s name stores the address of the first element. Therefore, the type of a one-
dimensional array of integers is an integer pointer. If we want to create an array using
malloc, we need to use int *.
1 i n t arr [6]; /* an array of fixed size , 6 elements */
2 i n t * arr2 ; /* an integer pointer */
3 arr2 = malloc (9 * s i z e o f ( i n t ) ) ; /* 9 elements */
4 arr2 [4] = 19; /* arr2 [4] is an integer */
5 free ( arr2 ) ;
Imagine a new type called one d array. A two-dimensional array would be an array of
one d array. To create this two-dimensional array, malloc is used:
1 one_d_array * arr2d ;
2 arr2d = malloc ( numrow * s i z e o f ( one_d_array ) ) ;
The one-dimensional array is itself a pointer to integer; thus, one d array should be
replaced by int *. Consequently, the type of arr2d is int * *. That means that the arr2d
is pointing to int *, and indeed, the first element of arr2d has type int *.
Heap Memory 121
1 i n t * * arr2d ;
2 arr2d = malloc ( numrow * s i z e o f ( i n t *) ) ;
This only allocates enough space for the pointers: arr2d[i] is a pointer to an integer.
There is no space for the integers yet. It is necessary to allocate the memory for the integers
separately:
3 f o r ( row = 0; row < NUMROW ; row ++)
4 {
5 arr2d [ row ] = malloc ( NUMCOLUMN * s i z e o f ( i n t ) ) ;
6 }
These arrays must be freed later in the program.
7 f o r ( row = 0; row < NUMROW ; row ++)
8 {
9 free ( arr2d [ row ]) ;
10 }
11 free ( arr2d ) ; // must be after free ( arr2d [ row ])
If we freed arr2d before freeing the individual rows, then attempting to free the rows
would be an error. Thus, malloc and free are always in the reverse order: malloc must
be followed by free, and the memory must not be accessed after a call to free.
This is the code implementing this concept.
1 /* twodarray . c
2 purpose : show how to create a two - dimensional array
3 The size of the array is 8 rows x 3 columns
4 */
5 #i n c l u d e < stdio .h >
6 #i n c l u d e < stdlib .h >
7 #d e f i n e NUMROW 8
8 #d e f i n e NUMCOLUMN 3
9 i n t main ( i n t argc , char * argv [])
10 {
11 i n t * * arr2d ;
12 i n t row ;
13 /* step 1: create an array of integer pointers */
14 arr2d = malloc ( NUMROW * s i z e o f ( i n t *) ) ;
15 f o r ( row = 0; row < NUMROW ; row ++)
16 {
17 /* step 2: for each row ( i . e . , integer pointer ) ,
18 create an integer array */
19 arr2d [ row ] = malloc ( NUMCOLUMN * s i z e o f ( i n t ) ) ;
20 }
21 /* now , the two - dimensional array can be used */
22 arr2d [4][1] = 6;
23 arr2d [6][0] = 19;
24 /* the first index can be 0 to 7 ( inclusive ) */
25 /* the second index can be 0 to 2 ( inclusive ) */
26
31 }
32 total = sum ( arr , length ) ;
33 printf ( " Total is % d .\ n " , total ) ;
34 free ( arr ) ;
35 return EXIT_SUCCESS ;
36 }
In this example, arr is passed to function sum as an argument. The sum function itself
does not need to know whether the value in array is an address in stack memory or heap
memory. The function only needs to know that array contains a valid address somewhere
in memory. The address is copied when it is passed as the argument array in the function
called sum. The call stack and the heap memory look like the following inside the sum
function, just before the for block starts:
Frame Symbol Address Value Address Value
answer 211 0 10044 11
iter 210 - 10040 10
length 209 12 10036 9
sum
array 208 10000 10032 8
value address 207 205 10028 7
return location 206 line 33 10024 6
total 205 ? 10020 5
length 204 12 10016 4
iter 203 13 10012 3
main
arr 202 10000 10008 2
argv 201 - 10004 1
argc 200 - 10000 0
Inside sum, array[0] refers to the value stored at 10000 and it is 0. Similarly, array[7]
refers to the value stored at 10028 (10000 + 7 × sizeof(int)) and it is 7.
In the following example, the function multi2 doubles the array elements:
1 // double . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 void multi2 ( i n t * array , i n t length )
5 {
6 i n t iter ;
7 f o r ( iter = 0; iter < length ; iter ++)
8 {
9 array [ iter ] *= 2;
10 }
11 }
12 i n t main ( i n t argc , char * argv [])
13 {
14 i n t * arr ;
15 i n t iter ;
16 i n t length = 12;
17 arr = malloc ( length * s i z e o f ( i n t ) ) ;
18 i f ( arr == NULL )
19 {
124 Intermediate C Programming
44 free ( arr ) ;
45 return EXIT_SUCCESS ;
46 }
The output of this program is shown below:
Original array: 0 1 2 3 4 5 6 7 8 9 10 11
New array: 0 2 4 6 8 10 12 14 16 18 20 22
Remember free must be called before the program ends, otherwise, the program has
a memory leak. Also, to make the program easier to understand and easier to debug, the
program should call malloc and free in the same function. If a program calls malloc and
free in different functions, then it becomes much harder to track whether:
1. memory allocated by calling malloc is released by calling free later or,
2. memory released by calling free has been allocated by calling malloc earlier.
Chapter 9
Programming Problems Using Heap
Memory
125
126 Intermediate C Programming
random since the seed is predictable. Generating truly random numbers is beyond the scope
of this book. This is the program for generating test inputs:
1 // testgen . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < time .h >
5 #i n c l u d e < string .h >
6 #d e f i n e RANGE 10000
7 i n t main ( i n t argc , char * * argv )
8 {
9 i f ( argc < 2)
10 {
11 printf ( " need a positive integer \ n " ) ;
12 return EXIT_FAILURE ;
13 }
14 i n t num = strtol ( argv [1] , NULL , 10) ;
15 i f ( num <= 0)
16 {
17 printf ( " need a positive integer \ n " ) ;
18 return EXIT_FAILURE ;
19 }
20 srand ( time ( NULL ) ) ; // set the seed
21 i n t count ;
22 f o r ( count = 0; count < num ; count ++)
23 {
24 printf ( " % d \ n " , rand () % RANGE ) ;
25 }
26 return EXIT_SUCCESS ;
27 }
This problem requires the output to be sorted. If the generator produces a sequence of
random numbers, how can we get the correctly sorted result without first knowing that the
final program is correct? This is a circular problem: We do not have the program correctly
written yet so we do not have the sorted numbers. Without sorted numbers, we cannot check
whether the program is correct. Fortunately, we can use the sort program in Linux. The
sort program treats the values as strings; “9” is greater than “10” because the character
’9’ is greater than the character ’1’. Adding -n after sort treats the values as numbers, and
9 is smaller than 10. Below is the Makefile—it calls the sort program in Linux.
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3
4 testgen : testgen . c
5 $ ( GCC ) testgen . c -o testgen
6
7 inputgen : testgen
8 ./ testgen 6 > input6
9 ./ testgen 20 > input20
10 ./ testgen 50 > input50
11 ./ testgen 100 > input100
12 sort -n input6 > expected6
13 sort -n input20 > expected20
Programming Problems Using Heap Memory 127
17 clean :
18 / bin / rm testgen input * expected *
Now if we type:
$ make inputgen
eight files will be generated: Four are called input and the other four are called expected.
The input files have numbers in some random order. The numbers in the expected files are
sorted.
9 .c.o:
10 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
11
12 testinput : mysort
13 ./ mysort 6 < input6 > temp6
14 diff temp6 input6
15 ./ mysort 20 < input20 > temp20
16 diff temp20 input20
17 ./ mysort 50 < input50 > temp50
18 diff temp50 input50
19 ./ mysort 100 < input100 > temp100
20 diff temp100 input100
21
22 testgen : testgen . c
23 $ ( GCC ) testgen . c -o testgen
24
25 inputgen : testgen
26 ./ testgen 6 > input6
27 ./ testgen 20 > input20
28 ./ testgen 50 > input50
29 ./ testgen 100 > input100
30 sort -n input6 > expected6
31 sort -n input20 > expected20
32 sort -n input50 > expected50
33 sort -n input100 > expected100
34
35 clean :
36 / bin / rm testgen input * expected * temp *
If we type:
$ make testinput
it ensures that “mysort” has been compiled, and then runs the program in such a way that
the program reads input from an input file, instead of from the keyboard.
$ ./mysort 6 < input6 > temp6
The argument argv[1] is the number of integers. The actual values are stored in the
input file. In a later chapter we will explain how to get the number of integers from the file
itself without using argv[1]. With < input6, the input comes from the file called input6,
and not from the keyboard. With > temp6, the output is stored in the file called temp6,
and not printed to the computer screen. Using files like this helps us repeat the same tests
easily. The next line,
$ diff temp6 input6
checks whether the program reads the input values correctly. If the program is incorrect,
the values in temp6 and input6 are different. It is important to ensure the input values are
correct before sorting the values. If the program cannot read the input values correctly, the
program should be corrected before attempting to sort the values.
Programming Problems Using Heap Memory 129
parts: the part before ind1 has been sorted and the part after ind1 has not been sorted. To
sort the second part, we select the smallest inside this part and move it to the beginning of
the second part. Then, ind1 increases, effectively shrinking the second part.
Line 20 initializes minind to ind1. This stores the index of the smallest element seen
so far in the second part of the array. Then, lines 21 to 27 find the index of the smallest
element in the second part of the array. Lines 28 to 32 move the smallest value to the correct
place in the array. This is achieved by swapping the smallest value from its current location
to the correct location. The number of comparisons (line 23) depends on the number of
elements and is independent of the actual values of the elements.
This program uses the same swap function described in Section 4.4. The swap function
is marked static. A static function can be called by functions in the same file only. A static
function is invisible outside this file.
Consider the following example: The input values are 1694, 8137, 609, 7118, 5614, and
8848. The smallest value is the third element (index is 2). The first iteration of ind1 swaps
the first (index is 0) and the third elements and now the array’s elements are 609, 8137,
1694, 7118, 5614, and 8848. The following table shows the array’s elements in each iteration,
just before calling swap:
ind1 minind Sorted Unsorted
0 2 1694 8137 609 7118 5614 8848
1 2 609 8137 1694 7118 5614 8848
2 4 609 1694 8137 7118 5614 8848
3 3 609 1694 5614 7118 8137 8848
4 4 609 1694 5614 7118 8137 8848
5 5 609 1694 5614 7118 8137 8848
This is the sorted array: 609 1694 5614 7118 8137 8848. The main function has a few
places that require explanation:
• This main function stores the data in an array. The size of the array is given by
argv[1].
• Before using argv[1], the program must check that argc is 2. If argc is 1, then
argv[1] does not exist (argv[0] does) and attempting to access argv[1] will crash
the program.
• The program uses strtol to convert argv[1] from a string to an integer.
• The main function must call malloc to allocate heap memory for the array before
reading data from the file.
• The main function must call free to release the heap memory of the array before the
program ends.
1 /*
2 * main . c
3 */
4 #i n c l u d e < stdio .h >
5 #i n c l u d e < stdlib .h >
6 #i n c l u d e < string .h >
7 #i n c l u d e " mysort . h "
8 i n t main ( i n t argc , char * * argv )
9 {
10 i f ( argc != 2)
11 {
12 return EXIT_FAILURE ;
13 }
14 i n t number = strtol ( argv [1] , NULL , 10) ;
Programming Problems Using Heap Memory 131
15 i n t * arr ;
16 arr = malloc ( s i z e o f ( i n t ) * number ) ;
17 i f ( arr == NULL )
18 {
19 return EXIT_FAILURE ;
20 }
21 i n t ind ;
22 f o r ( ind = 0; ind < number ; ind ++)
23 {
24 scanf ( " % d " , & arr [ ind ]) ;
25 }
26 mysort ( arr , number ) ;
27 f o r ( ind = 0; ind < number ; ind ++)
28 {
29 printf ( " % d \ n " , arr [ ind ]) ;
30 }
31 free ( arr ) ;
32 return EXIT_SUCCESS ;
33 }
The Makefile has a section for testing. It runs the program for the four test cases and
compares the outputs with the expected results.
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3 OBJS = mysort . o main . o
4 HDRS = mysort . h
5
9 .c.o:
10 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
11
12 test : mysort
13 ./ mysort 6 < input6 > output6
14 diff output6 expected6
15 ./ mysort 20 < input20 > output20
16 diff output20 expected20
17 ./ mysort 50 < input50 > output50
18 diff output50 expected50
19 ./ mysort 100 < input100 > output100
20 diff output100 expected100
21
22 testgen : testgen . c
23 $ ( GCC ) testgen . c -o testgen
24
25 inputgen : testgen
26 ./ testgen 6 > input6
27 ./ testgen 20 > input20
28 ./ testgen 50 > input50
29 ./ testgen 100 > input100
132 Intermediate C Programming
35 clean :
36 / bin / rm -f temp * testgen input *
37 / bin / rm -f expected * *. o output * mysort
As we can see, Makefile can substantially simplify testing, because it saves a lot of time
typing these commands over and over again.
11 .c.o:
12 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
13
14 test : mysort
15 $ ( VALGRIND ) log6 ./ mysort 6 < input6 > output6
16 diff output6 expected6
17 $ ( VALGRIND ) log20 ./ mysort 20 < input20 > output20
18 diff output20 expected20
19 $ ( VALGRIND ) log50 ./ mysort 50 < input50 > output50
20 diff output50 expected50
21 $ ( VALGRIND ) log100 ./ mysort 100 < input100 > output100
22 diff output100 expected100
23
24 testgen : testgen . c
25 $ ( GCC ) testgen . c -o testgen
26
27 inputgen : testgen
28 ./ testgen 6 > input6
29 ./ testgen 20 > input20
30 ./ testgen 50 > input50
31 ./ testgen 100 > input100
32 sort -n input6 > expected6
Programming Problems Using Heap Memory 133
37 clean :
38 / bin / rm -f temp * testgen input * expected *
39 / bin / rm -f *. o output * mysort log *
If we remove
31 free ( arr ) ;
near the end of main, then the program will leak memory. The log files generated by
valgrind will show something like:
at the bottom. If we look backwards in the log file, we will something similar to:
The program leaks 24 bytes of memory, and the memory was allocated at line 16 in
main.c. Why does the program leak 24 bytes? The program allocates space for 6 inte-
gers by calling malloc. Each integer occupies 4 bytes so the program leaks 24 bytes. (i.e.,
sizeof(int) × 4 = 24.) If we put the free statement back, valgrind reports
We should always check valgrind’s reports when writing programs. Remember that if
valgrind reports problems then the program has problems. If valgrind reports no prob-
lems, then the program may still have problems but valgrind failed to detect them.
9.2.1 qsort
First, let’s examine the manual for qsort:
134 Intermediate C Programming
NAME
qsort - sorts an array
SYNOPSIS
#include <stdlib.h>
DESCRIPTION
RETURN VALUE
The qsort() function returns no value.
It is important to become comfortable with the manual pages for C functions. They may
appear terse at first, but they are well written. Their target audience is the people who have
some familiarity with C. The manual says qsort requires four arguments:
1. base: the address of the first element of the array. This should be & arr[0].
2. the number of elements (members) in the array.
3. the size of each element in bytes. If it is an integer array, this argument should be
sizeof(int). Some students write 4 and this is wrong. The size of an integer is not
necessarily 4. Your program will fail if the size is not 4.
4. a comparison function.
What is void *? Why is void * the type of base? It means that the memory address
can point to any type. This is important for a general-purpose function. Thus we can use
qsort to sort any type of array. It can be int *, or char *, or double *, as long as it is
an address of a valid array. The type being pointed to is specified indirectly by the third
argument. The third argument informs qsort of the size of each array element. Among the
four arguments, the last one requires a new concept: passing a function as an argument to
another function.
1. int(*compar)(const void *, const void *) means that this argument is the
name of a function. How do I know it is a function? Because of the parenthesis after
(*compar).
2. int before (* compar) means that the passed function must return an integer. Why
is there an asterisk? Because the name of a function is a pointer to the function.
Section 2.3 said that whenever a function is called, the return location is pushed onto
the call stack. What does this mean? Each line of a program has a location (i.e.,
an address). This address is neither in the call stack, nor in the heap. The address
Programming Problems Using Heap Memory 135
is in another part of memory that stores the compiled program’s instructions. The
instructions must have addresses because they are stored in memory. Every line of
a program is stored at a memory location. Thus, it is possible to use an address to
specify a particular line of a program. By convention, C uses the name of a function as
the address of the first line of a function. This is the reason a function can be expressed
by a pointer: The function name is the address of the first line of that function.
3. The passed function takes two input arguments. Each argument stores an address.
Again, void * means that the address can be of any type. Section 7.1.7 explains
the meaning of const. This function cannot change the value stored at the address
because const is in front of the type (even though the type is void).
Putting all these factors together, the comparison function must have the following type:
1 i n t comparefunc ( const void * a , const void * b )
type. It is meaningless comparing addresses. Instead, lines 8 and 9 retrieve the values
stored at those addresses.
3. Lines 11 to 15 return a negative, zero, or positive value based on whether val1 is less
than, equal to, or greater than val2. This comparison function will cause the array
elements to be sorted in ascending order. If we want the elements to be sorted in
the descending order, then we can change lines 11 to 15 so that the function returns
positive, zero, or negative if val1 is less than, equal to, or greater than val2.
The following shows a program that uses qsort to sort an array of integers:
1 // compareint . c
2 i n t comparefunc ( const void * arg1 , const void * arg2 )
3 {
4 const i n t * ptr1 = ( const i n t *) arg1 ;
5 const i n t * ptr2 = ( const i n t *) arg2 ;
6 i n t val1 = * ptr1 ;
7 i n t val2 = * ptr2 ;
8 i f ( val1 < val2 ) { return -1; }
9 i f ( val1 == val2 ) { return 0; }
10 return 1;
11 }
Here is the main function:
1 /*
2 * mainqsort . c
3 */
4 #i n c l u d e < time .h >
5 #i n c l u d e < stdio .h >
6 #i n c l u d e < stdlib .h >
7 #i n c l u d e < string .h >
8 #d e f i n e RANGE 10000
9 i n t comparefunc ( const void * arg1 , const void * arg2 ) ;
10
31 }
32 i n t * arr ;
33 arr = malloc ( s i z e o f ( i n t ) * size ) ;
34 i f ( arr == NULL )
35 {
36 return EXIT_FAILURE ;
37 }
38 i n t ind ;
39 srand ( time ( NULL ) ) ; // set the seed
40 f o r ( ind = 0; ind < size ; ind ++)
41 {
42 arr [ ind ] = rand () % RANGE ;
43 }
44 printArray ( arr , size ) ;
45 qsort (& arr [0] , size , s i z e o f ( i n t ) , comparefunc ) ;
46 printArray ( arr , size ) ;
47 free ( arr ) ;
48 return EXIT_SUCCESS ;
49 }
2. The second argument is the number of strings in the array and it is argc.
3. The third argument is the size of each element. Since each element is a string, the
type is char * and the size is sizeof(char *). Remember that an array of strings is
an array of pointers.
4. The last argument is the comparison function.
1 // mainqsortstr . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 i n t cmpstringp ( const void * arg1 , const void * arg2 ) ;
6 i n t main ( i n t argc , char * * argv )
7 {
8 i n t ar ;
9 i f ( argc < 2)
10 {
11 fprintf ( stderr , " Usage : % s < string >...\ n " , argv [0]) ;
12 return EXIT_FAILURE ;
13 }
14 qsort (& argv [0] , argc , s i z e o f ( char *) , cmpstringp ) ;
15 f o r ( ar = 0; ar < argc ; ar ++)
16 {
17 printf ( " % s \ n " , argv [ ar ]) ;
18 }
19 return EXIT_SUCCESS ;
20 }
The comparison function is similar to the one for an array of integers but the types
are different: arg1 and arg2 are the addresses of strings. Thus, their types are char * *.
Imagine that C has a type called string and arg1 and arg2 store the addresses of strings.
Thus, arg1 and arg2 are of type string *. Since string is actually char * in C, arg1 and
arg2 are of type char * *. After casting the types of arg1 and arg2, the program then
needs to get the strings from the addresses by adding * to retrieve the values stored at the
addresses. Finally, the program uses strcmp to compare the two strings.
1 // comparestr . c
2 #i n c l u d e < string .h >
3 i n t cmpstringp ( const void * arg1 , const void * arg2 )
4 {
5 // ptr1 and ptr2 are string *
6 // string is char * , thus ptr1 and ptr2 are char * *
7 const char * const * ptr1 = ( const char * *) arg1 ;
8 const char * const * ptr2 = ( const char * *) arg2 ;
9 const char * str1 = * ptr1 ; // type : string
10 const char * str2 = * ptr2 ;
11 return strcmp ( str1 , str2 ) ;
12 }
Quick sort is faster than selection sort because quick sort uses transitivity. How much
faster is it? Fig. 9.1 compares the execution times for sorting arrays of different sizes. When
the number of elements increases, the execution time increases for both quick sort and
selection sort. However, the execution time for selection sort increases much faster, i.e., the
ratio increases. The ratio actually increases to infinity as the size of the array increases. This
140 Intermediate C Programming
means that no matter how fast a computer is, if an array is sufficiently large then selection
sort will behave poorly when compared to quick sort. Selection sort can be faster for small
arrays. This is because the logic of selection sort is simpler. What counts as “small” or
“large” may be empirically determined for a given computer.
(a)
(b)
FIGURE 9.1: (a) Execution time for selection sort and quick sort. (b) The ratio of the
execution time. Please note that both axes use a logarithmic scale.
To summarize, selection sort is an algorithm that selects the smallest value among the
remaining unsorted array elements in each iteration. C has a built-in function called qsort
and it can sort arrays of different types. It knows how to sort elements because programmers
tell qsort the size of each element and provide functions that compares the elements.
Chapter 10
Reading and Writing Files
We have already taken advantage of redirection to use files as inputs and outputs. This
chapter explains how to use C functions to read from or to write to files without using
redirection.
141
142 Intermediate C Programming
Running the program without passing the file’s name on the command line will cause
an error message to be printed and the program returns EXIT FAILURE. Use gcc to compile
and link the program:
$ gcc -Wall -Wshadow file1.c -o file1
The program exits if running without any arguments:
$ ./file1
Need to provide the file’s name.
When the main function returns, the program terminates. By returning EXIT FAILURE, this
program informs the terminal that this program ends abnormally. If the file’s name is given,
then the program prints the file’s name:
$ ./file1 xyz
The name of the file is xyz.
(a) / * f i l e : E S S ; \n } \n EOF
(b) * f i l e : E S S ; \n } \n EOF
fgetc '/'
(c) f i l e : E S S ; \n } \n EOF
fgetc '*'
(d) EOF
fgetc '\n'
(e)
fgetc EOF
FIGURE 10.1: A file is a stream. This example uses the program source code as the input
file. (a) After calling fopen, the stream starts from the very first character of the file and
ends with EOF. EOF is a special character that does not actually exist in the file, but signifies
that there is no data left in the stream. (b),(c) Each time fgetc is called, one character is
taken out of the stream. (d) After calling fgetc enough times, all the characters in the file
are retrieved. We have not yet attempted to read past the end of the file. (e) Finally, the
end of file character EOF is returned because there are no more characters in the file.
How does fgetc work? After calling fopen, fptr points to a stream of characters, as
illustrated in Fig. 10.1. This stream starts at the beginning of the file. Every time fgetc
is called, one character is taken out from the stream. If the program keeps calling fgetc,
eventually all characters are taken and the special character EOF is returned.
This program counts the number of characters. A character may be a Latin character
(’a’ to ’z’ or ’A’ to ’Z’), a digit (’0’ to ’9’), a punctuation mark (such as ’,’ and ’;’), space, or
an invisible character. At the end of each line, a new line character ( ’\n’) is also counted.
144 Intermediate C Programming
When the program attempts to read beyond the end of the file, fgetc returns EOF. This
character is called end of file and its symbol EOF is defined in stdio.h. If we search EOF
using Linux’ grep command:
$ grep EOF /usr/include/stdio.h
we should find
Its value is −1. The manual for fgetc says, “fgetc() reads the next character from stream
and returns it as an unsigned char cast to an int, or EOF on end of file or error.”
What does this mean? This function reads one character from a file. This character is
treated as an unsigned character because ASCII (American Standard Code for Information
Interchange) has only positive values. Unsigned characters can have values between 0 and
255 inclusive. The function then casts the character to an integer. Why is this necessary?
Because EOF is negative and is not an unsigned character. Thus fgetc returns −1, or 0
to 255 inclusive. This guarantees that EOF can be distinguished from the valid characters
that are actually in the file. Another way to detect the end of file is by calling the function
feof. This function returns a non-zero value if the end of file has been reached. Thus we
can replace line 21 by:
21 while (! feof ( fptr ) )
and remove while ( ch != EOF ); at line 28.
This program reports the number of characters in the file. Suppose that the source for
this program is in the file file2.c and we compile and execute it like so:
Linux has a program called wc and it reports the numbers of lines, words, and characters
in a file. The program reports that the file2.c has 32 lines, 96 words, and 656 characters.
The wc program considers a word to be a non-zero-length sequence of characters delimited
by space.
$ wc file2.c
32 96 656 file2.c
Operating systems usually restrict the number files that a program can open at once
to ensure that one program does not use too many resources. Thus programs should call
fclose when a previously opened file is no longer needed. Just as with malloc and free,
it is a good habit to type fclose right after typing fopen and then insert appropriate
code between them. This can prevent forgetting to call fclose. In fact, fopen will allocate
memory in the program. Thus, if a program does not call fclose, then the program has
memory leak. Some students write this:
15 i f ( fptr == NULL )
16 {
17 printf ( " fopen fail .\ n " ) ;
18 fclose ( fptr ) ;
19 }
Reading and Writing Files 145
This is wrong. If fptr is NULL, then fopen fails to open the file. If the file is not open,
then it cannot be closed. The documentation of fclose clearly says:
Thus, fclose(NULL) is bad, since it results in unpredictable behavior. Also, note that
it is an error to close the same file pointer twice.
What is stored at the heap memory pointed by fptr? The following uses gdb to show
the contents at the memory address pointed by fptr.
As we can see, the data that the FILE * points to is complicated. Fortunately, we do
not need to know the details since they are purely internal to the C library, and should not
be modified or examined directly.
4 7 8
32
71
6 -2 5 8
Below is the output when we run the program with intfile as the command-line argument:
13 8644
16 9070
17 4930
18 8775
19 670
20 521
21 3582
22 8644
The following program solves this problem:
1 // addint . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t main ( i n t argc , char * argv [])
5 {
6 i f ( argc < 4) // need two inputs and one output
7 {
8 return EXIT_FAILURE ;
9 }
10 FILE * fin1 ;
11 FILE * fin2 ;
12 // open the two input files
13 fin1 = fopen ( argv [1] , " r " ) ;
14 i f ( fin1 == NULL ) // fail to open
15 {
16 return EXIT_FAILURE ;
17 }
18 fin2 = fopen ( argv [2] , " r " ) ;
19 i f ( fin2 == NULL )
20 {
21 fclose ( fin1 ) ; // need to close opened file
22 return EXIT_FAILURE ;
23 }
24 // open the output file
25 FILE * fout ;
26 fout = fopen ( argv [3] , " w " ) ;
27 i f ( fout == NULL )
28 {
29 fclose ( fin1 ) ;
30 fclose ( fin2 ) ;
31 return EXIT_FAILURE ;
32 }
33
34 i n t val1 ;
35 i n t val2 ;
36 i n t in1ok = 1; // can still read input file 1
37 i n t in2ok = 1; // can still read input file 2
38 // continue as long as one file still has numbers
39 while (( in1ok == 1) || ( in2ok == 1) )
40 {
41 val1 = 0; // reset the values before reading from files
42 val2 = 0;
Reading and Writing Files 149
61 return EXIT_SUCCESS ;
62 }
Line 6 checks whether enough arguments have been provided. Lines 13 to 32 open the
files. If fopen fails, then the program returns EXIT FAILURE. Please remember to close all
successfully opened files; otherwise, the program leaks memory allocated by fopen. At line
21, the program has failed to open the second file, and thus needs to close the first opened
file before returning. The condition at line 39 means “continue if one (or both) of the files
still has numbers”. This handles the situation when the two files have different numbers of
integers. The variables in1ok and in2ok are updated at lines 45 and 49.
Note that when a file reaches its end, fscanf returns EOF, and not zero. A common
mistake at lines 43 and 47 is using == 0. Since EOF is −1, if we replace != 1 by == 0 at
lines 43 and 47 then the program will enter an infinite loop. If the program reads successfully
from at least one of the two files, the program writes the sum to the output file. Lines 41 and
42 reset the values to zero. This is necessary because one file may have already reached the
end, in which case calling fscanf will not update one of val1 and val2. Without resetting
the values we get the wrong answer when one file is longer than the other.
This program specifically does not consider overflowing or underflowing of integers. What
does this mean? When a program creates an integer variable, the size of the variable is fixed
(dependent on the machine). Suppose an integer has 4 bytes, i.e., sizeof(int) is 4. One
byte is 8 bits and each bit can hold either 0 or 1. Thus, a 4-byte integer can hold 32 bits,
namely 232 possible values. The possible values include both positive and negative integers.
An integer can hold a value between 231 − 1 (2147483647) and −231 (−2147483648), totally
232 possible values. If a file contains a number greater than 2147483647 or smaller than
−2147483648, fscanf will not work. Thus the behavior of the program is unspecified if the
input numbers are too large or too small. By stating this, we put the burden on the user to
ensure that the numbers are within the range.
150 Intermediate C Programming
When the program cannot read from the file any more, fgets returns NULL. This means
that the end of the file has been reached. The C library has a function called getline and
it can be used to a line of arbitrary size.
Chapter 11
Programming Problems Using File
153
154 Intermediate C Programming
of debugging time, and often potential problems can be considered before typing a single
line of code.
Below is a sample solution for this program. If we compare the program and the steps
listed above, we will find a close correspondence between them. This program uses the
built-in qsort function to sort integers.
1 // sortint . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t comparefunc ( const void * arg1 , const void * arg2 )
5 {
6 const i n t * ptr1 = ( const i n t *) arg1 ; // cast type
7 const i n t * ptr2 = ( const i n t *) arg2 ;
8 const i n t val1 = * ptr1 ; // get the value from the address
9 const i n t val2 = * ptr2 ;
10 i f ( val1 < val2 ) // compare the value
11 { return -1; }
12 i f ( val1 == val2 )
13 { return 0; }
14 return 1;
15 }
16 i n t main ( i n t argc , char * argv [])
17 {
18 // need two file names : input and output
19 i f ( argc < 3)
20 {
21 return EXIT_FAILURE ;
22 }
23 // open the input file
24 FILE * infptr ;
25 infptr = fopen ( argv [1] , " r " ) ;
26 i f ( infptr == NULL )
27 {
28 return EXIT_FAILURE ;
29 }
30 // count the number of integers in the file
31 i n t count = 0;
32 i n t val ;
33 while ( fscanf ( infptr , " % d " , & val ) == 1)
34 {
35 count ++;
36 }
37 // allocate memory for the array
38 i n t * arr ;
39 arr = malloc ( s i z e o f ( i n t ) * count ) ;
40 i f ( arr == NULL )
41 {
42 fclose ( infptr ) ;
43 return EXIT_FAILURE ;
44 }
45 // go to the beginning of the file
Programming Problems Using File 155
46 i n t ind ;
47 f o r ( ind = 0; ind < NUM_CHAR ; ind ++)
48 {
49 fprintf ( outfptr , " % c : % d \ n " , ind + ’A ’ ,
50 charcount [ ind ]) ;
51 }
52 // close outupt file
53 fclose ( outfptr ) ;
54 return EXIT_SUCCESS ;
55 }
The main difference between this program and the previous program is in lines 24 to 35.
Line 27 uses the function isupper to determine whether the character is an uppercase
letter. This function is declared in ctype.h so the program needs to include this header
file. Calling isupper is equivalent to checking whether onechar is between ’A’ and ’Z’. The
ASCII value for ’A’ is 65 and the ASCII value for ’Z’ is 90. However, you should not check
whether onechar is between 65 and 90. There are a few reasons for this suggestion. First,
if you accidentally type 89 instead of 90, it is not easy to detect the mistake. It is difficult
remembering that ’Z’ is 90, not 89. By contrast, if you type ’Y’ instead ’Z’, it is easier to
detect the mistake. This brings us to the main reason for preferring ’A’ and ’Z’ to 64 and 90:
It is clear and easy to read. Clarity is one of the most important qualities of well-written
code. Did you notice that I incorrectly wrote 64, not 65? If you missed that mistake, it is
likely that you would miss similar mistakes in your programs.
How about converting uppercase letters to lowercase? Many students write
1 i f (( onechar >= 65) && ( onechar <= 90) )
2 {
3 onechar += 32;
4 }
This is bad. Why? It is difficult to understand the meaning of 65, 90, and 32. What
happens if we accidentally type 31 instead of 32? How much time does it take to find such
a mistake? It will take longer than you think. It is much better to write:
1 i f (( onechar >= ’A ’) && ( onechar <= ’Z ’) )
2 {
3 onechar = ( onechar - ’A ’) + ’a ’;
4 }
Do not overlook the importance of these details. I have seen many students making
“small” mistakes like these. They are overly confident that they do not make mistakes.
When you write a complex program, the problems from these details can easily take hours
to detect and correct. Good programmers know this well, and dramatically improve their
efficiency by making things as simple and as clear as possible. It allows programmers to
write sophisticated computer programs more easily.
Lines 29 and 33 use the values in the ASCII table to calculate the corresponding index
for the array charcount. If the character is ’A’, then onechar - ’A’ is 0. If the character
is ’B’, then onechar - ’A’ is 1. If the character is ’c’, onechar - ’a’ is 2. Some students
write something like:
1 i f ( onechar == ’A ’)
2 charcount [0] ++;
3 i f ( onechar == ’B ’)
4 charcount [1] ++;
158 Intermediate C Programming
5 i f ( onechar == ’C ’)
6 charcount [2] ++;
7 i f ( onechar == ’D ’)
8 charcount [2] ++;
9 i f ( onechar == ’E ’)
10 charcount [3] ++;
Fifty-two conditions are needed. The problem should be obvious: It is easy to make mistakes.
If fact, there are mistakes in the code above. Can you detect them easily? There is a general
principle in writing good programs: Do not copy-paste code. Write DRY code. DRY stands
for “Don’t Repeat Yourself”. The opposite of DRY code is WET code, which stands for
“We Enjoy Typing”.
There are many reasons to follow the DRY principle. If you copy-paste code, then you
increase the chances of mistakes. This is especially true when the code is modified after it is
written. Once we have two (or more) pieces of WET code, testing, debugging and improving
the code becomes more difficult. We need to remember to change all places that the code is
repeated. If we forget to change some places, then the program will surprise you: In some
situations, the program is correct, but in others it fails.
There are many simple and clear solutions that avoid WET code. For example, if the
two (or more) pieces of code are identical, then create a function and call it twice. If they
are mostly similar but with a few differences, then the function’s arguments can handle the
differences. Spending some time fixing WET code usually helps tremendously in developing
good test cases, since the programmer must think about and ultimately understand the
code. That also helps with debugging, since you will be more familiar with how the program
should behave as you step through it line by line.
Line 49 is the reverse way of using the ASCII values. If the index is 0, it corresponds to
’A’ so the value of ’A’ is added. The output of this program will look something like:
A: 39
B: 1
C: 41
D: 16
E: 69
F: 38
thus needs memory for 81 characters). The program does not count a word that spans two
or more lines. The program uses strstr to search a word within a line.
1 // countstr . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #d e f i n e LINE_LENGTH 81
6 i n t main ( i n t argc , char * argv [])
7 {
8 i f ( argc < 4) // input word output
9 {
10 return EXIT_FAILURE ;
11 }
12 // open the input file
13 FILE * infptr ;
14 infptr = fopen ( argv [1] , " r " ) ;
15 i f ( infptr == NULL )
16 {
17 return EXIT_FAILURE ;
18 }
19 // open the output file
20 FILE * outfptr ;
21 outfptr = fopen ( argv [2] , " w " ) ;
22 i f ( outfptr == NULL )
23 {
24 fclose ( infptr ) ;
25 return EXIT_FAILURE ;
26 }
27 i n t count = 0;
28 char oneline [ LINE_LENGTH ];
29 while ( fgets ( oneline , LINE_LENGTH , infptr ) != NULL )
30 {
31 i f ( strstr ( oneline , argv [3]) != NULL )
32 {
33 fprintf ( outfptr , " % s " , oneline ) ;
34 }
35 char * chptr = oneline ;
36 while ( chptr != NULL )
37 {
38 chptr = strstr ( chptr , argv [3]) ;
39 i f ( chptr != NULL )
40 {
41 count ++;
42 // if " eyeye " counts as two " eye "
43 chptr ++;
44 // if " eyeye " counts as one " eye "
45 // chptr += strlen ( argv [3]) ;
46 }
47 }
48 }
160 Intermediate C Programming
This explains the arguments, the behavior of the function, and the return value. Notice
how clear and dense the text is. No word is wasted.
A common mistake is to repeat information that is obvious from the syntax. The fol-
lowing comment is unnecessary:
1 /* This function has two arguments . Both are integers .
2 The function returns an integer . */
3 i n t func ( i n t a , i n t b ) ;
Compare with the informative comment:
1 /* The function returns
2 1 if a > b
3 0 if a == b
4 -1 if a < b
5 */
6 i n t func ( i n t a , i n t b ) ;
Comments should provide some information that is unavailable from the syntax. Com-
ments are important when explaining complex concepts. The example below shows a call
stack:
/*
---------------------------------------
| Frame | Symbol | Address | Value |
---------------------------------------
| | z | 103 | 5 |
| f2 | y | 102 | 8 |
| | x | 101 | -7 |
---------------------------------------
*/
/*
open input file -> count the number of lines -|
|
----------------------------------------------|
|
---> allocate memory -> return to the beginning of the file
*/
The following is also good for those who do not like drawing:
/*
1. open input file
2. count the number of lines
3. allocate memory
4. return to the beginning of the file
*/
It should be apparent that comments can express important concepts. It takes practice
to write comments well. It is often helpful to read others’ code to see what comments are
162 Intermediate C Programming
useful, and what are merely distractions. This is important for your own code. If you read
a program written six months ago, can you understand it easily? If the meaning is not
apparent, then the commenting can be improved.
With practice, comments become a good way to further understand your code by testing
your ability to explain it. This, in turn, helps catch subtle problems, and also helps you
generate good test cases. By guiding the eye of the reader through the code, good comments
augment carefully chosen variable names, and clear syntax. Doing this shows that you have
thought deeply about the program.
Part II
Recursion
163
This page intentionally left blank
Chapter 12
Recursion
Recursion is an everyday phenomenon that is natural to the human mind. Sometimes re-
cursive computer programs can be challenging to reason about; however, recursion itself is
readily understandable. For example, every person has parents, who have parents, who have
parents, etc. That is an example of recursion. For another example, take two mirrors and
make them face each other. A characteristic pattern will appear with images within images
within images, etc. We also see recursion in cell-growth, and this manifests in the shapes
of living things. For example, a tree’s trunk is divided into main branches. Each branch in
turn is further divided into smaller branches. Smaller branches are divided into twigs, and
eventually we have leaves. This is the third example of recursion. In this case the recursive
branching of the trunk into twigs is bounded by the leaves. Recursion is everywhere around
us, and is also part of us. It is part of language, the way we think, and how our bodies grow.
Recursion is one of nature’s ways for solving complex problems.
There are three essential properties of recursion:
1. Recurring patterns. The examples above describe some recurring patterns: a person,
the person’s parents, their parents ...
2. Changes. Recursion does not merely mean repeating. A person is younger than the
parents and they are younger than their parents. In the two mirrors, images become
smaller. For a tree, branches become thinner. Each step of a recursive pattern has a
characteristic change.
3. A terminating condition (or conditions). The recurring pattern eventually stops. A
family tree stops at the youngest member that has no child. The images in the fac-
ing mirrors will become smaller and eventually invisible. When a branch eventually
becomes leaves, the pattern stops.
Recursion can be a strategy for solving problems using a concept called divide and
conquer: Divide a complex problem into smaller problems and solve (conquer) the smaller
problems. This works when the smaller problems are related to the larger problem. Recursion
uses the following steps:
1. Identify the argument (or arguments) of a problem.
165
166 Intermediate C Programming
FIGURE 12.1: To decide f (n), consider the possibilities for the first ball. If the first ball
is B, the remaining n − 1 balls have f (n − 1) possibilities. If the first ball is R, then the
second ball must be B and the remaining n − 2 balls have f (n − 2) possibilities.
FIGURE 12.2: To decide f (n), consider the possibilities for the first ball. If the first ball
is G or B, the remaining n − 1 balls have f (n − 1) possibilities. If the first ball is R, the
second ball must be G or B and the remaining n − 2 balls have f (n − 2) possibilities.
3
if n is 1
f (n) = 8 if n is 2
f (n − 1) + f (n − 1) + f (n − 2) + f (n − 2) = 2f (n − 1) + 2f (n − 2) if n > 2
(12.2)
When n is two, there are seven possibilities. Please notice that G G is an invalid option.
1. R G
2. R B
3. G R
4. G B
5. B R
6. B G
7. B B
We use a different approach to solve this problem, by using some additional functions:
• r(n) is the number of possible sequences when selecting n balls and the first ball is R.
• g(n) is the number of possible sequences when selecting n balls and the first ball is G.
• b(n) is the number of possible sequences when selecting n balls and the first ball is B.
• f (n) is the number of possible sequences when selecting n balls and the first ball can
be any of the three colors. Thus, f (n) = r(n) + g(n) + b(n).
The following table shows the values of these functions for n equal to 1 or 2:
n 1 2
r(n) 1 2
g(n) 1 2
b(n) 1 3
f (n) 3 7
How is r(n) calculated when n is greater than 2? By definition, r(n) means the number
of possible sequences of n balls and the first one is R. For the remaining n − 1 balls, the first
ball (the second among the n balls) can be either G or B. If the first (the second among the
n balls) ball is G, there are g(n − 1) possibilities. Similarly, when the first ball (the second
among the n balls) is B, there are b(n−1) options. Thus, r(n) = g(n−1)+b(n−1). Because
the same restrictions apply to both red and green balls, we can use the same reasoning to
write g(n) = r(n − 1) + b(n − 1).
How many possibilities are there when selecting n balls and the first ball is B? When
the first is blue then there is no restriction on the second ball in the sequence. The second
ball can be red, green, or blue; thus, this is true b(n) = g(n − 1) + b(n − 1) + r(n − 1). This
is also f (n − 1) because it means selecting n − 1 balls and the first ball can be G, B, or R.
There are f (n − 1) possible sequences of length n − 1 balls so b(n) = f (n − 1).
The complete solution is shown below:
F
D
A B
E C
FIGURE 12.3: A city’s streets form a grid, and are either east–west bound or north–south
bound. A car can move only east or north.
A city has severe traffic congestions during rush hours so the city government considers
adopting a rule: During rush hours, cars can move only east or north. All streets run either
east–west or north–south, forming a grid, as shown in Fig. 12.3.
Assume we had the location of a car’s origin and destination. How many ways can the
destination be reached by driving? This example may seem artificial but it is actually a
reasonable simplification of the one-way streets in the downtown districts of many cities.
These cities generally have one-way streets that run in opposite directions, so that cars can
move west and south as well. Nonetheless, the simplification is useful for analyzing traffic
patterns.
Fig. 12.3 marks three pairs of origins and destinations: A → B, C → D, and E → F.
How many turning options does a driver have going from one origin to their corresponding
destination? For the first two pairs A → B and C → D, the driver has only one option: not
to turn at all. This is shown in Fig. 12.4 (a) and (b). There are more options for E → F.
At E, the driver can go eastbound first or northbound first, as indicated by the two arrows
in Fig. 12.4 (c). The question is the number of different paths a driver can make between
the origin and the destination.
F F F
D D D
A B A B A B
E C E C E C
(a) (b) (c)
FIGURE 12.4: (a) A driver cannot turn anywhere when traversing from A to B. (b)
Likewise, a driver cannot turn anywhere when traversing from C to D. (c) There are some
turning options when traversing from E to F. At E, the driver can go northbound first or
eastbound first, as indicated by the two arrows.
This question can be answered using the four steps for solving the recursive problem:
1. Identify the argument (or arguments) of a problem. Suppose E is at the intersection
of (x1, y1) and F is at the intersection of (x2, y2). The distance between them can be
expressed as (∆x, ∆y) = (x2 − x1, y2 − y1).
Recursion 171
2. Express the solution based on the arguments. Let f (∆x, ∆y) express the number of
unique paths.
3. Determine the simple case(s) when the solutions are “obvious”. If ∆x < 0, the desti-
nation is at the west side of the origin and there is no solution. Similarly, there is no
solution if ∆y < 0. If ∆x > 0 and ∆y = 0 (the case A → B), then there is precisely
one solution. Likewise, if ∆x = 0 and ∆y > 0 (the case C → D), there is also precisely
one solution. These are the simple cases whose answers can be found easily. A special
case occurs when ∆x = ∆y = 0. This means that the destination is the same as the
origin. It can be defined as no solution or one solution depending on what the reader
prefers. Our solution considers that there is one solution for ∆x = ∆y = 0.
0 if ∆x < 0 or ∆y < 0
f (∆x, ∆y) = 1 if ∆x = 0 and ∆y ≥ 0 (12.4)
1 if ∆x ≥ 0 and ∆y = 0.
4. Derive the relationships between the complex case(s) and the simpler case(s). When
∆x > 0 and ∆y > 0 (the case E → F), then the driver has two options at the origin
(i.e., E): Either the driver goes north first or east first. If the driver heads north, then
the new origin is at (x1, y1 + 1). There are f (∆x, ∆y − 1) possible paths from this
point. If the driver goes east first, then the new origin is at (x1 + 1, y1). Similarly,
there are f (∆x−1, ∆y) possible paths from this point. These are the only two possible
options at position E and they are exclusive. Therefore, when ∆x > 0 and ∆y > 0,
the solution can expressed as f (∆x, ∆y) = f (∆x, ∆y − 1) + f (∆x − 1, ∆y).
0 if ∆x < 0 or ∆y < 0
1 if ∆x = 0 and ∆y ≥ 0
f (∆x, ∆y) = (12.5)
1 if ∆x ≥ 0 and ∆y = 0
f (∆x, ∆y − 1) + f (∆x − 1, ∆y) if ∆x > 0 and ∆y > 0
A B C A B C
⇒
(a) (b)
FIGURE 12.5: The Tower of Hanoi. (a) Some disks are on pole A and the goal is to move
all the disks to pole B, as shown in (b). A larger disk can never be placed on top of a smaller
disk. A third pole, C, can be used when necessary.
Some disks of different sizes are stacked on a single pole. The disks are arranged so that
smaller disks are above larger disks. The problem is to move the disks from one pole to
172 Intermediate C Programming
A B C A B C
⇒
(a) (b)
FIGURE 12.6: Moving one disk from A to B requires only one step.
A B C A B C
⇒
⇓
A B C A B C
another pole. Only one disk can be moved each time. A larger disk cannot be placed above
a smaller disk. The third pole can be used for “temporary storage”. Fig. 12.5 illustrates the
problem. If there are n disks, how many steps are needed to move them to the second pole?
First consider moving only one disk from A to B. This is the simplest case and that disk
can be moved directly from A to B as shown in Fig. 12.6.
Moving two disks requires more work. It is illegal to move the smaller disk to B and then
move the larger disk to B. Doing so would place the larger disk above the smaller disk and
violates the rules. Instead, it is necessary to move the smaller disk “somewhere else”, i.e.,
C, before moving the larger disk to B. Then, move the larger disk from A to B and move
the smaller disk from C to B. The steps are illustrated in Fig. 12.7. As illustrated, when
there are two disks, the problem can be solved in three steps. Can you think of a solution
that requires fewer steps for two disks?
Fig. 12.8 illustrates how to move three disks. The first three steps and the last three
steps are somewhat similar. The first three steps move the top two disks from A to C. The
last three steps move the top two disks from C to B. Between these steps is the fourth step,
which is to move the largest disk from A to B.
What is the general strategy for moving n disks? If there is only one disk (i.e., n is one),
the problem can be solved easily. Otherwise, the solution is divided into three parts:
1. Move the first n − 1 disks from A to C.
2. Move the largest disk from A to B.
3. Move the first n − 1 disks from C to B.
Now we put the steps together to solve the problem using recursion. The four-step
approach for solving this problem is listed below:
1. Identify the argument (or arguments) of a problem.
The number n is naturally the argument for the problem.
Recursion 173
A B C A B C
⇒
⇓
A B C A B C
⇐
⇓
A B C A B C
⇒
⇓
A B C A B C
In this case it possible to find a closed form formula: f (n) is expressed without f (n − 1)
appearing on the right side of the = sign.
f (n) = 2f (n − 1) + 1
= 4f (n − 2) + 2 + 1
= 8f (n − 3) + 4 + 2 + 1
= 16f (n − 4) + 8 + 4 + 2 + 1
(12.7)
= 2k f (n − k) + 2k−1 + 2k−2 + ... + 4 + 2 + 1
= 2n−1 f (1) + 2n−2 + 2n−3 + ... + 4 + 2 + 1, when k = n − 1
= 2n−1 + 2n−2 + 2n−3 + ... + 4 + 2 + 1, because f (1) = 1
= 2n − 1
It is not always possible, or easy, to find closed form formulas for recursive equations.
Usually this requires a working knowledge of various series. Proving the answer is correct
requires mathematical induction.
1 = 1 2 = 1 + 1 3 = 1 + 1 + 1 4 = 1 + 1 + 1 + 1
= 2 = 1 + 2 = 1 + 1 + 2
= 2 + 1 = 1 + 2 + 1
= 3 = 1 + 3
= 2 + 1 + 1
= 2 + 2
= 3 + 1
= 4
This question wants to answer the number of different partitions for a positive integer
n. This problem can be solved by using the four-step approach solving recursive problems:
1. Identify the argument (or arguments) of a problem.
The number n is naturally the argument for the problem.
2. Express the solution based on the arguments.
Let f (n) be the number of different partitions for integer n.
3. Determine the simple case(s) when the solutions are “obvious”.
When n is 1, there is only one way to partition the number: itself. When n is 2, there
are two ways: 1 + 1 and 2. Thus, f (1) = 1 and f (2) = 2.
4. Derive the relationships between the complex case(s) and the simpler case(s).
When n is larger than 2, the solution selects the first number. It must be an integer
between 1 and n inclusively. After selecting the first number, we have to partition
the remaining portion of the number. Thus for each of the n possibilities for the first
Recursion 175
number, we need to consider the number of possibilities for the remaining partition.
The relationship can be expressed in this table:
Total First Number Remaining Value to Partition
n 1 n−1
n 2 n−2
n 3 n−3
..
.
n n−2 2
n n−1 1
n n 0
These are all the possible cases and they are exclusive. If the first number is 1, then
the remaining value to be partitioned is n − 1. How many ways can n − 1 be partitioned?
By definition, it is f (n − 1). Continuing with this logic, if the first number is 2, then the
remaining value is n − 2, and by definition there are f (n − 2) ways to partition it. Using
recursion, we can assume that we have the answers to smaller versions of the problems.
This works because the smaller versions are expressed in terms of yet smaller versions, and
eventually we get to the trivial cases, i.e., f (1) = 1, and f (2) = 2.
The value of f (n) is therefore the sum of all the different cases when the first number is
1, 2, 3, . . . , n − 1, or n. Now, we can express f (n) as
1 if n is 1
f (n) = n−1
P (12.8)
f (n − 1) + f (n − 2) + . . . f (1) + 1 = 1 + f (i) if n > 1
i=1
f (n) = f (n − 1) +f (n − 2) + f (n − 3) + ... + 1
− f (n − 1) = +f (n − 2) + f (n − 3) + ... + 1
f (n) − f (n − 1) = f (n − 1)
f (n) = 2f (n − 1)
f (n) = 4f (n − 2)
f (n) = 8f (n − 3) (12.9)
f (n) = 16f (n − 4)
..
.
f (n) = 2n−1 f (1)
f (n) = 2n−1
smaller versions of the same function. We do not need to worry about the specific value of
g(n − 1), we just use it, confident that g(n − 1) will be expanded to g(n − 2), etc., until we
reach the trivial cases g(1) and g(2).
Continuing with this logic, when the first number is “2”, “1” is not used for the first
number but “1” may be used for partitioning the remaining value of n − 2. By definition,
“1” is used g(n − 2) times when partitioning n − 2.
Putting this all together, we calculate g(n) to be:
1 when n is 1
g(n) = n−1
2n−2 + g(n − 1) + g(n − 2) + . . . g(1) = 2n−2 +
P
g(i) when n > 1
i=1
(12.10)
To obtain the closed form, first find the relationship between g(n) and g(n − 1):
This table shows that the value of g(n) for 1 ≤ n ≤ 10. If a formula does not match
these cases, the formula is definitely wrong. However, matching these cases does not mean
that the formula is correct. It is necessary to have a systematic way to find the formula. It
is generally a bad idea to find a formula to match these finite values.
n 1 2 3 4 5 6 7 8 9
g(n) 1 2 5 12 28 64 144 320 704
(
1 when n is 1
h(n) =
h(n − 1) + h(n − 3) + h(n − 5)... + h(2) + 1 when n > 1 and n is odd
(12.15)
If n is even, n − 1 is odd so h(1) is included. Also n − 1, n − 3, ..., are all odd numbers.
Therefore the complete equation is shown below:
178 Intermediate C Programming
1
when n is 1
h(n) = h(n − 1) + h(n − 3) + h(n − 5)... + h(2) + 1 when n > 1 and n is odd
h(n − 1) + h(n − 3) + h(n − 5)... + h(1) when n is even
(12.16)
n = a1 + a2 + a3 + ... + ak (12.17)
The following conditions must be true:
• ai (1 ≤ i ≤ k) are positive integers
• ai < ai+1 (1 ≤ i < k)
Consider the first few cases of n:
• When n is 1, 1 is a valid partition.
• When n is 2, 2 is a valid partition but 1 + 1 is invalid.
• When n is 3, 1 + 2 and 3 are two valid partitions; 1 + 1 + 1, and 2 + 1 are invalid
partitions.
• When n is 4, 1 + 3 is a valid partition; 2 + 2 and 3 + 1 are invalid partitions.
• When n is 5, 1 + 4, 2 + 3 are valid partitions; 2 + 2 + 1, 3 + 2, 4 +1 are invalid
partitions.
To solve this problem, two arguments are needed for the equation. We define p(n, m)
to be the number of ways to partition n where m is the smallest number used. When
partitioning n, note the following:
• If 1 is used as the first number, then 2 is the smallest number that can be used when
partitioning n − 1. There are p(n − 1, 2) ways to partition n − 1 using 2 as the smallest
number.
• If 2 is used as the first number, then 3 is the smallest number that can be used to
partition n − 2. There are p(n − 2, 3) ways to partition n − 2 using 3 as the smallest
number.
• If 3 is used as the first number, then 4 is the smallest number that can be used to
partition n − 3. There are p(n − 3, 4) ways to partition n − 3 using 4 as the smallest
number.
Based on this reasoning,
By inspection we can tell that p(n, n) = 1. This means that there is one and only one
way to partition n using n as the smallest number. Also, p(n, m) = 0 if n < m because it
is impossible to partition an integer using a larger integer. This problem is different from
the previous ones because the recursive equations require two arguments. The fundamental
recursive reasoning is the same.
Recursion 179
1 = 1 2 = 2 3 = 1 + 2 4 = 1 + 2 + 1
= 2 + 1 = 4
= 3
5 = 1 + 4 6 = 1 + 2 + 1 + 2 7 = 1 + 2 + 1 + 2 + 1
= 2 + 1 + 2 = 1 + 2 + 3 = 1 + 6
= 2 + 3 = 1 + 4 + 1 = 2 + 1 + 4
= 3 + 2 = 2 + 1 + 2 + 1 = 2 + 3 + 2
= 4 + 1 = 3 + 2 + 1 = 2 + 5
= 5 = 6 = 3 + 4
= 4 + 1 + 2
= 4 + 3
= 5 + 2
= 6 + 1
= 7
The following table shows the solutions for n between 1 and 10.
n 1 2 3 4 5 6 7 8 9 10
number of partitions 1 1 3 2 6 6 11 16 22 37
This problem using alternating odd and even numbers can be solved by defining two
functions as follows:
• s(n) is the number of ways to partition n using an odd number as the first number.
• t(n) is the number of ways to partition n using an even number as the first number.
By observation we can create the following table:
n 1 2 3 4 5
s(n) 1 0 2 1 3
t(n) 0 1 1 1 3
sum 1 1 3 2 6
To calculate s(n), the first number can be 1, 3, 5, ... and the second number must be an
even number. For example, when 1 is used for the first number, then the remaining n − 1
must start with an even number. By definition, there are t(n − 1) ways to partition n − 1
starting with an even number. When 3 is used for the first number, then there are t(n − 3)
ways to partition n − 3 starting with an even number. Based on this reasoning, s(n) is
defined as:
• n − 3 is an odd number. This means that there are t(n − (n − 3)) = t(3) ways to
partition n with n − 3 as the first number. For example, if n = 10, there are t(3) ways
to partition 10 with 7 as the first number. Note that t(3) = 1, because the only valid
partition of 3 that starts with an even number is: 3 = 2 + 1.
• n − 2 is an even number. We skip this case because s(n) is only concerned with the
number of ways to partition n using an odd number as the first number.
• n − 1 is an odd number, so t(n − (n − 1)) = t(1) is included in the calculation of s(n).
Note, however, that t(1) = 0.
• n is an even number. We skip this case because s(n) only concerns itself with partitions
that begin with odd numbers.
Hence, when n is an even number:
(
t(n − 1) + t(n − 3) + t(n − 5)... + t(1) when n is even
s(n) = (12.22)
t(n − 1) + t(n − 3) + t(n − 5)... + t(2) + 1 when n is odd
(
s(n − 2) + s(n − 4) + s(n − 6)... + s(4) + s(2) + 1 when n is even
t(n) = (12.23)
s(n − 2) + s(n − 4) + s(n − 6)... + s(3) + s(1) when n is odd
Since a partition may start with an odd number or an even number, f (n) = s(n) + t(n)
and it is the answer to the question. This is the number of ways to partition n using alter-
nating odd and even numbers. Section 12.4.2 explains how to find the number of partitions
using odd numbers only. The answer is expressed as h(n). A similar procedure can be used
to find the number of partitions using even numbers only. Let’s call it u(n). Of course, u(n)
is zero if n is odd.
Is h(n) + u(n) the same as s(n) + t(n)? Why? I leave this question for you to answer. If
the answer is yes, prove it. If the answer is no, explain the reason.
In this chapter we convert the mathematical formulas from the previous chapter into C
programs. Recall that there are four steps to solving math problems that use recursion.
These steps are also used when writing C functions that use recursion.
1. Identify the argument (or arguments) of a problem. These will be, in general, the
argument (or arguments) for the recursive C function.
2. Express the solution based on the arguments.
3. Determine the simple cases when the solutions are “obvious”. The function has one
(or several) conditions detecting whether this (or these) simple case(s) can be solved
directly. This is usually referred to as the base case.
4. Derive the relationships between complex cases and simpler cases. We call this the
recursive case because the function calls itself with a simplified argument (or several
modified arguments).
A recursive function has the following structure:
1 return_type func ( arguments )
2 {
3 i f ( this is the base case ) /* by checking the arguments */
4 {
5 solve the problem
6 }
7 e l s e /* Recursive case */
8 {
9 func ( simplified arguments ) /* function calls itself */
10 }
11 }
A recursive function should first check whether the arguments specify a base case. A
base case means that the function can immediately return the answer. For this reason,
the if condition (or conditions) is called the terminating condition (or conditions) of the
recursive function. The terminating conditions indicate that a base case has been reached.
When the condition is true, the problem is trivial and recursive calls are unnecessary. If the
problem is not simple, then the function enters the recursive case and the function calls itself
with simplified versions of the arguments. The following sections implement the recursive
equations in the previous chapter.
183
184 Intermediate C Programming
n f (n)
1 2
2 3
3 5
4 8
5 13
6 21
Understanding how recursive functions work requires a full understanding of the call
stack. If you are unsure about how the call stack works, then please review Chapter 2. Let’s
see what happens when n (in main) is 3, just after finishing line 41, and before running line
42. For simplicity, the call stack does not show argc and argv.
Frame Symbol Address Value
n 101 3
main
c 100 garbage
Since m is greater than 2, the program will call f (m − 1) and assign the result in a.
Recursive calls follow the same procedure as any other function call. As the function is
called, the return location, value address, arguments, and local variables are pushed on to
the call stack.
Frame Symbol Address Value
b 111 garbage
a 110 garbage
f m 109 2
value address 108 105 (a’s address)
return location 107 line 30
b 106 garbage
a 105 garbage
f m 104 3
v value address 103 100 (c’s address)
return location 102 line 45
n 101 3
main
c 100 garbage
The value of m is 2 now and it is a base case. The function returns 3 without calling
itself again. The value 3 is written to address 105.
186 Intermediate C Programming
The value of m is 1 and it meets one terminating condition. The function returns 2, and
this value is written to address 106.
Frame Symbol Address Value
b 106 garbage → 2
a 105 3
f m 104 3
value address 103 100 (c’s address)
return location 102 line 45
n 101 3
main
c 100 garbage
The program has returned to a previous invocation of f. The sum of a and b is 5. This
value is written to the address of 100.
Frame Symbol Address Value
n 101 3
main
c 100 garbage → 5
A recursive function follows the same rules as any other function call. When a function
is called, a new frame is pushed. When a function finishes, the top frame is popped. A
common misconception is that the frames of a recursive function are merged into a single
frame. This is wrong. The call stack handles recursive calls in the same way as non-recursive
calls.
Recursive C Functions 187
18 return f ( dx - 1 , dy ) + f ( dx , dy - 1) ;
Both of these versions do exactly the same thing. The compiler actually creates temporary
variables if local variables are not used.
1 // hanoi2 . c
2 // print the steps moving n disks
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 void move ( i n t disk , char src , char dest , char additional )
6 {
7 /* Base case */
8 i f ( disk == 1)
9 {
10 printf ( " move disk 1 from % c to % c \ n " , src , dest ) ;
11 return ;
12 }
13 /* Recursive case */
14 move ( disk - 1 , src , additional , dest ) ;
15 printf ( " move disk % d from % c to % c \ n " , disk , src , dest ) ;
16 move ( disk - 1 , additional , dest , src ) ;
17 }
18 i n t main ( i n t argc , char * argv [])
19 {
20 int n;
21 i f ( argc < 2)
22 {
23 printf ( " need one positive integer .\ n " ) ;
24 return EXIT_FAILURE ;
25 }
26 n = ( i n t ) strtol ( argv [1] , NULL , 10) ;
27 i f ( n <= 0)
28 {
29 printf ( " need one positive integer .\ n " ) ;
30 return EXIT_FAILURE ;
31 }
32 move (n , ’A ’ , ’B ’ , ’C ’) ;
33 return EXIT_SUCCESS ;
34 }
When the input is 3, the program’s output is:
36 }
37 printf ( " f (% d ) = % d .\ n " , n , f ( n ) ) ;
38 return EXIT_SUCCESS ;
39 }
This program was executed with various arguments to produce the following result:
n f (n)
1 1
2 2
3 4
4 8
5 16
6 32
13.5 Factorial
Many books use factorial and Fibonacci numbers to motivate the need of recursion.
These are poor examples. This book does not start with these two popular examples for
good reasons, as explained below. The definition of factorial for positive integers or zero is:
(
1 when n is 0
f (n) = (13.1)
n × f (n − 1) when n > 0
It is possible to define factorial for negative values or non integers; however, that definition
is beyond the scope of this book.
1 // factorial1 . c
2 #i n c l u d e < stdio .h >
3 long i n t fac ( i n t n )
4 {
5 i f ( n < 0)
6 {
7 printf ( " n cannot be negative \ n " ) ;
8 return 0;
9 }
10 /* Base case */
11 i f ( n == 0)
12 {
13 return 1;
14 }
15 /* Recursive case */
16 return n * fac ( n - 1) ;
17 }
By now, the main function should be quite easy to follow:
1 // mainfactorial . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
192 Intermediate C Programming
4 #d e f i n e MAXN 20
5 long i n t fac ( i n t n ) ;
6 i n t main ( i n t argc , char * argv [])
7 {
8 i n t nval ;
9 f o r ( nval = 0; nval <= MAXN ; nval ++)
10 {
11 long i n t fval = fac ( nval ) ;
12 printf ( " fac (%2 d ) = % ld \ n " , nval , fval ) ;
13 }
14 return EXIT_SUCCESS ;
15 }
Here is the output of this program:
fac( 0) = 1
fac( 1) = 1
fac( 2) = 2
fac( 3) = 6
fac( 4) = 24
fac( 5) = 120
fac( 6) = 720
fac( 7) = 5040
fac( 8) = 40320
fac( 9) = 362880
fac(10) = 3628800
fac(11) = 39916800
fac(12) = 479001600
fac(13) = 6227020800
fac(14) = 87178291200
fac(15) = 1307674368000
fac(16) = 20922789888000
fac(17) = 355687428096000
fac(18) = 6402373705728000
fac(19) = 121645100408832000
fac(20) = 2432902008176640000
The function fac returns long int because the values quickly get too large for int when
n is greater than 12. The function fac is quite straightforward—a direct translation of the
mathematical definition. Why is this a bad example for introducing recursion? The reason
is that recursion is not necessary. It is possible to implement the same function without
using recursion.
1 // factorial2 . c
2 #i n c l u d e < stdio .h >
3 long i n t fac2 ( i n t n )
4 {
5 i f ( n < 0)
6 {
7 printf ( " n cannot be negative \ n " ) ;
8 return 0;
9 }
10 i f ( n == 0)
Recursive C Functions 193
11 {
12 return 1;
13 }
14 long i n t result = 1;
15 while ( n > 0)
16 {
17 result *= n ;
18 n - -;
19 }
20 return result ;
21 }
This function uses while and stores the result in a local variable called result.
“All right, you can use recursion but you don’t have to. Why do you say that factorial
is a bad example?” Recursion can be more flexible than iterative loops, such as while and
for. Every loop can be expressed easily with recursion. However, the reverse can be diffi-
cult. Some problems can be solved naturally by recursion; solving these problems without
recursion can sometimes be awkward. Why is this a bad example? The recursive solution
is actually worse than the iterative solution. This brings us to the second problem: The
recursive solution is slower. Why? Recursive functions must push and pop frames on the
call stack. Pushing and popping frames takes time.
We compared the execution times of both functions. The iterative solution (using while)
is 14% to 38% faster than the recursive solution. This performance difference may not seem a
lot; however, in the next example (Fibonacci numbers), we will see a remarkable performance
difference.
n f (n)
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34
10 55
up from smallest to n, just as if you were doing the calculation by hand. Please spend some
time to understand it. Fig. 13.1 illustrates the steps.
Why do we even bother to consider the bottom-up function? Isn’t the recursive function
good enough? It certainly looks simple since it is direct translation from the mathematical
definition. The problem is that the recursive function does a lot of unnecessary work, and
is hence rather slow. Fig. 13.2 shows the ratio of the execution time for the first (recursive,
top-down) and the second (non-recursive, bottom-up) functions. It is readily apparent that
the first function is slower (takes longer) than the second. Moreover, the ratio keeps rising.
Please notice that the vertical axis is in the logarithmic scale. The first function takes as
much as 2,000 times longer than the second when n is 20.
The data in Fig. 13.2 were generated by using the following program:
1 // fib . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < sys / time .h >
5 #d e f i n e MAXN 20
6 #d e f i n e REPEAT 100000
7 long i n t fib ( i n t n ) ;
8 long i n t fib2 ( i n t n ) ;
9 i n t main ( i n t argc , char * argv [])
10 {
11 i n t nval , rept ;
12 s t r u c t timeval time1 ;
13 s t r u c t timeval time2 ;
14 f l o a t intv1 , intv2 ;
15 f o r ( nval = 1; nval <= MAXN ; nval ++)
16 {
196 Intermediate C Programming
FIGURE 13.2: Ratio of the execution times of the recursive and the non-recursive versions
for calculating Fibonacci numbers. The recursive function is much slower and the ratio in
execution time keeps rising.
17 long i n t fval ;
18 gettimeofday (& time1 , NULL ) ;
19 f o r ( rept = 0; rept < REPEAT ; rept ++)
20 {
21 fval = fib ( nval ) ;
22 }
23 gettimeofday (& time2 , NULL ) ;
24 intv1 = ( time2 . tv_sec - time1 . tv_sec ) +
25 1e -6 * ( time2 . tv_usec - time1 . tv_usec ) ;
26 printf ( " fib (%2 d ) = % ld , time = % f \ n " ,
27 nval , fval , intv1 ) ;
28 gettimeofday (& time1 , NULL ) ;
29 f o r ( rept = 0; rept < REPEAT ; rept ++)
30 {
31 fval = fib2 ( nval ) ;
32 }
33 gettimeofday (& time2 , NULL ) ;
34 intv2 = ( time2 . tv_sec - time1 . tv_sec ) +
35 1e -6 * ( time2 . tv_usec - time1 . tv_usec ) ;
36 printf ( " fib2 (%2 d ) = % ld , time = % f \ n " ,
37 nval , fval , intv2 ) ;
38 printf ( " ratio = % f \ n " , intv1 / intv2 ) ;
39 }
40 return EXIT_SUCCESS ;
41 }
This program uses gettimeofday to measure the execution time of the two functions.
gettimeofday returns the time, expressed in seconds and microseconds, since 1970-01-01
00:00:00 (UTC). The two values are stored in a structure called struct in C programs.
Structures will be discussed in great detail later in this book. This program measures the
Recursive C Functions 197
difference of time before and after calculating Fibonacci numbers. The basic structure of
measuring a function’s execution time is
1. get the current time, call it t1;
2. call the function;
3. get the current time, call it t2.
The execution time of this function is t2 − t1. This method of measuring execution time
has limitations. The time t1 and t2 has finite precision. If this function’s execution time
is too short, t2 − t1 will be too small (possibly zero). To obtain acceptable accuracy, the
execution time needs to be much longer than the precision. For gettimeofday, the precision
is microseconds; thus, the execution time should be much longer than one microsecond. This
is the reason why the program calls the functions calculating Fibonacci numbers multiple
times between calling gettimeofday. The values may be slightly different when the program
is run multiple times because your computer also runs many other programs. This is the
last few lines of the program’s output:
fib (16) = 987 , time = 2.161532
fib2 (16) = 987 , time = 0.004730
ratio = 456.983490
fib (17) = 1597 , time = 3.500462
fib2 (17) = 1597 , time = 0.005093
ratio = 687.308472
fib (18) = 2584 , time = 5.678572
fib2 (18) = 2584 , time = 0.005474
ratio = 1037.371704
fib (19) = 4181 , time = 9.399386
fib2 (19) = 4181 , time = 0.005677
ratio = 1655.696045
fib (20) = 6765 , time = 15.530513
fib2 (20) = 6765 , time = 0.007862
ratio = 1975.389648
As you can see, the ratios of the execution time grow from 457 to 1975.
f(5) = f(4) + f(3) f(5) = f(4) + f(3) f(5) = f(4) + f(3) f(5) = f(4) + f(3)
f(3) + f(2) f(2) + f(1) f(3) + f(2) f(2) + f(1) f(3) + f(2) f(2) + f(1)
1 1
(a) (b) (c) (d)
FIGURE 13.3: Computing f (5) requires calling f (4) and f (3). Computing f (4) requires
calling f (3) and f (2).
Why is there such a large difference in the execution time? Fig. 13.3 illustrates the
sequence of computation. For the first function, to compute f (5), it is necessary to compute
f (4) and f (3). To compute f (4), it is necessary to compute f (3) and f (2). Fig. 13.4 redraws
Fig. 13.3. This looks like a “tree”. You need to use some imagination because the tree’s
“root” is at the top and the branches go downwards. Computing each value requires the
sum of two values, until reaching the bottom, called leaves. The leaves are the base cases,
where the recursion meets the terminating conditions, namely f (1) or f (2).
Let’s do a little more mathematics here to figure out some properties of this tree. By
definition,
198 Intermediate C Programming
f(5)
+
f(4) f(3)
+ +
f(2) f(1)
1 1
FIGURE 13.4: Redraw Fig. 13.3. This looks like a “tree”: computing each value requires
the sum of two values.
(
f (n) = f (n − 1) + f (n − 2)
(13.4)
f (n − 1) = f (n − 2) + f (n − 3)
Therefore,
f (n) = f (n − 1) + f (n − 2)
= (f (n − 2) + f (n − 3)) + f (n − 2) (13.5)
= 2f (n − 2) + f (n − 3).
Continuing this derivation for a few more steps we have:
f (n) = f (n − 1) + f (n − 2)
= 2f (n − 2) + f (n − 3)
= 2(f (n − 3) + f (n − 4)) + f (n − 3)
= 3f (n − 3) + 2f (n − 4)
= 3(f (n − 4) + f (n − 5)) + 2f (n − 4)
(13.6)
= 5f (n − 4) + 3f (n − 5)
= 5(f (n − 5) + f (n − 6)) + 3f (n − 5)
= 8f (n − 5) + 5f (n − 6)
= 8(f (n − 6) + f (n − 7)) + 5f (n − 6)
= 13f (n − 6) + 8f (n − 7).
Table 13.2 lists the coefficients for computing f (n). When comparing this table with
the values in Table 13.1, you may find that the coefficient of f (n − k) is actually f (k + 1).
Table 13.2 and Fig. 13.4 both express similar concepts. Fig. 13.4 computes f (5) so n is 5
and f (2) is f (n − 3). The coefficient for f (n − 3) is 3. If we count the occurrences of f (2)
in Fig. 13.4, we find that it is called three times.
The recursive method for calculating Fibonacci numbers is slower because it computes
the same value over and over again. When computing f (n − 1), it has to compute f (n − 2).
However, the recursive function in Section 13.6 does not remember the value for f (n − 2)
and then computes the value again later. As n becomes larger, the function performs more
and more redundant computations and becomes slower and slower. It is unclear why many
books use Fibonacci numbers to motivate the concept of recursion.
Recursive C Functions 199
function coefficient
f(n-1) 1 f (2)
f(n-2) 2 f (3)
f(n-3) 3 f (4)
f(n-4) 5 f (5)
f(n-5) 8 f (6)
f(n-6) 13 f (7)
Does this mean recursion is bad? Does this mean that recursion is slow? Why should we
even bother to learn recursion? The problem is not recursion, but the example. We cannot
generalize from a bad example to say that recursion is slow. Recursion can be used to good
advantage in some time-critical applications. If you have a nail and a knife, you will find
that hitting the nail with a knife is difficult. Does this mean a knife is bad? If you need to
hit a nail, you need a hammer. A knife is a bad tool for hitting a nail. If you want to cut
paper, a knife is better than a hammer. If you have a bolt, you need a wrench. A hammer
is not better than a wrench and a wrench is not better than a hammer. They are different.
One is better than the other in some scenarios. If a book tells you a hammer is better
than a wrench, you will say, “This doesn’t make sense. In some cases, a wrench is better.”
You cannot generalize this example and conclude that recursion is slower. Some books do
not explain that this particular top-down method is slower than this bottom-up method.
As a result some students mistakenly think recursion is slow by generalizing this example.
Some other books explain that this top-down method is slower than this bottom-up method
without further explanation. These books give students the strong impression that recursion
is slow. The truth is that recursion is a good approach for some problems, not all.
Consider the Fibonacci functions referenced earlier. After running the program, a special
file called gmon.out is generated. This file stores the profiling information for running the
program and it is not readable using a text editor. Please use the gprof program to view
the content in gmon.out. The command is:
200 Intermediate C Programming
$ gprof fibprog
The output is something like this:
Flat profile :
The previous two chapters explained how to obtain the formula for partitioning integers
and also how to write a C program that implements the formula. This chapter explains how
to print the partitions and also introduces some variations on the problem. The following
program prints the partitions for an integer that is specified on the command line:
1 // partition . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 void printPartition ( i n t * arr , i n t length )
6 {
7 i n t ind ;
8 f o r ( ind = 0; ind < length - 1; ind ++)
9 {
10 printf ( " % d + " , arr [ ind ]) ;
11 }
12 printf ( " % d \ n " , arr [ length - 1]) ;
13 }
14
201
202 Intermediate C Programming
This is the call stack and the heap memory after entering the function partition. RL
means return location.
Frame Symbol Address Value
val 106 garbage
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
The function then calls itself at line 26. Please notice the values of ind and left. The
index has changed from 0 to 1 because line 26 uses ind + 1. This is the next position in arr
for the next value. In the recursive call, the function needs to partition 3 because left - val
is 3. That is, we wrote 1 to position 0, and now we want to partition n − 1 = 4 − 1 = 3 into
the remaining portion of arr. Please pay attention to the top frame of the call stack.
204 Intermediate C Programming
The for loop starts with val equal to 1. Line 25 assigns 1 to arr[1] because ind is 1.
Frame Symbol Address Value
val 111 1
left 110 3
partition ind 109 1
arr 108 10000
RL 107 line 27
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
Now the value of left is 0 and the terminating condition at line 18 is true. This means
that we have reached a base case, and line 20 calls printPartition and prints the 4
elements in the array. Four elements are printed because ind is 4. Remember, ind gives
the next position to write an element into arr, and it also gives the length of the partition
so far. If you think about it carefully, you will see that these two things are equivalent.
Therefore, we can pass ind as the length of the array to printPartition. The program
now prints:
1 + 1 + 1 + 1
The function then returns because of line 21. The return statement at line 21 is not strictly
necessary, because the function will not call itself again. This is because the for loop starts
at 1 and val must be smaller than or equal to left. Because left is zero the function will
not enter the for loop, and will return normally anyway.
It is good practice to put the line 21 return statement. When people read recursive
functions they expect the functions to be divided neatly into the base case and the recursive
case. The return statement makes the base case clear. Clearly no recursion is going to
happen. No further analysis is required. Making code as clear as possible is one of the most
important parts of good programs. After meeting the terminating condition, the top frame
of the call stack is popped, and the program continues at line 27.
Frame Symbol Address Value
val 121 1
left 120 1
partition ind 119 3
arr 118 10000
RL 117 line 27
val 116 1
left 115 2
partition ind 114 2
arr 113 10000
RL 112 line 27
Integer Partition 207
val 111 1
left 110 3
partition ind 109 1
arr 108 10000
RL 107 line 27
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
For the next iteration, val increments to two. This violates the condition val <= left
and the for loop exists. Since the function has nothing else to do after the for loop, the
top frame is popped. The program now continues at line 27.
Frame Symbol Address Value
val 116 1
left 115 2
partition ind 114 2
arr 113 10000
RL 112 line 27
val 111 1
left 110 3
partition ind 109 1
arr 108 10000
RL 107 line 27
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
Now the for loop enters the next iteration: val becomes 2 and the condition val <=
left is satisfied. Line 25 assigns 2 to arr[2] and line 26 calls the function itself again.
Frame Symbol Address Value
val 121 garbage
left 120 0
partition ind 119 3
arr 118 10000
RL 117 line 27
val 116 2
left 115 2
partition ind 114 2
arr 113 10000
RL 112 line 27
val 111 1
left 110 3
partition ind 109 1
arr 108 10000
RL 107 line 27
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
Because left is zero, the terminating condition at line 18 is true. The program prints
the first 3 elements (because ind is 3) in arr. So the program prints:
1 + 1 + 2
The next iteration increments val to 3 but the condition val <= left is not satisfied.
The function exits the for loop. Since the function has nothing else to do after the for
loop, the function returns and the top frame is popped.
Frame Symbol Address Value
val 111 1
left 110 3
partition ind 109 1
arr 108 10000
RL 107 line 27
val 106 1
left 105 4
partition ind 104 0
arr 103 10000
RL 102 line 44
arr 101 10000
main
n 100 4
This process may seem tedious. Fortunately, computers are good at tedious work. Please
practice a few times and ensure that you fully understand the changes in the call stack and
heap memory. Then, leave the details to computers.
1 void f1 ()
2 {
3 f2 () ;
4 f3 () ;
5 }
This is the third example and the calling relation is shown in Fig. 14.3.
1 void f1 ()
2 {
3 f2 () ;
4 f3 () ;
5 }
6 void f2 ()
7 {
8 f3 () ;
9 }
FIGURE 14.3: Graphical illustration of f1 calls f2 and f3; f2 also calls f3.
Here we add a loop to the function f1 and Fig. 14.4 shows the relation.
1 void f1 ()
2 {
3 i n t count ;
4 f o r ( count = 1; count < 4; count ++)
5 {
6 f2 () ;
7 }
8 f3 () ;
212 Intermediate C Programming
9 }
10 void f2 ()
11 {
12 f3 () ;
13 }
FIGURE 14.5: Graphical illustration of partition when the initial value of left is 3.
7 {
8 i f (( arr [ ind ] % 2) == 0)
9 {
10 return ;
11 }
12 }
13 f o r ( ind = 0; ind < length - 1; ind ++)
14 {
15 printf ( " % d + " , arr [ ind ]) ;
16 }
17 printf ( " % d \ n " , arr [ length - 1]) ;
18 }
To check whether the numbers form an increasing sequence:
1 void printPartition ( i n t * arr , i n t length )
2 {
3 i n t ind ;
4 f o r ( ind = 0; ind < length - 1; ind ++)
5 {
6 i f ( arr [ ind ] >= arr [ ind + 1]) // not increasing
7 {
8 return ;
9 }
10 }
11 f o r ( ind = 0; ind < length - 1; ind ++)
12 {
13 printf ( " % d + " , arr [ ind ]) ;
14 }
15 printf ( " % d \ n " , arr [ length - 1]) ;
16 }
However, checking before printing is inefficient because many invalid partitions have al-
ready been generated. Instead, a more efficient solution does not generate invalid partitions.
This section explains how to generate valid partitions satisfying one of the following restric-
tions: (i) using odd numbers only, (ii) using increasing numbers, and (iii) using alternating
odd and even numbers.
10 {
11 arr [ ind ] = val ;
12 partition ( arr , ind + 1 , left - val ) ;
13 }
14 }
This will generate fewer partitions and all of them are valid.
11 i n t valid = 0;
12 i f ( ind == 0) // no restriction for the first number
13 {
14 valid = 1;
15 }
16 else
17 {
18 valid = ( arr [ ind - 1] % 2) != ( val % 2) ;
19 }
20 i f ( valid == 1)
21 {
22 arr [ ind ] = val ;
23 partition ( arr , ind + 1 , left - val ) ;
24 }
25 }
26 }
Line 18 tests whether arr[ind - 1] and val are both even or both odd. As you can see,
with only a few small changes, the program can print the solutions for the integer partition
problem under different restrictions.
21 {
22 i n t val ;
23 i f ( left == 0)
24 {
25 printPartition ( arr , ind ) ;
26 return ;
27 }
28 f o r ( val = 1; val <= left ; val ++)
29 {
30 i n t valid = 0;
31 i f ( ind == 0) // no restriction for the first number
32 {
33 valid = 1;
34 }
35 else
36 {
37 valid = ( arr [ ind - 1] % 2) != ( val % 2) ;
38 }
39 i f ( valid == 1)
40 {
41 arr [ ind ] = val ;
42 partition1 ( arr , ind + 1 , left - val ) ;
43 }
44 }
45 }
46 // 2. before printing , check whether the partition is valid
47 // check whether the numbers are alternating odd and even
48 // return 1 if valid
49 // return 0 if invalid
50 i n t isValid ( i n t * arr , i n t len )
51 {
52 i f ( len <= 1) // if only one number , it is valid
53 {
54 return 1;
55 }
56 i n t ind ;
57 f o r ( ind = 2; ind < len ; ind += 2)
58 {
59
72 return 0;
73 }
74 }
75 return 1;
76 }
77 // generate all possible partitions , including invalid
78 // check before printing
79 void partition2 ( i n t * arr , i n t ind , i n t left )
80 {
81 i n t val ;
82 i f ( left == 0)
83 {
84 i f ( isValid ( arr , ind ) == 1)
85 {
86 printPartition ( arr , ind ) ;
87 }
88 return ;
89 }
90 f o r ( val = 1; val <= left ; val ++)
91 {
92 arr [ ind ] = val ;
93 partition2 ( arr , ind + 1 , left - val ) ;
94 }
95 }
96
This report shows that nearly 99% of the program’s execution time is taken by
partition2 (55.7%) and isValid (43.3%). Since isValid is called only by partition2, the
report shows that partition2 takes 98.5% of the time. In contrast, partition1 takes only
1.5% of the time. Why is there such a large difference? The reason is that partition2 gen-
erates many invalid partitions and then uses isValid to check before printing. In contrast,
partition1 generates only valid partitions. The latter approach reduces huge portions of
the call tree, and is thus much more efficient.
Please notice that [4] shows printPartition is called by partition1 101393 times
and by partition2 101393 times. This is expected since partition1 and partition2
should print exactly the same partitions. The function partition2 is called 1073741823
times by recursion but partition1 is called only 308288 times. The report also shows that
isValid is called 536870912 times but printPartition is called only 202786 times (by
both partition1 and partition2). This means that most of the generated partitions are
invalid and are not printed. This is another indication that partition2 generates many
invalid partitions. Notice that 55.74%, 43.28%, and 1.50% sum to 100.52%. How can it be
possible that the cumulative time is above 100%? This shows a limitation of gprof: This
tool is intended to be fast but the accuracy is not so great. Profiling code always interferes
with the normal execution of the code, and there must be a trade-off somewhere.
When we want to improve a program’s performance, then we first need to identify
the functions that take the most amount of time. We can get some help by using gprof.
220 Intermediate C Programming
After finding the functions, we need to think about whether these functions are doing
any unnecessary work. Eliminating unnecessary computation should be the first step in
improving performance. Note that this is also the principle behind qsort: It uses transitivity
to eliminate unnecessary comparisons.
We can also use gcov to find the opportunities for improving performance. This is
primarily used to examine test coverage: It helps us understand the quality of the tests.
Section 5.5 explains how to use gcov to examine test coverage. Here we show how to use
gcov to look for performance bottlenecks. To run gcov, we must add -fprofile-arcs
-ftest-coverage after gcc. The program can be executed normally. When the program
runs, two files are generated: One has .gcda extension and the other has .gcno extension.
The next step is to run the gcov command. It will generate another file whose extension
is .gcov. This is the file that we want to examine. Here are the contents of the file from a
sample run (suppose the value of n is 30).
-: 0: Source : gprofeg . c
-: 0: Graph : gprofeg . gcno
-: 0: Data : gprofeg . gcda
-: 0: Runs :1
-: 0: Programs :1
-: 1: // partition using alternating odd and even
numbers
-: 2: // two ways to implement the partition :
-: 3: // 1. check before recursive calls
-: 4: // 2. generate all partitions and check
before printing
-: 5:# include < stdio .h >
-: 6:# include < stdlib .h >
-: 7:
202786: 8: void printPartition ( i n t * arr , i n t length )
-: 9:{
-: 10: /*
-: 11: int ind ;
-: 12: for ( ind = 0; ind < length - 1; ind ++)
-: 13: {
-: 14: printf ("% d + " , arr [ ind ]) ;
-: 15: }
-: 16: printf ("% d \ n " , arr [ length - 1]) ;
-: 17: */
202786: 18:}
-: 19:
-: 20: // 1. do not generate invalid partial
partitions
308289: 21: void partition1 ( i n t * arr , i n t ind , i n t left )
-: 22:{
-: 23: i n t val ;
308289: 24: i f ( left == 0)
-: 25: {
101393: 26: printPartition ( arr , ind ) ;
409682: 27: return ;
-: 28: }
835786: 29: f o r ( val = 1; val <= left ; val ++)
Integer Partition 221
-: 30: {
628890: 31: i n t valid = 0;
628890: 32: i f ( ind == 0) // no restriction for the
first number
-: 33: {
30: 34: valid = 1;
-: 35: }
-: 36: else
-: 37: {
628860: 38: valid = ( arr [ ind - 1] % 2) != ( val
% 2) ;
-: 39: }
628890: 40: i f ( valid == 1)
-: 41: {
308288: 42: arr [ ind ] = val ;
308288: 43: partition1 ( arr , ind + 1 , left -
val ) ;
-: 44: }
-: 45: }
-: 46:}
-: 47:
-: 48: // 2. before printing , check whether the
partition is valid
-: 49: // check whether the numbers are alternating
odd and even
-: 50: // return 1 if valid
-: 51: // return 0 if invalid
536870912: 52: i n t isValid ( i n t * arr , i n t len )
-: 53:{
536870912: 54: i f ( len <= 1) // if only one number , it is
valid
-: 55: {
1: 56: return 1;
-: 57: }
-: 58: i n t ind ;
1304963113: 59: f o r ( ind = 2; ind < len ; ind += 2)
-: 60: {
-: 61:
-: 62: // invalid if they are different
1283973260: 63: i f (( arr [ ind ] % 2) != ( arr [0] % 2) )
-: 64: {
515881058: 65: return 0;
-: 66: }
-: 67: }
33079750: 68: f o r ( ind = 1; ind < len ; ind += 2)
-: 69: {
-: 70:
-: 71: // invalid if they are the same
32978358: 72: i f (( arr [ ind ] % 2) == ( arr [0] % 2) )
-: 73: {
20888461: 74: return 0;
222 Intermediate C Programming
-: 75: }
-: 76: }
101392: 77: return 1;
-: 78:}
-: 79:
1073741824: 80: void partition2 ( i n t * arr , i n t ind , i n t left )
-: 81:{
-: 82: i n t val ;
1073741824: 83: i f ( left == 0)
-: 84: {
536870912: 85: i f ( isValid ( arr , ind ) == 1)
-: 86: {
101393: 87: printPartition ( arr , ind ) ;
-: 88: }
1610612736: 89: return ;
-: 90: }
1610612735: 91: f o r ( val = 1; val <= left ; val ++)
-: 92: {
1073741823: 93: arr [ ind ] = val ;
1073741823: 94: partition2 ( arr , ind + 1 , left - val ) ;
-: 95: }
-: 96:}
-: 97:
1: 98: i n t main ( i n t argc , char * argv [])
-: 99:{
1: 100: i f ( argc != 2)
-: 101: {
#####: 102: return EXIT_FAILURE ;
-: 103: }
1: 104: i n t n = ( i n t ) strtol ( argv [1] , NULL , 10) ;
1: 105: i f ( n <= 0)
-: 106: {
#####: 107: return EXIT_FAILURE ;
-: 108: }
-: 109: i n t * arr ;
1: 110: arr = malloc ( s i z e o f ( i n t ) * n ) ;
1: 111: printf ( " ----- Partition 1 - - - - -\ n " ) ;
1: 112: partition1 ( arr , 0 , n ) ;
1: 113: printf ( " ----- Partition 2 - - - - -\ n " ) ;
1: 114: partition2 ( arr , 0 , n ) ;
1: 115: free ( arr ) ;
1: 116: return EXIT_SUCCESS ;
-: 117:}
Please pay special attention to lines 85 and 87. Line 85 says isValid is called 536870912
times but it is true only 101393 times. In other words, most generated partitions are invalid.
This is another way to obtain the same information: partition2 generates many invalid
partitions.
Chapter 15
Programming Problems Using Recursion
This chapter describes several problems that can be solved using recursion.
223
224 Intermediate C Programming
• If the key is smaller than the center element, then the function discards the second
half (the elements with larger values) of the array, and considers the lower half.
These steps continue until either the index is found or it is impossible to find a match.
Fig. 15.1 is a graphical view of the steps:
FIGURE 15.1: In each step, the binary search reduces the number of elements to search
across by half. In the first step, key is compared with the element at the center. If key is
smaller, then it is impossible to find key in the upper half of the array. If key is greater
than the element at the center, then it is impossible to find key in the lower half of the
array. The array must have been sorted before performing a binary search.
1 // binarysearch . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < time .h >
5 #i n c l u d e < string .h >
6 #d e f i n e RANGE 100
7 i n t * arrGen ( i n t size ) ;
8 // generate a sorted array of integers
9 s t a t i c i n t binarySearchHelp ( i n t * arr , i n t low ,
10 i n t high , i n t key )
11 {
12 i f ( low > high )
13 {
14 return -1;
15 }
16 i n t ind = ( low + high ) / 2;
17 i f ( arr [ ind ] == key )
18 {
19 return ind ;
20 }
21 i f ( arr [ ind ] > key )
22 {
23 return binarySearchHelp ( arr , low , ind - 1 , key ) ;
24 }
25 return binarySearchHelp ( arr , ind + 1 , high , key ) ;
26 }
27 i n t binarySearch ( i n t * arr , i n t len , i n t key )
28 {
29 return binarySearchHelp ( arr , 0 , len - 1 , key ) ;
30 }
31 void printArray ( i n t * arr , i n t len ) ;
32 i n t main ( i n t argc , char * * argv )
Programming Problems Using Recursion 225
33 {
34 i f ( argc < 2)
35 {
36 printf ( " need a positive integer \ n " ) ;
37 return EXIT_FAILURE ;
38 }
39 i n t num = strtol ( argv [1] , NULL , 10) ;
40 i f ( num <= 0)
41 {
42 printf ( " need a positive integer \ n " ) ;
43 return EXIT_FAILURE ;
44 }
45 i n t * arr = arrGen ( num ) ;
46 printArray ( arr , num ) ;
47 i n t count ;
48 f o r ( count = 0; count < 10; count ++)
49 {
50 i n t key ;
51 i f (( count % 2) == 0)
52 {
53 key = arr [ rand () % num ];
54 }
55 else
56 {
57 key = rand () % 100000;
58 }
59 printf ( " search (% d ) , result = % d \ n " ,
60 key , binarySearch ( arr , num , key ) ) ;
61 }
62 free ( arr ) ;
63 return EXIT_SUCCESS ;
64 }
65 i n t * arrGen ( i n t size )
66 {
67 i f ( size <= 0)
68 {
69 return NULL ;
70 }
71 i n t * arr = malloc ( s i z e o f ( i n t ) * size ) ;
72 i f ( arr == NULL )
73 {
74 return NULL ;
75 }
76 srand ( time ( NULL ) ) ; // set the seed
77 i n t ind ;
78 arr [0] = rand () % RANGE ;
79 f o r ( ind = 1; ind < size ; ind ++)
80 {
81 arr [ ind ] = arr [ ind - 1] + ( rand () % RANGE ) + 1;
82 }
83 return arr ;
226 Intermediate C Programming
84 }
85 void printArray ( i n t * arr , i n t len )
86 {
87 i n t ind ;
88 f o r ( ind = 0; ind < len ; ind ++)
89 {
90 printf ( " % d " , arr [ ind ]) ;
91 }
92 printf ( " \ n \ n " ) ;
93 }
This program introduces the concept of helper functions. Helper functions are common in
recursion for organizing the arguments correctly. In this example, binarySearch has three
arguments; however, the recursive function requires four arguments. Instead of passing the
array’s length, two arguments indicate the contiguous part of the array that remains to be
searched. The range is expressed with the two arguments: low and high.
Please pay attention to how the range changes in recursive calls: The range must shrink
in each call. This ensures that the recursive call chain eventually reaches a terminating
condition. Line 16 uses integer division: If low + high is an odd number, then the remainder
is discarded because ind is an integer. Note carefully that line 23 uses ind - 1 for the new
high index. A common mistake is to use ind instead. This will cause a problem because
it does not guarantee that the range shrinks in recursive calls. For example, consider the
situation where the range has only one element. This occurs when low is the same as high.
Their average ind is also the same. If line 23 were to use ind, then the next recursive call
to the helper function would also have low equal to high. The arguments are unchanged
and the recursion will not end. Similarly, in line 25 the low index must be ind + 1 and
not ind. Another common mistake is using if (low >= high) for the condition at line
12. This is wrong when the array has only one element to check. This function returns −1
without checking whether or not that single element is the same as key.
The source listing above includes a function to generate test cases called arrGen. The
program calls binarySearch ten times. In five of the calls (when count is an even number
at line 51), key is an element of the array and therefore binarySearch should find key.
This program shows a strategy to test the program with known results.
have been sorted. This occurs when each part has only one element or no element at all.
How does the algorithm divide the array into three parts? One solution uses these steps:
1. Determine the value of the pivot. In this example, the pivot is the first element.
2. Iterate through the original array from left (smaller indexes) to right (larger indexes)
using two indexes called low and high. The initial value of low is one higher than the
index of the pivot. The initial value of high is the largest index of the range being
considered.
3. From the left side, if an element is smaller than the pivot, low increments. If an
element is greater than the pivot, stop changing low.
4. From the right side, if an element is greater than the pivot, high decrements. If an
element is smaller than the pivot, stop changing high.
5. Now swap the elements whose indexes are low and high.
6. Continue steps 2 to 4 until low is greater than high.
7. Put the pivot between the two parts.
Note that by the last step, the array will be ordered such that all of the elements smaller
than the pivot are together, and all of the elements larger than pivot are also together. When
the pivot is placed, it is in the correct position for the final sorted array. The following figure
illustrates the procedure. The pivot is 19, low is 1, and high is 11.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 23 8 31 6 42 28 16 51 33
variable pivot low high
The next value, 12, is also smaller than 19, and low increments again. The next value
is 23 and it is greater than 19. Thus, low stops incrementing.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 23 8 31 6 42 28 16 51 33
variable pivot low high
Following the algorithm, if the value whose index is high is greater than the pivot, then
decrement high. Since 33 is greater than 19, we must decrement high.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 23 8 31 6 42 28 16 51 33
variable pivot low high
At this moment, the value whose index is low is greater than the pivot. The value whose
index is high is smaller than the pivot. Now we swap these two values.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 16 8 31 6 42 28 23 51 33
variable pivot low high
228 Intermediate C Programming
Continuing the algorithm, the value of low increases because 16 is smaller than 19.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 16 8 31 6 42 28 23 51 33
variable pivot low high
Because 31 is greater than 19, low stops here. Since 23 is greater than the pivot, high
decrements.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 16 8 31 6 42 28 23 51 33
variable pivot low high
The index high decrements twice more, and the value at high is 6.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 19 7 12 16 8 31 6 42 28 23 51 33
variable pivot low high
If low increments, it will meet high. This means that the array has been divided into
three parts: (i) the first element, which is the pivot, (ii) the part that is smaller than the
pivot, and (iii) the part that is greater than the pivot.
Now the value at low and the pivot are swapped.
index 0 1 2 3 4 5 6 7 8 9 10 11
value 6 7 12 16 8 19 31 42 28 23 51 33
variable low high
The algorithm next sorts part (ii) using the same procedure.
index 0 1 2 3 4
value 6 7 12 16 8
variable pivot low high
The algorithm also sorts part (iii) using the same procedure.
index 6 7 8 9 10 11
value 31 42 28 23 51 33
variable pivot low high
A sample implementation of quick sort is shown below. The function quickSort takes
only two arguments: the array and its length. The recursive function needs three argu-
ments: the array and the range of indexes to be sorted. Thus, a helper function called
quickSortHelp is created. This helper function divides the array elements in the specified
range into three parts and recursively sorts the first and the third parts.
Programming Problems Using Recursion 229
1 // quicksort . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < time .h >
5 #i n c l u d e < string .h >
6 #d e f i n e RANGE 10000
7 i n t * arrGen ( i n t size ) ;
8 // generate a sorted array of integers
9 void swap ( i n t * a , i n t * b ) ;
10 s t a t i c void quickSortHelp ( i n t * arr , i n t first , i n t last )
11 {
12 // [ first , last ]: range of valid indexes ( not last - 1)
13 i f ( first >= last ) // no need to sort one or no element
14 {
15 return ;
16 }
17 #i f d e f DEBUG
18 printf ( " first = %d , last = % d \ n " , first , last ) ;
19 #e n d i f
20 i n t pivot = arr [ first ];
21 i n t low = first + 1;
22 i n t high = last ;
23 while ( low < high )
24 {
25 while (( low < last ) && ( arr [ low ] <= pivot ) )
26 {
27 // <= so that low will increment when arr [ low ]
28 // is the same as pivot , using < will stop
29 // incrementing low when arr [ low ] is the same
30 // as pivot and the outer while loop will not stop
31 low ++;
32 }
33 while (( first < high ) && ( arr [ high ] > pivot ) )
34 {
35 high - -;
36 }
37 i f ( low < high )
38 {
39 swap (& arr [ low ] , & arr [ high ]) ;
40 }
41 }
42 i f ( pivot > arr [ high ])
43 {
44 swap (& arr [ first ] , & arr [ high ]) ;
45 }
46 quickSortHelp ( arr , first , high - 1) ;
47 quickSortHelp ( arr , low , last ) ;
48 }
49 void quickSort ( i n t * arr , i n t len )
50 {
51 quickSortHelp ( arr , 0 , len - 1) ;
230 Intermediate C Programming
52 }
53 void printArray ( i n t * arr , i n t len ) ;
54 i n t main ( i n t argc , char * * argv )
55 {
56 i f ( argc < 2)
57 {
58 printf ( " need a positive integer \ n " ) ;
59 return EXIT_FAILURE ;
60 }
61 i f ( argc == 3)
62 {
63 srand ( strtol ( argv [2] , NULL , 10) ) ;
64 }
65 else
66 {
67 srand ( time ( NULL ) ) ; // set the seed
68 }
69 i n t num = strtol ( argv [1] , NULL , 10) ;
70 i f ( num <= 0)
71 {
72 printf ( " need a positive integer \ n " ) ;
73 return EXIT_FAILURE ;
74 }
75 i n t * arr = arrGen ( num ) ;
76 printArray ( arr , num ) ;
77 quickSort ( arr , num ) ;
78 printArray ( arr , num ) ;
79 free ( arr ) ;
80 return EXIT_SUCCESS ;
81 }
82 void swap ( i n t * a , i n t * b )
83 {
84 int s = * a;
85 * a = * b;
86 * b = s;
87 }
88 i n t * arrGen ( i n t size )
89 {
90 i f ( size <= 0)
91 {
92 return NULL ;
93 }
94 i n t * arr = malloc ( s i z e o f ( i n t ) * size ) ;
95 i f ( arr == NULL )
96 {
97 return NULL ;
98 }
99 i n t ind ;
100 f o r ( ind = 0; ind < size ; ind ++)
101 {
102 arr [ ind ] = rand () % RANGE ;
Programming Problems Using Recursion 231
103 }
104 return arr ;
105 }
106 void printArray ( i n t * arr , i n t len )
107 {
108 i n t ind ;
109 i n t sorted = 1;
110 f o r ( ind = 0; ind < len ; ind ++)
111 {
112 #i f d e f DEBUG
113 printf ( " % d " , arr [ ind ]) ;
114 #e n d i f
115 i f (( ind > 0) && ( arr [ ind ] < arr [ ind -1]) )
116 {
117 sorted = 0;
118 }
119 }
120 printf ( " \ nsorted = % d \ n \ n " , sorted ) ;
121 }
This implementation introduces a new way to debug. Lines 17 and 19 use #ifdef DEBUG
and #endif to enclose debugging code. If this program is compiled the normal way, the lines
between #ifdef DEBUG and #endif are skipped by the compiler. In other words, the line
(or lines) between #ifdef DEBUG and #endif has (or have) no effect. This is useful if the
program prints too many debugging messages. If you want to see the debugging messages,
compile the program in the following way:
When adding -DDEBUG (it is -D followed by the symbol after #ifdef) after gcc, the
debugging messages are shown. This flag tells gcc to define the symbol DEBUG. You can
define other symbols by adding -D in front of the symbol after the gcc command. It is also
possible to add -DDEBUG to CFLAGS in Makefile.
You may have noticed that the function printArray also checks whether the array is
sorted. This is another debugging technique. Visually inspecting whether an array is sorted
is useful for an array with only a few elements. Instead of using visual inspection, the
program automatically determines whether or not the array is sorted. Making the program
check for its own correctness allows us to test quickSort and its helper function with an
array of thousands of elements.
Another debugging technique is to use argv[2] to set the seed of the random numbers.
To test the program, you probably want to use random numbers so that the tests can cover
different scenarios. However, we need some way to repeat the test if a problem is found, and
that means we must be able to control the sequence of random numbers. One solution is to
use a command-line argument. If this argument is present, the random number generator
is seeded correspondingly, and then the same sequence of numbers is generated. Without
giving this command-line argument, the seed is determined by the system clock, and the
sequence will almost certainly be different every time the program is run.
At first glance, this program may appear straightforward. A closer look, however, re-
veals that some common mistakes can easily occur. The helper function’s second and third
arguments specify the range of indexes that is being sorted. This function assumes last is
a valid index and quickSort thus must use len - 1. If line 51 uses len, then the program
may access an invalid memory location because len is not a valid index. As explained in
232 Intermediate C Programming
Section 7.2.2, sometimes accessing an invalid memory address does not seem to cause prob-
lems but the program is still wrong. If the program is compiled on different platforms, with
different compilers, or if it is run enough times, then at some stage it will fail. Running
valgrind is extraordinarily helpful in picking up these types of errors. If line 51 uses len,
then valgrind reports:
The problem occurs when high is last. Please note that last can be len.
Now look at line 25, which uses arr[low] <= pivot. What happens if it is rewritten
as arr[low] < pivot? This small difference can cause problems when some of the array’s
elements have the same value as the pivot. When this occurs and line 25 has no =, then low
does not increment. If arr[high] is smaller than the pivot, then high does not decrement.
As a result, neither low nor high change and the program enters an infinite loop because
low < high is always true.
Some implementations of quick sort select random (not the first) array elements for the
pivots. Why? Quick sort can be fast due to transitivity. If the original array is already
sorted then quick sort is not faster because the first part (the part smaller than the pivot)
is empty, and the program does not take advantage of transitivity. Using a random element
in the array for the pivot reduces the chance when the pivot is always the smallest element
in the sorted array.
A B C D
The first item, A, may appear in the first, second, third, or fourth column.
A
A
A
A
Every time A moves, it is swapped with the item originally at that column. The second
item, B, may also appear in the first, second, third, or fourth column. However, we need to
exclude putting B in the first column because A appears in the second column by swapping
A and B. Thus, B already has a chance to be moved to the first column and needs to be
moved to only the second (original location), third, and fourth columns.
B
B
B
C
C
46 i f ( num <= 0)
47 {
48 return EXIT_FAILURE ;
49 }
50 i n t * arr ;
51 arr = malloc ( s i z e o f ( i n t ) * num ) ;
52 i n t ind ;
53 f o r ( ind = 0; ind < num ; ind ++)
54 {
55 arr [ ind ] = ind + ’A ’; // elements are ’A ’, ’B ’, ...
56 }
57 permute ( arr , num ) ;
58 free ( arr ) ;
59 return EXIT_SUCCESS ;
60 }
Lines 28 to 33 are the core that generates the permutations. One way to understand
how this works is to check the number of iterations generated. When ind is 0, the loop
iterates num times. When ind is 1, the loop iterates num − 1 times. When ind is 2, the
loop iterates num − 2 times. Finally, when ind is num − 1, the loop iterates only once. This
program will iterate num × (num − 1) × (num - 2) ... 1 = num! times. This is the number
of permutations for num items. If all the lines are unique then they are guaranteed to be the
correct permutations.
You may be wondering why loc starts at ind, because when loc is ind, line 30,
1 swap (& arr [ ind ] , & arr [ loc ]) ;
has no effect. It is true that swapping an element with itself has no effect. However, this is the
way to keep this element at the original location. Without this line, the element will never
stay in the original location. For example, if A is the first element, and loc starts at ind + 1,
then A is always swapped away from the first location, and no generated permutation will
begin with A. As a result, the program will fail to generate all possible permutations.
A different approach can be used to generate combinations. Instead of permuting an
array storing the items, an array is used to store whether a particular item is selected or
not. For example, if arr[0] is 0, A is not selected. If arr[0] is 1, then A is selected. If
arr[2] is 0, then C is not selected. If arr[2] is 1, then C is selected. The helper function
requires five arguments:
1. arr is a binary array storing whether an element is selected or not.
2. ind is the index of the item being decided on whether it is selected.
3. num is the total number of items.
4. sel is the number of items to be selected.
5. sum is the number of items already selected.
1 // combine . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 void printArray ( i n t * arr , i n t length )
6 {
7 i n t ind ;
8 f o r ( ind = 0; ind < length ; ind ++)
9 {
10 i f ( arr [ ind ] == 1)
Programming Problems Using Recursion 235
11 {
12 printf ( " % c " , ind + ’A ’) ;
13 }
14 }
15 printf ( " \ n " ) ;
16 }
17 void combineHelp ( i n t * arr , i n t ind , i n t num ,
18 i n t sel , i n t sum )
19 {
20 i f ( sum == sel ) // select enough items
21 {
22 printArray ( arr , num ) ;
23 return ;
24 }
25 i f ( ind == num ) // end of array , no more item to select
26 {
27 return ;
28 }
29 // select this element
30 arr [ ind ] = 1;
31 combineHelp ( arr , ind + 1 , num , sel , sum + 1) ;
32 // do not select this element
33 arr [ ind ] = 0;
34 combineHelp ( arr , ind + 1 , num , sel , sum ) ;
35 }
36 void combine ( i n t * arr , i n t num , i n t sel )
37 {
38 combineHelp ( arr , 0 , num , sel , 0) ;
39 }
40 i n t main ( i n t argc , char * argv [])
41 {
42 i f ( argc != 3) // need two numbers
43 {
44 return EXIT_FAILURE ;
45 }
46 i n t num = ( i n t ) strtol ( argv [1] , NULL , 10) ;
47 i f ( num <= 0)
48 {
49 return EXIT_FAILURE ;
50 }
51 i n t sel = ( i n t ) strtol ( argv [2] , NULL , 10) ;
52 i f (( sel <= 0) || ( sel > num ) )
53 {
54 return EXIT_FAILURE ;
55 }
56 i n t * arr ;
57 arr = malloc ( s i z e o f ( i n t ) * num ) ;
58 i n t ind ;
59 f o r ( ind = 0; ind < num ; ind ++)
60 {
61 arr [ ind ] = 0;
236 Intermediate C Programming
62 }
63 combine ( arr , num , sel ) ;
64 free ( arr ) ;
65 return EXIT_SUCCESS ;
66 }
When sum equals sel, enough items have been selected and the selected items are
printed. When ind equals num, no more items are available for selection. Line 30 selects the
item and one is added to sum when recursively calling combineHelp. Line 33 “unselects” the
item and sum is unchanged in the recursive call. Either the item is selected or it is not. The
helper function recursively calls itself to determine whether to select the remaining items.
From the examples of permutations and combinations, you can see recursion is a natural
way of solving these problems. Recursion is a good approach when the solutions
have “branches”. In permutation, each element can be in one of many locations. After
setting one element to a particular location, the next element also can be in one of many
locations. By putting recursive calls inside a loop, the solution naturally solves permutations.
For combinations, each element may be selected or not and there are two branches. One
reason that makes recursion a better solution is that the number of iterations changes. For
both cases, the call stack keeps the values of the array indexes. The indexes indicate which
element to consider next. Without using recursion, programmers have to allocate memory
for keeping the values of the indexes.
15.4.1 Example 1
Consider the sequence <2, 1>. When 2 is read from the sequence, the stack is empty
and 2 is pushed on to the stack (step 3). Next, 1 is read from the sequence, 1 is smaller
Programming Problems Using Recursion 237
than the element on top of the stack, and is therefore pushed to the stack (step 6). Now the
sequence is finished and we pop the numbers from the stack (step 8) and the result is <1,
2>. Below is a graphical illustration of the steps. The first number, 2, is read and pushed
to the stack.
2
15.4.2 Example 2
The next example considers the sequence <1, 2>. The first number, 1, is read from the
sequence and pushed to the stack.
The second number, 2, is read. Since 1 is smaller than 2, 1 is popped from the stack and 2
is pushed on to the stack.
15.4.3 Example 3
The third example is the sequence <1, 3, 2>. The first number, 1, is read from the
sequence and pushed on to the stack.
The second number, 3, is read. Since 1 is smaller than 3, 1 is popped from the stack and 3
is pushed on to the stack.
The third number, 2, is read. Since 2 is smaller than 3, 2 is pushed to the stack.
2
3
15.4.4 Example 4
In the fourth example we consider the sequence <2, 3, 1>. The first number, 2, is read
and pushed on to the stack.
The second number, 3, is read. Since 2 is smaller than 3, 2 is popped from the stack and 3
is pushed on to the stack.
The third number, 1, is read. Since 1 is smaller than 3, 1 is pushed on to the stack.
1
3
A: before M M B: after M
Step 5 of the algorithm pops all the numbers in the stack when M is read. Suppose MA
is the largest value in A. All the values must obey MA < M , because M is the largest value.
Suppose mB is the smallest value in B. When M is pushed to the stack, every number in A
must have already been popped. If mB < MA , mB should be popped before MA is popped.
However, when M is pushed, MA has already been popped and there is no chance for mB
to be popped before MA is popped. As a result, stack sort fails for this sequence. In the last
example, <2, 3, 1>, M is 3, MA is 2, and mB is 1. The condition mB < MA is satisfied so
<2, 3, 1> is not stack sortable.
Is this sequence <1, 5, 2, 3, 5, 4> stack sortable? Note that the largest value 5 appears
twice in this sequence. Following the procedure described earlier, the first number is pushed
to the stack.
1
2
5
Finally, all numbers are popped and the output sequence is 1, 2, 3, 4, 5, 5. It is sorted.
Thus, <1, 5, 2, 3, 5, 4> is stack sortable.
If the same largest value M (5 in this example) appears multiple times, which M should
be chosen for breaking the sequence into A, M , and B? Should it be the first M , the second
M , the last one, or will any one do? The answer is the first M because we compare only the
smallest element in B and do not care about the largest element in B. If we choose an M
other than the first, then the first M is in A and the sequence would never be considered
stack sortable, which is a mistake.
After breaking the sequence into the three parts, the same procedure is applied recur-
sively to A and B to determine whether in turn they are stack sortable. The algorithm for
checking stack sortable is described as follows:
1. An empty sequence is stack sortable. This is a base case.
2. Find the largest value in the sequence. If this value appears multiple times in the
array, select the first one.
3. Divide the sequence into three parts.
4. If both A and B are empty, then the sequence is stack sortable. The sequence has
only one element. This is another base case.
5. If A is empty (B must not be empty), then jump to step 9.
6. If B is empty (A must not be empty), then jump to step 8.
7. Since neither A nor B is empty, find MA and mB . If MA > mB , then the sequence is
not stack sortable. This is another base case.
8. Recursively check whether A is stack sortable.
9. Recursively check whether B is stack sortable.
The following implementation checks whether a sequence is stack sortable. We have used
the permutation program to generate all possible test cases.
1 // stacksort . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 i n t findIndex ( i n t * arr , i n t first , i n t last , i n t maxmin )
6 // find the index of the largest or smallest element
7 // the range is expressed by the indexes [ first , last ]
8 // maxmin = 1: find largest , maxmin = 0: find smallest
240 Intermediate C Programming
9 {
10 i n t ind ;
11 i n t answer = first ;
12 f o r ( ind = first + 1; ind <= last ; ind ++)
13 {
14 i f ((( maxmin == 1) && ( arr [ answer ] < arr [ ind ]) ) ||
15 (( maxmin == 0) && ( arr [ answer ] > arr [ ind ]) ) )
16 {
17 answer = ind ;
18 }
19 }
20 return answer ;
21 }
22 i n t findMaxIndex ( i n t * arr , i n t first , i n t last )
23 {
24 return findIndex ( arr , first , last , 1) ;
25 }
26 i n t findMinIndex ( i n t * arr , i n t first , i n t last )
27 {
28 return findIndex ( arr , first , last , 0) ;
29 }
30 i n t isStackSortable ( i n t * arr , i n t first , i n t last )
31 // check whether the range of the array is sortable
32 // return 1 if the range of the array is sortable
33 // return 0 if the range of the array is not sortable
34 {
35 i f ( first >= last ) // no or one element is stack sortable
36 {
37 return 1;
38 }
39 i n t maxIndex = findMaxIndex ( arr , first , last ) ;
40 // consider the four cases
41 // both A and B are empty
42 // The array has only one element , it is stack sortable
43 // already checked earlier
44
111 }
112 i n t num = ( i n t ) strtol ( argv [1] , NULL , 10) ;
113 i f ( num <= 0)
114 {
115 return EXIT_FAILURE ;
116 }
117 i n t * arr ;
118 arr = malloc ( s i z e o f ( i n t ) * num ) ;
119 i n t ind ;
120 f o r ( ind = 0; ind < num ; ind ++)
121 {
122 arr [ ind ] = ind + 1;
123 }
124 permute ( arr , num ) ;
125 free ( arr ) ;
126 return EXIT_SUCCESS ;
127 }
1
For an array of n distinct numbers, there are n! permutations. Among them, n+1 Cn2n
are stack sortable. This is called the Catalan number. The proof will be given later in this
book.
FIGURE 15.2: Determine the values of fv and count using a graphical illustration of the
calling relations.
The value of fv is 5. The value of count increments every time func is called. The figure
shows that both func and count are called 9 times.
This is a “top-down” approach to computing func. To compute func(4), the program
calls func(3). To compute func(3), func(2) is called. Computing func(4) calls func(2)
three times and each time that happens it needs to call func(1) + func(1). This is inef-
ficient. It would be more efficient if the program remembers the values of func(2) so that
it does not need to be recomputed. The following implementation shows a different way of
computing func. It is a “bottom-up” approach: func(2) and func(3) are computed before
computing func(4). This is more efficient because the program remembers the value of
func(2), and therefore does not need to recompute it.
1 // bottomup . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 i n t func ( i n t n )
5 {
6 i n t * arr ;
7 // need n + 1 for arr [ n ]
8 arr = malloc ( s i z e o f ( i n t ) * ( n + 1) ) ;
9 arr [0] = 1;
10 arr [1] = 1;
11 i n t ind ;
12 f o r ( ind = 2; ind <= n ; ind ++)
13 {
14 arr [ ind ] = arr [ ind - 1] + arr [ ind / 2];
15 }
16 i n t val = arr [ n ];
17 free ( arr ) ;
18 return val ;
19 }
244 Intermediate C Programming
20 {
21 i f ( low >= high ) /* ERROR : should be >, not >= */
22 {
23 return -1;
24 }
25 i n t mid = ( low + high ) / 2;
26 i f ( arr [ mid ] == key )
27 {
28 return mid ;
29 }
30 i f ( arr [ mid ] > key )
31 {
32 return binarySearchHelp ( arr , key , low , mid - 1) ;
33 }
34 return binarySearchHelp ( arr , key , mid + 1 , high ) ;
35 }
36
0
1
2
3
4
5
6
7
8
9
When searching for 1, the arguments low and high change as shown in the following:
low high mid arr[mid] key
0 9 4 65 1
0 3 1 12 1
0 0 0 1 1
The problem occurs when low and high are both zero and binarySearchHelp re-
turns −1 without checking whether arr[mid] is the same as key. Does this mistake cause
binarySearchHelp to always return −1? Consider searching for 12:
low high mid arr[mid] key
0 9 4 65 12
0 3 1 12 12
The function correctly returns 1. What will the function return when searching for 23?
246 Intermediate C Programming
-1
1
2
-1
4
5
-1
7
8
-1
As you can see, this program sometimes produces correct results and sometimes produces
incorrect results. This program has a 60% chance of producing correct results. This reinforces
the need to have a strategy for testing. It is usually important to automate testing so that
you can test many cases easily. A common mistake among beginning programmers is that
they test several cases and then believe their programs are correct.
Part III
Structure
247
This page intentionally left blank
Chapter 16
Programmer-Defined Data Types
Section 7.1.2 mentioned several reasons for creating header files (.h files), including defining
constants using #define, declaring functions, and defining new data types—usually referred
to as “types”. This chapter explains how to define new types using structures. First let us
consider what makes up a type by thinking about the differences between int and double.
A type specifies:
• The format for the data. For example, integers and double-precision floating-point
numbers are represented differently in the computer’s memory.
• The range of possible values, and the size required to store the data. A 4-byte integer
stores valid values that are between the range of −2,147,483,648 (−231 , approximately
−109 ) and 2,147,483,647 (231 − 1). In contrast, the absolute value of a double-precision
floating number (8 bytes) can be as small as 10−308 and as large as 10308 (approxi-
mately).
• The effect of operations on the data. Because the two types have different formats,
when a program has a statement a + b, the actual operations depend on whether a
and b are integers or floating-point numbers. Also, some operations are restricted to
certain data types. For example, switch must be used with an integer; double cannot
be used in switch statements.
C also supports other data types, such as char and float. C does not have a separate
type for strings; instead, C uses null-terminated arrays of char for strings, as explained in
Chapter 6. A natural question is whether C allows programmers to create new types. The
answer is yes.
Why would programmers want to create new types? The most obvious reason is to put
related data together. For example, a college student has a name (string), year (integer),
grade point average (floating-point), and so on. C programmers would have to do something
awkward if C did not allow the creation of new types. For example, separate arrays could
be created to store the students’ data: an array of strings for student names, an array of
integers for years, an array of floating-point numbers for scores, etc. There is no good way
to associate the elements in different arrays. There is no good way to ensure that the arrays
have the same numbers of elements. Therefore, supporting programmer-defined types is
essential.
249
250 Intermediate C Programming
6 int x;
7 int y;
8 int z;
9 } Vector ; /* don ’t forget ; */
10 #e n d i f
The type begins with typedef struct, which tells gcc that a new type is defined here.
This type contains multiple attributes and they form a structure. After the closing brace,
Vector is the name of the new type. Remember to add the semicolon after the name. This
book adopts the naming convention of using a capital letter to start the name of a structure.
It is common to have only one structure in each header file and the file’s name is the same as
the structure’s name, but in lowercase. Thus, the structure Vector is defined in the header
file vector.h.
In the following program, v1 is a Vector object. This object has three attributes. To
access an attribute, a period is needed after v1, such as v1.x and v1.y.
1 // vector . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #i n c l u d e " vector . h "
6 i n t main ( i n t argc , char * argv [])
7 {
8 Vector v1 ;
9 v1 . x = 3;
10 v1 . y = 6;
11 v1 . z = -2;
12 printf ( " The vector is (% d , %d , % d ) .\ n " ,
13 v1 .x , v1 .y , v1 . z ) ;
14 return EXIT_SUCCESS ;
15 }
The program’s output is shown below:
What does line 8 actually do? It creates an object on the call stack.
Symbol Address Value
v1.z 108 garbage
v1.y 104 garbage
v1.x 100 garbage
The type Vector requires three integers and the call stack stores those three integers.
Each attribute occupies 4 bytes (assuming that sizeof(int) is 4). The attributes are not
initialized and the values are garbage. Line 9 changes the value at address 100 to 3.
Symbol Address Value
v1.z 108 garbage
v1.y 104 garbage
v1.x 100 garbage → 3
A Vector object can be copied to another Vector object, as the following example
illustrates:
252 Intermediate C Programming
1 // vector2 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #i n c l u d e " vector . h "
6 i n t main ( i n t argc , char * argv [])
7 {
8 Vector v1 ;
9 v1 . x = 3;
10 v1 . y = 6;
11 v1 . z = -2;
12 printf ( " The vector is (% d , %d , % d ) .\ n " ,
13 v1 .x , v1 .y , v1 . z ) ;
14 Vector v2 = {0};
15 printf ( " The vector is (% d , %d , % d ) .\ n " ,
16 v2 .x , v2 .y , v2 . z ) ;
17 v2 = v1 ;
18 printf ( " The vector is (% d , %d , % d ) .\ n " ,
19 v2 .x , v2 .y , v2 . z ) ;
20 v1 . x = -4;
21 v2 . y = 5;
22 printf ( " The vector is (% d , %d , % d ) .\ n " ,
23 v1 .x , v1 .y , v1 . z ) ;
24 printf ( " The vector is (% d , %d , % d ) .\ n " ,
25 v2 .x , v2 .y , v2 . z ) ;
26 return EXIT_SUCCESS ;
27 }
The program’s output is:
Line 14 creates a Vector object and initializes every attribute to zero. Please remember
that C does not initialize the attributes for you. Attributes must be initialized explicitly.
Line 17 copies v1’s attributes to v2’s attributes. The attributes are copied from v1 to v2
one by one. The call stack is shown below:
Symbol Address Value
v2.z 120 −2
v2.y 116 6
v2.x 112 3
v1.z 108 −2
v1.y 104 6
v1.x 100 3
Since v1 and v2 occupy different addresses in the call stack, changing the attributes of
v1 does not affect the attributes of v2 and vice versa. Line 20 changes v1.x; line 21 changes
v2.y. The effects are limited to the corresponding addresses.
Programmer-Defined Data Types 253
10 i f ( v1 != v2 )
11 {
12 printf ( " v1 and v2 are different .\ n " ) ;
13 }
When compiling this function, gcc will say:
If we want to compare two Vector objects, we have to write a function that compares
the attributes, for example,
1 #i n c l u d e " vector . h "
2 i n t equalVector ( Vector v1 , Vector v2 )
3 // return 0 if any attribute is different
4 // return 1 if all attributes are equal
5 {
6 i f ( v1 . x != v2 . x ) { return 0; }
7 i f ( v1 . y != v2 . y ) { return 0; }
8 i f ( v1 . z != v2 . z ) { return 0; }
9 return 1;
10 }
The following section explains how to pass objects as function arguments.
as when passing other types of arguments, such as int and double: A copy of the argument
is passed. The following example shows a separate function for printing Vector objects:
1 // vector3 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #i n c l u d e " vector . h "
6 void printVector ( Vector v )
7 {
8 printf ( " The vector is (% d , %d , % d ) .\ n " , v .x , v .y , v . z ) ;
9 }
10
How do we know that the attributes are copied? In the following program changeVector
changes the Vector object passed to it. However, inside main, the attributes of v1 are
unchanged.
1 // vector4 . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #i n c l u d e " vector . h "
6 void printVector ( Vector v )
7 {
8 printf ( " The vector is (% d , %d , % d ) .\ n " , v .x , v .y , v . z ) ;
9 }
10
17 }
18
What really happens when a function’s argument is an object? This can be explained
by showing the call stack before calling changeVector:
Frame Symbol Address Value
v1.z 108 −2
main v1.y 104 6
v1.x 100 3
Calling changeVector pushes a new frame to the call stack. The argument is an object
that has three attributes. The values are copied from the calling function into the new
frame.
Frame Symbol Address Value
v.z 124 −2
v.y 120 6
changeVector
v.x 116 3
return location 112 line 27
v1.z 108 −2
main v1.y 104 6
v1.x 100 3
The function changeVector changes the attributes of the object in its own frame.
Frame Symbol Address Value
v.z 124 7
v.y 120 −3
changeVector
v.x 116 5
return location 112 line 27
v1.z 108 −2
main v1.y 104 6
v1.x 100 3
When changeVector finishes, the frame is popped and the program resumes at main.
The call stack is shown below:
256 Intermediate C Programming
Instead of copying the whole object, attribute by attribute, the argument p stores only
the address of the object v1. This is the address of the first attribute.
What is the -> symbol inside changeVector? The -> symbol takes the value at the
address, and then gets the attribute as if applying a . to a structure. Pointers are used with
structures often and C has this special syntax. It is equivalent to saying:
13 p -> x
is the same as
13 (* p ) . x
This dereferences p first, and then applies . for x. Dereferencing is the second way of using
* as explained in Table 4.1. Note that this means -> can only be used on a pointer to a
structure. It is illegal to use it in any other circumstance. If -> is at the left hand side (LHS)
of an assignment, then the attribute is modified (i.e., written). If -> is at the right hand
side (RHS) of an assignment, then the attribute is read. The statement,
13 p -> x = 5;
changes the value at address 100.
Frame Symbol Address Value
p 116 100
changeVector
return location 112 line 27
v1.z 108 −2
main v1.y 104 6
v1.x 100 3→5
14 p -> y = -3;
changes the value at address 104.
Frame Symbol Address Value
p 116 100
changeVector
return location 112 line 27
v1.z 108 −2
main v1.y 104 6 → -3
v1.x 100 5
Calling malloc allocates a piece of memory that is in the heap. The size is sufficient to
accommodate three integers. Suppose malloc returns 60000. Then the call stack becomes:
Frame Symbol Address Value
v 124 60000
c 120 −2
b 116 6
Vector construct
a 112 3
value address 108 100
return location 104 line 37
main v1 100 garbage
The pointer’s value takes on the address returned by calling malloc. Since it is a pointer,
the program uses -> to access the attributes. The statement,
16 v -> x = a ;
17 v -> y = b ;
18 v -> z = c ;
modifies the values at addresses 60000, 60004, and 60008 to 3, 6, and −2 respectively. The
heap memory is changed to:
Address Value
60008 garbage → −2
60004 garbage → 6
60000 garbage → 3
When Vector construct returns, v’s value is written to the return address 100. There-
fore, the call stack becomes:
Frame Symbol Address Value
main v1 100 60000
Note that, as always, the memory allocated on heap must be released by calling free.
This is the purpose of the destructor Vector destruct.
Programmer-Defined Data Types 261
Notice how the constructor initializes the attributes in the same order as they are de-
clared in the header file. This is a good programming habit. For the sake of clarity, make
things as consistent as possible. This habit prevents accidentally forgetting to initialize an
attribute. Below is the destructor:
1 #i n c l u d e " person . h "
2 #i n c l u d e < stdlib .h >
3 void Person_destruct ( Person * p )
4 {
5 // p -> name must be freed before p is freed
6 free ( p -> name ) ;
7 free ( p ) ;
8 }
Note that the destructor releases memory in the reverse order that the construc-
tor allocates memory. This is a general rule. If the destructor free (p) precedes
free (p -> name), then the program will have problems. Why? After free (p), the
object is no longer valid, and free (p -> name) is meaningless and dangerous. Af-
ter calling free (p), the program cannot access the memory that contains the pointer
free (p -> name). There is no guarantee that the address is still accessible. If the destruc-
tor does not call free (p -> name), then the program leaks memory. Thus, as a general
rule, the destructor releases memory in the reverse order that the constructor allocates
memory. Please remember that every malloc must have a corresponding free.
Below is the implementation of the Person print function:
1 #i n c l u d e " person . h "
2 #i n c l u d e < stdio .h >
3 void Person_print ( Person * p)
4 {
5 printf ( " Name : % s . " , p -> name ) ;
6 printf ( " Date of Birth : % d /% d /% d \ n " ,
7 p -> year , p -> month , p -> date ) ;
8 }
The following is an example of using the constructor and the destructor:
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdlib .h >
3 #i n c l u d e < string .h >
4 #i n c l u d e " person . h "
5
What is wrong? To understand the problem, we need to understand what the assignment
means. After the main function finishes line 9, this is what is in the call stack and heap
memory:
Both p1 and p2 point to the same memory address 60000. Line 13 calls the destructor
and releases the heap memory. This is perfectly correct. Line 14 calls the destructor again
but the memory has already been released. The same heap memory cannot be released
264 Intermediate C Programming
twice. Assigning p1 to p2 at line 10 merely copies the pointer, and the two different pointers
store the same memory address 60000.
Can the problem be solved by simply not calling the destructor for p2? Yes, but it
depends on the intention of the program. When p1 and p2 have the same value, changing
p1 -> name[0] (the first letter of the name) will also change p2 -> name[0]. However, in
the following code:
1 i n t x = 5;
2 int y = x;
3 x = 12;
What is the value of y? Should it be 5 or 12? Experience tells us that y should be 5. This
is correct because x and y occupy different memory addresses. Even though p1 and p2
have different addresses (100 and 104), they store the same value, 60000. Thus changing
p1 -> name[0] also changes p2 -> name[0]. In the next example, both p1 and p2 have
distinct values generated by calling Person construct. Will the following program work?
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdlib .h >
3 #i n c l u d e < string .h >
4 #i n c l u d e " person . h "
5 i n t main ( i n t argc , char * argv [])
6 {
7 Person * p1 = Person_construct (1989 , 8 , 21 , " Amy " ) ;
8 Person * p2 = Person_construct (1991 , 2 , 17 , " Bob " ) ;
9 p2 = p1 ;
10 Person_print ( p1 ) ;
11 Person_print ( p2 ) ;
12 Person_destruct ( p1 ) ;
13 Person_destruct ( p2 ) ;
14 return EXIT_SUCCESS ;
15 }
This is the call stack and the heap memory after line 9:
Does this program work? No, valgrind still reports problems in Person destruct. Line
10 still copies p1’s value to p2 and both are 60000. This also causes a memory leak because
the memory at 80000 and 85000 is no longer accessible.
Consider another scenario when the objects are not accessed through pointers:
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdlib .h >
3 #i n c l u d e < string .h >
4 #i n c l u d e " person . h "
5 i n t main ( i n t argc , char * argv [])
6 {
7 Person p1 ;
8 Person p2 ;
9 p1 . year = 1989;
10 p1 . month = 8;
11 p1 . date = 21;
12 p1 . name = strdup ( " Amy " ) ;
13 p2 . year = 1991;
14 p2 . month = 2;
15 p2 . date = 17;
16 p2 . name = strdup ( " Bob " ) ;
17
18 Person_print (& p1 ) ;
19 Person_print (& p2 ) ;
20 free ( p1 . name ) ;
21 free ( p2 . name ) ;
22 return EXIT_SUCCESS ;
23 }
The program uses strdup to copy strings. In this program, both p1 and p2 are objects
on the call stack:
Now, p1.name and p2.name have the same value (70000). The heap memory originally
pointed to by p2.name is still in the heap but is no longer accessible because p2.name is
no longer 85000. This causes memory leak. Moreover, lines 20 and 21 free the same heap
memory at 70000 twice. As you can see, if an object’s attribute is a pointer, we need to
be very careful about how memory is allocated and freed. If we are not careful, then the
program may leak memory or release the same memory twice, or both.
Are there general rules for handling objects that have attributes which are pointers?
Fortunately, there are. When an object’s attribute is a pointer, that usually indicates the
need for four functions.
• constructor: allocates memory for the attribute and assigns the value to the attribute.
• destructor: releases memory for the attribute.
• copy constructor replacing =: by creating a new object from an existing object. This is
sometimes referred to as cloning. The new object’s attribute points to heap memory
allocated by calling malloc.
• assignment replacing =: modifying an object that has already been created by using the
constructor or the copy constructor. Since the object has already been constructed,
the object’s attribute stores the address of a heap memory. This memory must be
released before allocating new memory.
The first two functions have already been given above. The other two functions are
shown below:
1 #i n c l u d e < stdio .h >
2 #i n c l u d e < stdlib .h >
3 #i n c l u d e < string .h >
4 #i n c l u d e " person . h "
5 Person * Person_copy ( Person * p ) ;
6 // create a new object by copying the attributes of p
7 Person * Person_assign ( Person * p1 , Person * p2 ) ;
8 // p1 is already a Person object , make its attribute
9 // the same as p2 ’s attributes ( deep copy )
10 Person * Person_copy ( Person * p )
11 {
12 return Person_construct ( p -> year , p -> month ,
13 p -> date , p -> name ) ;
14 }
15 Person * Person_assign ( Person * p1 , Person * p2 )
16 {
17 free ( p1 -> name ) ;
18 p1 -> year = p2 -> year ;
19 p1 -> month = p2 -> month ;
20 p1 -> date = p2 -> date ;
Programmer-Defined Data Types 267
46 Person_print ( p3 ) ;
47 Person_destruct ( p1 ) ;
48 Person_destruct ( p2 ) ;
49 Person_destruct ( p3 ) ;
50 return EXIT_SUCCESS ;
51 }
52 Person * Person_construct ( char * n , i n t y , i n t m , i n t d )
53 {
54 Person * p ;
55 p = malloc ( s i z e o f ( Person ) ) ;
56 i f ( p == NULL )
57 {
58 printf ( " malloc fail \ n " ) ;
59 return NULL ;
60 }
61 p -> name = malloc ( s i z e o f ( char ) * ( strlen ( n ) + 1) ) ;
62 // + 1 for the ending character ’\0 ’
63 strcpy ( p -> name , n ) ;
64 p -> dob = D a t e O f B i r t h _ c o n s t r u c t (y , m , d ) ;
65 return p ;
66 }
67 void Person_destruct ( Person * p )
68 {
69 // p must be released after p -> name has been released
70 free ( p -> name ) ;
71 free ( p ) ;
72 }
73 Person * Person_copy ( Person * p )
74 {
75 return Person_construct ( p -> name , p -> dob . year ,
76 p -> dob . month , p -> dob . date ) ;
77 }
78 Person * Person_assign ( Person * p1 , Person * p2 )
79 {
80 free ( p1 -> name ) ;
81
85 return p1 ;
86 }
87 void Person_print ( Person * p )
88 {
89 printf ( " Name : % s . " , p -> name ) ;
90 Dat eOfBir th_pri nt ( p -> dob ) ;
91 }
This program creates a hierarchy of structures. Then, Person construct calls
DateOfBirth construct. Person print calls DateOfBirth print. What is the advantage
of this approach? As programs become more complex, such a hierarchy becomes helpful
for organization. Creating one structure that contains everything can be impractical and
270 Intermediate C Programming
unclear. Instead, we should put related data together and create a structure, for example,
the DateOfBirth structure. We can use this structure inside other structures.
Each structure should have a constructor to initialize all attributes. If a structure has
pointers for dynamically allocated memory, then make sure that there is also a destructor.
If deep copy is required (true in most cases), remember to write a copy constructor and an
assignment function.
TABLE 16.1: Functions for opening, writing to, and reading from text and binary files.
1 // vectorfile . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e < string .h >
5 #i n c l u d e " vector . h "
6 Vector Vector_construct ( i n t a , i n t b , i n t c )
7 {
8 Vector v ;
9 v.x = a;
10 v.y = b;
11 v.z = c;
12 return v ;
13 }
14 void Vector_print ( char * name , Vector v )
15 {
16 printf ( " % s is (% d , %d , % d ) .\ n " , name , v .x , v .y , v . z ) ;
17 }
18 void Vector_writet ( char * filename , Vector v )
Programmer-Defined Data Types 271
70 fptr = fopen ( filename , " r " ) ; // " r " same as " rb " in Linux
71 i f ( fptr == NULL )
72 {
73 printf ( " Vector_readb fopen fail \ n " ) ;
74 return v ;
75 }
76 i f ( fread (& v , s i z e o f ( Vector ) , 1 , fptr ) != 1)
77 {
78 printf ( " fread fail \ n " ) ;
79 }
80 return v ;
81 }
82 i n t main ( i n t argc , char * argv [])
83 {
84 Vector v1 = Vector_construct (13 , 206 , -549) ;
85 Vector v2 = Vector_construct ( -15 , 8762 , 1897) ;
86 Vector_print ( " v1 " , v1 ) ;
87 Vector_print ( " v2 " , v2 ) ;
88 printf ( " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = \ n " ) ;
89 Vector_writet ( " vectort . dat " , v1 ) ;
90 v2 = Vector_readt ( " vectort . dat " ) ;
91 Vector_print ( " v1 " , v1 ) ;
92 Vector_print ( " v2 " , v2 ) ;
93
not fscanf. Four arguments are required for fread and the order of the arguments is the
same as that for fwrite.
What are advantages and disadvantages of text and binary files? If data are stored in a
text file, then it can be read by using the more command in terminal or simply viewing it
in your favorite text editor. Vector readt and Vector writet must handle the attributes
one by one. The order in Vector writet must be the same as the order in Vector readt. If
one more attribute is added to Vector (for example, t for time), then both Vector readt
and Vector writet must be changed. These requirements increase the chances of mistakes:
It is easy to change one place and forget to change the other. In contrast, Vector writeb
and Vector readb automatically handle the order of attributes. If an attribute is added
to Vector, there is no need to change Vector readb and Vector writeb because sizeof
reflects the new size. The disadvantage of using binary files is that they cannot be edited
and viewed directly. The data files are also specific to the platform the code is compiled on,
since the size and format of the binary data can vary between computers.
This page intentionally left blank
Chapter 17
Programming Problems Using Structure
43 Peter
87 Linda
57 Gregory
61 Larry
5 Eric
19 Dennis
56 Betty
70 Joshua
4 Donald
60 Susan
To test the program, we need to compare the answers of our program against the correct
answers. We can use the Linux program sort to generate the correct answers. The correct
answers are generated as follows:
• sort -n: sort the first column and treat the column as numbers. Without -n, the first
column will be treated as strings and “10” is before “9” because 1 is before 9 in the
dictionary.
• sort -k 2: sort by the second column.
This program uses the same Person structure defined earlier. Another structure is de-
fined to store an array of pointers to Person objects. This structure also has an attribute
as the number of pointers in the array. The program needs to implement the follow steps:
1. Read Person objects from a file.
275
276 Intermediate C Programming
15 {
16 fclose ( fptr ) ;
17 return NULL ;
18 }
19 // count the number of people in the file
20 // use the longest name for the size of the buffer
21 i n t numPerson = 0;
22 i n t longestName = 0; // length of buffer to read names
23 while (! feof ( fptr ) )
24 {
25 i n t age ;
26 // find a line that contains a number ( age )
27 i f ( fscanf ( fptr , " % d " , & age ) == 1)
28 {
29 numPerson ++;
30 // the remaning characters are the name
31 i n t nameLength = 0;
32 while ((! feof ( fptr ) ) && ( fgetc ( fptr ) != ’\ n ’) )
33 {
34 nameLength ++;
35 }
36 nameLength ++; // for ’\ n ’
37 i f ( longestName < nameLength )
38 {
39 longestName = nameLength ;
40 }
41 }
42 }
43 // the number of person is known now
44 perdb -> number = numPerson ;
45 perdb -> person = malloc ( s i z e o f ( Person *) * numPerson ) ;
46 // allocate a buffer to read the names
47 char * name = malloc ( s i z e o f ( char ) * longestName ) ;
48 i n t ind = 0;
49 // read the file again and store the data in the database
50 // return to the beginning of the file
51 fseek ( fptr , 0 , SEEK_SET ) ;
52 while (! feof ( fptr ) )
53 {
54 i n t age ;
55 i f ( fscanf ( fptr , " % d " , & age ) == 1)
56 {
57 // remove the space separating age and name
58 fgetc ( fptr ) ;
59 fgets ( name , longestName , fptr ) ;
60 // remove ’\ n ’
61 char * chptr = strchr ( name , ’\ n ’) ;
62 i f ( chptr != NULL ) // last line may not have ’\ n ’
63 {
64 * chptr = ’ \0 ’;
65 }
278 Intermediate C Programming
11 i f ( argc < 4)
12 {
13 return EXIT_FAILURE ;
14 }
15 PersonDatabase * perdb = Person_read ( argv [1]) ;
16 i f ( perdb == NULL )
17 {
18 return EXIT_FAILURE ;
19 }
20 // Person_print ( perdb ) ;
21 Per son_so rtByNa me ( perdb ) ;
22 // Person_print ( perdb ) ;
23 i f ( Person_write ( argv [2] , perdb ) == 0)
24 {
25 Person_destruct ( perdb ) ;
26 return EXIT_FAILURE ;
27 }
28 Person_sortByAge ( perdb ) ;
29 // Person_print ( perdb ) ;
30 i f ( Person_write ( argv [3] , perdb ) == 0)
31 {
32 Person_destruct ( perdb ) ;
33 return EXIT_FAILURE ;
34 }
35 Person_destruct ( perdb ) ;
36 return EXIT_SUCCESS ;
37 }
The example introduces a new way to debug a program. The intended output should
be a file on a disk. The sorted Person database can be printed to the computer screen
by using a pre-defined FILE pointer called stdout. It means “standard output”. You do
not (and cannot) fopen this pre-defined pointer. It already exists whenever any program
runs. Writing to stdout is precisely what printf does. So you have already been using
stdout since your first C program. The functions Person print and Person write both
call Person writeHelp, which writes the database to a file. Passing stdout means that the
database is written to the terminal.
This is an example of the DRY (Don’t Repeat Yourself) principle. Here we reuse the
same code for saving the data to a file and for printing the same data to the computer screen.
If something is wrong with Person writeHelp or the format of the data needs changes, then
you only need to make changes in one place. This saves a lot of time and reduces the chance
of mistakes. The DRY principle is a characteristic of well written code.
The program calls qsort, passing the array of Person * pointers—each element of the
array is a pointer to a Person object. Therefore, each item’s size is sizeof(Person *). The
two comparison functions, comparebyName and comparebyAge, need some careful thought.
Each argument is the address of an array element. Each element, in turn, is a pointer to a
Person object, i.e., Person *. Thus, the arguments to the comparison functions are pointers
to Person *, i.e., Person * *. A pointer stores a memory address. One of those * simply
means it is a pointer. The rest, Person *, is what is being pointed to. The two arguments
pp1 and pp2 in the comparison functions are Person * *. Inside each function pv1 and
pv2 are the pointers to Person objects, i.e., Person *. Please notice that * has different
meanings in the following statement:
Programming Problems Using Structure 281
1 × 24 + 1 × 22 + 1 × 21 + 0 × 20 (17.2)
Another commonly used number system is the hexadecimal system. Hexadecimal num-
bers are in base sixteen, and thus require sixteen symbols: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B,
C, D, E, F. What does EA29 mean in the hexadecimal system? It means
0b1010 is the number 10. This is an extension to normal C, and may not work in other
compilers. Instead, binary numbers should be expressed as hexadecimal numbers.
When inserting a decimal digit using DecPack insert, the function checks whether data
is full. If it is full, then the size of the data array doubles. The old array is copied to the
new array and the memory for the old array is released. If a byte has not been used, then
the decimal digit uses the upper 4 bits. If the upper 4 bits of a byte are already used, the
decimal digit uses the lower 4 bits. When deleting a decimal digit using DecPack delete,
the function modifies used and returns the most recently inserted decimal digit. The digit’s
Programming Problems Using Structure 283
value must be between 0 and 9 (not ’0’ to ’9’). DecPack delete does not shrink the data
array even if used is zero. The DecPack print function prints the decimal digits stored in
the object. The printed decimal digits should be between ’0’ and ’9’—if the decimal digit
is 0, then ’0’ is printed, if the decimal digit is 1, then ’1’ is printed, and so on. Finally,
DecPack destroy releases the memory.
The bit-wise AND operation is used between two numbers. If the bits from both numbers
are 1, the resultant bit is 1. If one or both bits are zero, then the resultant bit is zero. The
following shows some examples (in binary representation).
0 1 1 0 1 0 0 1
& 1 1 0 1 0 0 1 1
0 1 0 0 0 0 0 1
Sometimes, a program wants to keep some bits while discarding the other bits. For
example, if the program wants to keep only the lower (right) four bits of a byte, then the
program uses bit-wise AND with 0x0F, 0000 1111 in binary.
- - - - a b c d
& 0 0 0 0 1 1 1 1
0 0 0 0 a b c d
It does not matter whether - is 0 or 1, the first (higher, left) four bits of the result will
always be 0. The other four bits: a, b, c, d are either 0 or 1 depending on the values of
a, b, c, and d. This is also called a mask. A mask blocks some bits and allows the other
bits to pass through. If a program wants to check if the leftmost bit is 1 or 0, then it can
use a mask whose binary representation is 1000 0000b (0x80 in hexadecimal). The following
example checks whether the variable a’s leftmost bit is 1 or 0:
1 unsigned char a = 161;
2 unsigned char mask = 0 x80 ;
3 i f (( a & mask ) == mask )
4 {
5 // a ’s leftmost bit is 1
6 }
7 else
8 {
9 // a ’s leftmost bit is 0
10 }
In this example, a & mask equals mask because 161 is greater than 127 and the leftmost
bit must be set to 1. Please note that the following if condition is wrong. It is a common
mistake.
284 Intermediate C Programming
The bit-wise shift left operation moves bits to the left and adds zeros to the right. The
following example shows left-shifting by two. The leftmost two bits are discarded (marked
as -) and two zeros are added to the right:
0 1 1 0 1 0 0 1 << 2
- - 1 0 1 0 0 1 0 0
The next example shows shifting left by four. The leftmost four bits are discarded
(marked as -) and four zeros are added to the right:
1 1 0 1 1 1 1 0 << 4
- - - - 1 1 1 0 0 0 0 0
Shifting left by one is equivalent to multiplying by two. If the result is greater than 255,
then the leftmost bit is discarded.
The bit-wise shift left operation has a complementary bit-wise shift right operation,
moving bits to the right and adding zeros to the left. The following shows an example of
shifting right by two. The rightmost two bits are discarded (marked as -) and two zeros are
added to the left.
0 1 1 0 1 0 0 1 >> 2
0 0 0 1 1 0 1 0 - -
The next example shows shifting right by four. The rightmost four bits are discarded
(marked as -) and four zeros are added to the right.
1 1 0 1 1 1 1 0 >> 4
0 0 0 0 1 1 0 1 - - - -
1 // bits . c
2 # i n c l u d e < stdio .h >
3 # i n c l u d e < stdlib .h >
4 # i n c l u d e < string .h >
5 i n t main ( i n t argc , char * * argv )
6 {
7 unsigned char a = 129; // decimal 129 , hexadecimal 0 X81
8 unsigned char b = 0 XF0 ; // decimal 240
9 unsigned char c = a & b ; // hexadecimal 0 X80 , decimal 128
10 printf ( " %d , % X \ n " , c , c ) ; // 128 , 80
11 unsigned char d = a | b ; // hexadecimal 0 XF1 , decimal 241
12 printf ( " %d , % X \ n " , d , d ) ; // 241 , F1
13 unsigned char e = d << 3; // hexadecimal 0 X88 , decimal 136
14 printf ( " %d , % X \ n " , e , e ) ; // 136 , 88
15 unsigned char f = d >> 2; // hexadecimal 0 X3C , decimal 60
16 printf ( " %d , % X \ n " , f , f ) ; // 60 , 3 C
17 return EXIT_SUCCESS ;
18 }
The output of this program is:
128, 80
241, F1
136, 88
60, 3C
The final bit-wise operation that we will consider is the exclusive or operation, often
abbreviated as XOR. With XOR, the resulting bit is 1 if and only if the two input bits are
different from each other. Here is an illustrative example:
0 1 1 0 1 0 0 1
∧ 1 1 0 1 0 0 0 0
1 0 1 1 1 0 0 1
Please note that ∧ means exclusive or (XOR) in C programs. In some other languages, ∧
means exponential. C uses exp for exponential.
When deleting a digit, used should decrement before the retrieval. Aside from being
symmetric to insertion, this can be understood by working through some examples. Suppose
used is an even number, say 12, then six bytes are used. The last digit is at the 6th byte
and the index is 5. If used decrements first, it becomes 11 and used / 2 is 5. If used is an
odd number, say 9, then we are using five bytes. The last digit is at the 5th byte and the
index is 4. If used decrements first, it becomes 8 and used / 2 is 4. In both cases, used
should decrement before deletion.
112 {
113 i f (( iter % 2) == 0)
114 {
115 printf ( " % d " , ( dp - > data [ iter / 2] >> 4) ) ;
116 }
117 else
118 {
119 printf ( " % d " , ( dp - > data [ iter / 2] & 0 X0F ) ) ;
120 }
121 }
122 printf ( " \ n " ) ;
123 }
124 void DecPack_destroy ( DecPack * dp )
125 {
126 // if the object is empty , do nothing
127 i f ( dp == NULL ) { return ; }
128 // release the memory for the data
129 free ( dp -> data ) ;
130 // release the memory for the object
131 free ( dp ) ;
132 }
This is the main function:
1 // main . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e " decpack . h "
5 i n t main ( i n t argc , char * * argv )
6 {
7 DecPack * dp = DecPack_create (5) ;
8 i n t iter ;
9 f o r ( iter = 0; iter < 21 ; iter ++)
10 {
11 DecPack_insert ( dp , iter % 10) ;
12 }
13 DecPack_print ( dp ) ;
14 f o r ( iter = 0; iter < 7 ; iter ++)
15 {
16 printf ( " delete % d \ n " , DecPack_delete ( dp ) ) ;
17 }
18 DecPack_print ( dp ) ;
19 f o r ( iter = 0; iter < 6 ; iter ++)
20 {
21 DecPack_insert ( dp , iter % 10) ;
22 }
23 DecPack_print ( dp ) ;
24 f o r ( iter = 0; iter < 6 ; iter ++)
25 {
26 printf ( " delete % d \ n " , DecPack_delete ( dp ) ) ;
27 }
28 DecPack_print ( dp ) ;
290 Intermediate C Programming
29 DecPack_destroy ( dp ) ;
30 return EXIT_SUCCESS ;
31 }
Here is the Makefile:
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3 VALGRIND = valgrind -- tool = memcheck -- verbose -- log - file
4
9 clean :
10 / bin / rm -f *. o decpack * log
52 // release memory
53 free ( aptr1 -> data ) ;
54 free ( aptr1 ) ;
55 // read the data from the file
56 Array * aptr2 = NULL ;
57 aptr2 = malloc ( s i z e o f ( Array ) ) ;
58 fptr = fopen ( filename , " r " ) ;
59 i f ( fread ( aptr2 , s i z e o f ( Array ) , 1 , fptr ) != 1)
60 {
61 // fread fail
62 return EXIT_FAILURE ;
63 }
64 // add the data
65 i n t sum = 0;
66 f o r ( ind = 0; ind < ( aptr2 -> length ) ; ind ++)
67 {
68 sum += aptr2 -> data [ ind ];
69 }
70 printf ( " sum = % d \ n " , sum ) ;
71 // release memory
72 free ( aptr2 ) ;
73 return EXIT_SUCCESS ;
74 }
292 Intermediate C Programming
Assume this program runs on a 64-bit (8 bytes) machine and furthermore, that each
integer uses 4 bytes. Also assume that the program never returns EXIT FAILURE. What is
the output of this program? Here is a sample output:
sizeof(arrptr1) = 8
sizeof(arrptr1) = 8, sizeof(Array) = 12
sizeof(arrptr1) = 8, sizeof(arrptr1 -> data) = 8
ftell(fptr) = 12
sum = 1289469162
The value of sum changes if the program is run again. The array’s elements are set to
random values (line 49) after the data has been written to the file (line 36). When line
59 reads the data, the elements’ values should be 0, 1, 2, ..., right? Wrong. If we run this
program with valgrind, it will tell us that the program has an “Invalid read” at line 68.
Why? The reason is that we cannot use fwrite to save the value of a pointer because this
value is a memory address. The address is meaningless when saved into a file. Instead of
saving the address, the program must save the data stored at the address. To summarize, it
makes no sense to write memory addresses to a file; nor does it make sense to read memory
addresses from a file.
Chapter 18
Linked Lists
293
294 Intermediate C Programming
It may be necessary to remove accounts that have not been used for more than one year.
To manage this type of application, we must be able to allocate memory on an as-needed
basis. Memory usage must grow and shrink as the demands of the application require. This
chapter describes how to use dynamic structures. The book covers only the basic concepts
and does not give enough knowledge required to actually build a social network site. The
information in this chapter, however, provides a foundation.
If data structures, such as arrays, need to change size as programs run, we can create
a new larger (or smaller) array, copy the data, and then free the old array. Section 17.2
gives an example of an auto-resizing array: If too many digits are inserted, the array’s size
doubles. This chapter explains how to create a simple data structure that is designed to
grow without copying the existing data. It supports the following functions:
• Insert: add new data, and allocate memory as needed.
• Search: determine whether a piece of data has already been inserted.
• Delete: remove data and release memory if it is no longer needed.
• Print: print the stored data.
• Destroy: delete everything before the program ends.
The simple data structure is called a linked list, and is an example of dynamic structures
and is also an example of container structures. Such structures may contain different types
of data (int, char, Person ...). The code to insert, search, delete, print, and destroy is
quite similar for each different type. The next chapter will describe another type of container
structure called the binary tree.
FIGURE 18.1: A linked list starts empty, i.e., without any Node. This figure shows three
views: (a) the source code view, (b) a diagram, and (c) the stack memory.
one particular view than with other views. The three views are shown simultaneously so
that we can see the relationships between the different representations.
FIGURE 18.2: Creating a Node object whose value is 917. Please note that head is a
pointer.
Fig. 18.2 shows how to create the very first node in a linked list, and assign a number
to the value attribute. Calling malloc will allocate space in heap memory for the two
attributes. Suppose malloc returns address 60000, then this value is assigned to the value
of head. The next line is:
1 head -> next = NULL ;
This line assigns NULL to the node’s next attribute. Then, we assign 917 to the value
attribute, which is the data:
1 head -> value = 917;
Add space before or after -> makes no difference. We create a function List insert that
can make inserting nodes much more straightforward. The function can
• allocate memory for a new node.
• assign an address to the next attribute.
• assign a value to the value attribute.
Fig. 18.3 shows that the function can simplify inserting a node. Calling List insert
with −504 as the argument creates one more list node and it is inserted at the beginning
of the list. Later, we will explain how to insert −504 at the end of the list. It is simpler to
insert nodes onto the front of a linked list. Please note that this means that the value stored
296 Intermediate C Programming
917
(a) Node * head = NULL; (b)
head = List_insert(head, 917); head
at head must change. This is because head must be the newly inserted node that we just
allocated. Note that there is no guarantee that when calling malloc twice we will obtain
consecutive addresses. Fig. 18.4 shows a gap between the memory allocated to the two list
nodes. Fig. 18.5 shows three nodes.
-504 917
(a) Node * head = NULL; (b)
head = List_insert(head, 917); head
head = List_insert(head, -504);
60001 917
60000 NULL
FIGURE 18.4: Calling List insert again to insert another list node.
75001 -504
75000 60000
60001 917
60000 NULL
FIGURE 18.5: Insert the third object by calling List insert again.
60001 917
60000 NULL
75001 -504
75000 60000
60001 917
60000 NULL
FIGURE 18.7: To delete a list node, first create a pointer p that points to the same
memory address as head.
60001 917
60000 NULL
FIGURE 18.8: The function creates another pointer q. Its value is the same as p->next.
Node * p;
(a) p = head; 326 -504 917
Node * q; (b)
q = p -> next; head
p -> next = q -> next; p
q
60001 917
60000 NULL
FIGURE 18.9: Modify p->next to bypass the node that is about to be deleted.
21 {
22 // check w h e t h e r q is NULL before c h e c k i n g q -> value
23 p = p -> next ;
24 q = q -> next ;
25 }
26 i f ( q != NULL )
27 {
28 // delete the node whose value is val
29 p -> next = q -> next ;
30 free ( q ) ;
31 }
Linked Lists 301
32 return head ;
33 }
If head’s value (head -> value) is the same as val, the first node is deleted. This is
achieved by storing head -> next in p at line 14. In this case, the function returns p as the
new head of the list. It is possible that p is set to NULL. This occurs when the list has only
one node and its value is the same as val. After deleting this node, the list is empty.
If head’s value is different from val, then the node to be deleted, if it exists, is after
head somewhere. When we find this node, we must make the previous node point to the
next node. As we will see below, the List delete function uses another pointer q for this
purpose, and its value is p -> next.
Lines 20 to 25 find the node whose value is val. The while loop stops in one of two
conditions: either q is NULL or q -> value is val. To avoid a memory error, the function
must check the first condition before checking the second condition. If q is NULL, then
the second condition is not checked. When q is NULL, the first part of the logical AND
(&&) expression is false and the entire logical AND expression is false. The program does
check whether q -> value is the same as val. This is called short-circuit evaluation and
C programs often rely on it. Lines 23 and 24 move p and q to their next nodes. Since q is
initialized to p -> next, the code inside the entire block always keeps q as p -> next.
What does it means when q is NULL at line 26? It means that no node in the linked
list stores val, and therefore no node needs to be deleted. If q is not NULL, then a node
whose value is val has been located. The function changes p -> next to q -> next. This
bypasses the node q that is about to be deleted. In this method, q is the node to be deleted
and p is the node before q. It is necessary to keep p because it is not possible to go backward
from q to p. The purpose of keeping both p and q is that we cannot go back one node from
q without p. The function then releases the memory pointed to by q. The value stored in q
is still a memory address but that address is no longer valid. Using q’s value after free(q)
will cause segmentation fault. It is also possible to implement List delete with recursion.
Below is a sample implementation. Lines 19 to 31 above can be replaced by a single line as
shown below.
1 Node * L i s t _ d e l e t e 2 ( Node * head , i n t val )
302 Intermediate C Programming
2 {
3 printf ( " delete % d \ n " , val ) ;
4 i f ( head == NULL )
5 {
6 return NULL ;
7 }
8
14 {
15 return ;
16 }
17 L i s t _ d e s t r o y 2 ( head -> next ) ;
18 free ( head ) ; // must be after the r e c u r s i v e call
19 }
The following main function shows how to use the linked list functions we have developed
in this chapter.
1 // file : main . c
2 #i ncl ude " list . h "
3 #i ncl ude < stdlib .h >
4 #i ncl ude < stdio .h >
5 i n t main ( i n t argc , char * argv [])
6 {
7 Node * head = NULL ; /* must i n i t i a l i z e it to NULL */
8 head = L i s t _ i n s e r t( head , 917) ;
9 head = L i s t _ i n s e r t( head , -504) ;
10 head = L i s t _ i n s e r t( head , 326) ;
11 L i s t _ p r i n t( head ) ;
12 head = L i s t _ d e l e t e( head , -504) ;
13 L i s t _ p r i n t( head ) ;
14 head = L i s t _ i n s e r t( head , 138) ;
15 head = L i s t _ i n s e r t( head , -64) ;
16 head = L i s t _ i n s e r t( head , 263) ;
17 L i s t _ p r i n t( head ) ;
18 i f ( L i s t _ s e a r c h ( head , 138) != NULL )
19 {
20 printf ( " 138 is in the list \ n " ) ;
21 }
22 else
23 {
24 printf ( " 138 is not in the list \ n " ) ;
25 }
26 i f ( L i s t _ s e a r c h ( head , 987) != NULL )
27 {
28 printf ( " 987 is in the list \ n " ) ;
29 }
30 else
31 {
32 printf ( " 987 is not in the list \ n " ) ;
33 }
34 head = L i s t _ d e l e t e( head , 263) ; // delete the first Node
35 L i s t _ p r i n t( head ) ;
36 head = L i s t _ d e l e t e( head , 917) ; // delete the last Node
37 L i s t _ p r i n t( head ) ;
38 L i s t _ d e s t r o y ( head ) ; // delete all Nodes
39 return E X I T _ S U C C E S S ;
40 }
The output of this program is:
Linked Lists 305
insert 917
insert -504
insert 326
delete -504
insert 138
insert -64
insert 263
delete 917
19.1 Queues
The function List insert in Section 18.3 always inserts the new value at the beginning
of the list. If we always delete nodes from the front of the list, then the linked list is a
stack. In this problem we change the insert function so that the first inserted value is at
the beginning of the list and the latest inserted value is at the end of the list. If we still
remove elements from the beginning of the list we have created a queue, like a line at a store
waiting for service. The implementation below uses recursion.
1 Node * List_insert ( Node * head , i n t val )
2 {
3 i f ( head == NULL )
4 {
5 return Node_construct ( val ) ;
6 }
7 head -> next = List_insert ( head -> next , val ) ;
8 return head ;
9 }
When the if condition is (when head is NULL), the list is empty. Every recursive call
moves forward by following the next link. This condition can be true if the function has
reached the end of the list. When the frame from the call stack is popped, the previous
node’s next is set to a pointer to the newly created node. For nodes not at the end, line 7
is essentially head -> next = head -> next without changing the list.
You may have noticed that this particular linked list implementation of a queue is not
efficient. Every time a node is inserted, the function has to go through the entire list to
reach the end of the list. This is unavoidable because the program only tracks the beginning
of the list. One solution is to keep track of both the beginning (the head) and the end (the
tail) of the list. This requires two pointers.
Another problem is that the links are uni-directional. When deleting a node, it is nec-
essary to keep track of the node before the node to be deleted. This inconvenience can be
solved by using two links in each node: next and previous. If q is p -> next, then p is
q -> previous. The head of the list has no previous node so its previous points to NULL.
The tail of the list has no next node so its next points to NULL. This is called a doubly linked
list. The structure definition for a node in a doubly linked list is shown below:
307
308 Intermediate C Programming
1 typedef s t r u c t listnode
2 {
3 s t r u c t listnode * next ;
4 s t r u c t listnode * previous ;
5 i n t value ;
6 } Node ;
the element is deleted. If the value is not zero, then the element is added to the new
array.
The diagram below illustrates what happens when we join two sparse arrays. Each
element is expressed by an index–value pair.
index 0 102 315
Array 1
value 5 −5 8
index 11 102 315
Array 2
value 2 5 2
index 0 11 315
Array 1 + Array 2
value 5 2 10
The index 102 is not in the new array because the value becomes zero after the two
arrays are joined. To test this implementation, we want to read sparse arrays from disk.
For simplicity, we can assume that in each input file the indexes are distinct. Each line of
the file contains two integers: index and value. The two integers are separated by space.
The indexes stored in a file are not necessarily sorted. The header file is shown below. The
header file declares four functions:
1 // sparse . h
2 #i f n d e f SPARSE_H
3 #d e f i n e SPARSE_H
4 typedef s t r u c t linked {
5 i n t index ;
6 i n t value ;
7 s t r u c t linked * next ;
8 } Node ;
9 // read a sparse array from a file and return the array
10 // return NULL if reading fails
11 Node * List_read ( char * filename ) ;
12 // write a sparse array to a file
13 // return 1 if success , 0 if fail
14 i n t List_save ( char * filename , Node * arr ) ;
15 // merge two sparse arrays
16 // the two input arrays are not changed and the new array
17 // does not share memory with the input arrays
18 Node * List_merge ( Node * arr1 , Node * arr2 ) ;
19 // release all nodes in a sparse array
20 void List_destroy ( Node * arr ) ;
21 #e n d i f
Below is a sample implementation of these four functions. Even though the indexes from
input files are not sorted, the linked lists are sorted by the indexes.
1 // sparse . c
2 #i n c l u d e " sparse . h "
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 s t a t i c Node * Node_create ( i n t ind , i n t val ) ;
6 s t a t i c Node * List_insert ( Node * head , i n t ind , i n t val ) ;
7 s t a t i c Node * List_copy ( Node * head ) ;
8 Node * List_read ( char * filename )
9 {
310 Intermediate C Programming
61 arr = ptr ;
62 }
63 }
64 s t a t i c Node * Node_create ( i n t ind , i n t val )
65 {
66 Node * nd = malloc ( s i z e o f ( Node ) ) ;
67 i f ( nd == NULL )
68 {
69 return NULL ;
70 }
71 nd -> index = ind ;
72 nd -> value = val ;
73 nd -> next = NULL ;
74 return nd ;
75 }
76 // If the same index appears again , add the value
77 // The returned list is sorted by the index .
78 s t a t i c Node * List_insert ( Node * head , i n t ind , i n t val )
79 {
80 i f ( val == 0) // do not insert zero value
81 {
82 return head ;
83 }
84 i f ( head == NULL )
85 {
86 return Node_create ( ind , val ) ;
87 }
88 i f (( head -> index ) > ind )
89 {
90 // insert the new node before the list
91 Node * ptr = Node_create ( ind , val ) ;
92 ptr -> next = head ;
93 return ptr ;
94 }
95 i f (( head -> index ) == ind )
96 {
97 // merge the nodes
98 head -> value += val ;
99 i f (( head -> value ) == 0)
100 {
101 // delete this node
102 Node * ptr = head -> next ;
103 free ( head ) ;
104 return ptr ;
105 }
106 return head ;
107 }
108 head -> next = List_insert ( head -> next , ind , val ) ;
109 return head ;
110 }
111 Node * List_copy ( Node * arr )
312 Intermediate C Programming
112 {
113 Node * arr2 = NULL ;
114 while ( arr != NULL )
115 {
116 arr2 = List_insert ( arr2 , arr -> index , arr -> value ) ;
117 arr = arr -> next ;
118 }
119 return arr2 ;
120 }
Below is a sample main function that we can use to test our implementation.
1 // main . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e " sparse . h "
5 i n t main ( i n t argc , char ** argv )
6 {
7 i f ( argc != 4)
8 {
9 return EXIT_FAILURE ;
10 }
11 Node * arr1 = List_read ( argv [1]) ;
12 i f ( arr1 == NULL )
13 {
14 return EXIT_FAILURE ;
15 }
16 Node * arr2 = List_read ( argv [2]) ;
17 i f ( arr2 == NULL )
18 {
19 List_destroy ( arr2 ) ;
20 return EXIT_FAILURE ;
21 }
22 Node * arr3 = List_merge ( arr1 , arr2 ) ;
23 i n t ret = List_save ( argv [3] , arr3 ) ;
24 List_destroy ( arr1 ) ;
25 List_destroy ( arr2 ) ;
26 List_destroy ( arr3 ) ;
27 i f ( ret == 0)
28 {
29 return EXIT_FAILURE ;
30 }
31 return EXIT_SUCCESS ;
32 }
Here is a Makefile that combines the compiling and testing.
1 GCC = gcc
2 CFLAGS = -g - Wall - Wshadow
3 LIBS =
4 SOURCES = sparse . c main . c
5 TARGET = main
6 VALGRIND = valgrind -- tool = memcheck -- verbose -- log - file
Programming Problems Using Linked List 313
10 main : $ ( SOURCES )
11 $ ( GCC ) $ ( CFLAGS ) $ ( SOURCES ) -o $@
12 ./ main $ ( TEST0 )
13 diff -w outputs / output0 expected / expected0
14 ./ main $ ( TEST1 )
15 diff -w outputs / output1 expected / expected1
16 $ ( VALGRIND ) = outputs / valgrindlog0 ./ main $ ( TEST0 )
17 $ ( VALGRIND ) = outputs / valgrindlog1 ./ main $ ( TEST1 )
18
19 clean :
20 / bin / rm -f main outputs /*
Let’s assume that we have inputs input0A and input0B as shown below. The expected0
column shows the result of merging the two arrays.
Below is a second set of sample inputs and outputs. Some elements (indexes = 54 and
4019) are eliminated in the output because the values add to zero.
(a)
(b)
FIGURE 19.1: (a) The original linked list. The list’s head points to A. (b) The reversed
linked list. The list’s head points to E.
Programming Problems Using Linked List 315
Fig. 19.2 shows how to reverse a linked list. Suppose the first two nodes have already
been reversed: revhead points to the head of the partially reversed list; orighead points to
the head of the remaining original list; origsec points to the second node of the remaining
list.
(a)
(b)
(c)
(d)
(e)
FIGURE 19.2: (a) Three pointers are used. (b) Change orighead -> next and make it
point to revhead. (c) Update revhead to the new head of the reversed list. (d) Update
orighead to the new head of the remaining list. (e) Update origsec to the second node of
the remaining list.
12 {
13 origsec = orighead -> next ;
14 orighead -> next = revhead ;
15 revhead = orighead ;
16 orighead = origsec ;
17 }
18 return revhead ;
19 }
The order of the four steps inside while is important. It would be wrong if the order
were different. For example, reversing lines 13 and 14 would lose the remaining list because
orighead -> head has already been changed and origsec is the same as revhead. Re-
member to initialize revhead to NULL because it will be the end of the reversed list. It is
unnecessary to initialize origsec because it is orighead -> next before it is used.
Chapter 20
Binary Search Trees
The previous chapter explained linked lists. Each Node has precisely one link called next.
Traversing the list to find a given node means starting at the first node—usually called the
head—and visiting each node in turn. In a doubly linked list, each Node has two links (next
and previous). A doubly linked list allows for traversing forward using next and backward
using previous. Even though doubly linked lists are more convenient, they still have the
same limitations of singly linked lists. If the list is long, then finding particular nodes may
require visiting many nodes. If we want to efficiently add and remove data, a linked list is
insufficient.
Section 15.1 provides an example of quickly locating data in an array by skipping large
portions of it. This is a step in the right direction. Note, however, that the array must be
sorted before a binary search can be applied. Furthermore, the array’s size is fixed. Inserting
an element in an array can be expensive, because there may not be enough memory available.
It is necessary to allocate a new array and copying data before freeing the old array. If the
new element is inserted at the beginning, all the elements must be moved. This is inefficient.
Can a dynamic structure support the efficient searching properties of binary searching,
but still preserve the ability to quickly add and remove elements? Binary search trees are
designed to do precisely that. A binary search tree can typically discard half of the data in
a single comparison. This makes search efficient. A binary search tree is one type of binary
tree. A binary search tree is always a binary tree, but a binary tree may not necessarily
be a binary search tree. We will start exploring binary search trees and then generalize to
binary trees.
Like a linked list, a binary search tree is composed of nodes that are linked together.
The tree is a single root node similar to the head node in a linked list. Every node in a
binary tree has two links called left and right. A binary tree is different from a doubly
linked list. In a doubly linked list, if p -> next is q, then q -> previous must be p. It is
possible to reach p from q and it is also possible to reach q from p. Even though each node
has two links, they form a single chain.
The situation is fundamentally different in binary trees. Although each node has two
links, the links point to distinct nodes. This means that if q is p -> left or p -> right,
neither q -> left nor q -> right is p. It is possible to reach q from p but it is impossible
to reach p from q.
317
318 Intermediate C Programming
The following are some terms used for binary trees. If q is p -> left, then q is called
p’s left child. If r is p -> right, then r is p’s right child. We call p the parent of q and r.
We also say that q and r are siblings. If a node has no child, then it is called a leaf node.
All the nodes on the left side of p are called p’s left subtree. All the nodes on the right side
of p are called p’s right subtree. The top node is called the root of the tree, and the root
can reach every node in the tree.
Fig. 20.1 shows an example of a binary tree. The root stores the value 8. The value 4 is
stored in the left child of the root. The nodes storing 1, 4, 5, and 7 are the left subtree of
the root. The nodes storing 9, 10, 11, 12, and 15 are the right subtree of the root.
The distance of a node from the root is the number of links between the root and the
node. For example, the distance between the node 7 and the root is 2. The height of a
binary tree is one plus the longest distance in the tree. The height of the tree above is 4. In
a full binary tree, each node has either two children or no children. The tree in Fig. 20.1 is
not full because some nodes (nodes 7, 9, and 12) have only one child. In a complete binary
tree, each node, except the nodes just above the leaf nodes, has two children. Fig. 20.1 is
complete but not full.
It is possible for a node in a complete binary tree to have only one child, and thus not
be a full binary tree. It is also possible that a full binary tree is incomplete. If a binary tree
is full and complete and its height is n (i.e., the distance between a leaf node and the root
is n − 1), then the tree has precisely 2n − 1 nodes and 2n−1 leaf nodes. In a balanced binary
tree, the difference between the height of the left subtree and the height of the right subtree
is at most one. A binary tree is degenerated if each node has at most one child. In this case,
the binary tree is essentially a linked list.
We can create tree structures in which each object has three or more links. For example,
many computer games take advantage of octrees where each node has eight children. In this
case, octrees are used to partition three-dimensional space, and are thus useful for indexing
the objects in 3D worlds. This book focuses on binary trees because they are useful for a
wide array of problems.
(if a < b and b < c, then a < c). For example, integers, characters, floating-point
numbers, and strings all support total ordering. Complex numbers are not totally
ordered and cannot be used as the attributes for binary search trees.
• For every node p in a tree, if p has a left child node q, then q -> value must be
smaller than p -> value. Similarly, if p has a right child node r, then r -> value
must be greater than p -> value.
Fig. 20.1 is an example of a binary search tree. Below is the header file for a binary
search tree. It shows the structure definition for a tree node, and gives function declarations
for binary search trees.
1 // tree . h
2 #i f n d e f TREE_H
3 #d e f i n e TREE_H
4 #i n c l u d e < stdio .h >
5 typedef s t r u c t treenode
6 {
7 s t r u c t treenode * left ;
8 s t r u c t treenode * right ;
9 i n t value ;
10 } TreeNode ;
11 // insert a value v to a binary search tree starting
12 // with root , return the new root
13 TreeNode * Tree_insert ( TreeNode * root , i n t v ) ;
14 // search a value in a binary search tree starting
15 // with root , return the node whose value is v ,
16 // or NULL if no such node exists
17 TreeNode * Tree_search ( TreeNode * root , i n t v ) ;
18 // delete the node whose value is v in a binary search
19 // tree starting with root , return the root of the
20 // remaining tree , or NULL if the tree is empty
21 TreeNode * Tree_delete ( TreeNode * root , i n t v ) ;
22 // print the values stored in the binary search tree
23 void Tree_print ( TreeNode * root ) ;
24 // delete every node
25 void Tree_destroy ( TreeNode * root ) ;
26 #e n d i f
A binary search tree has similar functionality to a linked list. For example, both struc-
tures support insert, search, and delete. The differences are the internal organization and
the efficiency of these operations. Note that a linked list can be considered a special case of
a binary tree where every node uses only one link, and the other link is always NULL.
FIGURE 20.2: An empty tree has one pointer called root and its value is NULL; root is
a pointer and it is not a tree node.
FIGURE 20.3: A binary tree with only one tree node. Both left and right are NULL.
This node is called the root because it has no parent. It is also a leaf node because it has
no children.
root 917
(a) Node * root = NULL; (b)
root = List_insert(root, 917); -504
root = List_insert(root, -504);
60002 917
60001 NULL
60000 75000
FIGURE 20.4: A binary tree with two nodes. The node with value 917 remains the root.
It is no longer a leaf node because it has one child. The node with value −504 is a leaf node
because it has no children.
Binary Search Trees 321
75002 -504
75001 NULL
75000 NULL
60002 917
60001 86000
60000 75000
root
(d)
917
-504 1226
73 1085
FIGURE 20.6: A binary tree with five tree nodes. A new view (d) simplifies the repre-
sentation of the tree.
322 Intermediate C Programming
Fig. 20.4 shows the tree after inserting two nodes. Since −504 is smaller than 917, the
second node is inserted to the left of the first node. This is an essential property of binary
search trees. Also, note that the root’s value (the address of the root node) does not change
after inserting the first node. In this example, the value remains 60000.
Fig. 20.5 shows that another tree node is inserted. The value is 1226 and it is larger
than 917. Thus, it is inserted to the right side of the first node. Fig. 20.6 shows a tree with
five tree nodes. The second view in (b) quickly becomes complicated as more nodes are
inserted. The memory view is also getting quite long. Thus, a different representation is
more convenient, as shown in Fig. 20.6 (d). In this view, each tree node is represented as an
oval. Note that “tree”s are drawn upside down, i.e., the root is at the top. This is different
from the trees seen in forests.
The values −504 and 73 are smaller than the value at root, so both tree nodes are at
the left side of root. Because 73 is larger than −504, 73 is at the right side of −504. Binary
search tree has an important property: For any tree node whose value is val, all tree nodes
on the left side have values smaller than val. Furthermore, all tree nodes on the right side
have values larger than val. Fig. 20.7 shows a binary search as values are inserted. The first
inserted value is the tree’s root.
The following code listing gives an implementation Tree insert, as well as an auxiliary
function TreeNode construct. It is static because it should not be called by any function
outside this file. When inserting a new value into a binary search tree, a new leaf node is
created. That means that the new node never has any children. Tree insert is similar to
the insert function in Section 19.1, where a linked list is used as a queue, and the newly
created node is placed at the end.
1 // treeinsert . c
2 #i n c l u d e " tree . h "
Binary Search Trees 323
the left side and the right side of each node have the same number of nodes. In this case,
half of the search space is discarded after each comparison, because we no longer need to
consider half of the nodes in the tree. The following shows how to implement Tree search.
1 // treesearch . c
2 #i n c l u d e " tree . h "
3 TreeNode * Tree_search ( TreeNode * tn , i n t val )
4 {
5 i f ( tn == NULL )
6 {
7 // cannot find
8 return NULL ;
9 }
10 i f ( val == ( tn -> value ) )
11 {
12 // found
13 return tn ;
14 }
15 i f ( val < ( tn -> value ) )
16 {
17 // search the left side
18 return Tree_search ( tn -> left , val ) ;
19 }
20 return Tree_search ( tn -> right , val ) ;
21 }
Note the similarities to the binary search over an array as described in Section 15.1. A
binary search tree is more flexible than a sorted array because a binary search tree supports
efficient insertion and deletion.
When I teach binary search trees, I always get this question: Why do we use binary
search trees (two links per node)? Why don’t we use ternary search trees (three links)? Or
quaternary search trees (4 links)? Earlier, I said the main problem of linked lists (one link
per node) is that finding a node needs to visit many nodes. Do binary search trees solve
this problem? Why?
There is a fundamental difference between one and two. For any positive number n, it is
possible to find a number k (maybe negative or irrational) such that 2k is n. For example,
if n is 0.5, k is −1. If n is 3.7, k is approximately 1.8875. If n is 191.6, k is approximately
7.5819. In contrast, one does not have this property. For any number m, 1m is still one.
What does this mean? It is possible to accomplish something by using two but it cannot be
accomplished by using one. The most important difference between linked lists and binary
trees is that the latter may discard large amounts of data very quickly. This is impossible if
only one link is used. For the same positive number n, it is possible to find another number
p such that 3p is n. Thus, two can do what three can do and vice versa. Moving from one
link (linked list) to two links (binary tree) is a fundamental improvement. However, moving
from two links to three (or four) links is not a fundamental improvement. Ternary search
trees can be better in some scenarios but there is no fundamental advantage.
Binary Search Trees 325
It is important to understand these three traversal methods, because each one is useful
in different circumstances, as will become apparent later in this book. The code listing below
shows an example of printing a tree using pre-order, in-order, and post-order traversals.
1 // treeprint . c
2 #i n c l u d e " tree . h "
3 s t a t i c void TreeNode_print ( TreeNode * tn )
4 {
5 printf ( " % d " , tn -> value ) ;
6 }
7
8 s t a t i c void Tr ee _p ri nt Pr eo rd er ( TreeNode * tn )
9 {
10 i f ( tn == NULL )
11 {
12 return ;
13 }
14 TreeNode_print ( tn ) ;
15 Tr ee _p ri nt Pr eo rd er ( tn -> left ) ;
16 Tr ee _p ri nt Pr eo rd er ( tn -> right ) ;
17 }
18
30 s t a t i c void T re e _ pr i n tP o s t or d e r ( TreeNode * tn )
326 Intermediate C Programming
31 {
32 i f ( tn == NULL )
33 {
34 return ;
35 }
36 T r ee _ p ri n t Po s t or d e r ( tn -> left ) ;
37 T r ee _ p ri n t Po s t or d e r ( tn -> right ) ;
38 TreeNode_print ( tn ) ;
39 }
40
The pattern in in-order traversal is the easiest to see—the values are always visited
in the ascending order. In other words, this is a method of traversing the values in a
sorted ordering. Hence the name in-order traversal. One consequence of this is that in-order
traversal has the same outputs for the three differently shaped trees. Thus in-order traversal
cannot distinguish between different shapes of trees. In contrast, pre-order and post-order
traversals do distinguish the different shapes. If we want to describe the shape of a tree,
then in-order will not work. Pre-order and post-order traversals make this possible.
What are the outputs when printing with different traversal methods on the trees shown
in Fig. 20.9?
Binary Search Trees 327
(a) (b)
(a) (b)
pre-order 8 4 1 7 11 9 8 4 1 7 5 11 9 10 12
in-order 1 4 7 8 9 11 1 4 5 7 8 9 10 11 12
post-order 1 7 4 9 11 8 1 5 7 4 10 9 12 11 8
Answering this question helps visualize the different traversal techniques. To write the
output of a pre-order traversal for (b), write the value of the root first:
It is followed by the outputs of the left subtree and then the right subtree.
The pre-order traversal of the left subtree starts with 4. The pre-order traversal of 4’s
left subtree is 1 and the pre-order traversal of 4’s right subtree is 7 5.
The pre-order traversal of the right subtree starts with 11. The pre-order traversal of
11’s left subtree is 9 10 and the pre-order traversal of 11’s right subtree is 12.
Thus, the output is
8 4 1 7 5 11 9 10 12
(a)
(b)
(c)
FIGURE 20.10: (a) The original binary search tree. (b) Deleting the node “5”. The node
is a leaf node (has no children). This is the first case. The left child of node “7” becomes
NULL. (c) Deleting the node “14”. This node has one child. This is the second case. The
parent of “14” points to the only child of “14”, i.e., “12”.
3. If the node has two children, then find this node’s immediate successor. The immediate
successor is the node that appears immediately after this node in an in-order traversal.
The successor must be on the right side of the node. Exchange the values of the
node with its immediate successor. Then delete the successor. Fig. 20.11 (a) and (b)
illustrates this scenario. Note that the successor cannot not have the left child. Why?
Below is a sample implementation of the delete function. Lines 39–43 finds tn’s imme-
diate successor. The immediate successor is at the right side of tn and hence su starts with
tn -> right. The immediate successor must also be the leftmost node on tn’s right side.
The immediate successor cannot have a left child. Otherwise it would not be the immediate
successor. Note that the immediate successor may have the right child. It is also possible to
use the immediate predecessor but this book uses the successor.
1 // t r e e d e l e t e. c
2 #i ncl ude " tree . h "
3 #i ncl ude < stdlib .h >
4 T r e e N o d e * T r e e _ d e l e t e ( T r e e N o d e * tn , i n t val )
Binary Search Trees 329
(a)
(b)
FIGURE 20.11: (a) The node “8” has two children. This is the third case. Exchange the
values of this node and its successor. The tree temporarily loses its ordering property. (b)
Deleting the node “8” restores the property of the binary search tree.
5 {
6 i f ( tn == NULL ) { return NULL ; }
7 i f ( val < ( tn -> value ) )
8 {
9 tn -> left = T r e e _ d e l e t e( tn -> left , val ) ;
10 return tn ;
11 }
12 i f ( val > ( tn -> value ) )
13 {
14 tn -> right = T r e e _ d e l e t e ( tn -> right , val ) ;
15 return tn ;
16 }
17 // v is the same as tn -> value
18 i f ((( tn -> left ) == NULL ) && (( tn -> right ) == NULL ) )
19 {
20 // tn has no child
21 free ( tn ) ;
22 return NULL ;
23 }
24 i f (( tn -> left ) == NULL )
25 {
26 // tn -> right must not be NULL
27 T r e e N o d e * rc = tn -> right ;
28 free ( tn ) ;
29 return rc ;
30 }
31 i f (( tn -> right ) == NULL )
32 {
33 // tn -> left must not be NULL
330 Intermediate C Programming
34 T r e e N o d e * lc = tn -> left ;
35 free ( tn ) ;
36 return lc ;
37 }
38 // tn have two c h i l d r e n
39 // find the i m m e d i a t e s u c c e s s o r
40 T r e e N o d e * su = tn -> right ; // su must not be NULL
41 while (( su -> left ) != NULL )
42 {
43 su = su -> left ;
44 }
45 // su is tn ’s i m m e d i a t e s u c c e s s o r
46 // swap their values
47 tn -> value = su -> value ;
48 su -> value = val ;
49 // delete su
50 tn -> right = T r e e _ d e l e t e ( tn -> right , val ) ;
51 return tn ;
52 }
After swapping the values, Fig. 20.11 (a) is temporarily not a binary search tree. This is
because the value “8” is now on the right side of “9”, a violation of the binary search tree’s
properties. When “8” is deleted, then the tree becomes a binary search tree again.
A common mistake at line 49 is calling Tree delete(tn, val). This is wrong because
“8” is smaller than “9”. Using Tree delete(tn, val) causes the function to search for
(and attempt to delete) “8” from the left side of “9”. Since “8” is not on the left side, the
function will fail to do anything. If nothing is deleted, the function returns the root tn and
this line becomes tn -> right = tn. A node that is its own parent creates many problems
and furthermore all nodes to the right side (“8”, “11”, and “12”) are lost.
Another common mistake is to write while (su != NULL) at line 40. The while loop
will continue until su is NULL, and the program will have segmentation fault at line 46 when
it attempts to read su -> value.
12 free ( n ) ;
13 }
Note that every node must be destroyed once, and only once. Thus it is necessary to
traverse the tree. Also note that both left and right must be destroyed before this tree
node’s memory is released. Can you tell what type of traversal this is?
20.7 main
Below is a main function that inserts and deletes random values into a binary search
tree:
1 // main . c
2 #i ncl ude " tree . h "
3 #i ncl ude < time .h >
4 #i ncl ude < stdlib .h >
5 #i ncl ude < stdio .h >
6 i n t main ( i n t argc , char * argv [])
7 {
8 T r e e N o d e * root = NULL ;
9 i n t num = 0;
10 i n t iter ;
11 unsigned i n t seed = time ( NULL ) ;
12 seed = 0;
13 srand ( seed ) ;
14 i f ( argc >= 2)
15 {
16 num = ( i n t ) strtol ( argv [1] , NULL , 10) ;
17 }
18 i f ( num < 8)
19 {
20 num = 8;
21 }
22 i n t * array = malloc ( s i z e o f ( i n t ) * num ) ;
23 f o r ( iter = 0; iter < num ; iter ++)
24 {
25 array [ iter ] = rand () % 10000;
26 }
27 f o r ( iter = 0; iter < num ; iter ++)
28 {
29 i n t val = array [ iter ];
30 printf ( " insert % d \ n " , val ) ;
31 root = T r e e _ i n s e r t ( root , val ) ;
32 T r e e _ p r i n t( root ) ;
33 }
34 f o r ( iter = 0; iter < num ; iter ++)
35 {
36 i n t index = rand () % (2 * num ) ;
37 i f ( index < num )
332 Intermediate C Programming
38 {
39 i n t val = array [ index ];
40 printf ( " delete % d \ n " , val ) ;
41 root = T r e e _ d e l e t e ( root , val ) ;
42 T r e e _ p r i n t( root ) ;
43 }
44 }
45 T r e e _ d e s t r o y ( root ) ;
46 free ( array ) ;
47 return E X I T _ S U C C E S S ;
48 }
20.8 Makefile
The following listing is an example Makefile for compiling and running the code under
valgrind.
1 CFLAGS = -g - Wall - W s h a d o w
2 GCC = gcc $ ( CFLAGS )
3 SRCS = t r e e m a i n. c t r e e s e a r c h . c t r e e d e s t r o y . c t r e e i n s e r t. c
4 SRCS += t r e e p r i n t . c t r e e d e l e t e. c
5 OBJS = $ ( SRCS :%. c =%. o )
6
7 tree : $ ( OBJS )
8 $ ( GCC ) $ ( OBJS ) -o tree
9
10 memory : tree
11 v a l g r i n d -- leak - check = yes -- v e r b o s e ./ tree 10
12
13 .c.o:
14 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
15
16 clean :
17 rm -f *. o a . out tree
(a)
(b)
FIGURE 20.12: (a) There are two uniquely shaped binary trees with 2 nodes. (b) There
are five uniquely shaped binary trees with 3 nodes.
n 1 2 3 4 5 6 7 8 9 10
number of shapes 1 2 5 14 42 132 429 1430 4862 16796
TABLE 20.1: The numbers of shapes for binary trees of different sizes.
Suppose there are f (n) shapes for a binary tree with n nodes. First note that by obser-
vation we can tell that f (1) = 1 and f (2) = 2 We can use this as a base case for a recursive
function.
If there are k nodes on the left side of the root node, then there must be n − k − 1 nodes
on the right side or the root node. Here k can be 0, 1, 2, ..., n − 1. By definition, the left
subtree has f (k) possible shapes and the right side has f (n − k − 1) possible shapes. The
shapes on the two sides are independent of each other. This means that for every possible
shape in the left subtree, we count every shape in the right subtree. Thus, if the left subtree
has k nodes, then the total possible number of shapes is: f (k) × f (n − k − 1). The value of
k is between 0 and n − 1 nodes. The total number of shapes is the sum of all the different
possible values of k.
n−1
X
f (n) = f (k) × f (n − k − 1) (20.1)
k=0
Using recursion is easier because we can assume that simpler (smaller) cases have al-
ready been solved. This formula gives the Catalan numbers in Section 15.4. To prove the
equivalence, let us consider the six possible permutations of 1, 2, 3:
1. < 1, 2, 3 >
2. < 1, 3, 2 >
3. < 2, 1, 3 >
4. < 2, 3, 1 >
5. < 3, 1, 2 >
6. < 3, 2, 1 >
Among these six permutations, < 2, 3, 1 > is not stack sortable as shown in Section 15.4.
Thus, five permutations are stack sortable. Next, consider binary search trees that store the
three numbers.
It is not possible to have < 2, 3, 1 > as the result of pre-order traversal of a binary
search tree that stores 1, 2, and 3. This is not a coincidence. Suppose s(n) is the number
of possible stack-sortable permutations of 1, 2, 3, ..., n. It turns out s(n) is the Catalan
334 Intermediate C Programming
FIGURE 20.13: Five different shapes for the pre-order traversals of binary search trees
storing 1, 2, and 3. (a) < 1, 2, 3 >, (b) < 1, 3, 2 >, (c) < 2, 1, 3 >, (d) < 3, 2, 1 >, (e)
< 3, 1, 2 >.
numbers as well. As illustrated by the example above, s(n) ≤ n! because there are n! possible
permutations of 1, 2,..., n. If n > 2, s(n) must be smaller than n!. We have already seen
that s(3) = 5 ≤ 3! = 6
Below is a proof that s(n) defines the sequence of Catalan numbers. Suppose a se-
quence of numbers < a1 , a2 , a3 , ...an > is a particular permutation of 1, 2, ..., n and the
sequence is stack-sortable. Any prefix < a1 , a2 , a3 , ...ak >, k < n must be stack-sortable.
Suppose ai (1 ≤ i ≤ n) is n (the largest number in the entire sequence). Then the sequence
< a1 , a2 , a3 , ...an > can be divided into three parts:
What is the condition that makes a sequence stack-sortable? Section 15.4 explained
that max(a1 , a2 , ..., ai−1 ) must be smaller than min(ai+1 , ai+2 , ..., an ). Therefore, the first
sequence must be a permutation of 1, 2, ..., i − 1 and the second sequence must be a
permutation of i, i + 1, ..., n − 1. Moreover, the two sequences < a1 , a2 , ..., ai−1 > and
< ai+1 , ai+2 , ..., an > must also be stack-sortable; otherwise, the entire sequence cannot be
stack-sortable. Therefore the entire sequence includes two stack-sortable sequences divided
by ai = n. By definition, there are s(i − 1) stack-sortable permutations of 1, 2, 3, ..., i − 1.
There are s(n − i) stack-sortable permutations of i, ..., n − 1. The permutations in these two
sequences are independent so there are s(i − 1) × s(n − i) possible permutations of the two
sequences. The value of i is between 1 and n. When i is 1, the first value in the sequence is
n and this corresponds to the tree in which the root has no left child. When i is n, the last
value is n and this corresponds to the tree in which the root has no right child. Thus, the
total number of stack-sortable permutations is:
n
X n−1
X
s(n) = s(i − 1) × s(n − i) = s(i) × s(n − i − 1). (20.2)
i=1 i=0
Multi-core processors are everywhere: high-end desktops have multiple cores. Even mobile
phones also have multi-core chips. It has become difficult to make individual cores faster, but
adding more cores is easier. More cores do not necessarily mean faster programs, because
many factors affect a computer’s performance. The number of cores is one factor, but the
software is also important. If a program does not take advantage of multiple cores, then
it may as well be running on a single core processor. If a program is written for multiple
cores, then the program can be referred to as a parallel program. If a program is not written
for multiple cores, then it is called a sequential program. All programs so far in this book
are sequential programs. This chapter provides an introduction to writing parallel programs
using threads.
335
336 Intermediate C Programming
21.2 Multi-Tasking
If parallel programs take advantage of multiple cores, then does this mean that parallel
programs cannot run on single-core processors? Yes, they can. The operating system is be-
tween the program and the cores, and provides an abstraction so that programs do not need
to be too concerned about the number of cores. For example, how many computer programs
are running simultaneously on your computer right now? There could be a web browser, a
text editor, a music player, instant messaging, and many other programs, including various
system services.
How many cores does your computer have? One? Two? Four? It is the operating system
that makes sure everything works—every program gets a turn to execute some of its code
in a timely fashion. Specifically, the operating system gives each program a short time
interval (usually several milliseconds) in which one or several cores execute the program
and the program makes some progress. After this interval, the operating system suspends
the program, and then allows another program to run. By giving every program a short
time interval to make some progress, the operating system gives the user the impression that
every program makes progress. If a processor has only one core, then only one program can
run at any given moment. This is called multi-tasking and is analogous to several children
sharing a slide in a playground. Even though only one person can slide down at any moment
(for safety), everyone gets a turn, and everyone enjoys the slide.
In order to improve the overall performance, the operating system may change the
lengths of the time intervals depending on what particular programs are doing. For example,
when a program wants to read data from a file, this program has to wait for the disk to
get the data. During this waiting period, the operating system shortens the program’s time
interval so that another program can use the processor. If a program waits for a user to
enter something on the keyboard, then the operating system also shortens the program’s
time interval while the program is waiting for the user’s input. Shortened intervals also
occur when a program is waiting on data from the network. Because the lengths of the time
intervals can change, it is difficult to predict exactly which program is executing at any
given moment of time.
stored in pthread t. This is similar to the FILE pointer: it stores some information
about an opened file.
2. The second argument is the address of a structure called pthread attr t. This spec-
ifies optional attributes for initializing a thread. If this argument is NULL, the thread
is initialized with default values.
3. The third argument is the name of a function. Section 9.2 explains how to use a
function as an argument to another function (qsort). There are some differences
here. For pthread create, the function’s return type must be void* and the function
takes precisely one argument. For qsort, it expects a function that returns an integer
and the function takes two pointers as arguments.
4. The fourth argument is the argument passed to the function specified in the third
argument.
After calling pthread create, a new thread (the second thread) is created and executes
the function specified in the third argument. The main thread and the new thread may
now run simultaneously (if there are two or more cores). If the program equally divides its
tasks between the first thread and the second thread, then the program can be about twice
as fast. On a single core machine, the two threads take turns and there is no performance
improvement.
The main thread should call pthread join on the second thread before terminating.
This function causes the calling (main) thread to wait until another thread terminates.
After the call to pthread join, there will only be one thread executing. If the main thread
does not wait for the second thread to finish, and the main thread terminates, then the
second thread will also be terminated. Below is a code listing for a simple program creating
one thread:
1 // thread1 . c
2 #i n c l u d e < pthread .h >
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 void * printHello ( void * arg )
6 {
7 i n t * intptr = ( i n t *) arg ;
8 i n t val = * intptr ;
9 printf ( " Hello World ! arg = % d \ n " , val ) ;
10 return NULL ;
11 }
12 i n t main ( i n t argc , char * argv [])
13 {
14 pthread_t second ;
15 i n t rtv ; // return value of pthread_create
16 i n t arg = 12345;
17 rtv = pthread_create (& second , NULL ,
18 printHello , ( void *) & arg ) ;
19 i f ( rtv != 0)
20 {
21 printf ( " ERROR ; pthread_create () returns % d \ n " , rtv ) ;
22 return EXIT_FAILURE ;
23 }
24 rtv = pthread_join ( second , NULL ) ;
25 i f ( rtv != 0)
26 {
338 Intermediate C Programming
This will link the pthread library to the executable program. When the program runs,
it prints the following output:
How does this program work? Let’s start at line 17 where pthread create is called.
The first argument is the address of a pthread t object created at line 14. The second
argument is NULL, and thus the thread is initialized with the default attributes. The third
argument is printHello. This is the address of the printHello function and pthread will
use this address to execute printHello of lines 4 to 10. The function’s return type must be
void *. The function must take one and only one argument and the type must be void *.
The fourth argument to pthread create is the argument that pthreads will in turn pass
to printHello when it calls this function. In this case, it is the address of an integer. If
pthread create succeeds, it returns zero. This indicates that the thread is created normally.
There are now two parallel threads in the program.
Inside printHello, the argument is cast to the correct type. Since line 18 uses the
address of an integer, the correct type is int *. Line 7 dereferences the address to get the
integer value. The printHello has no useful information to return so it returns NULL at line
9. The main thread calls pthread join to wait for the second thread to finish executing
before main terminates. If pthread join is not called, the main thread may terminate and
destroy the second thread before it gets a chance to print anything to the terminal.
29 {
30 printf ( " % d \ n " , kval ) ;
31 i n t ind ;
32 f o r ( ind = 0; ind < num ; ind ++)
33 {
34 printf ( " % d \ n " , arr [ ind ]) ;
35 }
36 }
37 i n t main ( i n t argc , char ** argv )
38 {
39 i f ( argc < 4)
40 {
41 return EXIT_FAILURE ;
42 }
43 i n t numInt = ( i n t ) strtol ( argv [1] , NULL , 10) ;
44 i n t isValid = ( i n t ) strtol ( argv [2] , NULL , 10) ;
45 i n t hasSol = ( i n t ) strtol ( argv [3] , NULL , 10) ;
46 i f (( numInt < 3) || ( numInt > 31) )
47 {
48 return EXIT_FAILURE ;
49 }
50 i f (( hasSol != 0) && ( hasSol != 1) )
51 {
52 return EXIT_FAILURE ;
53 }
54 i f (( hasSol != 0) && ( hasSol != 1) )
55 {
56 return EXIT_FAILURE ;
57 }
58 srand ( time ( NULL ) ) ; // set the seed
59 i n t kval = 0;
60 i n t * arr = malloc ( s i z e o f ( i n t ) * numInt ) ;
61 i n t ind ;
62 // the array is increasing and all elements are distinct
63 arr [0] = rand () % 100;
64 f o r ( ind = 1; ind < numInt ; ind ++)
65 {
66 arr [ ind ] = arr [ ind - 1] + ( rand () % 10000) + 1;
67 }
68 i f ( isValid == 0)
69 {
70 i f (( rand () % 2) == 0)
71 {
72 // make two elements the same
73 arr [ numInt - 1] = arr [0];
74 }
75 else
76 {
77 // make an element negative or zero
78 arr [0] = - ( rand () % 10000) ;
79 }
Parallel Programming Using Threads 341
2n
lim = ∞. (21.1)
n→∞ np
This means that the exponential function eventually grows faster than any polynomial.
There is always a value of n such that 2n is larger than np , no matter what p is. This can
be proved by using the L’Hospital’s Rule from calculus.
This section asks you to write a program that counts the number of subsets whose sums
are equal to the given value k. Instead of finding a sophisticated algorithm, the section uses
a simple solution that enumerates all subsets (excluding the empty set). The program must
read the value of k and then the set’s elements from a file. After reading the data from the
file, the program checks whether the set is valid. The set is invalid if any element is zero
or negative, or if two elements have the same value. If the set is invalid, then the program
does not attempt to solve the subset sum problem.
If the set is valid, then the program generates all possible subsets of the given set. This
program first calculates the number of possible subsets. If a set has n elements, then there
are 2n subsets including the empty set. If each subset is given a number, then the subsets
are numbers between 0 and 2n − 1 inclusively. The empty set is not considered because
k 6= 0, and thus we only need to consider the subsets labeled 1 to 2n − 1. Note that since
each number corresponds to a subset, and we will check to total sum of the numbers in that
subset, we have each number corresponding to a subset sum. The following table explains
how the numbers are related to the subset sums:
Value Sum
1 a1
2 a2
3 a1 + a2
4 a3
5 a1 + a3
6 a2 + a3
7 a1 + a2 + a3
8 a4
.. ..
. .
2n − 1 a1 + a2 + a3 + ... + an
The test generator is restricted to at most 31 elements so that 2n can fit in a four-byte
integer. This following is a sample implementation of the sequential program as described
above. First is the main function:
1 // main . c
2 #i n c l u d e < pthread .h >
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 #i n c l u d e " subsetsum . h "
6 i n t main ( i n t argc , char * argv [])
7 {
8 // read the data from a file
9 i f ( argc < 2)
10 {
11 printf ( " Need input file name \ n " ) ;
12 return EXIT_FAILURE ;
13 }
14 FILE * fptr = fopen ( argv [1] , " r " ) ;
Parallel Programming Using Threads 343
15 i f ( fptr == NULL )
16 {
17 printf ( " fopen fail \ n " ) ;
18 return EXIT_FAILURE ;
19 }
20 i n t numInt = countInteger ( fptr ) ;
21 // go back to the beginning of the file
22 fseek ( fptr , 0 , SEEK_SET ) ;
23 i n t kval ; // the value equal to the sum
24 i f ( fscanf ( fptr , " % d " , & kval ) != 1)
25 {
26 printf ( " fscanf error \ n " ) ;
27 fclose ( fptr ) ;
28 return EXIT_FAILURE ;
29 }
30 numInt - -; // kval is not part of the set
31 i n t * setA = malloc ( s i z e o f ( i n t ) * numInt ) ;
32 i n t ind = 0;
33 f o r ( ind = 0; ind < numInt ; ind ++)
34 {
35 i n t aval ;
36 i f ( fscanf ( fptr , " % d " , & aval ) != 1)
37 {
38 printf ( " fscanf error \ n " ) ;
39 fclose ( fptr ) ;
40 return EXIT_FAILURE ;
41 }
42 setA [ ind ] = aval ;
43 }
44 fclose ( fptr ) ;
45 i f ( isValidSet ( setA , numInt ) == 1)
46 {
47 printf ( " There are % d subsets whose sums are % d \ n " ,
48 subsetSum ( setA , numInt , kval ) , kval ) ;
49 }
50 else
51 {
52 printf ( " Invalid set \ n " ) ;
53 }
54 free ( setA ) ;
55 return EXIT_SUCCESS ;
56 }
This main function calls several other functions that are declared in this header file.
1 // subsetsum . h
2 #i f n d e f SUBSETSUM_H
3 #d e f i n e SUBSETSUM_H
4 #i n c l u d e < stdio .h >
5 i n t subsetEqual ( i n t * setA , i n t sizeA , i n t kval ,
6 unsigned i n t code ) ;
7 // return 1 if the subset expressed by the code sums to kval
344 Intermediate C Programming
1 // countint . c
2 #i n c l u d e < stdio .h >
3 i n t countInteger ( FILE * fptr )
4 {
5 i n t numInt = 0; // how many integers
6 i n t value ;
7 while ( fscanf ( fptr , " % d " , & value ) == 1)
8 {
9 numInt ++;
10 }
11 return numInt ;
12 }
The function subsetSum counts the number of subsets:
Parallel Programming Using Threads 345
1 // sequential . c
2 #i n c l u d e " subsetsum . h "
3 i n t subsetSum ( i n t * setA , i n t sizeA , i n t kval )
4 {
5 unsigned i n t maxCode = 1;
6 unsigned i n t ind ;
7 f o r ( ind = 0; ind < sizeA ; ind ++)
8 {
9 maxCode *= 2;
10 }
11 i n t total = 0;
12 f o r ( ind = 1; ind < maxCode ; ind ++)
13 {
14 total += subsetEqual ( setA , sizeA , kval , ind ) ;
15 }
16 return total ;
17 }
The function subsetEqual determines whether a specific subset sums to the value of k:
1 // subsetequal . c
2 #i n c l u d e < stdio .h >
3 i n t subsetEqual ( i n t * setA , i n t sizeA , i n t kval ,
4 unsigned i n t code )
5 {
6 i n t sum = 0;
7 i n t ind = 0;
8 unsigned i n t origcode = code ;
9 while (( ind < sizeA ) && ( code > 0) )
10 {
11 i f (( code % 2) == 1)
12 {
13 sum += setA [ ind ];
14 }
15 ind ++;
16 code > >= 1;
17 }
18 i f ( sum == kval )
19 {
20 printf ( " equal : sum = %d , code = % X \ n " ,
21 sum , origcode ) ;
22 return 1;
23 }
24 return 0;
25 }
and t threads are used to solve the subset sum program (excluding the main thread). One
solution to distribute the work is to have the first thread check the subsets between 1 and
n n n
b 2t c. The second thread simultaneously checks the subsets between b 2t c + 1 and 2 × b 2t c.
n
It is important to handle the last thread with caution. If t is not a factor of 2 , then the
program must ensure that the thread includes the last set (value is 2n − 1).
The new subsetSum function contains three steps:
1. Create an object as the argument to each thread. This object contains multiple at-
tributes to a function. The attributes are put together into a single structure because
a thread can take only one argument. In this case, each object specifies the range
of subsets checked by the individual thread. The object includes (i) the range of the
subsets to be examined, (ii) the set, (iii) the set’s size, (iv) the value of k, and (v)
the number of subsets whose sums equal to k. It is necessary to give each thread all
relevant information because using global variables is strongly discouraged.
2. Create the threads. Each thread checks some subsets and computes the number of
subsets whose sums equal to k.
3. The main thread waits for every thread to complete and then adds the number subsets
that each thread reports.
The checkRange function is used by each thread, and is an argument of pthread create.
This is a SIMD program because the same function is used in every thread. Below is the
code listing for the subsetSum function using threads:
1 // threaddata . h
2 #i f n d e f THREADDATA_H
3 #d e f i n e THREADDATA_H
4 typedef s t r u c t
5 {
6 unsigned i n t minval ;
7 unsigned i n t maxval ;
8 i n t numSol ;
9 i n t * setA ;
10 i n t sizeA ;
11 i n t kval ;
12 } ThreadData ;
13 #e n d i f
1 // parallel . c
2 #i n c l u d e < pthread .h >
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 #i n c l u d e " threaddata . h "
6 #i n c l u d e " subsetsum . h "
7 #d e f i n e NUMBER_THREAD 16
8 void * checkRange ( void * range )
9 {
10 ThreadData * thd = ( ThreadData *) range ;
11 unsigned i n t minval = thd -> minval ;
12 unsigned i n t maxval = thd -> maxval ;
13 // printf (" minval = %d , maxval = % d \ n " , minval , maxval ) ;
14 unsigned i n t ind ;
15 // caution : need to use <= for max
16 f o r ( ind = minval ; ind <= maxval ; ind ++)
17 {
Parallel Programming Using Threads 347
question, because the execution of this program is unpredictable. When we executed this
program three different times, the program printed three different values:
value is 1
value is -1
value is 0
How can this be possible? If a thread increments and decrements the value before check-
ing, how can it be possible that the value is anything other than zero? It can be even more
surprising when we see the output that includes:
value is 0
This makes no sense because the program should print the message only if the value is
non-zero, based on the condition at line 12.
12 i f ((* intptr ) != 0)
The program prints the value only when *intptr is not zero. However, the program
ends up printing zero. What is wrong with this program? To understand what this means,
we must understand this statement: the operating system may change the lengths of the
time intervals. The operating system gives each thread a short time interval to execute
some code. If the program has multiple threads, then each thread gets some time intervals.
Due to many reasons, the operating system may decide to suspend a thread (or a program)
so that another thread (or another program) can run and make progress. There is no
guarantee when a thread is suspended. The operating system needs to manage all programs
and threads so that no single program or thread can occupy the processor for too long. If
a processor has multiple cores, two or more threads may be executing simultaneously. It
is possible that one thread is executing the machine instructions for line 10, while another
thread is executing the machine instructions for line 12. The microsecond differences in
what hundreds of different programs are doing at different times makes it impossible in
general to predict what any given thread is doing at any given time.
Let us look deeper into how this relates to the program. How specifically does this
make the value of * intptr anything other than zero after the increment and decrement
operations? What happens when the program executes this statement?
10 (* intptr ) ++;
Because intptr stores arg’s address, this statement increments the value stored in arg. To
execute this statement, the computer must do the following:
1. Read the value of arg.
2. Increment the value.
3. Write the new value to arg.
Please note that arg’s value is changed only at the last step. During the first and the
second steps, the value is stored in a temporary location (called register) inside the processor.
Threads may share memory space but they do not share registers. The operating system may
suspend a thread anywhere in these three steps. The following diagram shows one possible
interleaving of the execution of two threads. In this diagram, time progresses downwards.
We use - to indicate that a thread is currently suspended. If thread 1 is suspended right
after arg increments, then the value is 1 when thread 2 reads it. As a result, when thread
2 checks the value, it is not zero and it prints the message. Table 21.1 explains why the
program may print the value 1.
If we change the ordering, Table 21.2 shows why it is possible to see the value 0 printed.
In this scenario, thread 2 is suspended after it checks arg’s value and it is one. When
Parallel Programming Using Threads 351
thread 2 prints the value, it has already been changed to 0. Due to the subtle interleaving
of the threads, it is possible that arg’s value is nonzero when the condition is checked and
is 0 when the value is printed.
Is it possible for the program to print 2? Yes. This is one scenario: Thread 1 increments
and decrements arg and arg is 0 when it is suspended just before the if statement. The
thread enters the if statement, but is suspended before it does any printing. Now thread 2
increments arg and is then suspended. The value is now 1. Thread 3 increments arg and the
thread is then suspended. The value is 2 now. Thread 1 gets another turn on the processor,
and prints the value of arg and it is 2. If the threads always increment before decrementing,
how is it possible to print −1? Consider the scenario in Table 21.3.
How contrived is this example? Do scenarios like this happen when solving real world
problems? It happens almost always.
We can find analogous examples in the real world. For example, when several people try
to purchase tickets for the same flight, the shared variable is the total number of tickets
sold for the flight. If only one seat is available and several people buy the ticket at once,
then the flight is oversold (also called overbooked). How can it be possible to oversell one
flight? Suppose two customers check the flight at almost the same time (reading the shared
variable). The flight still has one seat available and both buy the tickets. Now, the flight
352 Intermediate C Programming
is oversold. Airline companies often do this on purpose because some people buy tickets
but never show up for their flight. It can save money, including the customer, if airlines do
this. Airlines can make reasonable accommodations in the unlikely scenario that everyone
actually checks in for the flight. One common solution is to give a voucher to a volunteer
for taking a later flight.
In some other real world cases, we need a solution that strictly prevents this type of
problem occurring altogether. Consider the following scenario: Two people share a bank
account and the current balance is $900. One day, they go to two ATMs (automatic teller
machine) side-by-side. Each withdraws $100 simultaneously. The two people stand next
to each other and attempt to hit the keys on the ATMs at the same time. The correct
remaining balance should be $700. However, subtle interleaving could make the remaining
balance $800 as illustrated below:
Customer 1 Customer 2 Balance
read balance - $900
- read balance $900
subtract $100 - $900
- subtract $100 $900
write balance - $800
- write balance $800
The bank gives each customer $100 and the remaining balance is $800 so the bank loses
$100. No bank would allow this to happen.
How could the designers of threads allow this to happen? First, it is not a flaw in the
specification of threads. The source of the problem is that there is no simple way to predict
the order in which multiple threads execute their instructions. Thus it allows operating
systems to manage the computer resources more efficiently.
The solution to the problem is to prevent any interleaving of the withdrawal operations.
If two requests come in simultaneously, then one must wait until the other request finishes
in its entirety. The entire withdrawal operation is said to be atomic: it cannot be divided
into parts, i.e., it is irreducible. Threads would not be particularly useful if they did not
support atomic operations, and this is the topic of the next section. Atom comes from the
Greek word atomon which means uncuttable. Such irreducible components of matter have
been hypothesized since at least the beginning of recorded history. Now we know that an
Parallel Programming Using Threads 353
atom can be divided to electrons, neutrons, and protons. Nevertheless, we still use “atomic
operation” to describe a computer operation that cannot be divided.
The following example shows how to lock and unlock a mutex in order to create a critical
section of code.
1 // sync . c
2 #i n c l u d e < pthread .h >
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 #d e f i n e NUMBER_THREAD 16
6 typedef s t r u c t
7 {
8 i n t * intptr ;
9 pthread_mutex_t * mlock ;
10 } ThreadData ;
11
49 i n t val = 0;
50 ThreadData arg ;
51 arg . intptr = & val ;
52 arg . mlock = & mlock ;
53 pthread_t tid [ NUMBER_THREAD ];
54 i n t rtv ; // return value of pthread_create
55 i n t ind ;
56 f o r ( ind = 0; ind < NUMBER_THREAD ; ind ++)
57 {
58 rtv = pthread_create (& tid [ ind ] , NULL ,
59 threadfunc , ( void *) & arg ) ;
60 i f ( rtv != 0)
61 {
62 printf ( " pthread_create () fail % d \ n " , rtv ) ;
63 return EXIT_FAILURE ;
64 }
65 }
66 f o r ( ind = 0; ind < NUMBER_THREAD ; ind ++)
67 {
68 rtv = pthread_join ( tid [ ind ] , NULL ) ;
69 i f ( rtv != 0)
70 {
71 printf ( " pthread_join () fail % d \ n " , rtv ) ;
72 return EXIT_FAILURE ;
73 }
74 }
75 p t h r e a d _ m u t e x _ d e s t r o y (& mlock ) ;
76 return EXIT_SUCCESS ;
77 }
The program has a structure ThreadData that includes two pointers: one for the integer’s
address (i.e., the shared memory) and the other for the mutex’s address. For a critical section
to work as intended, all the threads must attempt to lock and unlock the same mutex.
Hence a pointer to the mutex is passed in ThreadData. If each thread has its own mutex,
then this would be like the library having a key available for every student, even though
there is only one study room. All of them can enter the room and this will create problems.
The critical section includes the code that reads and writes the shared variable. Each
thread obtains the lock by calling pthread mutex lock right after entering the while block.
If the thread cannot lock the mutex (because some other thread is in the critical section),
then that thread will be waiting at pthread mutex lock until the thread can obtain a lock.
The mutex is unlocked by calling pthread mutex unlock at the end of the while block.
The main function creates a single ThreadData object shared by all threads. Before call-
ing pthread mutex lock or pthread mutex unlock, the lock must be initialized by calling
pthread mutex init. This is done in the main function. What is the output of this pro-
gram? Nothing. The if condition in threadfunc is never true and nothing is printed. This
means that all threads keep running indefinitely.
There is much more to say about critical sections of code, and thread synchronization.
This chapter is only an introduction, and covers the most important concepts. Writing
correct multi-threaded programs can be challenging, and developing better tools and pro-
gramming languages for this purpose is an ongoing topic of research. When writing multi-
threaded programs, it is important to identify critical sections and make them atomic.
356 Intermediate C Programming
Applications
357
This page intentionally left blank
Chapter 22
Finding the Exit of a Maze
The following chapters use problems and reference solutions to integrate what you have
learned in earlier chapters. The first problem is to develop a computer algorithm that finds
a way to get out of a maze.
Imagine that you are an adventurer looking for hidden treasure in far-off caves, perhaps
the remnants of a lost civilization. Unfortunately you become trapped in an underground
maze. Only walls are visible, and you must develop a strategy to find the exit. You need to
write a program that finds the path from your current location to the exit.
********* E ***********
* * * * *
* ******* * * * * * *
* * s * * * * * * * *
* * * * *** * * * * *
* * * * * * * * * *
* * * * * * * * * * *
* * * * * * * * * * *
* * * * * * * * * * *
* * * * * *
*********************
A maze is described by a file like the one shown in Table 22.1. This is the input to the
program. The characters represent:
• ’*’: brick
• ’ ’: (space) corridor
359
360 Intermediate C Programming
FIGURE 22.1: Coordinates (row, column). The upper left corner is (0, 0). Moving right
increases the column; moving down increases the row.
This chapter considers only valid mazes meeting the following properties:
• There is one and only one exit.
• There is one and only one starting location.
• There is one and only one route from the starting location to the exit.
• The maze is enclosed by bricks, except for the exit.
For the example in Fig. 22.1, the starting coordinate is (3, 4) and the exit is at (0, 9).
The output of the program is the path from the starting location to the exit, and is printed
in the following format:
Each line is one step and the two numbers give the coordinates that are stepped to.
The maze is very dark and you can see only one step in front of you. You do not know if a
corridor is a dead end until you reach the end. As a result, you may need to move backward
after discovering that a corridor is a dead end.
Consider the sequence of steps below:
Move to (5,19)
Move to (4,19)
Finding the Exit of a Maze 361
Move to (3,19)
Move to (2,19)
Move to (1,19)
Move to (2,19)
Move to (3,19)
Move to (4,19)
Move to (5,19)
Column 19 is the corridor at the right end of the maze. The top of this corridor is cell
(1, 19). After reaching this cell, you discover that it is a dead end. Then you have to turn
around and continue the search for the exit. This is called backtracking. As a result, the
coordinates (2, 19), (3, 19), . . . , are repeated showing backtracking.
This chapter provides an opportunity using several topics covered in this book so far:
• Reading data from a file.
• Creating a structure to hold the data of the maze.
• Allocating memory to store the maze cells.
• Using recursion to move around the maze and find the exit.
18 return EXIT_FAILURE ;
19 }
20 fptr = fopen ( argv [1] , " r " ) ;
21 i f ( fptr == NULL )
22 {
23 printf ( " fopen fail .\ n " ) ;
24 return EXIT_FAILURE ;
25 }
26 numberColumn = 0;
27 do
28 {
29 ch = fgetc ( fptr ) ;
30 switch ( ch )
31 {
32 case ’* ’:
33 numberBrick ++;
34 break ;
35 case ’E ’:
36 exitRow = row ;
37 exitColumn = column ;
38 break ;
39 case ’s ’:
40 startRow = row ;
41 startColumn = column ;
42 break ;
43 }
44 i f ( ch != EOF )
45 {
46 i f ( ch == ’\ n ’)
47 {
48 row ++;
49 numberColumn = column ;
50 column = 0;
51 }
52 else
53 {
54 column ++;
55 }
56 }
57 } while ( ch != EOF ) ;
58 fclose ( fptr ) ;
59 printf ( " The maze has % d rows and % d columns .\ n " ,
60 row , numberColumn ) ;
61 printf ( " The file has % d bricks .\ n " , numberBrick ) ;
62 printf ( " The exit is at (% d , % d ) .\ n " ,
63 exitRow , exitColumn ) ;
64 printf ( " The starting location is at (% d , % d ) .\ n " ,
65 startRow , startColumn ) ;
66 return EXIT_SUCCESS ;
67 }
Finding the Exit of a Maze 363
This is the output of the program when loading the file displayed in Fig. 22.1:
The program can be modified to handle mazes that are not rectangular. To do this,
replace:
49 numberColumn = column ;
with,
49 i f ( numberColumn < column )
50 {
51 numberColumn = column ;
52 }
If a maze is not rectangular, then numberColumn stores the size of the widest row.
To get out of the maze, the program needs to know where the bricks are. The program
creates a two-dimensional array to remember the maze cells. The program uses the following
procedure to read a maze from the file and store the information in a two-dimensional array.
1. Read the file once to determine the size (number of rows and number of columns) of
the maze.
2. Allocate enough memory to store the maze.
3. Use fseek to return to the beginning of the file.
4. Read the file again and store the maze in the allocated memory.
The previous program completes the first step. After finding the maze’s width and
height, a two-dimensional array can be allocated to store each cell of the maze. Section 8.4
explained how to allocate a two-dimensional array.
As illustrated in Fig. 10.1, calling fgetc each time removes one character from the
stream and eventually reaches the end of the file. If the program wants to read the file
again, then the program can call fclose and then fopen again. The second call to fopen
will start from the beginning of the file. Another solution is to use fseek to go back to the
beginning of a file stream. The program then reads the characters from the file again.
1 // readmaze . c
2 // read a maze file and store it in a two - dimensional array
3
18 }
19 fptr = fopen ( argv [1] , " r " ) ;
20 i f ( fptr == NULL )
21 {
22 printf ( " fopen fail .\ n " ) ;
23 return EXIT_FAILURE ;
24 }
25 numberColumn = 0;
26 // get the numbers of rows and columns
27 do
28 {
29 ch = fgetc ( fptr ) ;
30 i f ( ch != EOF )
31 {
32 i f ( ch == ’\ n ’)
33 {
34 row ++;
35 numberColumn = column ;
36 column = 0;
37 }
38 else
39 {
40 column ++;
41 }
42 }
43 } while ( ch != EOF ) ;
44 numberRow = row ;
45 // allocate memory for the mazeArr
46 mazeArr = malloc ( numberRow * s i z e o f ( i n t *) ) ;
47 f o r ( row = 0; row < numberRow ; row ++)
48 {
49 mazeArr [ row ] = malloc ( numberColumn * s i z e o f ( i n t ) ) ;
50 }
51 // return to the beginning of the file
52 fseek ( fptr , 0 , SEEK_SET ) ;
53 // read the file again and fill the two - dimensional array
54 row = 0;
55 column = 0;
56 do
57 {
58 ch = fgetc ( fptr ) ;
59 i f ( ch != EOF )
60 {
61 i f ( ch == ’\ n ’)
62 {
63 row ++;
64 column = 0;
65 }
66 else
67 {
68 mazeArr [ row ][ column ] = ch ;
Finding the Exit of a Maze 365
69 column ++;
70 }
71 }
72 } while ( ch != EOF ) ;
73 fclose ( fptr ) ;
74 printf ( " The mazeArr has % d rows and % d columns .\ n " ,
75 numberRow , numberColumn ) ;
76 f o r ( row = 0; row < numberRow ; row ++)
77 {
78 f o r ( column = 0; column < numberColumn ; column ++)
79 {
80 printf ( " % c " , mazeArr [ row ][ column ]) ;
81 }
82 printf ( " \ n " ) ;
83 }
84 // release the memory
85 f o r ( row = 0; row < numberRow ; row ++)
86 {
87 free ( mazeArr [ row ]) ;
88 }
89 free ( mazeArr ) ;
90 return EXIT_SUCCESS ;
91 }
The program stores the maze in a two-dimensional array called mazeArr. With mazeArr,
it is easier to determine whether a particular cell is a brick by giving the row and column
indexes. There is a fundamental problem, however: Several pieces of information are not
stored anywhere with the array. For example, the size of the maze, the location of the exit,
and the location of the starting point. It is better to create a structure so that all of this
related information can be better organized.
8 #d e f i n e INVALIDSYMBOL ’ - ’
9 typedef s t r u c t
10 {
11 i n t numRow , numCol ; // size of the maze
12 i n t startRow , startCol ; // starting location
13 i n t exitRow , exitCol ; // exit location
14 i n t curRow , curCol ; // current location
15 // brick ? exit ? starting point ? corridor ? 2 - dimensional
16 // array storing the cells
17 i n t * * cells ;
18 } Maze ;
19 // directions , ORIGIN marks the starting point
20 enum { ORIGIN , EAST , SOUTH , WEST , NORTH };
21 // move forward , backward , or found exit alread
22 enum { FORWARD , BACKWARD , DONE };
23 // read the maze from a file
24 Maze * Maze_construct ( char * fileName ) ;
25 // release memory before the program ends
26 void Maze_destruct ( Maze * mz ) ;
27 // print the maze ’s properties ( mainly for debugging )
28 void Maze_print ( Maze * mz ) ;
29 #e n d i f
The following listing gives sample implementations for the functions declared in the
header file.
1 // mazeread . c
2 #i n c l u d e " maze . h "
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 // A static function can be called by another function
6 // in the same file . A static function cannot be called
7 // by any function outside this file .
8 // If ptr is NULL , print an error message and exit
9 s t a t i c void checkMalloc ( void * ptr , char * message ) ;
10 // find the length of a line in a file ( EOF or ’\ n ’)
11 s t a t i c i n t findLineLength ( FILE * fh ) ;
12 // Find the numbers of rows and columns . If the maze is not
13 // rectangular , use the widest row
14 s t a t i c void Maze_findSize ( FILE * fh , i n t * numRow ,
15 i n t * numCol ) ;
16 s t a t i c void checkMalloc ( void * ptr , char * message )
17 {
18 i f ( ptr == NULL ) // malloc fail
19 {
20 printf ( " malloc for % s fail \ n " , message ) ;
21 }
22 }
23 s t a t i c i n t findLineLength ( FILE * fh )
24 {
25 i n t ch ;
26 i n t length = 0;
Finding the Exit of a Maze 367
forward mode
1 1 2 3 1
reach dead end
4
turn around
2 2
backward mode
FIGURE 22.2: Strategy to get out of a maze. Suppose ↑ is north and → is east. A gray
square is a brick. (a) If moving east in step 1 does not reach a dead end, then keep moving
east in step 2. (b) If the corridor has a turn, then follow the turn and keep moving forward.
(c) After encountering a dead end, turn around (i.e., backtrack) and move back along the
corridor.
1 1 1 1
2 2 3 4
FIGURE 22.3: Strategy at an intersection. (a) About to enter an intersection. (b) At the
intersection (marked as “2”), try to go east first. (c) It is a dead end. Turn around and
return to the previous intersection. (d) The mark “2” now becomes “4”, indicating that it
is the fourth visited cell. Since cell “3” is a dead end, it is marked black.
Finding the Exit of a Maze 371
1 1 1 1
4 4 4 4
5 5 5 5
6 6 7 8
FIGURE 22.4: (a) Since east is a dead end, try to go south. (b) Enter another intersection,
marked as “6”. (c) Go east and find that it is another dead end. (d) Turn around to the
previous intersection, now marked as “8”. (This is the eighth move in the sequence of moves.)
The dead end is replaced by black.
1 1 1 1
4 4 4 12
5 5 11
9 8 10
FIGURE 22.5: (a) It is not possible to go south at this intersection. Move west and mark
the cell as “9”. (b) This is another dead end. Turn around and mark the intersection as
“10”. (c) Since both options lead to dead ends, we return to the previous intersection. The
visited cells are marked black. (d) Back at the first intersection.
16
1 1 15
13 12 14
FIGURE 22.6: (a) Going west is an option. (b) It is another dead end. Return to the
previous intersection. (c),(d) All options at this intersection lead to dead ends, so it should
return along the corridor.
That is easy enough. This does not solve the entire problem but it is a good stepping
stone. Good programmers always take small steps toward solutions. What should the func-
tion do if the current location is not the exit? If the current location is not the exit, then
the function checks whether it is possible to move east. If this is possible (because canMove
returns 1), then the function moves east by adding 1 to the column index and calls getOut
again. This is a recursive call. Why should the function be recursive? The reason is that we
adopt the same strategy at each cell, until the exit is found.
1 void getOut ( Maze * mzptr , i n t row , i n t col )
2 {
3 i f (( mzptr -> maze ) [ row ][ col ] == ’E ’) // found exit
4 {
5 printf ( " Found the exit !\ n " ) ;
6 return ;
7 }
8 i f ( canMove ( mzptr , row , col , EAST ) )
9 {
10 getOut ( mzptr , row , col + 1) ;
11 // moving east means adding 1 to the column index
12 }
13 }
What should the function do if moving east is not possible? An example is shown in
Fig. 22.3 (a). In this case, the function tries to go south.
1 void getOut ( Maze * mz , i n t row , i n t col )
2 {
3 i f (( mz -> maze ) [ row ][ col ] == ’E ’) // found exit
4 {
5 printf ( " Found the exit !\ n " ) ;
6 return ;
7 }
8 i f ( canMove ( mz , row , col , EAST ) )
9 {
10 getOut ( mz , row , col + 1) ;
11 // moving east means adding 1 to the column index
12 }
13 i f ( canMove ( mz , row , col , SOUTH ) )
14 {
15 getOut ( mz , row + 1 , col ) ;
16 // moving south means adding 1 to the row index
17 }
18 }
What is the method to determine which of the four possible directions can be taken? The
order of calling canMove determines which direction is considered first. If the order of these
calls is changed, then the function tries another direction first. The other two directions
(west and north) are also checked using the following function:
1 void getOut ( Maze * mzptr , i n t row , i n t col )
2 {
3 i f (( mzptr -> maze ) [ row ][ col ] == ’E ’) // found exit
4 {
5 printf ( " Found the exit !\ n " ) ;
374 Intermediate C Programming
6 return ;
7 }
8 i f ( canMove ( mzptr , row , col , EAST ) )
9 {
10 getOut ( mzptr , row , col + 1) ;
11 // moving east : adding 1 to the column index
12 }
13 i f ( canMove ( mzptr , row , col , SOUTH ) )
14 {
15 getOut ( mzptr , row + 1 , col ) ;
16 // moving south : adding 1 to the row index
17 }
18 i f ( canMove ( mzptr , row , col , WEST ) )
19 {
20 getOut ( mzptr , row , col - 1) ;
21 // moving west : subtracting 1 from the column index
22 }
23 i f ( canMove ( mzptr , row , col , NORTH ) )
24 {
25 getOut ( mzptr , row - 1 , col ) ;
26 // moving north : subtracting 1 from the row index
27 }
28 }
This function implements the basic concepts of our strategy, however, it still needs a
few improvements. A common question from students is whether this function can handle a
corridor with an intersection. To answer this question, we need to understand the difference
between a corridor and an intersection. A corridor is enclosed by bricks on two sides. There-
fore, two of the if conditions are false. At an intersection, more than two if conditions are
true. It is possible to have a four-way intersection as shown in Fig. 22.7. There is nothing
special about corridors and intersections. If it is possible to move in a particular direction,
then the program moves in that direction. Thus, the same code can handle corridors and
intersections because along a corridor only two if conditions are true.
What about dead ends? If it is a dead end, then only one if condition is true.
How does the program determine if a cell is a dead end? Fig. 22.3–22.6 mark visited
cells as bricks. Marking visited cells seems a reasonable approach. If a cell is a dead end,
then the cell should be visited only once. If a cell is a corridor, then the cell may be visited
twice. If a cell is an intersection, then the cell may be visited more than twice. We need
Finding the Exit of a Maze 375
to distinguish between these conditions. If we simply mark a cell as visited, then we may
inadvertently eliminate an intersection. Therefore, the function does not mark cells that
have already been visited. Without marking visited cells, won’t the function revisit the
same cells over and over again and get stuck? Is marking visited cells necessary? Before
answering this question, let’s consider how the function handles dead ends.
Fig. 22.8 shows an example of a dead end. After discovering the dead end, we turn
around and move west. However, after moving one step to the west, the function finds that
it is possible to move east again. After moving east, we find the dead end and turn around
to move west. Again we find we can move east again and do so. This function has problems
because it gets stuck.
FIGURE 22.8: After reaching a dead end, we should turn around and move west. At
location 2, moving east is an option again. We will get stuck here in these two cells and
need a solution to prevent this from happening.
If you take a closer look of Fig. 22.2, you will find something different between Fig. 22.2
and Fig. 22.8. There are dotted lines in Fig. 22.2 as a barrier that prevents moving backward.
How can we write code for this? The function goes east if the two conditions are satisfied:
1. It is not a brick.
2. The previous location was not going west.
These two conditions prevent going back and forth as illustrated in Fig. 22.8. Similarly,
the function considers south if there is no brick to the south, and the previous step was
not north. To make this work, one more argument is needed for the getOut function. This
argument is dir and it tells the function the direction taken in the previous step.
1 void getOut ( Maze * mzptr , i n t row , i n t col , i n t dir )
2 {
3 i f (( mzptr -> maze ) [ row ][ col ] == ’E ’) // found exit
4 {
5 printf ( " Found the exit !\ n " ) ;
6 return ;
7 }
8 i f ( canMove ( mzptr , row , col , EAST ) && ( dir != WEST ) )
9 {
10 // move east if it is not a brick and
11 // the last step was not moving west
12 getOut ( mzptr , row , col + 1 , EAST ) ;
13 }
14 i f ( canMove ( mzptr , row , col , SOUTH ) && ( dir != NORTH ) )
15 {
16 getOut ( mzptr , row + 1 , col , SOUTH ) ;
17 }
376 Intermediate C Programming
previously visited cells because row and col are stored in the frames. This is an example
of using the call stack to store information. How many frames should be popped? When
will this * mode variable be changed back to FORWARD? The answer is after returning to a
location where another direction is possible and has not been taken. This is, by definition,
an intersection. Remember, intersections have at least two if conditions that are true.
In Fig. 22.3–22.6, when moving backward, the visited cells are marked black. This is
unnecessary in the function because the four if conditions already keep track of which
remaining options are available. In Fig. 22.3, number 2 is an intersection and the program
moves east when the first if condition is true. After finding that it is a dead end and turning
around, the function continues from the return location stored in the call stack. This is the
line after the first if condition. When returning to the previous location (now marked as
4), the first if condition has already been tested and will not be tested again.
This can be explained in a different way. Consider the following example:
1 void f ( i n t x , i n t y )
2 {
3 i f ( x == 1)
4 {
5 /* do A */
6 }
7 /* do B */
8 i f ( y == 1)
9 {
10 /* do C */
11 }
12 }
When the function reaches location B, the function will not test x == 1 again and the
function will not execute A again. This is the same even if A calls f itself. In Fig. 22.3, when
returning to the previous location, currently marked 4, the function has already checked
and taken the option of moving east. The function will not check whether moving east is
an option any more. Instead, the function will check the remaining three if conditions for
going south, west, and north. Each if condition uses dir != to prevent going back to the
previous cell. Thus, when an if condition is true, there must be an unexplored direction
and the function changes * mode to FORWARD. The getOut function is changed as follows:
1 void getOut ( Maze * mzptr , i n t row , i n t col ,
2 i n t dir , i n t * mode )
3 {
4 i f (( mzptr -> maze ) [ row ][ col ] == ’E ’)
5 {
6 printf ( " Found the exit !\ n " ) ;
7 return ;
8 }
9 i f ( canMove ( mzptr , row , col , EAST ) && ( dir != WEST ) )
10 {
11 (* mode ) = FORWARD ;
12 getOut ( mzptr , row , col + 1 , EAST , mode ) ;
13 }
14 i f ( canMove ( mzptr , row , col , SOUTH ) && ( dir != NORTH ) )
15 {
16 (* mode ) = FORWARD ;
17 getOut ( mzptr , row + 1 , col , SOUTH , mode ) ;
378 Intermediate C Programming
18 }
19 i f ( canMove ( mzptr , row , col , WEST ) && ( dir != EAST ) )
20 {
21 (* mode ) = FORWARD ;
22 getOut ( mzptr , row , col - 1 , WEST , mode ) ;
23 }
24 i f ( canMove ( mzptr , row , col , NORTH ) && ( dir != SOUTH ) )
25 {
26 (* mode ) = FORWARD ;
27 getOut ( mz , row - 1 , col , NORTH , mode ) ;
28 }
29 (* mode ) = BACKWARD ;
30 }
The rapid growth of digital photography is one of the most important technological changes
in the past fifteen years. Cameras are now standard on mobile phones, tablets, and laptops.
There is also a proliferation of webcams and surveillance cameras. All of these digital images
call for clever applications to improve our lives. For example, social media websites use facial
recognition in order to make it easier to see photos of your friends. Future applications may
be able to determine what people are doing in images, or sequences of images.
This chapter introduces some basics of image processing. The main goal of this chapter
is to explain how to read and write images, as well as how to modify the colors in the
image pixels. For simplicity, this chapter considers only one image format: bitmap (BMP).
BMP files are not normally compressed and the pixels are independently stored. A more
commonly used format is called Joint Photographic Experts Group, also known as JPEG.
JPEG files are compressed using the discrete cosine transform (DCT). This compression
algorithm is beyond the scope of this book.
381
382 Intermediate C Programming
FIGURE 23.1: Example of metadata: the exposure time, the focal length, the time and
the date, etc.
1 // bmpheader . h
2 #i f n d e f _BMPHEADER_H_
3 #d e f i n e _BMPHEADER_H_
4 #i n c l u d e < stdint .h >
5 // tell compiler not to add space between the attributes
6 #pragma pack (1)
7 // A BMP file has a header (54 bytes ) and data
8
9 typedef s t r u c t
10 {
11 uint16_t type ; // Magic identifier
12 uint32_t size ; // File size in bytes
13 uint16_t reserved1 ; // Not used
14 uint16_t reserved2 ; // Not used
15 uint32_t offset ; //
16 uint32_t header_size ; // Header size in bytes
17 uint32_t width ; // Width of the image
18 uint32_t height ; // Height of image
19 uint16_t planes ; // Number of color planes
20 uint16_t bits ; // Bits per pixel
21 uint32_t compression ; // Compression type
22 uint32_t imagesize ; // Image size in bytes
23 uint32_t xresolution ; // Pixels per meter
24 uint32_t yresolution ; // Pixels per meter
25 uint32_t ncolours ; // Number of colors
26 uint32_t importantcolours ; // Important colors
27
28 } BMP_Header ;
29 #e n d i f
This header file introduces several new concepts. The sixth line tells the compiler not to add
any padding between the attributes of a structure. This ensures that the size of a header
Image Processing 383
object is precisely 54 bytes. Without this line, the compiler may align the attributes for
better performance.
Another new concept is including the file <stdint.h>. This file contains definitions of
integer types that are guaranteed to have the same sizes on different machines. The int
type on one machine may have a different size from the int type on another machine. When
reading a 54 byte head from disk, we need to use the same size for the header regardless
of the machine. These types defined in <stdint.h> all have int in them, followed by the
number of bits, and t. Thus, a 32-bit integer is int32 t. If the type is unsigned, then it is
prefixed with a u. An unsigned 16-bit integer is uint16 t.
In the bitmap header structure, some attributes are 16 bits and the others are 32 bits.
They are all unsigned, because none of the attributes can take on negative values. The
order of the attributes is important because the order must meet the bitmap specification.
Reordering the attributes will cause errors. The size of the header is calculated as follows:
Attribute Type Size (Bytes) Cumulative Size (Bytes)
type uint16 t 2 2
size uint32 t 4 6
reserved1 uint16 t 2 8
reserved2 uint16 t 2 10
offset uint32 t 4 14
header size uint32 t 4 18
width uint32 t 4 22
height uint32 t 4 26
planes uint16 t 2 28
bits uint16 t 2 30
compression uint32 t 4 34
imagesize uint32 t 4 38
xresolution uint32 t 4 42
yresolution uint32 t 4 46
ncolours uint32 t 4 50
importantcolours uint32 t 4 54
We cannot store the image pixels in the header struct, because the header has a fixed
size. The header merely tells us how to read the rest of the file. To store the pixels in
memory, we need to use another type of structure. We will call this structure BMP Image,
as shown below.
1 // bmpimage . h
2 #i f n d e f _BMPIMAGE_H
3 #d e f i n e _BMPIMAGE_H
4 #i n c l u d e " bmpheader . h "
5 typedef s t r u c t
6 {
7 BMP_Header header ;
8 unsigned i n t data_size ;
9 unsigned i n t width ;
10 unsigned i n t height ;
11 unsigned i n t bytes_per_pixel ;
12 unsigned char * data ;
13 } BMP_Image ;
14 #e n d i f
A BMP Image includes the header, data size, width and height (duplicated from the
header), the number of bytes per pixel, and a pointer to the pixel data. The data size is
the size of the file after subtracting the size of the header, i.e., sizeof(BMP Header). Even
though sizeof(BMP Header) is 54, it is bad to write 54 directly. The size can be derived from
sizeof(BMP Header). Few people reading the code will know what 54 means, but every C
programmer will instantly understand sizeof(BMP Header). Therefore, you should not use
“54”. The number of bytes per pixel is the number of bits per pixel divided by 8. because
one byte is 8 bits. The following listing shows the header file and an implementation of
reading and saving image files.
1 // bmpfile . h
2 #i f n d e f _BMPFILE_H_
3 #d e f i n e _BMPFILE_H_
4 #i n c l u d e " bmpimage . h "
5 // open a BMP image given a filename
6 // return a pointer to a BMP image if success
7 // returns NULL if failure .
8 BMP_Image * BMP_open ( const char * filename ) ;
9 // save a BMP image to the given a filename
10 // return 0 if failure
11 // return 1 if success
12 i n t BMP_save ( const BMP_Image * image , const char * filename ) ;
13 // release the memory of a BMP image structure
14 void BMP_destroy ( BMP_Image * image ) ;
15 #e n d i f
1 // bmpfile . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e < stdlib .h >
4 #i n c l u d e " bmpfile . h "
5 // correct values for the header
6 #d e f i n e MAGIC_VALUE 0 X4D42
7 #d e f i n e BITS_PER_PIXEL 24
Image Processing 385
8 #d e f i n e NUM_PLANE 1
9 #d e f i n e COMPRESSION 0
10 #d e f i n e BITS_PER_BYTE 8
11
59 }
60 img = malloc ( s i z e o f ( BMP_Image ) ) ;
61 i f ( img == NULL )
62 {
63 return cleanUp ( fptr , img ) ;
64 }
65 // read the header
66 i f ( fread (& ( img -> header ) , s i z e o f ( BMP_Header ) ,
67 1 , fptr ) != 1)
68 {
69 // fread fails
70 return cleanUp ( fptr , img ) ;
71 }
72 i f ( checkHeader (& ( img -> header ) ) == 0)
73 {
74 return cleanUp ( fptr , img ) ;
75 }
76 img -> data_size =
77 ( img -> header ) . size - s i z e o f ( BMP_Header ) ;
78 img -> width = ( img -> header ) . width ;
79 img -> height = ( img -> header ) . height ;
80 img -> bytes_per_pixel =
81 ( img -> header ) . bits / BITS_PER_BYTE ;
82 img -> data =
83 malloc ( s i z e o f ( unsigned char ) * ( img -> data_size ) ) ;
84 i f (( img -> data ) == NULL )
85 {
86 // malloc fail
87 return cleanUp ( fptr , img ) ;
88 }
89 i f ( fread ( img -> data , s i z e o f ( char ) , img -> data_size ,
90 fptr ) != ( img -> data_size ) )
91 {
92 // fread fails
93 return cleanUp ( fptr , img ) ;
94 }
95 char onebyte ;
96 i f ( fread (& onebyte , s i z e o f ( char ) , 1 , fptr ) != 0)
97 {
98 // not at the of the file but the file still has data
99 return cleanUp ( fptr , img ) ;
100 }
101 // everything successful
102 fclose ( fptr ) ;
103 return img ;
104 }
105 i n t BMP_save ( const BMP_Image * img , const char * filename )
106 {
107 FILE * fptr = NULL ;
108 fptr = fopen ( filename , " w " ) ;
109 i f ( fptr == NULL )
Image Processing 387
110 {
111 return 0;
112 }
113 // write the header first
114 i f ( fwrite (& ( img -> header ) , s i z e o f ( BMP_Header ) , 1 ,
115 fptr ) != 1)
116 {
117 // fwrite fails
118 fclose ( fptr ) ;
119 return 0;
120 }
121 i f ( fwrite ( img -> data , s i z e o f ( char ) , img -> data_size ,
122 fptr ) != ( img -> data_size ) )
123 {
124 // fwrite fails
125 fclose ( fptr ) ;
126 return 0;
127 }
128 // everything successful
129 fclose ( fptr ) ;
130 return 1;
131 }
132 void BMP_destroy ( BMP_Image * img )
133 {
134 free ( img -> data ) ;
135 free ( img ) ;
136 }
33 }
34 // detect edges and save the edges in the image
35 pxl = 0;
36 f o r ( row = 0; row < height ; row ++)
37 {
38 pxl += 3; // skip the first pixel in each row
39 f o r ( col = 1; col < width ; col ++)
40 {
41 i n t diff = twoDGray [ row ][ col ] -
42 twoDGray [ row ][ col - 1];
43 // take the absolute value
44 i f ( diff < 0)
45 {
46 diff = - diff ;
47 }
48 i f ( diff > thrshd ) // an edge
49 {
50 // set color to white
51 img -> data [ pxl + 2] = 255;
52 img -> data [ pxl + 1] = 255;
53 img -> data [ pxl ] = 255;
54 }
55 e l s e // not an edge
56 {
57 // set color to black
58 img -> data [ pxl + 2] = 0;
59 img -> data [ pxl + 1] = 0;
60 img -> data [ pxl ] = 0;
61 }
62 pxl += 3;
63 }
64 }
65 f o r ( row = 0; row < height ; row ++)
66 {
67 free ( twoDGray [ row ]) ;
68 }
69 free ( twoDGray ) ;
70 }
the new color. Let x and y be the old and the new colors, then a linear equation has two
coefficients: a and b.
y = ax + b (23.1)
Suppose M and m are the original minimum and the maximum values. They should
become 0 and 255 after the scaling. The following two equations are used to determine the
correct values for a and b.
0 = am + b
(23.2)
255 = aM + b
255 255m
a= and b = − (23.3)
M −m M −m
The code listing below implements this color equalization scheme.
1 // bmpequalize . c
2 #i n c l u d e " bmpfunc . h "
3 void BMP_equalize ( BMP_Image * img )
4 {
5 i n t pxl ;
6 unsigned char redmin = 255;
7 unsigned char redmax = 0;
8 unsigned char greenmin = 255;
9 unsigned char greenmax = 0;
10 unsigned char bluemin = 255;
11 unsigned char bluemax = 0;
12 // find the maximum and the minimum values of each color
13 f o r ( pxl = 0; pxl < ( img -> data_size ) ; pxl += 3)
14 {
15 unsigned char red = img -> data [ pxl + 2];
16 unsigned char green = img -> data [ pxl + 1];
17 unsigned char blue = img -> data [ pxl ];
18 i f ( redmin > red ) { redmin = red ; }
19 i f ( redmax < red ) { redmax = red ; }
20 i f ( greenmin > green ) { greenmin = green ; }
21 i f ( greenmax < green ) { greenmax = green ; }
22 i f ( bluemin > blue ) { bluemin = blue ; }
23 i f ( bluemax < blue ) { bluemax = blue ; }
24 }
25 // calculate the scaling factors
26 // max and min must be different to prevent
27 // divided by zero error
28 double redscale = 1.0;
29 double greenscale = 1.0;
30 double bluescale = 1.0;
31 i f ( redmax > redmin )
32 {
33 redscale = 255.0 / ( redmax - redmin ) ;
34 }
35 i f ( greenmax > greenmin )
36 {
Image Processing 393
Section 19.4 introduced binary trees through the example of binary search trees. This chap-
ter describes another way to use binary trees in a popular compression technique called
Huffman Compression or Huffman Coding. Huffman Coding was developed by David Huff-
man in the early 1950s, while he was still a graduate student at MIT. After more than
60 years, Huffman Coding remains one of the best general-purpose compression algorithms
available, fast and widely used.
It is easier to understand Huffman Coding by comparing it to ASCII. ASCII is a “fixed-
length code” using 8 bits for each letter, even though some letters (such as e and s) are
more common than some others (such as q and z). In contrast, Huffman Compression uses
“variable-length code”. If a letter appears frequently, then it is encoded with fewer bits. If a
letter appears infrequently, then more bits are used. This means that the average length of
all letters is shorter—the information is compressed. Huffman Coding is lossless compression
because the original data can be fully recovered. Lossy compression means the original data
cannot be fully recovered. Lossy compression may achieve higher compression ratios than
lossless compression, and is useful when full recovery of the original data is unnecessary.
Lossy compression is frequently used to compress images and JPEG is an example of lossy
compression. A compression ratio is defined as:
size of uncompressed file
(24.1)
size of compressed file
This chapter uses Huffman Coding to compress articles written in English. Given a set
of letters (or symbols) and their frequencies, Huffman Coding is optimal because Huffman
Coding uses the fewest bits on average.
24.1 Example
Consider an article with only eight different characters: E, N, G, T, g, p, d, and h. To
encode these characters, only 3 bits are sufficient because 23 = 8. That means that each
395
396 Intermediate C Programming
character can be assigned to a unique 3-bit sequence. Next consider the situation when the
letters’ frequencies are quite different, as shown in this table:
character frequencies
E 0.56%
N 1.12%
G 3.93%
T 3.93%
g 10.67%
p 12.92%
d 25.84%
h 41.01%
Consider the following codes (bit sequences) for the characters. We will explain how to
generate these codes (called a “code book”) in the next section.
character code length
E 1110100 7
N 1110101 7
G 111011 6
T 11100 5
g 1111 4
p 110 3
d 10 2
h 0 1
24.2 Encoding
To compress a file, the following steps are needed:
1. Count the frequencies of the characters.
2. Sort the characters by frequencies
3. Build a tree similar to Fig. 24.1.
4. Build the code book.
5. Use the code book to compress the file.
The chapter uses “encoder” and “compressor” interchangeably. Similarly, “decoder” and
“decompresser” are used interchangeably.
The important point is that some characters appear much more frequently than the
others. For example, the letter ’e’ appears 263 times and ’t’ appears 157 times. In contrast,
’z’ appears only once and ’B’ is not in the article at all. Note that there are some invisible
characters. For example, 10 is the newline character (’\n’) and it is used 38 times (there
are 38 lines in the charter). The ASCII code 32 is used for a space character, and it is used
340 times.
Huffman Compression 399
The following code gives a sample implementation for determining the frequency of each
character, and then sorting the characters by their frequencies. This is the header file:
1 // freq . h
2 #i f n d e f FREQ_H
3 #d e f i n e FREQ_H
4 typedef s t r u c t
5 {
6 char value ;
7 i n t freq ;
8 } CharFreq ;
9 // count the frequencies of the letters
10 // NUMLETTER is a constant (128) defined in constant . h
11 // frequencies is an array of NUMLETTER elements
12 // The function returns the number of characters in the file
13 // The function returns 0 if cannot read from the file
14 i n t countFrequency ( char * filename , CharFreq * frequencies ) ;
15 // print the array
16 void printFrequency ( CharFreq * frequencies ) ;
17 // sort the array
18 void sortFrequency ( CharFreq * frequencies ) ;
19 #e n d i f
This listing defines the function implementations.
1 // freq . c
2 #i n c l u d e " constant . h "
3 #i n c l u d e " freq . h "
4 #i n c l u d e < stdio .h >
5 #i n c l u d e < stdlib .h >
400 Intermediate C Programming
(a)
(b)
FIGURE 24.2: (a) The characters are sorted by the frequencies. (b) A linked list is created.
List nodes are expressed by rectangles. Tree nodes are expressed by ovals.
frequencies. Each list node points to a tree node. At the beginning of the algorithm, none
of the tree nodes have children.
Fig. 24.3 shows how to merge the first two list nodes. The first two list nodes, L and R,
are taken. A new tree node N is created and its left and right children are L and R. The
frequency of the newly created tree node is the sum of the frequencies of the two children.
The newly created tree node is not a leaf node so its character is irrelevant. A new list node
is created and points to the newly created tree node. The list nodes must remain sorted in
the ascending order by the tree nodes’ frequencies. These three steps have removed the two
trees from the list with the smallest frequencies, combined them into a single tree, and then
placed the new tree back onto the list, keeping the list sorted by the frequencies.
Fig. 24.4 to Fig. 24.6 repeat the same steps: (i) taking the first two tree nodes, (ii)
creating a parent node (the node’s frequency is the sum of the frequencies of the two
children), (iii) creating a list node, and (iv) inserting the list node and keeping the list
sorted. As a result, two nodes are moved from the list, and one is added, giving a net
change of removing one list node. The linked list becomes shorter.
It is important to understand the concept that is described in the figures. The first part
of the program is described below.
1. A tree structure is created. Each tree node stores a character and the character’s
frequency.
1 // tree . h
2 #i f n d e f TREE_H
3 #d e f i n e TREE_H
4 typedef s t r u c t treenode
5 {
6 s t r u c t treenode * left ;
7 s t r u c t treenode * right ;
402 Intermediate C Programming
(a)
(b)
(c)
FIGURE 24.3: (a) Take the tree nodes, L and R, from the first two list nodes. (b) Create
a tree node N whose left and right children are L and R. (c) Create a new list node pointing
to the newly created tree node. The list nodes are sorted in the ascending order by the tree
nodes’ frequencies.
(a)
(b)
(c)
5. Take the first two nodes from the linked list (the head and the node after the head).
Take the two tree nodes pointed to by these two list nodes. Call these two tree nodes
L and R. Create a new tree node N whose left and right children are L and R. N’s
frequency is the sum of L’s and R’s frequencies. The character stored in this non-leaf
node is irrelevant.
6. Remove the first two list nodes and discard them. They are no longer needed.
7. Create a new list node pointing to N. Insert this list node so that the list nodes remain
sorted by the tree nodes’ frequencies.
Below is the program for building the tree.
1 // constant . h
2 #i f n d e f CONSTATNT_H
3 #d e f i n e CONSTATNT_H
4 #d e f i n e NUMLETTER 128
5 #d e f i n e TEXT 1
6 #d e f i n e BINARY 2
7 #e n d i f
404 Intermediate C Programming
(a)
(b)
FIGURE 24.5: At every step, two tree nodes are removed, combined into a single tree,
and then the new tree is added into the list.
1 // encode . h
2 #i f n d e f ENCODE_H
3 #d e f i n e ENCODE_H
Huffman Compression 405
(a) (b)
1 // tree . c
2 #i n c l u d e " tree . h "
3 #i n c l u d e < stdio .h >
4 #i n c l u d e < stdlib .h >
5 TreeNode * TreeNode_create ( char val , i n t freq )
6 {
7 TreeNode * tn = malloc ( s i z e o f ( TreeNode ) ) ;
8 tn -> left = NULL ;
9 tn -> right = NULL ;
10 tn -> value = val ;
11 tn -> freq = freq ;
12 return tn ;
13 }
14 TreeNode * Tree_merge ( TreeNode * tn1 , TreeNode * tn2 )
15 {
16 TreeNode * tn = malloc ( s i z e o f ( TreeNode ) ) ;
17 tn -> left = tn1 ;
18 tn -> right = tn2 ;
19 tn -> value = 0; // do not care
20 tn -> freq = tn1 -> freq + tn2 -> freq ;
406 Intermediate C Programming
FIGURE 24.7: Now the linked list has only one node. The tree has been built, and it is
in the only remaining list node.
21 return tn ;
22 }
23 // post - order
24 void Tree_print ( TreeNode * tn , i n t level )
25 {
26 i f ( tn == NULL )
27 {
28 return ;
29 }
30 TreeNode * lc = tn -> left ; // left child
31 TreeNode * rc = tn -> right ; // right child
32 Tree_print ( lc , level + 1) ;
33 Tree_print ( rc , level + 1) ;
34 i n t depth ;
35 f o r ( depth = 0; depth < level ; depth ++)
36 {
37 printf ( " ");
38 }
39 printf ( " freq = % d " , tn -> freq ) ;
40 i f (( lc == NULL ) && ( rc == NULL ) )
41 {
42 // a leaf node
43 printf ( " value = %d , ’% c ’" , tn -> value , tn -> value ) ;
44 }
45 printf ( " \ n " ) ;
46 }
Huffman Compression 407
1 // list . c
2 #i n c l u d e " list . h "
3 #i n c l u d e " freq . h "
4 #i n c l u d e < stdlib .h >
5 ListNode * ListNode_create ( TreeNode * tn )
6 {
7 ListNode * ln = malloc ( s i z e o f ( ListNode ) ) ;
8 ln -> next = NULL ;
9 ln -> tnptr = tn ;
10 return ln ;
11 }
12 // head may be NULL
13 // ln must not be NULL
14 // ln -> next must be NULL
15 ListNode * List_insert ( ListNode * head , ListNode * ln )
16 {
17 i f ( head == NULL )
18 {
19 return ln ;
20 }
21 i f ( ln == NULL )
22 {
23 printf ( " ERROR ! ln is NULL \ n " ) ;
24 }
25 i f (( ln -> next ) != NULL )
26 {
27 printf ( " ERROR ! ln -> next is not NULL \ n " ) ;
28 }
29 i n t freq1 = ( head -> tnptr ) -> freq ;
30 i n t freq2 = ( ln -> tnptr ) -> freq ;
31 i f ( freq1 > freq2 )
32 {
33 // ln should be the first node
34 ln -> next = head ;
35 return ln ;
36 }
37 // ln should be after head
38 head -> next = List_insert ( head -> next , ln ) ;
39 return head ;
40 }
41 // frequencies must be sorted
42 ListNode * List_build ( CharFreq * frequencies )
43 {
44 // find the first index whose frequency is nonzero
45 i n t ind = 0;
46 while ( frequencies [ ind ]. freq == 0)
47 {
48 ind ++;
49 }
50 i f ( ind == NUMLETTER )
51 {
408 Intermediate C Programming
52 // no letter appears
53 return NULL ;
54 }
55 // create a linked list , each node points to a tree node
56 ListNode * head = NULL ;
57 while ( ind < NUMLETTER )
58 {
59 TreeNode * tn =
60 TreeNode_create ( frequencies [ ind ]. value ,
61 frequencies [ ind ]. freq ) ;
62 ListNode * ln = ListNode_create ( tn ) ;
63 head = List_insert ( head , ln ) ;
64 ind ++;
65 }
66 return head ;
67 }
68 void List_print ( ListNode * head )
69 {
70 i f ( head == NULL )
71 {
72 return ;
73 }
74 Tree_print ( head -> tnptr , 0) ;
75 List_print ( head -> next ) ;
76 }
The following function implements the concept depicted earlier.
1 // encode . c
2 #i n c l u d e " encode . h "
3 #i n c l u d e " constant . h "
4 #i n c l u d e " freq . h "
5 #i n c l u d e " list . h "
6 #i n c l u d e < stdio .h >
7 #i n c l u d e < strings .h >
8 #i n c l u d e < stdlib .h >
9 i n t encode ( char * infile , char * outfile , i n t mode )
10 {
11 CharFreq frequencies [ NUMLETTER ];
12 // set the array elements to zero
13 bzero ( frequencies , s i z e o f ( CharFreq ) * NUMLETTER ) ;
14 i f ( countFrequency ( infile , frequencies ) == 0)
15 {
16 return 0;
17 }
18 // printFrequency ( frequencies ) ;
19 sortFrequency ( frequencies ) ;
20 // printFrequency ( frequencies ) ;
21 ListNode * head = List_build ( frequencies ) ;
22 i f ( head == NULL )
23 {
24 // the article is empty
Huffman Compression 409
25 return 0;
26 }
27 // merge the top two list nodes until only one list node
28 while (( head -> next ) != NULL )
29 {
30 List_print ( head ) ; printf ( " - - - - - - - - - - -\ n " ) ;
31 ListNode * second = head -> next ;
32 // second must not be NULL , otherwise , will not enter
33 ListNode * third = second -> next ;
34 // third may be NULL
35 // get the tree nodes of the first two list nodes
36 TreeNode * tn1 = head -> tnptr ;
37 TreeNode * tn2 = second -> tnptr ;
38 // remove the first two nodes
39 free ( head ) ;
40 free ( second ) ;
41 head = third ;
42 TreeNode * mrg = Tree_merge ( tn1 , tn2 ) ;
43 ListNode * ln = ListNode_create ( mrg ) ;
44 head = List_insert ( head , ln ) ;
45 }
46 List_print ( head ) ;
47 return 1;
48 }
Line 46 prints the linked list, and it should have only one list node. Otherwise, the
function should continue inside while. Calling Tree print prints the tree nodes using a
post-order traversal. Below is the output. Note that it matches Fig. 24.7.
Fig. 24.8 shows the list of tree nodes as displayed in the debugging program DDD. DDD
can help you visualize how the list nodes and the tree nodes change as the program makes
progress. Fig. 24.9 shows the tree in DDD after the tree has been completely built. It is the
same tree as Fig. 24.7. The visualization function in DDD can help you see the nodes of the
tree.
410 Intermediate C Programming
FIGURE 24.8: A list of tree nodes. This figure shows the list as it is being built. The tree
is the same as the one shown in Fig. 24.4 (c).
Do you notice that only one row (for ’h’) has 0 in the first column? The reason for this
is that there is only one node on the left side of the root. There are seven nodes on the
right side of the root and ones are filled to the first column of seven rows. From the root,
after moving down to the right child, there is only one node on the left side (for ’d’). As a
result, only one row has zero in column 2. The other six rows have ones in column 2.
Fig. 24.10 shows the general rule. If there are n leaf nodes on the left side of a node,
zeros are filled in n rows. The column of the zeros is determined by the distance to the
root. If the node is the root itself, then the first column is used. Similarly, if there are m
leaf nodes on the right side, ones are filled in m rows.
The following functions compute the height of a tree and the number of leaf nodes. To
determine a tree’s height, the function recursively computes the heights of the left subtree
and the right subtree. Then the function chooses the taller of the two subtrees. To determine
the number of leaf nodes, the function increments a counter when a leaf node is encountered.
A leaf node is a node that has no children nodes.
1 s t a t i c i n t Tree _heigh tHelpe r ( TreeNode * tn , i n t height )
2 {
412 Intermediate C Programming
(a) (b)
FIGURE 24.10: If there are n leaf nodes on the left side, zeros should be filled in n rows.
The column is determined by the distance from the root.
3 i f ( tn == 0)
4 {
5 return height ;
6 }
7 i n t lh = Tre e_heig htHel per ( tn -> left , height + 1) ;
8 i n t rh = Tre e_heig htHel per ( tn -> right , height + 1) ;
9 i f ( lh < rh )
10 {
11 return rh ;
12 }
13 i f ( lh > rh )
14 {
15 return lh ;
16 }
17 return lh ;
18 }
19
20 i n t Tree_height ( TreeNode * tn )
21 {
22 return Tree _heigh tHelpe r ( tn , 0) ;
23 }
24
36 (* num ) ++;
37 return ;
38 }
39 Tree_leafHelper ( lc , num ) ;
40 Tree_leafHelper ( rc , num ) ;
41 }
42
43 i n t Tree_leaf ( TreeNode * tn )
44 {
45 i n t num = 0;
46 Tree_leafHelper ( tn , & num ) ;
47 return num ;
48 }
The following listing is part of the encode function after the code tree has been built.
1 // the linked list is no longer needed
2 TreeNode * root = head -> tnptr ;
3 free ( head ) ;
4
3 {
4 i f ( tn == NULL )
5 {
6 return ;
7 }
8 // is it a leaf node ?
9 TreeNode * lc = tn -> left ;
10 TreeNode * rc = tn -> right ;
11 i f (( lc == NULL ) && ( rc == NULL ) ) // it is a leaf node
12 {
13 // finish one code
14 codebook [* row ][0] = tn -> value ; // the character
15 (* row ) ++; // finish one row
16 return ;
17 }
18 i f ( lc != NULL )
19 {
20 // populate this column of the entire subtree
21 i n t numRow = Tree_leaf ( lc ) ;
22 i n t ind ;
23 f o r ( ind = * row ; ind < (* row ) + numRow ; ind ++)
24 {
25 codebook [ ind ][ col ] = 0;
26 }
27 b u il d C od e B oo k H el p e r ( lc , codebook , row , col + 1) ;
28 }
29 i f ( rc != NULL )
30 {
31 i n t numRow = Tree_leaf ( rc ) ;
32 i n t ind ;
33 f o r ( ind = * row ; ind < (* row ) + numRow ; ind ++)
34 {
35 codebook [ ind ][ col ] = 1;
36 }
37 b u il d C od e B oo k H el p e r ( rc , codebook , row , col + 1) ;
38 }
39 }
40 void buildCodeBook ( TreeNode * root , i n t * * codebook )
41 {
42 i n t row = 0;
43 // column start at 1 , column = 0 stores the character
44 b u il d C od e B oo k H el p e r ( root , codebook , & row , 1) ;
45 }
46
54 i n t col = 1;
55 // print the code
56 while ( codebook [ row ][ col ] != -1)
57 {
58 printf ( " % d " , codebook [ row ][ col ]) ;
59 col ++;
60 }
61 printf ( " \ n " ) ;
62 }
63 }
After the code book has been built, it is printed. Below is the output of printCodeBook.
h: 0
d: 1 0
p: 1 1 0
T: 1 1 1 0 0
E: 1 1 1 0 1 0 0
N: 1 1 1 0 1 0 1
G: 1 1 1 0 1 1
g: 1 1 1 1
(d)
FIGURE 24.11: The expressions for the code trees are (a) 1a1b00, (b) 1a1b1c000, (c)
1a1b01c1d000, (d) 1a1b01c1d1e0000. For each tree, the number of 1s is the same as the
number of leaf nodes. The number of 0s is one plus the number of non-leaf nodes.
Character index
E 4
G 6
N 5
T 3
d 1
g 7
h 0
p 2
The listing below shows a sample implementation that makes the header and compresses
the data.
1 // compress . c
2 #i n c l u d e < stdio .h >
3 #i n c l u d e " tree . h "
4 #i n c l u d e " constant . h "
5 s t a t i c void Tre e_head erHelp er ( TreeNode * tn , FILE * outfptr )
6 {
7 i f ( tn == NULL )
8 {
9 return ; // should not get here
10 }
11 TreeNode * lc = tn -> left ;
12 TreeNode * rc = tn -> right ;
13 i f (( lc == NULL ) && ( rc == NULL ) )
14 {
15 // leaf node
16 fprintf ( outfptr , " 1% c " , tn -> value ) ;
17 return ;
Huffman Compression 417
18 }
19 Tre e_head erHelp er ( lc , outfptr ) ;
20 Tre e_head erHelp er ( rc , outfptr ) ;
21 fprintf ( outfptr , " 0 " ) ;
22 }
23 void Tree_header ( TreeNode * tn , char * outfile )
24 {
25 FILE * outfptr = fopen ( outfile , " w " ) ;
26 i f ( outfptr == NULL )
27 {
28 return ;
29 }
30 Tre e_head erHelp er ( tn , outfptr ) ;
31 fprintf ( outfptr , " 0\ n " ) ;
32 fclose ( outfptr ) ;
33 }
34 i n t compress ( char * infile , char * outfile ,
35 i n t * * codebook , i n t * mapping )
36 {
37 FILE * infptr = fopen ( infile , " r " ) ;
38 i f ( infptr == NULL )
39 {
40 return 0;
41 }
42 FILE * outfptr = fopen ( outfile , " a " ) ; // append
43 i f ( outfptr == NULL )
44 {
45 fclose ( outfptr ) ;
46 return 0;
47 }
48 while (! feof ( infptr ) )
49 {
50 i n t onechar = fgetc ( infptr ) ;
51 i f ( onechar != EOF )
52 {
53 i n t ind = mapping [ onechar ];
54 i n t ind2 = 1;
55 while ( codebook [ ind ][ ind2 ] != -1)
56 {
57 fprintf ( outfptr , " % d " , codebook [ ind ][ ind2 ]) ;
58 ind2 ++;
59 }
60 }
61 }
62 fclose ( infptr ) ;
63 fclose ( outfptr ) ;
64 return 1;
65 }
66 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
67 // continue encode ...
68 Tree_header ( root , outfile ) ;
418 Intermediate C Programming
ENNGGGGGGGTTTTTTTgggggggggggggggggggpppppppppppppppppppppppd
dddddddddddddddddddddddddddddddddddddddddddddhhhhhhhhhhhhhhh
hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
1h1d1p1T1E1N01G001g00000
1110100111010111101011110111110111110111110111110111110111110
1111100111001110011100111001110011100111111111111111111111111
1111111111111111111111111111111111111111111111111111110110110
1101101101101101101101101101101101101101101101101101101101101
0101010101010101010101010101010101010101010101010101010101010
1010101010101010101010101010100000000000000000000000000000000
000000000000000000000000000000000000000000
There is only one newline ’\n’, which appears after the header. Line breaks are added
so that the very long sequence of 1s and 0s can fit on this page. Please notice that the
header does not include the characters’ frequencies because this information is unnecessary
for decoding.
bit and a character into a single byte. Note, however, that if the command bit is 0, then
the rest of the header needs to be shifted right by one bit. If some bits in the last byte are
unused, then these bits are zero. The header in the ASCII format is:
1h1d1p1T1E1N01G001g00000
If each letter uses 7 bits, then the header in the binary format becomes:
For clarity, we have added space between every byte. The first four bytes are generated
by the following rules:
1. The letter ’h’ is 0x68 in hexadecimal and the 7-bit representation is 1101000. The first
byte is 11101000.
2. The letter ’d’ is 0x64 and the 7-bit representation is 1100100. The second byte is
11100100.
3. The letter ’p’ is 0x70 and the 7-bit representation is 1110000. The third byte is
11110000.
4. The letter ’T’ is 0x54 and the 7 bit representation is 1010100. The fourth byte is
11010100.
The first zero appears after ’N’ and it is the seventh byte. This zero is followed by
1. Thus, the first two bits are 01. The next character is ’G’. It is 0x47 and the 7-bit
representation is 01000111. Only six bits can be accommodated in the seventh byte and
this byte is 010100011. The remaining (rightmost) bit enters the leftmost part of the eighth
byte. The following table shows the bits for the header:
C: command D: data U: Unused
1 1 1 0 1 0 0 0 1 1 1 0 0 1 0 0
C ← ’h’ → C ← ’d’ →
1 1 1 1 0 0 0 0 1 1 0 1 0 1 0 0
C ← ’p’ → C ← ’T’ →
1 1 0 0 0 1 0 1 1 1 0 0 1 1 1 0
C ← ’E’ → C ← ’N’ →
0 1 1 0 0 0 1 1 1 0 0 1 1 1 0 0
C C ← ’G’ → C C C ← ’g’
1 1 1 0 0 0 0 0
→ C C C C U
One major problem of using bits is that the minimum unit of memory in C is a byte
(unsigned char). The following is a function for writing one bit to a file. This function
accumulates 8 bits in a buffer, and then writes to the file. It is called whenever a bit needs
to be written to the file. The function uses curbyte to keep all of the written bits and when
8 bits have been sent, the function writes a single byte to the file.
1 #i n c l u d e " utility . h "
2 // function for debugging purpose
3 s t a t i c void printByte ( unsigned char onebyte )
4 {
5 unsigned char mask = 0 x80 ;
6 while ( mask > 0)
7 {
8 printf ( " % d " , ( onebyte & mask ) == mask ) ;
9 mask > >= 1;
420 Intermediate C Programming
10 }
11 printf ( " \ n " ) ;
12 }
13 // write one bit to a file
14 //
15 // whichbit indicates which bit this is written to
16 // (0 means leftmost , 7 means rightmost )
17 //
18 // curbyte is the current byte
19 //
20 // if whichbit is zero , curbyte is reset and bit is put
21 // to the leftmost bit
22 //
23 // when whichbit reaches 7 , this byte is written to the
24 // file and whichbit is reset
25 //
26 // the function returns 1 if a byte is written to the file
27 // returns 0 if no byte is written
28 // -1 if it tries to write and fails
29 i n t writeBit ( FILE * fptr , unsigned char bit ,
30 unsigned char * whichbit ,
31 unsigned char * curbyte )
32 {
33 i f ((* whichbit ) == 0)
34 {
35 // reset
36 * curbyte = 0;
37 }
38 // shift the bit to the correct location
39 unsigned char temp = bit << (7 - (* whichbit ) ) ;
40 * curbyte |= temp ; // store the data
41 i n t value = 0;
42 i f ((* whichbit ) == 7)
43 {
44 i n t ret ;
45 ret = fwrite ( curbyte , s i z e o f ( unsigned char ) , 1 , fptr ) ;
46 // printByte (* curbyte ) ; // for debugging
47 i f ( ret == 1)
48 {
49 value = 1;
50 }
51 else
52 {
53 value = -1;
54 }
55 }
56 * whichbit = ((* whichbit ) + 1) % 8;
57 return value ;
58 }
Huffman Compression 421
This function writes the tree (the header of the file) to the file. When a leaf node is
visited, one command bit (value is 1) is written to the file, followed by the 7 bits of the
character. When a non-leaf node is visited, one commend bit (value is 0) is written to the
file. If some bits of the last byte are not used, these bits are set to zero. The new line
character ends the header.
1 // print the 7 bits of an ASCII character
2 s t a t i c void char2bits ( FILE * outfptr , i n t ch ,
3 unsigned char * whichbit ,
4 unsigned char * curbyte )
5 {
6 unsigned char mask = 0 x40 ; // only 7 bits
7 while ( mask > 0)
8 {
9 writeBit ( outfptr , ( ch & mask ) == mask ,
10 whichbit , curbyte ) ;
11 mask > >= 1;
12 }
13 }
14 s t a t i c void Tre e_head erHelp er ( TreeNode * tn , FILE * outfptr ,
15 unsigned char * whichbit ,
16 unsigned char * curbyte )
17 {
18 i f ( tn == NULL )
19 {
20 return ;
21 }
22 TreeNode * lc = tn -> left ;
23 TreeNode * rc = tn -> right ;
24 i f (( lc == NULL ) && ( rc == NULL ) )
25 {
26 // leaf node
27 writeBit ( outfptr , 1 , whichbit , curbyte ) ;
28 char2bits ( outfptr , tn -> value , whichbit , curbyte ) ;
29 return ;
30 }
31 Tre e_head erHelp er ( lc , outfptr , whichbit , curbyte ) ;
32 Tre e_head erHelp er ( rc , outfptr , whichbit , curbyte ) ;
33 writeBit ( outfptr , 0 , whichbit , curbyte ) ;
34 }
35 void Tree_header ( TreeNode * tn , char * outfile )
36 {
37 FILE * outfptr = fopen ( outfile , " w " ) ;
38 i f ( outfptr == NULL )
39 {
40 return ;
41 }
42 unsigned char whichbit = 0;
43 unsigned char curbyte = 0;
44 Tre e_head erHelp er ( tn , outfptr , & whichbit , & curbyte ) ;
45 while ( whichbit != 0)
422 Intermediate C Programming
46 {
47 // if the current byte has unused bits
48 writeBit ( outfptr , 0 , & whichbit , & curbyte ) ;
49 }
50 unsigned char newline = ’\ n ’; // add ’\ n ’ at the end
51 fwrite (& newline , s i z e o f ( unsigned char ) , 1 , outfptr ) ;
52 fclose ( outfptr ) ;
53 }
This is the compress function. It writes the bits to the file. If some bits of the last byte
is unused, these bits are zero.
1 i n t compress ( char * infile , char * outfile ,
2 i n t * * codebook , i n t * mapping )
3 {
4 FILE * infptr = fopen ( infile , " r " ) ;
5 i f ( infptr == NULL )
6 {
7 return 0;
8 }
9 FILE * outfptr = fopen ( outfile , " a " ) ; // append
10 i f ( outfptr == NULL )
11 {
12 fclose ( outfptr ) ;
13 return 0;
14 }
15 unsigned char whichbit = 0;
16 unsigned char curbyte = 0;
17 while (! feof ( infptr ) )
18 {
19 i n t onechar = fgetc ( infptr ) ;
20 i f ( onechar != EOF )
21 {
22 i n t ind = mapping [ onechar ];
23 i n t ind2 = 1;
24 while ( codebook [ ind ][ ind2 ] != -1)
25 {
26 writeBit ( outfptr , ( codebook [ ind ][ ind2 ] == 1) ,
27 & whichbit , & curbyte ) ;
28 ind2 ++;
29 }
30 }
31 }
32 while ( whichbit != 0)
33 {
34 // if the current byte has unused bits
35 writeBit ( outfptr , 0 , & whichbit , & curbyte ) ;
36 }
37 fclose ( infptr ) ;
38 fclose ( outfptr ) ;
39 return 1;
40 }
Huffman Compression 423
After writing the code book, the header uses the next 4 bytes (32 bits) to write the
length of the article. This is an unsigned integer and can be as large as 232 − 1, more than
four billion. A 1000-page novel has about 500,000 words, a few million characters. Thus, 32
bits are sufficient. After the length, a new line character is written to the file, signifying the
end of the header.
The output file cannot be viewed easily in a text editor, because the data is compressed
in bit representations. We can use the xxd program in Linux to see the hexadecimal values
that represent each byte in the file. The following shows the compressed file in hexadecimal
format using xxd.
0000000: e8e4 f0d4 c5ce 639c e0b2 0000 000a e9d7 ......c.........
0000010: af7d f7df 7df7 ce73 9ce7 3fff ffff ffff .}..}..s..?.....
0000020: ffff ffff 6db6 db6d b6db 6db6 d555 5555 ....m..m..m..UUU
0000030: 5555 5555 5555 5554 0000 0000 0000 0000 UUUUUUUT........
0000040: 00
24.3 Decoding
Decoding is the reverse of encoding. The decoder first reconstructs the code tree from
the file header, and then reads the compressed codes of the characters. From the codes, the
decompresser traverses the code tree and outputs the characters stored in the tree’s leaf
nodes. To reconstruct the tree, the decoder needs to know how the tree is represented in the
header. In our case, the code book is encoded using the rules described in Section 24.2.5.
The header contains both commands (1 bit, either 0 or 1) and characters (7 bits). To build
the code tree, the decoder does the following:
• The first bit is a command bit and it is always 1.
• If a command is 1, then the next 7 bits are the value stored in a leaf node. Create a
tree node to store this value. Add this tree node to the beginning of the list. This tree
node is a single-node tree.
• If a command is 0 and the list has only one node, then the complete tree has been
built. If a command is 0 and the list has two or more nodes, then take the first two
nodes from the list, create a tree node as the parent. Add this parent node to the list.
• After the tree is completely built, then read one more bit. If this is not the last
(rightmost) bit of the byte, discard the remaining bits in the byte. The next four
424 Intermediate C Programming
bytes (an unsigned int) store the number of characters in the article. This number
is followed by a new line ’\n’ character.
Consider the header in Section 24.2.6. The decoder reads one bit from the compressed
file. This bit is 1 and then the decoder reads another 7 bits. Fig. 24.12 to Fig. 24.15 show
how to reconstruct the tree from the header. The first character is ’h’. One tree node is
created and it is pointed to by one list node as shown in Fig. 24.12 (a). For decoding, as
long as the tree can be rebuilt, the frequencies of the characters are not needed. Fig. 24.12
(b) shows the list after reading the first two bytes. Fig. 24.12 (c) shows the list after reading
the first six bytes. In Fig. 24.12 (d), the first two tree nodes share the same parent. The
first tree node becomes the right child and the second tree node is the left child, making E
and N share a parent node. This is because the code book was encoded using a post-order
traversal. Please notice the symmetry between this figure and Fig. 24.3. In Fig. 24.12 (e),
this command is followed by 7 bits of data for the character G. Another tree node for G is
added to the list.
(a) (b)
(c)
(d)
(e)
FIGURE 24.12: (a) One tree node is added after reading the first command and the first
character. (b) After reading two bytes. (c) After reading six bytes. (d) The first bit in the
seventh byte is a command and it is 0. (e) The next command bit (the second bit in the
seventh byte) is 1.
Huffman Compression 425
(a)
(b)
(c)
FIGURE 24.13: (a) The next command (the second bit in the eighth byte) is 0. This will
create a common parent for the first two tree nodes. (b) The next command (the third bit
in the eighth byte) is also 0. This will create a common parent for the first two tree nodes.
(c) The next command (the fourth bit in the eighth byte) is 1. This will create a tree node
to store the value g.
426 Intermediate C Programming
(a) (b)
FIGURE 24.14: The remaining commands are 0. Continue building the tree.
(a) (b)
The following is the final version of the complete program, for both compression and
decompression.
1 // main . c
2 #i n c l u d e " encode . h "
3 #i n c l u d e " constant . h "
4 #i n c l u d e < stdlib .h >
5 #i n c l u d e < string .h >
6 i n t main ( i n t argc , char ** argv )
7 {
8 // argv [1]: " e " encode
9 // " d " decode
10 // argv [2]: name of input file
11 // argv [3]: name of output file
12 i f ( argc != 4)
13 {
14 return EXIT_FAILURE ;
15 }
16 i f ( strcmp ( argv [1] , " e " ) == 0)
17 {
18 encode ( argv [2] , argv [3]) ;
19 }
20 i f ( strcmp ( argv [1] , " d " ) == 0)
21 {
22 decode ( argv [2] , argv [3]) ;
23 }
24 return EXIT_SUCCESS ;
25 }
1 // encode . h
2 #i f n d e f ENCODE_H
3 #d e f i n e ENCODE_H
4 // encode the input ( text ) file
5 // save the result in the output ( binary ) file
6 // return 0 if cannot read from file or write to file
7 // return 1 if success
8 i n t encode ( char * infile , char * outfile ) ;
9 // decode the input ( binary ) file
10 // save the result in the output ( text ) file
11 // return 0 if cannot read from file or write to file
12 // return 1 if success
13 i n t decode ( char * infile , char * outfile ) ;
14 #e n d i f
1 // encode . c
2 #i n c l u d e " encode . h "
3 #i n c l u d e " constant . h "
4 #i n c l u d e " freq . h "
5 #i n c l u d e " list . h "
6 #i n c l u d e " utility . h "
7 #i n c l u d e < stdio .h >
8 #i n c l u d e < strings .h >
428 Intermediate C Programming
60 {
61 i n t row ;
62 f o r ( row = 0; row < numRow ; row ++)
63 {
64 // print the character
65 printf ( " % c : " , codebook [ row ][0]) ;
66 i n t col = 1;
67 while ( codebook [ row ][ col ] != -1)
68 {
69 printf ( " % d " , codebook [ row ][ col ]) ;
70 col ++;
71 }
72 printf ( " \ n " ) ;
73 }
74 }
75 i n t compress ( char * infile , char * outfile ,
76 i n t * * codebook , i n t * mapping )
77 {
78 FILE * infptr = fopen ( infile , " r " ) ;
79 i f ( infptr == NULL )
80 {
81 return 0;
82 }
83 FILE * outfptr = fopen ( outfile , " a " ) ; // append
84 i f ( outfptr == NULL )
85 {
86 fclose ( outfptr ) ;
87 return 0;
88 }
89 unsigned char whichbit = 0;
90 unsigned char curbyte = 0;
91 while (! feof ( infptr ) )
92 {
93 i n t onechar = fgetc ( infptr ) ;
94 i f ( onechar != EOF )
95 {
96 i n t ind = mapping [ onechar ];
97 i n t ind2 = 1;
98 while ( codebook [ ind ][ ind2 ] != -1)
99 {
100 writeBit ( outfptr , ( codebook [ ind ][ ind2 ] == 1) ,
101 & whichbit , & curbyte ) ;
102 // fprintf ( outfptr , "% d " , codebook [ ind ][ ind2 ]) ;
103 ind2 ++;
104 }
105 }
106 }
107 padZero ( outfptr , & whichbit , & curbyte ) ;
108 fclose ( infptr ) ;
109 fclose ( outfptr ) ;
110 return 1;
430 Intermediate C Programming
111 }
112 // if endec is 0: encode , if it is 1: decode
113 // encoded and decode must flip the order of the two
114 // subtrees
115 s t a t i c ListNode * MergeListNode ( ListNode * head , i n t endec )
116 {
117 ListNode * second = head -> next ;
118 // second must not be NULL , otherwise , will not enter
119 ListNode * third = second -> next ;
120 // third may be NULL
121 // get the tree nodes of the first two list nodes
122 TreeNode * tn1 = head -> tnptr ;
123 TreeNode * tn2 = second -> tnptr ;
124 // remove the first two nodes
125 free ( head ) ;
126 free ( second ) ;
127 head = third ;
128 TreeNode * mrg ;
129 i f ( endec == ENCODEMODE )
130 {
131 mrg = Tree_merge ( tn1 , tn2 ) ;
132 }
133 else
134 {
135 mrg = Tree_merge ( tn2 , tn1 ) ;
136 }
137 ListNode * ln = ListNode_create ( mrg ) ;
138 i f ( endec == ENCODEMODE )
139 {
140 head = List_insert ( head , ln , SORTED ) ;
141 }
142 else
143 {
144 head = List_insert ( head , ln , STACK ) ;
145 }
146 return head ;
147 }
148 // merge the top two list nodes until only one list node
149 s t a t i c TreeNode * list2Tree ( ListNode * head )
150 {
151 // merge the top two list nodes until only one list node
152 while (( head -> next ) != NULL )
153 {
154 List_print ( head ) ; printf ( " - - - - - - - - - - -\ n " ) ;
155 head = MergeListNode ( head , ENCODEMODE ) ;
156 }
157 List_print ( head ) ;
158 // the linked list is no longer needed
159 TreeNode * root = head -> tnptr ;
160 // the linked list is no longer needed
161 free ( head ) ;
Huffman Compression 431
213 i n t ind2 ;
214 f o r ( ind2 = 0; ind2 < numRow ; ind2 ++)
215 {
216 i f ( codebook [ ind2 ][0] == ind )
217 {
218 mapping [ ind ] = ind2 ;
219 }
220 }
221 }
222 f o r ( ind = 0; ind < NUMLETTER ; ind ++)
223 {
224 i f ( mapping [ ind ] != -1)
225 {
226 printf ( " % c :% d \ n " , ind , mapping [ ind ]) ;
227 }
228 }
229 Tree_header ( root , numChar , outfile ) ;
230 compress ( infile , outfile , codebook , mapping ) ;
231 // release memory
232 f o r ( ind = 0; ind < numRow ; ind ++)
233 {
234 free ( codebook [ ind ]) ;
235 }
236 free ( codebook ) ;
237
1 // tree . h
2 #i f n d e f TREE_H
3 #d e f i n e TREE_H
4 typedef s t r u c t treenode
5 {
6 s t r u c t treenode * left ;
7 s t r u c t treenode * right ;
8 char value ; // character
9 i n t freq ; // frequency
10 } TreeNode ;
11 TreeNode * TreeNode_create ( char val , i n t freq ) ;
12 TreeNode * Tree_merge ( TreeNode * tn1 , TreeNode * tn2 ) ;
13 void Tree_print ( TreeNode * tn , i n t level ) ;
14 // find the maximum height of the leaf nodes
15 i n t Tree_height ( TreeNode * tn ) ;
16 // find the number of leaf nodes
17 i n t Tree_leaf ( TreeNode * tn ) ;
18 // save the header of a compressed file
19 void Tree_header ( TreeNode * tn , unsigned i n t numChar , char *
20 outfile ) ;
21 void Tree_destroy ( TreeNode * tn ) ;
22 #e n d i f
Huffman Compression 435
1 // tree . c
2 #i n c l u d e " tree . h "
3 #i n c l u d e " utility . h "
4 #i n c l u d e < stdio .h >
5 #i n c l u d e < stdlib .h >
6 TreeNode * TreeNode_create ( char val , i n t freq )
7 {
8 TreeNode * tn = malloc ( s i z e o f ( TreeNode ) ) ;
9 tn -> left = NULL ;
10 tn -> right = NULL ;
11 tn -> value = val ;
12 tn -> freq = freq ;
13 return tn ;
14 }
15 TreeNode * Tree_merge ( TreeNode * tn1 , TreeNode * tn2 )
16 {
17 TreeNode * tn = malloc ( s i z e o f ( TreeNode ) ) ;
18 tn -> left = tn1 ;
19 tn -> right = tn2 ;
20 tn -> value = 0; // do not care
21 tn -> freq = tn1 -> freq + tn2 -> freq ;
22 return tn ;
23 }
24 // post - order
25 void Tree_print ( TreeNode * tn , i n t level )
26 {
27 i f ( tn == NULL )
28 {
29 return ;
30 }
31 TreeNode * lc = tn -> left ; // left child
32 TreeNode * rc = tn -> right ; // right child
33 Tree_print ( lc , level + 1) ;
34 Tree_print ( rc , level + 1) ;
35 i n t depth ;
36 f o r ( depth = 0; depth < level ; depth ++)
37 {
38 printf ( " ");
39 }
40 printf ( " freq = % d " , tn -> freq ) ;
41 i f (( lc == NULL ) && ( rc == NULL ) )
42 {
43 // a leaf node
44 printf ( " value = %d , ’% c ’" , tn -> value , tn -> value ) ;
45 }
46 printf ( " \ n " ) ;
47 }
48 s t a t i c i n t Tre e_heig htHelp er ( TreeNode * tn , i n t height )
49 {
50 i f ( tn == 0)
51 {
436 Intermediate C Programming
52 return height ;
53 }
54 i n t lh = Tre e_heig htHelp er ( tn -> left , height + 1) ;
55 i n t rh = Tre e_heig htHelp er ( tn -> right , height + 1) ;
56 i f ( lh < rh )
57 {
58 return rh ;
59 }
60 i f ( lh > rh )
61 {
62 return lh ;
63 }
64 return lh ;
65 }
66 i n t Tree_height ( TreeNode * tn )
67 {
68 return Tree _heigh tHelpe r ( tn , 0) ;
69 }
70 s t a t i c void Tree_leafHelper ( TreeNode * tn , i n t * num )
71 {
72 i f ( tn == 0)
73 {
74 return ;
75 }
76 // if it is a leaf node , add one
77 TreeNode * lc = tn -> left ;
78 TreeNode * rc = tn -> right ;
79 i f (( lc == NULL ) && ( rc == NULL ) )
80 {
81 (* num ) ++;
82 return ;
83 }
84 Tree_leafHelper ( lc , num ) ;
85 Tree_leafHelper ( rc , num ) ;
86 }
87 i n t Tree_leaf ( TreeNode * tn )
88 {
89 i n t num = 0;
90 Tree_leafHelper ( tn , & num ) ;
91 return num ;
92 }
93 // print the 7 bits of an ASCII character
94 s t a t i c void char2bits ( FILE * outfptr , i n t ch ,
95 unsigned char * whichbit ,
96 unsigned char * curbyte )
97 {
98 unsigned char mask = 0 x40 ; // only 7 bits
99 while ( mask > 0)
100 {
101 writeBit ( outfptr , ( ch & mask ) == mask ,
102 whichbit , curbyte ) ;
Huffman Compression 437
154 }
155 Tree_destroy ( tn -> left ) ;
156 Tree_destroy ( tn -> right ) ;
157 free ( tn ) ;
158 }
1 // list . h
2 #i f n d e f LIST_H
3 #d e f i n e LIST_H
4 #i n c l u d e " tree . h "
5 #i n c l u d e " constant . h "
6 #i n c l u d e " freq . h "
7 #i n c l u d e < stdio .h >
8 #d e f i n e QUEUE 0
9 #d e f i n e STACK 1
10 #d e f i n e SORTED 2
11 typedef s t r u c t listnode
12 {
13 s t r u c t listnode * next ;
14 TreeNode * tnptr ;
15 } ListNode ;
16 ListNode * List_build ( CharFreq * frequencies ) ;
17 ListNode * ListNode_create ( TreeNode * tn ) ;
18 // The mode is QUEUE , STACK , or SORTED
19 ListNode * List_insert ( ListNode * head , ListNode * ln , i n t
20 mode ) ;
21 void List_print ( ListNode * head ) ;
22 #e n d i f
1 // list . c
2 #i n c l u d e " list . h "
3 #i n c l u d e " freq . h "
4 #i n c l u d e < stdlib .h >
5 ListNode * ListNode_create ( TreeNode * tn )
6 {
7 ListNode * ln = malloc ( s i z e o f ( ListNode ) ) ;
8 ln -> next = NULL ;
9 ln -> tnptr = tn ;
10 return ln ;
11 }
12 // head may be NULL
13 // ln must not be NULL
14 // ln -> next must be NULL
15 ListNode * List_insert ( ListNode * head , ListNode * ln ,
16 i n t mode )
17 {
18 i f ( ln == NULL )
19 {
20 printf ( " ERROR ! ln is NULL \ n " ) ;
21 return NULL ;
22 }
Huffman Compression 439
1 // utility . h
2 #i f n d e f UTILITY_H
3 #d e f i n e UTILITY_H
4 #i n c l u d e < stdio .h >
5 // write one bit to a file
6
1 // utility . c
2 #i n c l u d e < stdio .h >
Huffman Compression 441
21 {
22 i n t ret = 1;
23 i f ((* whichbit ) == 0)
24 {
25 // read a byte from the file
26 ret = fread ( curbyte , s i z e o f ( unsigned char ) , 1 , fptr ) ;
27 }
28 i f ( ret != 1)
29 {
30 // read fail
31 return -1;
32 }
33 // shift the bit to the correct location
34 unsigned char temp = (* curbyte ) >> (7 - (* whichbit ) ) ;
35 temp = temp & 0 X01 ; // get only 1 bit , ignore the others
36 // increase by 1
37 * whichbit = ((* whichbit ) + 1) % 8;
38 * bit = temp ;
39 return 1;
40 }
This is the Makefile for the program.
1 CFLAGS = -g - Wall - Wshadow
2 GCC = gcc $ ( CFLAGS )
3 SRCS = main . c encode . c freq . c tree . c list . c utility . c
4 OBJS = $ ( SRCS :%. c =%. o )
5 VALGRIND = valgrind -- leak - check = full -- tool = memcheck
6 -- verbose -- log - file
7
8 code : $ ( OBJS )
9 $ ( GCC ) $ ( OBJS ) -o code
10
11 test1 : code
442 Intermediate C Programming
19 test2 : code
20 ./ code e input2 compress2
21 $ ( VALGRIND ) = logenc2 ./ code e input2 compress2
22 ./ code d compress2 output2
23 $ ( VALGRIND ) = logdec2 ./ code d compress2 output2
24 echo # add a blank line
25 diff input2 output2
26
27 test3 : code
28 ./ code e input3 compress3
29 $ ( VALGRIND ) = logenc3 ./ code e input3 compress3
30 ./ code d compress3 output3
31 $ ( VALGRIND ) = logdec3 ./ code d compress3 output3
32 echo # add a blank line
33 diff input3 output3
34
35 .c.o:
36 $ ( GCC ) $ ( CFLAGS ) -c $ *. c
37
38 clean :
39 rm -f *. o a . out code log *
This is the most complex program in this book. It integrates almost all topics in this
book. Please study this program carefully. It is a bridge for you from being an intermediate
programmer to becoming an advanced programmer.
Appendix A
Linux
All examples in this book are tested in the Linux programming environment. Linux is a
widely used operating system. It is free in two senses. First there is no need to pay anyone
to get the operating system and many tools for Linux. Second, all the source code for Linux
and many associated tools is freely available. Furthermore, this code can be modified and
used by anyone, for personal or business reasons. Google’s Search Engine uses Linux. So
does the software on the International Space Station. The mobile operating system Android
is based on Linux. Some estimate that over 60% of web servers run UNIX-based operating
systems, and among them Linux dominates.
Sometimes people are surprised by how widely Linux is used. Consider Amazon EC2
(elastic cloud computing). It gives the options for Linux and Windows. For the same ca-
pabilities (measured by the number of virtual processors and the amount of memory), the
price for a Linux instance is about half of the price for Windows. Many software companies,
such as Oracle and SAP, sell programs running on Linux. Why? Because many customers
prefer to use Linux for a variety of reasons. If a company does not support Linux, then this
company forgoes a large market segment. Linux is widely used in universities and compa-
nies. The skills learned using Linux are widely applicable. Learning Linux is important for
developing an understanding of computing in general, and is especially important in some
business and scientific fields.
443
444 Intermediate C Programming
more stable in Linux than in MacOS. Therefore, I recommend installing Linux even
if you already have MacOS. The advantage of dual booting is that each operating
system has the resources of the entire computer. The disadvantage is that it is more
difficult for the operating systems to share data and co-operate, since only one operat-
ing system can be used at a time. Changing the operating system requires restarting
the computer.
• Dual booting used to be more popular than it is today. Now we have access to high
quality and affordable (or free) virtual machines that are well supported by special
hardware. This is my preferred option because of the convenience, and also the wide-
spread usage in industry. A virtual machine is a computer program that runs an
operating system inside of it. The operating system thinks it is running directly on
the hardware, but it is actually embedded in a type of container. The vast majority
of modern computers have special hardware to support virtual machines. The two
operating systems run simultaneously, and it is often easy to move data between the
two “computers”—either moving files, or simply using the clipboard to copy data
from a Windows or MacOS program and paste it into a running Linux program. The
two operating systems must share the resources of your computer. If your computer
has less than 4GB of memory, you may notice occasional slowdown and you should
consider dual booting.
Assuming that your computer has 4GB or more memory, and is currently running
Windows, then when we install a virtual machine, Windows is called the host operating
system. The operating system (Linux) inside the virtual machine is called the guest
operating system. There are several choices for a virtual machine. VirtualBox from
Oracle is an excellent choice and it is free.
If you choose dual boot or virtual machine, you should always save the files already in
your computer before installing Linux. It is possible (even though unlikely) that something
may be wrong and you may lose the files in your computer.
After choosing how to install Linux, you now have to choose which distribution of Linux
you want to use. The common choices are: Fedora, Ubuntu, Mint, and SUSE. This chapter
uses Ubuntu as an example, but all of the distributions listed above are good choices. The
following sections explain how to install Linux as dual boot and how to install Linux inside
Virtualbox.
bootable, keep the flash drive plugged into the computer and restart the computer. When
the computer restarts, and for a few seconds, press F2 (or F10 or F12, depending on the
computer’s firmware or BIOS, namely basic input/output system) to change the computer’s
settings. You will need to select the flash drive as the first choice for booting the computer.
Save the change and restart the computer. Skip the next section and go directly to Section
A.4.
as to install new programs. There is a lot of information available on-line. Be aware that
instructions change over time, and the Internet keeps a lot of old and outdated information
around. It is important to find up-to-date instructions when managing Linux. Linux is widely
used in business and science for good reasons: Linux is powerful and flexible. Spend some
time to become familiar with Linux and the knowledge can help you understand computers
more deeply.
Appendix B
Version Control
B.1 Github.com
This book uses github.com because it is a popular web site that offers free version
control service for students and teachers. After creating an account, use the web interface
to create a new repository (also called repo).
Here account is your account name and password is your password. Replace github.
com/.../demorepo.git with the correct path for your repository. You will see something
like the following on the Linux terminal:
Type the ls command in terminal and you can see the demorepo directory and the
README file. There is also a hidden file called .gitignore. When a file begins with a period,
447
448 Intermediate C Programming
FIGURE B.1: Create a new repository. In this example, I call it “demorepo”. As a teacher,
I can create a free private repository. Check the box of “Initialize this repository with a
README”. Add .gitignore for C. This will ignore files that are not supposed to be in the
repository. Click “Create repository”.
the file is hidden. To see hidden files, please type ls -a. Use your preferred text editor to
add one line into README. Type the git diff command and the difference is shown between
the edited file, and the version of the file before changes. In particular, the line just added
appears with the “+” in front of it. Type this command to commit the change:
$ git commit README.md
Please give a meaningful comment. The comment becomes a part of the history in the
repository. Meaningful comments are required when working with a team. Comments help
document the progression of the program, and also make it easier to track old versions of
files. After committing the changes, the new version is stored. You can see the history using
this command:
$ git log
Creating a new version allows you to roll back to previous versions when necessary.
However, if your computer is broken, then you will lose everything in the computer. To
protect your programs, you want to keep another copy outside of your computer. You can
use github.com to store the repository using this command:
$ git push
If you are the only person working on the project and you have only one computer,
you can push right after commit. If you are working on a team project or you have several
computers, you should do
Version Control 449
$ git pull
before push to ensure that you have the latest version before pushing. Now you can go to
github.com and see the history of the repository in github.com.
451
452 Intermediate C Programming
C.1 Eclipse
There are several ways to install Eclipse. One is to download Eclipse from the web site.
Another is to use Ubuntu’s Software Center. After starting Eclipse, it asks you for the loca-
tion of a “workspace”. This is a folder where you keep your Eclipse programming projects.
The default location is a directory called workspace. Eclipse has a plug-in architecture for
adding features. This is one reason (possibly the most important) why Eclipse is a popular
IDE. To install a plug-in, click Help at the menubar and select Install New Software. Select
Programming Languages and C/C++ Development Tools, as shown in Fig. C.2. Eclipse
does not support the C programming language until this plugin is installed.
FIGURE C.3: Select Makefile Project and call the project “prog1”. Click Finish.
FIGURE C.5: Call the header file prog1.h. Eclipse automatically adds #ifndef, #define,
and #endif to the header file. Add two function declarations to the header file.
FIGURE C.7: You can customize the code formatting style by clicking Windows and
selecting Preferences. Choose a style you like. You can experiment with different styles and
decide which suits your preferences. This example uses the GNU style.
FIGURE C.8: Set the project’s property. Depending on your version of Eclipse and the
installed plug-ins, the build environment may already be set up correctly. Click Project (on
the menubar) and select Build Project. If Eclipse says “no rule to make target all”, then
you need to set the build environment. Select “Generate Makefiles automatically” and click
Apply.
456 Intermediate C Programming
FIGURE C.9: When you click Project and select Build Project, Eclipse will say “undefined
reference to addtwo” and “undefined reference to subtwo”. This should be expected because
these functions have not been implemented. Eclipse’s error message is displayed in the
Console. Eclipse also highlights the two lines that have the errors.
FIGURE C.10: To solve the build problem, we add another source file called addsub.c
and in this file we define the two functions.
Integrated Development Environments (IDE) 457
FIGURE C.11: When you build the project, Eclipse should say that the project is built
successfully. A valid Makefile is automatically generated by Eclipse.
FIGURE C.12: Running: Click Run in the menubar and then select Run.
458 Intermediate C Programming
FIGURE C.14: Eclipse uses gdb to debug programs, and also provides a convenient user
interface. To debug a program, click Run and select Debug.
Integrated Development Environments (IDE) 459
FIGURE C.15: Eclipse starts the program and stops at the first statement in main. This
is denoted by the arrow that is shown at line 13.
FIGURE C.16: Eclipse knows how to communicate with gdb, and provides a convenient
method for common debugging commands such as step over, step into, and toggle break-
point. Move the mouse cursor to line 18 in the source code, and toggle line breakpoint.
460 Intermediate C Programming
FIGURE C.17: Click Window, Show View, and Variables. Here you can see the values of
variables as the code executes. Note that the value of c is 96.
Computer Science & Engineering
Intermediate C Programming
“… an excellent entryway into practical software development practices … I
wished I had this book some 20 years ago … the hands-on examples … are eye
opening. I recommend this book to anyone who needs to write software beyond
the tinkering level.”
—From the Foreword by Gerhard Klimeck, Reilly Director of the Center for Predic-
tive Materials and Devices and the Network for Computational Nanotechnology
and Professor of Electrical and Computer Engineering, Purdue University; Fellow
of the IOP, APS, and IEEE
“This well-written book provides the necessary tools and practical skills to turn
students into seasoned programmers. It not only teaches students how to write
good programs but, more uniquely, also teaches them how to avoid writing bad
programs. The inclusion of Linux operations and Versioning control as well as the
coverage of applications and IDE build students’ confidence in taking control over
large-scale software developments.”
—Siau Cheng Khoo, Ph.D., National University of Singapore
“This book is unique in that it covers the C programming language from a bottom-
up perspective, which is rare in programming books. … students immediately
understand how the language works from a very practical and pragmatic per-
spective.”
—Niklas Elmqvist, Ph.D., Associate Professor and Program Director, Master of
Science in Human–Computer Interaction, University of Maryland
Intermediate C Programming provides a stepping-stone for intermediate-lev-
el students to go from writing short programs to writing real programs well. It
shows students how to identify and eliminate bugs, write clean code, share code
with others, and use standard Linux-based tools, such as ddd and valgrind. The
text enhances their programming skills by explaining programming concepts and
comparing common mistakes with correct programs. It also discusses how to use
debuggers and the strategies for debugging as well as studies the connection
Lu
between programming and discrete mathematics.
K25074
w w w. c rc p r e s s . c o m