Advanced C Programming

Spring 2019 :: ECE 264 :: Purdue University

⚠ This is for Spring 2019, not the current semester.
Due 2/20

Dynamic memory: smintf(…)

Goals

This assignment has the following objectives:
  1. Practice programming with dynamic memory (i.e., malloc).
  2. Solidify the lessons from strings about representations in memory versus code.
  3. Start learning to debug memory errors.

Overview

You will create a function called smintf(…) that behaves exactly the same as mintf in strings, except that instead of printing the result to the console (stdout), your smintf(…) will store it in a string and return a pointer to the new string. The description for strings serves as the specification for how the format string is to be used and anything else not covered here.

Example: smintf(…) vs. mintf(…)

The following two snippets both print "1 + 2 = 3":

// Using smintf(…)
char* s = smintf("1 + 2 = %d", 3);
printf("%s", s);
free(s);
// Using mintf(…)
mintf("1 + 2 = %d", 3);

Note: The example above could also work with printf(s) for that particular example, but if the string returned by smintf(…) includes any format specifiers, that could lead to problems when passing it to printf(s). See Q7 in the Q&A.

About malloc() and free(…)

When we need to create an array (such as a string) and we don't know in advance how long it needs to be, we must use dynamic memory. This is an alternative to the normal way we allocate arrays (e.g., int array[5];) with more flexibility.

The malloc(size) function reserves size bytes of space in the heap memory, which can be used for your array. It returns the address of the newly allocated block of memory. This is called allocation.

The free(addr) function releases that reservation when you are done with it, so that memory can be reused. This is called deallocation.

Example: using malloc(…) and free(…)

Let us consider an example. To reserve space in the heap memory for 6 characters including the null terminator at the end ('\0'), you would use the following:

char* s = malloc(sizeof(*s) * 6);

The sizeof(*s) term gives the number of bytes required for one character.

Some of you may have seen another syntax for this: sizeof(char). Either can work, but the we prefer sizeof(*s) because if you ever change the type of s—say, from char to int or wide characters—you only have to change one place. This avoids bugs. This form is required by our Code Quality Standards, as well as Google's style guide and presumeably others.

To write to our newly allocated block of memory, we treat it just like an array.

s[0] = 'A';
s[1] = 'B';
s[2] = 'C';
s[3] = 'D';
s[4] = 'E';
s[5] = '\0';   // Don't forget the null terminator!!!

We can now use it like any other array. For example:

printf("%s", s);

It is very important to deallocate all memory that was allocated. Here's how to do that.

free(s);

If you ever forget to deallocate memory, it is called a memory leak. This should be avoided. The valgrind command helps you check your code for memory leaks, and other programming mistakes involving memory. You will see how to use valgrind below.

Begin

To fetch the starter files, type this:

264get HW06

Then, cd hw06 to enter the new directory.

Warm-up exercises

This assignment includes a warm-up exercise to help you get ready. This accounts for 10% of your score for HW06. Scoring will be relatively light, but the usual base requirements apply.

In the starter files, you will find a file called warmup.c. It contains an unfinished function called my_strdup(…) which takes a string as an argument, and should return a separate copy. The copy will be dynamically allocated using malloc(…). The caller (i.e., main(…)) is responsible for deallocating the memory (i.e., calling free(…)).

Fill in the implementation of my_strdup(…). This can be done in as little as 8 sloc.

To test your warm-up exercise, type the following:

gcc -o warmup warmup.c
./warmup
valgrind ./warmup

The output should be similar to the following:

SAMPLE OUTPUT
aq@ecegrid-thin1 ~/264/hw06
$ gcc -o warmup warmup.c

aq@ecegrid-thin1 ~/264/hw06
$ ./warmup
abc
abc

aq@ecegrid-thin1 ~/264/hw06
$ valgrind ./warmup

==37134== Memcheck, a memory error detector
==37134== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==37134== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==37134== Command: ./warmup
==37134==
abc
abc
==37134==
==37134== HEAP SUMMARY:
==37134==     in use at exit: 0 bytes in 0 blocks
==37134==   total heap usage: 1 allocs, 1 frees, 5 bytes allocated
==37134==
==37134== All heap blocks were freed -- no leaks are possible
==37134==
==37134== For counts of detected and suppressed errors, rerun with: -v
==37134== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 6 from 6)

The valgrind command checks for any programming mistakes you might have made, that would lead to memory leaks. Notice in the valgrind output that there are 0 errors. That's what you always want to see. Also notice that 5 bytes were allocated. That includes 'a', 'b', 'c', '\n', and '\0' (null terminator). That gives you some confirmation that your code is working as expected.

You can safely ignore the part that says, “(suppressed: 6 from 6)”.

Opt out

In a hurry, and don't need the practice? This warm-up is here to help you learn what you need to succeed on the rest of this assignment—not to add additional work. Therefore, we give you an option. Those who feel that they do not need the practice may "opt out". by modifying warmup.c so that it does nothing but print the following message exactly and then exit:

I already know this and do not need to practice.

If you do that, then your score for HW06 will be based solely on the rest of this assignment. If you leave the warmup.c undone, if you do not turn in a warmup.c, or if the message it prints does not match perfectly, then you will receive 0 for the warmup portion of this assignment (10%).

Submission

The warm-up will be turned in together with the rest of HW06.

Doing the assignment

In addition to the warmup.c, you will find one more file in the startup files: smintf.h. That is the header file, which defines the signature of the smintf(…) function. The header file should be included (via #include) at the top of your smintf.c and test_smintf.c, just below any standard libraries.

You will create the following files:
  1. smintf.c is where your smintf(…) will go. To start this file, simply copy the smintf(…) function signatures from smintf.h into a new file called smintf.c and then add the braces to make them into functions.
  2. test_smintf.c will contain a main(…) function that will test your smintf(…). Since smintf(…) returns a char* and does not print anything to the console directly, you will need to use printf to print the result of smintf(…) to the console. Like strings, your test_smintf.c must exercise all of the functionality in your smintf(…) (and mintf(…) if you take the bonus option, described below). The requirements, with respect to completeness, are the same as for strings.
  3. expected.txt will contain the expected output from running your tests in test_smintf.c. It will be a simple text file.

To test if your code works perfectly, you will run the following:
gcc -o test_smintf test_smintf.c smintf.c # compile your test together with your smintf.c to create an executable called test_smintf
./test_smintf > actual.txt # run your executable and send the output to a file called actual.txt. That filename is arbitrary.
diff actual.txt expected.txt # compare the actual output with the expected output using the diff command

The diff command prints the differences so if you see any output at all, then your test failed. If you see no output, then it passed.

The following let you do the whole thing in one command.
gcc -o test_smintf test_smintf.c smintf.c && ./test_smintf | diff expected.txt -

You must also test for memory problems using Valgrind. To run Valgrind, compile your program and then enter valgrind ./test_smintf from bash. Details about the messages are on the course reference sheet.

How to work

It is expected that everyone will follow the test-driven development process (described in the strings description). We have no way to check this, but it will help you to do the assignment more quickly and with less frustration. If you ignore this, you do so at your own peril.

Keep your code clean (meeting the code quality standards) at all times. It should be properly indented at all times during the process. If you have warnings, fix them immediately. Course staff will not assist with code that is not properly formatted or has warnings (unless the warning is the issue you are asking for help with).

You are welcome to use your code from strings if you wish. However, if your strings code is messy (and it shouldn't be), then you should seriously consider starting from a fresh file. Modifying messy code is almost always more work than rewriting it.

Requirements

  1. Your submission must contain each of the following files, as specified:
    file contents
    smintf.c functions
    smintf(const char ✶format, ...)
    return type: char✶
    smintf(…) returns a dynamically allocated string containing the output that would ult from calling mintf(…) with the same arguments.
    smintf(…) is responsible for allocating the memory (i.e., calling malloc(…)).
    ⓑ If allocation fails (i.e., malloc(…) returns NULL), then smintf(…) should return NULL.
    ⓒ Deallocation (i.e., calling free(…)) is the responsibility of the caller (e.g., main(…)).
    malloc(…) may be called only once as a result of each call to smintf(…).
    mintf(const char ✶format, ...)
    return type: void
    mintf(…) should only be included if you are doing the bonus option. Its specification is the same as in strings. Do not include mintf(…) unless you are doing the bonus option.
    smintf.h declarations
    declaration of smintf(…)
    test_smintf.c functions
    main(int argc, char✶ argv[])
    return type: int
    Test your smintf(…). Your main(…) must return EXIT_SUCCESS (0).

    For the bonus option, this should also test mintf(…).

    expected.txt output Expected output from running your test_smintf.c.
    warmup.c function
    main(int argc, char✶ argv[])
    return type: int
    Do not modify main(…) unless you are opting out.

    If you choose to opt out, it should contain the following. (You may copy this.)

    printf("I already know this and do not need to practice.");
    return EXIT_SUCCESS;
    my strdup(const char✶ original)
    return type: char✶
  2. Do not include a print_integer function. If you need to use a helper function for base conversion (similar to print_integer), just name it something else. As per the code style guidelines, helper functions must begin with an underscore ("_").
  3. Do not modify the smintf.h, except that if you choose the bonus-option, you must uncomment the line with the mintf(…) declaration.
  4. Your test_smintf.c must exercise all functionality (e.g., all format codes, negative numbers, 0, etc.). If you choose the bonus option, it must excercise both smintf(…) and mintf(…).
  5. For format codes related to numbers, your program should handle any valid int (for %d, %x, %b) or double (%$) value on the system it is being run on, including 0, positive numbers, and negative numbers. It should make no assumptions about the size of an int.
  6. Only the following external header files, functions, and symbols are allowed in your smintf.c. That means you may use printf(…) in your test_smintf.c but not in your smintf.c.
    header functions/symbols allowed in…
    limits.h INT_MAX, INT_MIN test_smintf.c
    stdarg.h va_list, va_start, va_arg, va_end, va_copy smintf.c, test_smintf.c
    stdbool.h bool, true, false smintf.c, test_smintf.c, warmup.c
    stdio.h fputc, fputs, stdout test_smintf.c, warmup.c
    printf test_smintf.c
    stdlib.h malloc, abs smintf.c, test_smintf.c, warmup.c
    EXIT_SUCCESS, EXIT_FAILURE, free test_smintf.c, warmup.c
    fputc(…) and stdout may be used in smintf.c only if you are doing the bonus option. All others are prohibited unless approved by the instructor. Feel free to ask if there is something you would like to use. Also, note that if you are using free(…) in your smintf(…), you are almost certainly making a mistake.
  7. Submissions must meet the code quality standards and the policies on homework and academic integrity.

How much work is this?

This assignment—with or without the bonus option—will require a moderate modification of your strings. The real work will be to understand pointers, and think about how to structure your code. The fact that you can only use malloc(…) once per call to smintf (and/or mintf), and that you cannot use realloc(…), make this harder than it might otherwise be.

Bonus option

This homework comes with a bonus option for to receive 2 bonus points. (That is roughly 2/3 as much as an entire homework. See the course policies for details.) For the bonus option, you will create a smintf and mintf in one file that use the same code—i.e., call the same helper functions—to handle dealing with the arguments. The additional requirements are summarized below.

  1. smintf.c contains the following:
    1. mintf(…) – a very short (e.g., ≈4 lines) function that behaves exactly the same as the mintf(…) in strings, but uses a helper function to do nearly everything.
    2. smintf(…) – a very short (e.g., ≈12 lines) function that meets all of the requirements for smintf(…), but, like mintf(…) uses a helper function do nearly everything.
    3. ≥1 helper functions that handle reading the format string and building the formatted output
  2. fputc(…) is used on only one line of one function in your entire smintf.c file.
  3. va_arg(…) may be called any number of times, but only be used within a single function.
  4. Your test_smintf.c and expected.txt must cover both mintf(…) and smintf(…).
  5. mintf(…) may not result in any dynamic memory allocation (i.e., calling malloc(…)). In other words, you may not simply implement smintf(…) and then make a wrapper that prints its output to the console.
  6. Add the following line at/near the top of your smintf.c (above the first line of C code):
    #define HW06_BONUS
  7. All other requirements are the same as for the non-bonus HW06 option.

Hint: You can pass your va_list object to your helper function. This example code related to vprintf(…) illustrates how this looks.

You will turn in the same files by the same deadline, whether you choose the bonus option or the non-bonus option. To indicate that you are taking the bonus option, simply include a mintf(…) in your submission. Do not include mintf(…) if you are not doing the bonus option.

Should I do the bonus?

The bonus option may or may not be more work. The instructor's solution takes the bonus option. Starting with his strings solution, he changed fewer than 50 sloc* to produce a working smintf.c with the bonus option. Of the 50 sloc that were changed, about 20 were trivial changes (e.g., converting references to fputc(…) and print_integer(…) into references to to multi-purpose helper functions).

* sloc = "source lines of code" (excluding comments and blank lines)

The bonus option does not require any more advanced C programming knowledge than the non-bonus option. However, it will require a little more creative thinking to find a way to structure your code.

Choose carefully. Your strategy for the bonus option might be different from what you would choose for the non-bonus option. If you choose this path, you will be somewhat committed to it. If you were to start with the non-bonus option and then decide to do the bonus option later, you might find it to be far more extra work than if you just took the bonus option to begin with.

Submit

In general, to submit any assignment for this course, you will use the following command:

264submit ASSIGNMENT FILES…

For HW06, you will type 264submit hw06 smintf.c test_smintf.c expected.txt warmup.c from inside your hw06 directory.

You can submit as often as you want, even if you are not finished with the assignment. That saves a backup copy which we can retrieve for you if you ever have a problem.

Q&A

  1. Can we use our code from strings?
    Yes.
  2. How does smintf(…) print its result?
    It doesn't. It returns a char*. In your test code, you will want to pass that to printf. However, smintf(…) itself will not print anything to the console.
  3. How can I use va_copy(…)?
    va_copy(…) is a companion to va_start/va_arg/va_end that allows you to step through the optional arguments multiple times. It may also prevent some kinds of memory errors when passing the additional arguments to a helper function.

    Declare your "normal" instance of va_list and one "copy" instance of va_copy for each helper function that will step through the arguments. Call va_start to initialize the first instance. Then, for each time they will be passed to a helper function, call va_copy(…) to copy the first instance into each of the "copy" instances. At the end of your variadic function, call va_end, once for the "normal" instance and each of the "copy" instances.

    Here's an example:
    initial state of project files
    The output is as follows:
    decimal:      2015
    decimal:      2016
    decimal:      2017
    hexadecimal:  7df
    hexadecimal:  7e0
    hexadecimal:  7e1
    Do not attempt to model your HW06 code after the above example. It is only intended to show you how va_copy works. Despite the mention of printing in decimal and hexadecimal, this is very different from how your HW06 code will look. Also, note that the terms "normal" instance and "copy" instance are only used for purposes of this explanation, and are not used elsewhere. (Unfortunately, outside documentation for va_copy is a bit spotty.)
  4. How does pass-by-address work?
    Pass-by-address means passing values to functions by their memory, instead of passing the value. The main advantage is that the callee (the function you are calling) can modify the value of the parameter in-place, such that the changes will still be in effect even after the callee returns. That's because the variable resides in the caller's stack frame.

    Pass-by-address is not required for HW06, but some people might find that it makes things easier.

    Here is a very simple example to help you understand:
    #include <stdio.h>
    
    void make5(int* a_n) {
        *a_n = 5;
    }
    
    int main(int argc, char *argv[]) {
        int n = 0;
        printf("n == %d\n", n);
    
        make5(&n);
        printf("n == %d\n", n);
    
        return 0;
    }
    The output is as follows:
    n == 0
    n == 5
    Even though make5(…) does not return a value, it is still able to modify n because it receives its address. n exists in main's stack frame, so in the statement *a_n = 5, the code in make5(…) is actually modifying a variable in main's stack frame. This can be useful, for example, if you want to make a function that returns more than one value, or updates a variable that was declared in the caller.
  5. Do I need to worry about differences consisting of only blank lines?
    No. To tell diff to ignore blank lines, add -B to the diff comment.

    When there is a blank line difference, you will see something like this:
    8d7
    <
    Using the following command should silence that:
    gcc -o test_smintf test_smintf.c smintf.c && ./test_smintf | diff -B expected.txt -
  6. What is a helper function?
    A helper function is a function that is called only from one function or one file, and serves only to simplify other code. For example, in strings if your only goal were to implement mintf(…) (i.e., if print_integer(…) were not part of the spec), you might still create a print_integer(…) function as a helper function.

    Helper functions make the externally visible functions shorter and more readable, by replacing several statements with one function call that is (hopefully) named descriptively. For example, adding a call to print_integer(…) in your mintf(…) is a lot more readable than pasting the entire contents of your print_integer(…) in your mintf(…).

    By convention, helper functions are often given a name starting with '_', and this is required by the Code Quality Standards in this class. When someone else (or possibly you at a much later date) sees the '_' prefix, they know that the function is not called from outside that file, so it is safer to change. It also warns them that the function may only make sense in the context of whatever function(s) it is helping (i.e., the functions that call it).
  7. Can I pass the output of smintf(…) directly to printf(…)?
    Yes, but it will only work reliably if you pass it as an additional argument, like this example (which you may copy if you like):
    char* s = smintf(…);
    printf("%s", s);
    free(s);
    It might be tempting to just use printf(s), but consider what happens if your smintf(…) returns a string that contains a format specifier. For example:
    char* s = smintf("%%%c", 'd'); // returns "%d"
    printf(s);  // same as printf("%d"); ⇒ problem because printf will be looking for an argument
    free(s);
    A more direct alternative is fputs(…), which we haven't covered. You are welcome to use fputs(…) in your test_smintf.c.

The Q&A sections of strings and HW02 answer many more questions, including how to handle variadic functions, rounding floating point numbers (for %$), INT_MIN and lots more.