- Learn
- Computer Science & IT
- Programming
- Programming in C
- C Types, Operators and Control Flow
C Basics: Types, Operators and Control Flow
printf and scanf. Build expressions with C's arithmetic, comparison, logical and bitwise operators, predicting conversions and precedence, then steer a program with if, switch, while, do-while and for. Finish by learning why integers overflow, what happens when they do, and how to write and test code that prevents it.BeginnerLesson 1 of 8 in Programming in C Course
What you will learn
- Turn C source code into a running program, know what the compiler does at each stage, and use its messages to find and fix errors.
- Write, compile and run a first C programRead the parts of a minimal C program, compile it with gcc or clang, and run the executable from a terminal.
- Read compiler messages and fix errorsTell compile-time errors, warnings, link errors, run-time errors and logic errors apart, and use compiler messages and test runs to fix them.
- Write, compile and run a first C program
- Declare and initialize variables, choose between C's integer and floating-point types, and predict the range and precision each type gives.
- Declare, initialize and name variablesCreate variables with a type and a meaningful name, give them starting values, and use constants for fixed values.
- Choose an integer type and know its rangePick among C's signed and unsigned integer types, character and Boolean values, using their sizes, ranges and literal forms.
- Work with floating-point numbers and their limitsStore real numbers in float and double, predict their precision and rounding errors, and convert safely between floating-point and integer values.
- Declare, initialize and name variables
- Format numbers and text precisely with printf, and read and check values typed by the user with scanf.
- Format output with printfUse conversion specifications, field widths, precision, flags and escape sequences to produce exactly the output you intend.
- Read and check input with scanfRead numbers and characters typed by the user, check that the reading succeeded, and recover from invalid input.
- Format output with printf
- Use C's arithmetic, assignment, comparison, logical and bitwise operators, and predict the value and type of any expression from conversions, precedence and associativity.
- Calculate with arithmetic and assignment operatorsApply +, -, *, / and %, integer division, compound assignment and increment and decrement, and avoid their typical traps.
- Predict type conversions in mixed expressionsExplain how C converts operands of different types, use casts deliberately, and avoid the signed–unsigned trap.
- Combine conditions with relational, logical and bitwise operatorsWrite correct comparisons and compound conditions, use short-circuit evaluation, and manipulate individual bits.
- Apply precedence, associativity and evaluation orderPredict how C groups the operators in an expression, evaluate expressions step by step, and use parentheses where grouping is unclear.
- Calculate with arithmetic and assignment operators
- Choose between alternatives with if, else and switch, and repeat work with while, do-while and for loops that start, run and stop exactly as intended.
- Make decisions with if and elseWrite if, else if and else statements with correct conditions and braces, and avoid the common traps of C's syntax.
- Select among cases with switchUse switch with case labels, break and default to choose among fixed integer values, and use fall-through only on purpose.
- Repeat while a condition holds with while and do-whileWrite condition-controlled loops that start correctly, make progress and stop, including input loops and loops that must run at least once.
- Count with for loops, nest loops and stop earlyWrite for loops that run exactly the intended number of times, combine loops inside loops, and use break and continue deliberately.
- Make decisions with if and else
- Explain what happens when a result does not fit its integer type, and write and test code that avoids or detects overflow.
- Explain what happens when a value does not fitDescribe unsigned wraparound, undefined signed overflow and out-of-range conversions, and connect them to real failures.
- Write code that avoids or detects overflowChoose types with enough range, check operations before they overflow, and use compiler tools and boundary tests to catch overflow early.
- Explain what happens when a value does not fit
C Types, Operators and Control Flow study guide
1Build and run C programs
C code becomes a running program through a build. Learn the parts of a complete first program, compile it with a chosen language standard, and run the resulting executable. When a build or test goes wrong, use the stage of failure and the first compiler message to locate the cause. Following the edit, build, and test cycle carefully keeps an old executable from hiding a failed change and makes each repair easier to check.
1.1 From source code to a running program
A C program starts as plain text and runs as machine code. The compiler connects those two forms. Saving an edit changes the source; it does not change the executable you built earlier.
Read a complete program
Save this as hello.c:
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
#include <stdio.h> makes declarations for standard input and output available, including printf. Execution starts in main. In int main(void), int names the return type and void means there are no parameters. Braces enclose the body. The call to printf writes the quoted text; \n moves to a new line. return 0; ends the program and reports success to the operating system. A nonzero status can report failure. Since C99, reaching the end of main also returns zero.
Simple statements end with semicolons. The include line does not. C distinguishes uppercase and lowercase: Printf and printf are different names. Spaces between tokens usually do not affect the program, but consistent indentation makes the block structure visible.
Compile, then run
In the terminal, open the folder holding the source and enter:
gcc -std=c17 -Wall -Wextra hello.c -o hello
./hello
The first command builds an executable named hello; the second runs it from the current folder. Its output is Hello, world! followed by a newline. You can use clang in place of gcc. Without -o hello, the usual Linux or macOS output name is a.out. Windows executables use .exe. CS50's make hello is a shortcut that asks the build tool to invoke the compiler.
After changing the greeting, save, compile again, and run again. If compilation fails, an older executable may still be present. Running it shows the old behavior, so check that the build succeeded before judging your edit.
What the build does
The preprocessor handles directives such as #include, inserting header text. The compiler proper translates C into assembly language. The assembler produces object code, which can still contain unresolved names. The linker combines object code and resolves references such as printf. Library code can be copied into an executable by static linking or supplied through a shared library loaded when the program runs.
You can stop the build at intermediate stages:
| Option | Result |
|---|---|
-E | Preprocessed text |
-S | Assembly language |
-c | An object file, such as hello.o |
An object file is not yet the finished executable. An executable can run without the compiler, but it is built for a particular operating system and processor. To move the program to another platform, compile its source there.
Comments and language versions
// begins a comment that ends at the newline. /* ... */ can cover several lines, but these comments cannot nest: the first */ ends the comment. Use comments to explain a decision that the code alone leaves unclear.
Select the language version explicitly. C17 supports the examples above, including // comments, long long, and declarations in for headers. C23 adds features such as bool as a keyword; it was published as ISO/IEC 9899:2024. -Wall -Wextra enables useful warnings. Compiler defaults vary, so an explicit -std=c17 keeps the intended language rules clear.
Key Takeaways
- C source must be compiled again after each change.
- Execution starts in
main; returning zero reports success. - Build stages are preprocessing, compilation, assembly, and linking.
- Use
-std=c17 -Wall -Wextrato select the standard and enable warnings.
1.2 Finding the first cause of an error
The time at which a problem appears helps you find its cause. A program may fail to build, stop while running, or finish with the wrong answer. Those situations call for different checks.
Separate the kinds of problems
| Kind | What you see | Where to look |
|---|---|---|
| Compile-time error | No new executable | Syntax, declarations, and types |
| Warning | A likely problem; building usually continues | The expression named in the message |
| Link error | Object code exists, but the executable cannot be completed | Unresolved function names or required libraries |
| Run-time error | A problem appears during execution | Operations and values reached by that input |
| Logic error | The program runs but gives an unintended answer | Conditions, formulas, and updates |
A warning can identify invalid code or undefined behavior as well as an ordinary mistake. Successful compilation does not guarantee a correct program.
Read a diagnostic
A compiler message normally gives a source location, a severity, and a description. A message such as hello.c:5:16: error: expected ';' after expression points to a missing semicolon near line 5. Sometimes the compiler notices the problem on the next line. Inspect the reported line and the one just before it.
Fix the first error first, then compile again. An unclosed quote can make later lines look like part of a string; an unmatched brace can make later declarations appear in the wrong block. Several messages may disappear after one repair. Do not start by changing every line mentioned in the output.
For this fragment:
int total = 6
printf("%d\n", total);
The declaration needs a semicolon after 6. The printf line may be where the compiler first detects that the previous declaration did not end. Also check exact spelling, balanced parentheses and braces, and the header that declares a library function. A call to printf needs <stdio.h>; undeclared function calls are not allowed in C99 and later.
Take warnings seriously
Build with -Wall -Wextra. These options help catch an assignment in a condition, unused variables, signed–unsigned comparisons, and mismatched output formats. For example, printf("%d\n", 3.5); asks for an integer but passes a double; the format does not convert the argument for you. Fix the format to %f or deliberately convert the value. -Werror makes warnings stop the build.
Understand undefined behavior
Undefined behavior means C places no requirements on the program's behavior. Signed integer overflow, integer division by zero, and some reads of uninitialized variables can cause it. A program might stop, print an unexpected value, or appear to work. Compiler settings can change what happens.
A message such as "Floating point exception" can even result from integer division by zero on some systems. Diagnose the operation and input, not just the wording of an operating-system message. Check a divisor before executing the division.
Trace a wrong result
For a logic error, predict a small case by hand and compare it with the actual output. Add temporary printf calls to show a running total or the value tested by a condition. Change one cause at a time and rebuild after each change. Test ordinary values and boundaries such as zero, one, a negative input, and the largest expected input. A trace shows where the program first diverges from your prediction.
Key Takeaways
- Fix the first diagnostic and rebuild before chasing later messages.
- A warning can identify undefined behavior; compilation alone does not establish correctness.
- Match format conversions to argument types.
- For a wrong result, trace a small input and compare intermediate values.
- Test boundaries as well as ordinary inputs.
2Store data in variables of the basic types
A type determines how a stored value is interpreted and what values it can hold. Begin with declarations, starting values, and the names available inside a block. Then choose integer types by range and floating types by precision as well as range. Character codes and Boolean values fit into this picture too. Reading literals and checking limits prepares you to predict what a calculation can represent before you write it.
2.1 Variables, names, and fixed values
A variable gives a stored value a name. Its type tells C how to interpret that storage. You declare the type before using the variable, and it remains that type throughout its lifetime.
Give each variable a starting value
int count = 0;
double length_m = 1.8;
count = count + 1;
The first two lines declare and initialize variables. The third assigns a new value to an existing variable: it reads the current count, adds one, and stores the result back. Assignment is an action, so count = count + 1 is meaningful even though it would not be a mathematical equation.
Initialization happens in a declaration; assignment can happen later. In int a, b = 5;, only b is initialized. A local a has an indeterminate value until you store one. Reading it can produce unpredictable behavior, including undefined behavior. Initialize counters and totals when you declare them, such as int total = 0;.
Assignment copies the value at that moment:
int width = 4;
int area = width * 3;
width = 7;
printf("%d %d\n", width, area);
This prints 7 12. The assignment to area calculated 12 once. It did not create a formula that recalculates whenever width changes. To update area, assign a new calculation to it.
Choose readable names
An identifier can contain letters, digits, and underscores, but cannot start with a digit or be a keyword. height_cm and second_place work; 2nd_place does not, and my-score is read as subtraction. Names are case-sensitive, so total and Total differ. Names that include a meaning or unit make formulas easier to inspect.
Follow block scope
A local name is available from its declaration to the end of its block. Since C99, declarations can appear among other statements.
int count = 2;
{
int count = 9;
printf("%d\n", count);
}
printf("%d\n", count);
The inner declaration hides the outer name while that block runs. The output is 9 and then 2, on separate lines. These are two variables, not two assignments to one variable. Declaring the same name twice in one block is an error. Reusing names in nested blocks is allowed but makes code harder to follow, so prefer names that keep each role clear.
Name fixed values
const int SECONDS_PER_MINUTE = 60;
#define MAX_HEIGHT 8
const creates a typed variable that cannot be assigned a new value through that name. A later statement SECONDS_PER_MINUTE = 90; requires a diagnostic. The initializer gives the fixed value at the declaration.
#define creates a preprocessor replacement: later occurrences of MAX_HEIGHT are replaced by the token 8 before compilation. There is no = and no semicolon in this directive. A semicolon would become part of the replacement and could break an expression such as height > MAX_HEIGHT.
A name such as SECONDS_PER_MINUTE tells a reader why 60 appears in a calculation and gives you one place to maintain it. Capital letters are a naming convention, not a rule that makes a value constant. The declaration or directive supplies that behavior.
Key Takeaways
- Initialize local variables before reading them.
- Assignment copies a value once; dependent values do not update themselves.
- A block limits where a local name can be used.
- Names are case-sensitive and cannot begin with a digit.
constsupplies a typed read-only variable;#definesupplies a token replacement.
2.2 Choosing integer, character, and Boolean types
Integer types store whole numbers exactly within a fixed range. Choose a type by the values the program must handle, including intermediate results, and by whether negative values make sense.
Sizes and ranges
The standard guarantees minimum widths: at least 8 bits for char, 16 for short and int, 32 for long, and 64 for long long. Each type in this sequence is at least as wide as the previous one. Actual sizes depend on the platform.
| Type | Typical bytes on 64-bit Linux/macOS | 64-bit Windows difference |
|---|---|---|
char | 1 | None |
short | 2 | None |
int | 4 | None |
long | 8 | 4 bytes |
long long | 8 | None |
sizeof(int) reports the size in bytes as a size_t; print it with %zu. The header <limits.h> supplies names such as INT_MIN, INT_MAX, UINT_MAX, LLONG_MAX, and CHAR_BIT, the number of bits per byte.
With N value bits, an unsigned integer has values 0 through 2ᴺ − 1. A two's-complement signed integer with N bits ranges from −2ᴺ⁻¹ through 2ᴺ⁻¹ − 1. Thus a 32-bit int ranges from −2,147,483,648 to 2,147,483,647, while a 32-bit unsigned int reaches 4,294,967,295. C23 requires two's complement; C17 also allowed other representations.
In an 8-bit two's-complement pattern, the highest bit has weight −128. 11111111 represents −1, and 10000000 represents −128. To form −5, start with 00000101, invert the bits to 11111010, and add one to get 11111011.
Characters are integers too
A char stores a character code in one byte. Use single quotes for a character constant, as in 'A'. In ASCII, 'A' has code 65 and '0' has code 48. printf("%c %d\n", 'A', 'A'); prints A 65 in ASCII. A character constant in C has type int, so sizeof('A') is sizeof(int).
Plain char can be signed or unsigned depending on the implementation. Use signed char or unsigned char when the numerical range matters. <stdint.h> offers exact-width types such as int32_t and uint8_t on platforms that support those exact widths; mainstream platforms do.
Read integer literals
A decimal constant such as 42 has type int when it fits. Suffixes request other types: 42u is unsigned, 42L is long, 42LL is long long, and 42ULL is unsigned long long. Hexadecimal uses 0x, as in 0x2A. A leading zero means octal: 052 also means 42, and 010 means 8. 09 is invalid octal. C23 adds binary notation such as 0b101010.
Truth values and type choice
In C17, include <stdbool.h> for bool, true, and false; C23 makes them keywords. A conversion to bool gives false for zero and true for every nonzero value, including 0.5. Print a Boolean with %d to see 0 or 1.
Use int for ordinary counts and loop counters. Use long long or a suitable exact-width type when the range can exceed about 2.1 billion. Unsigned types suit bit patterns, but subtraction below zero wraps to a large value, so they still need range checks.
Key Takeaways
- Use the type limits, not a guessed size for
long. - A 32-bit signed
intreaches 2,147,483,647. - Unsigned values have no negative range and wrap on underflow.
- A leading zero starts an octal integer literal.
- Plain
charhas implementation-dependent signedness. - In C17, Boolean names come from
<stdbool.h>.
2.3 Decimal values and limited precision
A floating-point type stores an approximation to a real number. It has a large range, but only a fixed number of significant digits. Range and precision answer different questions: how large a value can be, and how finely nearby values can be distinguished.
Choose a floating type
On common IEEE 754 systems, float occupies 4 bytes and carries about 6–7 significant decimal digits. double occupies 8 bytes and carries about 15–16. Their largest finite values are about 3.4 × 10³⁸ and 1.8 × 10³⁰⁸. long double is at least as precise as double, with a platform-dependent format. double is a useful default for measured quantities.
Constants such as 2.0, 3.14, and 1e-3 are double; 1e-3 means 0.001. Add f for float, as in 2.5f, or L for long double. The constant 2 is an integer, so it can lead to integer division where 2.0 would produce floating-point division.
See the stored approximation
Many decimal fractions have no exact finite binary form. On a common double-precision system:
printf("%.20f\n", 0.1);
printf("%.17f\n", 0.1 + 0.2);
The outputs are 0.10000000000000000555 and 0.30000000000000004. The extra digits expose the stored approximation. They explain why 0.1 + 0.2 == 0.3 can be false.
Use a tolerance when comparing calculated measurements:
#include <math.h>
/* inside main */
if (fabs(a - b) < 1e-9) {
printf("Close enough\n");
}
fabs returns an absolute difference. Choose the tolerance for the scale and precision of the values; 1e-9 is an example, not a universal setting. Some Linux builds need -lm at the end of the compiler command to link math functions.
Precision affects large whole numbers
A binary double represents every integer through 2⁵³ exactly. A binary float stops representing every integer above 2²⁴ = 16,777,216: assigning 16,777,217 to such a float gives 16,777,216. More range does not mean every whole-number step remains available.
Printing rounds the stored binary value. printf("%.2f\n", 2.675); commonly prints 2.67, because the stored value is slightly less than 2.675. For exact amounts in whole cents, use an integer such as long long cents = 1999;, then format the display at the end.
Convert and round deliberately
Conversion to an integer discards the fraction toward zero: (int)3.99 is 3, and (int)-3.99 is −3. round from <math.h> returns a floating value rounded to the nearest whole number, with halves away from zero: round(-2.5) gives −3.0. Convert that result to an integer only when it fits. A floating-to-integer conversion whose whole part is out of range has undefined behavior.
Under IEEE arithmetic, results outside the finite range can become infinity. Nonzero floating division by zero can produce infinity; zero divided by zero produces NaN, meaning "not a number." A NaN compares unequal even to itself. These are IEEE floating-point behaviors; integer division by zero has undefined behavior.
Key Takeaways
- Floating types have limited precision as well as limited range.
- Compare calculated measurements with a suitable tolerance.
- Printing rounds the stored approximation.
- Floating-to-integer conversion truncates toward zero and requires a representable result.
3Display output and read input with printf and scanf
A program needs a clear exchange of text and values. Format output so a reader can distinguish labels, fields, and rounded measurements, then match each input conversion to the variable receiving it. Reading text has several possible outcomes: all requested values, only some values, or no conversion at all. Checking those outcomes and the unread text makes later calculations depend on values the program has actually obtained.
3.1 Printing values with the intended format
printf turns values into text using a format string. Ordinary characters print as written. A conversion beginning with % tells it how to display the next argument.
Match each conversion to its argument
printf("%d samples, %.2f kg\n", 4, 1.5);
This prints 4 samples, 1.50 kg and a newline. The conversions correspond to arguments in order. A format does not convert the type of the supplied argument to whatever type you intended.
| Value type or display | Conversion |
|---|---|
int | %d or %i |
unsigned int | %u |
long, long long | %ld, %lld |
size_t | %zu |
double, or a promoted float | %f, %e, %g |
long double | %Lf |
| Character, string literal | %c, %s |
| Unsigned hexadecimal, octal | %x or %X, %o |
A float argument is promoted to double in printf, so both use %f; %lf is equivalent here. Passing 3 to %f is a type mismatch, not a request to print 3.0. Use 3.0 or (double)3. A mismatched type or a missing argument can cause undefined behavior. Build with warnings enabled so literal format strings are checked.
Set width and precision
A width is a minimum number of characters, including a sign and decimal point. A floating precision after the dot controls decimal places for %f. In %8.2f, eight is the minimum width and two is the number of places after the decimal point. A wider number is printed in full; width does not cut it off.
With the integer 42, %5d gives 42, %-5d gives 42 , %05d gives 00042, and %+d gives +42. Without a precision, %f prints six decimal places.
printf("%-6s%8s\n", "Item", "Price");
printf("%-6s%8.2f\n", "Tea", 2.5);
printf("%-6s%8.2f\n", "Cake", 12.75);
Each label occupies a left-aligned field of six, followed by a right-aligned price field of eight. Both prices have two decimal places, so their decimal points line up. Spaces come from the field widths, not from automatic separation between arguments.
Scientific and general displays
%e uses scientific notation: 12345.678 prints as 1.234568e+04 at the default precision. For %g, precision counts significant digits, with six by default. It uses scientific notation when the decimal exponent is below −4 or at least the precision, and removes trailing zeros. Thus %g prints 0.0001 as 0.0001, 0.00001 as 1e-05, and 1000000.0 as 1e+06. It does not simply choose the shortest possible spelling.
Print special characters
Inside a C string, \n means newline, \t means tab, \\ means a backslash, and \" means a quote. A percent sign in a format is %%: printf("100%%\n"); prints 100% and a newline.
printf adds no spaces or line endings on its own. printf("Count:"); printf("%d", 4); prints Count:4. A terminal may delay text without a newline; fflush(stdout); sends a pending prompt before input is requested.
Key Takeaways
- Match conversions to actual argument types.
- Field width is a minimum;
%fprecision counts decimal places. %gprecision counts significant digits.- Write
%%for a literal percent sign in a format. printfadds no spacing or newline unless the format supplies it.
3.2 Reading input and checking what was read
scanf reads text, converts it, and stores values in variables. Check its result before using those variables. Text that begins with digits can be only partly accepted, so a successful conversion and a fully valid input line are different things.
Supply a destination of the right type
int count;
double mass_kg;
if (scanf("%d %lf", &count, &mass_kg) != 2) {
return 1;
}
The & passes each variable's address so scanf can store into it. %d needs an int, %u an unsigned integer, %ld a long, and %lld a long long. For floating input, %f needs a float and %lf a double. Unlike printf, these two conversions are not interchangeable. A missing & or wrong destination type can cause undefined behavior.
%d reads decimal. %i also recognizes hexadecimal and leading-zero octal input; with %i, the text "010" means 8. Use %d for ordinary decimal counts.
Check the return value
scanf returns the number of assignments completed. For two requested values, success means 2. A failed conversion leaves its destination unchanged. It may still be uninitialized, so do not use it unless the return count confirms success. EOF means input ended or an input error occurred before the first conversion completed.
For scanf("%d:%d", &h, &m), the colon must appear literally. Input "14:30" fills both variables. Input "14 30" fills h, then fails to match the colon and leaves m unchanged. The return value is 1.
Understand whitespace and partial input
Numeric conversions skip leading spaces, tabs, and newlines. %c reads the very next character, including a leftover newline. Use scanf(" %c", &choice) to skip whitespace before reading a visible character. Avoid a trailing newline in the format: it can make the call wait for a later non-whitespace character.
With %d, "12abc" stores 12 and leaves "abc" unread. "3.7" stores 3 and leaves ".7". "abc" stores nothing and returns 0. Calling the same conversion again without consuming bad input can fail forever on the same character.
Recover from a failed conversion
int n, result;
while ((result = scanf("%d", &n)) != 1) {
if (result == EOF) {
return 1;
}
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}
printf("Try again: ");
fflush(stdout);
}
getchar returns an int, allowing it to represent both a character and EOF. The inner loop discards the failed line. The outer loop retries, but still accepts a numeric prefix such as 12 in "12abc". Requiring the next character to be '\n' checks the stricter rule of one integer immediately followed by a newline; that rule also rejects trailing spaces. A policy that permits spaces or a final line ending at EOF needs to handle those explicitly.
After a successful conversion, check the allowed range, such as 1 through 8 for a height setting. scanf does not safely reject an out-of-range numeric value: a value too large for its destination has undefined behavior. Use test inputs that fit the chosen type. To calculate a result such as mass divided by height squared, validate positive measurements and keep the square grouped as height_m * height_m.
Key Takeaways
- Pass variable addresses and use
%lfto read adouble. - Require the return count to match the number of values needed.
- A successful conversion can leave extra input unread.
- Consume bad input before retrying a failed numeric conversion.
- Check allowed values after reading; input conversion has its own type range limits.
- Use
" %c"when a character read should skip whitespace.
4Compute with operators and expressions
An expression combines values, types, and operators. Trace integer division and updates first, then follow the conversions that occur at each operation. Comparisons and logical guards control whether a calculation is reached, while bitwise operations act on individual bits. Finally, distinguish the grouping set by precedence from the sequencing needed when an expression changes a variable. Both the value and the type of an intermediate result matter.
4.1 Arithmetic, assignment, and one-step updates
Every C expression has a value and a type. The type determines how an operation works. Integer division, for example, cannot preserve a fractional result even if you later store it in a floating-point variable.
Quotients and remainders
+, -, and * add, subtract, and multiply. / divides, and % gives an integer remainder. Multiplication, division, and remainder bind more tightly than addition and subtraction.
When both division operands are integers, C truncates the quotient toward zero. 17 / 5 is 3, -7 / 2 is −3, and 7 / -3 is −2. The remainder satisfies the division identity: multiplying the quotient by the divisor and adding the remainder gives the dividend. Thus -7 % 2 is −1 and 7 % -3 is 1. A nonzero remainder has the dividend's sign.
To check evenness, use n % 2 == 0. To check oddness for positive or negative integers, use n % 2 != 0; checking for exactly 1 misses negative odd values. % needs integer operands; <math.h> provides fmod for floating remainders. An integer divisor of zero makes both / and % undefined, so check it before the operation.
For 135 minutes, 135 / 60 gives two whole hours and 135 % 60 gives 15 remaining minutes. For a positive decimal integer, % 10 extracts its last digit and / 10 removes that digit: 4096 gives 6 and 409.
Keep fractions when you need them
int correct = 45, total = 60;
double percent = 100.0 * correct / total;
The floating constant makes the multiplication floating-point, and the division gives 75.0. In correct / total * 100, the first operation is integer division and gives zero. Multiplying that zero cannot recover the lost fraction. Integer multiplication first, correct * 100 / total, gives 75 here, provided the product fits.
Store a new value
An assignment evaluates its right side and stores the converted value on the left. The assignment expression has that stored value, so a = b = 0; sets both variables to zero, grouping from the right.
Compound assignments combine a calculation and storage: x += 5 adds five to x, and x /= 2 divides x by two using its current type. The whole right operand belongs to the calculation. If x is 4, x *= 2 + 1 makes it 12, because it multiplies by three.
Prefix and postfix updates
++ adds one and -- subtracts one. On a line by themselves, i++; and ++i; have the same final effect. Inside a larger expression, postfix yields the old value; prefix yields the new value.
| Start | Statement | Final values |
|---|---|---|
a = 5 | y = a++; | y = 5, a = 6 |
a = 5 | y = ++a; | y = 6, a = 6 |
Keep modifications easy to order. i = i++ + 1; and printf("%d %d\n", n, n++); have unsequenced conflicting uses of the same variable and undefined behavior. Separate the read, the update, and the output into statements when their order matters.
Key Takeaways
- Integer division truncates toward zero.
- A nonzero integer remainder has the dividend’s sign.
- Use a floating operand before division if you need a fraction.
- Postfix yields the old value; prefix yields the updated value.
- Keep conflicting reads and updates in separate statements.
4.2 Types are decided before a result is stored
C converts values as each operator is applied. The destination variable receives the finished result; it does not reach back into the expression and change earlier operations.
Convert before dividing
int sum = 7, count = 2;
double a = sum / count;
double b = (double)sum / count;
double c = (double)(sum / count);
a becomes 3.0: the two integer operands produce integer 3, which is then converted. b becomes 3.5: the cast converts sum before division, so count is converted to double too. c becomes 3.0: its cast is outside the division and arrives after the fraction has been discarded.
The same rule applies within a longer expression. 7 / 2 * 2.0 gives 6.0, because the first result is integer 3. 7 / 2.0 * 2 gives 7.0. Work through the operators in their grouping order, marking the type of each intermediate value.
Find a common arithmetic type
If either operand is floating-point, arithmetic generally converts to the widest floating type involved: long double, then double, then float. For integers, small types first undergo integer promotion. On platforms with 8-bit char, 16-bit short, and 32-bit int, every value of the smaller types fits in int, so those types promote to int.
With unsigned char a = 200, b = 100;, a + b is the integer 300 on an 8-bit-byte, 32-bit-int system. It does not wrap at 255 during addition. Storing 300 into an 8-bit unsigned character reduces it to 44. Promotion can also produce signed overflow: two unsigned short values of 50,000 multiply as int, but 2,500,000,000 exceeds a 32-bit INT_MAX.
For signed and unsigned types, rank and representable range govern the common type. int with unsigned int converts to unsigned. A higher-rank signed type can keep the operation signed when it represents every value of the unsigned type; otherwise unsigned arithmetic can still result.
The signed–unsigned trap
With a 32-bit unsigned integer, -1 < 0u is false. The signed −1 converts to the unsigned value 4,294,967,295 before comparison. Likewise, 2u - 10 gives 4,294,967,288. A sizeof result is unsigned too, which matters when comparing it with a possibly negative count. Enable warnings and keep related quantities in compatible types.
Storage conversions can lose information
A floating-to-integer conversion truncates toward zero. Its whole part must fit, or behavior is undefined. An integer converted to an N-bit unsigned type is reduced modulo 2ᴺ; −1 becomes the maximum unsigned value. Under C17, an out-of-range integer conversion to a signed type has an implementation-defined result or raises an implementation-defined signal. That conversion is distinct from undefined signed arithmetic overflow.
Character constants also take part in arithmetic. '7' - '0' gives 7 because C guarantees consecutive codes for decimal digits. In ASCII, letters are consecutive too, but do not assume that property for every character set. For integer formulas, multiplying before dividing can preserve useful information only if that product fits its calculation type.
Key Takeaways
- The destination type does not change earlier arithmetic.
- Cast an operand before the operation that needs a wider or floating type.
- Small integers promote before arithmetic.
- Mixed signed and unsigned operands can change comparisons and subtraction.
- Out-of-range conversion and signed arithmetic overflow follow different rules.
- Track intermediate types as well as final values.
4.3 Conditions and bit patterns
A condition asks whether something is true. A bitwise operation works on the individual bits of an integer. C gives these jobs different operators, even when their symbols look similar.
Compare values and combine tests
The comparisons <, >, <=, >=, ==, and != produce an int: 1 for true and 0 for false. In a condition, zero is false and any nonzero value is true. Thus if (count) tests whether count is nonzero. == compares two values; = assigns a new one.
&& means both conditions must be true, || means at least one must be true, and ! reverses truth. They return 0 or 1. A range from 1 through 8 needs two comparisons:
height >= 1 && height <= 8
1 <= height <= 8 does not express that range in C. It groups as (1 <= height) <= 8, so the second comparison receives 0 or 1 and is always true. To test outside the range, write height < 1 || height > 8.
Negating a compound condition changes both parts: !(a && b) is equivalent to !a || !b, and !(a || b) to !a && !b. These are De Morgan's laws. They explain why being outside an interval needs an "or," even though being inside needs an "and."
Put a guard before the risky operation
if (d != 0 && n / d > 2) {
printf("Above two\n");
}
&& evaluates its left side first. If that side is false, it skips the right side. || skips its right side when the left side is true. This short-circuit evaluation lets a guard protect a later operation. Reversing the two parts above would divide before checking the divisor. For signed integers, a separate range issue exists when INT_MIN is divided by −1 on a two's-complement system; guard that pair as well when arbitrary values are possible.
A leap-year test combines divisibility and exceptions: (y % 4 == 0 && y % 100 != 0) || y % 400 == 0. The year 2000 passes the 400 test; 1900 fails because the century exception applies.
Work one bit at a time
Bitwise & keeps bits set in both operands; | keeps bits set in either; ^ keeps bits that differ; ~ inverts bits. With 12 (1100) and 10 (1010), the results are 8, 14, and 6 for &, |, and ^.
Shifts move bits. For nonnegative values where the result fits, x << k multiplies by 2ᵏ, and x >> k discards k low bits, dividing by 2ᵏ with the remainder discarded. Use unsigned types for bit work. Under C17, shifting by a negative count or at least the promoted type's width is undefined; left-shifting a negative signed value is undefined, and right-shifting one is implementation-defined.
Bit positions start at zero. flags | (1u << 3) sets bit 3; flags & ~(1u << 3) clears it; (flags >> 3) & 1u reads it. 1 & 2 is 0 because no bit is shared, while 1 && 2 is 1 because both values are nonzero. Bitwise operators evaluate both operands and cannot replace logical guards.
Key Takeaways
- Comparisons and logical operators produce 0 or 1.
- A range test joins two comparisons with
&&. - Short-circuit guards must come before the protected operation.
- Use logical operators for truth and unsigned bitwise operators for masks.
- Bit positions start at zero; shift counts must be within the promoted type’s width.
4.4 Grouping expressions and ordering effects
Two questions help you read an expression: which operands belong to each operator, and when do operations with side effects happen? Precedence and associativity answer the grouping question. They do not generally fix the order of operand evaluation.
Follow precedence
Operators higher in this table bind more tightly. Each row is lower than the preceding row; the operators separated by "then" have distinct levels.
| From high to low | Operators |
|---|---|
| Postfix | x++, x--, calls |
| Unary | ++x, --x, +, -, !, ~, casts, sizeof, address & |
| Multiplicative | *, /, % |
| Additive | +, - |
| Shift | <<, >> |
| Relational | <, <=, >, >= |
| Equality | ==, != |
| Bitwise | &, then ^, then ` |
| Logical | &&, then ` |
| Conditional | ?: |
| Assignment | =, +=, and other compound assignments |
| Comma | , |
Parentheses override grouping. 2 + 3 * 4 is 14; (2 + 3) * 4 is 20.
Use associativity at equal levels
Most binary arithmetic operators group left to right. 10 - 4 - 3 means (10 - 4) - 3, which is 3. 100 / 10 / 2 gives 5. Assignment groups right to left, so a = b = 0 stores zero in both variables. Grouping describes the expression structure; it does not establish a general left-to-right execution rule.
For int a = 3, b = 4, c = 5;, trace a + b * c > 20 && c % 2 == 1. Multiplication gives 20, addition gives 23, and 23 > 20 gives 1. Remainder gives 1, and its equality test gives 1. The final 1 && 1 is 1.
Protect bit tests and assignments with parentheses
x & 1 == 0 means x & (1 == 0), so it always produces zero. Write (x & 1) == 0 to test the low bit. Addition also binds more tightly than shifting: 1 << 2 + 1 is 1 << 3, or 8.
Assignment binds below comparison. a = b == c stores the comparison's 0 or 1 in a. In while ((ch = getchar()) != '\n'), the inner parentheses make the assignment receive the character first. Without them, ch would receive a comparison result.
Choose one value or sequence two actions
condition ? first : second evaluates the condition and then only the selected value expression. max = (a > b) ? a : b; chooses a value; an if can choose a block of actions. The comma operator evaluates its left expression first and yields its right expression's value. It can coordinate two updates in a for header. Commas between function arguments are separators and do not establish that order.
Keep side effects separate
C leaves many operand and argument orders unspecified. n++ * n++ has unsequenced changes to the same variable, so it has undefined behavior. Extra parentheses do not fix the missing sequencing. Use separate statements when one action must precede another. &&, ||, ?:, and the comma operator provide specific sequencing rules; ordinary arithmetic precedence supplies no such guarantee.
Key Takeaways
- Precedence determines grouping; it does not generally determine evaluation order.
- At one arithmetic level, grouping is usually left to right.
- Parenthesize a bitwise result before comparing it.
- The conditional operator evaluates only the selected value expression.
- The comma operator sequences operands; argument-separating commas do not.
- Separate conflicting updates to avoid undefined behavior.
5Control program flow with conditions and loops
Control flow determines which statements run and how often. Use conditions to select a branch, exact integer cases to select a switch entry, and loops to repeat a task until its stopping rule is met. Traces make the order visible: starting values, tests, body statements, and updates. Pay particular attention to boundary inputs and to the destination of break or continue inside nested constructs, where a small syntax change can alter the whole path.
5.1 Choosing the right branch
An if statement chooses whether to run an action. Its condition must be inside parentheses. Zero means false; every nonzero value means true.
Use braces to show the branch
if (temperature < 0) {
printf("Freezing\n");
} else {
printf("At or above freezing\n");
}
The first block runs for a negative temperature. At exactly zero, the comparison is false and the else block runs. Read the boundary directly from <, <=, >, or >=; the message should agree with that boundary.
Without braces, only the next statement is controlled:
if (temperature > 30)
printf("Hot\n");
printf("Drink water\n");
The second message runs at every temperature. Indentation helps people, but does not change the grammar. Braces let you add a second statement to a branch without accidentally leaving it outside.
Order an else-if chain
An if–else if–else chain runs the first branch whose condition is true, then skips the rest. For score bands, begin with the highest lower bound:
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else {
grade = 'F';
}
A score of 85 fails the first test and passes the second. A score of 90 passes the first. If you tested score >= 70 first, 85 would enter that branch and never reach the intended B branch. Separate if statements behave differently: each condition is tested independently, so several blocks may run.
Watch two small syntax mistakes
if (x = 5) assigns 5, then treats that nonzero value as true. Use if (x == 5) to compare. With if (x = 0), assignment still happens but the condition is false. Warnings help reveal accidental assignments in conditions.
In if (x > 0);, the semicolon is an empty controlled statement. A block placed after it runs independently, even when x is negative. Do not add a semicolon between an if condition and its intended block.
Pair each else explicitly
An else attaches to the nearest earlier if that does not already have an else. In nested decisions, use braces to make the intended pairing clear. Nesting is useful when failed tests need different messages: first test whether a measurement is valid, then test whether its value is in the desired band. Combining both with && works when only one shared action is needed.
Put exceptions before broad rules
Leap years provide a useful ordered decision. First test divisibility by 400, which means leap year. Next test divisibility by 100, which means not a leap year. Then test divisibility by 4; otherwise the year is not a leap year. For 2000, the first branch applies. For 1900, the second applies. Checking only divisibility by four would miss the century exception. The order matters because broad rules can include cases with a more specific exception.
Key Takeaways
- An else-if chain runs only its first true branch.
- Braces control which statements belong to a branch.
==compares;=assigns.- A semicolon after an
ifcondition creates an empty branch.
5.2 Selecting fixed cases with switch
A switch chooses a starting point using one integer value. It is useful for a command character or menu number with several fixed alternatives. Execution can then continue into later cases unless you stop it.
Match a case and leave it
switch (choice) {
case 1:
printf("Start\n");
break;
case 2:
printf("Pause\n");
break;
default:
printf("Unknown choice\n");
break;
}
With choice equal to 2, the program starts at case 2, prints Pause, and leaves the switch at break. If no case matches, execution starts at default. Without a default, an unmatched value skips the switch.
The controlling expression must have an integer type, including char or an enumeration. Each case must be a distinct integer constant expression, such as 3, 'q', or a suitable #define name. In C17, a const int variable is not an integer constant expression for a case label. Floating values and strings cannot be switch controlling values.
Understand fall-through
Case labels mark entry points, not separate blocks that automatically end. Remove the first break above and a choice of 1 prints both Start and Pause. The program does not test whether choice is 2 before continuing through that case. Execution stops only at a control transfer or the end of the switch.
Several labels can share one action:
case 'y':
case 'Y':
printf("Yes\n");
break;
Both characters enter the same action. When one case deliberately does work and continues into another, explain the intention with a comment. C23 also offers [[fallthrough]];. Warnings can help find a missing break, but grouping labels deliberately needs no repeated action.
Give case-local variables a block
Under C17, a declaration cannot directly follow a case label. Use braces around that case's statements:
case 3: {
int delay = 5;
printf("%d\n", delay);
break;
}
The block also keeps delay local to that case. C23 relaxes the label-before-declaration restriction, but braces still make the scope clear.
Choose between switch and if
Use switch for exact integer alternatives. Use if–else if for inequalities, conditions involving several variables, or floating measurements. Equal-width score bands can sometimes be converted to exact cases by integer division. With scores from 0 through 100, score / 10 gives 8 for 85; cases 9 and 10 can share the top band. Validate that domain so negative or excessive scores are not silently assigned a band.
A switch inside a loop
A break leaves the nearest enclosing switch or loop. Inside a switch that is itself inside a loop, the break ends only the switch. To end the menu loop, set a flag checked by its condition, or return from main when the program should end. A continue in that switch applies to the enclosing loop, sending it to its next iteration. Trace which construct each transfer targets before deciding whether the menu stops.
Key Takeaways
- A switch selects an entry label using one integer value.
- Execution falls through until
break, another control transfer, or the end. - Case values must be distinct integer constant expressions.
- Use a block for a case-local declaration in C17.
- A switch’s
breakdoes not also end an enclosing loop.
5.3 Repeating until the data says stop
A while loop tests a condition before each pass. A do–while loop tests after the body, so it runs at least once. Choose the form that puts the test where the program needs it.
Trace a condition-controlled calculation
int n = 4096, sum = 0;
while (n > 0) {
sum += n % 10;
n /= 10;
}
n % 10 extracts a digit, and n /= 10 removes it. The values after the passes are:
| Pass | sum | n |
|---|---|---|
| 1 | 6 | 409 |
| 2 | 15 | 40 |
| 3 | 15 | 4 |
| 4 | 19 | 0 |
The next condition is false, so the loop ends with a digit sum of 19. If n begins at zero, the body runs zero times and the sum remains zero. Negative inputs also skip this loop, so a program that accepts them needs a defined policy.
A counting loop sets a counter, tests it, and updates it. Other loops make progress by reading data or reaching break. For repeated integer halving of 100, the values are 50, 25, 12, 6, 3, and 1: six passes are needed to reach one.
Let input control repetition
int x, sum = 0, count = 0;
while (scanf("%d", &x) == 1 && x != 0) {
sum += x;
count++;
}
if (count != 0) {
printf("%.2f\n", (double)sum / count);
}
For input "5 8 3 0", the sentinel zero ends the loop and is not added. The sum is 16, the count is 3, and the displayed average is 5.33. The return-count test protects use of x; the count test protects the division if the first value is zero. This calculation also requires inputs and totals that fit in int.
Without the sentinel check, while (scanf("%d", &x) == 1) stops on failed numeric conversion or end of input. At a terminal, Ctrl+D normally signals end of input on Linux and macOS; on Windows use Ctrl+Z followed by Enter.
Run once before testing
int height;
do {
printf("Height (1-8): ");
fflush(stdout);
if (scanf("%d", &height) != 1) {
return 1;
}
} while (height < 1 || height > 8);
The prompt and read happen before the range test. Values zero and nine repeat; values one and eight are accepted. The semicolon after while (...) is required in this form. The conversion check ends this example on nonnumeric input; the range test alone cannot handle failed conversion.
Make progress on every continuing path
break exits the innermost loop immediately. continue skips the rest of the body and returns to the condition in a while loop. If an update lies below a continue, it is skipped too. A loop can then keep testing the same value forever.
Before running a loop, identify what changes and why the stop condition will eventually be reached. A deliberate while (1) can stop through break; a missing counter update is a different situation. Ctrl+C stops a program stuck in a terminal. Test the smallest input and the value at the stopping boundary to catch zero-pass and one-extra-pass mistakes.
Key Takeaways
whiletests first;do–whiletests after at least one pass.- A sentinel stops input without becoming part of the data.
- Initialize accumulators outside the loop.
- A continuing path must make progress toward the stop condition.
- A
whilecontinue can skip a later update. - Check for zero data items before calculating an average.
5.4 Counting passes and combining loops
A for loop keeps a counting pattern in one header. Nested loops repeat an inner task for each outer pass. Both become easier to check when you list the actual counter values and identify every exit.
Follow the header in order
for (int i = 0; i < 3; i++) {
printf("%d ", i);
}
Initialization runs once. Then the condition is tested, the body runs if it is true, and the update runs before the next test. The output is 0 1 2 . The variable declared in the header is local to the loop; declare it before the loop if you need its final value afterward.
For nonnegative n, i = 0; i < n; i++ runs n times, visiting zero through n − 1. With i <= n, it runs one extra time. For a positive step s from a through an included b, the count is (b - a) / s + 1 using integer division, provided the arithmetic and counter updates fit. From 2 through 20 in steps of 3, the values are 2, 5, 8, 11, 14, 17, 20: seven passes.
Keep the accumulator outside
int total = 0;
for (int i = 1; i <= 100; i++) {
total += i;
}
The final total is 5050. The initialization must happen once before the loop. Moving int total = 0; into the body creates a fresh total on every pass, losing the earlier work.
Choose counters that can reach the end
A semicolon directly after the for header makes the body empty. A following block then runs once after the loop. Use braces attached directly to the header to make the body visible.
An unsigned countdown with i >= 0 cannot stop that way: unsigned values are always nonnegative, and decrementing zero wraps to the maximum. A floating counter that repeatedly adds 0.1 can skip the exact value 1.0, so x != 1.0 is unreliable as a stopping rule. Count with an integer and calculate a measurement such as x = i / 10.0 inside the body.
Build rows with nested loops
For each outer pass, the inner loop starts and runs its own sequence. Three outer passes and four fixed inner passes execute the inner body twelve times. When the inner bound depends on the row, count each row separately.
int height = 3;
for (int row = 1; row <= height; row++) {
for (int s = 0; s < height - row; s++) printf(" ");
for (int h = 0; h < row; h++) printf("#");
printf("\n");
}
The rows contain two spaces and one hash, one space and two hashes, then three hashes. The newline belongs after the inner loops, so each outer pass makes one row.
Skip work or stop early
continue in a for loop goes to the update and then the next test. A loop adding only odd values from 1 through 9 can skip even values and finish with 25. break leaves the innermost loop immediately. In nested loops, an inner break does not stop the outer loop.
A for loop is often the clearest choice for a known counter range; while suits data-controlled repetition, and do–while suits work that must happen before testing. Rewriting a for loop as a while loop requires special care with continue, because the update must still happen on that path.
Key Takeaways
- A
forloop runs initialization once and update after each continuing pass. - Check the visited values to catch off-by-one errors.
- Use integer counters with enough range.
- Nested loops restart the inner sequence for each outer pass.
- An inner
breakleaves only the inner loop. - A
forcontinue still runs the header update. - Keep a running total’s initialization outside the repeated body.
6Recognize and prevent integer overflow
Finite integer ranges affect calculations even when the final answer looks small enough. Distinguish unsigned wraparound, signed overflow, and values lost during conversion, then inspect the types of intermediate operations. Prevention starts before a risky calculation: widen an operand, prove bounds, or use a checked operation. Counter ranges and boundary tests help you find failures that ordinary small inputs and short test runs can miss.
6.1 What happens when an integer does not fit
An integer's range is finite. A mathematical answer can be valid and still be too large for the C type used to calculate it. The consequences depend on whether the operation is unsigned, signed, or a conversion.
Unsigned arithmetic wraps
An N-bit unsigned result is reduced modulo 2ᴺ. With a 32-bit unsigned int, UINT_MAX + 1u gives zero, 0u - 1u gives 4,294,967,295, and 3u - 5u gives 4,294,967,294. The result is defined, but it can be wrong for the task: subtracting five items from a stock of three should not produce billions of items.
For uint8_t b = 255;, b = b + 1; stores zero. On a typical 32-bit-int system the addition first occurs as int and produces 256; converting that value back to an 8-bit unsigned type produces zero. Small unsigned storage does not mean every intermediate operation has that small type.
Signed arithmetic overflow is undefined
With a 32-bit int, adding one to INT_MAX needs 2,147,483,648. C does not define the signed arithmetic result. Hardware may appear to wrap to INT_MIN, but the compiler can optimize based on signed overflow never occurring. A check such as if (x + 1 < x) can be removed for signed integers, because every defined signed addition of one gives a larger value.
Do not calculate first and then inspect the damage. An undefined operation has already happened before a later test runs.
A wider destination can be too late
int a = 50000, b = 50000;
long long product = a * b;
The multiplication is performed as int, so it overflows on a 32-bit-int system. The destination's width cannot repair it. (long long)a * b changes the operation's type before multiplication and produces 2,500,000,000.
Factorials grow quickly: 12! is 479,001,600 and fits in a 32-bit signed integer; 13! is 6,227,020,800 and does not. With 32-bit unsigned arithmetic, 13! wraps to 1,932,053,504. A 64-bit signed long long holds 20! = 2,432,902,008,176,640,000, but not 21!.
Conversion has separate rules
Converting an integer to an unsigned type uses modulo reduction. Under C17, converting an out-of-range integer to a signed type gives an implementation-defined result or signal. Converting a floating value whose truncated whole part does not fit in the integer destination has undefined behavior. Reading an out-of-range number through a numeric scanf conversion is also undefined.
Range failures can have large effects
Ariane 5's first flight on June 4, 1996 was lost after a floating value could not fit a 16-bit signed integer in its inertial reference software. That program was written in Ada; the lesson about checking conversion ranges applies to C too.
A signed 32-bit seconds counter starting at the Unix epoch reaches 2,147,483,647 at 03:14:07 UTC on January 19, 2038. Systems using that time representation need a wider one. In 2015, the FAA also addressed Boeing 787 generator-control software that could overflow after 248 days of continuous power. These failures show why both long running time and intermediate values belong in a range estimate.
Floating-point limits are different: common IEEE systems can produce infinity, and a float can lose whole-number precision before reaching its maximum. Unsigned integer wrap produces an ordinary numeric value; signed overflow offers no reliable result at all.
Key Takeaways
- Unsigned arithmetic uses modulo reduction.
- Signed arithmetic overflow has undefined behavior.
- A wider destination cannot fix a narrow intermediate operation.
- Integer conversion, floating conversion, and arithmetic overflow have different rules.
- Estimate values over the full operating duration.
6.2 Preventing overflow before it happens
Start with the largest value each calculation can produce. Include intermediate products and counter updates, not just the final answer. Then choose a type or a pre-check that keeps every operation in range.
Widen the calculation early
A 32-bit signed integer holds about 2.1 billion, while a 64-bit signed long long holds up to 9,223,372,036,854,775,807. A millisecond counter reaches the 32-bit signed limit in about 24.9 days. Use a 64-bit type when a duration can run longer.
Write (long long)a * b to widen an integer product before it is evaluated. Constants matter too: 86400LL * days starts the multiplication in long long. (long long)(a * b) converts only after the narrower product, so it cannot prevent that product's overflow. Wider types still have limits; check the largest possible values against them.
Check addition using subtraction
For int a and int b, include <limits.h> and test:
if ((b > 0 && a > INT_MAX - b) ||
(b < 0 && a < INT_MIN - b)) {
printf("Sum out of range\n");
} else {
int sum = a + b;
printf("%d\n", sum);
}
Each subtraction is protected by the sign of b, keeping the check itself representable. For 2,000,000,000 plus 200,000,000, INT_MAX - b is 1,947,483,647, below a, so the addition is refused. The equality case fits: when a equals INT_MAX - b, the sum is exactly INT_MAX.
Check positive multiplication by division
For positive int operands, a > INT_MAX / b means the product would exceed the maximum. The condition avoids calculating the product and avoids a zero divisor because b is positive. In a factorial calculation, the test before multiplying 12! by 13 detects that 479,001,600 is greater than 165,191,049. Stop before that multiplication.
For unsigned stock, test stock >= requested before subtracting. Switching to unsigned alone does not prevent a nonsensical wrapped result.
Rewrite risky intermediate expressions
The midpoint (low + high) / 2 can overflow even when its answer fits. When 0 <= low <= high and both are int, use low + (high - low) / 2. The difference is nonnegative and within range, and the result never exceeds high. These conditions matter; the formula is not a general proof for arbitrary signed endpoints.
Loop counters need range too. A 16-bit short cannot reach 40000, so a loop using it to count that far cannot terminate as intended. Use a counter type that can represent the bound and any final update.
Use checked arithmetic and tests
C23 provides <stdckdint.h> with ckd_add, ckd_sub, and ckd_mul. For example, ckd_add(&r, a, b) stores a result and returns true when the mathematical answer does not fit r. GCC and clang also provide __builtin_add_overflow, __builtin_sub_overflow, and __builtin_mul_overflow as compiler extensions.
Warnings catch some constant overflow and signed–unsigned mistakes. In GCC or clang, -fsanitize=undefined -g can report signed overflow reached during a test; -ftrapv requests traps for signed addition, subtraction, and multiplication overflow. Exercise zero, one, negative one, the largest expected input, and values at and just below limits. A test tool checks the paths you run, so keep the range reasoning in the code.
Key Takeaways
- Widen an operand before a risky operation.
- Use sign-aware pre-checks for signed addition.
- The division pre-check for multiplication requires positive operands.
- Unsigned subtraction still needs a range check.
- The safer midpoint formula requires ordered nonnegative endpoints.
- Checked arithmetic reports whether the result fits its destination.
- Boundary tests and sanitizers complement range reasoning.
C Types, Operators and Control Flow examples
1Build a greeting and diagnose a failed edit
9 steps
- 1IntroductionFollow the build
A saved C program is text. Running it requires an executable produced from that text. Keep the two files’ roles clear: an edit changes the source, while a successful build replaces the executable.
You will build a short greeting, inspect what each stage does, and diagnose an edit that fails. The central habit is to check the build result before using a run as evidence about the new source.
- 2ExplanationRead the program’s frame
The program begins at
main:#include <stdio.h> int main(void) { printf("Ready\n"); return 0; }The header supplies the declaration of
printf. The body prints a line, then returns success.voidmeans main has no parameters here;intnames the type of its return value. Braces hold the body, and simple statements end with semicolons. - 3ExampleBuild and run it
Save the source as
ready.c. In its folder, enter:gcc -std=c17 -Wall -Wextra ready.c -o ready ./readyThe build selects C17 and useful warnings, then names the executable
ready. The run printsReadyfollowed by a newline. On Linux or macOS,./selects the current folder. You can use clang in the same command position; Windows executables use an.exesuffix. - 4ConnectionSee where object code fits
Preprocessing handles the include directive. Compilation then produces assembly, and assembly produces object code. Linking resolves references and completes the executable; library code may be supplied by a shared library at run time.
The options
-E,-S, and-cstop at progressively later stages. In particular,-cstops at an object file: it has not yet completed the linking step needed for the executable. - 5ChallengePredict the next run
You build the greeting, then save an edit changing "Ready" to "Begin". You run the existing executable without compiling. What text appears, and how do you change it?
---reveal---
It still prints
Ready. The executable reflects the earlier successful build. Compile the saved edit and run the new executable to printBegin. Saving source alone does not update machine code. - 6ExampleRepair the earliest error
An edit leaves this line without a semicolon:
printf("Begin\n") return 0;The compiler may point at the first line or at
return, where it notices the missing end of the expression statement. Add the semicolon after the call and rebuild. Start with this earliest cause even if later messages mention other lines; a single syntax error can disrupt the compiler’s reading of the following code. - 7TipTreat warnings as useful evidence
A warning usually allows a build to continue, but it can identify a serious mistake.
printf("%d\n", 2.5);passes a double where the format requires int, which has undefined behavior.Repair the type mismatch instead of accepting whatever a test run prints. Warnings from
-Wall -Wextrahelp expose such mistakes;-Werrormakes them stop the build. For a wrong calculation, print intermediate values and compare them with a small hand trace. - 8ChallengeSeparate a failed build from a wrong run
After a successful greeting build, a new edit fails to compile. Running
./readystill prints a greeting. Does that show the edit is valid, and what should you inspect next?---reveal---
No. The old executable can remain after a failed build. Inspect the first compiler error, including the preceding source line, fix it, and rebuild successfully. Then run that new executable and compare its output with your prediction. The existing greeting says nothing about the failed edit.
- 9RecapUse one clear repair cycle
Keep each test connected to the source that actually built. Save the edit, compile, read the first diagnostic, repair its cause, and build again before running.
If the program builds but calculates the wrong value, use a small trace and boundary inputs. If an operation has undefined behavior, change the code so it cannot occur; a normal-looking output is not a dependable result for that operation.
2Choose storage for counts and measurements
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionAsk what a value needs
- 2ExampleA copied value stays copied
- 3ExplanationKeep names inside their blocks
- 4ChallengeFollow a hidden name
- 5ExplanationCompare ranges before choosing
- 6ExampleRead a bit pattern and a literal
- 7ChallengeChoose a portable range
- 8ExplanationSee why decimal fractions drift
- 9ExampleCompare an allowed error
- 10ChallengeSeparate conversion from rounding
- 11Key InsightPrecision can fail before range
- 12ChallengeMatch the type to the task
- 13RecapCheck storage in two ways
3Read measurements and print clear results
11 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionConnect typed values with text
- 2ExplanationRead the output format
- 3ExampleBuild an aligned display
- 4ChallengePredict a padded integer
- 5ExplanationRead into the right destinations
- 6ExampleNotice a partial conversion
- 7TipDo not read a leftover newline by accident
- 8ChallengeTrack what a failed read preserves
- 9ExampleCheck the calculation’s domain
- 10ChallengeCombine the read and the display
- 11RecapUse two checks at input
4Read an expression from types to effects
17 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionMark both values and types
- 2ExampleSplit a quantity into whole parts
- 3ExplanationDistinguish the two increments
- 4ChallengeTrace a compound update
- 5ExamplePut the floating operand early enough
- 6Key InsightSmall unsigned values may calculate as signed int
- 7ChallengeFollow a signed value into unsigned arithmetic
- 8ExplanationJoin complete comparisons
- 9ExampleMake a guard run first
- 10ExampleChange one flag without disturbing others
- 11ChallengeCompare truth with shared bits
- 12ExplanationGroup before tracing
- 13TipPut the mask inside the comparison
- 14ConnectionChoose a value without evaluating both
- 15ChallengeCan parentheses repair two updates?
- 16ChallengeCombine grouping with a range test
- 17RecapUse three passes through an expression
5Trace decisions and build a running total
18 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionFollow the path actually taken
- 2ExplanationUse the first true branch
- 3TipBraces and equality each matter
- 4ChallengeFind the first matching band
- 5ExplanationSelect a command entry
- 6ExampleShare an action deliberately
- 7ChallengeFind the target of break
- 8ExplanationRead before using input
- 9ExampleTrace the sentinel calculation
- 10ChallengeHandle an immediate stop
- 11ConnectionAsk once with do-while
- 12ExplanationCount exactly the intended visits
- 13ExampleKeep a total between passes
- 14TipAvoid counters that miss the exit
- 15ExampleCount rows separately
- 16ChallengeTrace a nested early exit
- 17ChallengeCombine a flag and a menu loop
- 18RecapCheck every route to the next pass
6Make a product safe before computing it
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionInspect the arithmetic before storage
- 2ExampleFind the narrow operation
- 3ExplanationUnsigned has a defined but possibly wrong result
- 4ChallengeDoes a late cast repair the product?
- 5ExampleWiden the operation itself
- 6ExplanationTest addition without adding
- 7ExampleTest a positive product by dividing
- 8ChallengeCheck the boundary correctly
- 9Key InsightA safe answer can hide an unsafe sum
- 10TipUse tools and exercise the limits
- 11ChallengeCheck a complete range design
- 12RecapMake the proof happen before the operation
C Types, Operators and Control Flow practice questions
Set 1: Build programs and follow stored values10 Practice Questions
Question 1 of 10
Show answerHide answer
Question 2 of 10
Show answerHide answer
Question 3 of 10
What values are printed by this code?
int units = 6;
int total = units * 4;
units = 9;
printf("%d %d\n", units, total);
Show answerHide answer
Question 4 of 10
Show answerHide answer
Question 5 of 10
014 represent?Show answerHide answer
Question 6 of 10
Show answerHide answer
Question 7 of 10
printf("|%04d|", 23);?Show answerHide answer
Question 8 of 10
int a = 2, b = 9;, a call to scanf("%d:%d", &a, &b) receives "17 42". What return value and stored values result?Show answerHide answer
Question 9 of 10
On a system with 8-bit unsigned char and 32-bit int, what does this print?
unsigned char a = 230, b = 40;
int sum = a + b;
unsigned char small = sum;
printf("%d %u\n", sum, (unsigned)small);
Show answerHide answer
Question 10 of 10
gcc -std=c17 -c sensor.c succeeds. What did it produce, and what build stage is still needed before ordinary execution?Show answerHide answer
Set 2: Read input and calculate with C types10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
double distance_m;. What scanf conversion and argument match its type?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
-23 / 6 and -23 % 6 in C17?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
int distance = 19, time = 4;, what expression calculates a double speed that keeps the fractional part?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
%d accepts the input "46kg". What value is stored, and what input remains unread immediately after the conversion?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
total and count, what condition checks that the integer average is above 5 without dividing by zero?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
2 + 5 * 3 % 7 produce?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
printf("|%6.1f|", 8.375); print with ordinary rounding to the nearest tenth?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
What values are printed?
int n = 8;
int a = n++;
int b = ++n;
a *= b % 4;
printf("%d %d %d\n", a, b, n);
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
With 32-bit int and unsigned int, what does this print?
int delta = -3;
unsigned int limit = 2;
printf("%d %u\n", delta < limit, limit + delta);
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
flags = 10u. What value results from setting bit 2, counting the lowest bit as bit 0?Show answerHide answer
Save this lesson to see this answer.
Set 3: Trace branches, switches, and loops10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What is printed?
int level = 2;
if (level = 0) {
printf("Low");
} else {
printf("Other");
}
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
for (int i = 4; i < 19; i += 4)?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
A score is 76. What character is assigned?
if (score >= 70) grade = 'C';
else if (score >= 85) grade = 'B';
else grade = 'F';
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
What is printed when mode is 2?
switch (mode) {
case 1: printf("A"); break;
case 2: printf("B");
case 3: printf("C"); break;
default: printf("D");
}
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What final values do n and passes have?
int n = 45, passes = 0;
while (n > 2) {
n /= 2;
passes++;
}
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
What total is printed?
int total = 0;
for (int r = 1; r <= 4; r++) {
for (int c = 1; c <= r; c++) {
if (c == 3) break;
total += c;
}
}
printf("%d\n", total);
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
What sum results?
int sum = 0;
for (int i = 2; i <= 8; i++) {
if (i % 3 == 0) continue;
sum += i;
}
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
How many times is "tick" printed?
int running = 1, passes = 0;
while (running && passes < 3) {
switch (passes) {
case 1: break;
default: printf("tick\n");
}
passes++;
}
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Input is "9 4 0 7". What count and average are printed?
int x, sum = 0, count = 0;
while (scanf("%d", &x) == 1 && x != 0) {
sum += x;
count++;
}
if (count) printf("%d %.1f\n", count, (double)sum / count);
Show answerHide answer
Save this lesson to see this answer.
Set 4: Diagnose conversions and range failures10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
0u - 1u produce when unsigned int has 32 value bits?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
a = 60000 and b = 40000, what expression safely computes their exact product in a typical 64-bit long long?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
flags = 6u, what result does flags & 2u == 0u produce?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
a + b?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
f * i would exceed INT_MAX before multiplication?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
-fsanitize=undefined. What conclusion should guide the repair?Show answerHide answer
Save this lesson to see this answer.
Set 5: Combine input, control flow, and safe arithmetic10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
What value is printed?
int total = 0;
for (int r = 2; r <= 5; r++) {
for (int c = 1; c < r; c++) {
if (c % 2 == 0) continue;
total += r;
}
}
printf("%d\n", total);
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
days is 30000. What expression correctly computes its duration in milliseconds in a 64-bit long long?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
What does this C17 fragment guarantee about its output?
int n = 4;
printf("%d %d\n", n++, n++);
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What is printed when a = -2 and b = 7?
if (a > 0)
if (b > 0) printf("Both");
else printf("A only");
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Why does this loop stop making progress after printing zero once?
int i = 0;
while (i < 4) {
if (i == 1) continue;
printf("%d\n", i);
i++;
}
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
With 32-bit unsigned int, what value does this code leave in flags?
unsigned int flags = 13u;
flags &= ~(1u << 2);
flags |= 1u << 1;
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
A menu starts with running = 1 and actions = 0. Input is "s q s". What is actions after this loop?
char cmd;
while (running && scanf(" %c", &cmd) == 1) {
switch (cmd) {
case 'q': running = 0; break;
case 's': actions++; break;
}
}
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
What value is assigned to rate?
int completed = 14, planned = 20;
double rate = completed / planned * 100.0;
Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Accumulator
Assignment
count = count + 1; reads the old value and replaces it with a value one larger.Assignment count
Associativity
24 / 4 / 2 as (24 / 4) / 2, giving three.B
Bit mask
1u << 5 selects bit five, counting the least significant bit as position zero.Bitwise operator
& keeps only the common set bits of twelve and ten, giving eight.Block scope
Boolean value
<stdbool.h> in C17, bool ready = 2; stores the Boolean value true.Boundary test
Branch
Break statement
C
Case label
case 4: is valid in C17; an ordinary const int variable cannot supply its value.Cast
(double)sum / count takes effect before division, preserving a fractional quotient.Character constant
'A' can print as the letter A with %c or the number 65 with %d.Checked arithmetic
ckd_mul can report a product that does not fit its destination.Comma operator
i++, j--; commas between printf arguments do not.Compile-time error
Compiler
Compound assignment
total += item; updates the running total without changing the item.Condition
if (items) is false for zero, while a negative nonzero count takes the branch.Conditional operator
condition ? a : b to choose one value expression after testing the condition.condition ? a : b evaluates b and skips a.Continue statement
Conversion specification
%lld matches a long long argument; %d cannot automatically convert it.D
Dangling else
Declaration
int count = 0; gives the name an integer type and also supplies its starting value.Default label
Do-while loop
E
End of input
Escape sequence
\n adds a newline inside a C string; printf uses %% for a percent sign.Evaluation order
Exact-width type
uint8_t, when provided, holds values from zero through 255.Executable
./hello starts the executable in the current folder, without invoking the C compiler again.Exit status
F
Fall-through
Field width
%6d adds four spaces before 42, while a seven-digit value still prints in full.Floating-point value
For loop
i < n and i++ makes n passes for nonnegative n when updates fit.Format string
printf("Mass: %.1f kg\n", mass) supplies a label, decimal precision, and a unit.I
Identifier
distance_m is valid; 2distance cannot be a name because it starts with a digit.Implementation-defined behavior
Implicit conversion
Infinite loop
Initialization
Integer division
22 / 5 gives four even when the completed result is assigned to a double.Integer literal
0x30 uses hexadecimal, while 48u writes that value as unsigned decimal.Integer overflow
Integer promotion
Intermediate result
(low + high) / 2 is an intermediate result that can overflow even when the midpoint fits.Iteration
L
Linker
printf; its library code may be supplied by a shared library.Logic error
Logical operator
&&, ||, or !.ready && valid tests whether both stored values are nonzero.M
Macro
#define LIMIT 8, the token LIMIT is replaced by 8 wherever that macro applies.Matching failure
%d conversion cause matching failure and remain unread for a retry.N
NaN
Nested loop
O
Off-by-one error
i < n with i <= n in a zero-based counter loop causes an off-by-one error.Operand
sum / count, the operands’ types determine whether division keeps a fractional result.Out-of-range conversion
Output buffering
fflush(stdout) sends it before an input call.P
Postfix increment
n = 3, the postfix increment in old = n++; gives old three and leaves n at four.Precedence
4 * 2 together before addition in 3 + 4 * 2.Precision
Preprocessor
#include and macro replacement before the main C translation stages.R
Range check
Relational operator
<, <=, >, or >= and produces int zero or one.height >= 1 checks one boundary; another comparison checks the other.Remainder
% operation, with a nonzero result having the dividend’s sign.-22 % 5 is −2 because the truncated quotient is −4 and the division identity must hold.Rounding error
S
Sanitizer
Sentinel
Shift
Short-circuit evaluation
count != 0 && sum / count > 4 skips division when count is zero.Side effect
Signed integer
Source code
Standard input
Statement block
Switch statement
T
Tolerance
fabs(measured - target) with the tolerance instead of demanding exact equality.Trace
Truncation toward zero
Two’s complement
10000000 represents negative 128.U
Undefined behavior
Unsigned integer
Unsigned wraparound
Usual arithmetic conversions
V
Variable
mass_kg stores a measurement while count records how many samples were entered.W
Warning
%d was paired with a double argument, an invalid combination.While loop
Whitespace
%c reads a leftover newline unless told to skip it.Widening
a * b changes the product’s type; casting after the product is too late.About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.