How to initialize everything in modern C and stay sane

Have you ever struggled with initialization of a struct, trying to set all its members to default values while writing code that’s both efficient and readable? Or committed a piece of code where you memset-ed a just declared array with zero’s? In this article, you’ll learn how to initialize just about everything in C to zero, using consistent, modern syntax and getting rid of those nasty manual memsets.

How to initialize everything in C?

If you came here just to see the answer to the title question, here it is. If you are happy with having everything set to 0 use = { 0 }. So instead writing:

#include <string.h>

typedef struct Point2D {
  int x;
  int y;
} Point2D_s;

typedef struct Circle {
  Point2D_s center;
  int radius;
} Circle_s;


int main( void ){
  char big_array[1'000];
  Circle_s circle;
  memset(big_array, 0, sizeof(big_array)/sizeof(big_array[0]));  
  memset(&circle, 0, sizeof(circle));
}

Just use ={ 0 }:

int main( void ){
  char big_array[1'000] = { 0 };  
  Circle_s circle = { 0 };
}

Really, that’s all you need to fill the content of any object with zero’s:

#include <complex.h>

typedef struct Point2D {
  int x;
  int y;
} Point2D_s;

typedef struct Circle {
  Point2D_s center;
  int radius;
} Circle_s;


int main( void ){
  double number = { 0 };
  char letter = { 0 };

  double complex z = { 0 };

  char big_array[1'000] = { 0 };
  
  Circle_s circle = { 0 };
}

This syntax might look strange, so let’s walk through it step by step. In C, every object can be initialized using an initializer enclosed in curly braces. That includes scalars, aggregates (structs and unions) and arrays. For scalars there can be only one initializer expression in the curly braces:

double pi = { 3.1415927 };

However, for objects that have more elements, like big_array and circle, more initializers can be added–their number equal to the number of sub-objects being initialized:

int seven_primes[7] = {2, 3, 5, 7, 11, 13, 17};

If there are less initializers than sub-objects, empty initialization is performed on the remainder. In the expression:

char big_array[1'000] = { 0 };

The first element of big_array is initialized with 0 and the remaining 999 chars are empty-initialized. The curious empty initialization simply means setting everything to 0. In fact, all object types are initialized with 0 or its equivalent during empty initialization, except for decimal floating types, for which some obscure details are implementation-defined. Consequently, when initializing a struct with = { 0 }:

Point2D_s point = { 0 };
Circle_s circle = { 0 };

The only explicit initializer is consumed to give the value to the first struct’s member, and the rest of the sub-objects are set to 0 as by empty initialization. For point, which has two sub-objects, x and y, this rule results in:

  • x is explicitly initialized with 0 provided in the brace-enclosed initializer list,
  • y is implicitly initialized with 0 as if by performing empty initialization.

For circle, the implementation of the rule means that:

  • center is explicitly initialized with 0 in the brace-enclosed initializer list:

    • its first sub-object, x, “consumes” this 0 during initialization,
    • since there are no more initializers left, its second sub-object, y is initialized as if by performing empty initialization,
  • there’s also no initializer for radius, so it undergoes similar treatment as cecnter.y–it is set to 0 as if by empty initialization.

Actually, the second example is a trap, or at least it doesn’t follow the good C coding rules to the letter. center is a sub-object of circle and its initializer should be enclosed in curly braces:

Circle_s circle = { { 0 } };

But, compilers do not mind Circle_s circle = { 0 }; and accept it with no complaints. They will, however, scream when they encounter:

Circle_s circle = { 1 };

It’s a matter of accepted semantics of = { 0 } that translates into: just initialize the whole object with zero’s. As a side note, writing Circle_s circle = { 1 }; is legal, albeit not pretty, and has the same meaning as:

Circle_s circle = { {1, 0}, 0 };

In C23 there is {}

In C23, instead of writing = { 0 } which is confusing at best, you’ll be able to write ={}:

int main( void ){
  double number = {};
  char letter = {};

  double complex z = {};

  char big_array[1'000] = {};
  
  Circle_s circle = {};
}

={} is also where empty initialization takes its name from. This is much better because it expresses the intent directly and harmonizes the syntax of C and C++. If you don’t think that the current syntax is confusing, try to explain why:

int big_array[1'000] = { 0 };

Initializes all the element of the array with zeros, while:

int big_array[1'000] = { 42 };

Initializes its first element with 42 and the remaining 999 with zero’s.

On top of that empty initializer can be used with variable length arrays:

void vla_function(size_t sz) {
  int numbers[sz] = {};

  // ~~~ //
}

Freeing you from writing:

void vla_function(size_t sz) {
  int numbers[sz];
  memset(numbers, 0, sizeof(int[sz]));

  // ~~~ //
}

You probably shouldn’t use VLA’s anyway, but if you really insist on having one you can, as of C23, zero-initialize it with ease.

Let the compiler do its job

This post started with dropping manual memset for ={0}. If you were wondering how an object is zeroed when the compiler makes the call, then the answer will disappoint you. As it often happens, it is: it depends. On:

  • the compiler you use,
  • the optimization level that you pass to the compiler,
  • the size of the object being initialized.

Generally speaking you’ll see one of two variants. Either it will be a direct initialization:

Or… a call to memset, which seems to be the preferred way of clang for aggregates and arrays of any size. At least that’s what happens for gcc and clang with no optimizations. When you increase the optimization level you’ll see that compilers can make smart and informed decisions about how to best perform zeroing. You shouldn’t always, but in this case, trust your compiler.

Summary

What you’ve learned? Hopefully quite a lot about using empty initialization:

  • Don’t call memset manually when initialization an object with zero’s.
  • ={ 0 } initializes any object in C with zero’s.
  • In C23 there an empty initializer, ={}, which empty-initializes any object.
  • Empty initialization means setting everything to 0 in C.