C++ Notes
C++ gives you three distinct syntaxes for setting a variable's initial value, and they don't all behave the same way once type conversion is involved.
Assignment = | Braces {} (preferred) | Functional () |
|---|---|---|
int num; num = 10;Declaration and assignment can be separate. |
int num{5};Assigns 5 directly at declaration. |
int num(10);Assigns 10 at declaration. |
Allows implicit conversion silently. int num = 3.5; becomes 3, no error. |
Blocks narrowing conversions. int num{3.5}; is a compilation error, forcing you to notice the type change. |
Also allows implicit conversion, same as assignment, just with different syntax. Does not force you to notice type changes. |
| In Java, an uninitialized declaration is a compile error. In C++ it is not, the variable just holds a garbage value. | int num{}; assigns zero, which is exactly how you avoid that garbage value. |
More commonly reached for in C++ OOP contexts than in plain variable declarations. |
| — | You can run expressions inside the braces, like int num{33+66};, and int x{10}; int y{x}; copies the data of x into y. |
— |
Braces initialization is the one worth defaulting to. It gives you the safety of a compile error the moment a value doesn't cleanly fit the type, instead of a silent truncation you only discover later while debugging.
sizeof(datatype) or sizeof(variableName) tells you how much memory something occupies, in bytes.int takes 4 bytes and stores whole numbers in binary internally.| Format | Prefix | Example |
|---|---|---|
| Decimal | none | 15 |
| Octal | 0 | 017 |
| Hexadecimal | 0x | 0x0F |
| Binary | 0b | 0b00001111 |
By default, output is always printed in decimal, no matter which format you used to store the value.
| Modifier | Behavior |
|---|---|
signed int | Holds both negative and positive values. This is the default behavior of plain int. |
unsigned int | Holds only positive values. Assigning a negative value causes wrap-around. |
short int / short | Usually 2 bytes. Both spellings are identical. |
long int / long | Usually 4 or 8 bytes depending on platform. Both spellings are identical. |
long long int / long long | Usually 8 bytes or more. Both spellings are identical. |
unsigned int x = -1; becomes 4294967295 on 32-bit systems, that's the wrap-around in action. But write it as unsigned int x{-5}; instead, and it's a compilation error, because braces initialization blocks narrowing conversions like this one outright.
unsigned int, C++ guarantees wrapping behavior, with the lower limit fixed at 0.signed int, C++ does not guarantee wrapping. The behavior is technically undefined.Signed overflow being undefined isn't just a technicality. It may appear to wrap like unsigned does, but the compiler is allowed to assume signed overflow never happens at all, and optimize your code accordingly, which can produce results you didn't expect with no compilation error and no warning, only a runtime problem. The only real fix is not storing values in a variable smaller than what you actually need.
#include <iostream> int main() { int number1{15}; // decimal int number2{017}; // octal (prefix with 0) int number3{0x0F}; // hexadecimal (prefix with 0x) int number4{0b00001111}; // binary (prefix with 0b) // all four hold the same binary value, output will be same std::cout << "Number 1:" << number1 << '\n'; // prints: 15 std::cout << "Number 2:" << number2 << '\n'; // prints: 15 std::cout << "Number 3:" << number3 << '\n'; // prints: 15 std::cout << "Number 4:" << number4 << "\n\n"; // prints: 15 std::cout << "Memory of int (in bytes): " << sizeof(int) << "\n\n"; // prints: 4 int num{33+66}; // braces allow expressions int num2(5.55); // functional init, implicit conversion, becomes 5 int num3; num3 = 2.3; // assignment init, implicit conversion, becomes 2 std::cout << "num:" << num << '\n'; // prints: 99 std::cout << "num2:" << num2 << '\n'; // prints: 5 std::cout << "num3:" << num3 << "\n\n"; // prints: 2 // integer modifiers int signed_int{-10}; unsigned int unsigned_int{10}; unsigned int wrapped = -1; // wraps around to 4294967295 short int short1{5}; short short2{5}; // same as short int signed short int short3{-5}; long int long1{100}; long long2{100}; // same as long int signed long int long3{-100}; long long int ll1{1000}; long long ll2{1000}; // same as long long int signed long long int ll3{-1000}; // unsigned int error_case{-5}; // compile-time error (narrowing blocked by braces) std::cout << "signed int: " << signed_int << '\n'; // prints: -10 std::cout << "unsigned int: " << unsigned_int << '\n'; // prints: 10 std::cout << "unsigned int = -1: " << wrapped << "\n\n"; // prints: 4294967295 std::cout << "short int: " << short1 << '\n'; // prints: 5 std::cout << "short (same): " << short2 << '\n'; // prints: 5 std::cout << "signed short int: " << short3 << '\n'; // prints: -5 std::cout << "sizeof(short): " << sizeof(short) << " bytes\n\n"; // prints: 2 std::cout << "long int: " << long1 << '\n'; // prints: 100 std::cout << "long (same): " << long2 << '\n'; // prints: 100 std::cout << "signed long int: " << long3 << '\n'; // prints: -100 std::cout << "sizeof(long): " << sizeof(long) << " bytes\n\n"; // prints: 4 or 8 std::cout << "long long int: " << ll1 << '\n'; // prints: 1000 std::cout << "long long (same): " << ll2 << '\n'; // prints: 1000 std::cout << "signed long long: " << ll3 << '\n'; // prints: -1000 std::cout << "sizeof(long long): " << sizeof(long long) << " bytes\n"; // prints: 8 return 0; }
Also called floating numbers, these represent values with a fractional part. There are three of them.
| Type | Size | Precision |
|---|---|---|
float | 4 bytes | ~7 significant digits |
double | 8 bytes | ~15 significant digits |
long double | 12 bytes | beyond double |
The recommended default is double. CPUs are optimized for it, and it's accurate enough for general use.
{} does not allow narrowing, so assigning a long double into a double, or a double into a float, will not compile.f for float, L for long double, like float num{1.123f};, or when copying from a variable of another type, use assignment initialization instead, which allows the implicit cast: float y = x;.std::cout << std::setprecision(20); controls how many digits get printed.#include <iomanip>.float can reliably represent about 6 to 7 significant digits. Beyond that, the binary value is rounded to the nearest representable pattern, which is binary rounding, not decimal rounding.float n{123456789.0f} ends up stored as 123456792.float can hold a very large number, it just won't hold it accurately once you're past its precision limit.Only reach for float when low precision is genuinely acceptable, graphics colors and vectors, audio samples, ML tensors, sensor data, percentages where a small error doesn't matter. Use double for everything else.
#include <iostream> #include <iomanip> // required for std::setprecision() int main() { std::cout << "sizeof(float): " << sizeof(float) << '\n'; // prints: 4 std::cout << "sizeof(double): " << sizeof(double) << '\n'; // prints: 8 std::cout << "sizeof(long double): " << sizeof(long double) << "\n\n"; // prints: 12 or 16 float num1 {1.12345678901234567890f}; // f suffix makes it float (precision ~7) double num2 {1.12345678901234567890}; // default is double (precision ~15) long double num3 {1.12345678901234567890L}; // L suffix keeps long double precision std::cout << std::setprecision(20); // show 20 digits (default is 6) std::cout << "float: " << num1 << '\n'; // prints: ~1.12345671653747558594 (imprecise after 7) std::cout << "double: " << num2 << '\n'; // prints: ~1.12345678901234568 (imprecise after 15) std::cout << "long double: " << num3 << "\n\n"; // prints: most precise of the three // scientific notation: e means "times 10 to the power of" double n1 {1.23e5}; // means 1.23 x 10^5 = 123000 std::cout << n1 << "\n\n"; // prints: 123000 double n2 {10.2e-5}; // means 10.2 x 10^-5 = 0.000102 std::cout << n2 << "\n\n"; // prints: 0.000102 float n {123456789123456789123456789.0f}; std::cout << n << '\n'; // prints: some large rounded value, NOT the original return 0; }
bool stores a state, either true or false.true, and zero is false.1 or 0, not the words true or false.true and false are logical concepts. 1 and 0 are the machine reality underneath them.std::cout << std::boolalpha; makes bools print as true or false instead. It's useful for readability, not required for logic.#include <iostream> int main() { bool a {true}; // same as bool a{1}; bool b {false}; // same as bool b{0}; std::cout << a << '\n'; // prints: 1 std::cout << b << "\n\n"; // prints: 0 std::cout << std::boolalpha; // switch to printing true/false instead of 1/0 std::cout << a << '\n'; // prints: true std::cout << b << "\n\n"; // prints: false if(1) { std::cout << "a is true (1)\n\n"; // prints: a is true (1) } else { std::cout << "a is false (0)\n\n"; } std::cout << "sizeof(bool): " << sizeof(bool) << '\n'; // prints: 1 return 0; }
char takes 1 byte (8 bits). One byte gives 256 possible values, since 2 to the power of 8 is 256.| ASCII Range | Represents |
|---|---|
48 to 57 | digits 0 to 9 |
65 to 90 | uppercase A to Z |
97 to 122 | lowercase a to z |
static_cast<int>('A') reveals the numeric ASCII value behind a character.int variable also works: int x = 'A'; stores 65.#include <iostream> int main() { char a {48}; // ASCII 48 = '0' char b {57}; // ASCII 57 = '9' char c {65}; // ASCII 65 = 'A' char d {90}; // ASCII 90 = 'Z' char e {97}; // ASCII 97 = 'a' char f {122}; // ASCII 122 = 'z' std::cout << a << '\n'; // prints: 0 std::cout << b << '\n'; // prints: 9 std::cout << c << '\n'; // prints: A std::cout << d << '\n'; // prints: Z std::cout << e << '\n'; // prints: a std::cout << f << "\n\n"; // prints: z // get the numeric ASCII value of a char std::cout << static_cast<int>('A') << '\n'; // prints: 65 std::cout << static_cast<int>('Z') << '\n'; // prints: 90 // storing char directly into int gives its ASCII value int x{'a'}; std::cout << x << '\n'; // prints: 97 int y{'z'}; std::cout << y << '\n'; // prints: 122 int z{'@'}; std::cout << z << "\n\n"; // prints: 64 int n {static_cast<int>('+')}; std::cout << n << '\n'; // prints: 43 return 0; }
auto lets the compiler deduce the variable's type from the value assigned to it.auto does not make code safer, it makes code shorter. You still need to know what the deduced type actually is.| Declaration | Deduced As |
|---|---|
auto var{12}; | int |
auto var{13.0}; | double |
auto var{14.0f}; | float |
auto var{15.0L}; | long double |
auto var{'e'}; | char |
auto var{123u}; | unsigned int |
auto var{123ul}; | unsigned long int |
auto var{123ll}; | long long int |
Watch out for this one: auto var{123u} deduces to unsigned int. If you later assign -22 to it, no error is thrown, the value just silently wraps around, -22 becomes 4294967274. This can break your logic without any warning at all, precisely because auto hid the type from you at the point you'd have noticed it was unsigned.
#include <iostream> int main() { auto var1{12}; // int auto var2{13.0}; // double auto var3{14.0f}; // float auto var4{15.0L}; // long double auto var5{'e'}; // char auto var6{123u}; // unsigned int auto var7{123ul}; // unsigned long int auto var8{123ll}; // long long int std::cout << "var1 (int): " << sizeof(var1) << " bytes\n"; // prints: 4 std::cout << "var2 (double): " << sizeof(var2) << " bytes\n"; // prints: 8 std::cout << "var3 (float): " << sizeof(var3) << " bytes\n"; // prints: 4 std::cout << "var4 (long double): " << sizeof(var4) << " bytes\n"; // prints: 12 or 16 std::cout << "var5 (char): " << sizeof(var5) << " bytes\n"; // prints: 1 std::cout << "var6 (unsigned int): " << sizeof(var6) << " bytes\n"; // prints: 4 std::cout << "var7 (unsigned long): " << sizeof(var7) << " bytes\n"; // prints: 4 or 8 std::cout << "var8 (long long): " << sizeof(var8) << " bytes\n"; // prints: 8 return 0; }
That's every primitive type C++ hands you and the sharp edges each one carries. Next up, what happens when you start combining them with operators.