Sie sind auf Seite 1von 50

A Short C Course

CO120.3
Lecture 5
Dynamic Data Structures and Function Pointers in C

Dr Maria Valera(m.valera-espina@doc.ic.ac.uk)
Imperial College London

Summer 2015

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

1 / 47

Outline

Dynamic Data Structures.


Double Link Lists.
Function Pointers.
A Binary Tree Data Type.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

2 / 47

Dynamic Data Structures

The C standard library doesnt contain any form of dynamic data


structures (sets, hash tables, etc.)
We can use dynamic memory to create our own data structures that
grow and shrink as required during execution.
In this set of slides, well look at an implementation of a doubly linked
list and a binary tree.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

3 / 47

Why are there no containers in the C standard library?

Data structures in C tend to be extremely specialised to application. Its


not easy to write a one size fits all implementation.
Things to consider:
How much of the implementation does the container API expose?
Are elements held by value or via pointers?
Is the container responsible for performing its own dynamic memory
allocation?
How are error conditions handled and signalled?

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

4 / 47

Implementation Choices

Lets say we want a singly linked list data structure. Well consider how we
might store the struct listed below:
struct name {
char first[25];
char last[25];
};

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

5 / 47

Element storage by value


How about a list of nodes containing each value?
struct list_elem {
struct list_elem *next;
struct name value;
};
...

next = . . .

next = . . .

value =
first = John

value =
first = Jane

last = Smith
struct name

last = Smith
struct name

struct list elem

struct list elem

...

We can only store elements of type struct name in this list.


FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

6 / 47

Element storage by pointer


How about a list of nodes containing pointers?
struct list_elem {
struct list_elem *next;
struct name *value;
};
...

next = . . .

next = . . .

value = . . .
struct list elem

value = . . .
struct list elem

first = John

first = Jane

last = Smith
struct name

last = Smith
struct name

...

We can now store elements of arbitrary types. However, memory


management is more complex for any values we wish to reference from the
list.
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

7 / 47

Intrusive Data Structures


Put the pointers inside the struct to be stored?
struct name {
char first[25];
char last[25];
struct list_elem elem;
};

struct list_elem {
struct list_elem *next;
};

...

first = John

first = Jane

last = Smith

last = Smith

next = . . .

next = . . .

struct list elem

struct list elem

struct name

struct name

...

Our data structure now has to do no dynamic memory allocation.


FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

8 / 47

A note on struct dependencies


Well be making extensive use of structs in this lecture. If one struct is
nested in another, it is necessary for it to be declared before the struct
that contains it.
If a struct only contains a pointer to another struct, it is possible to use a
forward declaration since the compiler doesnt need to know the structs
size or members.

Forward declaration of struct A


Cannot be reordered
struct A {
};
struct B {
struct A a;
};

FR & MV (Imperial College London)

struct A;
struct B {
struct A *a;
};
struct A {
};
A Short C Course CO120.3

Summer 2015

9 / 47

Dynamic Data Structures


We will look at the implementation of a doubly linked list that holds
integers.
Each value in the list will be held in a different heap allocated list
element and have pointers to the previous and following elements.
We will have two struct types: struct list and struct
list elem.
struct list will be the handle type held by the programmer.
struct list elem will be used to hold the elements of the list.
struct list {
struct list_elem *first;
struct list_elem *last;
};

FR & MV (Imperial College London)

struct list_elem {
int value;
struct list_elem *prev;
struct list_elem *next;
};

A Short C Course CO120.3

Summer 2015

10 / 47

Doubly Linked Lists


Using these structures, we could draw the resulting data structure when
holding the integers 9, 4 and 3 as follows:

first = . . .
last = . . .
struct list
value = 9
prev = NULL

value = 4
prev = . . .

value = 3
prev = . . .

next = . . .
struct list elem

next = . . .
struct list elem

next = NULL
struct list elem

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

11 / 47

Design Considerations
This design is workable, but has a couple of issues:
Any function that inserts an element will have to handle special cases
at the beginning and end of the list.
Any function that removes an element will have to handle special
cases at the beginning and end of the list.
The other thing wed like to be able to do is support iterators. Ideally,
these should allow us to:
Iterate forwards and backwards over the list.
Insert elements.
Delete elements.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

12 / 47

Iterators

You are probably all familiar with Java iterators. Successive calls to the
next() method return successive items from the collection. We will not
be basing our iterators on this model.
Instead, our iterator functionality will be based on C pointers:
They will have separate functions to advance their location (like ++)
and to obtain their value (like *).
To define a range, a start and end iterator will be required iterators
will not know about ranges.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

13 / 47

Pointers as iterators
Here, we iterate over the elements of an array using iter:
double arr[100];
double *begin = arr;
double *end = arr+100;
for(double* iter = begin; iter != end; ++iter) {
*iter = 0.0;
}
We want to iterate over our lists in a similar manner.
Note that dereferencing end is invalid. end points to an element after the
end of the array arr.
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

14 / 47

Our Revised Design


To avoid special cases, we place an additional element at the start
and the end of the linked items.
Our iterator should only ever point to elem0, elem1 or footer.
header = . . .
footer = . . .
struct list
value =
prev = NULL

value = 4
prev = . . .

value = 7
prev = . . .

value =
prev = . . .

next = . . .
struct list elem
header

next = . . .
struct list elem
elem0

next = . . .
struct list elem
elem1

next = NULL
struct list elem
footer

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

15 / 47

Preliminaries

We define our two struct types:


struct list {
struct list_elem *header;
struct list_elem *footer;
};

struct list_elem {
int value;
struct list_elem *prev;
struct list_elem *next;
};

These are the same as before except that we have renamed the pointers in
struct list to header and footer from first and last to reflect
their new roles.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

16 / 47

List memory allocation


We also define the routines that will allocate and free our list elements:
#include <stdio.h>
#include <stdlib.h>
struct list_elem *list_alloc_elem(void) {
struct list_elem *elem = malloc(sizeof(struct list_elem));
if (elem == NULL) {
perror("list_alloc_elem");
exit(EXIT_FAILURE);
}
return elem;
}
void list_free_elem(struct list_elem *elem) {
free(elem);
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

17 / 47

Creating a new list


We will define a routine that takes a struct list* and initialises it:
void list_init(struct list *l) {
l->header = list_alloc_elem();
l->footer = list_alloc_elem();
l->header->prev = NULL;
l->footer->next = NULL;
l->header->next = l->footer;
l->footer->prev = l->header;
}

We can then initialise a new list as follows:


struct list example;
list_init(&example);

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

18 / 47

The Empty List


After construction, our list looks like the following:
header = . . .
footer = . . .
struct list
value =
prev = NULL

value =
prev = . . .

next = . . .
struct list elem
header

next = NULL
struct list elem
footer

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

19 / 47

The begin and end iterators


We will define a typedef for our iterator:
typedef struct list_elem* list_iter;

We can now define the methods that return the iterators to the beginning
and end of our linked list:
list_iter list_begin(struct list *l) {
return l->header->next;
}
list_iter list_end(struct list *l) {
return l->footer;
}

Note that for the empty list, both will return footer (the invalid element).
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

20 / 47

Inserting an item
We will define a method for inserting a value before the supplied iterator.
Inserting before the invalid element footer will place an element at the
end of the list.
void list_insert(struct list *l, list_iter iter, int value) {
struct list_elem *new_elem = list_alloc_elem();
new_elem->value = value;
new_elem->prev = iter->prev;
new_elem->next = iter;
iter->prev->next = new_elem;
iter->prev = new_elem;
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

21 / 47

Incrementing and Dereferencing


We increment an iterator by taking the next pointer of the current
element:
list_iter list_iter_next(list_iter iter) {
return iter->next;
}

To dereference, we just look at the value member of the element:


int list_iter_value(list_iter iter) {
assert(list_is_internal(iter));
return iter->value;
}

By checking prev and next arent NULL, we can check that we arent
dereferencing header or footer:
int list_is_internal(list_iter iter) {
return iter->prev != NULL && iter->next != NULL;
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

22 / 47

Adding new elements

With our iterators defined, we can easily define methods that insert
elements at the start or end of our list:
void list_insert_front(struct list *l, int value) {
list_insert(l, list_begin(l), value);
}
void list_insert_back(struct list *l, int value) {
list_insert(l, list_end(l), value);
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

23 / 47

Destroying a list
Once weve finished with a list, we should reclaim any memory it is using.
We define a method to destroy a list that simply calls list free elem on
all list elements including header and footer:
void list_destroy(struct list *l) {
struct list_elem *elem = l->header;
while (elem != NULL) {
struct list_elem *next = elem->next;
list_free_elem(elem);
elem = next;
}
}

Note that we record elem->next since calling free makes it invalid to


access *elem.
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

24 / 47

An example
int main(void) {
struct list l;
list_init(&l);
list_insert_front(&l, 1);
list_insert_front(&l, 2);
list_insert_back(&l, 1);
list_insert_back(&l, 2);
for(list_iter iter = list_begin(&l);
iter != list_end(&l);
iter = list_iter_next(iter)) {
printf("%i\n", list_iter_value(iter));
}
list_destroy(&l);
return 0;
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

25 / 47

Sorted Lists
Using our iterator functionality, we can write a method that constructs
sorted lists:
void list_insert_ascending(struct list *l, int value) {
list_iter iter = list_begin(l);
/* We *must* check we havent hit the end of the list first */
while(iter != list_end(l) && list_iter_value(iter) < value) {
iter = list_iter_next(iter);
}
list_insert(l, iter, value);
}

We iterate though the list until we reach a value larger than the one we
are inserting, or hit the end of the list.
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

26 / 47

Function Pointers
At this point, you should be pretty familiar with pointers to values.
However, C also supports pointers to functions. Heres how we can take a
pointer to a sum function:
static int sum(int a, int b) {
return a + b;
}
int main(void) {
int (*sum_ptr)(int, int);
sum_ptr = &sum;
return 0;
}

Weve written the declaration of sum ptr the same way wed have written
a function declaration except we replaced the function name with
(*sum ptr).
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

27 / 47

Function Pointers
Its possible to invoke a function pointer in exactly the same way as
normal function.
#include <stdio.h>
static int sum(int a, int b) {
return a + b;
}
int main(void) {
int (*sum_ptr)(int, int) = &sum;
printf("The sum of 39 and 73 is %i.\n", sum_ptr(39, 73));
return 0;
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

28 / 47

Function Pointers
We can pass them to other functions as well.
#include <stdio.h>
static int sum(int a, int b) { return a + b; }
static int mul(int a, int b) { return a * b; }
static void print_result(int (*func)(int, int), int a, int b) {
printf("func(%i, %i) = %i\n", a, b, func(a, b));
}
int main(void) {
int a = 42;
int b = 37;
print_result(&sum, a, b);
print_result(&mul, a, b);
return 0;
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

29 / 47

A Binary Tree Data Type


As with our linked list data type, we will use two struct types. One is a
handle to our data structure, the other represents internal nodes.
typedef int (*bst_compare_t)(void *val1, void *val2);
struct bst_elem;
struct bst {
bst_compare_t compare;
struct bst_elem *tree;
};
struct bst_elem {
struct bst_elem *left;
struct bst_elem *right;
void *value;
};
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

30 / 47

A Binary Tree Data Type


As before, well define functions for allocating and deallocating memory.
#include <stdio.h>
#include <stdlib.h>
struct bst_elem *bst_alloc_elem(void) {
struct bst_elem *elem = malloc(sizeof(struct bst_elem));
if (elem == NULL) {
perror("bst_alloc_elem");
exit(EXIT_FAILURE);
}
return elem;
}
void bst_free_elem(struct bst_elem *elem) {
free(elem);
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

31 / 47

Initialising the Binary Tree


Initialisation is much simpler this time:
void bst_init(struct bst *handle, bst_compare_t compare) {
handle->compare = compare;
handle->tree = NULL;
}

Our empty binary tree looks like this:


compare = . . .
tree = NULL
struct bst

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

32 / 47

Inserting Elements
Our insert function will defer to a recursive implementation that returns
the updated tree structure.
void bst_insert(struct bst *handle, void *value) {
handle->tree =
bst_insert_elem(handle->tree, handle->compare, value);
}

Well consider growing a tree of strings, using the following comparison


function:
#include <string.h>
int string_compare(void *val1, void *val2) {
return strcmp((const char*) val1, (const char*) val2);
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

33 / 47

Inserting Elements
struct bst tree;
bst_init(&tree, &string_compare);

compare = . . .
tree = NULL
struct bst

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

34 / 47

Inserting Elements
char s1[] = "Bob";
bst_insert(&tree, s1);

compare = . . .

right = NULL

tree = . . .
struct bst

FR & MV (Imperial College London)

value = . . .

Bob

left = NULL
struct bst elem

char[]

A Short C Course CO120.3

Summer 2015

34 / 47

Inserting Elements
char s2[] = "Alice";
bst_insert(&tree, s2);

compare = . . .

right = NULL

tree = . . .
struct bst

value = . . .

Bob

left = . . .
struct bst elem

char[]

right = NULL

FR & MV (Imperial College London)

value = . . .

Alice

left = NULL
struct bst elem

char[]

A Short C Course CO120.3

Summer 2015

34 / 47

Inserting Elements
char s3[] = "Eve";
bst_insert(&tree, s3);
right = NULL

compare = . . .
tree = . . .
struct bst

value = . . .

Eve

left = NULL
struct bst elem

char[]

right = . . .
value = . . .

Bob

left = . . .
struct bst elem

char[]

right = NULL

FR & MV (Imperial College London)

value = . . .

Alice

left = NULL
struct bst elem

char[]

A Short C Course CO120.3

Summer 2015

34 / 47

Implementation of bst insert elem


struct bst_elem *bst_insert_elem(struct bst_elem *const elem,
bst_compare_t compare, void *value) {
if (elem == NULL) {
struct bst_elem *new_elem = bst_alloc_elem();
new_elem->left = new_elem->right = NULL;
new_elem->value = value;
return new_elem;
} else {
const int comparison = compare(value, elem->value);
if (comparison < 0) {
elem->left = bst_insert_elem(elem->left, compare, value);
} else if (comparison > 0) {
elem->right = bst_insert_elem(elem->right, compare, value);
}
return elem;
}
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

35 / 47

Printing the Tree


Now that we have a sorted list of items, we need a way to print them.
Lets define a function that applies a given function to each element.
void bst_for_each(struct bst *handle, void (*func)(void*)) {
bst_for_each_elem(handle->tree, func);
}
void bst_for_each_elem(struct bst_elem *elem,
void (*func)(void*)) {
if (elem == NULL)
return;
bst_for_each_elem(elem->left, func);
func(elem->value);
bst_for_each_elem(elem->right, func);
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

36 / 47

The Print Function

All our print function needs to do is cast the element value back to a
string, then use printf() to display it.
#include <stdio.h>
void bst_print_string(void *value) {
const char *str = (const char*) value;
printf("%s\n", str);
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

37 / 47

The Destroy Function


Our destroy function merely recurses the tree, making sure to destroy
parent nodes after their children.
void bst_destroy(struct bst *handle) {
bst_destroy_elem(handle->tree);
}
void bst_destroy_elem(struct bst_elem *elem) {
if (elem == NULL)
return;
bst_destroy_elem(elem->left);
bst_destroy_elem(elem->right);
bst_free_elem(elem);
}

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

38 / 47

Binary Tree Example


int main(void) {
char s1[] = "Bob";
char s2[] = "Alice";
char s3[] = "Eve";
struct bst tree;
bst_init(&tree, &string_compare);
bst_insert(&tree, s1);
bst_insert(&tree, s2);
bst_insert(&tree, s3);
bst_for_each(&tree, &bst_print_string);
bst_destroy(&tree);
return 0;
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

39 / 47

Freeing values in the tree as well

Its likely that the values we place in our binary tree will have been
allocated on the heap. In that case, we might want to call free() on
every value in our tree before destroying it.
Fortunately, free() already has the right function signature for us to be
able to do this:
bst_for_each(&tree, &free); /* Free each heap-allocated string */
bst_destroy(&tree);
/* Deallocate tree itself */

This would be useful, for example, if all the strings we inserted had been
copied to the heap after being read from a text file.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

40 / 47

What about state?


Instead of printing our strings to a file, suppose we want to write them to
some file we opened? We cant do this with our current interface because
our callback method has no state.
We will solve this by defining a struct which will contain the state we
require:
struct print_state {
FILE *file;
};

Next, we will define an updated print method that will have access to both
the value to print and a struct print state.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

41 / 47

What about state?


Our updated print function now takes a parameter called context which
is a pointer to a struct print state. It accesses the file descriptor to
write to by dereferencing the context variable.
void bst_print_string_context(void *context, void *value) {
struct print_state *state = (struct print_state*) context;
const char *str = (const char*) value;
fprintf(state->file, "%s\n", str);
}

Since our tree implementation does not know anything about the type of
context, like value, it must also be passed as a void* and casted
appropriately.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

42 / 47

What about state?


Lastly, we define two new bst for each functions that accept the state
parameter (context), and a pointer to a function that takes both a
pointer to a state and a pointer to a value.
void bst_for_each_context(struct bst *handle,
void (*func)(void*, void*), void *context) {
bst_for_each_elem_context(handle->tree, func, context);
}
void bst_for_each_elem_context(struct bst_elem *elem,
void (*func)(void*, void*), void *context) {
if (elem == NULL) return;
bst_for_each_elem_context(elem->left, func, context);
func(context, elem->value);
bst_for_each_elem_context(elem->right, func, context);
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

43 / 47

Printing the tree to a file


We can now print a list of strings to a file like this:
int main(void) {
struct bst tree;
bst_init(&tree, &string_compare);
/* Populate tree with heap-allocated strings */
FILE *out = fopen("output.txt", "w");
struct print_state context;
context.file = out;
/* Set-up the context */
bst_for_each_context(&tree, &bst_print_string, &context);
fclose(out);
bst_for_each(&tree, &free); /* Free strings */
bst_destroy(&tree);
/* Free tree memory */
return 0;
}
FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

44 / 47

Taking it further
Theres no need to restrict ourselves to passing around just one function.
If we pass around a struct of function pointers, we can emulate an
interface in C.
Each function pointer in the struct needs to take a state parameter as a
void*. This emulates the member variables of a class in an object
oriented setting.
The struct of function pointers corresponds to the methods supported by
the interface being emulated.

A stream reading interface from the Vorbisfile library


typedef struct {
size_t (*read_func)
int
(*seek_func)
int
(*close_func)
long
(*tell_func)
} ov_callbacks;

(void
(void
(void
(void

FR & MV (Imperial College London)

*ptr, size_t size, size_t nmemb, void *datasource);


*datasource, ogg_int64_t offset, int whence);
*datasource);
*datasource);

A Short C Course CO120.3

Summer 2015

45 / 47

Summary

In this lecture:
Weve seen how to use dynamic memory allocation in C to implement
our own data structures.
Weve seen the syntax for the declaration and use of function pointers.
Weve seen how function pointers can be used to implement a
polymorphic data structure in C.
Weve seen how structs of function pointers can be used to emulate
an interface in C.
The problem of implementing the search and delete operations on the
tree are left as an exercise.

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

46 / 47

Libraries you should know


There are two general purpose libraries you should be aware of if you want
to write reasonably portable C that requires access to lower-level operating
system functionality or require more functionality than the standard library
provides:
APR The Apache Portable Runtime1 provides a consistent
interface to many OS-specific functions (e.g. threads,
networking, opening libraries at runtime) and other useful
tools (e.g. memory pools to simplify memory management
and hash tables).
GLib GLib2 is similar to APR, but has a stronger focus on
user-level applications. It contains functionality for character
conversion, regular expressions, multiple container types,
dynamically sized strings and more.
1
2

http://apr.apache.org/
http://developer.gnome.org/glib/stable/

FR & MV (Imperial College London)

A Short C Course CO120.3

Summer 2015

47 / 47

Das könnte Ihnen auch gefallen