Introduction to C

C's the day.

  • Course Length: 3 weeks
  • Course Type: Short Course
  • Category:
    • Technology and Computer Science
    • High School

Schools and Districts: We offer customized programs that won't break the bank. Get a quote.

Get a Quote

You want to program and you're tired of the training wheels like memory management and concatenation operators.

…Did we scare off the newbs with our jargon yet? Good.

Not that we don't love beginners, but if you're looking for a programming language to start learning computer science, C probably isn't for you. If you know a thing or two, though, and you're ready to make video games, build robots, or create your very own operating system, C's the perfect starting point. 

Pretty much every programming language is built on C, too, which means just about every language will become much easier to learn. (Programming languages, that is. If you're trying to learn Xhosa, you're on your own.) As soon as you've mastered how C does

  • conditions
  • loops
  • pointers
  • structs
  • random number generators
  • memory management

you'll be ready to take on anything a programming language has to throw at you. All those newbs will be so jealous.

Required Skills

You don't have to be the next Elon Musk, but you do need to know a little bit about programming before you start this course. Ideally, you've taken at least a semester—if not a year—programming before you take on C.


Unit Breakdown

1 Introduction to C - Introduction to C

The best things come in Cs. After all the conditions, loops, pointers, and structs, you'll be ready to start coding your own projects in C—ideally without any seg faults.


Recommended prerequisites:

  • Foundations of Programming—Semester A

  • Sample Lesson - Introduction

    Lesson 1.09: Do Your Job

    A bunch of decorated cookies.
    To keep morale up, we give ourselves a cookie for every hour spent cleaning. There were a lot of cookies…
    (Source)

    What if we told you there was a way to make your programs more readable? How about more maintainable? Does it sound too good to be true? Do we sound like a late-night infomercial for a blender that can make smoothies and barbecued pulled pork sandwiches?

    Probably a little bit.

    Here's the deal. There is a way to make your programs way more readable and easy to maintain: you can use functions, named pieces of code that run any time you call them. You can use as many as you want, and…you should. Here's why that's a good idea.

    Imagine that you wrote a function for you to clean your room and eat cookies. (You might be part of The Matrix in this example; just go with it.) Let's say your dad wants you to clean your room. Sure! If you clean your room, that means you get to chow down on some cookies because they're both part of the same function you wrote. Sounds awesome. Bring on the feather duster and bag of Oreos.

    Wait, though—if cleaning your room and eating cookies are bundled together in the same little pack of code, that means the opposite situation is true, too. In other words, any time you want to dunk a cookie in some milk or see how many ginger snaps you can fit in your mouth at once, you have to clean your room. Uh, no thanks.

    By working definition, a function is a group of statements that performs a specific task. If you define a function to perform just one task, there's less chance of it being buggy and more chance of it being reused. In other words, give each of your tasks its own function, and everything works better—whether that's doing calculations or trying to find your bedroom floor through three feet of dirty clothes.


    Sample Lesson - Reading

    Reading 1.1.09a: What's So Fun About Functions?

    We've been keeping things from you. Sorry it had to come out this way, but functions have been among us all along. And we aren't just talking about that function we showed you when talking about pointers.

    One of the most obvious ones we've seen is the main function. The main function often has an int right before it, which is known as the return type. The reason is pretty simple: if the main function in a program could return an int, then the computer would know when it's done running. If it never returns a value, that means something's wrong with the program. That's a good thing to know, if you ask us.

    Other functions that we've already put to work, such as printf and scanf (and anything we've called by writing a word followed by parentheses, really), are included in some library, such as stdio.h. We called these functions by using the function name and then passing in arguments inside parentheses. For example, when we use printf ("Hi There");, the name of the function is printf and the argument is "Hi There".

    Build-A-Function

    Pre-made functions are super helpful, but there are also times when we need to define our own functions. Fittingly, these functions are called user-defined functions. Let's take a look at the anatomy of a function:

    returnType functionName(parameters) {
        body of function
    }

    The first part of any C function is the function header. In the above example, the header is this line:

    returnType functionName(parameters)

    It's a very helpful little line. Let's break down what each part of that line means. First up, returnType. In many cases, when we call a function we need to return something. Think: that number we send back in main every time to tell the computer it finished running successfully. Before we can return anything in the function, we need to explain what type we'll be sending back so that others know what kind of variable to save the value to. That's exactly what returnType does in the header. As we'll see, some functions don't need to return anything; for this we use the keyword void.

    (If you need a way to remember that, think about an astronaut trying to shout in the void of space. Nothing's going to get sent back, which happens to be the way void likes it.)

    Next, we have functionName. This is the name of the—that's right; you guessed it—function. By giving a function a name, calling the code inside it is as simple as writing that name anywhere in the code. After that, it's (parameters). Each item that shows up in-between those parentheses is going to have both a type and a name (just like the function call itself). Together, we use the function name and the set of parameters to call the function.

    The other part of a C function is the function body, which is the part inside the curly braces. That's where the code that takes the parameters and does something with them goes. For example, if we wanted an extremely unuseful function like addTwoNumbers, it would look something like this:

    int addTwoNumbers( int number1, int number1){
        return number1 + number2;
    }

    If we call addTwoNumbers anywhere in the code, putting in numbers to stand in for and number2, we'll get back the sum. Every. Single. Time. That's the power of a good function.

    Declaring (Slightly More Useful) Functions

    In C, a function must be declared before it's used. If you aren't groaning now, you might want to, since that's extremely limiting in what it allows us to do as programmers. Luckily, C gives us the ability to declare a function prototype, so that we can implement (i.e. actually write out) the function anywhere in the program. Thanks, C. You're a peach.

    Now that we've got the basics down, let's look at another example—one that doesn't require any return type. Maybe we'd like to print just the header at the very beginning of a program. To call that function without declaring it at the top of the C file, we can write out the function header at the top without anything in-between the braces (which is what the function prototype is). Writing the header alone tells the compiler that the function does exist, we're just going to define it later. Check it out:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
        
    /* function prototype does not include a function body, just the header */
    void printHeader();
        
    int main() {
        /* call the printHeader function  */
        printHeader();
        return 0;
    }
        
    void printHeader() {
        time_t currentDateTime;
        char* stringifyDateTime;
        
        printf("Program Header\n");
        printf("Date: ");
        
        /* Obtain the current time by calling the time function. */
        currentDateTime = time(0);
        
        /* Convert to a printable local date and time. */
        stringifyDateTime = ctime(¤tDateTime);
        
        printf("Today's date and time  is %s", stringifyDateTime);
    }

    Notice that, within the printHeader function, we're calling time and ctime functions to retrieve the current data and the time we'll print in the header. Functions calling functions; it's a beautiful thing.

    Now, let's say we want to add a function that prints a name whenever the program is executed, such as the name of the user of the program. First, we declare the prototype before the main function:

    void printName(const char * playerName);

    We can then implement the function in this way.

    void printName(const char * playerName) {
    printf ("Hello, my name is %s\n", playerName);
    }

    What's that? What's that char * business we're talking about? It's a pointer variable just like what we worked with in the last lesson, and it lets us save strings (of characters) without having to separate out each individual char.

    We can call the function from the main function like this:

    char * name = "Sally Mustang";
    printName(name);

    Sample Lesson - Reading

    Reading 1.1.09b: Scope the Joint

    Now that you know the basics of functions, let's talk a little about scope here. First off, let's bring back printHeader.

    void printHeader() {
        time_t currentDateTime;
        char* stringifyDateTime;
        
        printf("Program Header\n");
        printf("Date: ");
        
        /* Obtain the current time by calling the time function. */
        currentDateTime = time(0);
        
        /* Convert to a printable local date and time. */
        stringifyDateTime = ctime(¤tDateTime);
        
        printf("Today's date and time  is %s", stringifyDateTime);
    }

    In this function, you'll see that there are two variables called currentDateTime and stringifyDateTime declared inside the printHeader function body. These variables are local variables, meaning they only show up in the printHeader function. The scope of these local variables is confined to the printHeader function and once the function exits, the memory space those variables take up is freed. You can think of them as dead if you want to.

    What if we were trying to figure out how many times a function is called? That number would be helpful if we were trying to optimize performance of a program, for example. We can create a static variable to hold its value in-between function calls. These static variables are created once in memory and maintain their space (read: value) there until the program ends.

    Take this lovely printName function.

    void printName(const char * playerName) {
        /* local variable to keep track of how many times the function 
        * is called */
        
        static int callCount;
        callCount ++;
        printf(" The function has been called %d times\n", callCount);
        
        printf ("Hello, my name is %s\n", playerName);
    }

    Then we'll make a for loop to call the function five times, like this.

    char * name = "Shadow Moon";
    for (int i = 0; i < 5; i++ ) {
        printName(name);
    }

    Here's the output from the program:

     The function has been called 1 times
    Hello, my name is Shadow Moon
    The function has been called 2 times
    Hello, my name is Shadow Moon
    The function has been called 3 times
    Hello, my name is Shadow Moon
    The function has been called 4 times
    Hello, my name is Shadow Moon
    The function has been called 5 times

    As you can see, the static variable maintains its value between function calls, even though the scope of the variable is still the function printName.

    Static Magic

    Did you see something a little funky happening with that variable declaration? Notice how we didn't clear out the memory for that static variable? That's because the static variable needs to save its state no matter the function call. For that reason, the first time a static variable is called, it's automatically initialized to a default value. The same is true of global variables.

    If we wanted to, we could initialize the variable at the beginning of the function and it would have the same effect: only the function call will initialize the static variable. So if we wanted to mess with a programmer and make callCount start at 3, we could rewrite the function this way.

    void printName(const char * playerName) {
        /* local variable to keep track of how many times the function 
        * is called */
        
        static int callCount = 3;
        callCount ++;
        printf(" The function has been called %d times\n", callCount);
        
        printf ("Hello, my name is %s\n", playerName);
    }

    Now the code's going to print this:

    The function has been called 4 times
    Hello, my name is Shadow Moon
    The function has been called 5 times
    Hello, my name is Shadow Moon
    The function has been called 6 times
    Hello, my name is Shadow Moon
    The function has been called 7 times
    Hello, my name is Shadow Moon
    The function has been called 8 times

    When Globalization Works…In Code

    In the context of functions, global variables sound helpful, but…they should really be avoided. Think about it in terms of a large system, like an airline reservation system. If you had a seat variable declared globally, and various functions had direct access to that seat reservation, a new programmer might come into the system and accidentally rewrite the seat reservation from anywhere just because they could. We don't know if you've spent a lot of time in airports, which tend to be full of a lot of tired, stressed, peanut-filled people on a strict schedule, but we wouldn't want to be the one to tell a customer their reservation has been lost.

    Just…don't do it.

    On the other hand, global constants are totally fine. They can't be changed by a running program, meaning that it's totally okay to make them accessible everywhere. That isn't too dangerous.

    Ready for another example? Good. Because we were going to show you one no matter what.

    In this example, we'll determine the time it takes for an object to land on Mars given its distance from the surface, assuming it begins at rest and then starts free-falling due to a little something called gravity.

    We know the gravity on Mars to be 3.711 m/s², and that of Earth to be 9.8 m/s². We can declare these constants as global constants. Check it out:

    #include 
    #include 
    #include 
        
    /* Mars Gravity Constant in m/s^2 */
    const float MARS_GRAVITY = 3.711f;
        
    /* Earth Gravity Constant in m/s^2 */
    const float EARTH_GRAVITY = 9.8f;
        
    void printTimeToFall(float distance, float gravity);
        
    int main() {
        float distance;
        printf("Please enter the distance in meters the object is");
        printf(" from the surface:");
        scanf("%f",&distance);
        
        printf ("On Earth, ");
        printTimeToFall(distance, EARTH_GRAVITY);
        printf ("On Mars, ");
        printTimeToFall(distance, MARS_GRAVITY);
        return 0;
    }
        
    void printTimeToFall(float distance, float gravity) {
        float tSquared;
        float t;
        /* formula is t = squareRoot(2y/g) where y is distance from surface 
        * and g is gravity */
        tSquared = 2 * distance / gravity;
        t = sqrtf( tSquared);
        printf("the time it takes to fall is %f seconds\n", t);
    }

    Those constants are totally cool because no one can change the value of those different planets' gravity. If gravity could be changed in any function…that would be terrifying.


    Sample Lesson - Reading

    Reading 1.1.09c: Picking Functions

    The functions we've written so far are very specific in their responsibilities. Making functions specific increases reuse and maintainability. For example, all that the printTimeToFall function does is calculate the time for an object to fall and print it. If we also included some other thing for it to do—like give suggestions of other objects to test, it's now not as widely applicable to other situations. If we keep it small and specific, it's much more helpful to many more programs.

    To improve re-use of this function, we could then return a value from it, so that the user could print or save it for some other purpose. If we wanted to implement that change, we'd change the function name to calcTimeToFall, and the return type to float.

    As a general rule, functions should have one specific task that they do. That way, we can reuse them in the widest variety of situations. We can even build functions that call functions to keep things super separated.

    Say we wanted to make a bakeACake() function. It might look something like this.

    void bakeACake( char * cakeFlavor, char * icingFlavor){
        
        mixBatter(cakeFlavor);
        putCakeInOven();
        coolCake();
        mixIcing(icingFlavor);
        iceCake();
    }

    That's one, specific thing the function's doing, and it's also calling five functions that do one, specific thing. Since we've isolated out all those different processes, if we needed to mixIcing for a different baked good (like cupcakes or even—gasp—a cookie), we can call that function separately from bakeACake.

    Now we've got plenty of time to perfect our Russian piping tip flowers.

    In the meantime, we've got two activities waiting for you.


    Sample Lesson - Activity

    Activity 1.09a: Across the Universe

    As a programmer, you'll need to be able to write functions and call them many times. We'll kick things off with an assignment that'll get you some function-writing experience, and that utilizes a familiar example from the reading: making objects fall on different planets.

    We tried to pull some strings with NASA to let you do egg drops on Neptune, but we kinda tuned them out after the seventh "frivolous use of taxpayer dollars."

    Some people just don't understand. In the meantime, you might as well model it in C.

    Step One

    Create a directory called Lesson1-10 and make a file called gravity.c. To get you started, we've included starter code below for you to add to that new file.

    #include 
    #include 
    #include 
    /* Mars Gravity Constant in m/s^2 */
    const float MARS_GRAVITY = 3.711f;
    const float MERCURY_GRAVITY = 3.72f;
    const float URANUS_GRAVITY = 8.69;
    const float VENUS_GRAVITY = 8.87;
    const float SATURN_GRAVITY = 10.44;
    const float NEPTUNE_GRAVITY = 11.15;
    const float JUPITER_GRAVITY = 24.79;
        
    /* Earth Gravity Constant in m/s^2 */
    const float EARTH_GRAVITY = 9.8f;
        
    float calcTimeToFall(float distance, float gravity);
    char getPlanet();
        
        
    int main() {
        float distance, time;
        printf("Please enter the distance in meters the object is from ");
        printf( "the surface:");
        scanf("%f",&distance);
        
        time = calcTimeToFall(distance, EARTH_GRAVITY);
        printf ("On Earth, the time it takes to fall is %3.3f seconds\n", time);
        
        time = calcTimeToFall(distance, MARS_GRAVITY);
        printf ("On Mars, the time it takes to fall is %3.3f seconds\n", time);
        
        return 0;
    }
        
    float calcTimeToFall(float distance, float gravity) {
        float tSquared;
        float t; /* Time in seconds */
        /* Formula is t = squareRoot(2y/g) where y is distance from surface 
        * and g is gravity */
        tSquared = 2 * distance / gravity;
        t = sqrtf( tSquared);
        return t;
    }

    Take a couple of minutes to make sure you know what's going on in the code. Since we walked you through it in the reading, though, you should only really need to look at it for five minutes or so.

    Step Two

    Using the gravity code example given below, add a function to allow the user to select one planet from a list using a character code (r for Mercury, m for Mars, u for Uranus, v for Venus, s for Saturn, n for Neptune). The constants have been added for you in the code you already (thanks, Planet Facts).

    This function called getPlanet should have a printf prompt and a scanf call to get the character from user input, and return the character. Here's the function prototype:

    char getPlanet();

    That's in the code already, so you can totally call it in main without a compilation error, but you'll need to define the function itself below main.

    Step Three

    In main, call the getPlanet function (before getting the distance from the surface) to get the character code from the user. Store the character code in a char called planetCode. Once it's in, use a switch block in the main function to select the chosen planet's constant to pass to the calcTimeToFall function . We'll start you off with the code snippet below.

    switch(planetCode) {
        case('m'): time = calcTimeToFall(distance, MARS_GRAVITY);
        printf ("On Mars, the time it takes to fall is %3.3f seconds\n", time);
        break;
    }

    This start to the switch gives the case code for Mars; it's your job to implement the remaining planets within the same switch.

    Step Four

    Your code should be completely written, so compile, debug and run it to make sure it works. If it does, it can take any of the planet characters, any distance (in meters), and print out how long it's going to take to fall on Earth and on the picked planet.

    Here's a sample to show what it should look like.

    How the code should look in the console.

    Then you're done. Nice work.


    Sample Lesson - Activity

    Activity 1.09b: How to Pay Off Your Private Jet in 435 Easy Installments

    You've got some practice writing and calling functions under your belt. Now it's time to do something you'll do all the time as a programmer: write functions that perform calculations.

    Write a program that determines how long it'll take, in months, to pay for an item given the total cost of the item, plus

    • a percent sales tax (fixed at 5%).
    • a shipping and handling charge (purchase amounts over $50 ship for free; $50 or less costs $10).
    • a down payment.
    • and the amount one can pay per month.

    The program should contain individual functions that do the following:

    • Compute and return sales tax.
    • Compute and return shipping and handling costs (if any).
    • Determine the total cost of the item after the down payment.
    • Determine the number of months it takes to pay the amount due based on a monthly amount.

    Here's a sample run from the program:

    Please enter the cost of the item: 1000.53
        
    The total cost is $1050.56, please enter how much can you pay down: 100.20
        
    The cost with your down payment is $950.36
        
    Please enter the amount you can pay per month: 100.20
        
    You will have 9 monthly payments of 100.20
        
    You will have 1 additional month's payment of 48.56

    We didn't give you any starter code this time; it's all up to you. Put your coding helmet on and get to work.


    Sample Lesson - Activity

    1. Which of the following is a function prototype?

    2. Which of the following statements is true about all C functions?

    3. Which of the following choices is the return type of the function with prototype int myFunk(char c, float f);?

    4. Which of the following is a function call to call myFunk?

    5. Which of the following is a valid function implementation?