CMPUT-201 · Lesson 3 of 12 · 29 min
Types, operators and control flow in C
Much of C's syntax will look familiar after Python: arithmetic operators, comparisons, if statements and loops all exist with similar meanings. The differences are in the details, and in C the details matter, because the type of every value decides how many bytes it occupies, what range of numbers it can hold and what an operator does to it. Integer division, mixed signed and unsigned arithmetic, overflow and floating-point rounding all behave in ways that surprise programmers coming from Python. This lesson covers C's basic types and their sizes, the fixed-width types in stdint.h, literals and conversions, the operators including the bitwise ones, and the control-flow statements if, switch, while, do-while and for, with real output from programs compiled with the sanitizers.
Basic types and their sizes
C's integer types are char, short, int, long and long long, each available in a signed form (the default, except possibly for char) and an unsigned form that holds only non-negative values. The floating-point types are float, double and long double. The C standard deliberately does not fix the size of most types; it only sets minimum ranges, so that C could be implemented efficiently on very different machines. An int must hold at least -32,767 to 32,767, which needs 16 bits, but on every desktop and server system today it is 32 bits. A long is 64 bits on Linux and macOS but 32 bits on 64-bit Windows, a classic portability trap.
The sizeof operator gives the size of a type or variable in bytes, as a value of type size_t, an unsigned integer type used for sizes and counts throughout the standard library; it is printed with the %zu conversion. The header limits.h defines the minimum and maximum of each integer type, such as INT_MAX and LLONG_MIN.
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("%-12s %5s %22s\n", "type", "bytes", "maximum");
printf("%-12s %5zu %22d\n", "char", sizeof(char), CHAR_MAX);
printf("%-12s %5zu %22d\n", "short", sizeof(short), SHRT_MAX);
printf("%-12s %5zu %22d\n", "int", sizeof(int), INT_MAX);
printf("%-12s %5zu %22ld\n", "long", sizeof(long), LONG_MAX);
printf("%-12s %5zu %22lld\n", "long long", sizeof(long long), LLONG_MAX);
printf("%-12s %5zu %22u\n", "unsigned", sizeof(unsigned), UINT_MAX);
printf("%-12s %5zu %22g\n", "float", sizeof(float), FLT_MAX);
printf("%-12s %5zu %22g\n", "double", sizeof(double), DBL_MAX);
printf("float digits: %d, double digits: %d\n", FLT_DIG, DBL_DIG);
return 0;
}type bytes maximum char 1 127 short 2 32767 int 4 2147483647 long 8 9223372036854775807 long long 8 9223372036854775807 unsigned 4 4294967295 float 4 3.40282e+38 double 8 1.79769e+308 float digits: 6, double digits: 15
A char is always exactly one byte, and sizeof(char) is 1 by definition. Whether plain char is signed or unsigned depends on the platform: it is signed on x86-64 Linux and unsigned on ARM Linux. When you need a small number rather than a character, use signed char or unsigned char explicitly, or better, the fixed-width types described next.
Fixed-width integers and bool
Because the basic types vary between systems, C99 added the header stdint.h with types whose sizes are exact: int8_t, int16_t, int32_t and int64_t, and their unsigned versions uint8_t to uint64_t. Use them whenever the exact size matters, such as file formats, network protocols, hardware registers (as in ROB-200) and hashing. The header inttypes.h supplies matching printf conversion macros, such as PRId64 for int64_t, because the correct letter differs between platforms. C99 also added stdbool.h, which defines bool, true and false; in C23 these became keywords and the header is no longer needed. Before that, C used int for truth values, with 0 meaning false and any other value meaning true, and that rule still applies to every condition.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
int main(void) {
uint8_t level = 250;
level = level + 10; /* unsigned arithmetic wraps modulo 256 */
printf("uint8_t 250 + 10 = %u\n", (unsigned)level);
int64_t population = 41000000;
int64_t bytes = population * 1024;
printf("41 million kilobytes = %" PRId64 " bytes\n", bytes);
bool ready = (level < 10);
printf("ready = %d\n", ready);
printf("sizeof(bool) = %zu\n", sizeof(bool));
return 0;
}uint8_t 250 + 10 = 4 41 million kilobytes = 41984000000 bytes ready = 1 sizeof(bool) = 1
Unsigned arithmetic is defined to wrap around: results are reduced modulo 2 to the power of the number of bits, so 250 + 10 in an 8-bit unsigned type gives 4. That is well-defined and sometimes useful, for example in hash functions and checksums. Signed overflow, as lesson 1 showed, is undefined behavior. When a calculation might exceed the range of int, use a wider type such as int64_t before the arithmetic happens, not after.
Literals and conversions
Integer literals are int by default; a suffix changes the type: 10L is long, 10LL is long long, 10U is unsigned. Literals can be written in decimal, hexadecimal with 0x (0xFF is 255), octal with a leading 0 (beware: 010 is eight) and, in C23, binary with 0b. A floating-point literal such as 3.5 is a double; 3.5f is a float. Character literals in single quotes, such as 'A', are small integers (65 in ASCII), and arithmetic on them is common: 'a' + 1 is 'b', and c - '0' converts a digit character to its value. String literals use double quotes; lesson 6 explains how strings are stored.
When an operator combines values of different types, C converts them to a common type first, following the usual arithmetic conversions. Types smaller than int are promoted to int; if one operand is a double, the other becomes double; and if one is unsigned and the other signed of the same size, the signed one is converted to unsigned. That last rule causes one of C's most notorious bugs: comparing a negative int with an unsigned value converts -1 into a huge positive number. The -Wextra option warns about this, which is another reason to keep it on.
#include <stdio.h>
int main(void) {
int total = 7, count = 2;
printf("7 / 2 = %d\n", total / count); /* integer division */
printf("7 %% 2 = %d\n", total % count);
printf("-7 / 2 = %d\n", -total / count); /* truncates toward zero */
printf("-7 %% 2 = %d\n", -total % count);
printf("7.0 / 2 = %.2f\n", total / 2.0);
printf("(double)7/2 = %.2f\n", (double)total / count);
printf("(double)(7/2) = %.2f\n", (double)(total / count));
int balance = -1;
unsigned limit = 10;
if (balance < (int)limit) printf("-1 < 10 when both are int\n");
if ((unsigned)balance > limit) printf("-1 becomes %u as unsigned\n", (unsigned)balance);
return 0;
}7 / 2 = 3 7 % 2 = 1 -7 / 2 = -3 -7 % 2 = -1 7.0 / 2 = 3.50 (double)7/2 = 3.50 (double)(7/2) = 3.00 -1 < 10 when both are int -1 becomes 4294967295 as unsigned
Integer division discards the fractional part, truncating toward zero, so -7 / 2 is -3, not -4 as Python's // gives, and the remainder takes the sign of the dividend, so -7 % 2 is -1. To get a floating-point result, at least one operand must be floating-point before the division happens. A cast, written (type) value, converts explicitly; (double)total / count converts total and then divides, while (double)(total / count) divides first and converts the already truncated answer. Converting a double to an int truncates toward zero, and converting a value that does not fit in the target integer type is undefined behavior.
Operators
C's arithmetic operators are +, -, *, / and %; there is no power operator, and pow from math.h works on doubles. The comparison operators are ==, !=, <, <=, > and >=, and they produce the int 1 or 0. The logical operators are && (and), || (or) and ! (not); like Python's and and or they short-circuit, so the right side is evaluated only if needed, which makes tests such as count > 0 && total / count > 50 safe. Assignment is an expression in C, so a = b = 0 sets both, and the compound operators +=, -=, *=, /= and %= work as in Python. The increment and decrement operators ++ and -- add or subtract one: i++ uses the old value then increments, while ++i increments first. Never modify a variable twice in one expression, as in i = i++ + 1; that is undefined behavior.
The bitwise operators work on the individual bits of integers: & (and), | (or), ^ (exclusive or), ~ (complement), << (shift left) and >> (shift right). They are essential for flags, masks, hardware registers and compact data, and CMPUT-229 studies them at the machine level. Use them on unsigned types: shifting a negative number or shifting by the width of the type or more is undefined or implementation-defined. A common pattern stores several yes-or-no options in one unsigned integer, with one bit per option.
#include <stdio.h>
enum { READ = 1u << 0, WRITE = 1u << 1, EXEC = 1u << 2 };
static void show(unsigned perms) {
printf("%c%c%c (%u)\n", (perms & READ) ? 'r' : '-',
(perms & WRITE) ? 'w' : '-', (perms & EXEC) ? 'x' : '-', perms);
}
int main(void) {
unsigned perms = READ | WRITE; /* set two flags */
show(perms);
perms |= EXEC; /* add a flag */
show(perms);
perms &= ~WRITE; /* clear a flag */
show(perms);
printf("0x2C >> 2 = 0x%X, 5 ^ 3 = %d\n", 0x2Cu >> 2, 5 ^ 3);
return 0;
}rw- (3) rwx (7) r-x (5) 0x2C >> 2 = 0xB, 5 ^ 3 = 6
Operator precedence in C has some surprises. The bitwise operators bind more loosely than comparisons, so flags & MASK == 0 means flags & (MASK == 0), almost certainly not what was intended; GCC warns about it with -Wall. The safe rule is to use parentheses whenever you mix bitwise, comparison and logical operators. The conditional operator, condition ? a : b, is C's equivalent of Python's a if condition else b, and it is used in the show function above.
Control flow
C's if statement takes a condition in parentheses, followed by a statement or a block in braces; else if chains several tests. Always use braces, even for one statement: code such as if (x > 0) count++; total += x; looks as if both statements are conditional, but only the first is. A famous 2014 security flaw in Apple's TLS code, known as goto fail, was caused by exactly this kind of missing-brace mistake. Note also that = is assignment and == is comparison; if (x = 0) assigns 0 and is always false. GCC warns about an assignment used as a condition unless you add an extra pair of parentheses to show you meant it.
The switch statement selects among integer constants. Each case label marks an entry point, and execution falls through into the following cases until it reaches a break, which is a common source of bugs; GCC's -Wimplicit-fallthrough, enabled by -Wextra, warns when a case falls through without a comment or attribute saying it is deliberate. A default label handles any other value.
#include <stdio.h>
static const char *grade_name(char grade) {
switch (grade) {
case 'A':
return "excellent";
case 'B':
case 'C': /* two labels share one action */
return "good";
case 'D':
return "pass";
default:
return "unknown";
}
}
int main(void) {
const char grades[] = "ABCDZ";
for (int i = 0; grades[i] != '\0'; i++) {
printf("%c: %s\n", grades[i], grade_name(grades[i]));
}
int n = 27, steps = 0; /* Collatz steps with a while loop */
while (n != 1) {
n = (n % 2 == 0) ? n / 2 : 3 * n + 1;
steps++;
}
printf("27 reaches 1 in %d steps\n", steps);
int tries = 0;
do { /* body runs at least once */
tries++;
} while (tries < 0);
printf("do-while ran %d time\n", tries);
return 0;
}A: excellent B: good C: good D: pass Z: unknown 27 reaches 1 in 111 steps do-while ran 1 time
The for loop, for (init; condition; update) body, is a compact while loop: init runs once, the condition is tested before each pass, and update runs after each pass. It is not a foreach like Python's for x in items; you loop over indices. The do-while loop tests its condition after the body, so the body always runs at least once, which suits input validation. break leaves the innermost loop or switch, and continue skips to the next iteration. C also has goto, which jumps to a label in the same function; it is discouraged in general, but lesson 10 shows the one widely accepted use, jumping to cleanup code when an error occurs.
Worked problem
Finding the bug in a running average
This code should print the average of the marks 70, 85 and 90, but prints 81.00: int sum = 0; int n = 3; for (int i = 0; i < n; i++) sum += marks[i]; double avg = sum / n; printf("%.2f", avg); Explain the result and fix it.
Show solution
- The true average is 245 / 3 = 81.666..., so the program has lost the fraction.
- sum / n divides two ints, so C performs integer division and produces 81. Only then is 81 converted to double for the assignment, giving 81.0.
- The type of the variable on the left of = does not affect how the right side is computed.
- Fix it by converting before dividing: double avg = (double)sum / n; which now prints 81.67.
Answer: Integer division happened before the conversion; write (double)sum / n.
Worked problem
A loop that never ends
A countdown loop written as for (unsigned i = 10; i >= 0; i--) printf("%u ", i); never stops. Why, and what are two ways to fix it?
Show solution
- An unsigned value is never negative, so the condition i >= 0 is always true. GCC with -Wextra warns that the comparison is always true.
- When i is 0, i-- wraps around to UINT_MAX, 4,294,967,295, and the loop continues.
- Fix 1: use a signed type: for (int i = 10; i >= 0; i--).
- Fix 2: keep unsigned but test before decrementing: for (unsigned i = 11; i-- > 0; ) prints 10 down to 0.
Answer: Unsigned values cannot be negative, so i >= 0 is always true; use int or restructure the test.
Common mistakes
MistakeExpecting 7 / 2 to give 3.5 in C.
InsteadInteger division truncates; convert an operand first, as in 7 / 2.0 or (double)a / b.
MistakeWriting if (x = 5) when you meant to compare.
InsteadUse == for comparison; keep -Wall on so GCC warns about assignments in conditions.
MistakeMixing signed and unsigned values in comparisons.
InsteadKeep both operands the same signedness, or cast deliberately after checking for negative values.
MistakeLeaving out break in a switch case.
InsteadEnd each case with break or return, and mark any deliberate fall-through with a comment.
MistakeAssuming long is 64 bits everywhere.
InsteadUse int64_t from stdint.h when you need exactly 64 bits.
MistakeUsing ^ for exponentiation.
Instead^ is bitwise exclusive or; use a loop or pow from math.h.
Summary and key terms
- C has integer types char, short, int, long and long long, signed and unsigned, and floating types float and double; sizes vary by platform.
- sizeof gives sizes in bytes as size_t (printed with %zu), and limits.h gives each type's range.
- stdint.h provides exact-width types such as int32_t and uint8_t; stdbool.h provides bool.
- Unsigned arithmetic wraps around; signed overflow is undefined behavior.
- Integer division truncates toward zero, and mixed expressions follow the usual arithmetic conversions, including the signed-to-unsigned trap.
- Bitwise operators manipulate individual bits; use them on unsigned types and parenthesize mixed expressions.
- Control flow uses if, switch (with break), while, do-while and for; always use braces.
- size_t
- The unsigned integer type returned by sizeof and used for sizes and counts.
- Fixed-width type
- An integer type of exact size from stdint.h, such as int32_t.
- Integer promotion
- The automatic conversion of types smaller than int to int in expressions.
- Usual arithmetic conversions
- The rules that convert operands of different types to a common type.
- Cast
- An explicit conversion written (type) expression.
- Wrap-around
- Reduction of an unsigned result modulo 2 to the power of its width.
- Bitwise operator
- An operator such as &, |, ^, ~, << or >> that works on individual bits.
- Short-circuit evaluation
- Evaluating the right side of && or || only when needed.
- Fall-through
- Execution continuing from one switch case into the next when break is missing.
- Conditional operator
- The expression condition ? a : b.
Practice problems
Work each problem on paper first, then reveal the answer to check.
What does sizeof(int) give on 64-bit Linux, and what type is the result?
Show answer
4, of type size_t.
Evaluate 17 / 5, 17 % 5, -17 / 5 and -17 % 5 in C.
Show answer
3, 2, -3 and -2.
A uint8_t holds 200. What does it hold after adding 100?
Show answer
44, because 300 modulo 256 is 44.
Why is -1 < 10u false?
Show answer
-1 is converted to unsigned, becoming 4,294,967,295, which is greater than 10.
Write an expression that converts the digit character c to its integer value.
Show answer
c - '0'
Set bit 3 of flags, then clear bit 0.
Show answer
flags |= 1u << 3; flags &= ~1u;
What is wrong with if (flags & MASK == 0)?
Show answer
== binds more tightly than &, so it means flags & (MASK == 0); write (flags & MASK) == 0.
Rewrite total = x > 0 ? x : -x; as an if statement.
Show answer
if (x > 0) { total = x; } else { total = -x; }
When should you use a do-while loop?
Show answer
When the body must run at least once, for example when prompting until valid input arrives.
Which header and conversion print an int64_t portably?
Show answer
inttypes.h with the PRId64 macro, as in printf("%" PRId64 "\n", x).
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.