Object Life Cycle

"An object is a region of storage that has a type and a value" (Lospinoso). Whenever you declare a variable, you create an object.

Storage duration and lifetime

An object's storage duration begins when storage is allocated for that object, and ends when storage is deallocated for that object. (Memory is reserved for the object during this period.)

An object's lifetime begins when the object's constructor returns, and ends just before the object's destructor is called. (You can use the object during this period.)

Generally:

  1. Object's storage duration begins storage is allocated
  2. Object's lifetime begins object's constructor returns
  3. (... you can use the object in your program ...)
  4. Object's lifetime ends object's destructor is called
  5. Object's storage duration ends storage is deallocated

Types of storage duration

Instead of relying on the garbage collector (aka. automatic memory manager) to automatically deallocates the memory of objects you no longer use, you should know these types of memory management in C++.

Automatic storage duration

All local variables and function parameters have this storage duration.

  • Storage is allocated at the start of the enclosing code block ({)
  • Storage is deallocated at the end of the enclosing code block (})
// `a` and `b` have automatic storage duration
void addOne(int a) {
  int b = 1;
  return a + b;
}

Static storage duration

Variables declared at global scope (or namespace scope), including those declared with the static or extern keyword, have this storage duration.

  • Storage is allocated when the program begins
  • Storage is deallocated when the program ends
// `a` and `b` have static storage duration
static int a = 100;
extern int b = 200;

// `c` has static storage duration
struct MyClass {
  static int c = 300;
}

// `d` has static storage duration
int main() {
  static int d = 10;
}

Local static variables

Local static variables (cppreference.com):
  • the storage is allocated when the program begins
  • the value is initialized the first time control passes through their declaration (except for zero or constant initialization, which can be done before the block is first entered).

Normal class member vs static class member

  • Local class members follow the class' storage duration
  • Static class members have static storage duration

Thread-local storage duration

Variables declared with thread_local have thread-local storage duration.

  • Storage is allocated when the thread begins
  • Storage is deallocated when the thread ends
// `tl_a` has thread-local storage duration
thread_local int tl_a = 1;

// `tl_b` has thread-local storage duration
void myFunction() {
  static thread_local int tl_b = 5;
}

thread_local implies static

thread_local can be combined with the static or extern keyword. If none is specified, static is implied.

For the initialization of local thread_local variables, see "local static variable" side note above.

Dynamic storage duration

Objects with dynamic storage duration are allocated and deallocated upon your request. These objects are also called dynamic objects.

  • To allocate storage, use the new expression
  • To deallocate storage, use the delete expression
int main() {
  // Simple example with variables
  int* my_int_ptr = new int{ 42 };
  printf("%d\n", *my_int_ptr); //=> 42
  delete my_int_ptr;

  // With array, the length does not need to be a constant.
  // (The length could also be inferred from the init list.)
  const int length = 5;
  int* my_arr_ptr_1 = new int[length]{};
  int* my_arr_ptr_2 = new int[]{ 111, 222 };
  printf("%d %d\n", my_arr_ptr_1[0], my_arr_ptr_1[1]); //=> 0 0
  printf("%d %d\n", my_arr_ptr_2[0], my_arr_ptr_2[1]); //=> 111 222
  // `delete[]` tells the CPU that it needs to clean up multiple variables
  // (`new type[]` keeps track of how much memory was used)
  delete[] my_arr_ptr_1;
  delete[] my_arr_ptr_2;
}

You have to make sure that you delete dynamic objects once you no longer use them (e.g. at the end of the scope or inside the class' destructor).

Not doing this will cause memory leak, the condition where your program's memory usage keeps increasing because it does not release the memory it no longer uses.

Dynamic array vs fixed array

Dynamic arrays (allocated via new type[]) are stored in the heap, as opposed to fixed arrays which are stored in the stack.

This allows you to allocate large-sized arrays (e.g. arrays with 1,000,000 elements).

Storage duration example

The following code:

struct Tracer {
  const char* const name;
  Tracer(const char* name) : name{name} {
    printf("Constructed: %s\n", name);
  }
  ~Tracer() {
    printf("Destructed: %s\n", name);
  }
};

static Tracer t1{ "Global static variable" };
thread_local Tracer t2{ "Thread-local variable" };

int main() {
  // Start of program:
  //=> Constructed: Global static variable

  const Tracer* t2_ptr = &t2;
  //=> Constructed: Thread-local variable

  Tracer t3 = { "Automatic variable" };
  //=> Constructed: Automatic variable

  const auto* t4 = new Tracer{ "Dynamic variable" };
  //=> Constructed: Dynamic variable

  // End of function:
  // Note that 'Dynamic variable' is not destructed!
  //=> Destructed: Automatic variable
  //=> Destructed: Thread-local variable

  // End of program:
  //=> Destructed: Global static variable
}

will produce the following output:

Constructed: Global static variable
Constructed: Thread-local variable
Constructed: Automatic variable
Constructed: Dynamic variable
Destructed: Automatic variable
Destructed: Thread-local variable
Destructed: Global static variable

References