Advanced C Programming

Autumn 2015 :: ECE 264 :: Purdue University

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

Strings: make your own printf

Goals

This assignment has the following goals:
  1. Practice programming with strings (statically allocated).
  2. Understand the relationship between data displayed on screen and its representations in memory.
  3. Understand number bases.
  4. Practice designing simple algorithms.
  5. Get a gentle introduction to test-driven development.

Overview

You will create a function called mintf(…) that is similar to the standard printf(…) but in place of the usual format codes, your mintf(…) will support the following format codes:
%dinteger (int, short, or char), expressed in decimal notation, with no prefix
%xinteger (int, short, or char), expressed in hexadecimal notation with the prefix "0x"; use lowercase letters for digits beyond 9
%binteger (int, short, or char), expressed in binary notation with the prefix "0b"
%$float or double, expressed as currency with a dollar sign prefix, and exactly two digits to the right of the decimal point., Example: "$35.99"
%sstring (char* or string literal)
%ccharacter (int, short, or char, between 0 and 255) expressed as its corresponding ASCII character
%%a single percent sign (no parameter)
For each occurrence of any of the above codes, your program shall print one of the arguments (after the format) to mintf(…) in the specified format. Anything else in the format string should be expressed as is. For example, if the format string included "%z", then "%z" would be printed.

Your code will also handle \n and other backslash escapes, but you will not need to do anything special. The C compiler converts those for you. If this is confusing, just ignore it, and then try putting a \n in the format string when calling your mintf(…).

As an intermediate step, you will create a print_integer(…) function, which prints a single integer by itself, with no newline ("\n") at the end.

Prepare

Read this assignment description carefully, including the code quality standards.

This assignment will also entail a small amount of external reading. You can learn what you need from the textbook or from the web. The Resources page contains hand-picked online references that should be simple (and short) enough so that you can learn what you need without getting too bogged down.
variadic functions
A variadic function is any function that takes a variable number of parameters. You may have noticed how printf, unlike most C functions you have used, can take any number of additional parameters, after the initial format string. You don't need to know this deeply, but you will need to look up the syntax, and also understand how a program determines the number of arguments. See pp. 651-652 in the textbook or the links on the Resources page.
printf
Make sure you understand the %d, %x, %o, %s, and %c format codes. Try writing very small programs with them, and make sure you can predict the output. See pp. 18-22 and 493-497 in the textbook or the links on the Resources page. More detailed information can be found by typing man printf from bash.
#include
The #include directive is effectively replaced with the entire contents of the file it refers to. See pp. 90-91 in the textbook or the links on the Resources page.
#include guards
You will not need to use #include guards for this assignment, but you will see them in the header file (hw02.h). See the link on the Resources page.
number bases
For this assignment, you will need a good understanding of number bases. You will be writing code to express integers in a variety of number bases (e.g., binary, hexadecimal, etc.). However, most of the algorithms you see online will give you the digits from right-to-left. You will need to print them left-to-right. See pp. 134-137 in the textbook or the links on the Resources page.
fputc(…, stdout)
To print a single character (ch) to the console (stdio), use this code: fputc(ch, stdout); That's all you need to know, for now. Some may ask how this is different from putchar(…). It is equivalent, but please use the fputc(…) form. If you would like to know more about fputc(…), see p. 659 in the textbook, the link on the Resources page, or simply type man fputc from bash.

About using web resources

You are welcome to look at other code on the web, even if it does exactly what you need. However, the code you write must typed by you and composed by you. It should not be so close to something else that it looks like it was copied. Changing variable names or spacing is not enough to make it your own. What can you do if you see something that is a little too helpful? You have two options: (1) Take a walk or each lunch and let the idea gel in your head. Then, come back and re-express it in your own way. (2) Sketch out the underlying idea on a piece of paper in words or as a picture. Then use your sketch to write your own code.

This does not apply to elemental patterns that are so generic, that anybody doing the same task would have no choice but to use the same code. For example, everyone will have #include <stdio.h> and some form of va_list-va_start-va_arg-va_end pattern. Even then, we strongly recommend typing things by hand. You will learn much faster. Do not copy-paste from the web or anywhere else except for code that you wrote and own.

Instructions

Start by getting the files. Type 264get hw02 and then cd hw02 from bash.

You will find only one file: hw02.h. That is the header file. It defines the signature of two of the functions you will implement: mintf(…) and print_integer(…).

You will be creating three new files: hw02.c, hw02test.c, and hw02test.txt. There is no starter code (other than the header file).

hw02.c is where your mintf(…) and print_integer(…) implementations will go. To start this file, simply copy the two function signatures from hw02.h into a new file called hw02.c and then add the braces to make them into functions.

hw02test.c will contain a main(…) function that will test your mintf(…) and print_integer(…). It must exercise all of the functionality in those two functions. For this homework, that means that in the course of running your hw02test.c file, every line of code in your hw02.c should be executed at some point. To do this, you simply have several mintf(…) that include all of the format codes above, including a variety of valid values, including positive integers, negative integers, positive float values (for currency), negative float values, 0, strings, and an empty string. It should also have a few print_integer(…) statements to test that on its own. This will give you a very simple way to know for sure if your code works.

hw02test.txt will contain the expected output from running your tests in hw02test.c. It will be a simple text file.

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

Pro tips (optional)

  • bash, && – You can do this with a bash one-liner. To join commands, use the && operator in bash. With &&, the second command runs only if the first succeeded (i.e., no compiler warnings or errors). You can also send the output of your hw02test directly to diff using the pipe operator (|). Putting it all together, you get this:
    gcc -o hw02test hw02test.c hw02.c && ./hw02test | diff hw02test.txt -
    To repeat, use your up arrow key (in bash)
  • bash, alias – To avoid typing all of that, you can create an alias in bash. (This is one command, i.e., one line.)
    alias testhw2='gcc -o hw02test hw02test.c hw02.c && ./hw02test | diff hw02test.txt -'
    After that, you can simply type testhw2 in bash. To make it work in future sessions, add it to your .bashrc toward the end, after the other gcc alias that adds in other compilation flags.
  • vim, mappings – To do all of this from vim, you can map it to a key, such as F5. (Enter all of that as a single command in Vim.)
    :nmap <F5> :!gcc hw02.c hw02test.c --std=c99 --pedantic -g -Wall -Wshadow -o hw02test && ./hw02test | diff - hw02test.txt<CR>
    After that, you can simply press F5 from Vim. To make it work in future sessions, add it to your .vimrc.
  • diff, return code – A more precise way to verify that it passed—instead of visually looking at the output—is to type echo $? in bash. That displays the return code of the last thing you ran, in this case diff. If it is 0, then it means there was no difference, so your test passed. (At the end of any main function, you will usually see return 0, if there was no error. That is the return code.),
  • vim, splits – To work with multiple files at the same time, use :sp file.c to make a horizontal split, or :vs file.c to make a vertical split. Use F6 to move between splits (using the provided .vimrc). For example, to open all four files for this assignment at once, you could do the following:
    vim hw02.cfrom bash
    :sp hw02test.cin vim
    :vs hw02test.txtin vim
    F6in vim
    :vs hw02.hin vim
    The result will look like this:
  • vim, tabs – Tabs are another way to work with multiple files at the same time. To open all four files in separate tabs, use the -p flag when opening vim:
    vim -p hw02.c hw02.h hw02test.c hw02test.txt
    ... or more concisely using wildcards ...
    vim -p hw02*.*
    To open another file in a new tab while vim is already open, use :tabe instead of :e like this:
    :tabe hw02test.c
    Once the tabs are open, you can use [ and ] to switch between tabs or click with the mouse. (This last part with the brackets depends on using our sample .vimrc.)
    The result will look like this:

Test-driven development

The most efficient way to complete this assignment will be to follow a the test-driven development method.

First, you will need to set up a skeleton hw02.c, hw02test.c, and hw02test.txt. Your print_integer(…) and mintf(…) will do nothing. Your hw02test.txt will be empty. Therefore, the test procedure described above will appear to pass. Now, repeat the following 2-step process until the assignment is complete.

  1. Add a single test to your hw02test.c and hw02test.txt files. You will want to start with print_integer(…), so add a single call to print_integer(…) to your main(…) and a corresponding line in your hw02test.txt.
  2. Add just enough to your print_integer(…) so that it can pass that test. Hint: To print the digit 9, you can simply add the character literal for '0' to the number 9, i.e., '0' + 9. To the C compiler, there is almost no difference between '0' and the number 48 (ASCII value of '0'), so to that expression is the same as 48 + 9, which would give you the ASCII value for the digit '9'.
  3. Save a copy of your code by running 264submit hw02 hw02* every time you get a new part of the functionality working.

You will want to start with very simple test cases, e.g., print_integer(1, 20, ""). Then, add a test for something slightly more complicated (e.g., multi-digit number) and implement enough of print_integer(…) to make that work. Keep adding tests and the corresponding code until print_integer(…) handles everything.

Requirements

  1. Your submission must contain the following files: hw02.c, hw02test.c, hw02test.txt.
    • hw02.c – contains mintf(…) and print_integer(…), but not a main(…)
    • hw02test.c – main(…)
    • hw02test.txt – expected output of your hw02test.c if compiled and run with any correct hw02.c
  2. Your hw02test.c must exercise all functionality (e.g., all format codes, negative numbers, 0, etc.).
  3. 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.
  4. The only external functions/macros/symbols you may use in your hw02.c are fputc(), va_list, va_start, va_arg, va_end, and stdout.
  5. Do not call printf.
  6. Do not include any header file other than stdio.h, stdarg.h, stdbool.h, and hw02.h. For example, you may not use printf(…) in your hw02.c. However, you may use printf(…) (or anything else you like) in your hw02test.c.
  7. 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.
  8. All code must compile on ecegrid using the version of gcc installed there, using the flags in the provided .bashrc file, without any warnings or errors.
  9. Your code must run without crashing, for your test file and for any other valid test file. We might even test your hw02.c with other students' hw02test.c and hw02test.txt.
  10. For negative numbers, print the "-" before the prefix (e.g., "-$3.00" not "$-3.00", "-0x12AD" not "0x-12AD").
  11. All code must conform to the code quality standards. (Throughout the semester, we may check style automatically and/or manually, on some assignments but probably not all. However, for any given assignment, style-checking will be consistent across the class. When requesting help from course staff, it is expected that your code follow these rules. This will enable us to provide you with the most effective assistance.)

How much work is this?

Some parts of this—i.e., print_integer(…) and parsing the format string—might feel like a puzzle. Think it through before you start writing a lot of code. The rest will involve a modest amount of code. (The instructor's solution is 75 sloc* for hw02.c, and 18 sloc for hw02_test.c.) Expect to do a fair amount of debugging to get this right.
* sloc = "source lines of code" (excluding comments and blank lines)

Submit

To submit, type the following in bash:
264submit hw02 hw02.c hw02test.c hw02test.txt.

Q&A

  1. Is an int always in decimal (base 10)?
    No. See the next question.
  2. Does my print_integer need to deal with inputs that are specified in number bases other than decimal? For example, does my print_integer need to be able to convert from hex to binary?
    An int does not have a number base. It is just a value. The base (or "radix") of a number only refers to the way it is written, i.e., in your C code, or in the output of a program you write. For example, if n is the number of fingers on two hands, you could write that as 10, 0b1010, or 0xa, but it the same quantity no matter how you write it.

    When gcc reads your code for the first time, it interprets any integer literals (raw numbers in your code) according to how you write your code. Once your code has been compiled, the base that you used to write it becomes completely moot.

    Example: To gcc, the following are equivalent and have exactly the same effect.

    int n = 65;
    printf("n can be written as %d in decimal, as 0x%x in hexadecimal, or as %c if interpreted as an ASCII character.", n, n, n);

    int n = 0x41;
    printf("n can be written as %d in decimal, as 0x%x in hexadecimal, or as %c if interpreted as an ASCII character.", n, n, n);

    int n = 0b1000001; // non-standard, but accepted by gcc; do not use for ECE 264
    printf("n can be written as %d in decimal, as 0x%x in hexadecimal, or as %c if interpreted as an ASCII character.", n, n, n);

    They all print the following:

    n can be written as 65 in decimal, as 0x41 in hexadecimal, or as A if interpreted as an ASCII character.
  3. How do I print a string?
    Remember that a string is just an array of characters with a null character ('\0') at the end. Just start with the first character, and keep printing characters using fputc(ch, stdout) until you encounter the '\0'.
  4. What if there is a '\0' in the middle of the string?
    By definition, a string ends at the first '\0', so you would stop printing characters when you see the '\0'.
  5. Should I print the '\0'?
    No.
  6. What if mintf is called without any optional parameters?
    That is fine and normal. For example, the following should have the same effect:
    mintf("Hello world!\n");
    printf("Hello world!\n");
  7. What's the deal with variadic functions / va_arg / va_list?
    Make sure you read at least one of the suggested articles. If you don't get it after reading one, read the other. Also, try testing with one of the examples in those articles, and then modifying it to make sure you understand. (Pro tip: To paste code into vim via PuTTY without messing up the indenting, open vim, enter :set paste , right-click in PuTTY to paste the code, enter :set nopaste , and press Esc.)

    Here's a quick summary: A variadic function is any function that takes an unknown number of optional parameters. The optional parameters are represented by three dots (e.g., int foo(int n, ...)). Those dots are part of the C language. The optional arguments are accessed using va_arg. You must call va_start once in that function, before the first use of va_arg. You must call va_end once in the function, after the last use of va_arg. You must include stdarg.h at the top of the file (i.e., #include <stdarg.h>). There is no way to know from va_arg how many optional arguments there are, so you need to use some other information (in this case the format string) to know how many there are (and hence how many times to call va_arg).

    Again... read those articles and play with the examples if you are having trouble understanding.
  8. What are the parameters to print_integer?
    The parameters are declared in the hw02.h, with n being the number to print, radix being the radix (base) with which to print it, and prefix being an arbitrary string to print just before the first digit. For example, print_integer(-13, 10, "yo") would print "yo13" (without quotes). print_integer(-13, 10, "yo") would print "-yo13". print_integer(13, 16, "cranberry") would print "cranberryc". The value of n may be any valid value for an int on the platform where your code is running. The radix may be any integer between 2 and 36 (inclusive).
  9. Should hex digits above 10 be uppercase or lowercase?
    They should be lowercase, just like printf.
  10. The specification states, "For each occurrence of any of the above codes, your program shall print one of the arguments (after the format) to mintf(…) in the specified format." What does this mean?
    It works just like printf. For each format code (e.g., "%d", "%x", etc) other than "%%" in the format string, your mintf should take one of the optional arguments and print it in the specified format. For example, printf("I knew that %s was %d%% Martian because of the %$ tab for breakfast.", "Mason", 99, 67.8) would print "I knew that Mason was 99% martian because of the $67.80 tab for breakfast.".
  11. The specification states, "For each occurrence of any of the above codes, your program shall print one of the arguments (after the format) to mintf(…) in the specified format." What does this mean?
    It works just like printf. For each format code (e.g., "%d", "%x", etc) other than "%%" in the format string, your mintf should take one of the optional arguments and print it in the specified format. For example, printf("I knew that %s was %d%% Martian because of the %$ tab for breakfast.", "Mason", 99, 67.8) would print "I knew that Mason was 99% martian because of the $67.80 tab for breakfast.".
  12. The "0x" prefix for hexadecimal and "0b" prefix for binary output seem somewhat arbitrary. Where do these come from?
    These prefixes mimic the formats that gcc accepts when you specify raw integers in your code. The following statements are completely identical in the eyes of gcc.
    int n = 10;
    int n = 0xa;
    int n = 0b1010; // non-standard, but accepted by gcc; do not use for ECE 264
  13. Why do we need the prefix parameter to print_integer?
    In a sense, it is a convenience. When printing a negative number that requires a prefix, the prefix should go between the minus sign and the first digit. For example, mintf("%$", -12.3) should print "-$12.30" not "$-12.30". Similarly, mintf("%x", -10) should print "-0xa" not "0x-a". Since your mintf will most likely depend on your print_integer, having this prefix parameter to print_integer will make your mintf code simpler than it would be otherwise. If this still doesn't make sense, just think through how you would structure your mintf. You will most likely find that the only reasonable way to do it is to have a separate function like print_integer.
  14. What is the relationship between print_integer and mintf?
    Your print_integer will most likely be used by your mintf. For that reason, you may want to write your print_integer first.
  15. Will my print_integer be called directly by external code?
    Yes.
  16. Should I implement or print_integer or mintf first?
    You probably want to write your print_integer first. When that is done, then write your mintf. However, it might also make sense to get your mintf working for a simple hello world (e.g., mintf("Hello World!\n")).
  17. How do I handle \n?
    See the note in the assignment description about this. If your code works for everything other than \n, then it should work for \n, too. You shouldn't need to do anything special. When gcc reads your code, it will automatically interpret the \n as ASCII character 10 (newline).
  18. What are we turning in?
    The specification (above) lists three files. Your hw02test.txt is just a plain text file. When you compile and run your hw02test.c file, the output should match your hw02test.txt file exactly. It should work the same, even if we substitute in the solution hw02.c or another classmates' correct hw02.c.
  19. How do I deal with a float for the "%$"?
    Here's a hint: To extract the integer portion of a float (e.g., 3.5 ⇒ 3), put (int) before the float. For example, the following code prints 3.
    float f = 3.5;
    int n = (int) f;
    printf("%d", n);
    Also note that for the "%$" format code, you print exactly 2 decimal digits. For example, mintf("%$", 1.2) would print "$1.20". This makes things much easier for you than they could be otherwise.
  20. What if someone passes an integer instead of a float or double for "%$"?
    It will probably work, but I'm not 100% positive about that. In any case, you may assume that the type of parameters will match the corresponding format code.
  21. Is an int the same as an "integer"?
    When we say int, we mean the C data type, which has a limited range (-2,147,483,648 to 2,147,483,647 on many 64-bit systems, but you may not assume those particular bounds). An "integer" is a mathematical concept; it is the same as a whole number.
  22. What if my output doesn't match up with someone else's output perfectly?
    It should. This assignment is tightly specified. If everyone creates an adequate tester (hw02test.c) and a correct implementation (hw02.c), then
  23. What radixes (number bases) must my print_integer support?
    Your print_integer should support any value for radix between 2 and 36 (inclusive). For radixes above 10, it should represent digits beyond than 9 with lowercase letters. For example, print_integer(11, 16, "") would print "b" because b is the (11-9)th lowercase letter of the alphabet. Similarly, print_integer(20, 21, "") would print "k" because k is the (20-9)th letter of the alphabet.

    This converter may help you get a sense (and test your code).
  24. What is the difference between a "radix" and a "base"?
    They are synonyms for the same thing.
  25. What should my program do if someone passes the wrong number of parameters to mintf?
    You may assume that the caller will only pass the correct number of parameters. There will be no penalty if your mintf crashes or prints garbage in case there aren't enough parameters for the given format string.
  26. What should my program do if the type of a parameter does not match the format code (e.g., mintf("%d", "birthday")?
    You may assume that the type of parameters will match the corresponding format code. There will be no penalty if your mintf misbehaves in case of such a mismatch.
  27. Should I write #include "hw02.h" (quotation marks) or #include <hw02.h> (angle brackets)?
    Use quotation marks for header files in your project. Use angle brackets for standard header files. Thus, you should start with this:
    #include <stdio.h>
    #include "hw02.h"
    (You will need to include one or two other standard headers.)
  28. May we modify hw02.h?
    No.
  29. May we turn in our own hw02.h?
    Yes, you may turn it in, but that file will be ignored. We will copy your files into an empty directory and then copy our test files, including hw02.h on top. Thus, any hw02.h you submit will be overwritten.
  30. How does mintf relate to printf?
    Your mintf will have the same behavior as printf for the format codes "%d", "%c", "%s", and "%%". For "%x" your mintf is different in that yours will automatically add the "0x" prefix, whereas printf does not. For example, mintf("%x", 10) prints "0xa" whereas printf("%x", 10) prints "a". In addition, your mintf will support two format codes that printf does not support at all: "%b" and "%$".
  31. What types can I use with each format code?
    For %d, %b, or %x, you would normally pass an int, although a char or short will also work. For %$, pass a float or double. For %s, pass a string. For %c, you would normally pass a char (e.g., 'c') but an int or short will also work, as long as they are in the range of 0 to 255 (inclusive). For %%, you would not pass any argument. This is implied by the table at the top of this assignment description.

    The underlying rule is explained more explicitly here, in one of the two recommended articles. For most people, these details shouldn't be necessary. If you were thinking of trying unsigned types or long types, don't.

Updates

9/5: Correction: print_integer(...) *does* include the prefix.

9/5: Correction: fputc(...) takes stdout for this assignment, not stdin or stderr.

9/5: Added a Q&A with 30 questions/answers.

9/7: Added Q31 to the Q&A, to clarify the types; added the same information and another minor clarification ("use lowercase letters for digits beyond 9" for %x) to the table in the Overview above

9/11: Changed % to hw02.c hw02test.c in the vim mapping in the pro tip. (In most vim commands, % is replaced by the path to the current file. As it was, it would compile only with the current file, and would never have both hw02.c and hw02test. Thus, it would completely fail to compile.)