What a Variable Actually Is
In Python you write x = 5 and the name x is simply attached to an object. In C++ a variable is a region of memory with a name, a fixed size, and a fixed type decided at compile time. Writing int age = 25; tells the compiler to set aside a chunk of memory big enough for an integer, call it age, and put 25 in it.
Because the type is fixed, the compiler knows exactly how many bytes to reserve and exactly which machine instructions to emit — and it can reject age = "twenty five"; before the program ever runs. This is called static typing, and it is one of the main reasons C++ is fast. There is no type tag being checked at run time; the decision was made once, during compilation.
It also means the type you choose is a real engineering decision, not a formality. An int and a long long are not interchangeable when the value is large, and a float and a double are not interchangeable when the value needs precision. The rest of this lesson is mostly about making those choices deliberately.
The code below shows the fundamental types with sensible values. Read the comments: several of the sizes are not what beginners assume, and one of them is different on Windows than on Linux.
#include <iostream>
#include <cstdint>
int main() {
// Integers
int age = 25; // at least 16 bits by the standard,
// 32 bits on every mainstream platform
short small = 100; // at least 16 bits
long big = 1000000L; // 64-bit on Linux/macOS, 32-bit on Windows
long long veryBig = 9000000000LL; // at least 64 bits, everywhere
unsigned int positiveOnly = 42u; // no negatives, wider positive range
// Floating point
float price = 9.99f; // ~7 significant decimal digits
double pi = 3.14159265358979; // ~15-16 digits — the everyday default
// Character and boolean
char grade = 'A'; // exactly 1 byte, by definition
bool passed = true;
// Fixed-width types, when the exact size matters
std::int32_t fileVersion = 3;
std::int64_t nanoseconds = 1'500'000'000LL; // ' is a digit separator (C++14)
std::cout << age << ' ' << price << ' ' << grade << '\n';
} Initialise Everything, Immediately
This is the most important paragraph in the lesson. In C++, a local variable that you declare without giving it a value does not start at zero. It starts holding whatever bytes happened to be sitting at that memory address, left there by some earlier part of the program. Reading it is undefined behaviour.
What makes this dangerous rather than merely annoying is that the leftover bytes are very often zero, so the buggy program appears to work. It works on your laptop, it works in the lab, and then it prints 32767 on the evaluator's machine because that memory had been used for something else first. Students conclude the compiler is buggy. The compiler is fine; the code was always wrong and only sometimes looked right.
The habit that removes the entire problem is to give every variable a value on the line where you declare it. If you genuinely do not know the value yet, initialise to a sensible default and overwrite it later — the cost is nothing and the class of bug disappears.
C++11 added brace initialisation, written int x{5};, and it has one advantage worth knowing: it refuses narrowing. Assigning 3.9 to an int with = silently throws away the fraction, whereas int x{3.9}; is rejected by the compiler. Empty braces, int x{};, mean "zero-initialise", which is a compact way to guarantee a clean starting value.
#include <iostream>
int main() {
int bad; // UNDEFINED — contains garbage
// std::cout << bad; // undefined behaviour, may print anything
int good = 0; // fine
int alsoGood{0}; // brace init
int zeroed{}; // empty braces == 0
double rate{0.08};
// Brace init refuses to lose data silently
int truncated = 3.9; // compiles, quietly becomes 3
// int strict{3.9}; // error: narrowing conversion
std::cout << good << ' ' << zeroed << ' ' << truncated << '\n';
} -Wallcatches many uninitialised reads withwarning: 'bad' is used uninitialised. It cannot catch all of them, because the compiler cannot always trace every path through your program — which is exactly why the habit matters more than the warning.
Integer Overflow: The Bug That Costs Marks
A 32-bit int holds values from −2,147,483,648 to 2,147,483,647 — roughly 2.1 billion either side of zero. That sounds like plenty until you write a program that multiplies two numbers, and this is the single most common reason a correct-looking DSA solution fails on the larger test cases.
Consider a problem where n can be up to 100,000 and you need n × (n − 1) / 2, the number of pairs. With n = 100,000 that product is about 10 billion, which is roughly five times too large for an int. If both operands are int, C++ performs the multiplication in int and overflows before it ever reaches the assignment — so declaring the result as long long does not save you. You have to make at least one operand long long, so the arithmetic itself happens in the wider type.
Signed overflow is undefined behaviour, not a wraparound you can plan around. Unsigned types are different: they are defined to wrap around, which is occasionally useful and is also why unsigned subtraction is a trap. If a and b are unsigned and b is larger than a, then a - b does not go negative; it becomes an enormous positive number.
The practical rules: use int for loop counters and small quantities, reach for long long the moment a value could plausibly exceed a couple of billion, and check std::numeric_limits when you are unsure what a type can hold on your machine.
#include <iostream>
#include <limits>
int main() {
std::cout << std::numeric_limits<int>::max() << '\n'; // 2147483647
std::cout << std::numeric_limits<long long>::max() << '\n'; // 9223372036854775807
int n = 100000;
long long wrong = n * (n - 1) / 2; // WRONG: computed as int, overflows
long long right = 1LL * n * (n - 1) / 2; // widen before multiplying
std::cout << wrong << '\n'; // garbage
std::cout << right << '\n'; // 4999950000
// Unsigned subtraction does not go negative
unsigned int a = 3, b = 5;
std::cout << a - b << '\n'; // a huge number, not -2
} - The
1LL *trick is worth memorising. It costs one token, forces the whole expression into 64-bit arithmetic, and removes an entire category of wrong-answer submissions.
const, constexpr and auto
const marks a variable as read-only after initialisation. Its value is not the point — the point is that you are telling both the compiler and the next reader "this is not going to change". If someone later writes an assignment to it, the build fails immediately instead of the bug surfacing in output three months later. Use it for anything that is genuinely fixed: a tax rate, a maximum size, a filename.
constexpr goes further and says the value must be computable at compile time. A constexpr int MAX = 100; is not stored and read like a normal variable; the compiler substitutes 100 wherever it appears, and the value can be used in places that demand a compile-time constant, such as the size of a std::array. If you can write constexpr, prefer it over const for simple constants.
auto asks the compiler to work out the type from the initialiser. It is not dynamic typing — the type is still fixed at compile time, you simply did not have to spell it out. It shines when the real type is long and mechanical, such as an iterator, and it hurts readability when the type is short and meaningful. auto total = 0; saves nothing over int total = 0; and tells the reader less.
One auto gotcha worth knowing early: auto drops references and const unless you ask for them. Writing auto x = someVector[0]; makes a copy. If you meant to refer to the element in place, write auto& x = someVector[0];. Silent copying of large objects inside loops is one of the quieter performance problems in beginner C++.
#include <array>
#include <string>
#include <vector>
int main() {
const double TAX_RATE = 0.18;
// TAX_RATE = 0.12; // error: assignment of read-only variable
constexpr int MAX_STUDENTS = 60; // known at compile time
std::array<int, MAX_STUDENTS> marks{}; // needs a compile-time size
std::vector<std::string> names = {"Ananya", "Rahul"};
auto count = names.size(); // std::size_t — spelled out, it is ugly
auto copy = names[0]; // makes a COPY of the string
auto& ref = names[0]; // refers to the element itself
ref += " Sharma"; // names[0] changes; copy does not
return static_cast<int>(count) - 2;
} - In a range-based loop, prefer
for (const auto& item : container)when you only read, andfor (auto& item : container)when you modify. Plainfor (auto item : container)copies every element, which for a vector of strings means a fresh allocation on every iteration.
Type Conversion, static_cast and sizeof
C++ converts between numeric types automatically in many situations. Assigning an int to a double is a widening conversion — every int value fits in a double, so nothing is lost and nobody complains. Going the other way is narrowing, and information can vanish: assigning 3.9 to an int gives 3, because conversion to an integer truncates toward zero rather than rounding.
When you genuinely want a conversion, say so with static_cast<T>(value). C also allows the shorter (int)value, and you will see it in older code, but C++ deliberately made its own casts ugly and searchable. A C-style cast will silently do whatever it takes to make the conversion happen, including things you almost never intend, such as stripping const or reinterpreting one pointer type as another. static_cast only performs conversions the compiler considers reasonable, and if you ever need to find every cast in a codebase, you can grep for the word.
sizeof reports how many bytes a type or object occupies. It is answered at compile time, so it costs nothing at run time. sizeof(char) is 1 by definition; the others are up to the platform, which is precisely why printing them once on your own machine is a useful exercise.
One conversion trap deserves naming now because it will bite you in every loop you write over a container. The .size() of a std::vector or std::string is an unsigned type. Compare it with a signed int and the compiler converts the int to unsigned to make the comparison, which turns any negative value into a huge positive one. With -Wall -Wextra you get a signed/unsigned comparison warning; heed it rather than silencing it.
#include <iostream>
#include <string>
int main() {
// Widening — safe, automatic
int x = 10;
double y = x; // 10.0
// Narrowing — say it out loud
double pi = 3.99;
int truncated = static_cast<int>(pi); // 3, not 4 (truncates toward zero)
// Integer division is a conversion trap of its own
int marks = 7, total = 2;
std::cout << marks / total << '\n'; // 3
std::cout << static_cast<double>(marks) / total << '\n'; // 3.5
// sizeof — answered at compile time
std::cout << sizeof(char) << '\n'; // always 1
std::cout << sizeof(int) << '\n'; // 4 on mainstream platforms
std::cout << sizeof(double) << '\n'; // 8
// The signed/unsigned comparison trap
std::string s = "hello";
int i = -1;
if (i < s.size()) { // WARNING: -1 becomes a huge unsigned value,
std::cout << "taken\n"; // so this branch is taken
}
} - Prefer
doubleoverfloatunless you have a specific reason.doubleis the default type of a literal like3.14, it is what the maths functions in<cmath>take and return, and on modern processors it is not meaningfully slower. - Never use floating point for money.
0.1 + 0.2is not exactly0.3in binary floating point, and those tiny errors accumulate. Store paise as along longinteger and divide by 100 only when you print.
