← Back to all posts

02 C++ Notes

Variables and Data Types

C++ Notes


Integer

Three Ways to Initialize a Variable

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.

Integer Basics

FormatPrefixExample
Decimalnone15
Octal0017
Hexadecimal0x0x0F
Binary0b0b00001111

By default, output is always printed in decimal, no matter which format you used to store the value.


Integer Modifiers

ModifierBehavior
signed intHolds both negative and positive values. This is the default behavior of plain int.
unsigned intHolds only positive values. Assigning a negative value causes wrap-around.
short int / shortUsually 2 bytes. Both spellings are identical.
long int / longUsually 4 or 8 bytes depending on platform. Both spellings are identical.
long long int / long longUsually 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.


Overflow and Wrapping

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.


Code Example: main.cpp

C++
#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;
}

Fractional Numbers

Floating Point Types

Also called floating numbers, these represent values with a fractional part. There are three of them.

TypeSizePrecision
float4 bytes~7 significant digits
double8 bytes~15 significant digits
long double12 bytesbeyond double

The recommended default is double. CPUs are optimized for it, and it's accurate enough for general use.


setprecision


Floating Point Overflow

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.


Code Example: main.cpp

C++
#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;
}

Boolean


Code Example: main.cpp

C++
#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;
}

Characters

ASCII RangeRepresents
48 to 57digits 0 to 9
65 to 90uppercase A to Z
97 to 122lowercase a to z

Code Example: main.cpp

C++
#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;
}

The auto Keyword

DeclarationDeduced 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.


Code Example: main.cpp

C++
#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;
}

Closing

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.

← Previous 01 - Getting Started with C++ Next → 03 - Operators and Data Operations