K&R C book, Exercise 1-21: Replace tabs with spaces
Constants


#define TAB_SIZE 4



This is the old way of doing things.  Modernly C uses the same syntax as C++:  

const int TAB_SIZE = 4;


While both will still function, this has the added benefit of offering type safety.  The #define is just a search and replace used by the preprocessor.  I.e. it replaces TAB_SIZE with the character 4 in an intermediate copy of the file.  The const variable will be managed by the compiler.  

Update assignments


                character_count = character_count + whitespace_count;



This is more idiomatically written 

                character_count += whitespace_count;


That form is exactly the same as the longer form.  It evaluates the left-hand side of the assignment, adds it to the right-hand side, and stores the result in the left-hand side.  But it is shorter and more easily recognizable.  

Simplifying logic

However, I wouldn't do either of these.  Instead, on each iteration of the loop, just increment character_count.  Then you never have to add character_count and whitespace_count.  This will work in your program because you never use character_count separately from whitespace_count.  So make this change and replace character_count + whitespace_count with just character_count.  

Yoda conditions


        if (c == ' ') {



This is often written as 

        if (' ' == c) {


The reason is that if you accidentally leave off an equals sign ' ' = c will generate a compiler error.  Meanwhile, c = ' ' will silently do the wrong thing.  It wouldn't be so bad here, as it will be immediately obvious in the display.  But in a different kind of program, assigning to c might make a much more subtle bug.  

Correct naming


            //we encountered a non-whitespace char. Print all 'saved' whitespaces.



This is incorrect.  A whitespace is any character that can produce indent.  It specifically includes tabs (and new lines).  But what you check is non-space characters.  This is easy enough to fix, just remove "white" both times here and rename whitespace_count to space_count.  

Consistent naming style

You have the camelCased numberOfCharsToReachTabStop and snake_cased character_count and whitespace_count.  Please pick one kind of casing per identifier type and stick to it.  If local variables are snake_cased, then number_of_chars_to_reach_tab_stop.  Or change them all to camelCase.  I personally prefer snake_case, which is easier for non-native English speakers, as it doesn't rely on an ability to differentiate between capital and lowercase letters.  But the most important thing is to be consistent so that people can see snake_case and realize that it is a local variable or one of the other things that use snake_case.  

TAB_SIZE is a constant, so it is appropriate to use a different casing style (e.g. ALL_CAPS) for it.  

Bugs

As already noted, you don't print spaces at the end of a line.  But there is a more serious bug.  You treat tabs as single characters in the character count.  You should not.  Instead, tabs are TAB_SIZE characters.  Don't forget to adjust for the tab stop.  I.e. if you are currently one character past the tab stop, you'd only add three characters for the tab.  So if you adopt my previous suggestion of incrementing character_count on every iteration, you need something like 

if ('\t' == c) {
    /* update character_count appropriately */
} else {
    character_count++;
}


I'll leave the actual update to you, as that's the exercise that you're exploring.