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-based for.
  • 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).

Typical sizes of the common C++ types, one square per byte, with their ranges and the exact-width types from cstdint


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 const for 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 by const & 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

Note: this post is in progress. Planned additions: floating-point pitfalls (epsilon comparison), structured bindings, std::optional intro.