A C program prints the wrong average. You change a comparison, add a cast, and move a statement, but the next result is still wrong. The problem is no longer just the original bug: several changes have made it difficult to tell which explanation was correct.
Debugging works better as a sequence of small experiments. Preserve the failure, calculate the result you expect, observe the program’s state, and test one explanation at a time. This guide follows a complete example with two independent mistakes, then explains how warnings, regression tests, debuggers, and sanitizers help with different kinds of failure.
First identify the stage that failed
“My code does not work” leaves too many possibilities open. Record what you actually observe. A compiler diagnostic, an unresolved function, a process that stops unexpectedly, and a successful run with an incorrect answer call for different investigations.
| Symptom | What to inspect first |
|---|---|
| Compilation fails | The first diagnostic, its reported line, and the preceding source line. |
| Linking fails | Function definitions, linked source files, and required libraries. |
| Execution fails or stalls | Input waits, loop progress, bounds, pointer lifetime, and the runtime message. |
| Output is incorrect | The requirement, the input, intermediate values, and the calculation’s types. |
Fix the earliest meaningful build error before chasing every later message; one missing token can confuse the parser about the rest of a function. For a runtime problem, preserve the exact input and exit message. If a program appears stuck, distinguish waiting for input from repeatedly executing a loop.
Our example belongs to the last row. It builds and exits normally. Its result is wrong, so an exit status of 0 cannot settle whether the program meets its requirement. The compiler error guide covers translation diagnostics in more detail.
Make the expected result explicit before editing
The task is to calculate the average of the strictly positive values in a list. Negative numbers and zero are excluded from both the sum and the divisor. If no positive values exist, report that no average is available.
For 4, −2, 0, and 7, only 4 and 7 qualify. Their sum is 11, their count is 2, and the expected average is 5.5. Printing two decimal places should produce Average: 5.50. The following deliberately incorrect program prints Average: 2.00 instead:
#include <stdio.h>
int average_positive(const int values[],
size_t count, double *result) {
long total = 0;
size_t positive_count = 0;
for (size_t i = 0; i < count; i++) {
if (values[i] > 0) {
total += values[i];
positive_count++;
}
}
if (positive_count == 0) {
return 0;
}
*result = total / count;
return 1;
}
int main() {
int values[] = {4, -2, 0, 7};
size_t count = sizeof values / sizeof values[0];
double average = 0.0;
if (average_positive(values, count, &average)) {
printf("Average: %.2f\n", average);
} else {
puts("No positive values.");
}
return 0;
}Use fixed data at first. It removes input conversion from the experiment, so every run receives the same values. You can run this example in the Circuit online C compiler; keep a named copy before editing so the original failure remains reproducible.
State the function’s boundaries
For this exercise, the caller supplies at most 1,000 values, each between −1,000 and 1,000. A nonzero count requires a valid array range, and result must point to a writable double. The long sum can hold the largest permitted positive total, 1,000,000. The function returns 1 and writes the answer on success; it returns 0 without changing the output when there are no positive values.
These are preconditions, not input checks hidden inside the example. An application accepting arbitrary text must validate it separately. Increasing the permitted range would also require reconsidering overflow. A small stated contract gives this debugging exercise a result you can reason about.
Warnings are evidence, not a correctness certificate
Read every warning before investigating a wrong result. Format mismatches, suspicious conversions, and uninitialized values can point to real defects. Do not silence a warning with a cast until you understand what values and types are involved.
For a desktop GCC setup, save the first program as average.c and build with:
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 average.c -o averageThe GCC warning documentation explains the warning groups. -Wall does not enable every possible warning. Here, -g provides debug information and -O0 keeps this learning build unoptimized. Exact diagnostics depend on the compiler and flags; the program can still compile without reporting either logic mistake.
This is a desktop command, not a command to type into Circuit’s program terminal. On a Unix-like desktop, run ./average; in PowerShell, use .\average.exe when the toolchain creates that executable. The desktop setup guide explains how to verify a compiler independently of the editor.
Observe the values immediately before the wrong calculation
The loop has one job: collect the sum and count of qualifying values. Trace those two facts before assuming the loop is broken. After each element of the sample, the intended state is:
| Value | Total | Positive count |
|---|---|---|
| 4 | 4 | 1 |
| −2 | 4 | 1 |
| 0 | 4 | 1 |
| 7 | 11 | 2 |
Temporarily insert this statement just before the assignment to *result, after the no-positive-values check:
fprintf(stderr, "total=%ld selected=%zu all=%zu\n",
total, positive_count, count);For the sample it reports:
total=11 selected=2 all=4The sum and selected count agree with the manual calculation. That narrows the investigation to how the result is formed. The labels are useful: three unlabeled numbers are easier to misread. %ld matches the long total, while %zu matches each size_t count.
Diagnostic messages use stderr, leaving ordinary output on stdout. Both are available in Circuit’s terminal, but do not depend on a particular interleaving of the two streams. If the program handles many entries, trace a small failing input instead of flooding the terminal with output.
Test one explanation at a time—and look for a second defect
The expression total / count divides by all four entries even though only two contributed to the numerator. The first hypothesis is that the divisor should be positive_count. Try that change alone:
*result = total / positive_count;The sample now prints Average: 5.00. The divisor is correct, but the answer is still not 5.50. Both operands are integer types, so the division discards the fractional part before its value is assigned to the double output. A floating-point destination does not retroactively change the operation.
Convert an operand before dividing:
*result = (double) total / positive_count;This computes 11.0 divided by 2 and produces 5.5. Casting the result afterward, as in (double) (total / positive_count), would preserve the already truncated value 5.0. Likewise, adding a cast while keeping the original divisor would produce 2.75, which is a more precise answer to the wrong question.
| Version | Printed average |
|---|---|
| Original calculation | 2.00 |
| Change only the divisor | 5.00 |
| Convert first, but keep the wrong divisor | 2.75 |
| Correct divisor and floating-point division | 5.50 |
Keep the input fixed while comparing these versions. Changing the data and calculation simultaneously can make an incorrect version appear successful. The improvement from 2.00 to 5.00 is useful evidence, but “closer” is not the same as meeting the requirement.
The complete corrected program
The only necessary calculation change is the assignment to *result. This clean version removes the temporary diagnostic and keeps the guard against dividing when no positive values exist:
#include <stdio.h>
int average_positive(const int values[],
size_t count, double *result) {
long total = 0;
size_t positive_count = 0;
for (size_t i = 0; i < count; i++) {
if (values[i] > 0) {
total += values[i];
positive_count++;
}
}
if (positive_count == 0) {
return 0;
}
*result = (double) total / positive_count;
return 1;
}
int main() {
int values[] = {4, -2, 0, 7};
size_t count = sizeof values / sizeof values[0];
double average = 0.0;
if (average_positive(values, count, &average)) {
printf("Average: %.2f\n", average);
} else {
puts("No positive values.");
}
return 0;
}Average: 5.50The status return and output parameter describe different things. The return value tells the caller whether an average exists; the pointed-to object receives the average only when it does. Read that object as an answer only after a successful return. This is a useful place to review functions and pointer parameters.
Keep the failure as a regression test
A regression test protects behavior you have already corrected. The mixed input must remain in the test set, but it is not enough by itself. All-positive values with a fractional average isolate integer division. Zero and negative entries challenge the selection rule. Empty input checks the no-result path.
Keep average_positive from the corrected program and replace its main with the following test driver. The additional header supplies fabs. The driver calls the real function, reports a failing case by name, and returns a nonzero status if a check fails.
#include <math.h>
int main() {
const struct {
const char *name;
int values[4];
size_t count;
int expected_ok;
double expected;
} tests[] = {
{"mixed", {4, -2, 0, 7}, 4, 1, 5.5},
{"single", {9}, 1, 1, 9.0},
{"fraction", {2, 3}, 2, 1, 2.5},
{"zero excluded", {0, 8}, 2, 1, 8.0},
{"negative excluded", {-3, 6}, 2, 1, 6.0},
{"no positives", {-3, 0, -2}, 3, 0, 0.0},
{"empty", {0}, 0, 0, 0.0}
};
size_t test_count = sizeof tests / sizeof tests[0];
for (size_t i = 0; i < test_count; i++) {
double answer = -1.0;
const int *data = tests[i].count ? tests[i].values : NULL;
int ok = average_positive(data, tests[i].count, &answer);
if (ok != tests[i].expected_ok ||
(ok && !(fabs(answer - tests[i].expected) <= 1e-9)) ||
(!ok && answer != -1.0)) {
fprintf(stderr, "FAIL: %s\n", tests[i].name);
return 1;
}
}
printf("All %zu regression tests passed.\n", test_count);
return 0;
}With the corrected function, it prints All 7 regression tests passed. With the original calculation, it reports FAIL: mixed and exits with code 1. The empty case passes a null data pointer with count 0; the function performs no element access and leaves the output unchanged. A null pointer with a nonzero count is outside the contract.
The small examples use an absolute tolerance of 1e-9 when comparing computed averages. A tolerance should match the scale and accuracy needs of the problem, not be copied as a universal constant. The check also refuses a non-finite NaN result rather than accidentally treating it as close enough.
On a Unix-like desktop, one build command for this test version is gcc -std=c17 -Wall -Wextra average.c -lm -o average-tests. The -lm option links the math library on systems that require it. Circuit supplies its supported math facilities through the browser toolchain. You can also test properties: adding a zero must not change a positive-only average, and reordering valid input must not change the answer.
Use a debugger when inspecting state is easier than adding prints
A debugger lets you stop before a statement and inspect its inputs. For a desktop with GCC and GDB installed, build the original example with the earlier -g -O0 command, then start gdb ./average. Enter these commands inside GDB:
break average.c:19
run
print total
print positive_count
print count
print total / positive_count
print (double) total / positive_countThe breakpoint targets the assignment line in the exact first program above. If you edited the file or added trace statements, use the assignment’s current line number instead. GDB stops before that statement executes, so the loop has already established the values you want to inspect.
Expect the sum 11, positive count 2, and full count 4. Comparing the two division expressions exposes truncation without changing the source. The official manuals explain setting breakpoints and examining data. An IDE debugger can offer the same investigation through its breakpoint and variable panels.
These are desktop tools. Circuit’s current workspace provides compilation, interactive execution, diagnostics, and output; it does not expose a GDB session or a variable-inspection debugger. Download the source when you need that workflow rather than typing debugger commands into a C program’s stdin.
Use memory tools for memory defects, not as a substitute for requirements
An incorrect average is different from accessing a freed object or an array element outside its bounds. AddressSanitizer can detect categories of invalid memory access. UndefinedBehaviorSanitizer checks selected undefined operations, including signed integer overflow. They instrument supported builds and report problems encountered during execution.
For a supported desktop Clang installation with the required runtime libraries, a separate diagnostic build can use:
clang -std=c17 -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined average.c -o average-sanitizedThe backslash is a Unix-shell line continuation; use one line in PowerShell. Tool availability depends on the platform and installed runtimes. This is not a promise that Circuit’s WebAssembly target supports those flags or native sanitizer libraries.
Neither sanitizer should be expected to diagnose the original average calculation for our valid sample. Integer division by a nonzero divisor is defined, and dividing by the wrong count does not inherently violate memory rules. The missing ingredient is knowledge of the intended result, which belongs in your specification and tests.
A clean instrumented run covers the paths you actually exercised. It does not prove the entire program correct. Likewise, C undefined behavior does not have to crash, and some invalid accesses can stay within a WebAssembly memory region. Read the memory-failure guide when the symptom suggests bounds or pointer lifetime.
Apply the same method to other common C problems
An array loop reads one extra element
If a loop uses i <= count, the final index equals the number of elements. That is beyond the last valid index for an array of that length. Inspect the bound before running it; change the condition to match the intended range, then test zero, one, and several elements. Do not use a predictable-looking value from an invalid access as your expected answer.
An input-driven program uses a value that was not read
Check the conversion count returned by scanf before using a destination variable. For one %d conversion, require a return of 1. If a letter is entered instead, a program that ignores the return value may use an uninitialized or stale value. The input guide explains conversion failures and leftover input; validating an integer’s representable range is a separate parsing concern.
A program runs without finishing
Ask whether it is waiting for stdin or repeating work. For a loop, inspect how the condition can become false and whether the relevant variable actually changes. In Circuit, Stop ends a run you no longer need. Consult the documented execution limits rather than interpreting every timeout as proof of the same bug.
Keep a short investigation record
Before the next edit, write down four things: the exact input, expected behavior, observed behavior, and one explanation you can test. For our example, the record starts with “mixed values should average 5.5, but print 2.00.” It then separates the wrong divisor from integer truncation instead of grouping both under “the math looks wrong.”
- Preserve a failing source version and its input.
- Reduce unrelated input, functions, and formatting while keeping the failure.
- Inspect the first state that disagrees with the requirement.
- Predict what one proposed change should do.
- Make that change, rebuild, and compare the actual result.
- Add the failing case to tests and rerun related cases.
- Remove temporary traces and rerun the clean version.
Reduction is an experiment too. Removing the negative and zero entries from our sample would remove evidence of the wrong divisor, leaving only the truncation defect. A smaller program is useful only if it still demonstrates the behavior under investigation.
When asking for help, share a small non-sensitive program, the input, expected and actual output, compiler or browser details, and relevant diagnostics as text. Describe changes already tested. This lets another person reproduce the problem instead of guessing from a screenshot.
The goal is to finish with an explanation and a check that can fail again if the defect returns. A corrected line is more valuable when you know why it is correct and which test protects it.