Advanced C Programming

Autumn 2015 :: ECE 264 :: Purdue University

This is for Fall 2015 (9 years ago)
Due 9/16

Malloc: make your own sprintf

Goals

This assignment has the following objectives:
  1. Practice programming with dynamic memory (i.e., malloc).
  2. Solidify the lessons from HW02 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 HW02, except that instead of printing the result to the console (stdout), your smint will store it in a string and return a pointer to the new string. The description for HW02 serves as the specification for how the format string is to be used and anything else not covered here.

The output of printf( smintf(…) ) will be exactly the same as mintf(…).

Read this carefully, even if parts of it look identical. There are a few small differences.

Prepare

The following will help you get started:

  1. Read this assignment description carefully.
  2. Read the code quality standards.
  3. Write some sample programs using malloc to make sure you understand its use.
  4. Write some sample programs using address types (e.g., int*), and address of address (e.g., int**) types.
  5. Write a swap(…) function that swaps the value of two integers by reference.
  6. Write a program that has an intentional memory leak. Use Valgrind to detect it. Make sure you understand the message. (Valgrind is a command-line tool for detecting memory problems. Its use is described briefly below.)
  7. Read each of the examples of memory problems on the back of the course reference sheet.

Doing the assignment

Type 264get hw03 to get the files, and then cd hw03 (from bash) to enter that directory.

You will find only one file: hw03.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 hw03.c and hw03test.c, just below any standard libraries.

You will create the following files:
  1. hw03.c is where your smintf(…) will go. To start this file, simply copy the smintf(…) function signatures from hw03.h into a new file called hw03.c and then add the braces to make them into functions.
  2. hw03test.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 HW02, your hw03test.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 HW02.
  3. hw03test.txt will contain the expected output from running your tests in hw03test.c. It will be a simple text file.

Your starting setup will look something like this:
initial state of project files
Note: The contents of the smintf and main in this illustration are not adequate and not entirely correct.

To test if your code works perfectly, you will run the following:
gcc -o hw03test hw03test.c hw03.c # compile your test together with your hw03.c to create an executable called hw03test
./hw03test > hw03test.output.txt # run your executable and send the output to a file called hw03test.actual.txt (That filename is arbitrary.)
diff hw03test.actual.txt hw03test.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 hw03test hw03test.c hw03.c && ./hw03test | diff hw03test.txt -

You must also test for memory problems using Valgrind. To run Valgrind, compile your program and then enter valgrind --leak-check=full ./hw03test 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 HW02 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 HW02 if you wish. However, if your HW02 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 the following files:
    1. hw03.c – contains mintf(…) and not a main(…)
    2. hw03test.c – main(…)
    3. hw03test.txt – expected output of your hw03test.c if compiled and run with any correct hw03.c
  2. Code must meet the code quality standards.
  3. Do not use dynamic arrays (C99).
  4. Malloc may only be called once per call to smintf.
  5. Your hw03.c may not call printf, realloc, calloc, or any other external function/macro/symbol except: va_list, va_start, va_arg, va_copy, va_end, and stdout, malloc, and free. fputc() may be used only if you are doing the bonus option.
  6. Your hw03.c may not include any header file except: stdio.h, stdlib.h, stdarg.h, stdbool.h, and hw03.h.
  7. If malloc fails, smintf must return NULL.
  8. Where possible, free memory in the same scope that it was allocated. Roughly speaking, this means the number of calls to malloc will be the same as the number of calls to free. If you find a situation where this is impossible, it must be clearly documented in the code.
  9. Your code may not have any memory leaks or other memory, such as the ones on the course reference sheet. Valgrind can help you catch most memory problems, but you are responsible for all memory problems, whether detected by Valgrind or not. (Practically speaking, Valgrind is our primary method of detection, but if a memory problem causes your program to crash or misbehave, then your score would be subject to the usual penalty for memory errors. See the course policies.)
  10. Do not include a mintf function, unless you are doing the bonus option.
  11. 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 ("_"). For example, _print_integer(…) would be allowed, although a more descriptive name would be better since smint(…) does not print anything on its own.
  12. Do not modify the hw03.h, except that if you choose the bonus-option, you must uncomment the line with the mintf(…) declaration.
  13. Your hw03test.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(…).
  14. For format codes related to numbers (%d, %x, %b, %$), your program should handle any valid int value on the system it is being run on, including 0, positive numbers, and negative numbers.
  15. Your hw03test.c may include any standard header and call any function, including printf(…).
  16. Your code may not make any assumptions about the size of an int. You may use sizeof(int) if you feel that it would be helpful.
  17. Code must compile without warnings or errors on ecegrid using the installed version of gcc installed there and the flags given in the provided .bashrc file (-std=c99 -pedantic -g -Wall -Wshadow).
  18. Code must run without crashing, for your test file and for any other valid test file.
  19. For negative numbers, print the "-" before the prefix (e.g., "-$3.00" not "$-3.00", "-0x12AD" not "0x-12AD").

How much work is this?

This assignment—with or without the bonus option—will require a moderate modification of your hw02.c. 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 60% 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 for the bonus You will have one file (hw03.c) containing the following functions:

  1. hw03.c contains the following:
    1. mintf(…) – a very short (e.g., ≈4 lines) function that behaves exactly the same as the mintf(…) in HW02, 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 hw03.c file.
  3. va_arg(…) may be called any number of times, but only be used within a single function.
  4. Your hw03test.c and hw03test.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 smint(…) and then make a wrapper that prints its output to the console.
  6. All other requirements are the same as for the non-bonus HW03 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 HW02 solution, he changed fewer than 50 sloc* to produce a working hw03.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

To submit, type the following in bash:
264submit hw03 hw03.c hw03test.c hw03test.txt.

Note: The general format of the 264submit command is 264submit <asg_id> <files…>   where asg_id is like "hw03" and files is one or more files (e.g., "hw03.c"), possibly using wildcards (e.g., "hw03*").

Q&A

  1. Can we use our code from HW02?
    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 your operional 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 pbe 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 HW03 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 HW03 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 HW03, but some people might find that it makes things easier.

    Below is the example I used in class on 9/10/2015:
    #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 hw03test hw03test.c hw03.c && ./hw03test | diff -B hw03test.txt -
  6. Can I use sizeof with a string (e.g., char* s = "abc"; int n=sizeof(s);)
    Yes and no. It is not prohibited, but we strongly recommend that you use sizeof only with types (e.g., sizeof(int) or sizeof(char)), and not with variables (sizeof(n)). In particular, using sizeof with arrays and strings (e.g., sizeof(my_array) or sizeof(my_string)) does not do what most students expect.

    Also regarding sizeof, be sure that when using malloc to allocate space for a string, you measure the space in terms of sizeof(char) and not sizeof(char*).
    char* s_too_big = malloc(sizeof(char*) * (num_chars + 1)); // BAD!!!
    char* s_correct = malloc(sizeof(char) * (num_chars + 1));  // GOOD

Updates

9/9: Added a screenshot of the starting point.

9/10: Correction: You may include stdlib.h.

9/10: Clarification: The smintf and main in the screenshot are neither adequate nor entirely correct.

9/11: Q&A: Added an explanation of va_copy(…).

9/15: Extended deadline to 9/16 (announced by email 9/14), va_copy is allowed

9/16: Clarification: Emphasized that the test case shown in the screenshot is not adequate or correct. (bold print, slightly clearer wording); added Q5 and Q6 to the Q&A