The C++ Textbook

Part 2 · Memory and objects

Objects and storage

What a variable actually is: a named region of bytes with a type stamped on it.

By the end of this chapter you can

  • Explain the difference between an object, a value, and a name
  • Predict sizeof for a struct given its members
  • Describe what padding is and why the compiler inserts it

In Python, a variable is a label tied to an object that lives somewhere the runtime manages. In C++, a variable is the storage. The name is yours, the bytes are real, and the type decides how those bytes are read.

That single difference is the source of nearly everything that makes C++ fast and nearly everything that makes it dangerous. This chapter makes the bytes visible.

Three words that are not synonyms

  • An object is a region of storage. It has a size, an address, a type, and a lifetime. (Nothing to do with classes — an int is an object.)
  • A value is what the bytes mean when read through that type.
  • A name is an identifier your source code uses to refer to an object.

An object can have no name (new int(7)), several names (a reference), or a name that outlives nothing at all. Keeping these apart is what lets you reason about the next four chapters.

One value, several ways to reach it
#include <iostream>

int main() {
    int count = 7;        // an object named `count`, holding the value 7
    int& alias = count;   // another name for the same object
    int* address = &count; // a different object, holding count's address

    alias = 9;

    std::cout << "count   = " << count << '\n';
    std::cout << "alias   = " << alias << '\n';
    std::cout << "*address= " << *address << '\n';
    std::cout << "sizeof(count)   = " << sizeof(count) << " bytes\n";
    std::cout << "sizeof(address) = " << sizeof(address) << " bytes\n";
}

Assigning through alias changed count, because they are the same object. address is a different object — its own bytes, holding a number that happens to be where count lives.

sizeof tells you the truth

sizeof is a compile-time operator that yields the number of bytes an object of a given type occupies. It is not a function call and it never evaluates its argument.

Sizes of the built-in types
#include <iostream>

int main() {
    std::cout << "char        " << sizeof(char) << '\n';
    std::cout << "short       " << sizeof(short) << '\n';
    std::cout << "int         " << sizeof(int) << '\n';
    std::cout << "long        " << sizeof(long) << '\n';
    std::cout << "long long   " << sizeof(long long) << '\n';
    std::cout << "float       " << sizeof(float) << '\n';
    std::cout << "double      " << sizeof(double) << '\n';
    std::cout << "void*       " << sizeof(void*) << '\n';
}

sizeof(char) is 1 by definition — a byte is whatever a char is. Everything else is up to the implementation. The standard guarantees only relative ordering: charshortintlonglong long. If you need an exact width, say so with <cstdint>: std::int32_t, std::uint64_t.

Padding: the bytes you did not ask for

Processors read memory fastest when an object’s address is a multiple of its size. The compiler therefore inserts unused bytes — padding — to keep members aligned. This means a struct can be larger than the sum of its parts, and reordering members can change its size.

The same three members, two layouts
#include <iostream>
#include <cstddef>   // offsetof

struct Wasteful {
    char  a;   // 1 byte, then 3 bytes of padding
    int   b;   // 4 bytes
    char  c;   // 1 byte, then 3 bytes of tail padding
};

struct Tight {
    int   b;   // 4 bytes
    char  a;   // 1 byte
    char  c;   // 1 byte, then 2 bytes of tail padding
};

int main() {
    std::cout << "sizeof(Wasteful) = " << sizeof(Wasteful) << '\n';
    std::cout << "sizeof(Tight)    = " << sizeof(Tight) << '\n';
    std::cout << "alignof(int)     = " << alignof(int) << '\n';

    std::cout << "offset of b in Wasteful = " << offsetof(Wasteful, b) << '\n';
    std::cout << "offset of b in Tight    = " << offsetof(Tight, b) << '\n';
}

Twelve bytes versus eight, for identical data. The rule the compiler follows:

  1. Each member is placed at the next offset that is a multiple of its alignment.
  2. The struct’s own alignment is the largest alignment among its members.
  3. The total size is rounded up to a multiple of that alignment, so that arrays of the struct keep every element aligned.

Step 3 is why Tight is 8 and not 6: an array of Tight needs each element’s int on a 4-byte boundary.

Every object has an address

The & operator yields the address of an object. Addresses are what make pointers, references, containers, and polymorphism possible, and they are the reason C++ can hand you a bare block of memory and let you interpret it.

Objects laid out in memory
#include <iostream>

int main() {
    int values[4] = {10, 20, 30, 40};

    for (int i = 0; i < 4; ++i) {
        std::cout << "values[" << i << "] at " << &values[i]
                  << "  value " << values[i] << '\n';
    }

    std::cout << "\ndistance between elements: "
              << reinterpret_cast<char*>(&values[1]) - reinterpret_cast<char*>(&values[0])
              << " bytes\n";
}

The addresses differ by exactly sizeof(int). An array is not a list of references to values living elsewhere — it is one contiguous block, and that contiguity is why iterating an array is so fast.

Check yourself

Practice