0% found this document useful (0 votes)
12 views

Intro To C - Module 7

Uploaded by

Andrew Fu
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views

Intro To C - Module 7

Uploaded by

Andrew Fu
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

Introduction to C: Module 7

Weekly Reading: Beej Guide, Ch. 20.8-11, 24, 28

We are, as they say, over the hump—We've covered the core concepts of C, and you now know
enough to be a productive programmer. If you've followed along through Modules 1-6, you
probably know more than many professionals.

Still, the language has its dark corners, and knowing the capabilities of a language does not
mean one has mastered its use.

Resizable Arrays

Let's illustrate some principles of C engineering by building a resizable array type.

Python lists and Java ArrayLists are resizable—they are designed to let you insert or delete
elements at random. The abstractions obscure performance issues—resizings, data layout and
movement—but, for many applications, the builtin data structures are fast enough. As a C
programmer, though, you want total control.

Remember that a C "array" is a pointer to a reserved block of contiguous memory, and that it's
possible for this block to abut something else. Expanding the array requires (a) finding space for
a bigger one, (b) moving existing data to the new location, and (c) freeing the old version of the
array, to avoid memory leakage. Luckily, we have the standard library function realloc to take
care of all of these.

Let's get started with some requisite #includes as well as a struct type.

#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>

typedef struct res_array {


int* buf;
size_t size;
size_t capacity;
} res_array;

res_array* res_array_new() {
res_array* out = malloc(sizeof(res_array));
if (out) {
out->size = 0;
out->capacity = 4;
out->buf = malloc(out->capacity * sizeof(int));
if (!out->buf) {
free(out);
return NULL;
}
}
return out;
}

The res_array type uses r->size to indicate the number of valid elements in the array and
r->capacity to track the store of its backing buffer. If r->size is 13 and r->capacity is 16,
then r->buf[13] is an address owned by this data structure, but meaningless. However, if the
array needs to increment r->size to store a new element, no expansion is needed. The reason
we put a margin between the structure's size and underlying capacity is that resizing the array is
expensive—we'd usually rather tolerate some "waste" by resizing rarely than achieve 100%
memory utilization but have to resize it every time the array expands.

We program res_array_new defensively—if either allocation fails, we clean up and return NULL.
We would typically also want to print an error message—allocation failures are usually a sign of
something going wrong—but, for the sake of brevity, I elide that here.

Since we're creating objects on the heap, we need to be able to delete them:

void res_array_delete(res_array* r) {
if (r) free(r->buf);
free(r);
}

Note that if (r) is identical to if (r != NULL); pointers are implicitly truthy except when null.
We check it because, in the off chance that r is null, r->buf would dereference a null pointer,
which we are not allowed to do (undefined behavior.)

We'll create a "setter" and "getter" method that allow random access into the array. These
methods check for out-of-bounds access and return false in the error case; the getter does not
return the retrieved item, but places it into the address (that is, an int*) given as its last
argument—if a null pointer is supplied (also an error) it returns false.

bool res_array_get(res_array* r, int idx, int* rtn) {


if (!r) return false;
if (rtn && 0 <= idx && idx < r->size) {
*rtn = r->buf[idx];
return true;
} else return false;
}
bool res_array_set(res_array* r, int idx, int val) {
if (!r) return false;
if (0 <= idx && idx < r->size) {
r->buf[idx] = val;
return true;
} else return false;
}

For simplicity's sake, we only allow resizing at the end, via the following two methods, one to
expand (or "push") and one to contract (or "pop") it:

bool res_array_push(res_array* r, int elem) {


if (!r) return false;
if (r->size == r->capacity) {
int* new_buf = realloc(r->buf, 2 * r->capacity * sizeof(int));
if (!new_buf) return false;
r->buf = new_buf;
r->capacity *= 2;
}
r->buf[r->size] = elem;
r->size += 1;
return true;
}

In the normal execution path, this method increases r->size by 1 and puts the value of elem in
now-valid slot of the backing array. However, if this would increase r->size over r->capacity,
that would not be safe, so we have to increase the size of our buffer. We use realloc for this
and immediately return false—making no changes, so the ailing program still has a legal data
structure—if it fails. Otherwise, we set realloc to the new pointer value (which may be the old
one, in which case nothing changes) and increase r->capacity before doing the expansion,
which is now safe.

The code we've seen here is more defensive—more verbose—than you're probably used to
writing, but this is typical in C libraries. An executable that will run once may crash in case of an
allocation failure, but this library should not make that decision for the user. These functions, by
design, do the best they can, and return false if anything goes wrong.

Here's our method for popping from the array. Our getter treats a NULL storage location as an
error, but we sometimes do want to pop from a stack and throw its result away, so this function
considers that valid and handles that case.

bool res_array_pop(res_array* r, int* elem) {


if (!r || r->size == 0) return false;
int new_size = r->size - 1;
if (r->capacity > 4 && r->capacity > (4 * new_size)) {
int* new_buf = realloc(r->buf, (r->capacity / 2) * sizeof(int));
if (!new_buf) return false;
else {
r->buf = new_buf;
r->capacity /= 2;
}
}
if (elem) *elem = r->buf[new_size];
r->size = new_size;
return true;
}

In an error condition, we return false and make no alterations. We use realloc again, but this
time to downsize r->buf, since we don't want to hog the system's resources. Using 25% as the
lowest acceptable utilization, we halve the backing array when we get to that level, restoring us
to 50%.

There is nothing special about these "magic numbers." If your application used a very large
resizable array, you might upsize only by 20% (to 83.3%) utilization and downsize at 75%. The
more aggressively you resize, the less memory you waste, but your array operations will
perform worse—you could resize on every update, but you will be doing an O(N) operation each
time! Our implementation "wastes space" by running as low as 25% capacity, but it isn't truly
waste because, if we make resizing rare, we can have constant average (or amortized)
performance.

Hashtables—often used to implement associative arrays (or "dictionaries")—are a form of


resizable array, but will not be covered in this course.

Note: For Phase 2 of the interpreter project, you will need a resizable data structure of some
kind for name resolution, but you are not required to build a hashtable, and O(N) lookup is
acceptable.

How to Make union Types and Why

Most of the struct types we've seen are product types—that is, if you have a

struct thing {
int x;
float y;
}
then you have an int and a float in your thing object. Sometimes, however, you have an
object that is an int or a float. We've seen this before, in the discussion of tagged unions (see
Module 5.)

Using struct for a tagged union, giving each possibility its own field, works, but it's not the most
memory-efficient way to achieve this. Let's say you have six different types, A, B, C, D, E, and F.
Then the pattern we've used before would have us creating:

typedef enum dyn_type {A, B, C, D, E, F} dyn_type;

typedef struct dyn_1 {


dyn_type type;
A* if_A;
B* if_B;
C* if_C;
D* if_D;
E* if_E;
F* if_F;
} dyn_1;

If we know that five of the pointers are always going to be NULL, and thus meaningless, we're
wasting a lot of space on them. On my machine, sizeof(dyn_1) is 56—if we had millions of
them to store, we might not want a more compact structure. A union type allows us, when we
know that exactly one of the component types will be relevant, to store all those possibilities at
the same address. Below is a use of a union type that makes it more efficient:

typedef union dyn_2_data {


A* as_A;
B* as_B;
C* as_C;
D* as_D;
E* as_E;
F* as_F;
} dyn_data;

typedef struct dyn_2 {


dyn_type type;
dyn_data data;
} dyn_2;

On my system, sizeof(dyn_2) is 16.


Unions, like everything else, must be initialized—to set and get their values, use dot notation as
for structs. That is, if you had:

typedef union int_or_float {


int as_i;
float as_f;
} int_or_float;

These would both be legal usages:

int_or_float x;
x.as_i = 10;
int a = x.as_i;

int_or_float y;
y.as_f = 3.141592654;
float b = y.as_f;

This, on the other hand, will probably not do what you want:

int_or_float z;
z.as_f = 1.0;
int c = z.as_i;
printf("%d\n", c);

We store 1.0, represented as a 32-bit float, in z. We read it back as an int and get
1065353216—or 0x3f800000, the bit pattern used to represent that value. The computer didn't
do anything wrong here—the compiler keeps track for us of what is an int and what is a float;
at runtime, that information is thrown away. We said, "I know what I'm doing" and the compiler
trusted us.

However, unless you explicitly intend to manipulate bit-level representations—for example,


you're working with hardware—you should avoid using more than one accessor for any
particular value in a union type; doing so subverts the type system and is possibly undefined
behavior.

Bit Operations and Bit Fields

You've seen the binary operators before. You can use them for good and evil. Here are some
harmless applications:

bool is_odd(int x) {
return x & 1;
}
void swap(int* x, int* y) {
*x ^= *y;
*y ^= *x;
*x ^= *y;
}

int double_int(int x) {
return x << 1;
}

It's common, when multiplying by, dividing by, or computing modulo a power of two, to use
bit-level operations:

x << 4 instead of x * 16
x >> 5 instead of x / 32
x & 0x7f instead of x % 128

These days, you don't have to write this stuff—the compiler can figure it out for you—but you
should be able to recognize it when you see it.

Collections of boolean flags are often collated into a single integer, both for efficiency and to put
similar entities together. For example, if you had a collection of ten flags that could be true or
false, you wouldn't create a function:

void f(bool red, bool orange, bool yellow, bool green, bool blue, bool
purple, bool white, bool gray, bool brown, bool black)

Instead, you'd do something like this:

# define F_RED 0x001


# define F_ORANGE 0x002
# define F_YELLOW 0x004
# define F_GREEN 0x008
# define F_BLUE 0x010
# define F_PURPLE 0x020
# define F_WHITE 0x040
# define F_GRAY 0x080
# define F_BROWN 0x100
# define F_BLACK 0x200

The flags' integer values correspond to powers of 2—written in hexadecimal for visual
clarity—and, if we wanted to represent that the green, purple, and black flags were set, we'd
use: F_GREEN | F_PURPLE | F_BLACK, which corresponds to the correct numerical value. To
test wanting to test a flag, we'd use the & operator; for example:

if (a & F_GRAY) {
...
}

executes the block if and only if the F_GRAY flag is set.

To set a flag, use the idiom:

a = a | F_BLUE

and to clear one, use:

a = a & (~F_BLUE)

Sometimes, people will operate against bits by number or index; similarly, to set the 7th bit,
you'd use:

a |= (1 << 7)

and to clear it, you'd use:

a &= (~(1 << 7))

You don't have to do this stuff until you're optimizing for performance, but you will see it in other
people's code, so you should know what's going on and why people are doing it.

Bit fields take this principle even further, allowing you to write very compact structs. Let's say
that you're creating a record for a creature in Tragic: the Blathering. You could create the type:

typedef struct creature {


bool is_flying;
int loudness;
int stamina;
int colorless_blather;
int black_blather;
int blue_blather;
int green_blather;
int red_blather;
int white_blather;
} creature;

This entry takes up 36 bytes—288 bits—and mostly doesn't need them. Creatures above 10/10
in stats are rare, and mana—sorry, blather—costs in the double-digits are unplayable. We
anticipate that loudness and stamina will never exceed 20, that colorless blather costs will never
exceed 15, and that colored-blather costs will never exceed 7 in each. Then we can write:

typedef struct creature {


bool is_flying:1;
unsigned int loudness:5;
unsigned int stamina:5;
unsigned int colorless_blather:4;
unsigned int black_blather:3;
unsigned int blue_blather:3;
unsigned int green_blather:3;
unsigned int red_blather:3;
unsigned int white_blather:3;
} creature;

All these types are unsigned—as a general rule, whenever you're operating with bit patterns,
treat them as unsigned. This construction reduces sizeof(creature) to 4 bytes—substantial
savings in space—but, in terms of time, your implementation will often perform worse. That's
because, while the compiler is hiding these machinations from you, it is now forced to do a
bunch of bit arithmetic at runtime to accommodate these compact structs—values are typically
faster to read and write when they adhere to byte and word boundaries, which is not the case
here.

And if they ever print a 32/32 creature, your world breaks.

The conditions in which you need to use bit fields are rare—but again, you'll encounter them, so
you should know what they are.

Module 7 Questions (Answers Due October 10)


7.1. Should the second malloc (line 6) of res_array_new be replaced with calloc? Why or why
not?

7.2. Write a method, like res_array_push, that prepends to the resizable array—that is, that
puts the newly placed element at index 0. Why would you prefer to rarely use this?

7.3. (This question is ungraded. Any submission on time will receive full credit.) Is there anything
that has not been covered that you would like to see? If you have no suggestions, feel free to
leave this blank.

Module 7 Writeup

Please submit your answers to questions 7.1–7.3 in PDF form, answers on one page, by
October 10. There is nothing due this week with regard to the code project—due October
17—but you should keep going on it.

You might also like