Skip to content

Types and Initialization

New to this topic?

C++ provides several built-in types for different kinds of data.

Integeral Types

C++ provides several fixed width integer types (from the <cstdint> header). These types are guaranteed to be the same size across all platforms.

cpp
#include <cstdint>  // For fixed-width integer types

// Fixed-width signed integers
std::int8_t x = 42;        // Exactly 8 bits (-128 to 127)
std::int16_t y = 1000;     // Exactly 16 bits (-32,768 to 32,767)
std::int32_t z = 1000000;  // Exactly 32 bits
std::int64_t w = 123456789012345; // Exactly 64 bits

// Fixed-width unsigned integers
std::uint8_t a = 255;      // Exactly 8 bits (0 to 255)
std::uint16_t b = 65535;   // Exactly 16 bits (0 to 65,535)
std::uint32_t c = 4294967295; // Exactly 32 bits
std::uint64_t d = 18446744073709551615ULL; // Exactly 64 bits

// Traditional types (avoid in new code)
int old_style = 42;   // Platform-dependent size

Why Use Fixed-Width Integers?

The traditional integer types (int, long, etc.) have different sizes on different platforms:

  • int might be 16 bits on one system, 32 bits on another
  • long might be 32 bits on Windows, 64 bits on Linux

Fixed-width integers from <cstdint> solve this problem:

cpp
// These are guaranteed to be the same size everywhere
int32_t x = 42;       // Always 32 bits, regardless of platform
uint64_t y = 1000;    // Always 64 bits, regardless of platform

When to use which?

  • int32_t/uint32_t: Most common, good for most applications
  • int64_t/uint64_t: When you need very large numbers
  • int8_t/uint8_t: When you need to save memory (arrays, network protocols)
  • int16_t/uint16_t: Middle ground, less common

These integral types along with bool, char, float, and double are called primitive types in C++. If you declare a variable without initializing it, it will contain a garbage value!

The auto Keyword

The auto keyword tells the compiler to figure out the type automatically:

cpp
auto x = 42;          // x is an int
auto y = 3.14;        // y is a double
auto z = "Hello";     // z is a const char*
auto flag = true;     // flag is a bool

Why use auto?

  • Less typing: You don't need to write the type
  • Fewer mistakes: The compiler picks the right type
  • Easier maintenance: If you change the initializer, the type updates automatically

Initialization Methods

C++ provides several ways to initialize variables. Understanding the differences is crucial.

Copy Initialization

cpp
int x = 5;            // Copy initialization
double d = 3.14;      // Copy initialization

Direct Initialization

cpp
int x(5);             // Direct initialization
double d(3.14);       // Direct initialization

Uniform Initialization

cpp
int x{5};             // Uniform initialization
double d{3.14};       // Uniform initialization
int arr{1, 2, 3, 4}; // Array initialization

So many initialization methods! What do I do?

C++11 introduced uniform initialization because classes, structs, and primitive types had different initialization methods. Before C++11, you would have to use copy initialization for primitives and direct initialization for classes and structs.

Uniform initialization is now the preferred way to initialize variables, classes, structs, and primitive types. It works regardless of the type of variable!

Additionally, uniform initialization prevents narrowing conversions.

cpp
int x = 5.5;           // Works, but x becomes 5(truncated)
int y{5.5};            // Error! Narrowing conversion not allowed
int z{5};              // OK, no narrowing

Zero Initialization

You can initialize variables to zero using several methods:

cpp
int x = 0;            // Copy initialization to zero
int y(0);             // Direct initialization to zero
int z{};              // Zero initialization (C++11)
int w{0};             // Explicit zero with uniform initialization

The {} syntax is special: It initializes the variable to zero (or the default value for the type).

cpp
int x{};              // x is 0
double d{};           // d is 0.0
bool b{};             // b is false
int* ptr{};           // ptr is nullptr

Think of it like: Empty braces {} are like saying "give me the default value" - just like asking for a "default coffee" at a café.

Default Member Initialization

In C++11, you can provide default values for class members:

cpp
class MyClass {
    int x = 42;           // Default member initialization
    double y{3.14};       // Uniform initialization
    std::string name{"default"};

public:
    MyClass() {}          // Uses default values
    MyClass(int val) : x(val) {} // Overrides default for x
};

Think of it like: Setting default settings on your phone. You can change them later, but they start with sensible defaults.

Static Storage Variables

Variables with static storage duration are automatically initialized to zero:

cpp
int globalVar;        // Automatically initialized to 0
static int staticVar; // Automatically initialized to 0

void function() {
    static int localStatic; // Automatically initialized to 0
    int localVar;           // Contains garbage value!
}

Key difference: Static variables (global, static local, static class members) are zero-initialized by default, while automatic variables (local variables) contain garbage values if not explicitly initialized. Think of it like: Static variables are like hotel rooms that are cleaned before you arrive (zero-initialized), while local variables are like random rooms that might contain anything (garbage values).

Common Pitfalls

1. Uninitialized Local Variables

Problem: Local variables contain garbage values if not initialized.

cpp
void badFunction() {
    int x;              // Contains garbage value!
    std::cout << x;     // Undefined behavior!
}

void goodFunction() {
    int x{};            // Properly initialized to 0
    std::cout << x;     // Safe!
}

Always initialize your variables!

2. Narrowing Conversions with {}

Problem: Uniform initialization prevents narrowing conversions.

cpp
int x = 5.5;           // Works, but x becomes 5 (truncated)
int y{5.5};            // Error! Narrowing conversion not allowed
int z{5};              // OK, no narrowing

This is actually a good thing! It prevents accidental data loss.

3. The Most Vexing Parse

Problem: The compiler interprets what looks like a variable declaration as a function declaration.

cpp
class MyClass {
public:
    MyClass() {}
};

// This looks like creating an object, but it's actually declaring a function!
MyClass obj();         // Declares a function named 'obj' that returns MyClass

// Correct ways to create an object:
MyClass obj1;          // Default constructor
MyClass obj2{};        // Uniform initialization
MyClass obj3 = MyClass(); // Copy initialization

Think of it like: The compiler is being overly helpful and thinks you're declaring a function instead of creating an object. It's like saying "I want a coffee" and the barista thinks you're asking "Do you have coffee?"

Best Practices

  1. Always initialize variables: Use {} for zero initialization
  2. Use auto when the type is obvious: auto x = 42; is clearer than int x = 42;
  3. Use uniform initialization: int x{5}; prevents narrowing conversions
  4. Initialize class members: Use default member initialization
  5. Be aware of the most vexing parse: Use {} or = to avoid it

Example: Putting It All Together

cpp
#include <iostream>
#include <string>

class Student {
    std::string name{"Unknown"};  // Default member initialization
    int age{};                    // Zero initialization
    double gpa{0.0};              // Zero initialization

public:
    Student() = default;          // Use default constructor

    Student(const std::string& n, int a, double g) 
        : name{n}, age{a}, gpa{g} {}  // Member initializer list

    void print() const {
        std::cout << "Name: " << name << ", Age: " << age 
                  << ", GPA: " << gpa << std::endl;
    }
};

int main() {
    // Different initialization methods
    auto x = 42;                  // auto with copy initialization
    int y{10};                    // uniform initialization
    double z{};                   // zero initialization

    // Creating objects
    Student s1;                   // Uses default values
    Student s2{"Alice", 20, 3.8}; // Custom values

    s1.print();
    s2.print();

    return 0;
}

Summary

You've learned:

  • The fundamental types in C++
  • How to use auto for type deduction
  • Different initialization methods and when to use them
  • How zero initialization works
  • Common pitfalls and how to avoid them
  • Best practices for initialization

Remember: Always initialize your variables! It's one of the easiest ways to prevent bugs in your C++ programs.

Questions

Q: What is the difference between copy initialization and direct initialization?

Copy initialization uses the = operator (e.g., int x = 5), while direct initialization uses parentheses (e.g., int x(5)). Both achieve the same result but use different syntax.

Q: What does uniform initialization with {} do?

Uniform initialization with {} prevents narrowing conversions. For example, int x{5.5} would cause a compilation error because 5.5 cannot be converted to int without losing data.

Q: When should you use uint8_t instead of int?

Use uint8_t when you specifically need exactly 8 bits and values from 0 to 255, such as for network protocols, binary data, or when memory usage is critical.

Q: What is the most vexing parse in C++?

The most vexing parse occurs when the compiler interprets what looks like a variable declaration as a function declaration. For example, 'MyClass obj();' declares a function, not a variable.

Q: What does auto keyword do?

The auto keyword tells the compiler to automatically deduce the type from the initializer. For example, auto x = 5; makes x an int, auto y = 3.14; makes y a double.

Q: What is the advantage of using int32_t instead of int?

int32_t is guaranteed to be exactly 32 bits on all platforms, while int can be 16, 32, or 64 bits depending on the platform. This makes int32_t portable and predictable.