Go back to this week's lab writeup here.
These optional problems are an opportunity to further exercise your understanding of arrays, pointers and the heap.
1) Code Study: Heap Use
About 15 minutes
Let's take a look at code that dynamically allocates memory on the heap. Specifically, the function join(strings, strings_count)
in the provided bonus_heap1.c
program returns a string which is the concatenation of all strings_count
strings in strings
. The returned string is heap-allocated and becomes the client's responsibility to free when done. A core function that makes this implementation possible is the realloc
function.
1 char *join(char *strings[], int strings_count) {
2 char *result = strdup(strings[0]);
3 assert(result != NULL);
4
5 for (int i = 1; i < strings_count; i++) {
6 result = realloc(result, strlen(result) + strlen(strings[i]) + 1);
7 assert(result != NULL);
8 strcat(result, strings[i]);
9 }
10 return result;
11 }
At the outset of the loop, the total size needed is not known, so it cannot do the allocation upfront. Instead it makes an initial allocation and enlarges it for each additional string. This resize-as-you-go approach is an idiomatic use case for realloc
.
- Q: Is the size argument to
realloc
the number of additional bytes to add to the memory block, or the total number of bytes including any previously allocated part? - The variable
result
is initialized tostrdup(strings[0])
. Q: Why does it make a copy instead of just assigningresult = strings[0]
? - The
realloc
manual page says the return value fromrealloc(ptr, newsize)
may be the same asptr
, but also may be different. In the case where it is different,realloc
has copied the contents from the old location to the new. The old location is also freed. Change the code to callrealloc
without using its return value (i.e. don't re-assignresult
, just drop the result). Re-compile and you should get a warning from the compiler. Q: What error is that warning trying to caution you against? Arealloc
call that doesn't catch its return value is never the right call. - Try running the code; if you run
./heap
with one or more arguments, it will join all of the arguments together using thejoin
function.
2) Additional Code Study: Heap Use
About 15 minutes
Let's take a look at code that dynamically allocates memory on the heap using malloc
, strdup
and realloc
. Specifically, we'll look at bonus_heap2.c
, which you can run or trace in gdb, containing a sample function called remove_first_occurrence
.
The function remove_first_occurrence(text, pattern)
returns a string which is a copy of text
with the first occurrence of pattern
removed. The returned string is heap-allocated and becomes the client's responsibility to free when done.
1 char *remove_first_occurrence(const char *text,
2 const char *pattern) {
3
4 char *found = strstr(text, pattern);
5
6 if (!found) {
7 char *result = strdup(text);
8 assert(result != NULL);
9 return result;
10 }
11
12 int nbefore = found - text;
13 int nremoved = strlen(pattern);
14 char *result = malloc(strlen(text) + 1 - nremoved);
15 assert(result != NULL);
16
17 strncpy(result, text, nbefore);
18 strcpy(result + nbefore, found + nremoved);
19 return result;
20 }
Work through the following questions to understand this code. Ask questions about anything confusing!
- What is the purpose of the library function
strstr
? What does it return? - Lines 6-8 handle the case where there is no occurrence of
pattern
intext
, i.e. nothing to remove. Why does the function returnstrdup(text)
rather than just returningtext
? - Line 10 subtracts two pointers. What is the result of subtracting two pointers? Here's a hint: if q = p + 3, then q - p => 3. How does the sizeof(pointee) affect the result of a subtraction?
assert
terminates the program if the passed-in expression is false. What is the check on line 13?- After line 17 executes, is the result string null-terminated? Why or why not?
- Your colleague proposes changing line 18 to instead:
strcat(result, found + nremoved)
claiming it will operate equivalently. You try it and it even seems to work. Is it equivalent? No, not quite... The code is just "getting lucky". In what way does it (wrongly) depend on the initial contents of newly allocated heap memory? How will the function break when that wrong assumption is not true? - Try running the code; if you run
./bonus_heap
with no arguments, it will run a short test of the function, removing "time" from "centimeter".
3) Code study: calloc
About 15-20 minutes
Let's take a look at the implementation of calloc
, an alternative to the malloc
memory-allocating function, to better understand how heap memory allocation works. First, read the manual page for the function (man calloc
) and then review the implementation for musl_calloc
shown below (based on code from musl):
1 void *musl_calloc(size_t nelems, size_t elemsz) {
2 if (nelems && (elemsz > SIZE_MAX / nelems)) {
3 return NULL;
4 }
5 size_t total = nelems * elemsz;
6 void *p = malloc(total);
7 if (p != NULL) {
8 long *wp;
9 size_t nw = (total + sizeof(*wp) - 1) / sizeof(*wp);
10 for (wp = p; nw != 0; nw--, wp++) {
11 *wp = 0;
12 }
13 }
14 return p;
15 }
The calloc
interface is presented as allocating an array of nelems
elements each of size elemsz
bytes. It initializes the memory contents to zero, which is the feature that distinguishes calloc
from ordinary malloc
.
Here are some issues to go through to understand the operation of this function.
- The
calloc
interface as described in terms of arrays is somewhat misleading because there is nothing array-specific to its operation. It simply allocates a region of total sizenelems * elemsz
bytes. The callcalloc(10, 2)
has exactly the same effect ascalloc(4, 5)
orcalloc(1, 20)
and memory allocated by any of those calls could be used to store a length-20 string or an array of 5 integers, for example. - What are lines 2-4 of the function doing? What would be the effect on the function's operation if these were removed?
- A colleague objects to the use of division on line 2 because division is more expensive than multiplication and it requires an extra check for a zero divisor. They propose changing it to
if (nelems * elemsz > SIZE_MAX)
, claiming it will operate equivalently and more efficiently. Explain to them why this plan won't work. - Line 6 declares
p
to be of typevoid*
, which matches the return type ofmalloc
. Avoid*
pointer is a generic pointer. It stores an address, but makes no claim about the type of the pointee. It is illegal to dereference or do pointer arithmetic on avoid*
, since both of these operations depend on the type of the pointer, which is not known in this case. However,void *
can be assigned to/from without an explicit cast. - What is the purpose of line 7? What would be the effect on the function's operation if that test were removed and it always proceeded into the loop?
- Line 9 contains the expression
sizeof(*wp)
. Given thatwp
was declared and not assigned, this looks to dereference an uninitialized pointer! Not to fear, it turns out thatsizeof
is evaluated at compile-time by working out the size for an expression from its declared type. Sincewp
was declared as along *
, the expression*wp
is of typelong
which on Myth systems has size 8 bytes. The compiler will have transformed it into(total + 8 - 1)/8
. Phew! There will be no nasty runtime surprises. - Let's further dig into line 9. It should remind you of code we've seen previously (roundup in lab1). What is the value of
nw
for the callcalloc(20, 4)
, forcalloc(2, 7)
, or forcalloc(3, 1)
? Aha, this is a rounding operation, as suspected. This calculation is making an assumption about how many bytesmalloc
will actually reserve for a given requested size. What is that assumption? (This behavior is not a documented public feature and no client that was a true outsider should assume this, butcalloc
andmalloc
were authored by the same programmer, and thuscalloc
is being written by someone with firsthand knowledge of themalloc
internals and acting as a privileged insider.) - The initialization of the
for
loop on line 10 assignswp = p
, yetwp
andp
are not of the same declared type. Why is no typecast required here? Would the function behave any differently if you added the typecastwp = (long *)p
? - The increment of the
for
loop on line 10 offers a rare glimpse of C's comma operator. You can read more about the comma operator on Wikipedia; the only use you are likely to see in practice is to pack multiple expressions into a loop init or increment. In this case, the increment will both advance the pointer and decrement the count of iterations remaining. - Imagine if line 8 instead declared
char *wp
with no other changes to the code. How would this change the function's behavior? What if the declaration wasint *wp
? What if the declaration werevoid *wp
? For each, consider how the pointee type affects the operationswp++
and*wp = 0
. What do you think is the motivation for the author's choice oflong *
?
4) Calloc Challenge
About 20-30min
The challenge.c
file contains the calloc
function from the earlier problem. The task is to modify the implementation to fill the memory with a specified pattern, instead of just 0s. Here's what the function prototype could look like:
void *musl_calloc2(size_t nelems, size_t elemsz, unsigned long pattern, int width) {
...
}
The first two parameters are the same, but the last two are new. pattern
would be a bit pattern, between 0 and 64 bits. width
would be the width (in bits) of that pattern. Only the least-significant width
bits should be used as the pattern, even if other bits are non-zero. In other words, you shouldn't assume what is in the sizeof(long) - width
other bits.
This new version of calloc
should fill in the memory with the pattern of the given size. For instance, if the pattern is 0x8
and the width is 4
, if you allocate 16 bytes of memory, each byte should look like 0x8888888888888888
. The pattern is used to fill in every width
bits of the allocated memory. Therefore, if you allocate 16 bytes of memory with the pattern 0x8000
and width 16
, each byte should look like 0x8000800080008000
. If the pattern does not fit evenly into a long
, it should be copied in whole as much as possible from right to left. For example, if the pattern is 0x15
with width 5
(0b10101
), each byte should look like 0x0ad6b5ad6b5ad6b5
(note the 4 leading zeros).
This task should exercise your bit and memory skills. Give it a shot!