C Introductory Lab

Table of Contents

This assignment is to be done individually. You can talk to other people in the class, me (Dave), and any of the course staff (graders, lab assistants, teaching assistants, prefects) for ideas and to gain assistance. You can help each other debug programs, if you wish. The code that you write should be your own, however, and you shouldn't directly share your code with others. See the course syllabus for more details or just ask me if I can clarify.

Update all of the git cloning text; I've done my exercises in a blank copy of the Scheme repl for the heck of it

This is an individual assignment.

This lab is broken up into two halves. There are C programming activities listed further down called "Exercises"; the first half of the lab is Exercises 1-4, and the second half is Exercises 5-9.

1 Part 1

1.1 Make sure GitHub is ready for use

Before getting started with C programming, get set up with GitHub to submit your code. Do the following:

  • Make sure that you have completed the GitHub invitation.
  • Visit our course organization dashboard; directions on how to do so are again in the GitHub invitation assignment. Once you've landed at the dashboard, on the right hand side of the screen, in a list called "Repositories you contribute to," you should see one called username-c_lab. (username is your own username, term is this term). Contact me right away if the repository isn't there.

1.2 Clone the repository

For all of the Git work that we'll be doing, you've got a choice as to which Git tool you'd like to use. Some folks use the command-line. Some use tools such as GitHub Desktop. I've got my own Git client called Elegit which I'm developing. You're free to use any one that you like. For brevity, I'm going to include command line instructions here.

If you are using using COURSES to share a directory between your own computer and mirage, it seems to me that you'll want to make sure that you do all of your Git commands always on the same computer. In other words, don't sometimes do Git commands on mirage, and sometimes on your own computer. Which one should you choose? I think it doesn't matter much. I guess I've got a weak recommendation for doing it on mirage since Git is already installed there anyway.

In your web browser, go to the GitHub course dashboard, then click on the repository titled username-c_lab. This should take you to the home page for that repository. Find the box on the page that shows you the link for your repository, and make sure "HTTP" is selected instead of "SSH". (You can use SSH if you want to figure out how to set it up on your own. HTTP is easier to configure.) Highlight the https link shown, and copy it to the clipboard. The link that you're copying should look something like

https://github.com/carleton251-term/username-c_lab.git

… though your username and term will be different.

Then connect to mirage, and navigate to an appropriate folder that you wish your repository to live in. Then type "git clone", then a space, then paste in the URL from the github page that you copied from above. For example, I would type the following, though your username and term will be different:

git clone https://github.com/carleton251-term/yourgithubusername-c_lab.git

If all goes well, you should receive an empty directory, ready for your code.

1.3 Add a file to the repository

  1. To make sure that Git and GitHub are up and running, we will create a file and add it to the repository.
  2. Create a readme.txt file in the directory that you cloned from GitHub (presumably named username-c_lab). Put some kind of text in that readme.txt file. Then issue the following two commands on mirage:

    git status
    git add readme.txt
    

    The first command should show you that you have a new file that is not yet tracked by git. The second command will add it for tracking.

  3. Next, issue these two commands:

    git status
    git commit -am "I just added a readme.txt file."
    

    The first command should now show that you have a file with changes ready to be committed. The second command will commit the changes to your local repository. If this is your first time using git, you may receive an warning that your name and email address were configured automatically. To make that message go away in the future, enter in the two git config messages that it will show you.

  4. Once you have successfully added your file and committed it to the local repository, push it to GitHub by issuing the following command:

    git push
    

    This tells git to push your changes to GitHub. If this is your first time pushing, you might get a warning message claiming that "push.default is unset." If so, follow the recommendation it provides to "adopt the new behavior now." and copy and paste

    git config --global push.default simple
    

    into the terminal window. Then try

    git push
    

    again, and hopefully all will go well. If it works, go back to GitHub in your browser, and within your repository, click on the readme.txt file. You should hopefully be able to see your file there.

    Note: if this is your first time pushing, you may have to instead type

    git push origin master
    

    If pushing doesn't seem to work, try it instead. You should only need to add "origin master" the first time.

  5. Hopefully the above all works as expected; if not, ask for help before going further.

1.4 Running your first program, and pushing to GitHub

Create a new file in Repl.it called hello.c. Then, copy and paste into the Repl.it hello.c file the content from this linked hello.c file. This program (unsurprisingly) is supposed to print "Hello, world!" to the screen.

Then add it to the repository by issuing the following commands into the Repl.it terminal window:

git status
git add hello.c
git commit -am "Adding my new hello.c"

Next, compile the program to a binary executable by running the following in the terminal:

clang -o hello hello.c

This command should produce a file named hello. You should be able to see it in the file bar on the left in Repl.it, and also if you type ls in the terminal. Try running it at the command prompt:

./hello

If this worked, you should see "Hello, world!" output to the terminal.

EXERCISE 1: Change the program to print

Hello, <your name>!

Run the program again. When complete, you should commit and push your updates to GitHub. See instructions above. Verify that you can see your updates on the GitHub website.

1.5 Printing

The first non-commented line of hello.c is: #include <stdio.h>. This directs the C compiler (or technically, a subprocess called the C preprocessor) to include a header file named stdio.h that lists the signatures of standard IO functions, such as printf.

The documentation for most of the built-in C functions can be found in the UNIX manual ("man") pages. Take a look at the documentation for printf in particular. It tells you which header file to include to get access to that function (in this case, stdio.h), as well as documenting the interface and behavior of the function of interest. In this case, the manual page also includes several similarly-named functions such as sprintf and fprintf. Read through the first few paragraphs the man page for printf, and skim through the rest so you get a sense of what these pages look like. There are examples at the end, which is often the most useful part.

printf is similar to Python's string formatting operator, in that it accepts a string that includes format specifiers to print integers, floats, etc. To see more about how printf and other operations work, create a file in Repl.it called printing.c, and then copy and paste in the contents from this printing.c file. It contains within a number of different examples of print formats. To compile and run your code, go back and look at how we did it for the hello.c program, and change accordingly.

Here are some common C types and their printf formatting specifications:

Format specifier Type
%c char
%i or %d int
%li or %ld long
%f float
%s string (really a char *)
%p pointer (e.g. int *

EXERCISE 2: Add code to printing.c that subtracts the value 15 from 87 and prints the result, together with an appropriate message. Add your file with Git, commit your updates, and then push your changes to GitHub. Verify that you can see your updates on the GitHub website.

1.6 User Input

You can get user input using the scanf function (the manual page is here):

int i;
scanf("%i", &i);
printf("You entered: %i\n", i);

The first argument to scanf is a string containing formatting specification(s) for the type of input expected. In this case we expect an int.

The second argument should look pretty weird to you. How is scanf able to modify the value of i? What's that ampersand symbol? The answer is that we're actually passing a pointer to i's location, not i itself - so the value of i can be modified from within scanf. There will be much more on pointers later.

EXERCISE 3: Write a program temperature.c that asks the user for a temperature in Fahrenheit and prints the temperature converted to Celsius. Assume that the input is handled as a float, instead of as an int. The relevant forumla is:

temp_c = (temp_f - 32) * 5/9

For example, here's what a sample run of temperature.c might now look like:

What is the temperature in degrees Fahrenheit? 42.5
42.500000 degrees Fahrenheit is 5.833333 degrees Celsius.

(If you're interested: there are variants on the formatting specifiers that limit the number of zeroes. Read up on that further if you like.)

Add, commit, push to GitHub, and check on GitHub that your file made it there.

1.7 Loops and if statements

C has basically the same syntax for if statements, for loops, while loops and do/while loops as Java. However, there is a critical and very subtle difference with if statements that you should be aware of. C doesn't have boolean variables, exactly. It just has integers, so false is 0 and true is any non-zero value. So the following abomination is legal C code:

int x = 1;
int y = 0;
if (x - y) {
    printf("You entered two different numbers!\n");
} else {
    printf("You entered the same number!\n");
}

If you want to make your code more clear, you can use the library stdbool.h as follows:

#include <stdbool.h>

int main() {
    bool x = true;
}

However, this is just syntactic sugar, which is a fancy phrase which means that we haven't changed the underlying language, we've just changed the syntax a little bit. With the above, x is really just an int, and true is really just a 1. Why does this matter? Try running the following BUGGY code and see what happens. (You can create a new file for this called buggy.c for it.)

#include <stdio.h>

int main() {
    int x = 3;
    if (x = 5) {
        printf("x must be 5, even though I assigned it to 3.\n");
    }
}

Happily, clang does try to warn you when you do this.

EXERCISE 4: What happens if the user enters an impossible temperature in Exercise 3? Absolute zero, which is the coldest temperature anything can possibly be, is -459.67 Fahrenheit. Modify temperature.c so that if a temperature lower than that is input, an appropriate error message is displayed. Use git to add, commit, and push to GitHub when complete.

Exercise 4 is the last exercise that is due in the first part of this lab. You have now pushed a number of commits to GitHub, and you'll follow with more for the remaining exercises. The graders and I need to know which commit is the one that we should use for grading. This is handled well in Git with tags. Here's how to do it at the command line:

git commit -am "All my changes are committed, I may have already done this"
git push
git tag firsthalf
git push --tags

Then look on GitHub after the last push, and click where it says "X commits" (where X is a number). That will show you the commit history. If you click on your most recent commit, you should be able to see the tag listed there.

You should wait to tag until you're sure you have committed the version you want us to grade. That said, in the unlikely event that you goof and realize that you want to commit a newer version for us to grade, you'll need to use another tag. Reusing the same tag for a different commit is generally a really bad idea. If you need to tag another commit, append a ".1" (or whatever version number you're up to. Make sure you first add, commit, and push your commit before tagging.

Here's a long tangent. Note that GitHub displays the tags wrong. In Git a tag is associated with only a single commit, but GitHub will show you that same tag if you click on all commits that precede it. (Note that this is making a distinction between Git, the local software tool, and GitHub, the webserver.) GitHub is trying to be helpful by showing you all commits that contribute to a release, as that's how tags are often used, but GitHub is ultimately misrepresenting what a tag in Git actually is. The command line can show you a much better representation of your Git history, tags included. To do this, I recommend making a custom git log command. Copy and paste the following, if you like, to create a new command to show your git graph well.

git config --global alias.lol "log --graph --decorate --color --pretty=format:'%h %Cred%d%Creset %ad %ae %n%s%n' --abbrev-commit --date=local --all"

Then issue the following command to make it work:

git lol

2 Part 2

2.1 Guess a number

EXERCISE 5: Write a program guess.c that picks a random number between 1 and 100 and has the user repeatedly guess the number in a while loop. For random numbers, use the random function. Here is a sample usage:

#include <stdlib.h>
#include <time.h>

...

srandom((unsigned int)time(NULL));
long num = random();

A run of your program might look like this:

Guess a number: 50
Too low! Guess again: 75
Too low! Guess again: 87
Too low! Guess again: 93
Too low! Guess again: 97
Too high! Guess again: 95
Too high! Guess again: 94
Correct! The answer was 94.

Add, commit, and push to GitHub when complete.

2.2 Arrays

You can declare an array in C by giving the type and size of the array. See arrays.c for a sample program: an explanation of some of it follows here.

int array[10];

This allocates space (on the stack) for an array containing 10 integers.

Like Java, you can read and write elements of the array using [] notation.

for (int i = 0; i < 10; i++) {
    array[i] = i;
}

Unlike Java, there's no reasonable way to find out the length of an array, so you need to keep track of an array's length yourself. And if you write past the end of an array, there's no check or "out of bounds error" - it will just modify whatever happens to be next in memory! Try uncommenting the line:

array[10] = 5;

in arrays.c and observe how the value of x changes. Note that clang issues a warning that you're doing a bad thing, which you are, but the program should still run. What does this imply about the layout of the variables in memory? (Make sure that you are executing this on Repl.it; if you're using a Mac and compiling it locally somehow, you won't see the issue. This is forcing a bug to happen in a way that's platform dependent.)

EXERCISE 6: Add code to arrays.c to calculate the sum of the array and print it. Add, commit, and push to GitHub when complete.

2.3 Pointers

The variable array is actually a pointer to the first element of the array. A pointer is a special type containing the address in memory of some data. You can use pointer arithmetic to access the elements in the array.

printf("Array address: %p\n", array);
for (int i = 0; i < 10; i++) {
    printf("Array element at address %p: %i\n", array + i, *(array + i));
}

Here the expression array + i gives the address of the i-th element in the array. The expression *(array + i) retrieves the integer value stored at that address.

Here's a quick summary of C's basic pointer operations (we'll discuss more). &E evaluates to the address of an expression E. *p gives the value stored at the location pointed to by p. p must have a pointer type, indicated by a * in the type declaration. For example, int * is the type representing a pointer to an int. So if p has type T *, then *p has type T.

As an example, read and then run the program pointers.c. Try drawing the execution of the program on paper, with boxes for memory locations and arrows for pointers. Can you see how we end up with the final values for a and b? Hopefully it also makes more sense why we pass arguments to scanf using the & operator.

EXERCISE 7: Write a program stack.c that illustrates whether the the program stack is growing "up" or "down." In other words, when one function calls another, are the new stack frames (i.e., the memory allocated for local variables) allocated at higher or lower addresses than old stack frames? You should NOT assume that variables in a single function are allocated in the order they're declared. You can define a new function much like you would define a new method in Java; for example, here is a function that takes a string and returns nothing:

void foo(char *s) {
    ...
}

In your code, make sure to include a comment with your conclusion, i.e., indicate if new stack frames are allocated at higher or lower memory addresses than old stack frames.

Add, commit, and push to GitHub when complete.

2.4 Structs

C does not have classes or objects. However, you'll often run into situations where you want to group related values together. For this purpose, you can create a struct, a special kind of user-defined type.

Structs are defined using the keyword struct, a name for the struct, and a list of member variables within curly braces. For example, here's a struct to represent a student (see student.c for the full code listing):

struct Student {
    char *first_name;
    char *last_name;
    int id;
};

You can create an instance of a struct type by declaring it with the struct keyword, and access member variables using the dot (.) operator. See student.c for details.

EXERCISE 8: Create a new file complex.c. In this file, add a struct type struct Complex containing two doubles representing the real and imaginary parts of a complex number. Put in some test code to show that you can display a complex number as output. Add, commit, and push to GitHub when complete.

EXERCISE 9: Add a function multiplyComplex to complex.c that takes two complex numbers and returns a new complex number representing their product. Note that if you have two complex numbers \(c_1\) and \(c_2\) whose real parts are \(a_1\) and \(a_2\) and whose imaginary parts are \(b_1\) and \(b_2\), respectively, then the real part of their product is \(a_1 * a_2 - b_1 * b_2\), and their imaginary part is \(a_1 * b_2 + a_2 * b_1\). Include code testing your multiplication function in main. Commit and push to GitHub when complete.

As you did at the end of Exercise 4, again create a tag to indicate the commit that we should grade. Use a tag of secondhalf; look back after Exercise 4 if you need a reminder on how to tag a commit. Make sure to push the update properly (remember, a normal push doesn't push tags, you need to do that additionally), and check on GitHub to make sure that it received your updates.

3 Turning in your work

If appropriate, also include a file credits.txt listing anyone other than me who helped you with the assignment, and any websites, books or other sources you used for reference. Please try to be thorough; in particular, if you had more than a passing interaction with another student, list them.

Instead of using Moodle, we're using GitHub as the submission mechanism. Check again in GitHub again to verify that your work has made it up there.

This lab was written and modified over multiple iterations by Laura Effinger-Dean, Andy Exley, Jed Yang, and Dave Musicant, with help from Kochan's Programming in C.