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:
- The condition
(++x) > 0is checked.xis incremented to 10. The condition is true. - Because it's true, the first part of the ternary operator is executed:
(++x). xis incremented AGAIN to 11. This is the value assigned toresult.
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. |