C++ Course — 01 · Syntax, Types, References, and Control Flow
This post covers the syntactic spine of C++: primitive types, variable declaration, const, references, operators, control flow, and the first taste of functions. C++ is statically typed — every variable's type is fixed at compile time — which forces you to think about representation up front. The payoff: huge classes of bugs caught before your program runs.
Series map: 00 Setup → 01 Syntax & Types (this post) → 02 Pointers & Memory → 03 Classes & OOP → 04 Templates & STL → 05 Modern C++ → (future).
Learning Outcome
- Declare and initialise variables of every primitive type.
- Use
const,auto, and references correctly. - Use arithmetic, comparison, logical, and bitwise operators.
- Write
if/else,for,while,do-while,switch, and range-basedfor. - Understand integer overflow, type promotion, and narrowing.
- Define and call simple functions; understand pass-by-value vs pass-by-reference.
Chapter 1 — Primitive types
Terminology
- Type — describes the size + interpretation of a value (e.g., 32-bit signed integer).
- Variable — a typed, named memory location.
- Declaration — introduces a name + type (
int x;). - Initialisation — gives it a value at the point of declaration (
int x = 5;). - Definition — a declaration that allocates storage.
Common types
| Type | Size (typical) | Range |
|---|---|---|
bool |
1 byte | true / false |
char |
1 byte | signed -128..127 (impl-defined) |
unsigned char |
1 byte | 0..255 |
short |
2 bytes | -32,768..32,767 |
int |
4 bytes | ±2.1 × 10⁹ |
unsigned int |
4 bytes | 0..4.29 × 10⁹ |
long |
4 or 8 bytes | impl-defined |
long long |
8 bytes | ±9.2 × 10¹⁸ |
float |
4 bytes | ±3.4 × 10³⁸ (≈7 decimal digits) |
double |
8 bytes | ±1.8 × 10³⁰⁸ (≈15 decimal digits) |
long double |
≥8 bytes | impl-defined |
nullptr_t |
impl-defined | the type of nullptr |
For portable fixed-width integers, use <cstdint>:
1
2
3
4
5
6
#include <cstdint>
int8_t a; // exactly 8-bit signed
uint16_t b; // exactly 16-bit unsigned
int32_t c; // exactly 32-bit signed
int64_t d; // exactly 64-bit signed
Use int32_t/int64_t for code that crosses platforms (file formats, network protocols).
Chapter 2 — Variable declaration and initialisation
1
2
3
4
5
6
7
8
9
10
11
int x; // declared, uninitialised — value is GARBAGE
int y = 5; // copy-initialisation
int z(7); // direct initialisation (constructor-style)
int w{9}; // braced (uniform / list) initialisation — RECOMMENDED
double pi = 3.14159;
char letter = 'A';
bool active = true;
std::string name = "Ada"; // requires #include <string>
Why prefer {}?
Braced initialisation rejects narrowing conversions at compile time:
1
2
3
int x = 3.7; // compiles — silently truncates to 3
int y{3.7}; // ERROR — narrowing forbidden
This catches the kind of bugs that ruin nights. Use {} everywhere modern compilers allow it.
auto — let the compiler deduce
1
2
3
4
5
auto x = 5; // int
auto y = 3.14; // double
auto name = "Ada"; // const char* (NOT std::string!)
auto s = std::string{"Ada"}; // explicit
auto is great when:
- The type is obvious from the right side.
- The type is long (iterators, lambdas, template instantiations).
Don't use auto for everything — sometimes spelling out the type makes intent clearer.
Chapter 3 — const and constexpr
const
1
2
3
const int MAX = 100;
MAX = 200; // ERROR — cannot modify const
Once declared const, the variable cannot be reassigned. Use const aggressively — it documents intent and lets the compiler optimise.
constexpr (compile-time constant)
1
2
3
constexpr int BUFFER = 1024; // value known at compile time
constexpr double PI = 3.14159265358979;
constexpr is evaluated during compilation. Use for sizes, mathematical constants, lookup tables.
Trick: default to
constfor any variable you don't actively change after initialisation. It costs nothing and prevents accidental mutation.
Chapter 4 — References (first taste)
A reference is an alias for another variable.
1
2
3
4
5
6
int x = 5;
int& ref = x; // ref is another name for x
ref = 10; // changes x too
std::cout << x; // 10
Key facts about references:
- Must be initialised at declaration — no "uninitialised reference".
- Cannot be re-seated to another variable.
- Always refer to a valid object (unlike pointers, which can be null).
const reference
1
2
3
4
void print(const std::string& s) { // pass by const ref — no copy, can't modify
std::cout << s << "\n";
}
This is the way to pass large objects to functions in C++: by const &. It avoids copies but guarantees the function can't modify the argument.
We'll cover pointers (int*) in detail in Post 02. References are usually the cleaner choice.
Chapter 5 — Operators
Arithmetic
+, -, *, /, % (modulo, integers only). Watch out: 5 / 2 is 2, not 2.5, because both operands are int. Use 5.0 / 2 or 5 / 2.0 to force floating-point.
Comparison
==, !=, <, >, <=, >= — return bool.
Logical
&& (and), || (or), ! (not). They short-circuit.
Bitwise
&, |, ^ (xor), ~ (not), << (left shift), >> (right shift). These operate on individual bits — useful for low-level manipulation and flags:
1
2
3
4
5
int flags = 0;
flags |= (1 << 0); // set bit 0
flags |= (1 << 2); // set bit 2
if (flags & (1 << 2)) { ... } // test bit 2
Assignment shorthand
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=.
Increment / decrement
1
2
3
4
int i = 5;
i++; // post-increment (returns old value, then increments)
++i; // pre-increment (increments, then returns)
Prefer ++i when the result isn't used — slightly more efficient for non-trivial types.
Chapter 6 — Integer overflow + promotion
Signed integer overflow is undefined behaviour in C++:
1
2
3
int x = INT_MAX;
int y = x + 1; // UB — compiler is allowed to do anything
Use int64_t or be sure your arithmetic stays in range.
Unsigned overflow wraps (well-defined):
1
2
3
uint32_t u = 0;
u--; // becomes 4,294,967,295
Type promotion
When mixing types, the compiler promotes the smaller/narrower to the wider:
1
2
3
4
int i = 5;
double d = 2.0;
auto r = i + d; // r is double, value 7.0
Signed + unsigned of the same size → unsigned. Mixing signed and unsigned is a notorious bug source:
1
2
3
4
unsigned int u = 1;
int s = -1;
if (s < u) { ... } // SURPRISE: s is converted to unsigned (huge), so condition is FALSE
Use signed types unless you have a specific reason for unsigned.
Chapter 7 — Control flow
if / else
1
2
3
4
5
6
7
8
if (x > 0) {
std::cout << "positive";
} else if (x == 0) {
std::cout << "zero";
} else {
std::cout << "negative";
}
Braces are optional for single-statement bodies, but always use them — prevents the dangling-statement bug.
C++17 if with init
1
2
3
4
if (auto it = m.find(key); it != m.end()) {
// use it->second
}
The variable it is scoped to the if/else. Keeps tiny helpers out of the surrounding scope.
while / do-while
1
2
3
4
5
6
7
8
9
while (n > 0) {
process(n);
--n;
}
do {
/* runs at least once */
} while (still_running());
Classic for
1
2
3
4
for (int i = 0; i < 10; ++i) {
std::cout << i << " ";
}
Three parts: init, condition, increment.
Range-based for (C++11+)
1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<int> v{1, 2, 3, 4, 5};
for (int x : v) { // copy each element
std::cout << x << " ";
}
for (const auto& x : v) { // const reference — no copy, no mutation
std::cout << x << " ";
}
for (auto& x : v) { // mutable reference — can modify
x *= 2;
}
const auto& is the idiomatic default. Use plain auto only for small types like int/double.
switch
1
2
3
4
5
6
7
8
9
10
11
12
switch (cmd) {
case 1:
do_one();
break;
case 2:
case 3:
do_two_or_three();
break;
default:
do_other();
}
Don't forget break — without it, execution falls through to the next case. (Sometimes useful; usually a bug.)
C++17 added [[fallthrough]]; to mark intentional fall-through:
1
2
3
4
5
6
7
case 1:
foo();
[[fallthrough]];
case 2:
bar();
break;
Chapter 8 — Functions (intro)
1
2
3
4
5
6
7
8
9
10
11
12
13
int add(int a, int b) {
return a + b;
}
void greet(const std::string& name) {
std::cout << "Hello, " << name << "\n";
}
int main() {
std::cout << add(3, 4) << "\n"; // 7
greet("Ada");
}
Pass by value vs reference
1
2
3
4
5
6
7
8
void by_value(int x) { x = 999; }
void by_reference(int& x) { x = 999; }
void by_const_ref(const int& x) { /* x = 999; // ERROR */ }
int n = 5;
by_value(n); // n is still 5
by_reference(n); // n is now 999
Default rule:
- Small types (
int,double, pointers) → pass by value. - Large types (
std::string, containers, your own classes) → pass byconst &unless you need to mutate.
Default arguments
1
2
3
4
5
6
7
8
9
int power(int base, int exp = 2) {
int r = 1;
for (int i = 0; i < exp; ++i) r *= base;
return r;
}
power(5); // 25
power(5, 3); // 125
Defaults must be on trailing parameters.
Practice Exercises
Exercise 1 — FizzBuzz
Print 1–100; for multiples of 3 print "Fizz", 5 → "Buzz", both → "FizzBuzz".
Solution
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
int main() {
for (int i = 1; i <= 100; ++i) {
if (i % 15 == 0) std::cout << "FizzBuzz";
else if (i % 3 == 0) std::cout << "Fizz";
else if (i % 5 == 0) std::cout << "Buzz";
else std::cout << i;
std::cout << "\n";
}
}
Exercise 2 — Sum of digits
Read a positive integer, print the sum of its digits.
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
int main() {
long long n;
std::cin >> n;
long long sum = 0;
while (n > 0) {
sum += n % 10;
n /= 10;
}
std::cout << sum << "\n";
}
Exercise 3 — Temperature converter (with constants)
Use constexpr for the conversion factor. Read Celsius, print Fahrenheit.
Solution
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
constexpr double NINE_FIFTHS = 9.0 / 5.0;
constexpr double OFFSET = 32.0;
int main() {
double c;
std::cin >> c;
std::cout << c << "C = " << (c * NINE_FIFTHS + OFFSET) << "F\n";
}
Exercise 4 — Pass by reference swap
Write void swap_ints(int& a, int& b) that exchanges two ints. Verify from main.
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
void swap_ints(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swap_ints(x, y);
std::cout << x << " " << y << "\n"; // 2 1
}
Exercise 5 — Bit flags
Define 4 flag constants (FLAG_A = 1 << 0, etc.). Write a function that takes an int flags and prints which flags are set.
Cheat-sheet
| What | How |
|---|---|
| Brace init | int x{5}; (catches narrowing) |
| Auto deduce | auto x = expr; |
| Constant | const int MAX = 10; |
| Compile-time | constexpr int BUF = 1024; |
| Reference | int& r = x; |
| Const ref param | void f(const std::string& s) |
| Pointer to int | int* p = &x; (Post 02) |
| Fixed-width int | #include <cstdint> + int32_t |
| Range-for | for (const auto& x : v) |
| If init | if (auto v = expr; cond) { } |
| Switch fall | mark [[fallthrough]]; |
| Integer divide | 5 / 2 == 2 |
| Float divide | 5.0 / 2 == 2.5 |
| Pre-increment | ++i |
What's next
Post 02 covers pointers, raw arrays, references vs pointers, dynamic allocation (new/delete), and the all-important RAII idiom that defines modern C++ memory safety.
References
- cppreference.com / language / declarations
- A Tour of C++ — Ch. 1, 2.
- Effective Modern C++ by Scott Meyers — Items 5–6 (
auto).
Note: this post is in progress. Planned additions: floating-point pitfalls (epsilon comparison), structured bindings,
std::optionalintro.










