C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
25: break, continue, and goto
By now, you're comfortable with for and while loops. They work great when the logic is simple: "do this ten times" or "do this until the user hits Q." But in the real world, data is messy. You'll often find yourself in a loop where you need to skip a specific iteration because the data is corrupt, or kill the loop entirely because you've found what you were looking for and there's no point in wasting CPU cycles on the remaining ten thousand elements.
Fighting the indentation pyramid
Let's say we're processing a buffer of sensor readings. Some readings are valid, but others are "noise" (marked as -999) that we need to ignore. A beginner's instinct is to wrap the entire logic of the loop inside an if statement. It looks something like this:
for (int i = 0; i < 100; i++) {
if (readings[i] != -999) {
// Imagine 20 lines of complex math here
double normalized = (readings[i] - offset) * scale;
sum += normalized;
count++;
// ... more logic ...
}
}
This isn't "wrong," but as your logic grows, you end up with a "pyramid of doom"—deeply indented code that's hard to read. This is where continue saves your sanity. Instead of saying "if this is valid, do everything," we say "if this is noise, skip it immediately."
for (int i = 0; i < 100; i++) {
if (readings[i] == -999) continue;
// The "happy path" stays at the top level of indentation
double normalized = (readings[i] - offset) * scale;
sum += normalized;
count++;
}
I call this the "guard clause" pattern. You handle the edge cases or the "junk" data first and get them out of the way. It keeps the core logic of your loop clean and readable.
Killing the sentinel flag
Now, imagine you're searching that same array for a specific "critical" value. Once you find it, you're done. The naive approach is to add a boolean flag to your loop condition.
bool found = false;
int target_index = -1;
while (i < 100 && !found) {
if (readings[i] == CRITICAL_VALUE) {
target_index = i;
found = true;
}
i++;
}
This works, but it's clunky. You're forcing the while loop to evaluate that !found expression on every single iteration, and you've introduced an extra variable that only exists to manage the loop. break is the surgical tool here. It tells the CPU: "I'm done here, get me out of this loop right now."
for (int i = 0; i < 100; i++) {
if (readings[i] == CRITICAL_VALUE) {
target_index = i;
break;
}
}
It's more direct and more honest about what the code is doing. You aren't "waiting for a condition to become false"; you are actively terminating the process.
The necessary evil of the emergency exit
Finally, we have goto. You've probably been told since day one of programming that goto is evil and leads to "spaghetti code." For the most part, that's true. If you use it to jump backward or skip around randomly, you're creating a maintenance nightmare.
However, there is one specific scenario where goto is actually the cleanest tool in C: breaking out of deeply nested loops. Imagine you're searching a 2D grid for a specific coordinate. If you find it in the inner loop, a break only gets you out of the inner loop. You're still trapped in the outer loop.
The "safe" way is to use another flag, but then you're checking that flag in every single level of the nest. It gets ugly fast. In these cases, a goto to a label immediately following the nested structure is the professional's choice.
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (grid[x][y] == TARGET) {
found_x = x;
found_y = y;
goto found_it; // The emergency exit
}
}
}
found_it:
printf("Target found at %d, %d\n", found_x, found_y);
I only use goto for two things: breaking out of nested loops and jumping to a centralized cleanup section at the end of a function (to avoid repeating free() calls). If you use it for anything else, you're playing with fire.
📋 Practical Task
Exercise: The Corrupt Packet Scanner
You are writing a driver for a network device. You receive a 2D array of bytes (representing multiple packets). Each packet is a row in the array.
Your goal is to scan the data and find the first occurrence of a "Magic Byte" (0xAA). However, there are rules:
- If you encounter a "Noise Byte" (
0x00), you must skip that specific byte and move to the next one. - If you encounter an "Error Byte" (
0xFF), the entire packet is corrupted. You should stop scanning that packet immediately and move to the next packet. - The moment you find the "Magic Byte" (
0xAA), you should stop scanning entirely (across all packets) and print the coordinates (row and column) where it was found.
Requirements: Implement this using a 2D array of unsigned char. You must use continue to handle noise, break to handle packet errors, and goto to exit the nested loops once the Magic Byte is found.
There are no comments for now.