top of page

Harvard's CS50x Week 1: An Introduction to the World of C


In Week 0, we looked at the fundamentals of programming visually through the lens of Scratch.


This week’s lecture, however, will tackle amongst the most infamous of programming languages – C.


So why do we begin our journey with such a challenging language?


In Professor Malan’s words, “C is just about as close to a computer's hardware as you can get before you have assembly language.”


It is because of C’s slightly arcane nature that we can gain a better understanding of what goes on ‘underneath the hood’, dealing with things like memory and security that are usually abstracted away for us.


Once again, this guide will be split into two parts: my personal, simplified notes and observations on the course content, and my experiences with the introductory problem set.


Lecture Notes


Learning how to write code is one thing, but writing good code is an entirely different beast. We can measure code by the following:


  • Correctness – the accuracy of the output

  • Design – the efficiency of the code

  • Style – the consistency and aesthetics of the code


With this in mind, let’s break down Week 1.


An Introduction to VS Code and Compilers


Much like with language barriers, computers and humans face an obvious gap in communication – humans write in source code, human-readable syntax, while machines only understand binary, or machine code.


This begs the question, how do both parties understand one another? Think of a special piece of software, called a compiler, like a translator.


Compilers can convert source code, like C, into machine-readable code.





Visual Studio Code is the compiler and IDE (integrated development environment) used throughout CS50x. VS Code is divided into 3 separate sections:


  • File editor, where all files are stored

  • Text editor, where code can be written

  • Command line interface (CLI), or terminal window, where commands can be sent to the computer


Using VS Code, we can construct our first program in C – the conventional and slightly comical ‘hello, world’.


In the terminal window, start by typing ‘code hello.c’, creating a new file ‘hello’ which will appear in our file explorer, equipped with the ‘.c’ extension.


Then, within the text editor, write the following code:





Note that within the terminal window, we’ve:


  • accessed the ‘hello.c’ file using the command ‘cd’ (change directory)

  • compiled the file using the keyword ‘make’

  • executed the program using ‘./hello’


C and the Building Blocks of Programming


Much like Scratch, every programming language is built upon the same foundational building blocks.


In C, along with most other traditional coding languages, these blocks take the form of functions, variables, conditionals, and loops. Let’s take a closer look at each.


Speaking of parallels with Scratch, the C function ‘printf()’ works in a similar way as Scratch’s ‘say’ function: within printf()’s parentheses, an argument is passed (in our case, ‘hello, world\n’) and the program produces this argument. 


The only major difference between the languages is, evidently, the syntax: in C, our code statement had to be closed off with a semicolon and used this seemingly odd ‘\n’, which really only creates a new line after ‘hello, world’ is printed, for its aesthetic value.


#include <stdio.h>


int main(void)

{

    printf("hello, world\n")

}


Running ‘make hello’ on this code snippet will lend you numerous errors!


If you haven’t already spotted it, the error here is the missing semicolon after the print statement. You can imagine why lower-level languages like C are notorious for this reason!


Another thing worth mentioning is that ‘#include <stdio.h>’ tag at the top of the file.


While it might seem complicated, all it really does is tells the compiler to recognize certain user-built functions within the stdio.h library, abstracting away some of the lower-level details.


Libraries exist so that programmers don’t need to reinvent the wheel every time they want to perform common tasks, such as printing something to the terminal.


Recall that in Scratch, we could ask the user for their name and essentially store that value for later use.


Similarly, in C, we can use variables to store data. Let’s modify our ‘hello, world’ program using variables!





Using the cs50.h library, we can access the ‘get_string()’ function, which retrieves a string from the user.


Our variable name is like a holding place for our data. Note that because name is of type string, we have to use the placeholder ‘%s’ within our printf() function, and then pass name afterwards to prevent our program from printing literally ‘hello, name’.


String, in fact, is just one of several data types that we can assign to variables, a few others being int, bool, and char.


Another metaphor ‘brick’ you might have encountered while traversing Scratch was conditionals if this a condition is met, perform a certain action; if not, perform a different action.


Malan demonstrates conditional statements by comparing two integers and printing the result of their comparison:


#include <cs50.h>

#include <stdio.h>


int main(void)

{

    int x = get_int("What's x? ");

    int y = get_int("What's y? ");


    if (x < y)

    {

        printf("x is less than y\n");

    }

    else if (x > y)

    {

        printf("x is greater than y\n");

    }

    else

    {

        printf("x is equal to y\n");

    }

}


Notice that instead of strings, the variables x and y store integers retrieved using CS50’s ‘get_int()’ function.


The syntax for conditionals is slightly trickier than for variables or functions:

  • If statements compare data using logical operators (<, >, ==, etc.)

  • Else-if statements run only when the original if statement evaluates to false

  • Else statements are like catch-alls for conditionals, producing an output when all preceding if and else-if statements evaluate to false

  • Note that else-if and else statements cannot be used without an if statement as a precursor


In order to prevent unnecessary repetition in code, which is considered bad design, multiple conditions can be checked at once using ‘or’ and ‘and’ operators. These are denoted by ‘| |’ and ‘&&,’ respectively, in C. Let’s take another example:


#include <cs50.h>

#include <stdio.h>


int main(void)

{

    // Prompt user to agree

    char c = get_char("Do you agree? ");


    // Check whether agreed

    if (c == 'Y' || c == 'y')

    {

        printf("Agreed.\n");

    }

    else if (c == 'N' || c == 'n')

    {

        printf("Not agreed.\n");

    }

}


This code snippet checks whether our variable c, with data type char or character, represents ‘yes’ or ‘no.’ Of course, we could use multiple if statements to check if c holds the value of ‘Y’ and ‘y,’ and then ‘n’ and ‘N’; however, using ‘| |’ saves time and improves design.


The final building block of programming in C is loops, which execute the same lines of code again and again, much like repeat blocks in Scratch.


Let’s assume that we want to print out ‘hello’ to the terminal exactly three times. We could always simply perform ‘printf()’ thrice, but that would take a toll on our design, increasing repetition and reducing efficiency.


#include <stdio.h>


int main(void)

{

    printf("hello\n");

    printf("hello\n");

    printf("hello\n");

}


Now, what if we wanted to display ‘hello’ 100 times? 1,000 times? Hand-typing each print statement would make for a tedious task – that’s where loops come in.


#include <stdio.h>


int main(void)

{

    int i = 0;

    while (i < 3)

    {

        printf("hello\n");

        i++;

    }

}


Let’s walk through how this program works. First, a variable i of type integer is set to zero (which, remember, is the convention in programming).


Our while loop will run as long as i < 3. In its first iteration, the loop checks whether 0 < 3, which we know to be true.


Then, ‘hello’ is printed, and i increments by 1 (note that i++ is a shorter way of writing i += 1, which adds 1 to the current value of i).


This loop will run twice more until i == 3. Since the statement 3 < 3 is false, the while loop will terminate.


Believe it or not, we can use another type of loop to further improve this program’s design. With a for loop, we can condense this six-line loop into just four!


#include <stdio.h>


int main(void)

{

    for (int i = 0; i < 3; i++)

    {

        printf("hello\n");

    }

}


Through this method, we’ve defined i, compared it to 3, and incremented it by 1, all in a singular line!


In Scratch, we were exposed to forever blocks, which perform an action forever… or at least until the loop is broken. These can also be replicated using loops in C:


#include <cs50.h>

#include <stdio.h>


int main(void)

{

    while (true)

    {

        printf("hello\n");

    }

}


Because true will always be, well, true, this loop will never terminate.


Unlike in Scratch, which comes with some precautions against infinite loops, in C you may be at risk of losing control of your terminal window, or more extremely, memory leakages.


If this ever happens, Ctrl+C is your friend!


Handy Command-Line Commands


  • cd - changes the current directory or folder

  • cp - copies a file or directory

  • ls - lists all files in a directory

  • mkdir - makes a new directory

  • mv - moves or renames files and directories

  • rm - removes files

  • rmdir - removes directories


A Window to Problem Set 1: Mario


With a conceptual idea of the basics of programming in C, why don’t we apply these ideas to something slightly more practical – a video game! Or at least the beginnings of one.





How could we replicate these four blocks within this mario game using good design?


First, let’s create a new file ‘mario.c’ using the code command in the terminal window.


Then, we can print four question marks horizontally using a for loop, omitting ‘\n’ which would start a new line after every question mark:


#include <stdio.h>


int main(void)

{

    for (int i = 0; i < 4; i++)

    {

        printf("?");

    }

    printf("\n");

}


For vertical blocks, we can do the same, but including ‘\n.’


Now how about a brick wall with side lengths of say three?





In other words, we need to create three rows of three blocks. So one for loop that handles the vertical columns, and one that handles horizontal rows… meaning a for loop inside of a for loop?


int main(void)

{

    const int n = 3;

    for (int i = 0; i < n; i++)

    {

        for (int j = 0; j < n; j++)

        {

            printf("#");

        }

        printf("\n");

    }

}


In this problem, because n is unchanging, we can declare it as a constant using const, rather than writing ‘3’ where n is.


This ensures that you don’t accidentally mistype a number other than ‘3,’ and makes it so that you only have to change the length of the wall in one place instead of multiple, if the need should ever arise.


Problem Set 1: Mario and Cash


This week’s problem sets contained two challenges: Mario and Cash.


I started off by setting up my development environment for Mario, importing the necessary directory and creating a ‘mario.c’ file.


For this problem set, we were tasked with creating a right-aligned pyramid of blocks, as seen in Nintendo’s Super Mario Brothers.





My initial thoughts were to implement a solution akin to that of the brick wall structure that the lecture walked us through.


As such, I would need multiple for loops embedded within one another to account for width and height.


Another thing to consider was that pyramids do not have a constant number of blocks per row, meaning that I would need to increment the length by one after every iteration.





Here, the ‘blanketing’ for loop accounts for the height of the pyramid, and the embedded for loop accounts for the rows, increasing by one each iteration using the variable i.


However, there is still a minor problem – while this program prints a pyramid, it is yet left-aligned. A simple fix is to print enough spaces on the left in order to align the pyramid to the right.





Now, starting from ‘height-1’ we are able to decrement j through every iteration, pushing our hashes to the right.


Keep in mind that the variable height stores a user-inputted value using a do-while loop along with CS50’s ‘get_int()’ function.





In C, do-while’s are a subset of while loops, allowing us to declare and populate a variable before checking a condition.


Say we were to use a traditional while loop here, the variable height would store null, or nothing, resulting in an error.


With Mario out of the way, let’s take a look at Cash, which zeroes in on functions.


Remember that in C, functions are accompanied by a couple of ground rules:

  • Functions help with abstracting away lower-level code by condensing code snippets into single lines, which can be reused

  • Before a function is called, it must be declared by writing its signature – its return type, name, and parameters


Most of the program is pre-filled, so all we really have to do is implement the ‘get_cents()’ function to retrieve a number of cents from the user, and complete functions for each coin to return the minimum number of coins we can get with a certain amount of pennies.


Similar to our implementation of the height variable in Mario, we can make use of a do-while loop to return the amount of pennies a user might want, ensuring that this value is both an integer and greater than zero.





Notice how ‘get_cents()’ takes no parameters (shown by the keyword void within the parentheses) and returns in integer c (shown through the data type int behind the function name).


The next four functions use the same logic, but with slightly different numbers. Let’s begin with ‘calculate_quarters().’


We need to return the maximum number of quarters that we could make with our given number of cents – sounds like division!


Because 25 cents makes one quarter, we can divide our cents parameter by 25 to get the resulting number of quarters, returning that value.


We can repeat this process with our other coin functions, of course, with their respective values.








In our main function, each of our functions is called, and the number of cents decreases before the next function is called, accounting for the number of coins newly made from the pennies.


Key points:

  • We start with the largest values of coins, quarters, in order to minimize the total number of coins given to the customer

  • Because we are dividing integers by integers, the result will also be an integer, meaning no decimals

  • By nature of integer division, the product is always rounded down to the lowest, nearest integer, which is perfect for our problem set!


Final Thoughts


The learning curve for computer science is described as something close to a logistic growth function – difficult at first, but with time, it becomes easier to understand.


CS50x will follow C for the next few lectures, so stay prepared for long and content-dense lectures in the weeks to come.


For now, this concludes Week 1!


See you soon! Meanwhile, stay tuned for updates by following my blog and LinkedIn page.


Note: this article has images and code belonging to CS50. You are free to you remix, transform, or build upon materials obtained from this article. For further information, please check CS50x’s license.


Comments


bottom of page