Advanced C Programming

Spring 2022 ECE 264 :: Purdue University

⚠ This is a PAST SEMESTER (Spring 2022).
Due 3/7

Dynamic memory: smintf(…)


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


You will create a function called smintf(…) that behaves exactly the same as mintf in HW05, with one main difference: instead of printing the result to the console (stdout), your smintf(…) will store it in a string and return the address of the new string.

The description for HW05 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);
// 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.


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.


To fetch the starter files, type this:

264get HW09

Then, cd hw09 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 HW09. 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
valgrind ./warmup

The output should be similar to the following:

aq@ecegrid-thin1 ~/264/hw09
$ gcc -o warmup warmup.c

aq@ecegrid-thin1 ~/264/hw09
$ ./warmup

aq@ecegrid-thin1 ~/264/hw09
$ 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== HEAP SUMMARY:
==37134==     in use at exit: 0 bytes in 0 blocks
==37134==   total heap usage: 1 allocs, 1 frees, 5 bytes allocated
==37134== All heap blocks were freed -- no leaks are possible
==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 HW09 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%).


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

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 HW05, 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 HW05.
  3. Modify the variables at the top of your Makefile (from miniunit) with the filenames for this assignment.

    SRC_C         = smintf.c
    SRC_H         = smintf.h miniunit.h clog.h
    TEST_C        = test_smintf.c
    EXECUTABLE    = test_smintf

    Amend your make test rule so that it also checks your code with Valgrind. To do this, add the following action to the end of your make test rule.

    valgrind ./$(EXECUTABLE)

    How to work

    It is expected that everyone will follow the test-driven development process (described in the HW05 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 HW05 if you wish. However, if your HW05 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.


    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 result 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 HW05. 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(…).
      • This should consist primarily of calls to mu_run(_test_▒▒▒).
      • 100% code coverage is required.
      • Your main(…) must return EXIT_SUCCESS.
      • For the bonus option, this should also test mintf(…).
       test ▒▒▒()
      return type: int
      • This should use your mu_check(…) to check that your smintf(…) is working correctly.
      • Optional: To make this code tidier, you may add a wrapper macro to your miniunit.h like this:
        #define mu_check_strings_equal(s1, s2)  mu_check(strcmp((s1), (s2)) == 0)

        Then, in your test_smintf.c, add a macro that uses mu_check_strings_equal(…) but also ensures that the result of smintf(…) gets freed.

        #define mu_check_smintf(expected, ...)              \
            do {                                            \
                char* actual = smintf(__VA_ARGS__);         \
                mu_check_strings_equal((expected), (actual));  \
                free(actual);                               \
            } while(false)
        Finally, use mu_check_smintf(…) in each of your _test_▒▒▒(…) functions to test that your smintf(…) works properly like this:
        int _test_▒▒▒() {
            mu_check_smintf("▒▒▒▒▒▒▒▒", "▒▒▒", ▒▒▒, ▒▒▒);
            mu_check_smintf("▒▒▒▒▒▒▒▒", "▒▒▒", ▒▒▒, ▒▒▒);
            mu_check_smintf("▒▒▒▒▒▒▒▒", "▒▒▒", ▒▒▒, ▒▒▒);
            mu_check_smintf("▒▒▒▒▒▒▒▒", "▒▒▒", ▒▒▒, ▒▒▒);
        To use strcmp, you must add #include <string.h> to the top of your miniunit.h.
        👌You may copy/adapt the mu_check_strings_equal(…) and mu_check_smintf(…) macros above if you understand them and accept ultimate responsibility for the correctness of your submission.
       test ▒▒▒()
      return type: int
       test ▒▒▒()
      return type: int
      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✶
      miniunit.h macros
      from miniunit
      clog.h macros
      from miniunit
    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, %$) 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
      string.h strcmp, (anything else) test_smintf.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 HW05. The real work will be to understand memory, addresses, 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 HW05, 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 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 HW09_BONUS
    7. All other requirements are the same as for the non-bonus HW09 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 HW05 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.


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

    264submit ASSIGNMENT FILES…

    For HW09, you will type 264submit hw09 smintf.c test_smintf.c warmup.c miniunit.h clog.h Makefile from inside your hw09 directory.

    Makefile, miniunit.h, and clog.h will not be tested directly (i.e., to determine if they are correct). Most likely, we will not look at your Makefile at all, but we ask that you submit them, just in case they are useful for troubleshooting as we refine our tester.

    Be sure to include your miniunit.h and clog.h. Otherwise, your test_smintf.c will not compile correctly.

    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.


    1. Can we use our code from HW05?
    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 HW09 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 HW09 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 HW09, 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);
          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. 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 HW05 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).
    6. 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);
      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
      A more direct alternative is fputs(…), which we haven't covered. You are welcome to use fputs(…) in your test_smintf.c.
    7. How do I get 100% test code coverage while also checking if malloc(…) failed?

      Instead of checking if malloc(…) failed, check if it passed.
      // Okay to copy/adapt this snippet
      char* s = malloc(…);
      if(s != NULL) {
      return s;
    8. How can I achieve 100% code coverage while still checking the return value of malloc(…)?
      See this thread on Piazza.
    9. make test: “/bin/bash: -c: line 5: syntax error: unexpected end of file”?
      Make sure you don't forget the \ at the end of your valgrind ./$(EXECUTABLE); \.

    The Q&A sections of HW05 and HW02 answer many more questions, including how to handle variadic functions, INT_MIN and lots more.


    • You do not need to use expected.txt for anything. Use Miniunit to test.
    • mu_check_strings_equal(…) (not mu_check_strings_eq(…))