CS U111 · Blocks B–C · Lesson, for your first time through
Operators, I/O and selection, from the beginning
This page is the small print of C: what an expression really does, when && skips work, how switch falls through, and how scanf reads a line. Allow about 60 minutes if you stop at each "check yourself" and try it. The compressed version lives on the notes page.
Labs 1–3 covered this ground, and at the time it felt easy: printf, if, a switch menu. Then three past mid-sem papers arrived, and the same topics came back as trick questions. What does b = (a+1, c+1, 100, a+c) store? Does a || --b change b? What does scanf("%2d %5f %2d") make of 1234.5678? By my count these mechanics carry roughly a quarter to a half of the marks in each of those papers.
A fact worth holding onto: none of this is taught in school. The comma operator, short-circuit side effects and switch fall-through are new to every student in the hall, JEE background or not. This is not a gap you're behind on. It is a small, closed set of rules, and this page walks through all of them.
1 · An expression is a machine that makes a value
Every expression in C, from 2 + 3 to a-- && --b, is worked out to produce a single value. Some expressions also have a side effect: they change a variable on the way. Keep those two ideas apart and most trick questions come undone. a + 1 has a value and no side effect. a++ has a value and changes a.
The first rule that surprises people: dividing two ints throws the fraction away. It doesn't round. It chops towards zero.
printf("%d\n", 7 / 2); // 3, not 3.5
printf("%d\n", 2*3/4 - 3/4*2); // a past-paper question
For the second line, C works * and / before -, and it works operators of equal rank left to right. So 2*3/4 is 6/4 = 1 (the 0.5 is chopped), and 3/4*2 is 0*2 = 0, because 3/4 is already 0 before the *2 happens. The answer is 1 - 0 = 1. Order matters: 2*3/4 and 3/4*2 look like the same arithmetic, but they aren't.
Check yourself: what do 7/2*2 and 7*2/2 print?
7/2*2 is 3*2 = 6, because the division comes first and loses the half. 7*2/2 is 14/2 = 7. Same numbers, different order, different answer. Always evaluate left to right among * / %.
2 · Mixing int and double
If either side of an arithmetic operator is a double (or float), C converts the other side up and does real division. So the fix for an average is to make one operand a double before the division happens:
int sum = 7, n = 2;
(double) sum / n // 3.50: sum becomes 7.0 first, then 7.0 / 2
(double) (sum / n) // 3.00: the int division already chopped to 3
sum / n * 1.0 // 3.00: same problem; 7/2 happened first
A cast, meaning a type name in brackets like (double) or (int), converts the single value right after it. Where you put it decides whether it helps. Going the other way, storing a real number in an int chops it silently. int x = 2.5; stores 2, and clang only warns: implicit conversion from 'double' to 'int' changes value from 2.5 to 2.
Remainders of negative numbers
% gives the remainder, and for negative numbers the remainder takes the sign of the left number: -7 % 2 is −1, and -7 / 2 is −3. That quietly breaks a common odd-number test:
int x = -3;
x % 2 == 1 // 0: false! -3 % 2 is -1, not 1
x % 2 != 0 // 1: the safe way to say "odd"
x % 2 == 0 // the even test is fine: -4 % 2 is 0
The 2024 paper's "negative even values" question relies on the even test working for negatives, and it does.
Check yourself: (double)(7/2) versus (double)7/2. What does each give?
(double)(7/2): the brackets make 7/2 happen first, as int division, giving 3. Then it's converted to 3.0. (double)7/2: the cast applies to the 7 alone, so this is 7.0/2 = 3.5. The cast has to reach an operand before the division runs.
3 · True and false are just 1 and 0
C has no separate true/false type in these programs. A comparison like x < 5 produces the int 1 if true and 0 if false. Going the other way, anything that isn't 0 counts as true: if (7) runs, if (-3) runs, and only if (0) doesn't.
! flips this: !0 is 1, and ! of anything nonzero is 0. So !5 is 0 and !(-3) is 0.
Past papers love combining these with precedence, meaning which operator grabs its operands first. You don't need the whole C table; this ladder covers every question in the papers, strongest at the top:
| Rank | Operators | What to remember |
|---|---|---|
| 1 | ! ++ -- -x (type) sizeof | Unary operators grab the one thing next to them first. ! beats % (a 2026 question) |
| 2 | * / % | Left to right among themselves |
| 3 | + - | Left to right |
| 4 | << >> | Bit shifts: i << 1 doubles i |
| 5 | < <= > >= | Give 1 or 0 |
| 6 | == != | Give 1 or 0 |
| 7 | & | Bitwise AND (§5). Not the same as && |
| 8 | && | Logical AND; binds tighter than || |
| 9 | || | Logical OR |
| 10 | ? : | Conditional (§9) |
| 11 | = += -= … | Assignment, right to left |
| 12 | , | Comma: the weakest of all (§6) |
Two consequences show up in exams. First, !x < 5 means (!x) < 5, because ! is rank 1. For x = 0 that's 1 < 5 = 1. clang notices and warns: logical not is only applied to the left hand side of this comparison. Second, 3 < 2 < 1 is not maths. It is (3 < 2) < 1 = 0 < 1 = 1. The clang on a Mac (version 21) refuses to compile it at all: error: chained comparison 'X < Y < Z' does not behave the same as a mathematical expression. Older compilers accept it silently, so in an exam, evaluate it the C way.
Check yourself: with x = 9, what is !x < 5?
!9 is 0 (9 is nonzero, so "not true"), and 0 < 5 is 1. It's 1 for x = 0 as well, because !0 = 1 < 5. The expression is 1 for every x: !x is always 0 or 1, and both are below 5. That's exactly the trap in the 2025 paper's || !x < 5, which makes the whole condition true.
4 · && and || stop as soon as they know
&& is true only if both sides are true. So if the left side is already 0 (false), the answer is decided, and C does not evaluate the right side at all. || is true if either side is true, so if the left side is already nonzero, C skips the right side. This is called short-circuit evaluation.
On its own that is just efficiency. It matters when the skipped side had a side effect. A --b that is skipped never happens, so b keeps its value. That is the entire trick in several past-paper questions. Step through them below: the stepper highlights each piece as C evaluates it, and strikes through anything it skips.
Grouping matters too. Because && binds tighter than ||, !i && ++j || ++k means (!i && ++j) || ++k. With i = -3, !i is 0, so the && skips ++j. The left group is 0, so || must evaluate ++k. Final values: i = -3, j = 2, k = 1, m = 1.
Check yourself: int a = 0, b = 5; int r = a && b++; What are r and b?
a is 0, so && already knows the answer is false. b++ is skipped: r = 0, b = 5. If it were a || b++, the left side 0 decides nothing, so b++ runs: r = 1 (5 is nonzero) and b = 6.
5 · & is not &&
A single & is a completely different operator: bitwise AND. It writes both numbers in binary and keeps a 1 only where both have a 1 in the same position. Two consequences:
- Both sides are always evaluated. There is no short-circuit, so side effects on the right always happen.
- The result can be 0 even when both sides are "true".
1 & 4: 1 is001in binary and 4 is100, with no 1s in common, so the result is 0. But1 && 4is 1.
This is the exact trap in two questions of the 2025 paper, and the official answer key fell into it both times. In Q2b the condition is (y>=0) & (x=x+1). The key assumed x only grows when y >= 0, which would be && behaviour, and answered 14. With &, x=x+1 runs on every pass, and 1 & 4 = 0 blocks a pass the key counted. Compiled, it prints 16. Use the last two buttons on the stepper above to see one pass each way. The same thing happens in Q2h, where a printf sits on the right of & and therefore runs every time: the output is 012348, not the key's 348.
Where & is the right tool: counting 1-bits
int n = 2273, count = 0, i = 1;
for (int j = 0; j < 32; j++) {
if (n & i) count++; // is bit j of n a 1?
i = i << 1; // move to the next bit: 1, 2, 4, 8, …
}
printf("%d", count); // 5
i holds a single 1-bit that moves left each pass (i << 1 doubles it). n & i is nonzero exactly when n has a 1 in that position. So the loop counts the 1s in n's binary form.
Unpack this step: 2273 in binary
Take away the largest powers of 2 that fit: 2273 = 2048 + 225 = 2048 + 128 + 97 = 2048 + 128 + 64 + 33 = 2048 + 128 + 64 + 32 + 1. That's five powers of 2, so five 1-bits: 100011100001.
Check yourself: what is 6 & 3? And 6 && 3?
6 is 110 and 3 is 011. They share only the middle bit (the 2s place), so 6 & 3 = 2. 6 && 3 asks "are both nonzero?", so it's 1.
6 · ++, --, the comma, and = in a condition
You met i++ and ++i in the loops lesson. Postfix hands over the old value and then changes the variable; prefix changes it first and hands over the new value. Past papers hide one inside a condition:
int i = 0;
do {
if (i == 3) break;
printf("%d ", i);
} while (i++); // prints just: 0
First pass: prints 0. Then while (i++) tests the old value, 0, which is false, so the loop ends, even though i has become 1. The output is 0, not 0 1 2.
i = i++ + ++i; has no defined answer in C. Different compilers legitimately give different numbers, and clang warns multiple unsequenced modifications to 'i'. If an exam offers it, the defensible answer is "undefined behaviour". In your own code, one ++ per variable per statement.
The comma operator
Inside brackets, a comma between expressions means: evaluate each one left to right, throw every value away except the last, and use that.
int a = 7;
int c = (a >= 7) ? a++ : ++a; // c = 7, then a = 8
int b = (a+1, c+1, 100, a+c); // b = a+c = 8+7 = 15
a+1, c+1 and 100 are computed and discarded (clang warns left operand of comma operator has no effect). Only a+c survives. The brackets are essential: without them, the commas would be separating things in the declaration instead.
= inside a condition
if (x = 0) is not a comparison. It assigns 0 to x, and the value of an assignment is the value stored, here 0, so the if is always false. clang warns using the result of an assignment as a condition without parentheses. Sometimes it's deliberate, as in the (x = x + 1) of the 2025 question; usually it's a typo for ==.
Check yourself: int x = 3; if (x = 0) printf("yes"); else printf("no %d", x);
It prints no 0. The assignment stores 0 in x, the condition's value is 0 (false), so the else runs, and it prints the new x.
7 · Characters are small numbers
A char is stored as an integer code (ASCII). The digit characters '0'…'9' are codes 48…57 in order, the letters 'a'…'z' are 97…122 in order, and pressing Enter sends code 10, written '\n'. Because they're numbers, you can do arithmetic on them:
char c = '7';
printf("%d", c); // 55: the code
printf("%d", c - '0'); // 7: the digit's value, because 55 - 48 = 7
printf("%c", 'a' + 2); // c
'z' > 'd' // 1: letters compare alphabetically
c - '0' is the standard way to turn a digit character into its value. The 2024 paper's 10-mark program hangs on it: read characters until code 10, add up the digit values, and keep the alphabetically highest letter.
Check yourself: what does printf("%d", '7' - '0' + 1) print? And printf("%c", '7' + 1)?
'7' - '0' is 7, plus 1 is 8. '7' + 1 is code 56, and printed with %c that's the character 8. Same digit on screen, reached two different ways.
8 · if, else, and which if an else belongs to
Nested ifs make a decision tree, and reading one is a matter of following one branch at a time. Here is the 2024 paper's Q12:
if (a > b)
if (b > c) printf("s1");
else printf("s2");
else
if (a > c) printf("s3");
else printf("s4");
b > c, then yes to a > b. So b < a and b <= c.The path to s2 is "yes, then no", and a "no" to b > c means b <= c, including equality. Options that say b < c lose the equal case. The right option is (c): b < a and b <= c.
The dangling else
Without braces, an else always belongs to the nearest earlier if that doesn't have one yet. Indentation means nothing to the compiler:
int a = 3, b = 5;
if (a > b)
if (a > 0) printf("x");
else // looks like it pairs with a > b …
printf("y"); // … but it pairs with a > 0
printf("end"); // output: end (no y!)
Since a > b is false, the whole inner if … else is skipped, and nothing but end prints. clang flags it: add explicit braces to avoid dangling else. The cure is braces, always.
9 · ? : is an if that produces a value
condition ? X : Y evaluates the condition and then only one of X or Y. The whole thing has that value. It's the one C operator with three parts, and its job is to let a choice sit inside an expression:
max = (a > b) ? a : b;
Chains read from left to right like an else-if ladder, because each : leads into the next question. The 2024 paper asked for this rewrite of a grade ladder:
grade = (score >= 90) ? 'A' :
(score >= 80) ? 'B' :
(score >= 70) ? 'C' :
(score >= 60) ? 'D' : 'F'; // score 85 gives 'B'
Because only the chosen branch runs, a ?: can pick which printf happens. printf is a function that returns a value, so it's allowed in an expression: (a == 1) ? printf("One") : (a == 2) ? printf("Two") : printf("Other"); prints Other for a = 5.
Check yourself (2026 paper): a = 15, b = 9, c = 12; m = (a < b ? (a < c ? a : c) : (b > c ? b : c));
a < b is 15 < 9, false, so jump to the part after the outer :. That is b > c ? b : c, and 9 > 12 is false, so m = 12. Only one bracket's contents were ever evaluated.
10 · switch: jump in, then fall through
A switch is not a set of separate little ifs. It works like this:
- Evaluate the expression in
switch ( … )once. - Jump to the
caselabel with that value. If there's no such label, jump todefault, wherever it sits. If there's nodefaulteither, skip the whole block. - From there, run statements downwards, straight through any later labels, until a
breakor the closing brace.
Step 3 is the one people forget. The labels are only entry points; they don't stop anything.
i = 2: jump to case 2, fall through case 4 and default, and leave at the break. The result is j = 256. With i = 1 there is no matching case, so it jumps to default: 1 × 1 = 1.Two more past-paper shapes. In 2024 P1 Q7, default is written first, but position doesn't matter: with a = 50, C jumps straight to case 50, runs a-- and breaks, giving 49. default is only used when nothing matches. In 2026 Q2(b), a positive/negative/zero program has no breaks at all, so for a positive number it prints PositiveZero: it enters at case 1 and falls into default. The fix is a break after each case.
Check yourself: the 2024 Q7 switch again, but with a = 52.
There's no case 52, so C jumps to default: a = 45; break;. It prints 45. The labels written after default are irrelevant, because break leaves first.
11 · scanf reads by pattern; printf returns a count
Think of scanf's format string as a pattern it tries to match against what was typed, left to right, one piece at a time:
%dand%ffirst skip any spaces or newlines, then read as many characters as make sense for a number, and stop at the first one that doesn't.- A width caps how many characters that piece may take:
%2dreads at most 2, and%5fat most 5 (the decimal point counts). - A space in the format means "skip any amount of whitespace here, including none".
- Any other character in the format, such as the
-in"%d-%d", must appear exactly in the input. If it doesn't,scanfstops right there and leaves the remaining variables untouched. %cdoes not skip whitespace: it takes the very next character, even a space or an Enter. Write" %c"to skip whitespace first.
Trace 2024 P1 Q2, scanf("%2d %5f %2d", &n, &x, &m) with input 1234.5678 99.88 777:
| piece | input left before it | takes | stored |
|---|---|---|---|
| %2d | 1234.5678 99.88 777 | 12 | n = 12 |
| (space) | 34.5678 99.88 777 | nothing to skip | — |
| %5f | 34.5678 99.88 777 | 34.56 | x = 34.56 |
| (space) | 78 99.88 777 | nothing to skip | — |
| %2d | 78 99.88 777 | 78 | m = 78 |
The output is n= 12,x=34.56,m = 78, and the numbers typed later are never read. In 2024 P1 Q15, scanf("%d-%d", &n, &m) with input 10 20: n gets 10. The next input character is a space, but the format wants a literal -, so scanf gives up and m keeps its initial 0. It prints 10-0.
Match the type letter to the variable, too: %d for int, %f for float, %lf for double, %Lf for long double, %c for char, %u for unsigned int. A mismatch compiles, and clang warns, e.g. format specifies type 'double *' but the argument has type 'long double *'.
printf gives back a number
printf returns how many characters it printed. printf("Hello") prints Hello and has the value 5. The 2024 paper used this to print "HelloWorld" with an if/else where normally only one branch could run:
if (!printf("Hello")) // prints Hello; value 5; !5 is 0 → false
printf("Hello");
else
printf("World"); // runs: output HelloWorld
For output formats: %5.2f means a field at least 5 characters wide with 2 decimals, so 3.14159 prints as 3.14. %.1f rounds to one decimal: 2.75 prints as 2.8.
Check yourself: same scanf("%2d %5f %2d"), but the input is 12 34 56.
%2d takes 12. The space skips the blank. %5f would allow 5 characters, but stops at the space after 34, so x = 34.00. %2d skips the blank and takes 56. Result: n = 12, x = 34.00, m = 56. A width is a maximum, not a promise.
12 · The rewrite questions
Two papers had a whole section of "rewrite this using…" questions: 20 marks in 2024, and part of 24 in 2025. They test whether you know what each construct means, so learn the three conversions as templates.
switch → nested if-else, with no else if
Each case becomes a test, and "everything else" goes inside the else's braces. The brace nesting is what "no else-if" is checking:
if (drink == 1) {
printf("Soda: 20 INR\n");
} else {
if (drink == 2) {
printf("Juice: 25 INR\n");
} else {
if (drink == 3) printf("Water: 10 INR\n");
else printf("Invalid selection\n"); // the default
}
}
if-else ladder ↔ ?: chain
Each if (cond) x = V; becomes cond ? V :, and the final else value ends the chain (the grade example in §9). Go the other way just as mechanically.
for → do-while (and nested)
Move the for's three parts to where a do-while keeps them: initialisation before the do, update at the end of the body, condition in the while. For nested loops, the inner loop's initialisation goes inside the outer body, so it restarts every row:
int count = 1, i = 0;
do {
int j = 0; // inner init: inside, so it resets each row
do {
printf("%d ", count);
count++;
j++;
} while (j < 3);
printf("\n");
i++;
} while (i < 3); // prints 1 2 3 / 4 5 6 / 7 8 9
A for tests first, and a do-while always runs once. With n = 0, for (i = 0; i < n; i++) runs 0 times but the do-while version runs once. If the loop could legitimately run zero times, say so in one line on the answer sheet. Both papers explicitly allow you to "state your assumption".
13 · You're ready: what to do next
That's the whole small print: values and side effects, conversion, 1 and 0, short-circuit, bitwise &, comma, characters, nesting, ?:, switch, and scanf's patterns. To turn it into marks:
- Operators & selection notes: rule cards, three exam-level worked examples, the trap table, and links to every past-paper question on this topic.
- Operators predict-the-output drill: generated questions in these exact shapes, marked instantly. One sheet a day until the mid-sem.
- Past mid-sem papers: every question verbatim, with traced answers and the key's mistakes flagged.
A chat AI is good at this topic if you make it trace rather than tell. Pin it to C and to the exam format:
"I'm a first-year learning C (Hanly & Koffman ch. 2 and 4). Give me 5 one-line C expressions that mix &&, || and ++/-- on int variables with given starting values. For each, I'll write the final value of every variable. Then check my answers by listing, in order, which operands C actually evaluates and which it skips."
"Quiz me on scanf format strings: give me a scanf call with widths or literal characters and one line of input, and I'll say what each variable gets. Don't reveal the answer until I commit, then explain character by character."
One caution, learned the hard way from the 2025 answer key: people and AIs both misread & as &&. For any disputed output, compile and run it. That's the only authority.