CSE 240 Study Guide

A comprehensive guide to C programming fundamentals, paradigms, and the C preprocessor.

1. Programming Paradigms

A programming paradigm is a fundamental style or "way of thinking" about programming. It's not a specific language, but a model that languages follow. Understanding paradigms helps you learn new languages faster because you can recognize the underlying patterns.

The Imperative Paradigm (The "How-To" Guide)

This is the paradigm C belongs to. It's based on the idea of giving the computer a sequence of commands to change the program's state. You are in full control, telling the computer exactly how to do something, step-by-step.

  • Core Idea: A program is a list of statements that update variables in memory.
  • Focus: Control flow (loops, conditionals) and state management.
  • Example Languages: C, C++, Java, Python.

2. C Language Fundamentals

Typing Systems

A type system defines how a language classifies values and expressions into "types," how it can manipulate those types, and how it checks for errors. C has a statically typed system, which means type checking is done at compile-time.

  • Strong vs. Weak Typing: C is considered a weakly-typed language compared to others like Java. This gives you more flexibility but also more responsibility. For example, C allows you to perform operations that might not be safe, like treating a character (`char`) as a small integer (`int`).
  • Primitive Types: These are the basic building blocks: int, char, float, double.

Control Structures

These are the tools you use to direct the flow of your imperative program.

// if-else statement
if (condition) {
    // do this if true
} else {
    // do this if false
}

// while loop
while (condition) {
    // repeat this as long as condition is true
}

// for loop (initialization; condition; update)
for (int i = 0; i < 10; i++) {
    // repeat this 10 times
}

3. Deep Dive: The C Preprocessor & Macros

This is one of the most unique and powerful features of C, but also a common source of bugs if not used carefully. The preprocessor is a tool that runs *before* your code is compiled. Its job is to modify your source code based on special instructions called directives (lines starting with #).

What is a Macro?

A macro, defined with #define, is a rule for direct text replacement. The preprocessor scans your code, finds all instances of the macro's name, and replaces it with the body of the macro. It's like a powerful find-and-replace.

How to Create Macros

1. Object-like Macros (Constants): Used for defining constant values.

#define PI 3.14159
#define MAX_USERS 100

// PREPROCESSOR SEES: float circ = 2 * PI * r;
// COMPILER SEES:    float circ = 2 * 3.14159 * r;

2. Function-like Macros: These can take arguments, making them look like functions.

// A macro to calculate the square of a number
#define SQUARE(x) (x * x)

// PREPROCESSOR SEES: int result = SQUARE(5);
// COMPILER SEES:    int result = (5 * 5);

The Dangers of Macros & How to Avoid Them

Because macros are simple text replacements, they don't follow the same rules as functions. This can lead to surprising and buggy behavior.

Rule #1: Always Wrap Macro Arguments in Parentheses

Consider our SQUARE(x) macro. What happens here?

int result = SQUARE(2 + 3);

// PREPROCESSOR REPLACES IT WITH:
int result = (2 + 3 * 2 + 3); // result is 11, not 25!

Due to order of operations, the multiplication happens first. The fix is to wrap every argument and the entire expression in parentheses.

// The CORRECT way to write the macro
#define SQUARE(x) ((x) * (x))

// NOW, THE PREPROCESSOR REPLACES IT WITH:
int result = ((2 + 3) * (2 + 3)); // result is 25. Correct!

Rule #2: Never Pass Arguments with Side Effects (The Homework Problem!)

A "side effect" is any operation that changes a variable's state, like ++x or x--. This is the most critical rule. Let's look at the problem from your homework:

#define isPositive(x) ((x) > 0 ? (x) : 0)

int x = 9;
int result = isPositive(++x);

The preprocessor replaces this with:

int result = ((++x) > 0 ? (++x) : 0);

Look closely: ++x appears twice! Here's what happens:

  1. The condition (++x) > 0 is checked. x is incremented to 10. The condition is true.
  2. Because it's true, the first part of the ternary operator is executed: (++x).
  3. x is incremented AGAIN to 11. This is the value assigned to result.

The Fix: Never put an expression with a side effect inside a macro call. Do it on a separate line before the call.

// CORRECT CODE
int x = 9;
x++; // Perform the side effect safely here
int result = isPositive(x); // Now result is 10

Macros vs. Functions: A Quick Comparison

Feature Macros Functions
Execution Text replacement by preprocessor before compilation. Code is compiled; called and executed at runtime.
Performance Faster (no function call overhead). Code is "inlined". Slightly slower due to function call stack operations.
Type Checking None. A macro will accept any data type, which can lead to errors. Yes. The compiler checks that argument types are correct.
Debugging Harder. Errors are reported in the expanded code, not the macro definition. Easier. You can step into a function with a debugger.
Safety Less safe. Prone to side-effect bugs and operator precedence errors. Much safer. Arguments are evaluated only once before the function is called.