Intro To C - Module 7
Intro To C - Module 7
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
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>
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.
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:
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.
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.
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.
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:
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:
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.
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)
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) {
...
}
a = a | F_BLUE
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)
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:
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:
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.
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.
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.