2023-01-16
This technical article is written to reflect on the building process of a command-line blackjack game I wrote in C. This is my first project in C, so I will write about experiences I had building my first project in C.
This technical article is written to reflect on the building process of a command-line blackjack game I wrote in C. This is my first project in C, so I will write about experiences I had building my first project in C.
The instructions from the class course material I followed to build this provided detailed specifications for the project, so there won’t be any discussions on design choices.
In C, a function is, by default, not callable before its definition. We run into a compile-time error in the code below because we call the greet
function before we define it:
#include <stdio.h>
int main() {
greet("World");
}
void greet(char *name) {
printf("Hello, %s\n", name);
}
We get this output if we try to run this:
$ gcc -o code code.c && ./code
code.c:4:5: error: call to undeclared function 'greet'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
greet("World");
^
code.c:7:6: error: conflicting types for 'greet'
void greet(char *name) {
^
code.c:4:5: note: previous implicit declaration is here
greet("World");
^
2 errors generated.
One way to handle this is to include a function prototype for our function at the top of our file. A function prototype specifies the return type, the name of the function, and the types of the function’s arguments.
Here is our modified code that includes a function prototype:
#include <stdio.h>
void greet(char *); // the prototype of greet
int main() {
greet("World");
}
void greet(char *name) {
printf("Hello, %s\n", name);
}
Our updated code containing the prototype compiles and runs successfully:
$ gcc -o code code.c && ./code
Hello, World
I do not enough about how compilers handle function prototypes to explain why the compiler is unable to compile code where a function is called before it is defined and why function prototypes allow compilers to compile the code, even when the body of the function comes after its usage. These links might provide some context. Header files are a helpful post-function prototype read.
Function documentation provides context that allows an IDE to display a brief description of a function when someone hovers over the name of the function. This allows someone working with a function to understand how to use a function and what it does without having to read through the definition of the function.
To write documentation for a function in C, place a multi-line comment directly before a function definition.
/*
This function prints the value of card. If it recieves an invalid
card number, it returns -1.
Parameters:
- card_number: The number of the card whose value is to be evaluated.
Returns:
The value of a card or -1 if an invalid card number is received.
*/
int card_value(int card_number)
{
// ...
}
This allows the IDE to show a tooltip like this, when someone hovers over card_value
.
The size of this project was not complex enough to make the time spent documenting functions very worthwhile. But function documentations turned out to be useful thinking pads for deciding what values a function should return.
For some information on best practices on writing documentation for C code, see this article. JavaScript programmers might be interested in JSDoc, a JavaScript equivalent of this.
Scanf
The scanf
function is used to accept input from the standard input stream. The code below reads two strings from standard input and prints the sting to the console each time:
#include <stdio.h>
int main() {
char word1[10], word2[10];
scanf("%9s", word1); // here, we specify a maximum width.
printf("%s\n", word1);
scanf("%9s", word1);
printf("%s\n", word1);
}
The code works as shown below when run:
$ gcc -o code code.c && ./code
hello
hello
world
world
scanf
can also be used to accept inputs of other kinds. It can be used to receive integer, floating point, or character inputs. To receive input of other types, replace %s
with the relevant format specifier for the input type you want to receive.
When we write code similar to our previous code that reads and writes to a character, we notice something unusual. Here is the code for this:
#include <stdio.h>
int main()
{
char char1, char2;
scanf("%c", &char1);
printf("%c\n", char1);
scanf("%c", &char2);
printf("%c\n", char2);
}
We expect this to prompt us for two strings and, each time, print the string to the console. When we run this, however, we notice a different behavior:
$ gcc -o code code.c && ./code
a
a
The application only prompts us once, instead of twice.
This distinctive behavior led to unexpected behavior when I was writing a user
function in the Blackjack project.
In the code block below from the project, we give the user the option to draw a card or stop drawing cards. When a user enters y
to hit, however, the application sometimes drew a card and printed Sorry I didn't get that
to the console immediately after, instead of allowing the user to choose whether they wanted to draw another card.
while (user_hand_value < 21 && should_keep_drawing)
{
printf("You have %d. Hit (y/n)? ", user_hand_value);
scanf("%c", &choice);
if (choice == 'y')
{
extra_hand = draw_card();
user_hand_value += card_value(extra_hand);
}
else if (choice == 'n')
{
should_keep_drawing = 0;
}
else
{
printf("Sorry I didn't get that.\n");
}
}
The reason for this behavior is that the enter keypress that is made after entering a character is added to the input buffer. So the next time that scanf
wants to read from the standard input, it finds a newline character in the input buffer and, without waiting for any user input, reads that.
This is the same thing that happened in our minimalist reproduction of this behavior. After we entered an “a”, our application printed the “a,” read a newline from standard input, and printed a newline, without waiting for a user input.
One approach to resolve this is to keep accepting characters from standard input until we get past the new line characters. Here is doing this looks like:
while (user_hand_value < 21 && should_keep_drawing)
{
printf("You have %d. Hit (y/n)? ", user_hand_value);
scanf("%c", &choice);
// keep reading characters till we get past new lines
while (choice == '\n')
{
scanf("%c", &choice);
}
if (choice == 'y')
{
extra_hand = draw_card();
user_hand_value += card_value(extra_hand);
}
else if (choice == 'n')
{
should_keep_drawing = 0;
}
else
{
printf("Sorry I didn't get that.\n");
}
}
An alternative, concise way to achieve the same behavior is to add a space before the character format specifier.
while (user_hand_value < 21 && should_keep_drawing)
{
printf("You have %d. Hit (y/n)? ", user_hand_value);
scanf(" %c", &choice); // notice the space before %c
...
}
Overall, the process of building this project in C was not technically difficult. The main benefit I gained from building this was experience with working with C control structures and standard input and output functions.