Vous êtes sur la page 1sur 191

Binary Trees Page: 1

Binary Trees
by Nick Parlante

This article introduces the basic concepts of binary trees, and then works through a series of practice problems with
solution code in C/C++ and Java. Binary trees have an elegant recursive pointer structure, so they are a good way to
learn recursive pointer algorithms.

Contents

Section 1. Binary Tree Structure -- a quick introduction to binary trees and the code that operates on them
Section 2. Binary Tree Problems -- practice problems in increasing order of difficulty
Section 3. C Solutions -- solution code to the problems for C and C++ programmers
Section 4. Java versions -- how binary trees work in Java, with solution code

Stanford CS Education Library -- #110

This is article #110 in the Stanford CS Education Library. This and other free CS materials are available at the
library (http://cslibrary.stanford.edu/). That people seeking education should have the opportunity to find it.
This article may be used, reproduced, excerpted, or sold so long as this paragraph is clearly reproduced. Copyright
2000-2001, Nick Parlante, nick.parlante@cs.stanford.edu.

Related CSLibrary Articles

Linked List Problems (http://cslibrary.stanford.edu/105/) -- a large collection of linked list problems


using various pointer techniques (while this binary tree article concentrates on recursion)
Pointer and Memory (http://cslibrary.stanford.edu/102/) -- basic concepts of pointers and memory
The Great Tree-List Problem (http://cslibrary.stanford.edu/109/) -- a great pointer recursion problem
that uses both trees and lists

Section 1 -- Introduction To Binary Trees


A binary tree is made of nodes, where each node contains a "left" pointer, a "right" pointer, and a data element.
The "root" pointer points to the topmost node in the tree. The left and right pointers recursively point to smaller
"subtrees" on either side. A null pointer represents a binary tree with no elements -- the empty tree. The formal
recursive definition is: a binary tree is either empty (represented by a null pointer), or is made of a single node,
where the left and right pointers (recursive definition ahead) each point to a binary tree.

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 2

A "binary search tree" (BST) or "ordered binary tree" is a type of binary tree where the nodes are arranged in order:
for each node, all elements in its left subtree are less-or-equal to the node (<=), and all the elements in its right
subtree are greater than the node (>). The tree shown above is a binary search tree -- the "root" node is a 5, and its
left subtree nodes (1, 3, 4) are <= 5, and its right subtree nodes (6, 9) are > 5. Recursively, each of the subtrees must
also obey the binary search tree constraint: in the (1, 3, 4) subtree, the 3 is the root, the 1 <= 3 and 4 > 3. Watch out
for the exact wording in the problems -- a "binary search tree" is different from a "binary tree".

The nodes at the bottom edge of the tree have empty subtrees and are called "leaf" nodes (1, 4, 6) while the others
are "internal" nodes (3, 5, 9).

Binary Search Tree Niche

Basically, binary search trees are fast at insert and lookup. The next section presents the code for these two
algorithms. On average, a binary search tree algorithm can locate a node in an N node tree in order lg(N) time (log
base 2). Therefore, binary search trees are good for "dictionary" problems where the code inserts and looks up
information indexed by some key. The lg(N) behavior is the average case -- it's possible for a particular tree to be
much slower depending on its shape.

Strategy

Some of the problems in this article use plain binary trees, and some use binary search trees. In any case, the
problems concentrate on the combination of pointers and recursion. (See the articles linked above for pointer articles
that do not emphasize recursion.)

For each problem, there are two things to understand...

The node/pointer structure that makes up the tree and the code that manipulates it
The algorithm, typically recursive, that iterates over the tree

When thinking about a binary tree problem, it's often a good idea to draw a few little trees to think about the
various cases.

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 3

Typical Binary Tree Code in C/C++

As an introduction, we'll look at the code for the two most basic binary search tree operations -- lookup() and
insert(). The code here works for C or C++. Java programers can read the discussion here, and then look at the Java
versions in Section 4.

In C or C++, the binary tree is built with a node type like this...

struct node {
int data;
struct node* left;
struct node* right;
}

Lookup()

Given a binary search tree and a "target" value, search the tree to see if it contains the target. The basic pattern of
the lookup() code occurs in many recursive tree algorithms: deal with the base case where the tree is empty, deal
with the current node, and then use recursion to deal with the subtrees. If the tree is a binary search tree, there is
often some sort of less-than test on the node to decide if the recursion should go left or right.

/*
Given a binary tree, return true if a node
with the target data is found in the tree. Recurs
down the tree, chooses the left or right
branch by comparing the target to each node.
*/
static int lookup(struct node* node, int target) {
// 1. Base case == empty tree
// in that case, the target is not found so return false
if (node == NULL) {
return(false);
}
else {
// 2. see if found here
if (target == node->data) return(true);
else {
// 3. otherwise recur down the correct subtree
if (target < node->data) return(lookup(node->left, target));
else return(lookup(node->right, target));
}
}
}

The lookup() algorithm could be written as a while-loop that iterates down the tree. Our version uses recursion to
help prepare you for the problems below that require recursion.

Pointer Changing Code

There is a common problem with pointer intensive code: what if a function needs to change one of the pointer
parameters passed to it? For example, the insert() function below may want to change the root pointer. In C and
C++, one solution uses pointers-to-pointers (aka "reference parameters"). That's a fine technique, but here we will
use the simpler technique that a function that wishes to change a pointer passed to it will return the new value of
the pointer to the caller. The caller is responsible for using the new value. Suppose we have a change() function
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 4

that may change the the root, then a call to change() will look like this...

// suppose the variable "root" points to the tree


root = change(root);

We take the value returned by change(), and use it as the new value for root. This construct is a little awkward, but
it avoids using reference parameters which confuse some C and C++ programmers, and Java does not have reference
parameters at all. This allows us to focus on the recursion instead of the pointer mechanics. (For lots of problems
that use reference parameters, see CSLibrary #105, Linked List Problems, http://cslibrary.stanford.edu/105/).

Insert()

Insert() -- given a binary search tree and a number, insert a new node with the given number into the tree in the
correct place. The insert() code is similar to lookup(), but with the complication that it modifies the tree structure.
As described above, insert() returns the new tree pointer to use to its caller. Calling insert() with the number 5 on
this tree...

2
/ \
1 10

returns the tree...

2
/ \
1 10
/
5

The solution shown here introduces a newNode() helper function that builds a single node. The base-case/recursion
structure is similar to the structure in lookup() -- each call checks for the NULL case, looks at the node at hand, and
then recurs down the left or right subtree if needed.

/*
Helper function that allocates a new node
with the given data and NULL left and right
pointers.
*/
struct node* NewNode(int data) {
struct node* node = new(struct node); // "new" is like "malloc"
node->data = data;
node->left = NULL;
node->right = NULL;

return(node);
}

/*
Give a binary search tree and a number, inserts a new node
with the given number in the correct place in the tree.
Returns the new root pointer which the caller should
then use (the standard trick to avoid using reference
parameters).
*/
struct node* insert(struct node* node, int data) {

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 5

// 1. If the tree is empty, return a new, single node


if (node == NULL) {
return(newNode(data));
}
else {
// 2. Otherwise, recur down the tree
if (data <= node->data) node->left = insert(node->left, data);
else node->right = insert(node->right, data);

return(node); // return the (unchanged) node pointer


}
}

The shape of a binary tree depends very much on the order that the nodes are inserted. In particular, if the nodes
are inserted in increasing order (1, 2, 3, 4), the tree nodes just grow to the right leading to a linked list shape where
all the left pointers are NULL. A similar thing happens if the nodes are inserted in decreasing order (4, 3, 2, 1). The
linked list shape defeats the lg(N) performance. We will not address that issue here, instead focusing on pointers
and recursion.

Section 2 -- Binary Tree Problems


Here are 14 binary tree problems in increasing order of difficulty. Some of the problems operate on binary search
trees (aka "ordered binary trees") while others work on plain binary trees with no special ordering. The next
section, Section 3, shows the solution code in C/C++. Section 4 gives the background and solution code in Java. The
basic structure and recursion of the solution code is the same in both languages -- the differences are superficial.

Reading about a data structure is a fine introduction, but at some point the only way to learn is to actually try to
solve some problems starting with a blank sheet of paper. To get the most out of these problems, you should at least
attempt to solve them before looking at the solution. Even if your solution is not quite right, you will be building up
the right skills. With any pointer-based code, it's a good idea to make memory drawings of a a few simple cases to
see how the algorithm should work.

1. build123()

This is a very basic problem with a little pointer manipulation. (You can skip this problem if you are already
comfortable with pointers.) Write code that builds the following little 1-2-3 binary search tree...

2
/ \
1 3

Write the code in three different ways...

a: by calling newNode() three times, and using three pointer variables


b: by calling newNode() three times, and using only one pointer variable
c: by calling insert() three times passing it the root pointer to build up the tree

(In Java, write a build123() method that operates on the receiver to change it to be the 1-2-3 tree with the given
coding constraints. See Section 4.)

struct node* build123() {

2. size()

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 6

This problem demonstrates simple binary tree traversal. Given a binary tree, count the number of nodes in the tree.

int size(struct node* node) {

3. maxDepth()

Given a binary tree, compute its "maxDepth" -- the number of nodes along the longest path from the root node down
to the farthest leaf node. The maxDepth of the empty tree is 0, the maxDepth of the tree on the first page is 3.

int maxDepth(struct node* node) {

4. minValue()

Given a non-empty binary search tree (an ordered binary tree), return the minimum data value found in that tree.
Note that it is not necessary to search the entire tree. A maxValue() function is structurally very similar to this
function. This can be solved with recursion or with a simple while loop.

int minValue(struct node* node) {

5. printTree()

Given a binary search tree (aka an "ordered binary tree"), iterate over the nodes to print them out in increasing
order. So the tree...

4
/ \
2 5
/ \
1 3

Produces the output "1 2 3 4 5". This is known as an "inorder" traversal of the tree.

Hint: For each node, the strategy is: recur left, print the node data, recur right.

void printTree(struct node* node) {

6. printPostorder()

Given a binary tree, print out the nodes of the tree according to a bottom-up "postorder" traversal -- both subtrees of
a node are printed out completely before the node itself is printed, and each left subtree is printed before the right
subtree. So the tree...

4
/ \
2 5
/ \
1 3

Produces the output "1 3 2 5 4". The description is complex, but the code is simple. This is the sort of bottom-up
traversal that would be used, for example, to evaluate an expression tree where a node is an operation like '+' and
its subtrees are, recursively, the two subexpressions for the '+'.

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 7

void printPostorder(struct node* node) {

7. hasPathSum()

We'll define a "root-to-leaf path" to be a sequence of nodes in a tree starting with the root node and proceeding
downward to a leaf (a node with no children). We'll say that an empty tree contains no root-to-leaf paths. So for
example, the following tree has exactly four root-to-leaf paths:

5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1

Root-to-leaf paths:
path 1: 5 4 11 7
path 2: 5 4 11 2
path 3: 5 8 13
path 4: 5 8 4 1

For this problem, we will be concerned with the sum of the values of such a path -- for example, the sum of the
values on the 5-4-11-7 path is 5 + 4 + 11 + 7 = 27.

Given a binary tree and a sum, return true if the tree has a root-to-leaf path such that adding up all the values
along the path equals the given sum. Return false if no such path can be found. (Thanks to Owen Astrachan for
suggesting this problem.)

int hasPathSum(struct node* node, int sum) {

8. printPaths()

Given a binary tree, print out all of its root-to-leaf paths as defined above. This problem is a little harder than it
looks, since the "path so far" needs to be communicated between the recursive calls. Hint: In C, C++, and Java,
probably the best solution is to create a recursive helper function printPathsRecur(node, int path[], int pathLen),
where the path array communicates the sequence of nodes that led up to the current call. Alternately, the problem
may be solved bottom-up, with each node returning its list of paths. This strategy works quite nicely in Lisp, since
it can exploit the built in list and mapping primitives. (Thanks to Matthias Felleisen for suggesting this problem.)

Given a binary tree, print out all of its root-to-leaf paths, one per line.

void printPaths(struct node* node) {

9. mirror()

Change a tree so that the roles of the left and right pointers are swapped at every node.

So the tree...
4
/ \
2 5
/ \
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 8

1 3

is changed to...
4
/ \
5 2
/ \
3 1

The solution is short, but very recursive. As it happens, this can be accomplished without changing the root node
pointer, so the return-the-new-root construct is not necessary. Alternately, if you do not want to change the tree
nodes, you may construct and return a new mirror tree based on the original tree.

void mirror(struct node* node) {

10. doubleTree()

For each node in a binary search tree, create a new duplicate node, and insert the duplicate as the left child of the
original node. The resulting tree should still be a binary search tree.

So the tree...
2
/ \
1 3

is changed to...
2
/ \
2 3
/ /
1 3
/
1

As with the previous problem, this can be accomplished without changing the root node pointer.

void doubleTree(struct node* node) {

11. sameTree()

Given two binary trees, return true if they are structurally identical -- they are made of nodes with the same values
arranged in the same way. (Thanks to Julie Zelenski for suggesting this problem.)

int sameTree(struct node* a, struct node* b) {

12. countTrees()

This is not a binary tree programming problem in the ordinary sense -- it's more of a math/combinatorics recursion
problem that happens to use binary trees. (Thanks to Jerry Cain for suggesting this problem.)

Suppose you are building an N node binary search tree with the values 1..N. How many structurally different
binary search trees are there that store those values? Write a recursive function that, given the number of distinct
values, computes the number of structurally unique binary search trees that store those values. For example,
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 9

countTrees(4) should return 14, since there are 14 structurally unique binary search trees that store 1, 2, 3, and 4. The
base case is easy, and the recursion is short but dense. Your code should not construct any actual trees; it's just a
counting problem.

int countTrees(int numKeys) {

Binary Search Tree Checking (for problems 13 and 14)

This background is used by the next two problems: Given a plain binary tree, examine the tree to determine if it
meets the requirement to be a binary search tree. To be a binary search tree, for every node, all of the nodes in its
left tree must be <= the node, and all of the nodes in its right subtree must be > the node. Consider the following four
examples...

a. 5 -> TRUE
/ \
2 7

b. 5 -> FALSE, because the 6 is not ok to the left of the 5


/ \
6 7

c. 5 -> TRUE
/ \
2 7
/
1

d. 5 -> FALSE, the 6 is ok with the 2, but the 6 is not ok with the 5
/ \
2 7
/ \
1 6

For the first two cases, the right answer can be seen just by comparing each node to the two nodes immediately
below it. However, the fourth case shows how checking the BST quality may depend on nodes which are several
layers apart -- the 5 and the 6 in that case.

13 isBST() -- version 1

Suppose you have helper functions minValue() and maxValue() that return the min or max int value from a
non-empty tree (see problem 3 above). Write an isBST() function that returns true if a tree is a binary search tree
and false otherwise. Use the helper functions, and don't forget to check every node in the tree. It's ok if your
solution is not very efficient. (Thanks to Owen Astrachan for the idea of having this problem, and comparing it to
problem 14)

Returns true if a binary tree is a binary search tree.

int isBST(struct node* node) {

14. isBST() -- version 2


http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 10

Version 1 above runs slowly since it traverses over some parts of the tree many times. A better solution looks at each
node only once. The trick is to write a utility helper function isBSTRecur(struct node* node, int min, int max) that
traverses down the tree keeping track of the narrowing min and max allowed values as it goes, looking at each node
only once. The initial values for min and max should be INT_MIN and INT_MAX -- they narrow from there.

/*
Returns true if the given tree is a binary search tree
(efficient version).
*/
int isBST2(struct node* node) {
return(isBSTRecur(node, INT_MIN, INT_MAX));
}

/*
Returns true if the given tree is a BST and its
values are >= min and <= max.
*/
int isBSTRecur(struct node* node, int min, int max) {

15. Tree-List

The Tree-List problem is one of the greatest recursive pointer problems ever devised, and it happens to use binary
trees as well. CLibarary #109 http://cslibrary.stanford.edu/109/ works through the Tree-List problem in detail
and includes solution code in C and Java. The problem requires an understanding of binary trees, linked lists,
recursion, and pointers. It's a great problem, but it's complex.

Section 3 -- C/C++ Solutions


Make an attempt to solve each problem before looking at the solution -- it's the best way to learn.

1. Build123() Solution (C/C++)

// call newNode() three times


struct node* build123a() {
struct node* root = newNode(2);
struct node* lChild = newNode(1);
struct node* rChild = newNode(3);

root->left = lChild;
root->right= rChild;

return(root);
}

// call newNode() three times, and use only one local variable
struct node* build123b() {
struct node* root = newNode(2);
root->left = newNode(1);
root->right = newNode(3);

return(root);
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 11

/*
Build 123 by calling insert() three times.
Note that the '2' must be inserted first.
*/
struct node* build123c() {
struct node* root = NULL;
root = insert(root, 2);
root = insert(root, 1);
root = insert(root, 3);
return(root);
}

2. size() Solution (C/C++)

/*
Compute the number of nodes in a tree.
*/
int size(struct node* node) {
if (node==NULL) {
return(0);
} else {
return(size(node->left) + 1 + size(node->right));
}
}

3. maxDepth() Solution (C/C++)

/*
Compute the "maxDepth" of a tree -- the number of nodes along
the longest path from the root node down to the farthest leaf node.
*/
int maxDepth(struct node* node) {
if (node==NULL) {
return(0);
}
else {
// compute the depth of each subtree
int lDepth = maxDepth(node->left);
int rDepth = maxDepth(node->right);

// use the larger one


if (lDepth > rDepth) return(lDepth+1);
else return(rDepth+1);
}
}

4. minValue() Solution (C/C++)

/*
Given a non-empty binary search tree,
return the minimum data value found in that tree.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 12

Note that the entire tree does not need to be searched.


*/
int minValue(struct node* node) {
struct node* current = node;

// loop down to find the leftmost leaf


while (current->left != NULL) {
current = current->left;
}

return(current->data);
}

5. printTree() Solution (C/C++)

/*
Given a binary search tree, print out
its data elements in increasing
sorted order.
*/
void printTree(struct node* node) {
if (node == NULL) return;

printTree(node->left);
printf("%d ", node->data);
printTree(node->right);
}

6. printPostorder() Solution (C/C++)

/*
Given a binary tree, print its
nodes according to the "bottom-up"
postorder traversal.
*/
void printPostorder(struct node* node) {
if (node == NULL) return;

// first recur on both subtrees


printTree(node->left);
printTree(node->right);

// then deal with the node


printf("%d ", node->data);
}

7. hasPathSum() Solution (C/C++)

/*
Given a tree and a sum, return true if there is a path from the root
down to a leaf, such that adding up all the values along the path
equals the given sum.

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 13

Strategy: subtract the node value from the sum when recurring down,
and check to see if the sum is 0 when you run out of tree.
*/
int hasPathSum(struct node* node, int sum) {
// return true if we run out of tree and sum==0
if (node == NULL) {
return(sum == 0);
}
else {
// otherwise check both subtrees
int subSum = sum - node->data;
return(hasPathSum(node->left, subSum) ||
hasPathSum(node->right, subSum));
}
}

8. printPaths() Solution (C/C++)

/*
Given a binary tree, print out all of its root-to-leaf
paths, one per line. Uses a recursive helper to do the work.
*/
void printPaths(struct node* node) {
int path[1000];

printPathsRecur(node, path, 0);


}

/*
Recursive helper function -- given a node, and an array containing
the path from the root node up to but not including this node,
print out all the root-leaf paths.
*/
void printPathsRecur(struct node* node, int path[], int pathLen) {
if (node==NULL) return;

// append this node to the path array


path[pathLen] = node->data;
pathLen++;

// it's a leaf, so print the path that led to here


if (node->left==NULL && node->right==NULL) {
printArray(path, pathLen);
}
else {
// otherwise try both subtrees
printPathsRecur(node->left, path, pathLen);
printPathsRecur(node->right, path, pathLen);
}
}

// Utility that prints out an array on a line.


void printArray(int ints[], int len) {
int i;
for (i=0; i<len; i++) {

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 14

printf("%d ", ints[i]);


}
printf("\n");
}

9. mirror() Solution (C/C++)

/*
Change a tree so that the roles of the
left and right pointers are swapped at every node.

So the tree...
4
/ \
2 5
/ \
1 3

is changed to...
4
/ \
5 2
/ \
3 1
*/
void mirror(struct node* node) {
if (node==NULL) {
return;
}
else {
struct node* temp;

// do the subtrees
mirror(node->left);
mirror(node->right);

// swap the pointers in this node


temp = node->left;
node->left = node->right;
node->right = temp;
}
}

10. doubleTree() Solution (C/C++)

/*
For each node in a binary search tree,
create a new duplicate node, and insert
the duplicate as the left child of the original node.
The resulting tree should still be a binary search tree.

So the tree...
2
/ \

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 15

1 3

Is changed to...
2
/ \
2 3
/ /
1 3
/
1

*/
void doubleTree(struct node* node) {
struct node* oldLeft;

if (node==NULL) return;

// do the subtrees
doubleTree(node->left);
doubleTree(node->right);

// duplicate this node to its left


oldLeft = node->left;
node->left = newNode(node->data);
node->left->left = oldLeft;
}

11. sameTree() Solution (C/C++)

/*
Given two trees, return true if they are
structurally identical.
*/
int sameTree(struct node* a, struct node* b) {
// 1. both empty -> true
if (a==NULL && b==NULL) return(true);

// 2. both non-empty -> compare them


else if (a!=NULL && b!=NULL) {
return(
a->data == b->data &&
sameTree(a->left, b->left) &&
sameTree(a->right, b->right)
);
}
// 3. one empty, one not -> false
else return(false);
}

12. countTrees() Solution (C/C++)

/*
For the key values 1...numKeys, how many structurally unique

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 16

binary search trees are possible that store those keys.

Strategy: consider that each value could be the root.


Recursively find the size of the left and right subtrees.
*/
int countTrees(int numKeys) {

if (numKeys <=1) {
return(1);
}
else {
// there will be one value at the root, with whatever remains
// on the left and right each forming their own subtrees.
// Iterate through all the values that could be the root...
int sum = 0;
int left, right, root;

for (root=1; root<=numKeys; root++) {


left = countTrees(root - 1);
right = countTrees(numKeys - root);

// number of possible trees with this root == left*right


sum += left*right;
}

return(sum);
}
}

13. isBST1() Solution (C/C++)

/*
Returns true if a binary tree is a binary search tree.
*/
int isBST(struct node* node) {
if (node==NULL) return(true);

// false if the min of the left is > than us


if (node->left!=NULL && minValue(node->left) > node->data)
return(false);

// false if the max of the right is <= than us


if (node->right!=NULL && maxValue(node->right) <= node->data)
return(false);

// false if, recursively, the left or right is not a BST


if (!isBST(node->left) || !isBST(node->right))
return(false);

// passing all that, it's a BST


return(true);
}

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 17

14. isBST2() Solution (C/C++)

/*
Returns true if the given tree is a binary search tree
(efficient version).
*/
int isBST2(struct node* node) {
return(isBSTUtil(node, INT_MIN, INT_MAX));
}

/*
Returns true if the given tree is a BST and its
values are >= min and <= max.
*/
int isBSTUtil(struct node* node, int min, int max) {
if (node==NULL) return(true);

// false if this node violates the min/max constraint


if (node->data<min || node->data>max) return(false);

// otherwise check the subtrees recursively,


// tightening the min or max constraint
return
isBSTUtil(node->left, min, node->data) &&
isBSTUtil(node->right, node->data+1, max)
);
}

15. TreeList Solution (C/C++)

The solution code in C and Java to the great Tree-List recursion problem is in CSLibrary #109
http://cslibrary.stanford.edu/109/

Section 4 -- Java Binary Trees and Solutions


In Java, the key points in the recursion are exactly the same as in C or C++. In fact, I created the Java solutions by
just copying the C solutions, and then making the syntactic changes. The recursion is the same, however the outer
structure is slightly different.

In Java, we will have a BinaryTree object that contains a single root pointer. The root pointer points to an internal
Node class that behaves just like the node struct in the C/C++ version. The Node class is private -- it is used only
for internal storage inside the BinaryTree and is not exposed to clients. With this OOP structure, almost every
operation has two methods: a one-line method on the BinaryTree that starts the computation, and a recursive
method that works on the Node objects. For the lookup() operation, there is a BinaryTree.lookup() method that
the client uses to start a lookup operation. Internal to the BinaryTree class, there is a private recursive
lookup(Node) method that implements the recursion down the Node structure. This second, private recursive
method is basically the same as the recursive C/C++ functions above -- it takes a Node argument and uses recursion
to iterate over the pointer structure.

Java Binary Tree Structure

To get started, here are the basic definitions for the Java BinaryTree class, and the lookup() and insert() methods
as examples...

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 18

// BinaryTree.java
public class BinaryTree {
// Root node pointer. Will be null for an empty tree.
private Node root;

/*
--Node--
The binary tree is built using this nested node class.
Each node stores one data element, and has left and right
sub-tree pointer which may be null.
The node is a "dumb" nested class -- we just use it for
storage; it does not have any methods.
*/
private static class Node {
Node left;
Node right;
int data;

Node(int newData) {
left = null;
right = null;
data = newData;
}
}

/**
Creates an empty binary tree -- a null root pointer.
*/
public void BinaryTree() {
root = null;
}

/**
Returns true if the given target is in the binary tree.
Uses a recursive helper.
*/
public boolean lookup(int data) {
return(lookup(root, data));
}

/**
Recursive lookup -- given a node, recur
down searching for the given data.
*/
private boolean lookup(Node node, int data) {
if (node==null) {
return(false);
}

if (data==node.data) {
return(true);
}
else if (data<node.data) {

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 19

return(lookup(node.left, data));
}
else {
return(lookup(node.right, data));
}
}

/**
Inserts the given data into the binary tree.
Uses a recursive helper.
*/
public void insert(int data) {
root = insert(root, data);
}

/**
Recursive insert -- given a node pointer, recur down and
insert the given data into the tree. Returns the new
node pointer (the standard way to communicate
a changed pointer back to the caller).
*/
private Node insert(Node node, int data) {
if (node==null) {
node = new Node(data);
}
else {
if (data <= node.data) {
node.left = insert(node.left, data);
}
else {
node.right = insert(node.right, data);
}
}

return(node); // in any case, return the new pointer to the caller


}

OOP Style vs. Recursive Style

From the client point of view, the BinaryTree class demonstrates good OOP style -- it encapsulates the binary tree
state, and the client sends messages like lookup() and insert() to operate on that state. Internally, the Node class
and the recursive methods do not demonstrate OOP style. The recursive methods like insert(Node) and lookup
(Node, int) basically look like recursive functions in any language. In particular, they do not operate against a
"receiver" in any special way. Instead, the recursive methods operate on the arguments that are passed in which is
the classical way to write recursion. My sense is that the OOP style and the recursive style do not be combined
nicely for binary trees, so I have left them separate. Merging the two styles would be especially awkward for the
"empty" tree (null) case, since you can't send a message to the null pointer. It's possible to get around that by having
a special object to represent the null tree, but that seems like a distraction to me. I prefer to keep the recursive
methods simple, and use different examples to teach OOP.

Java Solutions

Here are the Java solutions to the 14 binary tree problems. Most of the solutions use two methods:a one-line OOP

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 20

method that starts the computation, and a recursive method that does the real operation. Make an attempt to
solve each problem before looking at the solution -- it's the best way to learn.

1. Build123() Solution (Java)

/**
Build 123 using three pointer variables.
*/
public void build123a() {
root = new Node(2);
Node lChild = new Node(1);
Node rChild = new Node(3);

root.left = lChild;
root.right= rChild;
}

/**
Build 123 using only one pointer variable.
*/
public void build123b() {
root = new Node(2);
root.left = new Node(1);
root.right = new Node(3);
}

/**
Build 123 by calling insert() three times.
Note that the '2' must be inserted first.
*/
public void build123c() {
root = null;
root = insert(root, 2);
root = insert(root, 1);
root = insert(root, 3);
}

2. size() Solution (Java)

/**
Returns the number of nodes in the tree.
Uses a recursive helper that recurs
down the tree and counts the nodes.
*/
public int size() {
return(size(root));
}

private int size(Node node) {


if (node == null) return(0);
else {
return(size(node.left) + 1 + size(node.right));
}
}

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 21

3. maxDepth() Solution (Java)

/**
Returns the max root-to-leaf depth of the tree.
Uses a recursive helper that recurs down to find
the max depth.
*/
public int maxDepth() {
return(maxDepth(root));
}

private int maxDepth(Node node) {


if (node==null) {
return(0);
}
else {
int lDepth = maxDepth(node.left);
int rDepth = maxDepth(node.right);

// use the larger + 1


return(Math.max(lDepth, rDepth) + 1);
}
}

4. minValue() Solution (Java)

/**
Returns the min value in a non-empty binary search tree.
Uses a helper method that iterates to the left to find
the min value.
*/
public int minValue() {
return( minValue(root) );
}

/**
Finds the min value in a non-empty binary search tree.
*/
private int minValue(Node node) {
Node current = node;
while (current.left != null) {
current = current.left;
}

return(current.data);
}

5. printTree() Solution (Java)

/**
Prints the node values in the "inorder" order.
Uses a recursive helper to do the traversal.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 22

*/
public void printTree() {
printTree(root);
System.out.println();
}

private void printTree(Node node) {


if (node == null) return;

// left, node itself, right


printTree(node.left);
System.out.print(node.data + " ");
printTree(node.right);
}

6. printPostorder() Solution (Java)

/**
Prints the node values in the "postorder" order.
Uses a recursive helper to do the traversal.
*/
public void printPostorder() {
printPostorder(root);
System.out.println();
}

public void printPostorder(Node node) {


if (node == null) return;

// first recur on both subtrees


printPostorder(node.left);
printPostorder(node.right);

// then deal with the node


System.out.print(node.data + " ");
}

7. hasPathSum() Solution (Java)

/**
Given a tree and a sum, returns true if there is a path from the root
down to a leaf, such that adding up all the values along the path
equals the given sum.

Strategy: subtract the node value from the sum when recurring down,
and check to see if the sum is 0 when you run out of tree.
*/
public boolean hasPathSum(int sum) {
return( hasPathSum(root, sum) );
}

boolean hasPathSum(Node node, int sum) {


// return true if we run out of tree and sum==0
if (node == null) {

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 23

return(sum == 0);
}
else {
// otherwise check both subtrees
int subSum = sum - node.data;
return(hasPathSum(node.left, subSum) || hasPathSum(node.right, subSum));
}
}

8. printPaths() Solution (Java)

/**
Given a binary tree, prints out all of its root-to-leaf
paths, one per line. Uses a recursive helper to do the work.
*/
public void printPaths() {
int[] path = new int[1000];
printPaths(root, path, 0);
}

/**
Recursive printPaths helper -- given a node, and an array containing
the path from the root node up to but not including this node,
prints out all the root-leaf paths.
*/
private void printPaths(Node node, int[] path, int pathLen) {
if (node==null) return;

// append this node to the path array


path[pathLen] = node.data;
pathLen++;

// it's a leaf, so print the path that led to here


if (node.left==null && node.right==null) {
printArray(path, pathLen);
}
else {
// otherwise try both subtrees
printPaths(node.left, path, pathLen);
printPaths(node.right, path, pathLen);
}
}

/**
Utility that prints ints from an array on one line.
*/
private void printArray(int[] ints, int len) {
int i;
for (i=0; i<len; i++) {
System.out.print(ints[i] + " ");
}
System.out.println();
}

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 24

9. mirror() Solution (Java)

/**
Changes the tree into its mirror image.

So the tree...
4
/ \
2 5
/ \
1 3

is changed to...
4
/ \
5 2
/ \
3 1

Uses a recursive helper that recurs over the tree,


swapping the left/right pointers.
*/
public void mirror() {
mirror(root);
}

private void mirror(Node node) {


if (node != null) {
// do the sub-trees
mirror(node.left);
mirror(node.right);

// swap the left/right pointers


Node temp = node.left;
node.left = node.right;
node.right = temp;
}
}

10. doubleTree() Solution (Java)

/**
Changes the tree by inserting a duplicate node
on each nodes's .left.

So the tree...
2
/ \
1 3

Is changed to...
2

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 25

/ \
2 3
/ /
1 3
/
1

Uses a recursive helper to recur over the tree


and insert the duplicates.
*/
public void doubleTree() {
doubleTree(root);
}

private void doubleTree(Node node) {


Node oldLeft;

if (node == null) return;

// do the subtrees
doubleTree(node.left);
doubleTree(node.right);

// duplicate this node to its left


oldLeft = node.left;
node.left = new Node(node.data);
node.left.left = oldLeft;
}

11. sameTree() Solution (Java)

/*
Compares the receiver to another tree to
see if they are structurally identical.
*/
public boolean sameTree(BinaryTree other) {
return( sameTree(root, other.root) );
}

/**
Recursive helper -- recurs down two trees in parallel,
checking to see if they are identical.
*/
boolean sameTree(Node a, Node b) {
// 1. both empty -> true
if (a==null && b==null) return(true);

// 2. both non-empty -> compare them


else if (a!=null && b!=null) {
return(
a.data == b.data &&
sameTree(a.left, b.left) &&
sameTree(a.right, b.right)
);
}

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 26

// 3. one empty, one not -> false


else return(false);
}

12. countTrees() Solution (Java)

/**
For the key values 1...numKeys, how many structurally unique
binary search trees are possible that store those keys?

Strategy: consider that each value could be the root.


Recursively find the size of the left and right subtrees.
*/
public static int countTrees(int numKeys) {
if (numKeys <=1) {
return(1);
}
else {
// there will be one value at the root, with whatever remains
// on the left and right each forming their own subtrees.
// Iterate through all the values that could be the root...
int sum = 0;
int left, right, root;

for (root=1; root<=numKeys; root++) {


left = countTrees(root-1);
right = countTrees(numKeys - root);

// number of possible trees with this root == left*right


sum += left*right;
}

return(sum);
}
}

13. isBST1() Solution (Java)

/**
Tests if a tree meets the conditions to be a
binary search tree (BST).
*/
public boolean isBST() {
return(isBST(root));
}

/**
Recursive helper -- checks if a tree is a BST
using minValue() and maxValue() (not efficient).
*/
private boolean isBST(Node node) {
if (node==null) return(true);

// do the subtrees contain values that do not

http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 27

// agree with the node?


if (node.left!=null && maxValue(node.left) > node.data) return(false);
if (node.right!=null && minValue(node.right) <= node.data) return(false);

// check that the subtrees themselves are ok


return( isBST(node.left) && isBST(node.right) );
}

14. isBST2() Solution (Java)

/**
Tests if a tree meets the conditions to be a
binary search tree (BST). Uses the efficient
recursive helper.
*/
public boolean isBST2() {
return( isBST2(root, Integer.MIN_VALUE, Integer.MAX_VALUE) );
}

/**
Efficient BST helper -- Given a node, and min and max values,
recurs down the tree to verify that it is a BST, and that all
its nodes are within the min..max range. Works in O(n) time --
visits each node only once.
*/
private boolean isBST2(Node node, int min, int max) {
if (node==null) {
return(true);
}
else {
// left should be in range min...node.data
boolean leftOk = isBST2(node.left, min, node.data);

// if the left is not ok, bail out


if (!leftOk) return(false);

// right should be in range node.data+1..max


boolean rightOk = isBST2(node.right, node.data+1, max);

return(rightOk);
}
}

http://cslibrary.stanford.edu/110/
BinaryTrees.html
By Nick Parlante
Essential C Copyright 1996-2003, Nick Parlante

This Stanford CS Education document tries to summarize all the basic features of the C
language. The coverage is pretty quick, so it is most appropriate as review or for someone
with some programming background in another language. Topics include variables, int
types, floating point types, promotion, truncation, operators, control structures (if, while,
for), functions, value parameters, reference parameters, structs, pointers, arrays, the pre-
processor, and the standard C library functions.
The most recent version is always maintained at its Stanford CS Education Library URL
http://cslibrary.stanford.edu/101/. Please send your comments to
nick.parlante@cs.stanford.edu.
I hope you can share and enjoy this document in the spirit of goodwill in which it is given
away -- Nick Parlante, 4/2003, Stanford California.
Stanford CS Education Library This is document #101, Essential C, in the Stanford
CS Education Library. This and other educational materials are available for free at
http://cslibrary.stanford.edu/. This article is free to be used, reproduced, excerpted,
retransmitted, or sold so long as this notice is clearly reproduced at its beginning.
Table of Contents
Introduction .........................................................................................pg. 2
Where C came from, what is it like, what other resources might you look at.
Section 1 Basic Types and Operators ..........................................pg. 3
Integer types, floating point types, assignment operator, comparison operators,
arithmetic operators, truncation, promotion.
Section 2 Control Structures ........................................................pg. 11
If statement, conditional operator, switch, while, for, do-while, break, continue.
Section 3 Complex Data Types .....................................................pg. 15
Structs, arrays, pointers, ampersand operator (&), NULL, C strings, typedef.
Section 4 Functions ........................................................................pg. 24
Functions, void, value and reference parameters, const.
Section 5 Odds and Ends ..............................................................pg. 29
Main(), the .h/.c file convention, pre-processor, assert.
Section 6 Advanced Arrays and Pointers ....................................pg. 33
How arrays and pointers interact. The [ ] and + operators with pointers, base
address/offset arithmetic, heap memory management, heap arrays.
Section 7 Operators and Standard Library Reference ..............pg. 41
A summary reference of the most common operators and library functions.
The C Language
C is a professional programmer's language. It was designed to get in one's way as little as
possible. Kernighan and Ritchie wrote the original language definition in their book, The
C Programming Language (below), as part of their research at AT&T. Unix and C++
emerged from the same labs. For several years I used AT&T as my long distance carrier
in appreciation of all that CS research, but hearing "thank you for using AT&T" for the
millionth time has used up that good will.
2

Some languages are forgiving. The programmer needs only a basic sense of how things
work. Errors in the code are flagged by the compile-time or run-time system, and the
programmer can muddle through and eventually fix things up to work correctly. The C
language is not like that.
The C programming model is that the programmer knows exactly what they want to do
and how to use the language constructs to achieve that goal. The language lets the expert
programmer express what they want in the minimum time by staying out of their way.
C is "simple" in that the number of components in the language is small-- If two language
features accomplish more-or-less the same thing, C will include only one. C's syntax is
terse and the language does not restrict what is "allowed" -- the programmer can pretty
much do whatever they want.
C's type system and error checks exist only at compile-time. The compiled code runs in a
stripped down run-time model with no safety checks for bad type casts, bad array indices,
or bad pointers. There is no garbage collector to manage memory. Instead the
programmer mangages heap memory manually. All this makes C fast but fragile.

Analysis -- Where C Fits


Because of the above features, C is hard for beginners. A feature can work fine in one
context, but crash in another. The programmer needs to understand how the features work
and use them correctly. On the other hand, the number of features is pretty small.
Like most programmers, I have had some moments of real loathing for the C language. It
can be irritatingly obedient -- you type something incorrectly, and it has a way of
compiling fine and just doing something you don't expect at run-time. However, as I have
become a more experienced C programmer, I have grown to appreciate C's straight-to-the
point style. I have learned not to fall into its little traps, and I appreciate its simplicity.
Perhaps the best advice is just to be careful. Don't type things in you don't understand.
Debugging takes too much time. Have a mental picture (or a real drawing) of how your C
code is using memory. That's good advice in any language, but in C it's critical.
Perl and Java are more "portable" than C (you can run them on different computers
without a recompile). Java and C++ are more structured than C. Structure is useful for
large projects. C works best for small projects where performance is important and the
progammers have the time and skill to make it work in C. In any case, C is a very popular
and influential language. This is mainly because of C's clean (if minimal) style, it's lack
of annoying or regrettable constructs, and the relative ease of writing a C compiler.

Other Resources
• The C Programming Language, 2nd ed., by Kernighan and Ritchie. The thin book
which for years was the bible for all C programmers. Written by the original
designers of the language. The explanations are pretty short, so this book is better as a
reference than for beginners.
• http://cslibrary.stanford.edu/102/ Pointers and Memory -- Much more detail
about local memory, pointers, reference parameters, and heap memory than in this
article, and memory is really the hardest part of C and C++.
• http://cslibrary.stanford.edu//103/ Linked List Basics -- Once you understand the
basics of pointers and C, these problems are a good way to get more practice.
3

Section 1
Basic Types and Operators
C provides a standard, minimal set of basic data types. Sometimes these are called
"primitive" types. More complex data structures can be built up from these basic types.

Integer Types
The "integral" types in C form a family of integer types. They all behave like integers and
can be mixed together and used in similar ways. The differences are due to the different
number of bits ("widths") used to implement each type -- the wider types can store a
greater ranges of values.
char ASCII character -- at least 8 bits. Pronounced "car". As a practical matter
char is basically always a byte which is 8 bits which is enough to store a single
ASCII character. 8 bits provides a signed range of -128..127 or an unsigned range is
0..255. char is also required to be the "smallest addressable unit" for the machine --
each byte in memory has its own address.

short Small integer -- at least 16 bits which provides a signed range of


-32768..32767. Typical size is 16 bits. Not used so much.

int Default integer -- at least 16 bits, with 32 bits being typical. Defined to be
the "most comfortable" size for the computer. If you do not really care about the
range for an integer variable, declare it int since that is likely to be an appropriate
size (16 or 32 bit) which works well for that machine.

long Large integer -- at least 32 bits. Typical size is 32 bits which gives a signed
range of about -2 billion ..+2 billion. Some compilers support "long long" for 64 bit
ints.

The integer types can be preceded by the qualifier unsigned which disallows
representing negative numbers, but doubles the largest positive number representable. For
example, a 16 bit implementation of short can store numbers in the range
-32768..32767, while unsigned short can store 0..65535. You can think of pointers
as being a form of unsigned long on a machine with 4 byte pointers. In my opinion,
it's best to avoid using unsigned unless you really need to. It tends to cause more
misunderstandings and problems than it is worth.

Extra: Portability Problems


Instead of defining the exact sizes of the integer types, C defines lower bounds. This
makes it easier to implement C compilers on a wide range of hardware. Unfortunately it
occasionally leads to bugs where a program runs differently on a 16-bit-int machine than
it runs on a 32-bit-int machine. In particular, if you are designing a function that will be
implemented on several different machines, it is a good idea to use typedefs to set up
types like Int32 for 32 bit int and Int16 for 16 bit int. That way you can prototype a
function Foo(Int32) and be confident that the typedefs for each machine will be set so
that the function really takes exactly a 32 bit int. That way the code will behave the same
on all the different machines.

char Constants
A char constant is written with single quotes (') like 'A' or 'z'. The char constant 'A' is
really just a synonym for the ordinary integer value 65 which is the ASCII value for
4

uppercase 'A'. There are special case char constants, such as '\t' for tab, for characters
which are not convenient to type on a keyboard.
'A' uppercase 'A' character

'\n' newline character

'\t' tab character

'\0' the "null" character -- integer value 0 (different from the char digit '0')

'\012' the character with value 12 in octal, which is decimal 10

int Constants
Numbers in the source code such as 234 default to type int. They may be followed by
an 'L' (upper or lower case) to designate that the constant should be a long such as 42L.
An integer constant can be written with a leading 0x to indicate that it is expressed in
hexadecimal -- 0x10 is way of expressing the number 16. Similarly, a constant may be
written in octal by preceding it with "0" -- 012 is a way of expressing the number 10.

Type Combination and Promotion


The integral types may be mixed together in arithmetic expressions since they are all
basically just integers with variation in their width. For example, char and int can be
combined in arithmetic expressions such as ('b' + 5). How does the compiler deal
with the different widths present in such an expression? In such a case, the compiler
"promotes" the smaller type (char) to be the same size as the larger type (int) before
combining the values. Promotions are determined at compile time based purely on the
types of the values in the expressions. Promotions do not lose information -- they always
convert from a type to compatible, larger type to avoid losing information.

Pitfall -- int Overflow


I once had a piece of code which tried to compute the number of bytes in a buffer with
the expression (k * 1024) where k was an int representing the number of kilobytes
I wanted. Unfortunately this was on a machine where int happened to be 16 bits. Since
k and 1024 were both int, there was no promotion. For values of k >= 32, the product
was too big to fit in the 16 bit int resulting in an overflow. The compiler can do
whatever it wants in overflow situations -- typically the high order bits just vanish. One
way to fix the code was to rewrite it as (k * 1024L) -- the long constant forced the
promotion of the int. This was not a fun bug to track down -- the expression sure looked
reasonable in the source code. Only stepping past the key line in the debugger showed the
overflow problem. "Professional Programmer's Language." This example also
demonstrates the way that C only promotes based on the types in an expression. The
compiler does not consider the values 32 or 1024 to realize that the operation will
overflow (in general, the values don't exist until run time anyway). The compiler just
looks at the compile time types, int and int in this case, and thinks everything is fine.

Floating point Types


float Single precision floating point number typical size: 32 bits
double Double precision floating point number typical size: 64 bits
long double Possibly even bigger floating point number (somewhat obscure)
Constants in the source code such as 3.14 default to type double unless the are suffixed
with an 'f' (float) or 'l' (long double). Single precision equates to about 6 digits of
5

precision and double is about 15 digits of precision. Most C programs use double for
their computations. The main reason to use float is to save memory if many numbers
need to be stored. The main thing to remember about floating point numbers is that they
are inexact. For example, what is the value of the following double expression?
(1.0/3.0 + 1.0/3.0 + 1.0/3.0) // is this equal to 1.0 exactly?

The sum may or may not be 1.0 exactly, and it may vary from one type of machine to
another. For this reason, you should never compare floating numbers to eachother for
equality (==) -- use inequality (<) comparisons instead. Realize that a correct C program
run on different computers may produce slightly different outputs in the rightmost digits
of its floating point computations.

Comments
Comments in C are enclosed by slash/star pairs: /* .. comments .. */ which
may cross multiple lines. C++ introduced a form of comment started by two slashes and
extending to the end of the line: // comment until the line end
The // comment form is so handy that many C compilers now also support it, although it
is not technically part of the C language.
Along with well-chosen function names, comments are an important part of well written
code. Comments should not just repeat what the code says. Comments should describe
what the code accomplishes which is much more interesting than a translation of what
each statement does. Comments should also narrate what is tricky or non-obvious about a
section of code.

Variables
As in most languages, a variable declaration reserves and names an area in memory at run
time to hold a value of particular type. Syntactically, C puts the type first followed by the
name of the variable. The following declares an int variable named "num" and the 2nd
line stores the value 42 into num.
int num;
num = 42;

num 42

A variable corresponds to an area of memory which can store a value of the given type.
Making a drawing is an excellent way to think about the variables in a program. Draw
each variable as box with the current value inside the box. This may seem like a
"beginner" technique, but when I'm buried in some horribly complex programming
problem, I invariably resort to making a drawing to help think the problem through.
Variables, such as num, do not have their memory cleared or set in any way when they
are allocated at run time. Variables start with random values, and it is up to the program
to set them to something sensible before depending on their values.
Names in C are case sensitive so "x" and "X" refer to different variables. Names can
contain digits and underscores (_), but may not begin with a digit. Multiple variables can
be declared after the type by separating them with commas. C is a classical "compile
time" language -- the names of the variables, their types, and their implementations are all
flushed out by the compiler at compile time (as opposed to figuring such details out at run
time like an interpreter).
6

float x, y, z, X;

Assignment Operator =
The assignment operator is the single equals sign (=).
i = 6;
i = i + 1;

The assignment operator copies the value from its right hand side to the variable on its
left hand side. The assignment also acts as an expression which returns the newly
assigned value. Some programmers will use that feature to write things like the following.
y = (x = 2 * x); // double x, and also put x's new value in y

Truncation
The opposite of promotion, truncation moves a value from a type to a smaller type. In
that case, the compiler just drops the extra bits. It may or may not generate a compile
time warning of the loss of information. Assigning from an integer to a smaller integer
(e.g.. long to int, or int to char) drops the most significant bits. Assigning from a
floating point type to an integer drops the fractional part of the number.
char ch;
int i;

i = 321;
ch = i; // truncation of an int value to fit in a char
// ch is now 65

The assignment will drop the upper bits of the int 321. The lower 8 bits of the number
321 represents the number 65 (321 - 256). So the value of ch will be (char)65 which
happens to be 'A'.
The assignment of a floating point type to an integer type will drop the fractional part of
the number. The following code will set i to the value 3. This happens when assigning a
floating point number to an integer or passing a floating point number to a function which
takes an integer.
double pi;
int i;

pi = 3.14159;
i = pi; // truncation of a double to fit in an int
// i is now 3

Pitfall -- int vs. float Arithmetic


Here's an example of the sort of code where int vs. float arithmetic can cause
problems. Suppose the following code is supposed to scale a homework score in the
range 0..20 to be in the range 0..100.
{
int score;
...// suppose score gets set in the range 0..20 somehow
7

score = (score / 20) * 100; // NO -- score/20 truncates to 0


...

Unfortunately, score will almost always be set to 0 for this code because the integer
division in the expression (score/20) will be 0 for every value of score less than 20.
The fix is to force the quotient to be computed as a floating point number...
score = ((double)score / 20) * 100; // OK -- floating point division from cast

score = (score / 20.0) * 100; // OK -- floating point division from 20.0

score = (int)(score / 20.0) * 100; // NO -- the (int) truncates the floating


// quotient back to 0

No Boolean -- Use int


C does not have a distinct boolean type-- int is used instead. The language treats integer
0 as false and all non-zero values as true. So the statement...
i = 0;
while (i - 10) {
...

will execute until the variable i takes on the value 10 at which time the expression (i -
10) will become false (i.e. 0). (we'll see the while() statement a bit later)

Mathematical Operators
C includes the usual binary and unary arithmetic operators. See the appendix for the table
of precedence. Personally, I just use parenthesis liberally to avoid any bugs due to a
misunderstanding of precedence. The operators are sensitive to the type of the operands.
So division (/) with two integer arguments will do integer division. If either argument is
a float, it does floating point division. So (6/4) evaluates to 1 while (6/4.0)
evaluates to 1.5 -- the 6 is promoted to 6.0 before the division.
+ Addition

- Subtraction

/ Division

* Multiplication

% Remainder (mod)

Unary Increment Operators: ++ --


The unary ++ and -- operators increment or decrement the value in a variable. There are
"pre" and "post" variants for both operators which do slightly different things (explained
below)
var++ increment "post" variant

++var increment "pre" variant


8

var-- decrement "post" variant

--var decrement "pre" variant

int i = 42;
i++; // increment on i
// i is now 43
i--; // decrement on i
// i is now 42

Pre and Post Variations


The Pre/Post variation has to do with nesting a variable with the increment or decrement
operator inside an expression -- should the entire expression represent the value of the
variable before or after the change? I never use the operators in this way (see below), but
an example looks like...
int i = 42;
int j;

j = (i++ + 10);
// i is now 43
// j is now 52 (NOT 53)

j = (++i + 10)
// i is now 44
// j is now 54

C Programming Cleverness and Ego Issues


Relying on the difference between the pre and post variations of these operators is a
classic area of C programmer ego showmanship. The syntax is a little tricky. It makes the
code a little shorter. These qualities drive some C programmers to show off how clever
they are. C invites this sort of thing since the language has many areas (this is just one
example) where the programmer can get a complex effect using a code which is short and
dense.
If I want j to depend on i's value before the increment, I write...
j = (i + 10);
i++;

Or if I want to j to use the value after the increment, I write...


i++;
j = (i + 10);

Now then, isn't that nicer? (editorial) Build programs that do something cool rather than
programs which flex the language's syntax. Syntax -- who cares?

Relational Operators
These operate on integer or floating point values and return a 0 or 1 boolean value.
== Equal
9

!= Not Equal

> Greater Than

< Less Than

>= Greater or Equal

<= Less or Equal

To see if x equals three, write something like:


if (x == 3) ...

Pitfall = ==
An absolutely classic pitfall is to write assignment (=) when you mean comparison (==).
This would not be such a problem, except the incorrect assignment version compiles fine
because the compiler assumes you mean to use the value returned by the assignment. This
is rarely what you want
if (x = 3) ...

This does not test if x is 3. This sets x to the value 3, and then returns the 3 to the if for
testing. 3 is not 0, so it counts as "true" every time. This is probably the single most
common error made by beginning C programmers. The problem is that the compiler is no
help -- it thinks both forms are fine, so the only defense is extreme vigilance when
coding. Or write "= ≠ ==" in big letters on the back of your hand before coding. This
mistake is an absolute classic and it's a bear to debug. Watch Out! And need I say:
"Professional Programmer's Language."

Logical Operators
The value 0 is false, anything else is true. The operators evaluate left to right and stop as
soon as the truth or falsity of the expression can be deduced. (Such operators are called
"short circuiting") In ANSI C, these are furthermore guaranteed to use 1 to represent true,
and not just some random non-zero bit pattern. However, there are many C programs out
there which use values other than 1 for true (non-zero pointers for example), so when
programming, do not assume that a true boolean is necessarily 1 exactly.
! Boolean not (unary)

&& Boolean and

|| Boolean or

Bitwise Operators
C includes operators to manipulate memory at the bit level. This is useful for writing low-
level hardware or operating system code where the ordinary abstractions of numbers,
characters, pointers, etc... are insufficient -- an increasingly rare need. Bit manipulation
code tends to be less "portable". Code is "portable" if with no programmer intervention it
compiles and runs correctly on different types of computers. The bitwise operations are
10

typically used with unsigned types. In particular, the shift operations are guaranteed to
shift 0 bits into the newly vacated positions when used on unsigned values.
~ Bitwise Negation (unary) – flip 0 to 1 and 1 to 0 throughout

& Bitwise And

| Bitwise Or

^ Bitwise Exclusive Or

>> Right Shift by right hand side (RHS) (divide by power of 2)

<< Left Shift by RHS (multiply by power of 2)

Do not confuse the Bitwise operators with the logical operators. The bitwise connectives
are one character wide (&, |) while the boolean connectives are two characters wide (&&,
||). The bitwise operators have higher precedence than the boolean operators. The
compiler will never help you out with a type error if you use & when you meant &&. As
far as the type checker is concerned, they are identical-- they both take and produce
integers since there is no distinct boolean type.

Other Assignment Operators


In addition to the plain = operator, C includes many shorthand operators which represents
variations on the basic =. For example "+=" adds the right hand side to the left hand side.
x = x + 10; can be reduced to x += 10;. This is most useful if x is a long
expression such as the following, and in some cases it may run a little faster.
person->relatives.mom.numChildren += 2; // increase children by 2

Here's the list of assignment shorthand operators...


+=, -= Increment or decrement by RHS

*=, /= Multiply or divide by RHS

%= Mod by RHS

>>= Bitwise right shift by RHS (divide by power of 2)

<<= Bitwise left shift RHS (multiply by power of 2)

&=, |=, ^= Bitwise and, or, xor by RHS


11

Section 2
Control Structures
Curly Braces {}
C uses curly braces ({}) to group multiple statements together. The statements execute in
order. Some languages let you declare variables on any line (C++). Other languages insist
that variables are declared only at the beginning of functions (Pascal). C takes the middle
road -- variables may be declared within the body of a function, but they must follow a
'{'. More modern languages like Java and C++ allow you to declare variables on any line,
which is handy.

If Statement
Both an if and an if-else are available in C. The <expression> can be any valid
expression. The parentheses around the expression are required, even if it is just a single
variable.
if (<expression>) <statement> // simple form with no {}'s or else clause

if (<expression>) { // simple form with {}'s to group statements


<statement>
<statement>
}

if (<expression>) { // full then/else form


<statement>
}
else {
<statement>
}

Conditional Expression -or- The Ternary Operator


The conditional expression can be used as a shorthand for some if-else statements. The
general syntax of the conditional operator is:
<expression1> ? <expression2> : <expression3>

This is an expression, not a statement, so it represents a value. The operator works by


evaluating expression1. If it is true (non-zero), it evaluates and returns expression2 .
Otherwise, it evaluates and returns expression3.
The classic example of the ternary operator is to return the smaller of two variables.
Every once in a while, the following form is just what you needed. Instead of...
if (x < y) {
min = x;
}
else {
min = y;
}
12

You just say...


min = (x < y) ? x : y;

Switch Statement
The switch statement is a sort of specialized form of if used to efficiently separate
different blocks of code based on the value of an integer. The switch expression is
evaluated, and then the flow of control jumps to the matching const-expression case. The
case expressions are typically int or char constants. The switch statement is probably
the single most syntactically awkward and error-prone features of the C language.
switch (<expression>) {
case <const-expression-1>:
<statement>
break;

case <const-expression-2>:
<statement>
break;

case <const-expression-3>: // here we combine case 3 and 4


case <const-expression-4>:
<statement>
break;

default: // optional
<statement>
}

Each constant needs its own case keyword and a trailing colon (:). Once execution has
jumped to a particular case, the program will keep running through all the cases from that
point down -- this so called "fall through" operation is used in the above example so that
expression-3 and expression-4 run the same statements. The explicit break statements
are necessary to exit the switch. Omitting the break statements is a common error -- it
compiles, but leads to inadvertent fall-through behavior.
Why does the switch statement fall-through behavior work the way it does? The best
explanation I can think of is that originally C was developed for an audience of assembly
language programmers. The assembly language programmers were used to the idea of a
jump table with fall-through behavior, so that's the way C does it (it's also relatively easy
to implement it this way.) Unfortunately, the audience for C is now quite different, and
the fall-through behavior is widely regarded as a terrible part of the language.

While Loop
The while loop evaluates the test expression before every loop, so it can execute zero
times if the condition is initially false. It requires the parenthesis like the if.
while (<expression>) {
<statement>
}
13

Do-While Loop
Like a while, but with the test condition at the bottom of the loop. The loop body will
always execute at least once. The do-while is an unpopular area of the language, most
everyone tries to use the straight while if at all possible.
do {
<statement>
} while (<expression>)

For Loop
The for loop in C is the most general looping construct. The loop header contains three
parts: an initialization, a continuation condition, and an action.
for (<initialization>; <continuation>; <action>) {
<statement>
}

The initialization is executed once before the body of the loop is entered. The loop
continues to run as long as the continuation condition remains true (like a while). After
every execution of the loop, the action is executed. The following example executes 10
times by counting 0..9. Many loops look very much like the following...
for (i = 0; i < 10; i++) {
<statement>
}

C programs often have series of the form 0..(some_number-1). It's idiomatic in C for the
above type loop to start at 0 and use < in the test so the series runs up to but not equal to
the upper bound. In other languages you might start at 1 and use <= in the test.
Each of the three parts of the for loop can be made up of multiple expressions separated
by commas. Expressions separated by commas are executed in order, left to right, and
represent the value of the last expression. (See the string-reverse example below for a
demonstration of a complex for loop.)

Break
The break statement will move control outside a loop or switch statement. Stylistically
speaking, break has the potential to be a bit vulgar. It's preferable to use a straight
while with a single test at the top if possible. Sometimes you are forced to use a break
because the test can occur only somewhere in the midst of the statements in the loop
body. To keep the code readable, be sure to make the break obvious -- forgetting to
account for the action of a break is a traditional source of bugs in loop behavior.
while (<expression>) {
<statement>
<statement>

if (<condition which can only be evaluated here>)


break;

<statement>
<statement>
}
// control jumps down here on the break
14

The break does not work with if. It only works in loops and switches. Thinking that a
break refers to an if when it really refers to the enclosing while has created some high
quality bugs. When using a break, it's nice to write the enclosing loop to iterate in the
most straightforward, obvious, normal way, and then use the break to explicitly catch
the exceptional, weird cases.

Continue
The continue statement causes control to jump to the bottom of the loop, effectively
skipping over any code below the continue. As with break, this has a reputation as
being vulgar, so use it sparingly. You can almost always get the effect more clearly using
an if inside your loop.
while (<expression>) {
...
if (<condition>)
continue;
...
...
// control jumps here on the continue
}
15

Section 3
Complex Data Types
C has the usual facilities for grouping things together to form composite types-- arrays
and records (which are called "structures"). The following definition declares a type
called "struct fraction" that has two integer sub fields named "numerator" and
"denominator". If you forget the semicolon it tends to produce a syntax error in whatever
thing follows the struct declaration.
struct fraction {
int numerator;
int denominator;
}; // Don't forget the semicolon!

This declaration introduces the type struct fraction (both words are required) as a
new type. C uses the period (.) to access the fields in a record. You can copy two records
of the same type using a single assignment statement, however == does not work on
structs.
struct fraction f1, f2; // declare two fractions

f1.numerator = 22;
f1.denominator = 7;

f2 = f1; // this copies over the whole struct

Arrays
The simplest type of array in C is one which is declared and used in one place. There are
more complex uses of arrays which I will address later along with pointers. The following
declares an array called scores to hold 100 integers and sets the first and last elements.
C arrays are always indexed from 0. So the first int in scores array is scores[0]
and the last is scores[99].
int scores[100];

scores[0] = 13; // set first element


scores[99] = 42; // set last element
16

The name of the array refers to the


whole array. (implementation) it
scores works by representing a pointer to the
start of the array.

13 -5673 22541 42
Index 0 1 2 99

There is space for These elements Someone else’s memory


each int element in have random off either end of the
the scores array — values because the array — do not read or
this element is code has not yet write this memory.
referred to as initialized them to
scores[0]. anything.
It's a very common error to try to refer to non-existent scores[100] element. C does
not do any run time or compile time bounds checking in arrays. At run time the code will
just access or mangle whatever memory it happens to hit and crash or misbehave in some
unpredictable way thereafter. "Professional programmer's language." The convention of
numbering things 0..(number of things - 1) pervades the language. To best
integrate with C and other C programmers, you should use that sort of numbering in your
own data structures as well.

Multidimensional Arrays
The following declares a two-dimensional 10 by 10 array of integers and sets the first and
last elements to be 13.
int board [10][10];

board[0][0] = 13;
board[9][9] = 13;

The implementation of the array stores all the elements in a single contiguous block of
memory. The other possible implementation would be a combination of several distinct
one dimensional arrays -- that's not how C does it. In memory, the array is arranged with
the elements of the rightmost index next to each other. In other words, board[1][8]
comes right before board[1][9] in memory.
(highly optional efficiency point) It's typically efficient to access memory which is near
other recently accessed memory. This means that the most efficient way to read through a
chunk of the array is to vary the rightmost index the most frequently since that will access
elements that are near each other in memory.
17

Array of Structs
The following declares an array named "numbers" which holds 1000 struct
fraction's.
struct fraction numbers[1000];

numbers[0].numerator = 22; /* set the 0th struct fraction */


numbers[0].denominator = 7;

Here's a general trick for unraveling C variable declarations: look at the right hand side
and imagine that it is an expression. The type of that expression is the left hand side. For
the above declarations, an expression which looks like the right hand side
(numbers[1000], or really anything of the form numbers[...]) will be the type
on the left hand side (struct fraction).

Pointers
A pointer is a value which represents a reference to another value sometimes known as
the pointer's "pointee". Hopefully you have learned about pointers somewhere else, since
the preceding sentence is probably inadequate explanation. This discussion will
concentrate on the syntax of pointers in C -- for a much more complete discussion of
pointers and their use see http://cslibrary.stanford.edu/102/, Pointers and Memory.

Syntax
Syntactically C uses the asterisk or "star" (*) to indicate a pointer. C defines pointer types
based on the type pointee. A char* is type of pointer which refers to a single char. a
struct fraction* is type of pointer which refers to a struct fraction.
int* intPtr; // declare an integer pointer variable intPtr

char* charPtr; // declares a character pointer --


// a very common type of pointer

// Declare two struct fraction pointers


// (when declaring multiple variables on one line, the *
// should go on the right with the variable)
struct fraction *f1, *f2;

The Floating "*"


In the syntax, the star is allowed to be anywhere between the base type and the variable
name. Programmer's have their own conventions-- I generally stick the * on the left with
the type. So the above declaration of intPtr could be written equivalently...
int *intPtr; // these are all the same
int * intPtr;
int* intPtr;

Pointer Dereferencing
We'll see shortly how a pointer is set to point to something -- for now just assume the
pointer points to memory of the appropriate type. In an expression, the unary * to the left
of a pointer dereferences it to retrieve the value it points to. The following drawing shows
the types involved with a single pointer pointing to a struct fraction.
18

struct fraction* f1;

7 denominator
f1
22 numerator

struct fraction*
struct fraction int
(the whole (within
block of block of
memory) memory)
Expression Type
f1 struct fraction*
*f1 struct fraction
(*f1).numerator int

There's an alternate, more readable syntax available for dereferencing a pointer to a


struct. A "->" at the right of the pointer can access any of the fields in the struct. So the
reference to the numerator field could be written f1->numerator.
Here are some more complex declarations...
struct fraction** fp; // a pointer to a pointer to a struct fraction

struct fraction fract_array[20]; // an array of 20 struct fractions

struct fraction* fract_ptr_array[20]; // an array of 20 pointers to


// struct fractions

One nice thing about the C type syntax is that it avoids the circular definition problems
which come up when a pointer structure needs to refer to itself. The following definition
defines a node in a linked list. Note that no preparatory declaration of the node pointer
type is necessary.
struct node {
int data;
struct node* next;
};

The & Operator


The & operator is one of the ways that pointers are set to point to things. The & operator
computes a pointer to the argument to its right. The argument can be any variable which
takes up space in the stack or heap (known as an "LValue" technically). So &i and
&(f1->numerator) are ok, but &6 is not. Use & when you have some memory, and
you want a pointer to that memory.
19

void foo() {
int* p; // p is a pointer to an integer
int i; // i is an integer

p = &i; // Set p to point to i


*p = 13; // Change what p points to -- in this case i -- to 13

// At this point i is 13. So is *p. In fact *p is i.


}

i 13

When using a pointer to an object created with &, it is important to only use the pointer so
long as the object exists. A local variable exists only as long as the function where it is
declared is still executing (we'll see functions shortly). In the above example, i exists
only as long as foo() is executing. Therefore any pointers which were initialized with
&i are valid only as long as foo() is executing. This "lifetime" constraint of local
memory is standard in many languages, and is something you need to take into account
when using the & operator.

NULL
A pointer can be assigned the value 0 to explicitly represent that it does not currently
have a pointee. Having a standard representation for "no current pointee" turns out to be
very handy when using pointers. The constant NULL is defined to be 0 and is typically
used when setting a pointer to NULL. Since it is just 0, a NULL pointer will behave like
a boolean false when used in a boolean context. Dereferencing a NULL pointer is an error
which, if you are lucky, the computer will detect at runtime -- whether the computer
detects this depends on the operating system.

Pitfall -- Uninitialized Pointers


When using pointers, there are two entities to keep track of. The pointer and the memory
it is pointing to, sometimes called the "pointee". There are three things which must be
done for a pointer/pointee relationship to work...
(1) The pointer must be declared and allocated

(2) The pointee must be declared and allocated

(3) The pointer (1) must be initialized so that it points to the pointee (2)

The most common pointer related error of all time is the following: Declare and allocate
the pointer (step 1). Forget step 2 and/or 3. Start using the pointer as if it has been setup
to point to something. Code with this error frequently compiles fine, but the runtime
results are disastrous. Unfortunately the pointer does not point anywhere good unless (2)
and (3) are done, so the run time dereference operations on the pointer with * will misuse
and trample memory leading to a random crash at some point.
20

{
int* p;

*p = 13; // NO NO NO p does not point to an int yet


// this just overwrites a random area in memory
}

i -14346

Of course your code won't be so trivial, but the bug has the same basic form: declare a
pointer, but forget to set it up to point to a particular pointee.

Using Pointers
Declaring a pointer allocates space for the pointer itself, but it does not allocate space
for the pointee. The pointer must be set to point to something before you can dereference
it.
Here's some code which doesn't do anything useful, but which does demonstrate (1) (2)
(3) for pointer use correctly...
int* p; // (1) allocate the pointer
int i; // (2) allocate pointee
struct fraction f1; // (2) allocate pointee

p = &i; // (3) setup p to point to i


*p = 42; // ok to use p since it's setup

p = &(f1.numerator); // (3) setup p to point to a different int


*p = 22;

p = &(f1.denominator); // (3)
*p = 7;

So far we have just used the & operator to create pointers to simple variables such as i.
Later, we'll see other ways of getting pointers with arrays and other techniques.

C Strings
C has minimal support of character strings. For the most part, strings operate as ordinary
arrays of characters. Their maintenance is up to the programmer using the standard
facilities available for arrays and pointers. C does include a standard library of functions
which perform common string operations, but the programmer is responsible for the
managing the string memory and calling the right functions. Unfortunately computations
involving strings are very common, so becoming a good C programmer often requires
becoming adept at writing code which manages strings which means managing pointers
and arrays.
21

A C string is just an array of char with the one additional convention that a "null"
character ('\0') is stored after the last real character in the array to mark the end of the
string. The compiler represents string constants in the source code such as "binky" as
arrays which follow this convention. The string library functions (see the appendix for a
partial list) operate on strings stored in this way. The most useful library function is
strcpy(char dest[], const char source[]); which copies the bytes of
one string over to another. The order of the arguments to strcpy() mimics the arguments
in of '=' -- the right is assigned to the left. Another useful string function is
strlen(const char string[]); which returns the number of characters in C
string not counting the trailing '\0'.
Note that the regular assignment operator (=) does not do string copying which is why
strcpy() is necessary. See Section 6, Advanced Pointers and Arrays, for more detail on
how arrays and pointers work.
The following code allocates a 10 char array and uses strcpy() to copy the bytes of the
string constant "binky" into that local array.
{
char localString[10];

strcpy(localString, "binky");
}

localString

b i n k y 0 x x x x

0 1 2 ...
The memory drawing shows the local variable localString with the string "binky"
copied into it. The letters take up the first 5 characters and the '\0' char marks the end of
the string after the 'y'. The x's represent characters which have not been set to any
particular value.
If the code instead tried to store the string "I enjoy languages which have good string
support" into localString, the code would just crash at run time since the 10 character
array can contain at most a 9 character string. The large string will be written passed the
right hand side of localString, overwriting whatever was stored there.

String Code Example


Here's a moderately complex for loop which reverses a string stored in a local array. It
demonstrates calling the standard library functions strcpy() and strlen() and demonstrates
that a string really is just an array of characters with a '\0' to mark the effective end of the
string. Test your C knowledge of arrays and for loops by making a drawing of the
memory for this code and tracing through its execution to see how it works.
22

char string[1000]; // string is a local 1000 char array


int len;

strcpy(string, "binky");
len = strlen(string);

/*
Reverse the chars in the string:
i starts at the beginning and goes up
j starts at the end and goes down
i/j exchange their chars as they go until they meet
*/
int i, j;
char temp;
for (i = 0, j = len - 1; i < j; i++, j--) {
temp = string[i];
string[i] = string[j];
string[j] = temp;
}

// at this point the local string should be "yknib"

"Large Enough" Strings


The convention with C strings is that the owner of the string is responsible for allocating
array space which is "large enough" to store whatever the string will need to store. Most
routines do not check that size of the string memory they operate on, they just assume its
big enough and blast away. Many, many programs contain declarations like the
following...
{
char localString[1000];
...
}

The program works fine so long as the strings stored are 999 characters or shorter.
Someday when the program needs to store a string which is 1000 characters or longer,
then it crashes. Such array-not-quite-big-enough problems are a common source of bugs,
and are also the source of so called "buffer overflow" security problems. This scheme has
the additional disadvantage that most of the time when the array is storing short strings,
95% of the memory reserved is actually being wasted. A better solution allocates the
string dynamically in the heap, so it has just the right size.
To avoid buffer overflow attacks, production code should check the size of the data first,
to make sure it fits in the destination string. See the strlcpy() function in Appendix A.

char*
Because of the way C handles the types of arrays, the type of the variable
localString above is essentially char*. C programs very often manipulate strings
using variables of type char* which point to arrays of characters. Manipulating the
actual chars in a string requires code which manipulates the underlying array, or the use
23

of library functions such as strcpy() which manipulate the array for you. See Section 6 for
more detail on pointers and arrays.

TypeDef
A typedef statement introduces a shorthand name for a type. The syntax is...
typedef <type> <name>;

The following defines Fraction type to be the type (struct fraction). C is case
sensitive, so fraction is different from Fraction. It's convenient to use typedef to
create types with upper case names and use the lower-case version of the same word as a
variable.
typedef struct fraction Fraction;

Fraction fraction; // Declare the variable "fraction" of type "Fraction"


// which is really just a synonym for "struct fraction".

The following typedef defines the name Tree as a standard pointer to a binary tree node
where each node contains some data and "smaller" and "larger" subtree pointers.
typedef struct treenode* Tree;
struct treenode {
int data;
Tree smaller, larger; // equivalently, this line could say
}; // "struct treenode *smaller, *larger"
24

Section 4
Functions
All languages have a construct to separate and package blocks of code. C uses the
"function" to package blocks of code. This article concentrates on the syntax and
peculiarities of C functions. The motivation and design for dividing a computation into
separate blocks is an entire discipline in its own.
A function has a name, a list of arguments which it takes when called, and the block of
code it executes when called. C functions are defined in a text file and the names of all
the functions in a C program are lumped together in a single, flat namespace. The special
function called "main" is where program execution begins. Some programmers like to
begin their function names with Upper case, using lower case for variables and
parameters, Here is a simple C function declaration. This declares a function named
Twice which takes a single int argument named num. The body of the function
computes the value which is twice the num argument and returns that value to the caller.
/*
Computes double of a number.
Works by tripling the number, and then subtracting to get back to double.
*/
static int Twice(int num) {
int result = num * 3;
result = result - num;
return(result);
}

Syntax
The keyword "static" defines that the function will only be available to callers in the
file where it is declared. If a function needs to be called from another file, the function
cannot be static and will require a prototype -- see prototypes below. The static form
is convenient for utility functions which will only be used in the file where they are
declared. Next , the "int" in the function above is the type of its return value. Next
comes name of the function and its list of parameters. When referring to a function by
name in documentation or other prose, it's a convention to keep the parenthesis () suffix,
so in this case I refer to the function as "Twice()". The parameters are listed with their
types and names, just like variables.
Inside the function, the parameter num and the local variable result are "local" to the
function -- they get their own memory and exist only so long as the function is executing.
This independence of "local" memory is a standard feature of most languages (See
CSLibrary/102 for the detailed discussion of local memory).
The "caller" code which calls Twice() looks like...
int num = 13;
int a = 1;
int b = 2;
a = Twice(a); // call Twice() passing the value of a
b = Twice(b + num); // call Twice() passing the value b+num
// a == 2
// b == 30
// num == 13 (this num is totally independent of the "num" local to Twice()
25

Things to notice...
(vocabulary) The expression passed to a function by its caller is called the "actual
parameter" -- such as "a" and "b + num" above. The parameter storage local to the
function is called the "formal parameter" such as the "num" in "static int Twice(int
num)".

Parameters are passed "by value" that means there is a single copying assignment
operation (=) from each actual parameter to set each formal parameter. The actual
parameter is evaluated in the caller's context, and then the value is copied into the
function's formal parameter just before the function begins executing. The alternative
parameter mechanism is "by reference" which C does not implement directly, but
which the programmer can implement manually when needed (see below). When a
parameter is a struct, it is copied.

The variables local to Twice(), num and result, only exist temporarily while
Twice() is executing. This is the standard definition for "local" storage for
functions.

The return at the end of Twice() computes the return value and exits the function.
Execution resumes with the caller. There can be multiple return statements within a
function, but it's good style to at least have one at the end if a return value needs to be
specified. Forgetting to account of a return somewhere in the middle of a function
is a traditional source of bugs.

C-ing and Nothingness -- void


void is a type formalized in ANSI C which means "nothing". To indicate that a function
does not return anything, use void as the return type. Also, by convention, a pointer
which does not point to any particular type is declared as void*. Sometimes void* is
used to force two bodies of code to not depend on each other where void* translates
roughly to "this points to something, but I'm not telling you (the client) the type of the
pointee exactly because you do not really need to know." If a function does not take any
parameters, its parameter list is empty, or it can contain the keyword void but that style
is now out of favor.
void TakesAnIntAndReturnsNothing(int anInt);

int TakesNothingAndReturnsAnInt();
int TakesNothingAndReturnsAnInt(void); // equivalent syntax for above

Call by Value vs. Call by Reference


C passes parameters "by value" which means that the actual parameter values are copied
into local storage. The caller and callee functions do not share any memory -- they each
have their own copy. This scheme is fine for many purposes, but it has two
disadvantages.
1) Because the callee has its own copy, modifications to that memory are not
communicated back to the caller. Therefore, value parameters do not allow the callee
to communicate back to the caller. The function's return value can communicate some
information back to the caller, but not all problems can be solved with the single
return value.
26

2) Sometimes it is undesirable to copy the value from the caller to the callee because the
value is large and so copying it is expensive, or because at a conceptual level copying
the value is undesirable.

The alternative is to pass the arguments "by reference". Instead of passing a copy of a
value from the caller to the callee, pass a pointer to the value. In this way there is only
one copy of the value at any time, and the caller and callee both access that one value
through pointers.
Some languages support reference parameters automatically. C does not do this -- the
programmer must implement reference parameters manually using the existing pointer
constructs in the language.

Swap Example
The classic example of wanting to modify the caller's memory is a swap() function
which exchanges two values. Because C uses call by value, the following version of
Swap will not work...
void Swap(int x, int y) { // NO does not work
int temp;

temp = x;
x = y; // these operations just change the local x,y,temp
y = temp; // -- nothing connects them back to the caller's a,b
}

// Some caller code which calls Swap()...


int a = 1;
int b = 2;
Swap(a, b);

Swap() does not affect the arguments a and b in the caller. The function above only
operates on the copies of a and b local to Swap() itself. This is a good example of how
"local" memory such as ( x, y, temp) behaves -- it exists independent of everything else
only while its owning function is running. When the owning function exits, its local
memory disappears.

Reference Parameter Technique


To pass an object X as a reference parameter, the programmer must pass a pointer to X
instead of X itself. The formal parameter will be a pointer to the value of interest. The
caller will need to use & or other operators to compute the correct pointer actual
parameter. The callee will need to dereference the pointer with * where appropriate to
access the value of interest. Here is an example of a correct Swap() function.
static void Swap(int* x, int* y) { // params are int* instead of int
int temp;

temp = *x; // use * to follow the pointer back to the caller's memory
*x = *y;
*y = temp;
}
27

// Some caller code which calls Swap()...


int a = 1;
int b = 2;

Swap(&a, &b);

Things to notice...
• The formal parameters are int* instead of int.

• The caller uses & to compute pointers to its local memory (a,b).

• The callee uses * to dereference the formal parameter pointers back to get the caller's
memory.

Since the operator & produces the address of a variable -- &a is a pointer to a. In
Swap() itself, the formal parameters are declared to be pointers, and the values of
interest (a,b) are accessed through them. There is no special relationship between the
names used for the actual and formal parameters. The function call matches up the actual
and formal parameters by their order -- the first actual parameter is assigned to the first
formal parameter, and so on. I deliberately used different names (a,b vs x,y) to emphasize
that the names do not matter.

const
The qualifier const can be added to the left of a variable or parameter type to declare that
the code using the variable will not change the variable. As a practical matter, use of
const is very sporadic in the C programming community. It does have one very handy
use, which is to clarify the role of a parameter in a function prototype...
void foo(const struct fraction* fract);

In the foo() prototype, the const declares that foo() does not intend to change the struct
fraction pointee which is passed to it. Since the fraction is passed by pointer, we could
not know otherwise if foo() intended to change our memory or not. Using the const,
foo() makes its intentions clear. Declaring this extra bit of information helps to clarify the
role of the function to its implementor and caller.
28

Bigger Pointer Example


The following code is a large example of using reference parameters. There are several
common features of C programs in this example...Reference parameters are used to allow
the functions Swap() and IncrementAndSwap() to affect the memory of their callers.
There's a tricky case inside of IncrementAndSwap() where it calls Swap() -- no additional
use of & is necessary in this case since the parameters x, y inside InrementAndSwap() are
already pointers to the values of interest. The names of the variables through the
program(a, b, x, y, alice, bob) do not need to match up in any particular way for the
parameters to work. The parameter mechanism only depends on the types of the
parameters and their order in the parameter list -- not their names. Finally this is an
example of what multiple functions look like in a file and how they are called from the
main() function.
static void Swap(int* a, int* b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}

static void IncrementAndSwap(int* x, int* y) {


(*x)++;
(*y)++;
Swap(x, y); // don't need & here since a and b are already
// int*'s.
}

int main()
{
int alice = 10;
int bob = 20;

Swap(&alice, &bob);
// at this point alice==20 and bob==10

IncrementAndSwap(&alice, &bob);
// at this point alice==11 and bob==21

return 0;
}
29

Section 5
Odds and Ends
main()
The execution of a C program begins with function named main(). All of the files and
libraries for the C program are compiled together to build a single program file. That file
must contain exactly one main() function which the operating system uses as the starting
point for the program. Main() returns an int which, by convention, is 0 if the program
completed successfully and non-zero if the program exited due to some error condition.
This is just a convention which makes sense in shell oriented environments such as Unix
or DOS.

Multiple Files
For a program of any size, it's convenient to separate the functions into several separate
files. To allow the functions in separate files to cooperate, and yet allow the compiler to
work on the files independently, C programs typically depend on two features...

Prototypes
A "prototype" for a function gives its name and arguments but not its body. In order for a
caller, in any file, to use a function, the caller must have seen the prototype for that
function. For example, here's what the prototypes would look like for Twice() and
Swap(). The function body is absent and there's a semicolon (;) to terminate the
prototype...
int Twice(int num);
void Swap(int* a, int* b);

In pre-ANSI C, the rules for prototypes where very sloppy -- callers were not required to
see prototypes before calling functions, and as a result it was possible to get in situations
where the compiler generated code which would crash horribly.
In ANSI C, I'll oversimplify a little to say that...
1) a function may be declared static in which case it can only be used in the same file
where it is used below the point of its declaration. Static functions do not require a
separate prototype so long as they are defined before or above where they are called
which saves some work.

2) A non-static function needs a prototype. When the compiler compiles a function


definition, it must have previously seen a prototype so that it can verify that the two
are in agreement ("prototype before definition" rule). The prototype must also be seen
by any client code which wants to call the function ("clients must see prototypes"
rule).(The require-prototypes behavior is actually somewhat of a compiler option, but
it's smart to leave it on.)

Preprocessor
The preprocessing step happens to the C source before it is fed to the compiler. The two
most common preprocessor directives are #define and #include...
30

#define
The #define directive can be used to set up symbolic replacements in the source. As with
all preprocessor operations, #define is extremely unintelligent -- it just does textual
replacement without understanding. #define statements are used as a crude way of
establishing symbolic constants.
#define MAX 100
#define SEVEN_WORDS that_symbol_expands_to_all_these_words

Later code can use the symbols MAX or SEVEN_WORDS which will be replaced by the
text to the right of each symbol in its #define.

#include
The "#include" directive brings in text from different files during compilation. #include is
a very unintelligent and unstructured -- it just pastes in the text from the given file and
continues compiling. The #include directive is used in the .h/.c file convention below
which is used to satisfy the various constraints necessary to get prototypes correct.
#include "foo.h" // refers to a "user" foo.h file --
// in the originating directory for the compile

#include <foo.h> // refers to a "system" foo.h file --


// in the compiler's directory somewhere

foo.h vs foo.c
The universally followed convention for C is that for a file named "foo.c" containing a
bunch of functions...
• A separate file named foo.h will contain the prototypes for the functions in foo.c
which clients may want to call. Functions in foo.c which are for "internal use
only" and should never be called by clients should be declared static.

• Near the top of foo.c will be the following line which ensures that the function
definitions in foo.c see the prototypes in foo.h which ensures the "prototype
before definition" rule above.
#include "foo.h" // show the contents of "foo.h"
// to the compiler at this point

• Any xxx.c file which wishes to call a function defined in foo.c must include the
following line to see the prototypes, ensuring the "clients must see prototypes" rule
above.
#include "foo.h"
31

#if
At compile time, there is some space of names defined by the #defines. The #if test can
be used at compile-time to look at those symbols and turn on and off which lines the
compiler uses. The following example depends on the value of the FOO #define symbol.
If it is true, then the "aaa" lines (whatever they are) are compiled, and the "bbb" lines are
ignored. If FOO were 0, then the reverse would be true.
#define FOO 1

...

#if FOO
aaa
aaa
#else
bbb
bbb
#endif

You can use #if 0 ...#endif to effectively comment out areas of code you don't
want to compile, but which you want to keeep in the source file.

Multiple #includes -- #pragma once


There's a problem sometimes where a .h file is #included into a file more than one time
resulting in compile errors. This can be a serious problem. Because of this, you want to
avoid #including .h files in other .h files if at all possible. On the other hand, #including
.h files in .c files is fine. If you are lucky, your compiler will support the #pragma once
feature which automatically prevents a single file from being #included more than once in
any one file. This largely solves multiple #include problems.
// foo.h
// The following line prevents problems in files which #include "foo.h"
#pragma once

<rest of foo.h ...>

Assert
Array out of bounds references are an extremely common form of C run-time error. You
can use the assert() function to sprinkle your code with your own bounds checks. A few
seconds putting in assert statements can save you hours of debugging.
Getting out all the bugs is the hardest and scariest part of writing a large piece of
software. Assert statements are one of the easiest and most effective helpers for that
difficult phase.
#include <assert.h>
#define MAX_INTS 100
{
int ints[MAX_INTS];
i = foo(<something complicated>); // i should be in bounds,
// but is it really?
assert(i>=0); // safety assertions
assert(i<MAX_INTS);

ints[i] = 0;
32

Depending on the options specified at compile time, the assert() expressions will be left
in the code for testing, or may be ignored. For that reason, it is important to only put
expressions in assert() tests which do not need to be evaluated for the proper functioning
of the program...
int errCode = foo(); // yes
assert(errCode == 0);

assert(foo() == 0); // NO, foo() will not be called if


// the compiler removes the assert()
33

Section 6
Advanced Arrays and Pointers
Advanced C Arrays
In C, an array is formed by laying out all the elements contiguously in memory. The
square bracket syntax can be used to refer to the elements in the array. The array as a
whole is referred to by the address of the first element which is also known as the "base
address" of the whole array.
{
int array[6];

int sum = 0;
sum += array[0] + array[1]; // refer to elements using []
}

array
The array name acts like a pointer to the
first element- in this case an (int*).

array[0] array[1] array[2] ...


Index 0 1 2 3 4 5

The programmer can refer to elements in the array with the simple [ ] syntax such as
array[1]. This scheme works by combining the base address of the whole array with
the index to compute the base address of the desired element in the array. It just requires
a little arithmetic. Each element takes up a fixed number of bytes which is known at
compile-time. So the address of element n in the array using 0 based indexing will be at
an offset of (n * element_size) bytes from the base address of the whole array.
address of nth element = address_of_0th_element + (n * element_size_in_bytes)
The square bracket syntax [ ] deals with this address arithmetic for you, but it's useful to
know what it's doing. The [ ] takes the integer index, multiplies by the element size, adds
the resulting offset to the array base address, and finally dereferences the resulting pointer
to get to the desired element.
{
int intArray[6];

intArray[3] = 13;
}
34

intArray (intArray+3)
12 bytes of offset

13
Index 0 1 2 3 4 5
Offset 0 4 8 12 16 20
in bytes =
n * elem_size
Assume sizeof(int) = 4i.e. Each array
element takes up 4 bytes.

'+' Syntax
In a closely related piece of syntax, a + between a pointer and an integer does the same
offset computation, but leaves the result as a pointer. The square bracket syntax gives the
nth element while the + syntax gives a pointer to the nth element.
So the expression (intArray + 3) is a pointer to the integer intArray[3].
(intArray + 3) is of type (int*) while intArray[3] is of type int. The two
expressions only differ by whether the pointer is dereferenced or not. So the expression
(intArray + 3) is exactly equivalent to the expression (&(intArray[3])). In
fact those two probably compile to exactly the same code. They both represent a pointer
to the element at index 3.
Any [] expression can be written with the + syntax instead. We just need to add in the
pointer dereference. So intArray[3] is exactly equivalent to *(intArray + 3).
For most purposes, it's easiest and most readable to use the [] syntax. Every once in a
while the + is convenient if you needed a pointer to the element instead of the element
itself.

Pointer++ Style -- strcpy()


If p is a pointer to an element in an array, then (p+1) points to the next element in the
array. Code can exploit this using the construct p++ to step a pointer over the elements in
an array. It doesn't help readability any, so I can't recommend the technique, but you may
see it in code written by others.
(This example was originally inspired by Mike Cleron) There's a library function called
strcpy(char* destination, char* source) which copies the bytes of a C
string from one place to another. Below are four different implementations of strcpy()
written in order: from most verbose to most cryptic. In the first one, the normally
straightforward while loop is actually sortof tricky to ensure that the terminating null
character is copied over. The second removes that trickiness by moving assignment into
the test. The last two are cute (and they demonstrate using ++ on pointers), but not really
the sort of code you want to maintain. Among the four, I think strcpy2() is the best
stylistically. With a smart compiler, all four will compile to basically the same code with
the same efficiency.
35

// Unfortunately, a straight while or for loop won't work.


// The best we can do is use a while (1) with the test
// in the middle of the loop.
void strcpy1(char dest[], const char source[]) {
int i = 0;

while (1) {
dest[i] = source[i];
if (dest[i] == '\0') break; // we're done
i++;
}
}

// Move the assignment into the test


void strcpy2(char dest[], const char source[]) {
int i = 0;

while ((dest[i] = source[i]) != '\0') {


i++;
}
}

// Get rid of i and just move the pointers.


// Relies on the precedence of * and ++.
void strcpy3(char dest[], const char source[])
{
while ((*dest++ = *source++) != '\0') ;
}

// Rely on the fact that '\0' is equivalent to FALSE


void strcpy4(char dest[], const char source[])
{
while (*dest++ = *source++) ;
}

Pointer Type Effects


Both [ ] and + implicitly use the compile time type of the pointer to compute the
element_size which affects the offset arithmetic. When looking at code, it's easy to
assume that everything is in the units of bytes.
int *p;

p = p + 12; // at run-time, what does this add to p? 12?

The above code does not add the number 12 to the address in p-- that would increment p
by 12 bytes. The code above increments p by 12 ints. Each int probably takes 4 bytes, so
at run time the code will effectively increment the address in p by 48. The compiler
figures all this out based on the type of the pointer.
Using casts, the following code really does just add 12 to the address in the pointer p. It
works by telling the compiler that the pointer points to char instead of int. The size of
char is defined to be exactly 1 byte (or whatever the smallest addressable unit is on the
computer). In other words, sizeof(char) is always 1. We then cast the resulting
36

(char*) back to an (int*). The programmer is allowed to cast any pointer type to
any other pointer type like this to change the code the compiler generates.
p = (int*) ( ((char*)p) + 12);

Arrays and Pointers


One effect of the C array scheme is that the compiler does not distinguish meaningfully
between arrays and pointers-- they both just look like pointers. In the following example,
the value of intArray is a pointer to the first element in the array so it's an (int*).
The value of the variable intPtr is also (int*) and it is set to point to a single integer
i. So what's the difference between intArray and intPtr? Not much as far as the
compiler is concerned. They are both just (int*) pointers, and the compiler is perfectly
happy to apply the [] or + syntax to either. It's the programmer's responsibility to ensure
that the elements referred to by a [] or + operation really are there. Really its' just the
same old rule that C doesn't do any bounds checking. C thinks of the single integer i as
just a sort of degenerate array of size 1.
{
int intArray[6];
int *intPtr;
int i;

intPtr = &i;

intArray[3] = 13; // ok
intPtr[0] = 12; // odd, but ok. Changes i.
intPtr[3] = 13; // BAD! There is no integer reserved here!
}
37

intArray (intArray+3)

13
Index 0 1 2 3 4 5

intPtr (intPtr+3)

12 13

These bytes exist, but they have not been explicitly reserved.
They are the bytes which happen to be adjacent to the
memory for i. They are probably being used to store
something already, such as a smashed looking smiley face.
The 13 just gets blindly written over the smiley face. This
error will only be apparent later when the program tries to
read the smiley face data.

Array Names Are Const


One subtle distinction between an array and a pointer, is that the pointer which represents
the base address of an array cannot be changed in the code. The array base address
behaves like a const pointer. The constraint applies to the name of the array where it is
declared in the code-- the variable ints in the example below.
{
int ints[100]
int *p;
int i;

ints = NULL; // NO, cannot change the base addr ptr


ints = &i; // NO
ints = ints + 1; // NO
ints++; // NO

p = ints; // OK, p is a regular pointer which can be changed


// here it is getting a copy of the ints pointer

p++; // OK, p can still be changed (and ints cannot)


p = NULL; // OK
p = &i; // OK

foo(ints); // OK (possible foo definitions are below)


}
38

Array parameters are passed as pointers. The following two definitions of foo look
different, but to the compiler they mean exactly the same thing. It's preferable to use
whichever syntax is more accurate for readability. If the pointer coming in really is the
base address of a whole array, then use [ ].
void foo(int arrayParam[]) {
arrayParam = NULL; // Silly but valid. Just changes the local pointer
}

void foo(int *arrayParam) {


arrayParam = NULL; // ditto
}

Heap Memory
C gives programmers the standard sort of facilities to allocate and deallocate dynamic
heap memory. A word of warning: writing programs which manage their heap memory is
notoriously difficult. This partly explains the great popularity of languages such as Java
and Perl which handle heap management automatically. These languages take over a task
which has proven to be extremely difficult for the programmer. As a result Perl and Java
programs run a little more slowly, but they contain far fewer bugs. (For a detailed
discussion of heap memory see http://cslibrary.stanford.edu/102/, Pointers and Memory.)
C provides access to the heap features through library functions which any C code can
call. The prototypes for these functions are in the file <stdlib.h>, so any code which
wants to call these must #include that header file. The three functions of interest are...
void* malloc(size_t size) Request a contiguous block of memory
of the given size in the heap. malloc() returns a pointer to the heap block or NULL if
the request could not be satisfied. The type size_t is essentially an unsigned
long which indicates how large a block the caller would like measured in bytes.
Because the block pointer returned by malloc() is a void* (i.e. it makes no claim
about the type of its pointee), a cast will probably be required when storing the void*
pointer into a regular typed pointer.

void free(void* block) The mirror image of malloc() -- free takes a


pointer to a heap block earlier allocated by malloc() and returns that block to the heap
for re-use. After the free(), the client should not access any part of the block or
assume that the block is valid memory. The block should not be freed a second time.

void* realloc(void* block, size_t size); Take an existing heap


block and try to relocate it to a heap block of the given size which may be larger or
smaller than the original size of the block. Returns a pointer to the new block, or
NULL if the relocation was unsuccessful. Remember to catch and examine the return
value of realloc() -- it is a common error to continue to use the old block pointer.
Realloc() takes care of moving the bytes from the old block to the new block.
Realloc() exists because it can be implemented using low-level features which make
it more efficient than C code the client could write.

Memory Management
All of a program's memory is deallocated automatically when the it exits, so a program
only needs to use free() during execution if it is important for the program to recycle its
memory while it runs -- typically because it uses a lot of memory or because it runs for a
39

long time. The pointer passed to free() must be exactly the pointer which was originally
returned by malloc() or realloc(), not just a pointer into somewhere within the heap block.

Dynamic Arrays
Since arrays are just contiguous areas of bytes, you can allocate your own arrays in the
heap using malloc(). The following code allocates two arrays of 1000 ints-- one in the
stack the usual "local" way, and one in the heap using malloc(). Other than the different
allocations, the two are syntactically similar in use.
{
int a[1000];

int *b;
b = (int*) malloc( sizeof(int) * 1000);
assert(b != NULL); // check that the allocation succeeded

a[123] = 13; // Just use good ol' [] to access elements


b[123] = 13; // in both arrays.

free(b);
}

Although both arrays can be accessed with [ ], the rules for their maintenance are very
different....

Advantages of being in the heap


• Size (in this case 1000) can be defined at run time. Not so for an array like "a".

• The array will exist until it is explicitly deallocated with a call to free().

• You can change the size of the array at will at run time using realloc(). The following
changes the size of the array to 2000. Realloc() takes care of copying over the old
elements.

...
b = realloc(b, sizeof(int) * 2000);
assert(b != NULL);

Disadvantages of being in the heap


• You have to remember to allocate the array, and you have to get it right.

• You have to remember to deallocate it exactly once when you are done with it, and you
have to get that right.

• The above two disadvantages have the same basic profile: if you get them wrong, your
code still looks right. It compiles fine. It even runs for small cases, but for some input
cases it just crashes unexpectedly because random memory is getting overwritten
somewhere like the smiley face. This sort of "random memory smasher" bug can be a
real ordeal to track down.
40

Dynamic Strings
The dynamic allocation of arrays works very well for allocating strings in the heap. The
advantage of heap allocating a string is that the heap block can be just big enough to store
the actual number of characters in the string. The common local variable technique such
as char string[1000]; allocates way too much space most of the time, wasting
the unused bytes, and yet fails if the string ever gets bigger than the variable's fixed size.
#include <string.h>

/*
Takes a c string as input, and makes a copy of that string
in the heap. The caller takes over ownership of the new string
and is responsible for freeing it.
*/
char* MakeStringInHeap(const char* source) {
char* newString;

newString = (char*) malloc(strlen(source) + 1); // +1 for the '\0'


assert(newString != NULL);
strcpy(newString, source);
return(newString);
}
41

Section 7
Details and Library Functions
Precedence and Associativity
function-call() [] -> . L to R

! ~ ++ -- + - *(ptr deref) sizeof &(addr of) R to L


(all unary ops are the same)

* / % L to R
(the top tier arithmetic binary ops)

+ - L to R
(second tier arithmetic binary ops)

< <= > >= L to R

== != L to R

in order: & ^ | && || L to R


(note that bitwise comes before boolean)

= and all its variants R to L

, (comma) . L to R

A combinations which never works right without parens: *structptr.field


You have to write it as (*structptr).field or structptr->field

Standard Library Functions


Many basic housekeeping funcions are available to a C program in form of standard
library functions. To call these, a program must #include the appropriate .h file. Most
compilers link in the standard library code by default. The functions listed in the next
section are the most commonly used ones, but there are many more which are not listed
here.
stdio.h file input and output
ctype.h character tests
string.h string operations
math.h mathematical functions such as sin() and cos()
stdlib.h utility functions such as malloc() and rand()
assert.h the assert() debugging macro
stdarg.h support for functions with variable numbers of arguments
setjmp.h support for non-local flow control jumps
signal.h support for exceptional condition signals
time.h date and time
42

limits.h, float.h constants which define type range values such as INT_MAX

stdio.h
Stdio.h is a very common file to #include -- it includes functions to print and read strings
from files and to open and close files in the file system.
FILE* fopen(const char* fname, const char* mode);
Open a file named in the filesystem and return a FILE* for it. Mode = "r" read,"w"
write,"a"append, returns NULL on error. The standard files stdout, stdin,
stderr are automatically opened and closed for you by the system.

int fclose(FILE* file);


Close a previously opened file. Returns EOF on error. The operating system closes all
of a program's files when it exits, but it's tidy to do it beforehand. Also, there is
typically a limit to the number of files which a program may have open
simultaneously.

int fgetc(FILE* in);


Read and return the next unsigned char out of a file, or EOF if the file has been
exhausted. (detail) This and other file functions return ints instead of a chars because
the EOF constant they potentially is not a char, but is an int. getc() is an alternate,
faster version implemented as a macro which may evaluate the FILE* expression
more than once.

char* fgets(char* dest, int n, FILE* in)


Reads the next line of text into a string supplied by the caller. Reads at most n-1
characters from the file, stopping at the first '\n' character. In any case, the string is '\0'
terminated. The '\n' is included in the string. Returns NULL on EOF or error.

int fputc(int ch, FILE* out);


Write the char to the file as an unsigned char. Returns ch, or EOF on err. putc() is an
alternate, faster version implemented as a macro which may evaluate the FILE*
expression more than once.

int ungetc(int ch, FILE* in);


Push the most recent fgetc() char back onto the file. EOF may not be pushed back.
Returns ch or EOF on error.

int printf(const char* format_string, ...);


Prints a string with values possibly inserted into it to standard output. Takes a variable
number of arguments -- first a format string followed by a number of matching
arguments. The format string contains text mixed with % directives which mark
things to be inserted in the output. %d = int, %Ld=long int, %s=string, %f=double,
%c=char. Every % directive must have a matching argument of the correct type after
the format string. Returns the number of characters written, or negative on error. If
the percent directives do not match the number and type of arguments, printf() tends
to crash or otherwise do the wrong thing at run time. fprintf() is a variant which takes
an additional FILE* argument which specifies the file to print to. Examples...
printf("hello\n"); prints: hello
printf("hello %d there %d\n", 13, 1+1); prints: hello 13 there 2
printf("hello %c there %d %s\n", 'A', 42, "ok"); prints: hello A there 42 ok
43

int scanf(const char* format, ...)


Opposite of printf() -- reads characters from standard input trying to match elements
in the format string. Each percent directive in the format string must have a matching
pointer in the argument list which scanf() uses to store the values it finds. scanf()
skips whitespace as it tries to read in each percent directive. Returns the number of
percent directives processed successfully, or EOF on error. scanf() is famously
sensitive to programmer errors. If scanf() is called with anything but the correct
pointers after the format string, it tends to crash or otherwise do the wrong thing at
run time. sscanf() is a variant which takes an additional initial string from which it
does its reading. fscanf() is a variant which takes an additional initial FILE* from
which it does its reading. Example...
{
int num;
char s1[1000];
char s2[1000];

scanf("hello %d %s %s", &num, s1, s2);


}
Looks for the word "hello" followed by a number and two words (all separated by
whitespace). scanf() uses the pointers &num, s1, and s2 to store what it finds into the
local variables.

ctype.h
ctype.h includes macros for doing simple tests and operations on characters
isalpha(ch) // ch is an upper or lower case letter

islower(ch), isupper(ch) // same as above, but upper/lower specific

isspace(ch) // ch is a whitepace character such as tab, space, newline, etc.

isdigit(ch) // digit such as '0'..'9'

toupper(ch), tolower(ch) // Return the lower or upper case version of a


alphabetic character, otherwise pass it through unchanged.
44

string.h
None of these string routines allocate memory or check that the passed in memory is the
right size. The caller is responsible for making sure there is "enough" memory for the
operation. The type size_t is an unsigned integer wide enough for the computer's
address space -- most likely an unsigned long.
size_t strlen(const char* string);
Return the number of chars in a C string. EG strlen("abc")==3

char* strcpy(char* dest, const char* source);


Copy the characters from the source string to the destination string.

size_t strlcpy(char* dest, const char* source,


size_t dest_size);
Like strcpy(), but knows the size of the dest. Truncates if necessary. Use this to avoid
memory errors and buffer-overflow security problems. This function is not as
standard as strcpy(), but most sytems have it. Do not use the old strncpy() function --
it is difficult to use correctly.

char *strcat(char* dest, const char* source);


Append the characters from the source string to the end of destination string. (There is
a non-standard strlcat() variant that takes the size of the dest as third argument.)

int strcmp(const char* a, const char* b);


Compare two strings and return an int which encodes their ordering. zero:a==b,
negative:a<b, positive:a>b. It is a common error to think of the result of strcmp() as
being boolean true if the strings are equal which is, unfortunately, exactly backwards.

char* strchr(const char* searchIn, char ch);


Search the given string for the first occurence of the given character. Returns a
pointer to the character, or NULL if none is found.

char* strstr(const char* searchIn, const char* searchFor);


Similar to strchr(), but searches for an entire string instead of a single character. The
search is case sensitive.

void* memcpy(void* dest, const void* source, size_t n);


Copy the given number of bytes from the source to the destination. The source and
destination must not overlap. This may be implemented in a specialized but highly
optimized way for a particular computer.

void* memmove(void* dest, const void* source, size_t n);


Similar to memcpy() but allows the areas to overlap. This probably runs slightly
slower than memcpy().
45

stdlib.h
int rand();
Returns a pseudo random integer in the range 0..RAND_MAX (limits.h) which is at
least 32767.

void srand(unsigned int seed);


The sequence of random numbers returned by rand() is initially controlled by a global
"seed" variable. srand() sets this seed which, by default, starts with the value 1. Pass
the expression time(NULL) (time.h) to set the seed to a value based on the current
time to ensure that the random sequence is different from one run to the next.

void* malloc(size_t size);


Allocate a heap block of the given size in bytes. Returns a pointer to the block or
NULL on failure. A cast may be required to store the void* pointer into a regular
typed pointer. [ed: see the Heap Allocation section above for the longer discussion of
malloc(), free(), and realloc()]
void free(void* block);
Opposite of malloc(). Returns a previous malloc block to the system for reuse

void* realloc(void* block, size_t size);


Resize an existing heap block to the new size. Takes care of copying bytes from the
old block to the new. Returns the new base address of the heap block. It is a common
error to forget to catch the return value from realloc(). Returns NULL if the resize
operation was not possible.

void exit(int status);


Halt and exit the program and pass a condition int back to the operating sytem. Pass 0
to signal normal program termination, non-zero otherwise.

void* bsearch(const void* key, const void* base, size_t len,


size_t elem_size, <compare_function>);
Do a binary search in an array of elements. The last argument is a function which
takes pointers to the two elements to compare. Its prototype should be:
int compare(const void* a, const void* b);, and it should return 0, -1, or 1 as strcmp()
does. Returns a pointer to a found element, or NULL otherwise. Note that strcmp()
itself cannot be used directly as a compare function for bsearch() on an array of char*
strings because strcmp() takes char* arguments and bsearch() will need a comparator
that takes pointers to the array elements -- char**.

void qsort(void* base, size_t len, size_t elem_size,


<compare_function>);
Sort an array of elements. Takes a function pointer just like besearch().

Revision History
11/1998 -- original major version. Based on my old C handout for CS107. Thanks to Jon
Becker for proofreading and Mike Cleron for the original inspiration.
Revised 4/2003 with many helpful typo and other suggestions from Negar Shamma and
A. P. Garcia
Linked List
Basics
By Nick Parlante Copyright © 1998-2001, Nick Parlante

Abstract
This document introduces the basic structures and techniques for building linked lists
with a mixture of explanations, drawings, sample code, and exercises. The material is
useful if you want to understand linked lists or if you want to see a realistic, applied
example of pointer-intensive code. A separate document, Linked List Problems
(http://cslibrary.stanford.edu/105/), presents 18 practice problems covering a wide range
of difficulty.
Linked lists are useful to study for two reasons. Most obviously, linked lists are a data
structure which you may want to use in real programs. Seeing the strengths and
weaknesses of linked lists will give you an appreciation of the some of the time, space,
and code issues which are useful to thinking about any data structures in general.
Somewhat less obviously, linked lists are great way to learn about pointers. In fact, you
may never use a linked list in a real program, but you are certain to use lots of pointers.
Linked list problems are a nice combination of algorithms and pointer manipulation.
Traditionally, linked lists have been the domain where beginning programmers get the
practice to really understand pointers.

Audience
The article assumes a basic understanding of programming and pointers. The article uses
C syntax for its examples where necessary, but the explanations avoid C specifics as
much as possible — really the discussion is oriented towards the important concepts of
pointer manipulation and linked list algorithms.

Other Resources
• Link List Problems (http://cslibrary.stanford.edu/105/) Lots of linked
list problems, with explanations, answers, and drawings. The "problems"
article is a companion to this "explanation" article.

• Pointers and Memory (http://cslibrary.stanford.edu/102/) Explains all


about how pointers and memory work. You need some understanding of
pointers and memory before you can understand linked lists.

• Essential C (http://cslibrary.stanford.edu/101/) Explains all the basic


features of the C programming language.

This is document #103, Linked List Basics, in the Stanford CS Education Library. This
and other free educational materials are available at http://cslibrary.stanford.edu/. This
document is free to be used, reproduced, or sold so long as this notice is clearly
reproduced at its beginning.
2

Contents
Section 1 — Basic List Structures and Code 2
Section 2 — Basic List Building 11
Section 3 — Linked List Code Techniques 17
Section 3 — Code Examples 22

Edition
Originally 1998 there was just one "Linked List" document that included a basic
explanation and practice problems. In 1999, it got split into two documents: #103 (this
document) focuses on the basic introduction, while #105 is mainly practice problems.
This 4-12-2001 edition represents minor edits on the 1999 edition.

Dedication
This document is distributed for free for the benefit and education of all. That a person
seeking knowledge should have the opportunity to find it. Thanks to Stanford and my
boss Eric Roberts for supporing me in this project. Best regards, Nick --
nick.parlante@cs.stanford.edu

Section 1 —
Linked List Basics
Why Linked Lists?
Linked lists and arrays are similar since they both store collections of data. The
terminology is that arrays and linked lists store "elements" on behalf of "client" code. The
specific type of element is not important since essentially the same structure works to
store elements of any type. One way to think about linked lists is to look at how arrays
work and think about alternate approaches.

Array Review
Arrays are probably the most common data structure used to store collections of
elements. In most languages, arrays are convenient to declare and the provide the handy
[ ] syntax to access any element by its index number. The following example shows some
typical array code and a drawing of how the array might look in memory. The code
allocates an array int scores[100], sets the first three elements set to contain the
numbers 1, 2, 3 and leaves the rest of the array uninitialized...
void ArrayTest() {
int scores[100];

// operate on the elements of the scores array...


scores[0] = 1;
scores[1] = 2;
scores[2] = 3;
}
3

Here is a drawing of how the scores array might look like in memory. The key point is
that the entire array is allocated as one block of memory. Each element in the array gets
its own space in the array. Any element can be accessed directly using the [ ] syntax.

scores

1 2 3 -3451 23142

index 0 1 2 3 99

Once the array is set up, access to any element is convenient and fast with the [ ]
operator. (Extra for experts) Array access with expressions such as scores[i] is
almost always implemented using fast address arithmetic: the address of an element is
computed as an offset from the start of the array which only requires one multiplication
and one addition.
The disadvantages of arrays are...
1) The size of the array is fixed — 100 elements in this case. Most often this
size is specified at compile time with a simple declaration such as in the
example above . With a little extra effort, the size of the array can be
deferred until the array is created at runtime, but after that it remains fixed.
(extra for experts) You can go to the trouble of dynamically allocating an
array in the heap and then dynamically resizing it with realloc(), but that
requires some real programmer effort.

2) Because of (1), the most convenient thing for programmers to do is to


allocate arrays which seem "large enough" (e.g. the 100 in the scores
example). Although convenient, this strategy has two disadvantages: (a)
most of the time there are just 20 or 30 elements in the array and 70% of
the space in the array really is wasted. (b) If the program ever needs to
process more than 100 scores, the code breaks. A surprising amount of
commercial code has this sort of naive array allocation which wastes space
most of the time and crashes for special occasions. (Extra for experts) For
relatively large arrays (larger than 8k bytes), the virtual memory system
may partially compensate for this problem, since the "wasted" elements
are never touched.

3) (minor) Inserting new elements at the front is potentially expensive


because existing elements need to be shifted over to make room.

Linked lists have their own strengths and weaknesses, but they happen to be strong where
arrays are weak. The array's features all follow from its strategy of allocating the memory
for all its elements in one block of memory. Linked lists use an entirely different strategy.
As we will see, linked lists allocate memory for each element separately and only when
necessary.

Pointer Refresher
Here is a quick review of the terminology and rules for pointers. The linked list code to
follow will depend on these rules. (For much more detailed coverage of pointers and
memory, see Pointers and Memory, http://cslibrary.stanford.edu/102/).
4

• Pointer/Pointee A "pointer" stores a reference to another variable


sometimes known as its "pointee". Alternately, a pointer may be set to the
value NULL which encodes that it does not currently refer to a pointee. (In
C and C++ the value NULL can be used as a boolean false).

• Dereference The dereference operation on a pointer accesses its pointee.


A pointer may only be dereferenced after it has been set to refer to a
specific pointee. A pointer which does not have a pointee is "bad" (below)
and should not be dereferenced.

• Bad Pointer A pointer which does not have an assigned a pointee is


"bad" and should not be dereferenced. In C and C++, a dereference on a
bad sometimes crashes immediately at the dereference and sometimes
randomly corrupts the memory of the running program, causing a crash or
incorrect computation later. That sort of random bug is difficult to track
down. In C and C++, all pointers start out with bad values, so it is easy
to use bad pointer accidentally. Correct code sets each pointer to have a
good value before using it. Accidentally using a pointer when it is bad is
the most common bug in pointer code. In Java and other runtime oriented
languages, pointers automatically start out with the NULL value, so
dereferencing one is detected immediately. Java programs are much easier
to debug for this reason.

• Pointer assignment An assignment operation between two pointers like


p=q; makes the two pointers point to the same pointee. It does not copy
the pointee memory. After the assignment both pointers will point to the
same pointee memory which is known as a "sharing" situation.

• malloc() malloc() is a system function which allocates a block of


memory in the "heap" and returns a pointer to the new block. The
prototype for malloc() and other heap functions are in stdlib.h. The
argument to malloc() is the integer size of the block in bytes. Unlike local
("stack") variables, heap memory is not automatically deallocated when
the creating function exits. malloc() returns NULL if it cannot fulfill the
request. (extra for experts) You may check for the NULL case with
assert() if you wish just to be safe. Most modern programming systems
will throw an exception or do some other automatic error handling in their
memory allocator, so it is becoming less common that source code needs
to explicitly check for allocation failures.

• free() free() is the opposite of malloc(). Call free() on a block of heap


memory to indicate to the system that you are done with it. The argument
to free() is a pointer to a block of memory in the heap — a pointer which
some time earlier was obtained via a call to malloc().

What Linked Lists Look Like


An array allocates memory for all its elements lumped together as one block of memory.
In contrast, a linked list allocates space for each element separately in its own block of
memory called a "linked list element" or "node". The list gets is overall structure by using
pointers to connect all its nodes together like the links in a chain.
Each node contains two fields: a "data" field to store whatever element type the list holds
for its client, and a "next" field which is a pointer used to link one node to the next node.
Each node is allocated in the heap with a call to malloc(), so the node memory continues
to exist until it is explicitly deallocated with a call to free(). The front of the list is a
5

pointer to the first node. Here is what a list containing the numbers 1, 2, and 3 might look
like...

The Drawing Of List {1, 2, 3}

Stack Heap
BuildOneTwoThree()
head The overall list is built by connecting the
nodes together by their next pointers. The
nodes are all allocated in the heap.

1 2 3

A “head” pointer local to Each node Each node stores The next field of
BuildOneTwoThree() keeps stores one one next pointer. the last node is
the whole list by storing a data element NULL.
pointer to the first node. (int in this
example).

This drawing shows the list built in memory by the function BuildOneTwoThree() (the
full source code for this function is below). The beginning of the linked list is stored in a
"head" pointer which points to the first node. The first node contains a pointer to the
second node. The second node contains a pointer to the third node, ... and so on. The last
node in the list has its .next field set to NULL to mark the end of the list. Code can access
any node in the list by starting at the head and following the .next pointers. Operations
towards the front of the list are fast while operations which access node farther down the
list take longer the further they are from the front. This "linear" cost to access a node is
fundamentally more costly then the constant time [ ] access provided by arrays. In this
respect, linked lists are definitely less efficient than arrays.
Drawings such as above are important for thinking about pointer code, so most of the
examples in this article will associate code with its memory drawing to emphasize the
habit. In this case the head pointer is an ordinary local pointer variable, so it is drawn
separately on the left to show that it is in the stack. The list nodes are drawn on the right
to show that they are allocated in the heap.

The Empty List — NULL


The above is a list pointed to by head is described as being of "length three" since it is
made of three nodes with the .next field of the last node set to NULL. There needs to be
some representation of the empty list — the list with zero nodes. The most common
representation chosen for the empty list is a NULL head pointer. The empty list case is
the one common weird "boundary case" for linked list code. All of the code presented in
this article works correctly for the empty list case, but that was not without some effort.
When working on linked list code, it's a good habit to remember to check the empty list
case to verify that it works too. Sometimes the empty list case works the same as all the
cases, but sometimes it requires some special case code. No matter what, it's a good case
to at least think about.
6

Linked List Types: Node and Pointer


Before writing the code to build the above list, we need two data types...
• Node The type for the nodes which will make up the body of the list.
These are allocated in the heap. Each node contains a single client data
element and a pointer to the next node in the list. Type: struct node

struct node {
int data;
struct node* next;
};

• Node Pointer The type for pointers to nodes. This will be the type of the
head pointer and the .next fields inside each node. In C and C++, no
separate type declaration is required since the pointer type is just the node
type followed by a '*'. Type: struct node*

BuildOneTwoThree() Function
Here is simple function which uses pointer operations to build the list {1, 2, 3}. The
memory drawing above corresponds to the state of memory at the end of this function.
This function demonstrates how calls to malloc() and pointer assignments (=) work to
build a pointer structure in the heap.
/*
Build the list {1, 2, 3} in the heap and store
its head pointer in a local stack variable.
Returns the head pointer to the caller.
*/
struct node* BuildOneTwoThree() {
struct node* head = NULL;
struct node* second = NULL;
struct node* third = NULL;

head = malloc(sizeof(struct node)); // allocate 3 nodes in the heap


second = malloc(sizeof(struct node));
third = malloc(sizeof(struct node));

head->data = 1; // setup first node


head->next = second; // note: pointer assignment rule

second->data = 2; // setup second node


second->next = third;

third->data = 3; // setup third link


third->next = NULL;

// At this point, the linked list referenced by "head"


// matches the list in the drawing.
return head;
}

Exercise
Q: Write the code with the smallest number of assignments (=) which will build the
above memory structure. A: It requires 3 calls to malloc(). 3 int assignments (=) to setup
the ints. 4 pointer assignments to setup head and the 3 next fields. With a little cleverness
and knowledge of the C language, this can all be done with 7 assignment operations (=).
7

Length() Function
The Length() function takes a linked list and computes the number of elements in the list.
Length() is a simple list function, but it demonstrates several concepts which will be used
in later, more complex list functions...
/*
Given a linked list head pointer, compute
and return the number of nodes in the list.
*/
int Length(struct node* head) {
struct node* current = head;
int count = 0;

while (current != NULL) {


count++;
current = current->next;
}

return count;
}

There are two common features of linked lists demonstrated in Length()...

1) Pass The List By Passing The Head Pointer


The linked list is passed in to Length() via a single head pointer. The pointer is copied
from the caller into the "head" variable local to Length(). Copying this pointer does not
duplicate the whole list. It only copies the pointer so that the caller and Length() both
have pointers to the same list structure. This is the classic "sharing" feature of pointer
code. Both the caller and length have copies of the head pointer, but they share the
pointee node structure.

2) Iterate Over The List With A Local Pointer


The code to iterate over all the elements is a very common idiom in linked list code....
struct node* current = head;
while (current != NULL) {
// do something with *current node

current = current->next;
}

The hallmarks of this code are...


1) The local pointer, current in this case, starts by pointing to the same
node as the head pointer with current = head;. When the function
exits, current is automatically deallocated since it is just an ordinary
local, but the nodes in the heap remain.

2) The while loop tests for the end of the list with (current != NULL).
This test smoothly catches the empty list case — current will be NULL
on the first iteration and the while loop will just exit before the first
iteration.

3) At the bottom of the while loop, current = current->next;


advances the local pointer to the next node in the list. When there are no
more links, this sets the pointer to NULL. If you have some linked list
8

code which goes into an infinite loop, often the problem is that step (3) has
been forgotten.

Calling Length()
Here's some typical code which calls Length(). It first calls BuildOneTwoThree() to make
a list and store the head pointer in a local variable. It then calls Length() on the list and
catches the int result in a local variable.
void LengthTest() {
struct node* myList = BuildOneTwoThree();

int len = Length(myList); // results in len == 3


}

Memory Drawings
The best way to design and think about linked list code is to use a drawing to see how the
pointer operations are setting up memory. There are drawings below of the state of
memory before and during the call to Length() — take this opportunity to practice
looking at memory drawings and using them to think about pointer intensive code. You
will be able to understand many of the later, more complex functions only by making
memory drawings like this on your own.
Start with the Length() and LengthTest() code and a blank sheet of paper. Trace through
the execution of the code and update your drawing to show the state of memory at each
step. Memory drawings should distinguish heap memory from local stack memory.
Reminder: malloc() allocates memory in the heap which is only be deallocated by
deliberate calls to free(). In contrast, local stack variables for each function are
automatically allocated when the function starts and deallocated when it exits. Our
memory drawings show the caller local stack variables above the callee, but any
convention is fine so long as you realize that the caller and callee are separate. (See
cslibrary.stanford.edu/102/, Pointers and Memory, for an explanation of how local
memory works.)
9

Drawing 1 : Before Length()


Below is the state of memory just before the call to Length() in LengthTest() above.
BuildOneTwoThree() has built the {1, 2, 3} list in the heap and returned the head pointer.
The head pointer has been caught by the caller and stored in its local variable myList.
The local variable len has a random value — it will only be given the value 3 when then
call to Length() returns.

Stack Heap

LengthTest()
myList
len -14231

1 2 3

The head
pointer for
the list is len has a
stored in the random
local variable value until Nodes allocated in the heap
myList. it is via calls to malloc() in
assigned. BuildOneTwoThree().
10

Drawing 2: Mid Length


Here is the state of memory midway through the execution of Length(). Length()'s local
variables head and current have been automatically allocated. The current pointer
started out pointing to the first node, and then the first iteration of the while loop
advanced it to point to the second node.

Stack Heap

LengthTest()
myList
len -14231

1 2 3
Length()
head
current

Notice how the local variables in Length() (head and current) are separate from the
local variables in LengthTest() (myList and len). The local variables head and
current will be deallocated (deleted) automatically when Length() exits. This is fine
— the heap allocated links will remain even though stack allocated pointers which were
pointing to them have been deleted.

Exercise
Q: What if we said head = NULL; at the end of Length() — would that mess up the
myList variable in the caller? A: No. head is a local which was initialized with a copy
of the actual parameter, but changes do not automatically trace back to the actual
parameter. Changes to the local variables in one function do not affect the locals of
another function.

Exercise
Q: What if the passed in list contains no elements, does Length() handle that case
properly? A: Yes. The representation of the empty list is a NULL head pointer. Trace
Length() on that case to see how it handles it.
11

Section 2 —
List Building
BuildOneTwoThree() is a fine as example of pointer manipulation code, but it's not a
general mechanism to build lists. The best solution will be an independent function which
adds a single new node to any list. We can then call that function as many times as we
want to build up any list. Before getting into the specific code, we can identify the classic
3-Step Link In operation which adds a single node to the front of a linked list. The 3 steps
are...
1) Allocate Allocate the new node in the heap and set its .data to
whatever needs to be stored.
struct node* newNode;
newNode = malloc(sizeof(struct node));
newNode->data = data_client_wants_stored;

2) Link Next Set the .next pointer of the new node to point to the current
first node of the list. This is actually just a pointer assignment —
remember: "assigning one pointer to another makes them point to the same
thing."
newNode->next = head;

3) Link Head Change the head pointer to point to the new node, so it is
now the first node in the list.
head = newNode;

3-Step Link In Code


The simple LinkTest() function demonstrates the 3-Step Link In...
void LinkTest() {
struct node* head = BuildTwoThree(); // suppose this builds the {2, 3} list
struct node* newNode;

newNode= malloc(sizeof(struct node)); // allocate


newNode->data = 1;

newNode->next = head; // link next

head = newNode; // link head

// now head points to the list {1, 2, 3}


}
12

3-Step Link In Drawing


The drawing of the above 3-Step Link like (overwritten pointer values are in gray)...

Stack Heap

LinkTest()

head

newNode 2 3

1
Insert this node with the 3-Step Link In:
1) Allocate the new node
2) Set its .next to the old head
3) Set head to point to the new node
Before: list = {2, 3}
After: list = {1, 2, 3}

Push() Function
With the 3-Step Link In in mind, the problem is to write a general function which adds a
single node to head end of any list. Historically, this function is called "Push()" since
we're adding the link to the head end which makes the list look a bit like a stack.
Alternately it could be called InsertAtFront(), but we'll use the name Push().

WrongPush()
Unfortunately Push() written in C suffers from a basic problem: what should be the
parameters to Push()? This is, unfortunately, a sticky area in C. There's a nice, obvious
way to write Push() which looks right but is wrong. Seeing exactly how it doesn't work
will provide an excuse for more practice with memory drawings, motivate the correct
solution, and just generally make you a better programmer....
void WrongPush(struct node* head, int data) {
struct node* newNode = malloc(sizeof(struct node));

newNode->data = data;
newNode->next = head;
head = newNode; // NO this line does not work!
}

void WrongPushTest() {
List head = BuildTwoThree();

WrongPush(head, 1); // try to push a 1 on front -- doesn't work


}
13

WrongPush() is very close to being correct. It takes the correct 3-Step Link In and puts it
an almost correct context. The problem is all in the very last line where the 3-Step Link
In dictates that we change the head pointer to refer to the new node. What does the line
head = newNode; do in WrongPush()? It sets a head pointer, but not the right one. It
sets the variable named head local to WrongPush(). It does not in any way change the
variable named head we really cared about which is back in the caller WrontPushTest().

Exercise
Make the memory drawing tracing WrongPushTest() to see how it does not work. The
key is that the line head = newElem; changes the head local to WrongPush() not
the head back in WrongPushTest(). Remember that the local variables for WrongPush()
and WrongPushTest() are separate (just like the locals for LengthTest() and Length() in
the Length() example above).

Reference Parameters In C
We are bumping into a basic "feature" of the C language that changes to local parameters
are never reflected back in the caller's memory. This is a traditional tricky area of C
programming. We will present the traditional "reference parameter" solution to this
problem, but you may want to consult another C resource for further information. (See
Pointers and Memory (http://cslibrary.stanford.edu/102/) for a detailed explanation of
reference parameters in C and C++.)
We need Push() to be able to change some of the caller's memory — namely the head
variable. The traditional method to allow a function to change its caller's memory is to
pass a pointer to the caller's memory instead of a copy. So in C, to change an int in the
caller, pass a int* instead. To change a struct fraction, pass a struct
fraction* intead. To change an X, pass an X*. So in this case, the value we want to
change is struct node*, so we pass a struct node** instead. The two stars
(**) are a little scary, but really it's just a straight application of the rule. It just happens
that the value we want to change already has one star (*), so the parameter to change it
has two (**). Or put another way: the type of the head pointer is "pointer to a struct
node." In order to change that pointer, we need to pass a pointer to it, which will be a
"pointer to a pointer to a struct node".
Instead of defining WrongPush(struct node* head, int data); we define
Push(struct node** headRef, int data);. The first form passes a copy of
the head pointer. The second, correct form passes a pointer to the head pointer. The rule
is: to modify caller memory, pass a pointer to that memory. The parameter has the word
"ref" in it as a reminder that this is a "reference" (struct node**) pointer to the
head pointer instead of an ordinary (struct node*) copy of the head pointer.
14

Correct Push() Code


Here are Push() and PushTest() written correctly. The list is passed via a pointer to the
head pointer. In the code, this amounts to use of '&' on the parameter in the caller and use
of '*' on the parameter in the callee. Inside Push(), the pointer to the head pointer is
named "headRef" instead of just "head" as a reminder that it is not just a simple head
pointer..
/*
Takes a list and a data value.
Creates a new link with the given data and pushes
it onto the front of the list.
The list is not passed in by its head pointer.
Instead the list is passed in as a "reference" pointer
to the head pointer -- this allows us
to modify the caller's memory.
*/
void Push(struct node** headRef, int data) {
struct node* newNode = malloc(sizeof(struct node));

newNode->data = data;
newNode->next = *headRef; // The '*' to dereferences back to the real head
*headRef = newNode; // ditto
}

void PushTest() {
struct node* head = BuildTwoThree();// suppose this returns the list {2, 3}

Push(&head, 1); // note the &


Push(&head, 13);

// head is now the list {13, 1, 2, 3}


}
15

Correct Push() Drawing


Here is a drawing of memory just before the first call to Push() exits. The original value
of the head pointer is in gray. Notice how the headRef parameter inside Push() points
back to the real head pointer back in PushTest(). Push() uses *headRef to access and
change the real head pointer.

Stack Heap

PushTest()
head

Push() 2 3
headRef
data 1
1
The key point: the headRef This node inserted by the call to
parameter to Push() is not the Push(). Push follows its headRef to
real head of the list. It is a modify the real head.
pointer to the real head of the
list back in the caller’s
memory space.

Exercise
The above drawing shows the state of memory at the end of the first call to Push() in
PushTest(). Extend the drawing to trace through the second call to Push(). The result
should be that the list is left with elements {13, 1, 2, 3}.

Exercise
The following function correctly builds a three element list using nothing but Push().
Make the memory drawing to trace its execution and show the final state of its list. This
will also demonstrate that Push() works correctly for the empty list case.
void PushTest2() {
struct node* head = NULL; // make a list with no elements

Push(&head, 1);
Push(&head, 2);
Push(&head, 3);

// head now points to the list {3, 2, 1}


}

What About C++?


(Just in case you were curious) C++ has its built in "& argument" feature to implement
reference parameters for the programmer. The short story is, append an '&' to the type of
a parameter, and the compiler will automatically make the parameter operate by reference
for you. The type of the argument is not disturbed by this — the types continue to act as
16

they appear in the source, which is the most convenient for the programmer. So In C++,
Push() and PushTest() look like...
/*
Push() in C++ -- we just add a '&' to the right hand
side of the head parameter type, and the compiler makes
that parameter work by reference. So this code changes
the caller's memory, but no extra uses of '*' are necessary --
we just access "head" directly, and the compiler makes that
change reference back to the caller.
*/
void Push(struct node*& head, int data) {
struct node* newNode = malloc(sizeof(struct node));

newNode->data = data;
newNode->next = head; // No extra use of * necessary on head -- the compiler
head = newNode; // just takes care of it behind the scenes.
}

void PushTest() {
struct node* head = BuildTwoThree();// suppose this returns the list {2, 3}

Push(head, 1); // No extra use & necessary -- the compiler takes


Push(head, 13); // care of it here too. Head is being changed by
// these calls.

// head is now the list {13, 1, 2, 3}


}

The memory drawing for the C++ case looks the same as for the C case. The difference is
that the C case, the *'s need to be taken care of in the code. In the C++ case, it's handled
invisibly in the code.
17

Section 3 —
Code Techniques
This section summarizes, in list form, the main techniques for linked list code. These
techniques are all demonstrated in the examples in the next section.

1) Iterate Down a List


A very frequent technique in linked list code is to iterate a pointer over all the nodes in a
list. Traditionally, this is written as a while loop. The head pointer is copied into a local
variable current which then iterates down the list. Test for the end of the list with
current!=NULL. Advance the pointer with current=current->next.
// Return the number of nodes in a list (while-loop version)
int Length(struct node* head) {
int count = 0;
struct node* current = head;

while (current != NULL) {


count++;
current = current->next
}

return(count);
}

Alternately, some people prefer to write the loop as a for which makes the initialization,
test, and pointer advance more centralized, and so harder to omit...
for (current = head; current != NULL; current = current->next) {

2) Changing a Pointer With A Reference Pointer


Many list functions need to change the caller's head pointer. To do this in the C language,
pass a pointer to the head pointer. Such a pointer to a pointer is sometimes called a
"reference pointer". The main steps for this technique are...
• Design the function to take a pointer to the head pointer. This is the
standard technique in C — pass a pointer to the "value of interest" that
needs to be changed. To change a struct node*, pass a struct
node**.

• Use '&' in the caller to compute and pass a pointer to the value of interest.

• Use '*' on the parameter in the callee function to access and change the
value of interest.

The following simple function sets a head pointer to NULL by using a reference
parameter....
// Change the passed in head pointer to be NULL
// Uses a reference pointer to access the caller's memory
void ChangeToNull(struct node** headRef) { // Takes a pointer to
// the value of interest
18

*headRef = NULL; // use '*' to access the value of interest


}

void ChangeCaller() {
struct node* head1;
struct node* head2;

ChangeToNull(&head1); // use '&' to compute and pass a pointer to


ChangeToNull(&head2); // the value of interest
// head1 and head2 are NULL at this point
}

Here is a drawing showing how the headRef pointer in ChangeToNull() points back to
the variable in the caller...

Stack
ChangeCaller()
head1

ChangToNull()
headRef

See the use of Push() above and its implementation for another example of reference
pointers.

3) Build — At Head With Push()


The easiest way to build up a list is by adding nodes at its "head end" with Push(). The
code is short and it runs fast — lists naturally support operations at their head end. The
disadvantage is that the elements will appear in the list in the reverse order that they are
added. If you don't care about order, then the head end is the best.
struct node* AddAtHead() {
struct node* head = NULL;
int i;

for (i=1; i<6; i++) {


Push(&head, i);
}

// head == {5, 4, 3, 2, 1};


return(head);
}

4) Build — With Tail Pointer


What about adding nodes at the "tail end" of the list? Adding a node at the tail of a list
most often involves locating the last node in the list, and then changing its .next field
19

from NULL to point to the new node, such as the tail variable in the following
example of adding a "3" node to the end of the list {1, 2}...

Stack Heap

head 1 2

tail 3
newNode

This is just a special case of the general rule: to insert or delete a node inside a list, you
need a pointer to the node just before that position, so you can change its .next field.
Many list problems include the sub-problem of advancing a pointer to the node before the
point of insertion or deletion. The one exception is if the node is the first in the list — in
that case the head pointer itself must be changed. The following examples show the
various ways code can handle the single head case and all the interior cases...

5) Build — Special Case + Tail Pointer


Consider the problem of building up the list {1, 2, 3, 4, 5} by appending the nodes to the
tail end. The difficulty is that the very first node must be added at the head pointer, but all
the other nodes are inserted after the last node using a tail pointer. The simplest way to
deal with both cases is to just have two separate cases in the code. Special case code first
adds the head node {1}. Then there is a separate loop that uses a tail pointer to add all the
other nodes. The tail pointer is kept pointing at the last node, and each new node is added
at tail->next. The only "problem" with this solution is that writing separate special
case code for the first node is a little unsatisfying. Nonetheless, this approach is a solid
one for production code — it is simple and runs fast.
struct node* BuildWithSpecialCase() {
struct node* head = NULL;
struct node* tail;
int i;

// Deal with the head node here, and set the tail pointer
Push(&head, 1);
tail = head;

// Do all the other nodes using 'tail'


for (i=2; i<6; i++) {
Push(&(tail->next), i); // add node at tail->next
tail = tail->next; // advance tail to point to last node
}

return(head); // head == {1, 2, 3, 4, 5};


}
20

6) Build — Dummy Node


Another solution is to use a temporary dummy node at the head of the list during the
computation. The trick is that with the dummy, every node appear to be added after the
.next field of a node. That way the code for the first node is the same as for the other
nodes. The tail pointer plays the same role as in the previous example. The difference is
that it now also handles the first node.
struct node* BuildWithDummyNode() {
struct node dummy; // Dummy node is temporarily the first node
struct node* tail = &dummy; // Start the tail at the dummy.
// Build the list on dummy.next (aka tail->next)
int i;

dummy.next = NULL;

for (i=1; i<6; i++) {


Push(&(tail->next), i);
tail = tail->next;
}

// The real result list is now in dummy.next


// dummy.next == {1, 2, 3, 4, 5};
return(dummy.next);
}

Some linked list implementations keep the dummy node as a permanent part of the list.
For this "permanent dummy" strategy, the empty list is not represented by a NULL
pointer. Instead, every list has a dummy node at its head. Algorithms skip over the
dummy node for all operations. That way the heap allocated dummy node is always
present to provide the above sort of convenience in the code.
Our dummy-in-the stack strategy is a little unusual, but it avoids making the dummy a
permanent part of the list. Some of the solutions presented in this document will use the
temporary dummy strategy. The code for the permanent dummy strategy is extremely
similar, but is not shown.
7) Build — Local References
Finally, here is a tricky way to unifying all the node cases without using a dummy node.
The trick is to use a local "reference pointer" which always points to the last pointer in
the list instead of to the last node. All additions to the list are made by following the
reference pointer. The reference pointer starts off pointing to the head pointer. Later, it
points to the .next field inside the last node in the list. (A detailed explanation follows.)
struct node* BuildWithLocalRef() {
struct node* head = NULL;
struct node** lastPtrRef= &head; // Start out pointing to the head pointer
int i;

for (i=1; i<6; i++) {


Push(lastPtrRef, i); // Add node at the last pointer in the list
lastPtrRef= &((*lastPtrRef)->next); // Advance to point to the
// new last pointer
}

// head == {1, 2, 3, 4, 5};


return(head);
}
21

This technique is short, but the inside of the loop is scary. This technique is rarely used.
(Actually, I'm the only person I've known to promote it. I think it has a sort of compact
charm.) Here's how it works...
1) At the top of the loop, lastPtrRef points to the last pointer in the list.
Initially it points to the head pointer itself. Later it points to the .next
field inside the last node in the list.
2) Push(lastPtrRef, i); adds a new node at the last pointer. The
new node becaomes the last node in the list.
3) lastPtrRef= &((*lastPtrRef)->next); Advance the
lastPtrRef to now point to the .next field inside the new last node
— that .next field is now the last pointer in the list.
Here is a drawing showing the state of memory for the above code just before the third
node is added. The previous values of lastPtrRef are shown in gray...

Stack Heap

LocalRef()
head 1 2

lastPtrRef

This technique is never required to solve a linked list problem, but it will be one of the
alternative solutions presented for some of the advanced problems.
Both the temporary-dummy strategy and the reference-pointer strategy are a little
unusual. They are good ways to make sure that you really understand pointers, since they
use pointers in unusual ways.
22

Section 4 — Examples
This section presents some complete list code to demonstrate all of the techniques above.
For many more sample problems with solutions, see CS Education Library #105, --
Linked List Problems (http://cslibrary.stanford.edu/105/).

AppendNode() Example
Consider a AppendNode() function which is like Push(), except it adds the new node at
the tail end of the list instead of the head. If the list is empty, it uses the reference pointer
to change the head pointer. Otherwise it uses a loop to locate the last node in the list. This
version does not use Push(). It builds the new node directly.
struct node* AppendNode(struct node** headRef, int num) {
struct node* current = *headRef;
struct node* newNode;

newNode = malloc(sizeof(struct node));


newNode->data = num;
newNode->next = NULL;

// special case for length 0


if (current == NULL) {
*headRef = newNode;
}
else {
// Locate the last node
while (current->next != NULL) {
current = current->next;
}

current->next = newNode;
}
}

AppendNode() With Push()


This version is very similar, but relies on Push() to build the new node. Understanding
this version requires a real understanding of reference pointers.
struct node* AppendNode(struct node** headRef, int num) {
struct node* current = *headRef;

// special case for the empty list


if (current == NULL) {
Push(headRef, num);
} else {
// Locate the last node
while (current->next != NULL) {
current = current->next;
}

// Build the node after the last node


Push(&(current->next), num);
}
}
23

CopyList() Example
Consider a CopyList() function that takes a list and returns a complete copy of that list.
One pointer can iterate over the original list in the usual way. Two other pointers can
keep track of the new list: one head pointer, and one tail pointer which always points to
the last node in the new list. The first node is done as a special case, and then the tail
pointer is used in the standard way for the others...
struct node* CopyList(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* newList = NULL; // head of the new list
struct node* tail = NULL; // kept pointing to the last node in the new list

while (current != NULL) {


if (newList == NULL) { // special case for the first new node
newList = malloc(sizeof(struct node));
newList->data = current->data;
newList->next = NULL;
tail = newList;
}
else {
tail->next = malloc(sizeof(struct node));
tail = tail->next;
tail->data = current->data;
tail->next = NULL;
}
current = current->next;
}

return(newList);
}

CopyList() Memory Drawing


Here is the state of memory when CopyList() finishes copying the list {1, 2}...

Stack Heap

CopyList()
head 1 2

current

newList
1 2

tail
24

CopyList() With Push() Exercise


The above implementation is a little unsatisfying because the 3-step-link-in is repeated —
once for the first node and once for all the other nodes. Write a CopyList2() which uses
Push() to take care of allocating and inserting the new nodes, and so avoids repeating that
code.

CopyList() With Push() Answer


// Variant of CopyList() that uses Push()
struct node* CopyList2(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* newList = NULL; // head of the new list
struct node* tail = NULL; // kept pointing to the last node in the new list

while (current != NULL) {


if (newList == NULL) { // special case for the first new node
Push(&newList, current->data);
tail = newList;
}
else {
Push(&(tail->next), current->data); // add each node at the tail
tail = tail->next; // advance the tail to the new last node
}
current = current->next;
}

return(newList);
}

CopyList() With Dummy Node


Anther strategy for CopyList() uses a temporary dummy node to take care of the first
node case. The dummy node is temporarily the first node in the list, and the tail pointer
starts off pointing to it. All nodes are added off the tail pointer.
// Dummy node variant
struct node* CopyList(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* tail; // kept pointing to the last node in the new list
struct node dummy; // build the new list off this dummy node

dummy.next = NULL;
tail = &dummy; // start the tail pointing at the dummy

while (current != NULL) {


Push(&(tail->next), current->data); // add each node at the tail
tail = tail->next; // advance the tail to the new last node
}
current = current->next;
}

return(dummy.next);
}

CopyList() With Local References


The final, and most unusual version uses the "local references" strategy instead of a tail
pointer. The strategy is to keep a lastPtr that points to the last pointer in the list. All
node additions are done at the lastPtr, and it always points to the last pointer in the
25

list. When the list is empty, it points to the head pointer itself. Later it points to the
.next pointer inside the last node in the list.
// Local reference variant
struct node* CopyList(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* newList = NULL;
struct node** lastPtr;

lastPtr = &newList; // start off pointing to the head itself

while (current != NULL) {


Push(lastPtr, current->data); // add each node at the lastPtr
lastPtr = &((*lastPtr)->next); // advance lastPtr
current = current->next;
}

return(newList);
}

CopyList() Recursive
Finally, for completeness, here is the recursive version of CopyList(). It has the pleasing
shortness that recursive code often has. However, it is probably not good for production
code since it uses stack space proportional to the length of its list.
// Recursive variant
struct node* CopyList(struct node* head) {
if (head == NULL) return NULL;
else {
struct node* newList = malloc(sizeof(struct node)); // make the one node
newList->data = current->data;

newList->next = CopyList(current->next); // recur for the rest

return(newList);
}
}

Appendix —
Other Implementations
There are a many variations on the basic linked list which have individual advantages
over the basic linked list. It is probably best to have a firm grasp of the basic linked list
and its code before worrying about the variations too much.
• Dummy Header Forbid the case where the head pointer is NULL.
Instead, choose as a representation of the empty list a single "dummy"
node whose .data field is unused. The advantage of this technique is that
the pointer-to-pointer (reference parameter) case does not come up for
operations such as Push(). Also, some of the iterations are now a little
simpler since they can always assume the existence of the dummy header
node. The disadvantage is that allocating an "empty" list now requires
26

allocating (and wasting) memory. Some of the algorithms have an ugliness


to them since they have to realize that the dummy node "doesn't count."
(editorial) Mainly the dummy header is for programmers to avoid the ugly
reference parameter issues in functions such as Push(). Languages which
don't allow reference parameters, such as Java, may require the dummy
header as a workaround. (See the "temporary dummy" variant below.)

• Circular Instead of setting the .next field of the last node to NULL,
set it to point back around to the first node. Instead of needing a fixed head
end, any pointer into the list will do.

• Tail Pointer The list is not represented by a single head pointer. Instead
the list is represented by a head pointer which points to the first node and a
tail pointer which points to the last node. The tail pointer allows operations
at the end of the list such as adding an end element or appending two lists
to work efficiently.

• Head struct A variant I like better than the dummy header is to have a
special "header" struct (a different type from the node type) which
contains a head pointer, a tail pointer, and possibly a length to make many
operations more efficient. Many of the reference parameter problems go
away since most functions can deal with pointers to the head struct
(whether it is heap allocated or not). This is probably the best approach to
use in a language without reference parameters, such as Java.

• Doubly-Linked Instead of just a single .next field, each node


incudes both .next and .previous pointers. Insertion and deletion now
require more operations. but other operations are simplified. Given a
pointer to a node, insertion and deletion can be performed directly whereas
in the singly linked case, the iteration typically needs to locate the point
just before the point of change in the list so the .next pointers can be
followed downstream.

• Chunk List Instead of storing a single client element in each node, store
a little constant size array of client elements in each node. Tuning the
number of elements per node can provide different performance
characteristics: many elements per node has performance more like an
array, few elements per node has performance more like a linked list. The
Chunk List is a good way to build a linked list with good performance.

• Dynamic Array Instead of using a linked list, elements may be


stored in an array block allocated in the heap. It is possible to grow and
shrink the size of the block as needed with calls to the system function
realloc(). Managing a heap block in this way is a fairly complex, but can
have excellent efficiency for storage and iteration., especially because
modern memory systems are tuned for the access of contiguous areas of
memory. In contrast, linked list can actually be a little inefficient, since
they tend to iterate through memory areas that are not adjacent.
Linked List
Problems
By Nick Parlante Copyright ©1998-2002, Nick Parlante

Abstract
This document reviews basic linked list code techniques and then works through 18
linked list problems covering a wide range of difficulty. Most obviously, these problems
are a way to learn about linked lists. More importantly, these problems are a way to
develop your ability with complex pointer algorithms. Even though modern languages
and tools have made linked lists pretty unimportant for day-to-day programming, the
skills for complex pointer algorithms are very important, and linked lists are an excellent
way to develop those skills.
The problems use the C language syntax, so they require a basic understanding of C and
its pointer syntax. The emphasis is on the important concepts of pointer manipulation and
linked list algorithms rather than the features of the C language.
For some of the problems we present multiple solutions, such as iteration vs. recursion,
dummy node vs. local reference. The specific problems are, in rough order of difficulty:
Count, GetNth, DeleteList, Pop, InsertNth, SortedInsert, InsertSort, Append,
FrontBackSplit, RemoveDuplicates, MoveNode, AlternatingSplit, ShuffleMerge,
SortedMerge, SortedIntersect, Reverse, and RecursiveReverse.

Contents
Section 1 — Review of basic linked list code techniques 3
Section 2 — 18 list problems in increasing order of difficulty 10
Section 3 — Solutions to all the problems 20
This is document #105, Linked List Problems, in the Stanford CS Education Library.
This and other free educational materials are available at http://cslibrary.stanford.edu/.
This document is free to be used, reproduced, or sold so long as this notice is clearly
reproduced at its beginning.
Related CS Education Library Documents
Related Stanford CS Education library documents...
• Linked List Basics (http://cslibrary.stanford.edu/103/)
Explains all the basic issues and techniques for building linked lists.
• Pointers and Memory (http://cslibrary.stanford.edu/102/)
Explains how pointers and memory work in C and other languages. Starts
with the very basics, and extends through advanced topics such as
reference parameters and heap management.
• Binary Trees (http://cslibrary.stanford.edu/110/)
Introduction to binary trees
• Essential C (http://cslibrary.stanford.edu/101/)
Explains the basic features of the C programming language.
2

• The Great Tree List Problem (http://cslibrary.stanford.edu/109/)


Presents the greatest recursive pointer problem ever devised.
Why Linked Lists Are Great To Study
Linked lists hold a special place in the hearts of many programmers. Linked lists are great
to study because...
• Nice Domain The linked list structure itself is simple. Many linked list
operations such as "reverse a list" or "delete a list" are easy to describe and
understand since they build on the simple purpose and structure of the
linked list itself.

• Complex Algorithm Even though linked lists are simple, the algorithms
that operate on them can be as complex and beautiful as you want (See
problem #18). It's easy to find linked list algorithms that are complex, and
pointer intensive.

• Pointer Intensive Linked list problems are really about pointers. The
linked list structure itself is obviously pointer intensive. Furthermore,
linked list algorithms often break and re-weave the pointers in a linked list
as they go. Linked lists really test your understanding of pointers.

• Visualization Visualization is an important skill in programming and


design. Ideally, a programmer can visualize the state of memory to help
think through the solution. Even the most abstract languages such as Java
and Perl have layered, reference based data structures that require
visualization. Linked lists have a natural visual structure for practicing this
sort of thinking. It's easy to draw the state of a linked list and use that
drawing to think through the code.

Not to appeal to your mercenary side, but for all of the above reasons, linked list
problems are often used as interview and exam questions. They are short to state, and
have complex, pointer intensive solutions. No one really cares if you can build linked
lists, but they do want to see if you have programming agility for complex algorithms and
pointer manipulation. Linked lists are the perfect source of such problems.

How To Use This Document


Try not to use these problems passively. Take some time to try to solveeach problem.
Even if you do not succeed, you will think through the right issues in the attempt, and
looking at the given solution will make more sense. Use drawings to think about the
problems and work through the solutions. Linked lists are well-suited for memory
drawings, so these problems are an excellent opportunity to develop your visualization
skill. The problems in this document use regular linked lists, without simplifcations like
dummy headers.

Dedication
This Jan-2002 revision includes many small edits. The first major release was Jan 17,
1999. Thanks to Negar Shamma for her many corrections. This document is distributed
for the benefit and education of all. Thanks to the support of Eric Roberts and Stanford
University. That someone seeking education should have the opportunity to find it. May
you learn from it in the spirit of goodwill in which it is given.
Best Regards, Nick Parlante -- nick.parlante@cs.stanford.edu
3

Section 1 —
Linked List Review
This section is a quick review of the concepts used in these linked list problems. For more
detailed coverage, see Link List Basics (http://cslibrary.stanford.edu/103/) where all of
this material is explained in much more detail.

Linked List Ground Rules


All of the linked list code in this document uses the "classic" singly linked list structure:
A single head pointer points to the first node in the list. Each node contains a single
.next pointer to the next node. The .next pointer of the last node is NULL. The
empty list is represented by a NULL head pointer. All of the nodes are allocated in the
heap.
For a few of the problems, the solutions present the temporary "dummy node" variation
(see below), but most of the code deals with linked lists in their plain form. In the text,
brackets {} are used to describe lists — the list containing the numbers 1, 2, and 3 is
written as {1, 2, 3}. The node type used is...
struct node {
int data;
struct node* next;
};

To keep thing ssimple, we will not introduce any intermediate typedefs. All pointers to
nodes are declared simply as struct node*. Pointers to pointers to nodes are declared
as struct node**. Such pointers to pointers are often called "reference pointers".

Basic Utility Functions


In a few places, the text assumes the existence of the following basic utility functions...
• int Length(struct node* head);
Returns the number of nodes in the list.

• struct node* BuildOneTwoThree();


Allocates and returns the list {1, 2, 3}. Used by some of the example code
to build lists to work on.

• void Push(struct node** headRef, int newData);


Given an int and a reference to the head pointer (i.e. a struct
node** pointer to the head pointer), add a new node at the head of the
list with the standard 3-step-link-in: create the new node, set its .next to
point to the current head, and finally change the head to point to the new
node. (If you are not sure of how this function works, the first few
problems may be helpful warm-ups.)
4

Use of the Basic Utility Functions


This sample code demonstrates the basic utility functions being used. Their
implementations are also given in the appendix at the end of the document.
void BasicsCaller() {
struct node* head;
int len;

head = BuildOneTwoThree(); // Start with {1, 2, 3}

Push(&head, 13); // Push 13 on the front, yielding {13, 1, 2, 3}


// (The '&' is because head is passed
// as a reference pointer.)

Push(&(head->next), 42); // Push 42 into the second position


// yielding {13, 42, 1, 2, 3}
// Demonstrates a use of '&' on
// the .next field of a node.
// (See technique #2 below.)

len = Length(head); // Computes that the length is 5.


}

If these basic functions do not make sense to you, you can (a) go see Linked List Basics
(http://cslibrary.stanford.edu/103/) which explains the basics of linked lists in detail, or
(b) do the first few problems, but avoid the intermediate and advanced ones.

Linked List Code Techniques


The following list presents the most common techniques you may want to use in solving
the linked list problems. The first few are basic. The last few are only necessary for the
more advanced problems.

1. Iterate Down a List


A very frequent technique in linked list code is to iterate a pointer over all the nodes in a
list. Traditionally, this is written as a while loop. The head pointer is copied into a local
variable current which then iterates down the list. Test for the end of the list with
current!=NULL. Advance the pointer with current=current->next.
// Return the number of nodes in a list (while-loop version)
int Length(struct node* head) {
int count = 0;
struct node* current = head;

while (current != NULL) {


count++;
current = current->next;
}

return(count);
}

Alternately, some people prefer to write the loop as a for which makes the initialization,
test, and pointer advance more centralized, and so harder to omit...
for (current = head; current != NULL; current = current->next) {
5

2. Changing a Pointer Using a Reference Pointer


Many list functions need to change the caller's head pointer. In C++, you can just declare
the pointer parameter as an & argument, and the compiler takes care of the details. To do
this in the C language, pass a pointer to the head pointer. Such a pointer to a pointer is
sometimes called a "reference pointer". The main steps for this technique are...
• Design the function to take a pointer to the head pointer. This is the
standard technique in C — pass a pointer to the "value of interest" that
needs to be changed. To change a struct node*, pass a struct
node**.

• Use '&' in the caller to compute and pass a pointer to the value of interest.

• Use '*' on the parameter in the callee function to access and change the
value of interest.

The following simple function sets a head pointer to NULL by using a reference
parameter....
// Change the passed in head pointer to be NULL
// Uses a reference pointer to access the caller's memory
void ChangeToNull(struct node** headRef) { // Takes a pointer to
// the value of interest

*headRef = NULL; // use '*' to access the value of interest


}

void ChangeCaller() {
struct node* head1;
struct node* head2;

ChangeToNull(&head1); // use '&' to compute and pass a pointer to


ChangeToNull(&head2); // the value of interest
// head1 and head2 are NULL at this point
}

Here is a drawing showing how the headRef pointer in ChangeToNull() points back to
the variable in the caller...

Stack
ChangeCaller()

head1

ChangeToNull(&head1)
headRef
6

Many of the functions in this document use reference pointer parameters. See the use of
Push() above and its implementation in the appendix for another example of reference
pointers. See problem #8 and its solution for a complete example with drawings. For
more detailed explanations, see the resources listed on page 1.

3. Build — At Head With Push()


The easiest way to build up a list is by adding nodes at its "head end" with Push(). The
code is short and it runs fast — lists naturally support operations at their head end. The
disadvantage is that the elements will appear in the list in the reverse order that they are
added. If you don't care about order, then the head end is the best.
struct node* AddAtHead() {
struct node* head = NULL;
int i;

for (i=1; i<6; i++) {


Push(&head, i);
}

// head == {5, 4, 3, 2, 1};


return(head);
}

4. Build — With Tail Pointer


What about adding nodes at the "tail end" of the list? Adding a node at the tail of a list
most often involves locating the last node in the list, and then changing its .next field
from NULL to point to the new node, such as the tail variable in the following
example of adding a "3" node to the end of the list {1, 2}...

Stack Heap

head 1 2

tail 3
newNode

This is just a special case of the general rule: to insert or delete a node inside a list, you
need a pointer to the node just before that position, so you can change its .next field.
Many list problems include the sub-problem of advancing a pointer to the node before the
point of insertion or deletion. The one exception is if the operation falls on the first node
in the list — in that case the head pointer itself must be changed. The following examples
show the various ways code can handle the single head case and all the interior cases...
7

5. Build — Special Case + Tail Pointer


Consider the problem of building up the list {1, 2, 3, 4, 5} by appending the nodes to the
tail end. The difficulty is that the very first node must be added at the head pointer, but all
the other nodes are inserted after the last node using a tail pointer. The simplest way to
deal with both cases is to just have two separate cases in the code. Special case code first
adds the head node {1}. Then there is a separate loop that uses a tail pointer to add all the
other nodes. The tail pointer is kept pointing at the last node, and each new node is added
at tail->next. The only "problem" with this solution is that writing separate special
case code for the first node is a little unsatisfying. Nonetheless, this approach is a solid
one for production code — it is simple and runs fast.
struct node* BuildWithSpecialCase() {
struct node* head = NULL;
struct node* tail;
int i;

// Deal with the head node here, and set the tail pointer
Push(&head, 1);
tail = head;

// Do all the other nodes using 'tail'


for (i=2; i<6; i++) {
Push(&(tail->next), i); // add node at tail->next
tail = tail->next; // advance tail to point to last node
}

return(head); // head == {1, 2, 3, 4, 5};


}

6. Build — Temporary Dummy Node


This is a slightly unusual technique that can be used to shorten the code: Use a temporary
dummy node at the head of the list during the computation. The trick is that with the
dummy, every node appears to be added after the .next field of some other node. That
way the code for the first node is the same as for the other nodes. The tail pointer plays
the same role as in the previous example. The difference is that now it also handles the
first node as well.
struct node* BuildWithDummyNode() {
struct node dummy; // Dummy node is temporarily the first node
struct node* tail = &dummy; // Start the tail at the dummy.
// Build the list on dummy.next (aka tail->next)
int i;

dummy.next = NULL;

for (i=1; i<6; i++) {


Push(&(tail->next), i);
tail = tail->next;
}

// The real result list is now in dummy.next


// dummy.next == {1, 2, 3, 4, 5};
return(dummy.next);
}
8

Some linked list implementations keep the dummy node as a permanent part of the list.
For this "permanent dummy" strategy, the empty list is not represented by a NULL
pointer. Instead, every list has a heap allocated dummy node at its head. Algorithms skip
over the dummy node for all operations. That way the dummy node is always present to
provide the above sort of convenience in the code. I prefer the temporary strategy shown
here, but it is a little peculiar since the temporary dummy node is allocated in the stack,
while all the other nodes are allocated in the heap. For production code, I do not use
either type of dummy node. The code should just cope with the head node boundary
cases.

7. Build — Local References


Finally, here is a tricky way to unify all the node cases without using a dummy node at
all. For this technique, we use a local "reference pointer" which always points to the last
pointer in the list instead of to the last node. All additions to the list are made by
following the reference pointer. The reference pointer starts off pointing to the head
pointer. Later, it points to the .next field inside the last node in the list. (A detailed
explanation follows.)
struct node* BuildWithLocalRef() {
struct node* head = NULL;
struct node** lastPtrRef= &head; // Start out pointing to the head pointer
int i;

for (i=1; i<6; i++) {


Push(lastPtrRef, i); // Add node at the last pointer in the list
lastPtrRef= &((*lastPtrRef)->next); // Advance to point to the
// new last pointer
}

// head == {1, 2, 3, 4, 5};


return(head);
}

This technique is short, but the inside of the loop is scary. This technique is rarely used,
but it's a good way to see if you really understand pointers. Here's how it works...
1) At the top of the loop, lastPtrRef points to the last pointer in the list.
Initially it points to the head pointer itself. Later it points to the .next
field inside the last node in the list.
2) Push(lastPtrRef, i); adds a new node at the last pointer. The
new node becomes the last node in the list.
3) lastPtrRef= &((*lastPtrRef)->next); Advance the
lastPtrRef to now point to the .next field inside the new last node
— that .next field is now the last pointer in the list.
Here is a drawing showing the state of memory for the above code just before the third
node is added. The previous values of lastPtrRef are shown in gray...
9

Stack Heap

LocalRef()
head 1 2

lastPtrRef

This technique is never required to solve a linked list problem, but it will be one of the
alternative solutions presented for some of the advanced problems. The code is shorter
this way, but the performance is probably not any better.

Unusual Techniques
Both the temporary-stack-dummy and the local-reference-pointer techniques are a little
unusual. They are cute, and they let us play around with yet another variantion in pointer
intensive code. They use memory in unusual ways, so they are a nice way to see if you
really understand what's going on. However, I probably would not use them in production
code.
10

Section 2 —
Linked List Problems
Here are 18 linked list problems arranged in order of difficulty. The first few are quite
basic and the last few are quite advanced. Each problem starts with a basic definition of
what needs to be accomplished. Many of the problems also include hints or drawings to
get you started. The solutions to all the problems are in the next section.
It's easy to just passively sweep your eyes over the solution — verifying its existence
without lettings its details touch your brain. To get the most benefit from these problems,
you need to make an effort to think them through. Whether or not you solve the problem,
you will be thinking through the right issues, and the given solution will make more
sense.
Great programmers can visualize data structures to see how the code and memory will
interact. Linked lists are well suited to that sort of visual thinking. Use these problems to
develop your visualization skill. Make memory drawings to trace through the execution
of code. Use drawings of the pre- and post-conditions of a problem to start thinking about
a solution.
"The will to win means nothing without the will to prepare." - Juma Ikangaa, marathoner
(also attributed to Bobby Knight)

1 — Count()
Write a Count() function that counts the number of times a given int occurs in a list. The
code for this has the classic list traversal structure as demonstrated in Length().
void CountTest() {
List myList = BuildOneTwoThree(); // build {1, 2, 3}

int count = Count(myList, 2); // returns 1 since there's 1 '2' in the list
}

/*
Given a list and an int, return the number of times that int occurs
in the list.
*/
int Count(struct node* head, int searchFor) {
// Your code
11

2 — GetNth()
Write a GetNth() function that takes a linked list and an integer index and returns the data
value stored in the node at that index position. GetNth() uses the C numbering convention
that the first node is index 0, the second is index 1, ... and so on. So for the list {42, 13,
666} GetNth() with index 1 should return 13. The index should be in the range [0..length-
1]. If it is not, GetNth() should assert() fail (or you could implement some other error
case strategy).
void GetNthTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}
int lastNode = GetNth(myList, 2); // returns the value 3
}

Essentially, GetNth() is similar to an array[i] operation — the client can ask for
elements by index number. However, GetNth() no a list is much slower than [ ] on an
array. The advantage of the linked list is its much more flexible memory management —
we can Push() at any time to add more elements and the memory is allocated as needed.
// Given a list and an index, return the data
// in the nth node of the list. The nodes are numbered from 0.
// Assert fails if the index is invalid (outside 0..lengh-1).
int GetNth(struct node* head, int index) {
// Your code

3 — DeleteList()
Write a function DeleteList() that takes a list, deallocates all of its memory and sets its
head pointer to NULL (the empty list).
void DeleteListTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}

DeleteList(&myList); // deletes the three nodes and sets myList to NULL


}

Post DeleteList() Memory Drawing


The following drawing shows the state of memory after DeleteList() executes in the
above sample. Overwritten pointers are shown in gray and deallocated heap memory has
an 'X' through it. Essentially DeleteList() just needs to call free() once for each node and
set the head pointer to NULL.
12

Stack Heap
DeleteListTest()
myList

myList is 1 2 3
overwritten
with the
value NULL.

The three heap blocks are deallocated by calls to


free(). Their memory will appear to be intact for
a while, but the memory should not be
accessed.

DeleteList()
The DeleteList() implementation will need to use a reference parameter just like Push()
so that it can change the caller's memory (myList in the above sample). The
implementation also needs to be careful not to access the .next field in each node after
the node has been deallocated.
void DeleteList(struct node** headRef) {
// Your code

4 — Pop()
Write a Pop() function that is the inverse of Push(). Pop() takes a non-empty list, deletes
the head node, and returns the head node's data. If all you ever used were Push() and
Pop(), then our linked list would really look like a stack. However, we provide more
general functions like GetNth() which what make our linked list more than just a stack.
Pop() should assert() fail if there is not a node to pop. Here's some sample code which
calls Pop()....
void PopTest() {
struct node* head = BuildOneTwoThree(); // build {1, 2, 3}
int a = Pop(&head); // deletes "1" node and returns 1
int b = Pop(&head); // deletes "2" node and returns 2
int c = Pop(&head); // deletes "3" node and returns 3
int len = Length(head); // the list is now empty, so len == 0
}

Pop() Unlink
Pop() is a bit tricky. Pop() needs to unlink the front node from the list and deallocate it
with a call to free(). Pop() needs to use a reference parameter like Push() so that it can
change the caller's head pointer. A good first step to writing Pop() properly is making the
memory drawing for what Pop() should do. Below is a drawing showing a Pop() of the
first node of a list. The process is basically the reverse of the 3-Step-Link-In used by
Push() (would that be "Ni Knil Pets-3"?). The overwritten pointer value is shown in gray,
and the deallocated heap memory has a big 'X' drawn on it...
13

Stack Heap
PopTest()
head

The head pointer 1 2 3


advances to refer
to the node after
the unlinked one.
The unlinked node is deallocated by a call to free().
Ironically, the unlinked node itself is not changed
immediately. It is no longer appears in the list just
because the head pointer no longer points to it.

Pop()
/*
The opposite of Push(). Takes a non-empty list
and removes the front node, and returns the data
which was in that node.
*/
int Pop(struct node** headRef) {
// your code...

5 — InsertNth()
A more difficult problem is to write a function InsertNth() which can insert a new node at
any index within a list. Push() is similar, but can only insert a node at the head end of the
list (index 0). The caller may specify any index in the range [0..length], and the new node
should be inserted so as to be at that index.
void InsertNthTest() {
struct node* head = NULL; // start with the empty list

InsertNth(&head, 0, 13); // build {13)


InsertNth(&head, 1, 42); // build {13, 42}
InsertNth(&head, 1, 5); // build {13, 5, 42}

DeleteList(&head); // clean up after ourselves


}
14

InsertNth() is complex — you will want to make some drawings to think about your
solution and afterwards, to check its correctness.
/*
A more general version of Push().
Given a list, an index 'n' in the range 0..length,
and a data element, add a new node to the list so
that it has the given index.
*/
void InsertNth(struct node** headRef, int index, int data) {
// your code...

6 — SortedInsert()
Write a SortedInsert() function which given a list that is sorted in increasing order, and a
single node, inserts the node into the correct sorted position in the list. While Push()
allocates a new node to add to the list, SortedInsert() takes an existing node, and just
rearranges pointers to insert it into the list. There are many possible solutions to this
problem.
void SortedInsert(struct node** headRef, struct node* newNode) {
// Your code...

7 — InsertSort()
Write an InsertSort() function which given a list, rearranges its nodes so they are sorted in
increasing order. It should use SortedInsert().
// Given a list, change it to be in sorted order (using SortedInsert()).
void InsertSort(struct node** headRef) { // Your code

8 — Append()
Write an Append() function that takes two lists, 'a' and 'b', appends 'b' onto the end of 'a',
and then sets 'b' to NULL (since it is now trailing off the end of 'a'). Here is a drawing of
a sample call to Append(a, b) with the start state in gray and the end state in black. At the
end of the call, the 'a' list is {1, 2, 3, 4}, and 'b' list is empty.

Stack Heap

a 1 2

3 4
15

It turns out that both of the head pointers passed to Append(a, b) need to be reference
parameters since they both may need to be changed. The second 'b' parameter is always
set to NULL. When is 'a' changed? That case occurs when the 'a' list starts out empty. In
that case, the 'a' head must be changed from NULL to point to the 'b' list. Before the call
'b' is {3, 4}. After the call, 'a' is {3, 4}.

Stack Heap

3 4

// Append 'b' onto the end of 'a', and then set 'b' to NULL.
void Append(struct node** aRef, struct node** bRef) {
// Your code...

9 — FrontBackSplit()
Given a list, split it into two sublists — one for the front half, and one for the back half. If
the number of elements is odd, the extra element should go in the front list. So
FrontBackSplit() on the list {2, 3, 5, 7, 11} should yield the two lists {2, 3, 5} and {7,
11}. Getting this right for all the cases is harder than it looks. You should check your
solution against a few cases (length = 2, length = 3, length=4) to make sure that the list
gets split correctly near the short-list boundary conditions. If it works right for length=4,
it probably works right for length=1000. You will probably need special case code to deal
with the (length <2) cases.
Hint. Probably the simplest strategy is to compute the length of the list, then use a for
loop to hop over the right number of nodes to find the last node of the front half, and then
cut the list at that point. There is a trick technique that uses two pointers to traverse the
list. A "slow" pointer advances one nodes at a time, while the "fast" pointer goes two
nodes at a time. When the fast pointer reaches the end, the slow pointer will be about half
way. For either strategy, care is required to split the list at the right point.
/*
Split the nodes of the given list into front and back halves,
and return the two lists using the reference parameters.
If the length is odd, the extra node should go in the front list.
*/
void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {
// Your code...
16

10 RemoveDuplicates()
Write a RemoveDuplicates() function which takes a list sorted in increasing order and
deletes any duplicate nodes from the list. Ideally, the list should only be traversed once.
/*
Remove duplicates from a sorted list.
*/
void RemoveDuplicates(struct node* head) {
// Your code...

11 — MoveNode()
This is a variant on Push(). Instead of creating a new node and pushing it onto the given
list, MoveNode() takes two lists, removes the front node from the second list and pushes
it onto the front of the first. This turns out to be a handy utility function to have for
several later problems. Both Push() and MoveNode() are designed around the feature that
list operations work most naturally at the head of the list. Here's a simple example of
what MoveNode() should do...
void MoveNodeTest() {
struct node* a = BuildOneTwoThree(); // the list {1, 2, 3}
struct node* b = BuildOneTwoThree();

MoveNode(&a, &b);
// a == {1, 1, 2, 3}
// b == {2, 3}
}

/*
Take the node from the front of the source, and move it to
the front of the dest.
It is an error to call this with the source list empty.
*/
void MoveNode(struct node** destRef, struct node** sourceRef) {
// Your code

12 — AlternatingSplit()
Write a function AlternatingSplit() that takes one list and divides up its nodes to make
two smaller lists. The sublists should be made from alternating elements in the original
list. So if the original list is {a, b, a, b, a}, then one sublist should be {a, a, a} and the
other should be {b, b}. You may want to use MoveNode() as a helper. The elements in
the new lists may be in any order (for some implementations, it turns out to be convenient
if they are in the reverse order from the original list.)
/*
Given the source list, split its nodes into two shorter lists.
If we number the elements 0, 1, 2, ... then all the even elements
should go in the first list, and all the odd elements in the second.
The elements in the new lists may be in any order.
*/
void AlternatingSplit(struct node* source,
struct node** aRef, struct node** bRef) {
// Your code
17

13— ShuffleMerge()
Given two lists, merge their nodes together to make one list, taking nodes alternately
between the two lists. So ShuffleMerge() with {1, 2, 3} and {7, 13, 1} should yield {1, 7,
2, 13, 3, 1}. If either list runs out, all the nodes should be taken from the other list. The
solution depends on being able to move nodes to the end of a list as discussed in the
Section 1 review. You may want to use MoveNode() as a helper. Overall, many
techniques are possible: dummy node, local reference, or recursion. Using this function
and FrontBackSplit(), you could simulate the shuffling of cards.
/*
Merge the nodes of the two lists into a single list taking a node
alternately from each list, and return the new list.
*/
struct node* ShuffleMerge(struct node* a, struct node* b) {
// Your code

14 — SortedMerge()
Write a SortedMerge() function that takes two lists, each of which is sorted in increasing
order, and merges the two together into one list which is in increasing order.
SortedMerge() should return the new list. The new list should be made by splicing
together the nodes of the first two lists (use MoveNode()). Ideally, Merge() should only
make one pass through each list. Merge() is tricky to get right — it may be solved
iteratively or recursively. There are many cases to deal with: either 'a' or 'b' may be
empty, during processing either 'a' or 'b' may run out first, and finally there's the problem
of starting the result list empty, and building it up while going through 'a' and 'b'.
/*
Takes two lists sorted in increasing order, and
splices their nodes together to make one big
sorted list which is returned.
*/
struct node* SortedMerge(struct node* a, struct node* b) {
// your code...

15 — MergeSort()
(This problem requires recursion) Given FrontBackSplit() and SortedMerge(), it's pretty
easy to write a classic recursive MergeSort(): split the list into two smaller lists,
recursively sort those lists, and finally merge the two sorted lists together into a single
sorted list. Ironically, this problem is easier than either FrontBackSplit() or
SortedMerge().
void MergeSort(struct node* headRef) {
// Your code...
18

16 — SortedIntersect()
Given two lists sorted in increasing order, create and return a new list representing the
intersection of the two lists. The new list should be made with its own memory — the
original lists should not be changed. In other words, this should be Push() list building,
not MoveNode(). Ideally, each list should only be traversed once. This problem along
with Union() and Difference() form a family of clever algorithms that exploit the
constraint that the lists are sorted to find common nodes efficiently.
/*
Compute a new sorted list that represents the intersection
of the two given sorted lists.
*/
struct node* SortedIntersect(struct node* a, struct node* b) {
// Your code

17 — Reverse()
Write an iterative Reverse() function that reverses a list by rearranging all the .next
pointers and the head pointer. Ideally, Reverse() should only need to make one pass of the
list. The iterative solution is moderately complex. It's not so difficult that it needs to be
this late in the document, but it goes here so it can be next to #18 Recursive Reverse
which is quite tricky. The efficient recursive solution is quite complex (see next
problem). (A memory drawing and some hints for Reverse() are below.)
void ReverseTest() {
struct node* head;

head = BuildOneTwoThree();
Reverse(&head);
// head now points to the list {3, 2, 1}

DeleteList(&head); // clean up after ourselves


}

Stack Heap
List reverse before and after. Before (in
ReverseTest() gray) the list is {1, 2, 3}. After (in black),
the pointers have been rearranged so the
head list is {3, 2, 1}.

1 2 3

"Push" Reverse Hint


Iterate through the main list. Move each node to the front of the result list as you go. It's
like doing a Push() operation with each node, except you use pointer re-arrangement on
19

the existing node instead of allocating a new node. You can use MoveNode() to do most
of the work, or hand code the pointer re-arrangement.

"3 Pointers" Hint


This strategy is not as good as the "Push" strategy, but it's the first one I thought of
(thanks to Owen Astrachan for pointing out the better solution). Instead of running a
single "current" pointer down the list, run three pointers (front, middle, back) down the
list in order: front is on one node, middle is just behind it, and back in turn is one behind
middle. Once the code to run the three pointers down the list is clear and tested in a
drawing, add code to reverse the .next pointer of the middle node during the iteration.
Add code to take care of the empty list and to adjust the head pointer itself.
/*
Reverse the given linked list by changing its .next pointers and
its head pointer. Takes a pointer (reference) to the head pointer.
*/
void Reverse(struct node** headRef) {
// your code...

18 — RecursiveReverse()
(This problem is difficult and is only possible if you are familiar with recursion.) There is
a short and efficient recursive solution to this problem. As before, the code should only
make a single pass over the list. Doing it with multiple passes is easier but very slow, so
here we insist on doing it in one pass.. Solving this problem requires a real understanding
of pointer code and recursion.
/*
Recursively reverses the given linked list by changing its .next
pointers and its head pointer in one pass of the list.
*/
void RecursiveReverse(struct node** headRef) {
// your code...

The Tree-List Recursion Problem


Once you are done with these problems, see the best and most complex list recursion
problem of all time: The great Tree-List-Recursion problem at
http://cslibrary.stanford.edu/109/
20

Section 3 — Solutions
1 — Count() Solution
A straightforward iteration down the list — just like Length().
int Count(struct node* head, int searchFor) {
struct node* current = head;
int count = 0;

while (current != NULL) {


if (current->data == searchFor) count++;
current = current->next;
}

return count;
}

Alternately, the iteration may be coded with a for loop instead of a while...
int Count2(struct node* head, int searchFor) {
struct node* current;
int count = 0;

for (current = head; current != NULL; current = current->next) {


if (current->data == searchFor) count++;
}

return count;
}

2 — GetNth() Solution
Combine standard list iteration with the additional problem of counting over to find the
right node. Off-by-one errors are common in this sort of code. Check it carefully against a
simple case. If it's right for n=0, n=1, and n=2, it will probably be right for n=1000.
int GetNth(struct node* head, int index) {
struct node* current = head;
int count = 0; // the index of the node we're currently looking at

while (current != NULL) {


if (count == index) return(current->data);
count++;
current = current->next;
}

assert(0); // if we get to this line, the caller was asking


// for a non-existent element so we assert fail.
}
21

3 — DeleteList() Solution
Delete the whole list and set the head pointer to NULL. There is a slight complication
inside the loop, since we need extract the .next pointer before we delete the node, since
after the delete it will be technically unavailable.
void DeleteList(struct node** headRef) {
struct node* current = *headRef; // deref headRef to get the real head
struct node* next;

while (current != NULL) {


next = current->next; // note the next pointer
free(current); // delete the node
current = next; // advance to the next node
}

*headRef = NULL; // Again, deref headRef to affect the real head back
// in the caller.
}

4 — Pop() Solution
Extract the data from the head node, delete the node, advance the head pointer to point at
the next node in line. Uses a reference parameter since it changes the head pointer.
int Pop(struct node** headRef) {
struct node* head;
int result;

head = *headRef;
assert(head != NULL);

result = head->data; // pull out the data before the node is deleted

*headRef = head->next; // unlink the head node for the caller


// Note the * -- uses a reference-pointer
// just like Push() and DeleteList().

free(head); // free the head node

return(result); // don't forget to return the data from the link


}

5 — InsertNth() Solution
This code handles inserting at the very front as a special case. Otherwise, it works by
running a current pointer to the node before where the new node should go. Uses a for
loop to march the pointer forward. The exact bounds of the loop (the use of < vs <=, n vs.
n-1) are always tricky — the best approach is to get the general structure of the iteration
correct first, and then make a careful drawing of a couple test cases to adjust the n vs. n-1
cases to be correct. (The so called "OBOB" — Off By One Boundary cases.) The OBOB
cases are always tricky and not that interesting. Write the correct basic structure and then
use a test case to get the OBOB cases correct. Once the insertion point has been
determined, this solution uses Push() to do the link in. Alternately, the 3-Step Link In
code could be pasted here directly.
22

void InsertNth(struct node** headRef, int index, int data) {


// position 0 is a special case...
if (index == 0) Push(headRef, data);
else {
struct node* current = *headRef;
int i;

for (i=0; i<index-1; i++) {


assert(current != NULL); // if this fails, index was too big
current = current->next;
}

assert(current != NULL); // tricky: you have to check one last time

Push(&(current->next), data); // Tricky use of Push() --


// The pointer being pushed on is not
// in the stack. But actually this works
// fine -- Push() works for any node pointer.
}
}

6 — SortedInsert() Solution
The basic strategy is to iterate down the list looking for the place to insert the new node.
That could be the end of the list, or a point just before a node which is larger than the new
node. The three solutions presented handle the "head end" case in different ways...
// Uses special case code for the head end
void SortedInsert(struct node** headRef, struct node* newNode) {
// Special case for the head end
if (*headRef == NULL || (*headRef)->data >= newNode->data) {
newNode->next = *headRef;
*headRef = newNode;
}
else {
// Locate the node before the point of insertion
struct node* current = *headRef;
while (current->next!=NULL && current->next->data<newNode->data) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
}
}

// Dummy node strategy for the head end


void SortedInsert2(struct node** headRef, struct node* newNode) {
struct node dummy;
struct node* current = &dummy;
dummy.next = *headRef;

while (current->next!=NULL && current->next->data<newNode->data) {


current = current->next;
}

newNode->next = current->next;
current->next = newNode;
23

*headRef = dummy.next;
}

// Local references strategy for the head end


void SortedInsert3(struct node** headRef, struct node* newNode) {
struct node** currentRef = headRef;

while (*currentRef!=NULL && (*currentRef)->data<newNode->data) {


currentRef = &((*currentRef)->next);
}

newNode->next = *currentRef; // Bug: this line used to have


// an incorrect (*currRef)->next
*currentRef = newNode;
}

7 — InsertSort() Solution
Start with an empty result list. Iterate through the source list and SortedInsert() each of its
nodes into the result list. Be careful to note the .next field in each node before moving
it into the result list.
// Given a list, change it to be in sorted order (using SortedInsert()).
void InsertSort(struct node** headRef) {
struct node* result = NULL; // build the answer here
struct node* current = *headRef; // iterate over the original list
struct node* next;

while (current!=NULL) {
next = current->next; // tricky - note the next pointer before we change it
SortedInsert(&result, current);
current = next;
}

*headRef = result;
}

8 — Append() Solution
The case where the 'a' list is empty is a special case handled first — in that case the 'a'
head pointer needs to be changed directly. Otherwise we iterate down the 'a' list until we
find its last node with the test (current->next != NULL), and then tack on the 'b'
list there. Finally, the original 'b' head is set to NULL. This code demonstrates extensive
use of pointer reference parameters, and the common problem of needing to locate the
last node in a list. (There is also a drawing of how Append() uses memory below.)
void Append(struct node** aRef, struct node** bRef) {
struct node* current;

if (*aRef == NULL) { // Special case if a is empty


*aRef = *bRef;
}
else { // Otherwise, find the end of a, and append b there
current = *aRef;
while (current->next != NULL) { // find the last node
current = current->next;
}
24

current->next = *bRef; // hang the b list off the last node


}

*bRef=NULL; // NULL the original b, since it has been appended above


}

Append() Test and Drawing


The following AppendTest() code calls Append() to join two lists. What does memory
look like just before the call to Append() exits?
void AppendTest() {
struct node* a;
struct node* b;

// set a to {1, 2}
// set b to {3, 4}

Append(&a, &b);
}

As an example of how reference parameters work, note how reference parameters in


Append() point back to the head pointers in AppendTest()...

Stack Heap

AppendTest()
a 1 2

Append(&a, &b)
aRef 3 4
bRef

current

9 — FrontBackSplit() Solution
Two solutions are presented...
// Uses the "count the nodes" strategy
void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {

int len = Length(source);


int i;
struct node* current = source;
25

if (len < 2) {
*frontRef = source;
*backRef = NULL;
}
else {
int hopCount = (len-1)/2; //(figured these with a few drawings)
for (i = 0; i<hopCount; i++) {
current = current->next;
}

// Now cut at current


*frontRef = source;
*backRef = current->next;
current->next = NULL;
}
}

// Uses the fast/slow pointer strategy


void FrontBackSplit2(struct node* source,
struct node** frontRef, struct node** backRef) {
struct node* fast;
struct node* slow;

if (source==NULL || source->next==NULL) { // length < 2 cases


*frontRef = source;
*backRef = NULL;
}
else {
slow = source;
fast = source->next;

// Advance 'fast' two nodes, and advance 'slow' one node


while (fast != NULL) {
fast = fast->next;
if (fast != NULL) {
slow = slow->next;
fast = fast->next;
}
}

// 'slow' is before the midpoint in the list, so split it in two


// at that point.
*frontRef = source;
*backRef = slow->next;
slow->next = NULL;
}
}

10 — RemoveDuplicates() Solution
Since the list is sorted, we can proceed down the list and compare adjacent nodes. When
adjacent nodes are the same, remove the second one. There's a tricky case where the node
after the next node needs to be noted before the deletion.
// Remove duplicates from a sorted list
void RemoveDuplicates(struct node* head) {
struct node* current = head;
26

if (current == NULL) return; // do nothing if the list is empty

// Compare current node with next node


while(current->next!=NULL) {
if (current->data == current->next->data) {
struct node* nextNext = current->next->next;
free(current->next);
current->next = nextNext;
}
else {
current = current->next; // only advance if no deletion
}
}
}

11 — MoveNode() Solution
The MoveNode() code is most similar to the code for Push(). It's short — just changing a
couple pointers — but it's complex. Make a drawing.
void MoveNode(struct node** destRef, struct node** sourceRef) {
struct node* newNode = *sourceRef; // the front source node
assert(newNode != NULL);

*sourceRef = newNode->next; // Advance the source pointer

newNode->next = *destRef; // Link the old dest off the new node
*destRef = newNode; // Move dest to point to the new node
}

12 — AlternatingSplit() Solution
The simplest approach iterates over the source list and use MoveNode() to pull nodes off
the source and alternately put them on 'a' and b'. The only strange part is that the nodes
will be in the reverse order that they occurred in the source list.

AlternatingSplit()
void AlternatingSplit(struct node* source,
struct node** aRef, struct node** bRef) {
struct node* a = NULL; // Split the nodes to these 'a' and 'b' lists
struct node* b = NULL;

struct node* current = source;


while (current != NULL) {
MoveNode(&a, &current); // Move a node to 'a'
if (current != NULL) {
MoveNode(&b, &current); // Move a node to 'b'
}
}
*aRef = a;
*bRef = b;
}
27

AlternatingSplit() Using Dummy Nodes


Here is an alternative approach which builds the sub-lists in the same order as the source
list. The code uses a temporary dummy header nodes for the 'a' and 'b' lists as they are
being built. Each sublist has a "tail" pointer which points to its current last node — that
way new nodes can be appended to the end of each list easily. The dummy nodes give the
tail pointers something to point to initially. The dummy nodes are efficient in this case
because they are temporary and allocated in the stack. Alternately, the "local references"
technique could be used to get rid of the dummy nodes (see Section 1 for more details).
void AlternatingSplit2(struct node* source,
struct node** aRef, struct node** bRef) {
struct node aDummy;
struct node* aTail = &aDummy; // points to the last node in 'a'
struct node bDummy;
struct node* bTail = &bDummy; // points to the last node in 'b'
struct node* current = source;

aDummy.next = NULL;
bDummy.next = NULL;

while (current != NULL) {


MoveNode(&(aTail->next), &current); // add at 'a' tail
aTail = aTail->next; // advance the 'a' tail
if (current != NULL) {
MoveNode(&(bTail->next), &current);
bTail = bTail->next;
}
}

*aRef = aDummy.next;
*bRef = bDummy.next;
}

13 SuffleMerge() Solution
There are four separate solutions included. See Section 1 for information on the various
dummy node and reference techniques.

SuffleMerge() — Dummy Node Not Using MoveNode()


struct node* ShuffleMerge(struct node* a, struct node* b) {
struct node dummy;
struct node* tail = &dummy;
dummy.next = NULL;

while (1) {
if (a==NULL) { // empty list cases
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else { // common case: move two nodes to tail
tail->next = a;
tail = a;
a = a->next;
28

tail->next = b;
tail = b;
b = b->next;
}
}

return(dummy.next);
}

SuffleMerge() — Dummy Node Using MoveNode()


Basically the same as above, but use MoveNode().
struct node* ShuffleMerge(struct node* a, struct node* b) {
struct node dummy;
struct node* tail = &dummy;
dummy.next = NULL;

while (1) {
if (a==NULL) {
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else {
MoveNode(&(tail->next), &a);
tail = tail->next;
MoveNode(&(tail->next), &b);
tail = tail->next;
}
}

return(dummy.next);
}

SuffleMerge() — Local References


Uses a local reference to get rid of the dummy nodes entirely.
struct node* ShuffleMerge(struct node* a, struct node* b) {
struct node* result = NULL;
struct node** lastPtrRef = &result;

while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}
else {
MoveNode(lastPtrRef, &a);
lastPtrRef = &((*lastPtrRef)->next);
MoveNode(lastPtrRef, &b);
lastPtrRef = &((*lastPtrRef)->next);
29

}
}

return(result);
}

SuffleMerge() — Recursive
The recursive solution is the most compact of all, but is probably not appropriate for
production code since it uses stack space proportionate to the lengths of the lists.
struct node* ShuffleMerge(struct node* a, struct node* b) {
struct node* result;
struct node* recur;

if (a==NULL) return(b); // see if either list is empty


else if (b==NULL) return(a);
else {
// it turns out to be convenient to do the recursive call first --
// otherwise a->next and b->next need temporary storage.

recur = ShuffleMerge(a->next, b->next);

result = a; // one node from a


a->next = b; // one from b
b->next = recur; // then the rest
return(result);
}
}

14 — SortedMerge() Solution
SortedMerge() Using Dummy Nodes
The strategy here uses a temporary dummy node as the start of the result list. The pointer
tail always points to the last node in the result list, so appending new nodes is easy.
The dummy node gives tail something to point to initially when the result list is empty.
This dummy node is efficient, since it is only temporary, and it is allocated in the stack.
The loop proceeds, removing one node from either 'a' or 'b', and adding it to tail. When
we are done, the result is in dummy.next.
struct node* SortedMerge(struct node* a, struct node* b) {
struct node dummy; // a dummy first node to hang the result on
struct node* tail = &dummy; // Points to the last result node --
// so tail->next is the place to add
// new nodes to the result.

dummy.next = NULL;

while (1) {
if (a == NULL) { // if either list runs out, use the other list
tail->next = b;
break;
}
else if (b == NULL) {
tail->next = a;
break;
}
30

if (a->data <= b->data) {


MoveNode(&(tail->next), &a);
}
else {
MoveNode(&(tail->next), &b);
}
tail = tail->next;
}

return(dummy.next);
}

SortedMerge() Using Local References


This solution is structurally very similar to the above, but it avoids using a dummy node.
Instead, it maintains a struct node** pointer, lastPtrRef, that always points to the last
pointer of the result list. This solves the same case that the dummy node did — dealing
with the result list when it is empty. If you are trying to build up a list at its tail, either the
dummy node or the struct node** "reference" strategy can be used (see Section 1 for
details).
struct node* SortedMerge2(struct node* a, struct node* b) {
struct node* result = NULL;
struct node** lastPtrRef = &result; // point to the last result pointer

while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}

if (a->data <= b->data) {


MoveNode(lastPtrRef, &a);
}
else {
MoveNode(lastPtrRef, &b);
}
lastPtrRef = &((*lastPtrRef)->next); // tricky: advance to point to
// the next ".next" field

return(result);
}

SortedMerge() Using Recursion


Merge() is one of those nice recursive problems where the recursive solution code is
much cleaner than the iterative code. You probably wouldn't want to use the recursive
version for production code however, because it will use stack space which is
proportional to the length of the lists.
struct node* SortedMerge3(struct node* a, struct node* b) {
struct node* result = NULL;
31

// Base cases
if (a==NULL) return(b);
else if (b==NULL) return(a);

// Pick either a or b, and recur


if (a->data <= b->data) {
result = a;
result->next = SortedMerge3(a->next, b);
}
else {
result = b;
result->next = SortedMerge3(a, b->next);
}

return(result);
}

15 — MergeSort() Solution
The MergeSort strategy is: split into sublists, sort the sublists recursively, merge the two
sorted lists together to form the answer.
void MergeSort(struct node** headRef) {
struct node* head = *headRef;
struct node* a;
struct node* b;

// Base case -- length 0 or 1


if ((head == NULL) || (head->next == NULL)) {
return;
}

FrontBackSplit(head, &a, &b); // Split head into 'a' and 'b' sublists
// We could just as well use AlternatingSplit()

MergeSort(&a); // Recursively sort the sublists


MergeSort(&b);

*headRef = SortedMerge(a, b); // answer = merge the two sorted lists together
}

(Extra for experts) Using recursive stack space proportional to the length of a list is not
recommended. However, the recursion in this case is ok — it uses stack space which is
proportional to the log of the length of the list. For a 1000 node list, the recursion will
only go about 10 deep. For a 2000 node list, it will go 11 deep. If you think about it, you
can see that doubling the size of the list only increases the depth by 1.

16 — SortedIntersect() Solution
The strategy is to advance up both lists and build the result list as we go. When the
current point in both lists are the same, add a node to the result. Otherwise, advance
whichever list is smaller. By exploiting the fact that both lists are sorted, we only traverse
each list once. To build up the result list, both the dummy node and local reference
strategy solutions are shown...
// This solution uses the temporary dummy to build up the result list
struct node* SortedIntersect(struct node* a, struct node* b) {
struct node dummy;
32

struct node* tail = &dummy;

dummy.next = NULL;

// Once one or the other list runs out -- we're done


while (a!=NULL && b!=NULL) {
if (a->data == b->data) {
Push((&tail->next), a->data);
tail = tail->next;
a = a->next;
b = b->next;
}
else if (a->data < b->data) { // advance the smaller list
a = a->next;
}
else {
b = b->next;
}
}

return(dummy.next);
}

// This solution uses the local reference


struct node* SortedIntersect2(struct node* a, struct node* b) {
struct node* result = NULL;
struct node** lastPtrRef = &result;

// Advance comparing the first nodes in both lists.


// When one or the other list runs out, we're done.
while (a!=NULL && b!=NULL) {
if (a->data == b->data) { // found a node for the intersection
Push(lastPtrRef, a->data);
lastPtrRef = &((*lastPtrRef)->next);
a=a->next;
b=b->next;
}
else if (a->data < b->data) { // advance the smaller list
a=a->next;
}
else {
b=b->next;
}
}

return(result);
}

17 — Reverse() Solution
This first solution uses the "Push" strategy with the pointer re-arrangement hand coded
inside the loop. There's a slight trickyness in that it needs to save the value of the
"current->next" pointer at the top of the loop since the body of the loop overwrites that
pointer.
/*
Iterative list reverse.
Iterate through the list left-right.
33

Move/insert each node to the front of the result list --


like a Push of the node.
*/
static void Reverse(struct node** headRef) {
struct node* result = NULL;
struct node* current = *headRef;
struct node* next;

while (current != NULL) {


next = current->next; // tricky: note the next node

current->next = result; // move the node onto the result


result = current;

current = next;
}

*headRef = result;
}

Here's the variation on the above that uses MoveNode() to do the work...
static void Reverse2(struct node** headRef) {
struct node* result = NULL;
struct node* current = *headRef;

while (current != NULL) {


MoveNode(&result, &current);
}

*headRef = result;
}

Finally, here's the back-middle-front strategy...


// Reverses the given linked list by changing its .next pointers and
// its head pointer. Takes a pointer (reference) to the head pointer.
void Reverse(struct node** headRef) {
if (*headRef != NULL) { // special case: skip the empty list

/*
Plan for this loop: move three pointers: front, middle, back
down the list in order. Middle is the main pointer running
down the list. Front leads it and Back trails it.
For each step, reverse the middle pointer and then advance all
three to get the next node.
*/

struct node* middle = *headRef; // the main pointer

struct node* front = middle->next; // the two other pointers (NULL ok)
struct node* back = NULL;

while (1) {
middle->next = back; // fix the middle node

if (front == NULL) break; // test if done


34

back = middle; // advance the three pointers


middle = front;
front = front->next;
}

*headRef = middle; // fix the head pointer to point to the new front
}
}

18 — RecursiveReverse() Solution
Probably the hardest part is accepting the concept that the
RecursiveReverse(&rest) does in fact reverse the rest. Then then there's a trick
to getting the one front node all the way to the end of the list. Make a drwaing to see how
the trick works.
void RecursiveReverse(struct node** headRef) {
struct node* first;
struct node* rest;

if (*headRef == NULL) return; // empty list base case

first = *headRef; // suppose first = {1, 2, 3}


rest = first->next; // rest = {2, 3}

if (rest == NULL) return; // empty rest base case

RecursiveReverse(&rest); // Recursively reverse the smaller {2, 3} case


// after: rest = {3, 2}

first->next->next = first; // put the first elem on the end of the list
first->next = NULL; // (tricky step -- make a drawing)

*headRef = rest; // fix the head pointer


}

The inefficient soluition is to reverse the last n-1 elements of the list, and then iterate all
the way down to the new tail and put the old head node there. That solution is very slow
compared to the above which gets the head node in the right place without extra iteration.
35

Appendix
Basic Utility Function Implementations
Here is the source code for the basic utility functions.

Length()
// Return the number of nodes in a list
int Length(struct node* head) {
int count = 0;
struct node* current = head;

while (current != NULL) {


count++;
current=current->next;
}

return(count);
}

Push()
// Given a reference (pointer to pointer) to the head
// of a list and an int, push a new node on the front of the list.
// Creates a new node with the int, links the list off the .next of the
// new node, and finally changes the head to point to the new node.
void Push(struct node** headRef, int newData) {
struct node* newNode =
(struct node*) malloc(sizeof(struct node)); // allocate node
newNode->data = newData; // put in the data
newNode->next = (*headRef); // link the old list off the new node
(*headRef) = newNode; // move the head to point to the new node
}

BuildOneTwoThree()
// Build and return the list {1, 2, 3}
struct node* BuildOneTwoThree() {
struct node* head = NULL; // Start with the empty list
Push(&head, 3); // Use Push() to add all the data
Push(&head, 2);
Push(&head, 1);

return(head);
}
Pointers and
Memory
By Nick Parlante Copyright ©1998-2000, Nick Parlante

Abstract
This document explains how pointers and memory work and how to use them—from the
basic concepts through all the major programming techniques. For each topic there is a
combination of discussion, sample C code, and drawings.

Audience
This document can be used as an introduction to pointers for someone with basic
programming experience. Alternately, it can be used to review and to fill in gaps for
someone with a partial understanding of pointers and memory. Many advanced
programming and debugging problems only make sense with a complete understanding
of pointers and memory — this document tries to provide that understanding. This
document concentrates on explaining how pointers work. For more advanced pointer
applications and practice problems, see the other resources below.

Pace
Like most CS Education Library documents, the coverage here tries to be complete but
fast. The document starts with the basics and advances through all the major topics. The
pace is fairly quick — each basic concept is covered once and usually there is some
example code and a memory drawing. Then the text moves on to the next topic. For more
practice, you can take the time to work through the examples and sample problems. Also,
see the references below for more practice problems.

Topics
Topics include: pointers, local memory, allocation, deallocation, dereference operations,
pointer assignment, deep vs. shallow copies, the ampersand operator (&), bad pointers,
the NULL pointer, value parameters, reference parameters, heap allocation and
deallocation, memory ownership models, and memory leaks. The text focuses on pointers
and memory in compiled languages like C and C++. At the end of each section, there is
some related but optional material, and in particular there are occasional notes on other
languages, such as Java.
Pointers and Memory – document #102 in the Stanford CS Education Library. This and
other free educational materials are available at http://cslibrary.stanford.edu/102/. This
document is free to be used, reproduced, sold, or retransmitted so long as this notice is
clearly reproduced at its beginning.
Other CS Education Library Documents
• Point Fun With Binky Video (http://cslibrary.stanford.edu/104/)
A silly video about pointer basics.

• Linked list Basics (http://cslibrary.stanford.edu/103/)


Introduces the basic techniques for building linked lists in C.
2

• Linked List Problems (http://cslibrary.stanford.edu/105/)


18 classic linked list problems with solutions — a great way to practice
with realistic, pointer intensive C code, and there's just no substitute for
practice!

• Essential C (http://cslibrary.stanford.edu/101/)
Complete coverage of the C language, including all of the syntax used in
this document.

Table of Contents
Section 1 Basic Pointers.......................................................................... pg. 3
The basic rules and drawings for pointers: pointers, pointees, pointer
assignment (=), pointer comparison (==), the ampersand operator (&), the
NULL pointer, bad pointers, and bad dereferences.

Section 2 Local Memory ......................................................................... pg. 11


How local variables and parameters work: local storage, allocation,
deallocation, the ampersand bug. Understanding the separation of local
memory between separate functions.

Section 3 Reference Parameters.............................................................. pg. 17


Combines the previous two sections to show how a function can use
"reference parameters" to communicate back to its caller.

Section 4 Heap Memory ........................................................................ pg. 24


Builds on all the previous sections to explain dynamic heap memory: heap
allocation, heap deallocation, array allocation, memory ownership models,
and memory leaks.

Edition
The first edition of this document was on Jan 19, 1999. This Feb 21, 2000 edition
represents only very minor changes. The author may be reached at
nick.parlante@cs.stanford.edu. The CS Education Library may be reached at
cslibrary@cs.stanford.edu.

Dedication
This document is distributed for the benefit and education of all. That someone seeking
education should have the opportunity to find it. May you learn from it in the spirit in
which it is given — to make efficiency and beauty in your designs, peace and fairness in
your actions.

Preface To The First Edition


This article has appeared to hover at around 80% done for 6 months! Every time I add
one section, I think of two more which also need to be written. I was motivated to keep
working on it since there are so many other articles which use memory, &, ... in passing
where I wanted something to refer to. I hope you find it valuable in its current form. I'm
going to ship it quickly before I accidentally start adding another section!
3

Section 1 —
Basic Pointers
Pointers — Before and After
There's a lot of nice, tidy code you can write without knowing about pointers. But once
you learn to use the power of pointers, you can never go back. There are too many things
that can only be done with pointers. But with increased power comes increased
responsibility. Pointers allow new and more ugly types of bugs, and pointer bugs can
crash in random ways which makes them more difficult to debug. Nonetheless, even with
their problems, pointers are an irresistibly powerful programming construct. (The
following explanation uses the C language syntax where a syntax is required; there is a
discussion of Java at the section.)

Why Have Pointers?


Pointers solve two common software problems. First, pointers allow different sections of
code to share information easily. You can get the same effect by copying information
back and forth, but pointers solve the problem better. Second, pointers enable complex
"linked" data structures like linked lists and binary trees.

What Is A Pointer?
Simple int and float variables operate pretty intuitively. An int variable is like a
box which can store a single int value such as 42. In a drawing, a simple variable is a
box with its current value drawn inside.

num 42

A pointer works a little differently— it does not store a simple value directly. Instead, a
pointer stores a reference to another value. The variable the pointer refers to is
sometimes known as its "pointee". In a drawing, a pointer is a box which contains the
beginning of an arrow which leads to its pointee. (There is no single, official, word for
the concept of a pointee — pointee is just the word used in these explanations.)
The following drawing shows two variables: num and numPtr. The simple variable num
contains the value 42 in the usual way. The variable numPtr is a pointer which contains
a reference to the variable num. The numPtr variable is the pointer and num is its
pointee. What is stored inside of numPtr? Its value is not an int. Its value is a
reference to an int.

A simple int variable. The current


value is the integer 42. This variable
num 42 also plays the role of pointee for the
pointer below.
numPtr
A pointer variable. The current
value is a reference to the
pointee num above.
4

Pointer Dereference
The "dereference" operation follows a pointer's reference to get the value of its pointee.
The value of the dereference of numPtr above is 42. When the dereference operation is
used correctly, it's simple. It just accesses the value of the pointee. The only restriction is
that the pointer must have a pointee for the dereference to access. Almost all bugs in
pointer code involve violating that one restriction. A pointer must be assigned a pointee
before dereference operations will work.

The NULL Pointer


The constant NULL is a special pointer value which encodes the idea of "points to
nothing." It turns out to be convenient to have a well defined pointer value which
represents the idea that a pointer does not have a pointee. It is a runtime error to
dereference a NULL pointer. In drawings, the value NULL is usually drawn as a diagonal
line between the corners of the pointer variable's box...

numPtr

The C language uses the symbol NULL for this purpose. NULL is equal to the integer
constant 0, so NULL can play the role of a boolean false. Official C++ no longer uses the
NULL symbolic constant — use the integer constant 0 directly. Java uses the symbol
null.

Pointer Assignment
The assignment operation (=) between two pointers makes them point to the same
pointee. It's a simple rule for a potentially complex situation, so it is worth repeating:
assigning one pointer to another makes them point to the same thing. The example below
adds a second pointer, second, assigned with the statement second = numPtr;.
The result is that second points to the same pointee as numPtr. In the drawing, this
means that the second and numPtr boxes both contain arrows pointing to num.
Assignment between pointers does not change or even touch the pointees. It just changes
which pointee a pointer refers to.

num 42

A second pointer ptr initialized


numPtr with the assignment
second = numPtr;. This causes
second second to refer to the same
pointeee as numPtr.
After assignment, the == test comparing the two pointers will return true. For example
(second==numPtr) above is true. The assignment operation also works with the
NULL value. An assignment operation with a NULL pointer copies the NULL value
from one pointer to another.

Make A Drawing
Memory drawings are the key to thinking about pointer code. When you are looking at
code, thinking about how it will use memory at run time....make a quick drawing to work
out your ideas. This article certainly uses drawings to show how pointers work. That's the
way to do it.
5

Sharing
Two pointers which both refer to a single pointee are said to be "sharing". That two or
more entities can cooperatively share a single memory structure is a key advantage of
pointers in all computer languages. Pointer manipulation is just technique — sharing is
often the real goal. In Section 3 we will see how sharing can be used to provide efficient
communication between parts of a program.

Shallow and Deep Copying


In particular, sharing can enable communication between two functions. One function
passes a pointer to the value of interest to another function. Both functions can access the
value of interest, but the value of interest itself is not copied. This communication is
called "shallow" since instead of making and sending a (large) copy of the value of
interest, a (small) pointer is sent and the value of interest is shared. The recipient needs to
understand that they have a shallow copy, so they know not to change or delete it since it
is shared. The alternative where a complete copy is made and sent is known as a "deep"
copy. Deep copies are simpler in a way, since each function can change their copy
without interfering with the other copy, but deep copies run slower because of all the
copying.
The drawing below shows shallow and deep copying between two functions, A() and B().
In the shallow case, the smiley face is shared by passing a pointer between the two. In the
deep case, the smiley face is copied, and each function gets their own...

Shallow / Sharing Deep / Copying

A() A()

B() B()

Section 2 will explain the above sharing technique in detail.

Bad Pointers
When a pointer is first allocated, it does not have a pointee. The pointer is "uninitialized"
or simply "bad". A dereference operation on a bad pointer is a serious runtime error. If
you are lucky, the dereference operation will crash or halt immediately (Java behaves this
way). If you are unlucky, the bad pointer dereference will corrupt a random area of
memory, slightly altering the operation of the program so that it goes wrong some
indefinite time later. Each pointer must be assigned a pointee before it can support
dereference operations. Before that, the pointer is bad and must not be used. In our
memory drawings, the bad pointer value is shown with an XXX value...

numPtr

Bad pointers are very common. In fact, every pointer starts out with a bad value.
Correct code overwrites the bad value with a correct reference to a pointee, and thereafter
the pointer works fine. There is nothing automatic that gives a pointer a valid pointee.
6

Quite the opposite — most languages make it easy to omit this important step. You just
have to program carefully. If your code is crashing, a bad pointer should be your first
suspicion.
Pointers in dynamic languages such as Perl, LISP, and Java work a little differently. The
run-time system sets each pointer to NULL when it is allocated and checks it each time it
is dereferenced. So code can still exhibit pointer bugs, but they will halt politely on the
offending line instead of crashing haphazardly like C. As a result, it is much easier to
locate and fix pointer bugs in dynamic languages. The run-time checks are also a reason
why such languages always run at least a little slower than a compiled language like C or
C++.

Two Levels
One way to think about pointer code is that operates at two levels — pointer level and
pointee level. The trick is that both levels need to be initialized and connected for things
to work. (1) the pointer must be allocated, (1) the pointee must be allocated, and (3) the
pointer must be assigned to point to the pointee. It's rare to forget step (1). But forget (2)
or (3), and the whole thing will blow up at the first dereference. Remember to account for
both levels — make a memory drawing during your design to make sure it's right.

Syntax
The above basic features of pointers, pointees, dereferencing, and assigning are the only
concepts you need to build pointer code. However, in order to talk about pointer code, we
need to use a known syntax which is about as interesting as....a syntax. We will use the C
language syntax which has the advantage that it has influenced the syntaxes of several
languages.

Pointer Type Syntax


A pointer type in C is just the pointee type followed by a asterisk (*)...
int* type: pointer to int
float* type: pointer to float
struct fraction* type: pointer to struct fraction
struct fraction** type: pointer to struct fraction*
Pointer Variables
Pointer variables are declared just like any other variable. The declaration gives the type
and name of the new variable and reserves memory to hold its value. The declaration
does not assign a pointee for the pointer — the pointer starts out with a bad value.
int* numPtr; // Declare the int* (pointer to int) variable "numPtr".
// This allocates space for the pointer, but not the pointee.
// The pointer starts out "bad".
7

The & Operator — Reference To


There are several ways to compute a reference to a pointee suitable for storing in a
pointer. The simplest way is the & operator. The & operator can go to the left of any
variable, and it computes a reference to that variable. The code below uses a pointer and
an & to produce the earlier num/numPtr example.

num 42

numPtr

void NumPtrExample() {
int num;
int* numPtr;

num = 42;
numPtr = &num; // Compute a reference to "num", and store it in numPtr
// At this point, memory looks like drawing above
}

It is possible to use & in a way which compiles fine but which creates problems at run
time — the full discussion of how to correctly use & is in Section 2. For now we will just
use & in a simple way.

The * Operator — Dereference


The star operator (*) dereferences a pointer. The * is a unary operator which goes to the
left of the pointer it dereferences. The pointer must have a pointee, or it's a runtime error.

Example Pointer Code


With the syntax defined, we can now write some pointer code that demonstrates all the
pointer rules...
void PointerTest() {
// allocate three integers and two pointers
int a = 1;
int b = 2;
int c = 3;
int* p;
int* q;

// Here is the state of memory at this point.


// T1 -- Notice that the pointers start out bad...

a 1 p

b 2 q

c 3

p = &a; // set p to refer to a


8

q = &b; // set q to refer to b


// T2 -- The pointers now have pointees

a 1 p

b 2 q

c 3

// Now we mix things up a bit...


c = *p; // retrieve p's pointee value (1) and put it in c
p = q; // change p to share with q (p's pointee is now b)
*p = 13; // dereference p to set its pointee (b) to 13 (*q is now 13)
// T3 -- Dereferences and assignments mix things up

a 1 p

b 13 q

c 1
}

Bad Pointer Example


Code with the most common sort of pointer bug will look like the above correct code, but
without the middle step where the pointers are assigned pointees. The bad code will
compile fine, but at run-time, each dereference with a bad pointer will corrupt memory in
some way. The program will crash sooner or later. It is up to the programmer to ensure
that each pointer is assigned a pointee before it is used. The following example shows a
simple example of the bad code and a drawing of how memory is likely to react...
void BadPointer() {
int* p; // allocate the pointer, but not the pointee

*p = 42; // this dereference is a serious runtime error


}
// What happens at runtime when the bad pointer is dereferenced...

Pow!
9

Pointer Rules Summary


No matter how complex a pointer structure gets, the list of rules remains short.
• A pointer stores a reference to its pointee. The pointee, in turn, stores
something useful.

• The dereference operation on a pointer accesses its pointee. A pointer may


only be dereferenced after it has been assigned to refer to a pointee. Most
pointer bugs involve violating this one rule.

• Allocating a pointer does not automatically assign it to refer to a pointee.


Assigning the pointer to refer to a specific pointee is a separate operation
which is easy to forget.

• Assignment between two pointers makes them refer to the same pointee
which introduces sharing.

Section 1 — Extra Optional Material


Extra: How Do Pointers Work In Java
Java has pointers, but they are not manipulated with explicit operators such as * and &. In
Java, simple data types such as int and char operate just as in C. More complex types
such as arrays and objects are automatically implemented using pointers. The language
automatically uses pointers behind the scenes for such complex types, and no pointer
specific syntax is required. The programmer just needs to realize that operations like
a=b; will automatically be implemented with pointers if a and b are arrays or objects. Or
put another way, the programmer needs to remember that assignments and parameters
with arrays and objects are intrinsically shallow or shared— see the Deep vs. Shallow
material above. The following code shows some Java object references. Notice that there
are no *'s or &'s in the code to create pointers. The code intrinsically uses pointers. Also,
the garbage collector (Section 4), takes care of the deallocation automatically at the end
of the function.
public void JavaShallow() {
Foo a = new Foo(); // Create a Foo object (no * in the declaration)
Foo b = new Foo(); // Create another Foo object

b=a; // This is automatically a shallow assignment --


// a and b now refer to the same object.

a.Bar(); // This could just as well be written b.Bar();

// There is no memory leak here -- the garbage collector


// will automatically recycle the memory for the two objects.
}

The Java approach has two main features...


• Fewer bugs. Because the language implements the pointer manipulation
accurately and automatically, the most common pointer bug are no longer
possible, Yay! Also, the Java runtime system checks each pointer value
every time it is used, so NULL pointer dereferences are caught
immediately on the line where they occur. This can make a programmer
much more productive.
10

• Slower. Because the language takes responsibility for implementing so


much pointer machinery at runtime, Java code runs slower than the
equivalent C code. (There are other reasons for Java to run slowly as well.
There is active research in making Java faser in interesting ways — the
Sun "Hot Spot" project.) In any case, the appeal of increased programmer
efficiency and fewer bugs makes the slowness worthwhile for some
applications.

Extra: How Are Pointers Implemented In The Machine?


How are pointers implemented? The short explanation is that every area of memory in the
machine has a numeric address like 1000 or 20452. A pointer to an area of memory is
really just an integer which is storing the address of that area of memory. The dereference
operation looks at the address, and goes to that area of memory to retrieve the pointee
stored there. Pointer assignment just copies the numeric address from one pointer to
another. The NULL value is generally just the numeric address 0 — the computer just
never allocates a pointee at 0 so that address can be used to represent NULL. A bad
pointer is really just a pointer which contains a random address — just like an
uninitialized int variable which starts out with a random int value. The pointer has not
yet been assigned the specific address of a valid pointee. This is why dereference
operations with bad pointers are so unpredictable. They operate on whatever random area
of memory they happen to have the address of.

Extra: The Term "Reference"


The word "reference" means almost the same thing as the word "pointer". The difference
is that "reference" tends to be used in a discussion of pointer issues which is not specific
to any particular language or implementation. The word "pointer" connotes the common
C/C++ implementation of pointers as addresses. The word "reference" is also used in the
phrase "reference parameter" which is a technique which uses pointer parameters for two-
way communication between functions — this technique is the subject of Section 3.

Extra: Why Are Bad Pointer Bugs So Common?


Why is it so often the case that programmers will allocate a pointer, but forget to set it to
refer to a pointee? The rules for pointers don't seem that complex, yet every programmer
makes this error repeatedly. Why? The problem is that we are trained by the tools we use.
Simple variables don't require any extra setup. You can allocate a simple variable, such as
int, and use it immediately. All that int, char, struct fraction code you have
written has trained you, quite reasonably, that a variable may be used once it is declared.
Unfortunately, pointers look like simple variables but they require the extra initialization
before use. It's unfortunate, in a way, that pointers happen look like other variables, since
it makes it easy to forget that the rules for their use are very different. Oh well. Try to
remember to assign your pointers to refer to pointees. Don't be surprised when you forget.
11

Section 2 —
Local Memory
Thanks For The Memory
Local variables are the programming structure everyone uses but no one thinks about.
You think about them a little when first mastering the syntax. But after a few weeks, the
variables are so automatic that you soon forget to think about how they work. This
situation is a credit to modern programming languages— most of the time variables
appear automatically when you need them, and they disappear automatically when you
are finished. For basic programming, this is a fine situation. However, for advanced
programming, it's going to be useful to have an idea of how variables work...

Allocation And Deallocation


Variables represent storage space in the computer's memory. Each variable presents a
convenient names like length or sum in the source code. Behind the scenes at runtime,
each variable uses an area of the computer's memory to store its value. It is not the case
that every variable in a program has a permanently assigned area of memory. Instead,
modern languages are smart about giving memory to a variable only when necessary. The
terminology is that a variable is allocated when it is given an area of memory to store its
value. While the variable is allocated, it can operate as a variable in the usual way to hold
a value. A variable is deallocated when the system reclaims the memory from the
variable, so it no longer has an area to store its value. For a variable, the period of time
from its allocation until its deallocation is called its lifetime.
The most common memory related error is using a deallocated variable. For local
variables, modern languages automatically protect against this error. With pointers, as we
will see however, the programmer must make sure that allocation is handled correctly..

Local Memory
The most common variables you use are "local" variables within functions such as the
variables num and result in the following function. All of the local variables and
parameters taken together are called its "local storage" or just its "locals", such as num
and result in the following code...
// Local storage example
int Square(int num) {
int result;

result = num * num;

return result;
}

The variables are called "local" to capture the idea that their lifetime is tied to the
function where they are declared. Whenever the function runs, its local variables are
allocated. When the function exits, its locals are deallocated. For the above example, that
means that when the Square() function is called, local storage is allocated for num and
result. Statements like result = num * num; in the function use the local
storage. When the function finally exits, its local storage is deallocated.
12

Here is a more detailed version of the rules of local storage...


1. When a function is called, memory is allocated for all of its locals. In other
words, when the flow of control hits the starting '{' for the function, all of
its locals are allocated memory. Parameters such as num and local
variables such as result in the above example both count as locals. The
only difference between parameters and local variables is that parameters
start out with a value copied from the caller while local variables start with
random initial values. This article mostly uses simple int variables for its
examples, however local allocation works for any type: structs, arrays...
these can all be allocated locally.

2. The memory for the locals continues to be allocated so long as the thread
of control is within the owning function. Locals continue to exist even if
the function temporarily passes off the thread of control by calling another
function. The locals exist undisturbed through all of this.

3. Finally, when the function finishes and exits, its locals are deallocated.
This makes sense in a way — suppose the locals were somehow to
continue to exist — how could the code even refer to them? The names
like num and result only make sense within the body of Square()
anyway. Once the flow of control leaves that body, there is no way to refer
to the locals even if they were allocated. That locals are available
("scoped") only within their owning function is known as "lexical
scoping" and pretty much all languages do it that way now.

Small Locals Example


Here is a simple example of the lifetime of local storage...
void Foo(int a) { // (1) Locals (a, b, i, scores) allocated when Foo runs
int i;
float scores[100]; // This array of 100 floats is allocated locally.

a = a + 1; // (2) Local storage is used by the computation


for (i=0; i<a; i++) {
Bar(i + a); // (3) Locals continue to exist undisturbed,
} // even during calls to other functions.

} // (4) The locals are all deallocated when the function exits.

Large Locals Example


Here is a larger example which shows how the simple rule "the locals are allocated when
their function begins running and are deallocated when it exits" can build more complex
behavior. You will need a firm grasp of how local allocation works to understand the
material in sections 3 and 4 later.
The drawing shows the sequence of allocations and deallocations which result when the
function X() calls the function Y() twice. The points in time T1, T2, etc. are marked in
the code and the state of memory at that time is shown in the drawing.
13

void X() {
int a = 1;
int b = 2;
// T1

Y(a);
// T3
Y(b);

// T5

void Y(int p) {
int q;
q = p + 2;
// T2 (first time through), T4 (second time through)
}

T1 - X()'s locals T2 - Y() is T3 - Y() exits T4 - Y() is T5 - Y() exits


have been called with p=1, and its locals called again and its locals
allocated and and its locals are deallocated. with p=2, and are deallocated.
given values.. are allocated. We are left only its locals are X()'s locals will
X()'s locals with X()'s allocated a be deallocated
continue to be locals. second time. when it exits.
allocated.
p 1 p 2
Y() Y()
q 3 q 4

a 1 a 1 a 1 a 1 a 1
X() X() X() X() X()
b 2 b 2 b 2 b 2 b 2
(optional extra...) The drawing shows the sequence of the locals being allocated and
deallocated — in effect the drawing shows the operation over time of the "stack" which is
the data structure which the system uses to implement local storage.

Observations About Local Parameters


Local variables are tightly associated with their function — they are used there and
nowhere else. Only the X() code can refer to its a and b. Only the Y() code can refer to
its p and q. This independence of local storage is the root cause of both its advantages
and disadvantages.

Advantages Of Locals
Locals are great for 90% of a program's memory needs....
Convenient. Locals satisfy a convenient need — functions often need
some temporary memory which exists only during the function's
computation. Local variables conveniently provide this sort of temporary,
independent memory.

Efficient. Relative to other memory use techniques, locals are very


efficient. Allocating and deallocating them is time efficient (fast) and they
are space efficient in the way they use and recycle memory.
14

Local Copies. Local parameters are basically local copies of the


information from the caller. This is also known as "pass by value."
Parameters are local variables which are initialized with an assignment (=)
operation from the caller. The caller is not "sharing" the parameter value
with the callee in the pointer sense— the callee is getting its own copy.
This has the advantage that the callee can change its local copy without
affecting the caller. (Such as with the "p" parameter in the above
example.) This independence is good since it keeps the operation of the
caller and callee functions separate which follows the rules of good
software engineering — keep separate components as independent as
possible.

Disadvantages Of Locals
There are two disadvantages of Locals
Short Lifetime. Their allocation and deallocation schedule (their
"lifetime") is very strict. Sometimes a program needs memory which
continues to be allocated even after the function which originally allocated
it has exited. Local variables will not work since they are deallocated
automatically when their owning function exits. This problem will be
solved later in Section 4 with "heap" memory.

Restricted Communication. Since locals are copies of the caller


parameters, they do not provide a means of communication from the callee
back to the caller. This is the downside of the "independence" advantage.
Also, sometimes making copies of a value is undesirable for other reasons.
We will see the solution to this problem below in Section 3 "Reference
Parameters".

Synonyms For "Local"


Local variables are also known as "automatic" variables since their allocation and
deallocation is done automatically as part of the function call mechanism. Local variables
are also sometimes known as "stack" variables because, at a low level, languages almost
always implement local variables using a stack structure in memory.

The Ampersand (&) Bug — TAB


Now that you understand the allocation schedule of locals, you can appreciate one of the
more ugly bugs possible in C and C++. What is wrong with the following code where the
function Victim() calls the function TAB()? To see the problem, it may be useful to make
a drawing to trace the local storage of the two functions...
// TAB -- The Ampersand Bug function
// Returns a pointer to an int
int* TAB() {
int temp;
return(&temp); // return a pointer to the local int
}

void Victim() {
int* ptr;
ptr = TAB();
*ptr = 42; // Runtime error! The pointee was local to TAB
}
15

TAB() is actually fine while it is running. The problem happens to its caller after TAB()
exits. TAB() returns a pointer to an int, but where is that int allocated? The problem is
that the local int, temp, is allocated only while TAB() is running. When TAB() exits,
all of its locals are deallocated. So the caller is left with a pointer to a deallocated
variable. TAB()'s locals are deallocated when it exits, just as happened to the locals for
Y() in the previous example.
It is incorrect (and useless) for TAB() to return a pointer to memory which is about to be
deallocated. We are essentially running into the "lifetime" constraint of local variables.
We want the int to exist, but it gets deallocated automatically. Not all uses of & between
functions are incorrect — only when used to pass a pointer back to the caller. The correct
uses of & are discussed in section 3, and the way to pass a pointer back to the caller is
shown in section 4.

Local Memory Summary


Locals are very convenient for what they do — providing convenient and efficient
memory for a function which exists only so long as the function is executing. Locals have
two deficiencies which we will address in the following sections — how a function can
communicate back to its caller (Section 3), and how a function can allocate separate
memory with a less constrained lifetime (section 4).

Section 2 — Extra Optional Material


Extra: How Does The Function Call Stack Work?
You do not need to know how local variables are implemented during a function call, but
here is a rough outline of the steps if you are curious. The exact details of the
implementation are language and compiler specific. However, the basic structure below is
approximates the method used by many different systems and languages...
To call a function such as foo(6, x+1)...
1. Evaluate the actual parameter expressions, such as the x+1, in the caller's
context.

2. Allocate memory for foo()'s locals by pushing a suitable "local block" of


memory onto a runtime "call stack" dedicated to this purpose. For
parameters but not local variables, store the values from step (1) into the
appropriate slot in foo()'s local block.

3. Store the caller's current address of execution (its "return address") and
switch execution to foo().

4. foo() executes with its local block conveniently available at the end of the
call stack.

5. When foo() is finished, it exits by popping its locals off the stack and
"returns" to the caller using the previously stored return address. Now the
caller's locals are on the end of the stack and it can resume executing.
16

For the extremely curious, here are other miscellaneous notes on the function call
process...
• This is why infinite recursion results in a "Stack Overflow Error" — the
code keeps calling and calling resulting in steps (1) (2) (3), (1) (2) (3), but
never a step (4)....eventually the call stack runs out of memory.

• This is why local variables have random initial values — step (2) just
pushes the whole local block in one operation. Each local gets its own area
of memory, but the memory will contain whatever the most recent tenant
left there. To clear all of the local block for each function call would be
too time expensive.

• The "local block" is also known as the function's "activation record" or


"stack frame". The entire block can be pushed onto the stack (step 2), in a
single CPU operation — it is a very fast operation.

• For a multithreaded environment, each thread gets its own call stack
instead of just having single, global call stack.

• For performance reasons, some languages pass some parameters through


registers and others through the stack, so the overall process is complex.
However, the apparent the lifetime of the variables will always follow the
"stack" model presented here.
17

Section 3 —
Reference Parameters
In the simplest "pass by value" or "value parameter" scheme, each function has separate,
local memory and parameters are copied from the caller to the callee at the moment of the
function call. But what about the other direction? How can the callee communicate back
to its caller? Using a "return" at the end of the callee to copy a result back to the caller
works for simple cases, but does not work well for all situations. Also, sometimes
copying values back and forth is undesirable. "Pass by reference" parameters solve all of
these problems.
For the following discussion, the term "value of interest" will be a value that the caller
and callee wish to communicate between each other. A reference parameter passes a
pointer to the value of interest instead of a copy of the value of interest. This technique
uses the sharing property of pointers so that the caller and callee can share the value of
interest.

Bill Gates Example


Suppose functions A() and B() both do computations involving Bill Gates' net worth
measured in billions of dollars — the value of interest for this problem. A() is the main
function and its stores the initial value (about 55 as of 1998). A() calls B() which tries to
add 1 to the value of interest.

Bill Gates By Value


Here is the code and memory drawing for a simple, but incorrect implementation where
A() and B() use pass by value. Three points in time, T1, T2, and T3 are marked in the
code and the state of memory is shown for each state...
void B(int worth) {
worth = worth + 1;
// T2
}
void A() {
int netWorth;
netWorth = 55; // T1

B(netWorth);
// T3 -- B() did not change netWorth
}

T1 -- The value of interest T2 -- netWorth is copied T3 -- B() exits and its local
netWorth is local to A(). to B()'s local worth. B() worth is deallocated. The
changes its local worth value of interest has not
from 55 to 56. been changed.

B() worth 55 56

A() netWorth 55 A() netWorth 55 A() netWorth 55


18

B() adds 1 to its local worth copy, but when B() exits, worth is deallocated, so
changing it was useless. The value of interest, netWorth, rests unchanged the whole
time in A()'s local storage. A function can change its local copy of the value of interest,
but that change is not reflected back in the original value. This is really just the old
"independence" property of local storage, but in this case it is not what is wanted.

By Reference
The reference solution to the Bill Gates problem is to use a single netWorth variable
for the value of interest and never copy it. Instead, each function can receives a pointer to
netWorth. Each function can see the current value of netWorth by dereferencing its
pointer. More importantly, each function can change the net worth — just dereference
the pointer to the centralized netWorth and change it directly. Everyone agrees what
the current value of netWorth because it exists in only one place — everyone has a
pointer to the one master copy. The following memory drawing shows A() and B()
functions changed to use "reference" parameters. As before, T1, T2, and T3 correspond to
points in the code (below), but you can study the memory structure without looking at the
code yet.

T1 -- The value of interest, T2 -- Instead of a copy, B() T3 -- B() exits, and


netWorth, is local to A() receives a pointer to netWorth has been
as before. netWorth. B() changed.
dereferences its pointer to
access and change the real
netWorth.

B() worth

A() netWorth 55 A() netWorth 55 56 A() netWorth 56


The reference parameter strategy: B() receives a pointer to the value of interest instead of
a copy.

Passing By Reference
Here are the steps to use in the code to use the pass-by-reference strategy...
• Have a single copy of the value of interest. The single "master" copy.

• Pass pointers to that value to any function which wants to see or change
the value.

• Functions can dereference their pointer to see or change the value of


interest.

• Functions must remember that they do not have their own local copies. If
they dereference their pointer and change the value, they really are
changing the master value. If a function wants a local copy to change
safely, the function must explicitly allocate and initialize such a local
copy.
19

Syntax
The syntax for by reference parameters in the C language just uses pointer operations on
the parameters...
1. Suppose a function wants to communicate about some value of interest —
int or float or struct fraction.

2. The function takes as its parameter a pointer to the value of interest — an


int* or float* or struct fraction*. Some programmers will
add the word "ref" to the name of a reference parameter as a reminder that
it is a reference to the value of interest instead of a copy.

3. At the time of the call, the caller computes a pointer to the value of interest
and passes that pointer. The type of the pointer (pointer to the value of
interest) will agree with the type in (2) above. If the value of interest is
local to the caller, then this will often involve a use of the & operator
(Section 1).

4. When the callee is running, if it wishes to access the value of interest, it


must dereference its pointer to access the actual value of interest.
Typically, this equates to use of the dereference operator (*) in the
function to see the value of interest.

Bill Gates By Reference


Here is the Bill Gates example written to use reference parameters. This code now
matches the by-reference memory drawing above.
// B() now uses a reference parameter -- a pointer to
// the value of interest. B() uses a dereference (*) on the
// reference parameter to get at the value of interest.
void B(int* worthRef) { // reference parameter
*worthRef = *worthRef + 1; // use * to get at value of interest
// T2
}

void A() {
int netWorth;
netWorth = 55; // T1 -- the value of interest is local to A()

B(&netWorth); // Pass a pointer to the value of interest.


// In this case using &.

// T3 -- B() has used its pointer to change the value of interest


}

Don't Make Copies


Reference parameters enable communication between the callee and its caller. Another
reason to use reference parameters is to avoid making copies. For efficiency, making
copies may be undesirable if the value of interest is large, such as an array. Making the
copy requires extra space for the copy itself and extra time to do the copying. From a
design point of view, making copies may be undesirable because as soon as there are two
copies, it is unclear which one is the "correct" one if either is changed. Proverb: "A
person with one watch always knows what time it is. A person with two watches is never
sure." Avoid making copies.
20

Simple Reference Parameter Example — Swap()


The standard example of reference parameters is a Swap() function which exchanges the
values of two ints. It's a simple function, but it does need to change the caller's memory
which is the key feature of pass by reference.

Swap() Function
The values of interest for Swap() are two ints. Therefore, Swap() does not take ints
as its parameters. It takes a pointers to int — (int*)'s. In the body of Swap() the
parameters, a and b, are dereferenced with * to get at the actual (int) values of interest.
void Swap(int* a, int* b) {
int temp;

temp = *a;
*a = *b;
*b = temp;
}

Swap() Caller
To call Swap(), the caller must pass pointers to the values of interest...
void SwapCaller() {
int x = 1;
int y = 2;

Swap(&x, &y); // Use & to pass pointers to the int values of interest
// (x and y).
}

Swap() a b temp 1

SwapCaller() x 1 2 y2 1

The parameters to Swap() are pointers to values of interest which are back in the caller's
locals. The Swap() code can dereference the pointers to get back to the caller's memory to
exchange the values. In this case, Swap() follows the pointers to exchange the values in
the variables x and y back in SwapCaller(). Swap() will exchange any two ints given
pointers to those two ints.

Swap() With Arrays


Just to demonstrate that the value of interest does not need to be a simple variable, here's
a call to Swap() to exchange the first and last ints in an array. Swap() takes int*'s, but
the ints can be anywhere. An int inside an array is still an int.
void SwapCaller2() {
int scores[10];
scores[0] = 1;
scores[9[ = 2;
Swap(&(scores[0]), &(scores[9]));// the ints of interest do not need to be
// simple variables -- they can be any int. The caller is responsible
// for computing a pointer to the int.
21

The above call to Swap() can be written equivalently as Swap(scores, scores+9)


due to the array syntax in C. You can ignore this case if it is not familiar to you — it's
not an important area of the language and both forms compile to the exact same thing
anyway.

Is The & Always Necessary?


When passing by reference, the caller does not always need to use & to compute a new
pointer to the value of interest. Sometimes the caller already has a pointer to the value of
interest, and so no new pointer computation is required. The pointer to the value of
interest can be passed through unchanged.
For example, suppose B() is changed so it calls a C() function which adds 2 to the value
of interest...
// Takes the value of interest by reference and adds 2.
void C(int* worthRef) {
*worthRef = *worthRef + 2;
}

// Adds 1 to the value of interest, and calls C().


void B(int* worthRef) {
*worthRef = *worthRef + 1; // add 1 to value of interest as before

C(worthRef); // NOTE no & required. We already have


// a pointer to the value of interest, so
// it can be passed through directly.
}

What About The & Bug TAB?


All this use of & might make you nervous — are we committing the & bug from Section
2? No, it turns out the above uses of & are fine. The & bug happens when an & passes a
pointer to local storage from the callee back to its caller. When the callee exits, its local
memory is deallocated and so the pointer no longer has a pointee. In the above, correct
cases, we use & to pass a pointer from the caller to the callee. The pointer remains valid
for the callee to use because the caller locals continue to exist while the callee is running.
The pointees will remain valid due to the simple constraint that the caller can only exit
sometime after its callee exits. Using & to pass a pointer to local storage from the caller
to the callee is fine. The reverse case, from the callee to the caller, is the & bug.

The ** Case
What if the value of interest to be shared and changed between the caller and callee is
already a pointer, such as an int* or a struct fraction*? Does that change the
rules for setting up reference parameters? No. In that case, there is no change in the rules.
They operate just as before. The reference parameter is still a pointer to the value of
interest, even if the value of interest is itself a pointer. Suppose the value of interest is
int*. This means there is an int* value which the caller and callee want to share and
change. Then the reference parameter should be an int**. For a struct
fraction* value of interest, the reference parameter is struct fraction**. A
single dereference (*) operation on the reference parameter yields the value of interest as
it did in the simple cases. Double pointer (**) parameters are common in linked list or
other pointer manipulating code were the value of interest to share and change is itself a
pointer, such as a linked list head pointer.
22

Reference Parameter Summary


Passing by value (copying) does not allow the callee to communicate back to its caller
and has also has the usual disadvantages of making copies. Pass by reference uses
pointers to avoid copying the value of interest, and allow the callee to communicate back
to the caller.
For pass by reference, there is only one copy of the value of interest, and pointers to that
one copy are passed. So if the value of interest is an int, its reference parameter is an int*.
If the value of interest is a struct fraction*, its reference parameters is a struct fraction**.
Functions use the dereference operator (*) on the reference parameter to see or change the
value of interest.

Section 3 — Extra Optional Material


Extra: Reference Parameters in Java
Because Java has no */& operators, it is not possible to implement reference parameters
in Java directly. Maybe this is ok — in the OOP paradigm, you should change objects by
sending them messages which makes the reference parameter concept unnecessary. The
caller passes the callee a (shallow) reference to the value of interest (object of interest?),
and the callee can send it a message to change it. Since all objects are intrinsically
shallow, any change is communicated back to the caller automatically since the object of
interest was never copied.

Extra: Reference Parameters in C++


Reference parameters are such a common programming task that they have been added as
an official feature to the C++ language. So programming reference parameters in C++ is
simpler than in C. All the programmer needs to do is syntactically indicate that they wish
for a particular parameter to be passed by reference, and the compiler takes care of it. The
syntax is to append a single '&' to right hand side of the parameter type. So an int
parameter passes an integer by value, but an int& parameter passes an integer value by
reference. The key is that the compiler takes care of it. In the source code, there's no
additional fiddling around with &'s or *'s. So Swap() and SwapCaller() written with C++
look simpler than in C, even though they accomplish the same thing...
23

void Swap(int& a, int& b) { // The & declares pass by reference


int temp;

temp = a; // No *'s required -- the compiler takes care of it


a = b;
b = temp;
}

void SwapCaller() {
int x = 1;
int y = 2;

Swap(x, y); // No &'s required -- the compiler takes care of it


}

The types of the various variables and parameters operate simply as they are declared
(int in this case). The complicating layer of pointers required to implement the
reference parameters is hidden. The compiler takes care of it without allowing the
complication to disturb the types in the source code.
24

Section 4 —
Heap Memory
"Heap" memory, also known as "dynamic" memory, is an alternative to local stack
memory. Local memory (Section 2) is quite automatic — it is allocated automatically on
function call and it is deallocated automatically when a function exits. Heap memory is
different in every way. The programmer explicitly requests the allocation of a memory
"block" of a particular size, and the block continues to be allocated until the programmer
explicitly requests that it be deallocated. Nothing happens automatically. So the
programmer has much greater control of memory, but with greater responsibility since
the memory must now be actively managed. The advantages of heap memory are...
Lifetime. Because the programmer now controls exactly when memory is
allocated and deallocated, it is possible to build a data structure in
memory, and return that data structure to the caller. This was never
possible with local memory which was automatically deallocated when the
function exited.

Size. The size of allocated memory can be controlled with more detail.
For example, a string buffer can be allocated at run-time which is exactly
the right size to hold a particular string. With local memory, the code is
more likely to declare a buffer size 1000 and hope for the best. (See the
StringCopy() example below.)

The disadvantages of heap memory are...


More Work. Heap allocation needs to arranged explicitly in the code
which is just more work.

More Bugs. Because it's now done explicitly in the code, realistically on
occasion the allocation will be done incorrectly leading to memory bugs.
Local memory is constrained, but at least it's never wrong.

Nonetheless, there are many problems that can only be solved with heap memory, so
that's that way it has to be. In languages with garbage collectors such as Perl, LISP, or
Java, the above disadvantages are mostly eliminated. The garbage collector takes over
most of the responsibility for heap management at the cost of a little extra time taken at
run-time.

What Does The Heap Look Like?


Before seeing the exact details, let's look at a rough example of allocation and
deallocation in the heap...

Allocation
The heap is a large area of memory available for use by the program. The program can
request areas, or "blocks", of memory for its use within the heap. In order to allocate a
block of some size, the program makes an explicit request by calling the heap allocation
function. The allocation function reserves a block of memory of the requested size in the
heap and returns a pointer to it. Suppose a program makes three allocation requests to
25

allocate memory to hold three separate GIF images in the heap each of which takes 1024
bytes of memory. After the three allocation requests, memory might look like...

Local Heap

(Free)

(Gif3)
3 separate
heap
(Gif2) blocks —
each 1024
bytes in
(Gif1) size.

Each allocation request reserves a contiguous area of the requested size in the heap and
returns a pointer to that new block to the program. Since each block is always referred to
by a pointer, the block always plays the role of a "pointee" (Section 1) and the program
always manipulates its heap blocks through pointers. The heap block pointers are
sometimes known as "base address" pointers since by convention they point to the base
(lowest address byte) of the block.
In this example, the three blocks have been allocated contiguously starting at the bottom
of the heap, and each block is 1024 bytes in size as requested. In reality, the heap
manager can allocate the blocks wherever it wants in the heap so long as the blocks do
not overlap and they are at least the requested size. At any particular moment, some areas
in the heap have been allocated to the program, and so are "in use". Other areas have yet
to be committed and so are "free" and are available to satisfy allocation requests. The
heap manager has its own, private data structures to record what areas of the heap are
committed to what purpose at any moment The heap manager satisfies each allocation
request from the pool of free memory and updates its private data structures to record
which areas of the heap are in use.

Deallocation
When the program is finished using a block of memory, it makes an explicit deallocation
request to indicate to the heap manager that the program is now finished with that block.
The heap manager updates its private data structures to show that the area of memory
occupied by the block is free again and so may be re-used to satisfy future allocation
requests. Here's what the heap would look like if the program deallocates the second of
the three blocks...
26

Local Heap

(Free)

(Gif3)

(Free)

(Gif1)

After the deallocation, the pointer continues to point to the now deallocated block. The
program must not access the deallocated pointee. This is why the pointer is drawn in gray
— the pointer is there, but it must not be used. Sometimes the code will set the pointer to
NULL immediately after the deallocation to make explicit the fact that it is no longer
valid.

Programming The Heap


Programming the heap looks pretty much the same in most languages. The basic features
are....
• The heap is an area of memory available to allocate areas ("blocks") of
memory for the program.

• There is some "heap manager" library code which manages the heap for
the program. The programmer makes requests to the heap manager, which
in turn manages the internals of the heap. In C, the heap is managed by the
ANSI library functions malloc(), free(), and realloc().

• The heap manager uses its own private data structures to keep track of
which blocks in the heap are "free" (available for use) and which blocks
are currently in use by the program and how large those blocks are.
Initially, all of the heap is free.

• The heap may be of a fixed size (the usual conceptualization), or it may


appear to be of a fixed but extremely large size backed by virtual memory.
In either case, it is possible for the heap to get "full" if all of its memory
has been allocated and so it cannot satisfy an allocation request. The
allocation function will communicate this run-time condition in some way
to the program — usually by returning a NULL pointer or raising a
language specific run-time exception.

• The allocation function requests a block in the heap of a particular size.


The heap manager selects an area of memory to use to satisfy the request,
marks that area as "in use" in its private data structures, and returns a
pointer to the heap block. The caller is now free to use that memory by
dereferencing the pointer. The block is guaranteed to be reserved for the
sole use of the caller — the heap will not hand out that same area of
memory to some other caller. The block does not move around inside the
27

heap — its location and size are fixed once it is allocated. Generally, when
a block is allocated, its contents are random. The new owner is responsible
for setting the memory to something meaningful. Sometimes there is
variation on the memory allocation function which sets the block to all
zeros (calloc() in C).

• The deallocation function is the opposite of the allocation function. The


program makes a single deallocation call to return a block of memory to
the heap free area for later re-use. Each block should only be deallocated
once. The deallocation function takes as its argument a pointer to a heap
block previously furnished by the allocation function. The pointer must be
exactly the same pointer returned earlier by the allocation function, not
just any pointer into the block. After the deallocation, the program must
treat the pointer as bad and not access the deallocated pointee.

C Specifics
In the C language, the library functions which make heap requests are malloc() ("memory
allocate") and free(). The prototypes for these functions are in the header file <stdlib.h>.
Although the syntax varies between languages, the roles of malloc() and free() are nearly
identical in all languages...
void* malloc(unsigned long size); The malloc() function
takes an unsigned integer which is the requested size of the block
measured in bytes. Malloc() returns a pointer to a new heap block if the
allocation is successful, and NULL if the request cannot be satisfied
because the heap is full. The C operator sizeof() is a convenient way to
compute the size in bytes of a type —sizeof(int) for an int pointee,
sizeof(struct fraction) for a struct fraction pointee.

void free(void* heapBlockPointer); The free() function


takes a pointer to a heap block and returns it to the free pool for later re-
use. The pointer passed to free() must be exactly the pointer returned
earlier by malloc(), not just a pointer to somewhere in the block. Calling
free() with the wrong sort of pointer is famous for the particularly ugly
sort of crashing which it causes. The call to free() does not need to give
the size of the heap block — the heap manager will have noted the size in
its private data structures. The call to free() just needs to identify which
block to deallocate by its pointer. If a program correctly deallocates all of
the memory it allocates, then every call to malloc() will later be matched
by exactly one call to free() As a practical matter however, it is not always
necessary for a program to deallocate every block it allocates — see
"Memory Leaks" below.

Simple Heap Example


Here is a simple example which allocates an int block in the heap, stores the number 42
in the block, and then deallocates it. This is the simplest possible example of heap block
allocation, use, and deallocation. The example shows the state of memory at three
different times during the execution of the above code. The stack and heap are shown
separately in the drawing — a drawing for code which uses stack and heap memory needs
to distinguish between the two areas to be accurate since the rules which govern the two
areas are so different. In this case, the lifetime of the local variable intPtr is totally
separate from the lifetime of the heap block, and the drawing needs to reflect that
difference.
28

void Heap1() {
int* intPtr;
// Allocates local pointer local variable (but not its pointee)
// T1
Local Heap

intPtr

// Allocates heap block and stores its pointer in local variable.


// Dereferences the pointer to set the pointee to 42.
intPtr = malloc(sizeof(int));
*intPtr = 42;
// T2
Local Heap

intPtr 42

// Deallocates heap block making the pointer bad.


// The programmer must remember not to use the pointer
// after the pointee has been deallocated (this is
// why the pointer is shown in gray).
free(intPtr);
// T3
Local Heap

intPtr

Simple Heap Observations


• After the allocation call allocates the block in the heap. The program
stores the pointer to the block in the local variable intPtr. The block is the
"pointee" and intPtr is its pointer as shown at T2. In this state, the pointer
may be dereferenced safely to manipulate the pointee. The pointer/pointee
rules from Section 1 still apply, the only difference is how the pointee is
initially allocated.
29

• At T1 before the call to malloc(), intPtr is uninitialized does not have a


pointee — at this point intPtr "bad" in the same sense as discussed in
Section 1. As before, dereferencing such an uninitialized pointer is a
common, but catastrophic error. Sometimes this error will crash
immediately (lucky). Other times it will just slightly corrupt a random data
structure (unlucky).

• The call to free() deallocates the pointee as shown at T3. Dereferencing


the pointer after the pointee has been deallocated is an error.
Unfortunately, this error will almost never be flagged as an immediate
run-time error. 99% of the time the dereference will produce reasonable
results 1% of the time the dereference will produce slightly wrong results.
Ironically, such a rarely appearing bug is the most difficult type to track
down.

• When the function exits, its local variable intPtr will be automatically
deallocated following the usual rules for local variables (Section 2). So
this function has tidy memory behavior — all of the memory it allocates
while running (its local variable, its one heap block) is deallocated by the
time it exits.

Heap Array
In the C language, it's convenient to allocate an array in the heap, since C can treat any
pointer as an array. The size of the array memory block is the size of each element (as
computed by the sizeof() operator) multiplied by the number of elements (See CS
Education Library/101 The C Language, for a complete discussion of C, and arrays and
pointers in particular). So the following code heap allocates an array of 100 struct
fraction's in the heap, sets them all to 22/7, and deallocates the heap array...
void HeapArray() {
struct fraction* fracts;
int i;

// allocate the array


fracts = malloc(sizeof(struct fraction) * 100);

// use it like an array -- in this case set them all to 22/7


for (i=0; i<99; i++) {
fracts[i].numerator = 22;
fracts[i].denominator = 7;
}

// Deallocate the whole array


free(fracts);
}
30

Heap String Example


Here is a more useful heap array example. The StringCopy() function takes a C string,
makes a copy of that string in the heap, and returns a pointer to the new string. The caller
takes over ownership of the new string and is responsible for freeing it.
/*
Given a C string, return a heap allocated copy of the string.
Allocate a block in the heap of the appropriate size,
copies the string into the block, and returns a pointer to the block.
The caller takes over ownership of the block and is responsible
for freeing it.
*/
char* StringCopy(const char* string) {
char* newString;
int len;

len = strlen(string) + 1; // +1 to account for the '\0'


newString = malloc(sizeof(char)*len); // elem-size * number-of-elements
assert(newString != NULL); // simplistic error check (a good habit)
strcpy(newString, string); // copy the passed in string to the block

return(newString); // return a ptr to the block


}

Heap String Observations


StringCopy() takes advantage of both of the key features of heap memory...
Size. StringCopy() specifies, at run-time, the exact size of the block
needed to store the string in its call to malloc(). Local memory cannot do
that since its size is specified at compile-time. The call to
sizeof(char) is not really necessary, since the size of char is 1 by
definition. In any case, the example demonstrates the correct formula for
the size of an array block which is element-size * number-of-elements.

Lifetime. StringCopy() allocates the block, but then passes ownership of it


to the caller. There is no call to free(), so the block continues to exist even
after the function exits. Local memory cannot do that. The caller will need
to take care of the deallocation when it is finished with the string.

Memory Leaks
What happens if some memory is heap allocated, but never deallocated? A program
which forgets to deallocate a block is said to have a "memory leak" which may or may
not be a serious problem. The result will be that the heap gradually fill up as there
continue to be allocation requests, but no deallocation requests to return blocks for re-use.
For a program which runs, computes something, and exits immediately, memory leaks
are not usually a concern. Such a "one shot" program could omit all of its deallocation
requests and still mostly work. Memory leaks are more of a problem for a program which
runs for an indeterminate amount of time. In that case, the memory leaks can gradually
fill the heap until allocation requests cannot be satisfied, and the program stops working
or crashes. Many commercial programs have memory leaks, so that when run for long
enough, or with large data-sets, they fill their heaps and crash. Often the error detection
and avoidance code for the heap-full error condition is not well tested, precisely because
the case is rarely encountered with short runs of the program — that's why filling the
heap often results in a real crash instead of a polite error message. Most compilers have a
31

"heap debugging" utility which adds debugging code to a program to track every
allocation and deallocation. When an allocation has no matching deallocation, that's a
leak, and the heap debugger can help you find them.

Ownership
StringCopy() allocates the heap block, but it does not deallocate it. This is so the caller
can use the new string. However, this introduces the problem that somebody does need to
remember to deallocate the block, and it is not going to be StringCopy(). That is why the
comment for StringCopy() mentions specifically that the caller is taking on ownership of
the block. Every block of memory has exactly one "owner" who takes responsibility for
deallocating it. Other entities can have pointers, but they are just sharing. There's only
one owner, and the comment for StringCopy() makes it clear that ownership is being
passed from StringCopy() to the caller. Good documentation always remembers to
discuss the ownership rules which a function expects to apply to its parameters or return
value. Or put the other way, a frequent error in documentation is that it forgets to
mention, one way or the other, what the ownership rules are for a parameter or return
value. That's one way that memory errors and leaks are created.

Ownership Models
The two common patterns for ownership are...
Caller ownership. The caller owns its own memory. It may pass a pointer
to the callee for sharing purposes, but the caller retains ownership. The
callee can access things while it runs, and allocate and deallocate its own
memory, but it should not disrupt the caller's memory.

Callee allocated and returned. The callee allocates some memory and
returns it to the caller. This happens because the result of the callee
computation needs new memory to be stored or represented. The new
memory is passed to the caller so they can see the result, and the caller
must take over ownership of the memory. This is the pattern demonstrated
in StringCopy().

Heap Memory Summary


Heap memory provides greater control for the programmer — the blocks of memory can
be requested in any size, and they remain allocated until they are deallocated explicitly.
Heap memory can be passed back to the caller since it is not deallocated on exit, and it
can be used to build linked structures such as linked lists and binary trees. The
disadvantage of heap memory is that the program must make explicit allocation and
deallocate calls to manage the heap memory. The heap memory does not operate
automatically and conveniently the way local memory does.
Tree List Recursion Problem Page: 1

The Great Tree-List Recursion Problem


by Nick Parlante
nick.parlante@cs.stanford.edu
Copyright 2000, Nick Parlante

This article presents one of the neatest recursive pointer problems ever devised. This an advanced problem that
uses pointers, binary trees, linked lists, and some significant recursion. This article includes the problem statement,
a few explanatory diagrams, and sample solution code in Java and C. Thanks to Stuart Reges for originally showing
me the problem.

Stanford CS Education Library Doc #109

This is article #109 in the Stanford CS Education Library -- http://cslibrary.stanford.edu/109/. This and other free
educational materials are available at http://cslibrary.stanford.edu/. Permission is given for this article to be
used, reproduced, or sold so long this paragraph and the copyright are clearly reproduced. Related articles in the
library include Linked List Basics (#103), Linked List Problems (#105), and Binary Trees (#110).

Contents

1. Ordered binary tree


2. Circular doubly linked list
3. The Challenge
4. Problem Statement
5. Lessons and Solution Code

Introduction
The problem will use two data structures -- an ordered binary tree and a circular doubly linked list. Both data
structures store sorted elements, but they look very different.

1. Ordered Binary Tree


In the ordered binary tree, each node contains a single data element and "small" and "large" pointers to sub-trees
(sometimes the two pointers are just called "left" and "right"). Here's an ordered binary tree of the numbers 1
through 5...

http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 2

Figure-1 -- ordered binary tree

All the nodes in the "small" sub-tree are less than or equal to the data in the parent node. All the nodes in the
"large" sub-tree are greater than the parent node. So in the example above, all the nodes in the "small" sub-tree off
the 4 node are less than or equal to 4, and all the nodes in "large" sub-tree are greater than 4. That pattern applies
for each node in the tree. A null pointer effectively marks the end of a branch in the tree. Formally, a null pointer
represents a tree with zero elements. The pointer to the topmost node in a tree is called the "root".

2. Circular Doubly Linked List


Here's a circular doubly linked list of the numbers 1 through 5...

http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 3

Figure-2 -- doubly linked circular list

The circular doubly linked list is a standard linked list with two additional features...

"Doubly linked" means that each node has two pointers -- the usual "next" pointer that points to the next
node in the list and a "previous" pointer to the previous node.
"Circular" means that the list does not terminate at the first and last nodes. Instead, the "next" from the
last node wraps around to the first node. Likewise, the "previous" from the first node wraps around to the
last node.

We'll use the convention that a null pointer represents a list with zero elements. It turns out that a length-1 list
looks a little silly...

Figure-3 -- a length-1 circular doubly linked list

The single node in a length-1 list is both the first and last node, so its pointers point to itself. Fortunately, the
length-1 case obeys the rules above so no special case is required.

The Trick -- Separated at Birth?


Here's the trick that underlies the Great Tree-List Problem: look at the nodes that make up the ordered binary
tree. Now look at the nodes that make up the linked list. The nodes have the same type structure -- they each
contain an element and two pointers. The only difference is that in the tree, the two pointers are labeled "small"
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 4

and "large" while in the list they are labeled "previous" and "next". Ignoring the labeling, the two node types are
the same.

3. The Challenge
The challenge is to take an ordered binary tree and rearrange the internal pointers to make a circular doubly linked
list out of it. The "small" pointer should play the role of "previous" and the "large" pointer should play the role of
"next". The list should be arranged so that the nodes are in increasing order...

Figure-4 -- original tree with list "next" arrows added

This drawing shows the original tree drawn with plain black lines with the "next" pointers for the desired list
structure drawn as arrows. The "previous" pointers are not shown.

Complete Drawing

http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 5

Figure-5 -- original tree with "next" and "previous" list arrows added
This drawing shows the all of the problem state -- the original tree is drawn with plain black lines and the
desired next/previous pointers are added in as arrows. Notice that starting with the head pointer, the structure of
next/previous pointers defines a list of the numbers 1 through 5 with exactly the same structure as the list in
figure-2. Although the nodes appear to have different spatial arrangement between the two drawings, that's just
an artifact of the drawing. The structure defined by the the pointers is what matters.

4. Problem Statement
Here's the formal problem statement: Write a recursive function treeToList(Node root) that takes an ordered
binary tree and rearranges the internal pointers to make a circular doubly linked list out of the tree nodes. The
"previous" pointers should be stored in the "small" field and the "next" pointers should be stored in the "large"
field. The list should be arranged so that the nodes are in increasing order. Return the head pointer to the new list.
The operation can be done in O(n) time -- essentially operating on each node once. Basically take figure-1 as input
and rearrange the pointers to make figure-2.

Try the problem directly, or see the hints below.

Hints
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 6

Hint #1

The recursion is key. Trust that the recursive call on each sub-tree works and concentrate on assembling the outputs
of the recursive calls to build the result. It's too complex to delve into how each recursive call is going to work --
trust that it did work and assemble the answer from there.

Hint #2

The recursion will go down the tree, recursively changing the small and large sub-trees into lists, and then append
those lists together with the parent node to make larger lists. Separate out a utility function append(Node a,
Node b) that takes two circular doubly linked lists and appends them together to make one list which is returned.
Writing a separate utility function helps move some of the complexity out of the recursive function.

5. Lessons and Solution Code


The solution code is given below in Java and C. The most important method is treeToList() and the helper methods
join() and append(). Here are the lessons I see in the two solutions...

Trust that the recursive calls return correct output when fed correct input -- make the leap of faith. Look at
the partial results that the recursive calls give you, and construct the full result from them. If you try to
step into the recursive calls to think how they are working, you'll go crazy.
Decomposing out well defined helper functions is a good idea. Writing the list-append code separately
helps you concentrate on the recursion which is complex enough on its own.

Java Solution Code


// TreeList.java
/*
Demonstrates the greatest recursive pointer problem ever --
recursively changing an ordered binary tree into a circular
doubly linked list.
See http://cslibrary.stanford.edu/109/

This code is not especially OOP.

This code is free for any purpose.


Feb 22, 2000
Nick Parlante nick.parlante@cs.stanford.edu
*/

/*
This is the simple Node class from which the tree and list
are built. This does not have any methods -- it's just used
as dumb storage by TreeList.
The code below tries to be clear where it treats a Node pointer
as a tree vs. where it is treated as a list.
*/
class Node {
int data;
Node small;
Node large;

public Node(int data) {


http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 7

this.data = data;
small = null;
large = null;
}
}

/*
TreeList main methods:
-join() -- utility to connect two list nodes
-append() -- utility to append two lists
-treeToList() -- the core recursive function
-treeInsert() -- used to build the tree
*/
class TreeList {
/*
helper function -- given two list nodes, join them
together so the second immediately follow the first.
Sets the .next of the first and the .previous of the second.
*/
public static void join(Node a, Node b) {
a.large = b;
b.small = a;
}

/*
helper function -- given two circular doubly linked
lists, append them and return the new list.
*/
public static Node append(Node a, Node b) {
// if either is null, return the other
if (a==null) return(b);
if (b==null) return(a);

// find the last node in each using the .previous pointer


Node aLast = a.small;
Node bLast = b.small;

// join the two together to make it connected and circular


join(aLast, b);
join(bLast, a);

return(a);
}

/*
--Recursion--
Given an ordered binary tree, recursively change it into
a circular doubly linked list which is returned.
*/
public static Node treeToList(Node root) {
// base case: empty tree -> empty list
if (root==null) return(null);

// Recursively do the subtrees (leap of faith!)


Node aList = treeToList(root.small);
Node bList = treeToList(root.large);

// Make the single root node into a list length-1


// in preparation for the appending
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 8

root.small = root;
root.large = root;

// At this point we have three lists, and it's


// just a matter of appending them together
// in the right order (aList, root, bList)
aList = append(aList, root);
aList = append(aList, bList);

return(aList);
}

/*
Given a non-empty tree, insert a new node in the proper
place. The tree must be non-empty because Java's lack
of reference variables makes that case and this
method messier than they should be.
*/
public static void treeInsert(Node root, int newData) {
if (newData<=root.data) {
if (root.small!=null) treeInsert(root.small, newData);
else root.small = new Node(newData);
}
else {
if (root.large!=null) treeInsert(root.large, newData);
else root.large = new Node(newData);
}
}

// Do an inorder traversal to print a tree


// Does not print the ending "\n"
public static void printTree(Node root) {
if (root==null) return;
printTree(root.small);
System.out.print(Integer.toString(root.data) + " ");
printTree(root.large);
}

// Do a traversal of the list and print it out


public static void printList(Node head) {
Node current = head;

while (current != null) {


System.out.print(Integer.toString(current.data) + " ");
current = current.large;
if (current == head) break;
}

System.out.println();
}

// Demonstrate tree->list with the list 1..5


public static void main(String[] args) {

// first build the tree shown in the problem document


// http://cslibrary.stanford.edu/109/
Node root = new Node(4);
treeInsert(root, 2);
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 9

treeInsert(root, 1);
treeInsert(root, 3);
treeInsert(root, 5);

System.out.println("tree:");
printTree(root); // 1 2 3 4 5
System.out.println();

System.out.println("list:");
Node head = treeToList(root);
printList(head); // 1 2 3 4 5 yay!
}
}

C Solution Code
/*
TreeList.c

C code version of the great Tree-List recursion problem.


See http://cslibrary.stanford.edu/109/ for the full
discussion and the Java solution.

This code is free for any purpose.


Feb 22, 2000
Nick Parlante nick.parlante@cs.stanford.edu
*/

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

/* The node type from which both the tree and list are built */
struct node {
int data;
struct node* small;
struct node* large;
};
typedef struct node* Node;

/*
helper function -- given two list nodes, join them
together so the second immediately follow the first.
Sets the .next of the first and the .previous of the second.
*/
static void join(Node a, Node b) {
a->large = b;
b->small = a;
}

/*
helper function -- given two circular doubly linked
lists, append them and return the new list.
*/
static Node append(Node a, Node b) {
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 10

Node aLast, bLast;

if (a==NULL) return(b);
if (b==NULL) return(a);

aLast = a->small;
bLast = b->small;

join(aLast, b);
join(bLast, a);

return(a);
}

/*
--Recursion--
Given an ordered binary tree, recursively change it into
a circular doubly linked list which is returned.
*/
static Node treeToList(Node root) {
Node aList, bList;

if (root==NULL) return(NULL);

/* recursively solve subtrees -- leap of faith! */


aList = treeToList(root->small);
bList = treeToList(root->large);

/* Make a length-1 list ouf of the root */


root->small = root;
root->large = root;

/* Append everything together in sorted order */


aList = append(aList, root);
aList = append(aList, bList);

return(aList);

/* Create a new node */


static Node newNode(int data) {
Node node = (Node) malloc(sizeof(struct node));
node->data = data;
node->small = NULL;
node->large = NULL;
return(node);
}

/* Add a new node into a tree */


static void treeInsert(Node* rootRef, int data) {
Node root = *rootRef;
if (root == NULL) *rootRef = newNode(data);
else {
if (data <= root->data) treeInsert(&(root->small), data);
else treeInsert(&(root->large), data);
}
}

http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 11

static void printList(Node head) {


Node current = head;

while(current != NULL) {
printf("%d ", current->data);
current = current->large;
if (current == head) break;
}
printf("\n");
}

/* Demo that the code works */


int main() {
Node root = NULL;
Node head;

treeInsert(&root, 4);
treeInsert(&root, 2);
treeInsert(&root, 1);
treeInsert(&root, 3);
treeInsert(&root, 5);

head = treeToList(root);

printList(head); /* prints: 1 2 3 4 5 */

return(0);
}

http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Unix
Programming
Tools
By Parlante, Zelenski, and many others Copyright ©1998-2001, Stanford University

Introduction
This article explains the overall edit-compile-link-debug programming cycle and
introduces several common Unix programming tools -- gcc, make, gdb, emacs, and the
Unix shell. The goal is to describe the major features and typcial uses of the tools and
show how they fit together with enough detail for simple projects. We've used a version
of this article at Stanford to help students get started with Unix.

Contents
Introduction — the compile-link process 1
The gcc compiler/linker 2
The make project utility 5
The gdb debugger 8
The emacs editor 13
Summary of Unix shell commands 15
This is document #107, Unix Programming Tools, in the Stanford CS Education Library.
This and other free educational materials are available at http://cslibrary.stanford.edu/.
This document is free to be used, reproduced, or sold so long as it is intact and
unchanged.
Other Resources
This article is an introduction — for more detailed information about a particular tool, see
the tool's man pages and xinfo entries. Also, O'Reilly & Associates publishes a pretty
good set of references for many Unix related tools (the books with animal pictures on the
cover). For basic coverage of the C programming language, see CS Education Library
#101, (http://cslibrary.stanford.edu/101/).

The Compile Process


Before going into detail about the individual tools themselves, it is useful to review the
overall process that goes into building an executable program. After the source text files
have been edited, there are two steps in the build process: compiling and linking. Each
source file (foo.c) is compiled into an object file (foo.o). Each object file contain a system
dependent, compiled representation of the program as described in its source file.
Typically the file name of an object module is the same as the source file that produced it,
but with a ".o" file extension — "main.c" is compiled to produce "main.o". The .o file
will include references, known as symbols, to functions, variables, etc. that the code
needs. The individual object files are then linked together to produce a single executable
file which the system loader can use when the program is actually run. The link step will
2

also bring in library object files that contain the definitions of library functions like
printf() and malloc(). The overall process looks like this...

main.c module1.c module2.c

C compiler
main.o module1.o module2.o

library
Linker functions

program

Section 1 — gcc
The following discussion is about the gcc compiler, a product of the open-source GNU
project (www.gnu.org). Using gcc has several advantages— it tends to be pretty up-to-
date and reliable, it's available on a variety of platforms, and of course it's free and open-
source. Gcc can compile C, C++, and objective-C. Gcc is actually both a compiler and a
linker. For simple problems, a single call to gcc will perform the entire compile-link
operation. For example, for small projects you might use a command like the following
which compiles and links together three .c files to create an executable named "program".
gcc main.c module1.c module2.c -o program
The above line equivalently could be re-written to separate out the three compilation
steps of the .c files followed by one link step to build the program.
gcc -c main.c ## Each of these compiles a .c
gcc -c module1.c
gcc -c module2.c
gcc main.o module1.o module2.o -o program ## This line links the .o's
## to build the program

The general form for invoking gcc is...


gcc options files

where options is a list of command flags that control how the compiler works, and
files is a list of files that gcc reads or writes depending on the options

Command-line options
Like most Unix programs, gcc supports many command-line options to control its
operation. They are all documented in its man page. We can safely ignore most of these
options, and concentrate on the most commonly used ones: -c, -o, -g, -Wall,
-I, -L, and -l.
3

-c files Direct gcc to compile the source files into an object files without going
through the linking stage. Makefiles (below) use this option to compile
files one at a time.

-o file Specifies that gcc's output should be named file. If this option is not
specified, then the default name used depends on the context...(a) if
compiling a source .c file, the output object file will be named with the
same name but with a .o extension. Alternately, (b) if linking to create
an executable, the output file will be named a.out. Most often, the -o
option is used to specify the output filename when linking an
executable, while for compiling, people just let the default .c/.o
naming take over.

It's a memorable error if your -o option gets switched around in the


command line so it accidentally comes before a source file like
"...-o foo.c program" -- this can overwrite your source file --
bye bye source file!

-g Directs the compiler to include extra debugging information in its


output. We recommend that you always compile your source with this
option set, since we encourage you to gain proficiency using the
debugger such as gdb (below).

Note -- the debugging information generated is for gdb, and could


possibly cause problems with other debuggers such as dbx.

-Wall Give warnings about possible errors in the source code. The issues
noticed by -Wall are not errors exactly, they are constructs that the
compiler believes may be errors. We highly recommend that you
compile your code with -Wall. Finding bugs at compile time is soooo
much easier than run time. the -Wall option can feel like a nag, but it's
worth it. If a student comes to me with an assignment that does not
work, and it produces -Wall warnings, then maybe 30% of the time,
the warnings were a clue towards the problem. 30% may not sound
like that much, but you have to appreciate that it's free debugging.

Sometimes -Wall warnings are not actually problems. The code is ok,
and the compiler just needs to be convinced. Don't ignore the warning.
Fix up the source code so the warning goes away. Getting used to
compiles that produce "a few warnings" is a very bad habit.

Here's an example bit of code you could use to assign and test a flag
variable in one step...

int flag;

if (flag = IsPrime(13)) {
...
}

The compiler will give a warning about a possibly unintended


assignment, although in this case the assignment is correct. This
warning would catch the common bug where you meant to type == but
typed = instead. To get rid of the warning, re-write the code to make
the test explicit...
4

int flag;

if ((flag = IsPrime(13)) != 0) {
...
}

This gets rid of the warning, and the generated code will be the same
as before. Alternately, you can enclose the entire test in another set of
parentheses to indicate your intentions. This is a small price to pay to
get -Wall to find some of your bugs for you.

-Idir Adds the directory dir to the list of directories searched for #include
files. The compiler will search several standard directories
automatically. Use this option to add a directory for the compiler to
search. There is no space between the "-I" and the directory name. If
the compile fails because it cannot find a #include file, you need a -I to
fix it.

Extra: Here's how to use the unix "find" command to find your
#include file. This example searches the /usr/include directory for all
the include files with the pattern "inet" in them...

nick% find /usr/include -name '*inet*'


/usr/include/arpa/inet.h
/usr/include/netinet
/usr/include/netinet6

-lmylib (lower case 'L') Search the library named mylib for unresolved
symbols (functions, global variables) when linking. The actual name of
the file will be libmylib.a, and must be found in either the default
locations for libraries or in a directory added with the -L flag (below).

The position of the -l flag in the option list is important because the
linker will not go back to previously examined libraries to look for
unresolved symbols. For example, if you are using a library that
requires the math library it must appear before the math library on the
command line otherwise a link error will be reported. Again, there is
no space between the option flag and the library file name, and that's a
lower case 'L', not the digit '1'. If your link step fails because a symbol
cannot be found, you need a -l to add the appropriate library, or
somehow you are compiling with the wrong name for the function or
-Ldir globalthe
Adds variable.
directory dir to the list of directories searched for library files
specified by the -l flag. Here too, there is no space between the
option flag and the library directory name. If the link step fails because
a library file cannot be found, you need a -L, or the library file name is
wrong.
5

Section 2 — make
Typing out the gcc commands for a project gets less appealing as the project gets bigger.
The "make" utility automates the process of compiling and linking. With make, the
programmer specifies what the files are in the project and how they fit together, and then
make takes care of the appropriate compile and link steps. Make can speed up your
compiles since it is smart enough to know that if you have 10 .c files but you have only
changed one, then only that one file needs to be compiled before the link step. Make has
some complex features, but using it for simple things is pretty easy.

Running make
Go to your project directory and run make right from the shell with no arguments, or in
emacs (below) [esc]-x compile will do basically the same thing. In any case, make
looks in the current directory for a file called Makefile or makefile for its build
instructions. If there is a problem building one of the targets, the error messages are
written to standard error or the emacs compilation buffer.

Makefiles
A makefile consists of a series of variable definitions and dependency rules. A variable in
a makefile is a name defined to represent some string of text. This works much like
macro replacement in the C pre-processor. Variables are most often used to represent a
list of directories to search, options for the compiler, and names of programs to run.
Variables are not pre-declared, you just set them with '='. For example, the line :
CC = gcc

will create a variable named CC, and set its value to be gcc. The name of the variable is
case sensitive, and traditionally make variable names are in all upper case letters.
While it is possible to make up your own variable names, there are a few names that are
considered standard, and using them along with the default rules makes writing a
makefile much easier. The most important variables are: CC, CFLAGS, and LDFLAGS.
CC The name of the C compiler, this will default to cc or gcc in most
versions of make.

CFLAGS A list of options to pass on to the C compiler for all of your source
files. This is commonly used to set the include path to include non-
standard directories (-I) or build debugging versions (-g).

LDFLAGS A list of options to pass on to the linker. This is most commonly


used to include application specific library files (-l) and set the
library search path (-L).

To refer to the value of a variable, put a dollar sign ($) followed by the name in
parenthesis or curly braces...
CFLAGS = -g -I/usr/class/cs107/include
$(CC) $(CFLAGS) -c binky.c

The first line sets the value of the variable CFLAGS to turn on debugging information and
add the directory /usr/class/cs107/include to the include file search path. The
second line uses CC variable to get the name of the compiler and the CFLAGS variable
6

to get the options for the compiler. A variable that has not been given a value has the
empty-string value.
The second major component of a makefile is the dependency/build rule. A rule tells how
to make a target based on changes to a list of certain files. The ordering of the rules does
not make any difference, except that the first rule is considered to be the default rule --
the rule that will be invoked when make is called without arguments (the most common
way).
A rule generally consists of two lines: a dependency line followed by a command line.
Here is an example rule :
binky.o : binky.c binky.h akbar.h
tab$(CC) $(CFLAGS) -c binky.c

This dependency line says that the object file binky.o must be rebuilt whenever any of
binky.c, binky.h, or akbar.h change. The target binky.o is said to depend on
these three files. Basically, an object file depends on its source file and any non-system
files that it includes. The programmer is responsible for expressing the dependencies
between the source files in the makefile. In the above example, apparently the source
code in binky.c #includes both binky.h and akbar.h-- if either of those two .h
files change, then binky.c must be re-compiled. (The make depend facility tries to
automate the authoring of the makefile, but it's beyond the scope of this document.)
The command line lists the commands that build binky.o -- invoking the C compiler
with whatever compiler options have been previously set (actually there can be multiple
command lines). Essentially, the dependency line is a trigger which says when to do
something. The command line specifies what to do.
The command lines must be indented with a tab characte -- just using spaces will not
work, even though the spaces will sortof look right in your editor. (This design is a result
of a famous moment in the early days of make when they realized that the tab format was
a terrible design, but they decided to keep it to remain backward compatible with their
user base -- on the order of 10 users at the time. There's a reason the word "backward" is
in the phrase "backward compatible". Best to not think about it.)
Because of the tab vs. space problem, make sure you are not using an editor or tool which
might substitute space characters for an actual tab. This can be a problem when using
copy/paste from some terminal programs. To check whether you have a tab character on
that line, move to the beginning of that line and try to move one character to the right. If
the cursor skips 8 positions to the right, you have a tab. If it moves space by space, then
you need to delete the spaces and retype a tab character.
For standard compilations, the command line can be omitted, and make will use a default
build rule for the source file based on its file extension, .c for C files, .f for Fortran files,
and so on. The default build rule for C files looks like...
$(CC) $(CFLAGS) -c source-file.c

It's very common to rely on the above default build rule -- most adjustments can be made
by changing the CFLAGS variable. Below is a simple but typical looking makefile. It
compiles the C source contained in the files main.c, binky.c, binky.h, akbar.c,
akbar.h, and defs.h. These files will produce intermediate files main.o,
binky.o, and akbar.o. Those files will be linked together to produce the executable
file program. Blank lines are ignored in a makefile, and the comment character is '#'.
7

## A simple makefile

CC = gcc
CFLAGS = -g -I/usr/class/cs107/include
LDFLAGS = -L/usr/class/cs107/lib -lgraph

PROG = program
HDRS = binky.h akbar.h defs.h
SRCS = main.c binky.c akbar.c

## This incantation says that the object files


## have the same name as the .c files, but with .o
OBJS = $(SRCS:.c=.o)

## This is the first rule (the default)


## Build the program from the three .o's
$(PROG) : $(OBJS)
tab$(CC) $(LDFLAGS) $(OBJS) -o $(PROG)

## Rules for the source files -- these do not have


## second build rule lines, so they will use the
## default build rule to compile X.c to make X.o
main.o : main.c binky.h akbar.h defs.h

binky.o : binky.c binky.h

akbar.o : akbar.c akbar.h defs.h

## Remove all the compilation and debugging files


clean :
tabrm -f core $(PROG) $(OBJS)

## Build tags for these sources


TAGS : $(SRCS) $(HDRS)
tabetags -t $(SRCS) $(HDRS)

The first (default) target builds the program from the three .o's. The next three targets
such as "main.o : main.c binky.h akbar.h defs.h" identify the .o's that
need to be built and which source files they depend on. These rules identify what needs to
be built, but they omit the command line. Therefore they will use the default rule which
knows how to build one .o from one .c with the same name. Finally, make
automatically knows that a X.o always depends on its source X.c, so X.c can be
omitted from the rule. So the first rule could b ewritten without main.c --
"main.o : binky.h akbar.h defs.h".
The later targets, clean and TAGS, perform other convenient operations. The clean
target is used to remove all of the object files, the executable, and a core file if you've
been debugging, so that you can perform the build process from scratch . You can make
clean if you want to recover space by removing all the compilation and debugging
output files. You also may need to make clean if you move to a system with a
different architecture from where your object libraries were originally compiled, and so
8

you need to recompile from scratch. The TAGS rule creates a tag file that most Unix
editors can use to search for symbol definitions.

Compiling in Emacs
Emacs has built-in support for the compile process. To compile your code from emacs,
type M-x compile. You will be prompted for a compile command. If you have a
makefile, just type make and hit return. The makefile will be read and the appropriate
commands executed. The emacs buffer will split at this point, and compile errors will be
brought up in the newly created buffer. In order to go to the line where a compile error
occurred, place the cursor on the line which contains the error message and hit ^c-^c.
This will jump the cursor to the line in your code where the error occurred (“cc” is the
historical name for the C compiler).

Section 3 — gdb
You may run into a bug or two in your programs. There are many techniques for finding
bugs, but a good debugger can make the job a lot easier. In most programs of any
significant size, it is not possible to track down all of the bugs in a program just by staring
at the source — you need to see clues in the runtime behavior of the program to find the
bug. It's worth investing time to learn to use debuggers well.

GDB
We recommend the GNU debugger gdb, since it basically stomps on dbx in every
possible area and works nicely with the gcc compiler. Other nice debugging
environments include ups and CodeCenter, but these are not as universally available as
gdb, and in the case of CodeCenter not as cheaply. While gdb does not have a flashy
graphical interface as do the others, it is a powerful tool that provides the knowledgeable
programmer with all of the information they could possibly want and then some.
This section does not come anywhere close to describing all of the features of gdb, but
will hit on the high points. There is on-line help for gdb which can be seen by using the
help command from within gdb. If you want more information try xinfo if you are
logged onto the console of a machine with an X display or use the info-browser mode
from within emacs.

Starting the debugger


As with make there are two different ways of invoking gdb. To start the debugger from
the shell just type...

gdb program

where program is the name of the target executable that you want to debug. If you do
not specify a target then gdb will start without a target and you will need to specify one
later before you can do anything useful.

As an alternative, from within emacs you can use the command [Esc]-x gdb which
will then prompt you for the name of the executable file. You cannot start an inferior gdb
session from within emacs without specifying a target. The emacs window will then split
between the gdb buffer and a separate buffer showing the current source line.

Running the debugger


Once started, the debugger will load your application and its symbol table (which
contains useful information about variable names, source code files, etc.). This symbol
9

table is the map produced by the -g compiler option that the debugger reads as it is
running your program.

The debugger is an interactive program. Once started, it will prompt you for commands.
The most common commands in the debugger are: setting breakpoints, single stepping,
continuing after a breakpoint, and examining the values of variables.

Running the Program


run Reset the program, run (or rerun) from the
beginning. You can supply command-line
arguments the same way you can supply command-
line arguments to your executable from the shell.

step Run next line of source and return to debugger. If a


subroutine call is encountered, follow into that
subroutine.

step count Run count lines of source.

next Similar to step, but doesn't step into subroutines.

finish Run until the current function/method returns.

return Make selected stack frame return to its caller.

jump address Continue program at specified line or address.

When a target executable is first selected (usually on startup) the current source file is set
to the file with the main function in it, and the current source line is the first executable
line of the this function.

As you run your program, it will always be executing some line of code in some source
file. When you pause the program (when the flow of control hits a “breakpoint” of by
typing Control-C to interrupt), the “current target file” is the source code file in which the
program was executing when you paused it. Likewise, the “current source line” is the line
of code in which the program was executing when you paused it.

Breakpoints
You can use breakpoints to pause your program at a certain point. Each breakpoint is
assigned an identifying number when you create it, and so that you can later refer to that
breakpoint should you need to manipulate it.

A breakpoint is set by using the command break specifying the location of the code
where you want the program to be stopped. This location can be specified in several
ways, such as with the file name and either a line number or a function name within that
file (a line needs to be a line of actual source code — comments and whitespace don't
count). If the file name is not specified the file is assumed to be the current target file, and
if no arguments are passed to break then the current source line will be the breakpoint.
gdb provides the following commands to manipulate breakpoints:

info break Prints a list of all breakpoints with numbers and


status.
10

break function Place a breakpoint at start of the specified function


break linenumber Prints a breakpoint at line, relative to current source
file.
break filename:linenumber Place a breakpoint at the specified line within the
specified source file.

You can also specify an if clause to create a conditional breakpoint:

break fn if expression Stop at the breakpoint, only if expression evaluates


to true. Expression is any valid C expression,
evaluated within current stack frame when hitting
the breakpoint.

disable breaknum
enable breaknum Disable/enable breakpoint identified by breaknum

delete breaknum Delete the breakpoint identified by breaknum

commands breaknum Specify commands to be executed when breaknum


is reached. The commands can be any list of C
statements or gdb commands. This can be useful to
fix code on-the-fly in the debugger without re-
compiling (Woo Hoo!).

cont Continue a program that has been stopped.

For example, the commands...

break binky.c:120
break DoGoofyStuff

set a breakpoint on line 120 of the file binky.c and another on the first line of the function
DoGoofyStuff. When control reaches these locations, the program will stop and give
you a chance to look around in the debugger.

Gdb (and most other debuggers) provides mechanisms to determine the current state of
the program and how it got there. The things that we are usually interested in are (a)
where are we in the program? and (b) what are the values of the variables around us?

Examining the stack


To answer question (a) use the backtrace command to examine the run-time stack.
The run-time stack is like a trail of breadcrumbs in a program; each time a function call is
made, a crumb is dropped (an run-time stack frame is pushed). When a return from a
function occurs, the corresponding stack frame is popped and discarded. These stack
frames contain valuable information about the sequence of callers which brought us to the
current line, and what the parameters were for each call.

Gdb assigns numbers to stack frames counting from zero for the innermost (currently
executing) frame. At any time gdb identifies one frame as the “selected” frame. Variable
lookups are done with respect to the selected frame. When the program being debugged
stops (at a breakpoint), gdb selects the innermost frame. The commands below can be
used to select other frames by number or address.
11

backtrace Show stack frames, useful to find the calling


sequence that produced a crash.

frame framenumber Start examining the frame with framenumber. This


does not change the execution context, but allows
to examine variables for a different frame.

down Select and print stack frame called by this one. (The
metaphor here is that the stack grows down with
each function call.)

up Select and print stack frame that called this one.

info args Show the argument variables of current stack


frame.

info locals Show the local variables of current stack frame.

Examining source files


Another way to find our current location in the program and other useful information is to
examine the relevant source files. gdb provides the following commands:
list linenum Print ten lines centered around linenum in current
source file.

list function Print ten lines centered around beginning of


function (or method).

list Print ten more lines.

The list command will show the source lines with the current source line centered in
the range. (Using gdb from within emacs makes these command obsolete since it does
all of the current source stuff for you.)

Examining data
To answeer the question (b) “what are the values of the variables around us?” use the
following commands...

print expression Print value of expression. Expression is any valid C


expression, can include function calls and
arithmetic expressions, all evaluated within current
stack frame.

set variable = expression Assign value of variable to expression. You can


set any variable in the current scope. Variables
which begin with $ can be used as temporary
variables local to gdb.

display expression Print value of expression each time the program


stops. This can be useful to watch the change in a
variable as you step through code.

undisplay Cancels previous display requests.


12

In gdb, there are two different ways of displaying the value of a variable: a snapshot of
the variable’s current value and a persistent display for the entire life of the variable. The
print command will print the current value of a variable, and the display command
will make the debugger print the variable's value on every step for as long as the variable
exists. The desired variable is specified by using C syntax. For example...

print x.y[3]

will print the value of the fourth element of the array field named y of a structure variable
named x. The variables that are accessible are those of the currently selected function's
activation frame, plus all those whose scope is global or static to the current target file.
Both the print and display functions can be used to evaluate arbitrarily complicated
expressions, even those containing, function calls, but be warned that if a function has
side-effects a variety of unpleasant and unexpected situations can arise.

Shortcuts
Finally, there are some things that make using gdb a bit simpler. All of the commands
have short-cuts so that you don’t have to type the whole command name every time you
want to do something simple. A command short-cut is specified by typing just enough of
the command name so that it unambiguously refers to a command, or for the special
commands break, delete, run, continue, step, next and print you need only
use the first letter. Additionally, the last command you entered can be repeated by just
hitting the return key again. This is really useful for single stepping for a range while
watching variables change.

Miscellaneous
editmode mode Set editmode for gdb command line. Supported
values for mode are emacs, vi, dumb.

shell command Execute the rest of the line as a shell command.

history Print command history.

Debugging Strategies
Some people avoid using debuggers because they don't want to learn another tool. This is
a mistake. Invest the time to learn to use a debugger and all its features — it will make
you much more productive in tracking down problems.

Sometimes bugs result in program crashes (a.k.a. “core dumps”, “register dumps”, etc.)
that bring your program to a halt with a message like “Segmentation Violation” or the
like. If your program has such a crash, the debugger will intercept the signal sent by the
processor that indicates the error it found, and allow you to examine the state program.
Thus with almost no extra effort, the debugger can show you the state of the program at
the moment of the crash.

Often, a bug does not crash explicitly, but instead produces symptoms of internal
problems. In such a case, one technique is to put a breakpoint where the program is
misbehaving, and then look up the call stack to get some insight about the data and
control flow path that led to the bad state. Another technique is to set a breakpoint at
some point before the problems start and step forward towards the problems, examining
the state of the program along the way.
13

Section 4 — emacs
The following is a quick introduction to the “emacs” text editor which is a free program
produced by GNU (www.gnu.org). It's a fine editor, and it happens to integrate with
many other Unix tools nicely. There's a fabulous history of various editor adherents
having long and entertaining arguments about why their editor is best, but we're just
going to avoid that subject entirely.
To start editing a new or existing file using emacs, simply type the following to the UNIX
prompt...
emacs filename
where filename is the file to be edited. The X-Windows version of emacs is called
xemacs, and if you're using it... well just look in the menus. The commands are all the
same, but you don't have to remember the funny key-combinations.
All the fancy editing commands, such as find-and-replace, are invoked through typing
special key sequences. Two important key sequences to remember are: ^x (holding down
the “ctrl” key while typing “x”) and [esc]-x (simply pressing the “esc” key followed
by typing “x”), both of which are used to start many command sequences. Note that for
historical reasons in most user manuals for emacs, the “esc” key is actually referred to as
the “Meta” or “M-” key. Therefore, you may see the [esc]-x written as equivalently
as M-x.
To save the file being edited the sequence is ^x^s. To exit (and be prompted to save)
emacs, the sequence is ^x^c. To open another file within emacs, the sequence is ^x^f.
This sequence can be used to open an existing file as well as a new file. If you have
multiple files open, emacs stores them in different “buffers”. To switch from one buffer
to another (very handy when you are editing a .c source file and need to refer to the
prototypes and definitions in the .h header file), you use the key sequence ^x-b (note
the “b” is typed plain). You can then enter the name of the file to switch to the
corresponding buffer (a default name is provided for fast switching). The arrow keys
usually work as the cursor movement keys, but there are other nagivation key
combinations listed below.

Running emacs
emacs <filename> Run emacs (on a particular file). Make sure you don't already have
an emacs job running which you can just revive with fg. Adding a
'&' after the above command will run emacs in the background,
freeing up your shell)
^z Suspend emacs— revive with % command above, or the fg
command
^x^c Quit emacs
^x^f Load a new file into emacs
^x^v Load a new file into emacs and unload previous file
^x^s Save the file
^x-k Kill a buffer

Moving About
^f Move forward one character
^b Move backward one character
^n Move to next line
^p Move to previous line
14

^a Move to beginning of line


^e Move to end of line
^v Scroll down a page
M-v Scroll up a page

M-< Move to beginning of document


^x-[ Move to beginning of page
M-> Move to end of document
^x-] Move to end of page

^l Redraw screen centered at line under the cursor


^x-o Move to other screen
^x-b Switch to another buffer

Searching
^s Search for a string
^r Search for a string backwards from the cursor (quit both of these
with ^f)
M-% Search-and-replace

Deleting
^d Deletes letter under the cursor
^k Kill from the cursor all the way to the end of the line
^y Yanks back all the last kills. Using the ^k ^y combination you can
get a primitive cut-paste effect to move text around

Regions
emacs defines a region as the space between the mark and the point. A mark is set with
^-space (control-spacebar). The point is at the cursor position.

M-w Copy the region

^w Delete the region. Using ^y will also yank back the last region
killed or copied — this is the way to get a cut/copy/paste effect with
regions.

Screen Splitting
^x-2 Split screen horizontally
^x-3 Split screen vertically
^x-1 Make active window the only screen
^x-0 Make other window the only screen

Miscellaneous
M-$ Check spelling of word at the cursor
^g In most contexts, cancel, stop, go back to normal command
M-x goto-line num Goes to the given line number
^x-u Undo
M-x shell Start a shell within emacs
M-q Re-flow the current line-breaks to make a single paragraph of text
15

Compiling
M-x compile Compile code in active window. Easiest if you have a makefile set
up.
^c ^c Do this with the cursor in the compile window, scrolls to the next
compiler error. Cool!

Getting Help
^h emacs help
^h t Run the emacs tutorial

emacs does command completion for you. Typing M-x space will give you a list of emacs
commands. There is also a man page on emacs. Type man emacs in a shell.

Printing Your Source Files


There's a really neat way to print out hardcopies of your source files. Use a command
called “enscript”. Commonly, it's used at the Unix command line as follows:

enscript -2GrPsweet5 binky.c lassie.c *.h

This example shoes printing the two source files binky.c and lassie.c, as well as
all of the header files to printer sweet5. You can change these parameters to fit your
needs.

Section 5 — Unix Shell


This section summarizes many of the commands used in the Unix shell.

Directory Commands
cd directory Change directory. If directory is not specified, goes to home
directory.
pwd Show current directory (print working directory)
ls Show the contents of a directory. ls -a will also show files whose
name begins with a dot. ls -l shows lots of miscellaneous info about
each file. ls -t sorts the most recently changed to the top.
rm file Delete a file
mv old new Rename a file from old to new (also works for moving things
between directories). If there was already a file named new, it gets
overwritten.
cp old new Creates a file named new containing the same thing as old. If there
was already a file named new, it is overwritten.
mkdir name Create a directory
rmdir name Delete a directory. The directory must be empty.

Shorthand Notations & Wildcards


. Current directory
.. Parent directory
~ Your home directory
~/cs107 The cs107 directory in your home directory
~user Home directory of user
* Any number of characters (not '.') Ex: *.c is all files ending in '.c'
? Any single character (not '.')
16

Miscellaneous Commands
cat file Print the contents of file to standard output
more file Same as cat, but only a page at a time (useful for displaying)
less file Same as more, but with navigability (less is more)

w Find out who is on the system and what they are doing
ps List all your processes (use the process id's in kill below)
jobs Show jobs that have been suspended (use with fg)

program& Runs program in the background


ctrl-z Suspend the current program
% Continue last job suspended, or use fg (foreground)
%number Continue a particular job (the number comes from the jobs list)

kill process-id Kill a process


kill -9 process Kill a process with extreme prejudice

grep exp files Search for an expression in a set of files


wc file Count words, lines, and characters in a file
script Start saving everything that happens in a file. type exit when done

lpr file Print file to the default printer


lpr -Pinky file Print file to the printer named inky

diff file1 file2 Show the differences between two files


telnet hostname Log on to another machine

source file Execute the lines in the given file as if they were typed to the shell

Getting Help
man subject Read the manual entry on a particular subject
man -k keyword Show all the manual pages for a particular keyword

History
history Show the most recent commands executed
!! Re-execute the last command (or type up-arrow with modern shells)
!number Re-execute a particular command by number
!string Re-execute the last command beginning with string
^wrong^right^ Re-execute the last command, substituting right for wrong
ctrl-P Scroll backwards through previous commands

Pipes
a > b Redirect a's standard output to overwrite file b
a >> b Redirect a's standard output to append to the file b
a >& b Redirect a's error output to overwrite file b
a < b Redirect a's standard input to read from the file b
a | b Redirect a's standard output to b's standard input

Vous aimerez peut-être aussi