Skip to content

C Programming Review

Overview

This first session of CS 315 establishes the development environment and reviews the C programming language, which is the primary language we use to explore computer architecture. We begin with SSH setup for passwordless access to the department's Stargate gateway and BeagleV RISC-V machines, then review C as a compiled language: how source is turned into an executable with gcc, the core constructs (functions, data/variables, statements, expressions), control flow, function definitions, static typing, primitive types and their sizes, strings as character arrays, and a forward look at structs and pointers. The goal is to make sure everyone can compile and run a C program in the course environment and has a shared mental model of how C programs are organized.

Learning Objectives

  • Configure SSH key-based, passwordless access from your laptop to Stargate and the BeagleV machines, including correct file permissions
  • Explain the difference between a compiled language (C) and an interpreted language (Python)
  • Compile a C program with gcc and run the resulting executable
  • Identify the four core C constructs: functions, data/variables, statements, and expressions
  • Describe where data lives at run time: globals, the stack (local variables), and the heap (malloc/free)
  • Write functions with a return type, parameters, and a body, and use the basic control-flow statements (if, for, while, switch)
  • Explain static vs. dynamic typing and list the primitive C types with their typical sizes
  • Understand that C has no built-in string type and that strings are NUL-terminated character arrays

Prerequisites

  • A GitHub account and basic command-line familiarity (cd, ls, cat, less)
  • Prior programming experience in some language (Java, Python, or C)
  • Access to a laptop running macOS, Linux, or Windows with WSL
  • The course shell-basics guide skimmed beforehand

1. Setup Help and Office Hours

The course relies on the department's shared RISC-V hardware, so the first task is getting connected. The instructor offered drop-in setup help:

When Where Purpose
10:00 - 10:30 am CS Labs Hands-on setup help
1:40 - 2:30 pm HR 412A Office hours

The two machines you connect to are:

  • Stargate - the department gateway/jump host that is reachable from the public internet.
  • BeagleV - the RISC-V boards where you actually compile and run code. These are reached through Stargate.

The first lab deadline was extended to Wednesday (instead of Monday), and the autograder is configured by Tuesday. See /assignments/lab01/ for the full lab specification.


2. SSH and Passwordless Login

SSH (Secure Shell) lets you log in to a remote machine over an encrypted connection. Typing a password every time is slow and error-prone, so we set up key-based authentication: your laptop holds a private key, the server holds the matching public key, and the two are used to prove your identity automatically.

The Key Pair

An SSH key pair has two halves:

  • Private key (<key>) - stays on your laptop. Never share or copy it off your machine.
  • Public key (<key>.pub) - safe to copy to any server you want to log in to.
flowchart LR
    subgraph Laptop["Laptop (~/.ssh)"]
        CFG["config"]
        PRIV["&lt;key&gt; (private)"]
        PUB["&lt;key&gt;.pub (public)"]
    end
    subgraph Stargate["Stargate (~/.ssh)"]
        AUTH["authorized_keys"]
    end
    PUB -- "scp" --> AUTH
    PRIV -. "proves identity" .-> AUTH

What Lives Where

The diagram from class shows the layout of the ~/.ssh directory on each side:

On your laptop (~/.ssh/):

~/.ssh/
  config              # per-host SSH settings
  <key>               # private key
  <key>.pub           # public key

On Stargate (~/.ssh/):

~/.ssh/
  authorized_keys     # contains the contents of your <key>.pub

You copy the public key over (for example with scp) and append its contents to ~/.ssh/authorized_keys on the server. After that, the server can verify the laptop's private key without a password.

Generating and Installing a Key

# 1. Generate a key pair on your laptop (ed25519 is a good default)
ssh-keygen -t ed25519 -C "you@example.edu"
# -> creates ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public)

# 2. Copy the public key to Stargate (easiest with ssh-copy-id)
ssh-copy-id you@stargate.cs.usfca.edu

# Equivalent manual steps if ssh-copy-id is unavailable:
scp ~/.ssh/id_ed25519.pub you@stargate.cs.usfca.edu:~/
ssh you@stargate.cs.usfca.edu 'cat id_ed25519.pub >> ~/.ssh/authorized_keys'

Permissions Matter

The class diagram marks perms on both the laptop and the server side because SSH refuses to use keys or an authorized_keys file that other users could read or modify. If permissions are too open, SSH silently falls back to asking for a password. Set them like this:

chmod 700 ~/.ssh                      # directory: owner only
chmod 600 ~/.ssh/<key>                # private key: owner read/write
chmod 644 ~/.ssh/<key>.pub            # public key: world-readable is fine
chmod 600 ~/.ssh/authorized_keys      # on the server side

The config File

The ~/.ssh/config file lets you give short names to hosts and route through Stargate to a BeagleV board automatically:

Host stargate
    HostName stargate.cs.usfca.edu
    User you
    IdentityFile ~/.ssh/id_ed25519

Host beagle
    HostName beagle1.cs.usfca.edu
    User you
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump stargate          # hop through Stargate automatically
    ForwardAgent yes            # forward your key so GitHub works on the board

With this in place, ssh beagle connects through the gateway in one step. ForwardAgent yes lets the BeagleV machine use your laptop's key for GitHub operations (SSH agent forwarding), so you do not have to copy private keys onto shared hardware.

Quieting the Login Banner

To suppress the long login banner on Stargate, create an empty .hushlogin file in your home directory on that machine:

touch ~/.hushlogin

3. C Is a Compiled Language

Python runs through an interpreter: the interpreter reads your source and executes it directly. C is compiled: a separate program, the compiler, translates your source text into a machine-code executable ahead of time. You run the executable, not the source.

flowchart LR
    SRC["hello.c (source)"] -->|"gcc -o hello hello.c"| EXE["hello (executable)"]
    EXE -->|"./hello"| OUT["program output"]

The compile command from class:

gcc -o hello hello.c
#       ^^^^^   ^^^^^^^
#       |       source file
#       executable to produce
  • gcc is the GNU C compiler.
  • -o hello names the output executable hello.
  • hello.c is the C source file.

Running the Executable

./hello

You must type ./ in front of the name. The leading ./ means "the file named hello in the current directory." Standard commands like ls live in directories listed in your PATH (such as /usr/bin), but the current directory is not on PATH by default, so the shell will not find a bare hello.

Hello, World

#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}
  • #include <stdio.h> pulls in declarations for standard I/O functions such as printf. The #include is handled by the preprocessor before compilation.
  • int main(void) is the entry point. Execution starts here.
  • printf("Hello, World!\n") prints text; \n is a newline.
  • return 0; reports success to the shell (non-zero conventionally indicates an error).
$ gcc -o hello hello.c
$ ./hello
Hello, World!

4. C Constructs

C's core is intentionally small. Almost everything you write falls into one of four categories:

flowchart TD
    C["C Constructs"]
    C --> F["Functions (code)"]
    C --> D["Data / Variables"]
    C --> S["Statements"]
    C --> E["Expressions"]
    D --> G["globals"]
    D --> ST["stack (local vars)"]
    D --> H["heap (malloc / free)"]

Functions Hold the Code

In C, all code lives inside functions - there is no top-level statement execution as in Python. This is similar to Java, where everything is inside a method. The main function is where the program starts.

Data Lives in Three Places

At run time, the variables and data your program uses reside in one of three storage regions:

Region What lives there Lifetime How it is created
Globals Variables declared outside any function Whole program run Declared at file scope
Stack Local variables, function parameters While the function call is active Automatically on each call
Heap Dynamically allocated memory Until you free it malloc / free

The stack grows and shrinks as functions are called and return; each call gets its own stack frame holding its locals. The heap is for memory whose size or lifetime is not known at compile time. In this course we mostly use globals and the stack; malloc/free (the heap) are not used in this class, but you should know the region exists.

High addresses
+---------------------+
|       Stack         |  <- local variables, grows downward
|         |           |
|         v           |
|                     |
|         ^           |
|         |           |
|       Heap          |  <- malloc/free, grows upward
+---------------------+
|   Globals / Data    |  <- global variables
+---------------------+
|       Code          |  <- the functions themselves
+---------------------+
Low addresses

Statements and Expressions

A statement is a complete unit of execution. Statements end with a semicolon ; or are blocks grouped by braces { }. An expression is a piece of a statement that evaluates to a value. Not every statement is an expression, but many statements contain expressions.

The classic example from class is an assignment:

x = 3;

This whole line is a statement (it ends in ;). The part x = 3 is an expression - in C an assignment evaluates to the value assigned, so x = 3 has the value 3. That is why chained assignments like a = b = 0; work.


5. Statements and Control Flow

Statements are the building blocks of a function body. Here are the common ones reviewed in class.

Assignment

x = 3;          // store 3 into x
x = x - 1;      // read x, subtract 1, store back

if

if (x > 0) {
    x = x - 1;
}

The condition in parentheses is evaluated; if it is non-zero (true), the block runs. C has no separate boolean type historically - any non-zero value is "true," and 0 is "false."

Function Call

printf("hi");

A function call is itself a statement when followed by a semicolon. printf is a call into the standard library.

for

for (i = 0; i < 10; i++) {
    printf("i = %d\n", i);
}

The for header has three parts, separated by semicolons:

  1. init - i = 0 runs once before the loop (an assignment).
  2. condition - i < 10 is tested before each iteration; the loop continues while it is true.
  3. update - i++ runs after each iteration (here, increment i by 1).

%d in the format string is a placeholder for a decimal integer; the value of i is substituted.

while and switch

while (condition) {
    // repeat while condition is true
}

switch (value) {
    case 1:
        // ...
        break;
    case 2:
        // ...
        break;
    default:
        // ...
        break;
}

while repeats as long as its condition holds. switch branches on the value of an integer-like expression; remember the break statements, or control "falls through" to the next case.

Control-Flow at a Glance

flowchart TD
    A[if] --> A1["one-time conditional branch"]
    B[for] --> B1["counted loop (init; cond; update)"]
    C[while] --> C1["loop while condition true"]
    D[switch] --> D1["multi-way branch on a value"]

6. Functions

A function definition has three parts: a return value type, a list of parameters, and a body.

int add_2(int x) {
    return x + 2;
}

Annotated as in the class notes:

  return        parameters
  value type        |
      |             v
     int  add_2 ( int x )  {
         return x + 2;        <- body
     }
  • Return value type (int): the type of the value the function hands back to its caller.
  • Name (add_2): how callers refer to the function.
  • Parameters (int x): typed inputs, available as local variables inside the body.
  • Body ({ ... }): the statements that run when the function is called.
  • return x + 2; evaluates the expression x + 2 and sends it back to the caller.

Calling a Function

#include <stdio.h>

int add_2(int x) {
    return x + 2;
}

int main(void) {
    int result = add_2(40);   // result is 42
    printf("result = %d\n", result);
    return 0;
}

void Functions

A function that returns nothing has the return type void and does not need a return statement (though a bare return; is allowed to exit early):

void greet(void) {
    printf("Hello!\n");
}

Worked Example: Summing an Array

A common task reviewed in class is summing the elements of an array. Because C does not store an array's length with the array, you must pass the size explicitly.

#include <stdio.h>

// Sum n integers starting at array a.
int sum(int a[], int n) {
    int total = 0;                 // accumulator (local, on the stack)
    for (int i = 0; i < n; i++) {  // index declared in the for header
        total = total + a[i];
    }
    return total;
}

int main(void) {
    int nums[5] = {1, 2, 3, 4, 5};
    int s = sum(nums, 5);          // must pass the size: 5
    printf("sum = %d\n", s);       // sum = 15
    return 0;
}

Notes from the discussion:

  • The accumulator total starts at 0 and adds each element.
  • The loop index can be declared inside the for header (int i = 0) or before the loop. Declaring it in the header keeps its scope limited to the loop.
  • a[] as a parameter is really a pointer to the first element; the function has no idea how long the array is, which is exactly why n is required.

7. Static vs. Dynamic Typing

C is statically typed. Python is dynamically typed.

  • Static typing (C, Java): every variable has a type that is fixed at compile time. The compiler knows the type of x before the program runs and checks your operations against it. Type errors are caught during compilation.
  • Dynamic typing (Python): a name can refer to a value of any type, and the type is associated with the value at run time. Type errors surface only when the offending line executes.
int x = 3;      // x is an int, forever
x = "hello";    // compile error in C: incompatible types
x = 3           # x refers to an int
x = "hello"     # fine in Python: x now refers to a str

Static typing is central to this course: the type of a variable determines how many bytes it occupies and how those bytes are interpreted, which connects directly to data representation in computer architecture.


8. Data Types

C types divide into primitive/scalar types (a single value) and derived types (collections built from other types, such as arrays and structs).

Primitive / Scalar Types

Type Typical size Holds Example values
int 4 bytes whole numbers -2, -1, 0, 1, 2
char 1 byte a single character / small integer 'a'
float 4 bytes single-precision real 3.14
double 8 bytes double-precision real 3.14

A few clarifications from class:

  • An int is 4 bytes on the machines we use, which is why it can represent a limited range of whole numbers (we will look at exact ranges and two's complement later).
  • A char is 1 byte and is written with single quotes: 'a'. It is really a small integer holding a character code (ASCII).
  • float (4 bytes) and double (8 bytes) both store real numbers; double has more precision. Use double unless you have a reason not to.
  • Sizes can be confirmed at run time with the sizeof operator.
#include <stdio.h>

int main(void) {
    printf("int    = %zu bytes\n", sizeof(int));     // 4
    printf("char   = %zu bytes\n", sizeof(char));    // 1
    printf("float  = %zu bytes\n", sizeof(float));   // 4
    printf("double = %zu bytes\n", sizeof(double));  // 8
    return 0;
}

Derived Types: Arrays

An array is a fixed-size, contiguous sequence of elements of the same type. As noted in the array-sum example, the size is not carried with the array, so you track it yourself.

int nums[5] = {1, 2, 3, 4, 5};  // five ints, indices 0..4
nums[0] = 10;                    // first element
// nums[5] would be out of bounds - undefined behavior!

9. Strings Are Character Arrays

C has no built-in string type. A string is an array of char terminated by a special NUL byte ('\0', value 0).

When you write a string literal in double quotes, the compiler automatically appends the terminating NUL for you:

char str[] = "hello";   // string literal; NUL added automatically

The annotation from class points out that "hello" is a string literal, and the compiler will auto add the NUL terminator. So str actually occupies six bytes:

index:   0    1    2    3    4    5
        +----+----+----+----+----+----+
str:    | h  | e  | l  | l  | o  | \0 |
        +----+----+----+----+----+----+
                                  ^
                          NUL terminator (added by the compiler)

The NUL terminator is how library functions know where the string ends, since the length is not stored separately. strlen counts characters up to (but not including) the NUL:

#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "hello";
    printf("%s\n", str);               // prints: hello
    printf("length = %zu\n", strlen(str));  // length = 5 (NUL not counted)
    return 0;
}
  • %s in printf prints characters until it hits the NUL.
  • strlen is declared in <string.h>; you must include that header to use it. This is exactly the function needed for the argslens program in Lab 01 (see /assignments/lab01/).

This NUL-terminated convention has real consequences: forgetting the terminator, or writing past it, causes the classic C bugs we will study when we look at memory and security later in the course.


10. Looking Ahead: Structs and Pointers

The notes close by naming two topics we will return to in depth:

  • Structs - a derived type that groups several named fields of possibly different types into one value. Structs let you model records (for example, a point with x and y fields).
struct point {
    int x;
    int y;
};

struct point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);   // (3, 4)
  • Pointers - variables that hold the address of another value rather than the value itself. Pointers are how C refers to memory directly, how arrays decay into addresses, and how functions can modify their caller's data. They are essential for understanding the stack, the heap, and assembly later in the course.
int x = 42;
int *p = &x;     // p holds the address of x
printf("%d\n", *p);   // dereference: prints 42
*p = 7;               // writes through the pointer
printf("%d\n", x);    // x is now 7

We will treat both of these carefully in upcoming sessions, connecting them to how data is laid out in memory on a RISC-V machine.


Key Concepts

Concept Definition Example
Compiled language Source is translated to a machine-code executable before running gcc -o hello hello.c
Executable The runnable binary produced by the compiler ./hello
./ prefix "File in the current directory," needed because . is not on PATH ./hello
Function Named code unit with return type, parameters, and a body int add_2(int x) { return x + 2; }
Globals / stack / heap The three run-time storage regions for data locals on the stack, malloc on the heap
Statement A complete unit of execution ending in ; or a { } block x = 3;
Expression A piece of a statement that evaluates to a value x = 3 evaluates to 3
Static typing Variable types fixed and checked at compile time int x = 3;
int 4-byte whole-number type -2, -1, 0, 1, 2
char 1-byte character/small-integer type 'a'
String NUL-terminated array of char (no built-in type) char str[] = "hello";
NUL terminator The '\0' byte marking the end of a string added automatically to literals
SSH key pair Private key (laptop) + public key (server) for passwordless login id_ed25519 / id_ed25519.pub

Practice Problems

Problem 1: Compile and Run

You have a file greet.c. Write the two shell commands to compile it into an executable named greet and then run it. Explain why a bare greet would fail.

Click to reveal solution
gcc -o greet greet.c
./greet
A bare `greet` fails because the shell searches the directories on `PATH` (like `/usr/bin`) for commands, and the current directory `.` is not on `PATH` by default. The `./` prefix explicitly tells the shell to run the `greet` file in the current directory.

Problem 2: Statement vs. Expression

For the line y = x + 1;, identify the statement and the expression(s), and state the value the assignment expression produces if x is 4.

Click to reveal solution - The **statement** is the entire line `y = x + 1;` (it ends in a semicolon). - The **expression** is `y = x + 1` (an assignment expression), which contains the sub-expression `x + 1`. - If `x` is `4`, then `x + 1` is `5`, `y` is set to `5`, and the assignment expression itself evaluates to `5`. This is why `z = (y = x + 1);` would also set `z` to `5`.

Problem 3: Sum a Larger Array

Modify the sum function so main correctly sums this array of seven elements: {2, 4, 6, 8, 10, 12, 14}. What value is printed, and what must change at the call site?

Click to reveal solution The `sum` function does not change - it already takes the size as a parameter. Only `main` changes:
int main(void) {
    int nums[7] = {2, 4, 6, 8, 10, 12, 14};
    int s = sum(nums, 7);          // pass 7, not 5
    printf("sum = %d\n", s);       // sum = 56
    return 0;
}
The printed value is `56`. The key change is passing `7` as the size, because C does not store the array length with the array - the function relies entirely on the size argument.

Problem 4: String Length and Bytes

For char str[] = "CS315";, how many bytes does str occupy, and what does strlen(str) return? Why are they different?

Click to reveal solution - `str` occupies **6 bytes**: the five characters `C`, `S`, `3`, `1`, `5`, plus the automatically added NUL terminator `'\0'`. - `strlen(str)` returns **5**, because `strlen` counts characters up to but not including the NUL. They differ by one because the storage must include room for the terminator, but `strlen` reports only the visible character count.
+---+---+---+---+---+----+
| C | S | 3 | 1 | 5 | \0 |   <- 6 bytes total
+---+---+---+---+---+----+
  strlen counts these 5 ^

Problem 5: Type Sizes

Using sizeof, predict the output of this program on the course machines:

#include <stdio.h>
int main(void) {
    printf("%zu %zu %zu %zu\n",
           sizeof(char), sizeof(int), sizeof(float), sizeof(double));
    return 0;
}
Click to reveal solution
1 4 4 8
- `char` = 1 byte - `int` = 4 bytes - `float` = 4 bytes - `double` = 8 bytes These sizes are why type matters so much in C: the type determines exactly how much memory a value uses and how the bytes are interpreted.

Problem 6: Where Does It Live?

Classify each variable below by storage region (global, stack, or heap):

int g = 10;                 // (a)

int f(int p) {              // (b) p
    int local = p * 2;      // (c) local
    return local;
}
Click to reveal solution - **(a) `g`** - a **global**. It is declared outside any function, so it lives in the global/data region for the whole program run. - **(b) `p`** - on the **stack**. Function parameters are local to the call and live in the call's stack frame. - **(c) `local`** - on the **stack**. Local variables are allocated automatically in the current function's stack frame and disappear when the function returns. None of these use the **heap**; heap memory would require `malloc` (not used in this class).

Further Reading


Summary

  1. SSH key-based login uses a private key on your laptop and a matching public key (authorized_keys) on the server; correct file permissions are required or SSH falls back to a password. Reach the BeagleV boards through Stargate.

  2. C is compiled, not interpreted: gcc -o hello hello.c produces an executable you run with ./hello. The ./ is needed because the current directory is not on PATH.

  3. C's core is small and built from four constructs: functions (code), data/variables, statements, and expressions.

  4. Data lives in three regions at run time - globals, the stack (local variables and parameters), and the heap (malloc/free, not used in this class).

  5. All code lives in functions, each with a return type, parameters, and a body; void functions need no return. Control flow uses if, for, while, and switch.

  6. C is statically typed: types are fixed and checked at compile time, and the type determines how many bytes a value occupies and how they are interpreted - int (4), char (1), float (4), double (8).

  7. Strings are NUL-terminated char arrays; there is no built-in string type. String literals get their '\0' terminator added automatically, so "hello" occupies six bytes while strlen reports five.

  8. Structs and pointers are coming next - they let us group fields into records and refer to memory by address, connecting C directly to the architecture topics ahead.