Thanks for the other two answers.
I am now aware of unifdef and sunifdef. I am happy to know the existence of these tools, and that I am not the only one who wants to do this kind of code cleaning.
I have also written a rm_if0_endif.c (attached below) for removing an #if 0 ... #endif block which is sufficient for me. Its philosophy is based on text processing. It scans an input C script, locating #if 0 and the correct enclosing endif, so that this block can be omitted during char-to-char copying.
The text processing approach is limited, as it is designed for #if 0 ... #endif case only, but is all I need for now. A C program is not the only way for this kind of text processing. Jean-François Fabre's answer demonstrates how to do it in Python. I can also do something similar in R, using readLines, startsWith and writeLines. I chose to do it in C as I am not yet an expert in C so this task drives me to learn. Here is a demo of my rm_if0_endif.c. Note that the program can concatenate several C files and add header for each file.
original input file input.c
#define foo 3  /* this is a macro */
// a toy function
int test1 (void) {
  return foo;
  }
#if 0
#undef foo
#define foo 4
#ifdef bar
  #warning "??"
#endif
// this is deprecated
int main (void) {
  printf("%d\n", foo);
  return 0;
  }
#endif
// another toy
int test2 (void) {
  return foo;
  }
gcc pre-processing output "gcc_output.c" (taken as input for my program)
gcc -E -fpreprocessed -P -dD input.c > gcc_output.c
#define foo 3
int test1 (void) {
  return foo;
  }
#if 0
#undef foo
#define foo 4
#ifdef bar
  #warning "??"
#endif
int main (void) {
  printf("%d\n", foo);
  return 0;
  }
#endif
int test2 (void) {
  return foo;
  }
final output final_output.c from my program
rm_if0_endif.c has a utility function pattern_matching and a workhorse function rm_if0_endif:
void rm_if0_endif (char *InputFile,
                   char *OutputFile, char *WriteMode, char *OutputHeader);
The attached file below has a main function, doing
rm_if0_endif("gcc_output.c",
             "final_output.c", "w", "// this is a demo of 'rm_if0_endif.c'\n");
It produces:
// this is a demo of 'rm_if0_endif.c'
#define foo 3
int test1 (void) {
  return foo;
  }
int test2 (void) {
  return foo;
  }
Appendix: rm_if0_endif.c
#include <stdio.h>
int pattern_matching (FILE *fp, const char *pattern, int length_pattern) {
  int flag = 1;
  int i, c;
  for (i = 0; i < length_pattern; i++) {
    c = fgetc(fp);
    if (c != pattern[i]) {
      flag = 0; break;
      }
    }
  return flag;
  }
void rm_if0_endif (char *InputFile,
                   char *OutputFile, char *WriteMode, char *OutputHeader) {
  FILE *fp_r = fopen(InputFile, "r");
  FILE *fp_w = fopen(OutputFile, WriteMode);
  fpos_t pos;
  if (fp_r == NULL) perror("error when opening input file!");
  fputs(OutputHeader, fp_w);
  int c, i, a1, a2;
  int if_0_flag, if_flag, endif_flag, EOF_flag;
  const char *if_0 = "if 0";
  const char *endif = "endif";
  EOF_flag = 0;
  while (EOF_flag == 0) {
    do {
      c = fgetc(fp_r);
      while ((c != '#') && (c != EOF)) {
        fputc(c, fp_w);
        c = fgetc(fp_r);
        }
      if (c == EOF) {
        EOF_flag = 1; break;
        }
      fgetpos(fp_r, &pos);
      if_0_flag = pattern_matching(fp_r, if_0, 4);
      fsetpos(fp_r, &pos);
      if (if_0_flag == 0) fputc('#', fp_w);
      } while (if_0_flag == 0);
    if (EOF_flag == 1) break;
    a1 = 1; a2 = 0;
    do {
      c = fgetc(fp_r);
      while (c != '#') c = fgetc(fp_r);
      fgetpos(fp_r, &pos);
      if_flag = pattern_matching(fp_r, if_0, 2);
      fsetpos(fp_r, &pos);
      if (if_flag == 1) a1++;
      fgetpos(fp_r, &pos);
      endif_flag = pattern_matching(fp_r, endif, 5);
      fsetpos(fp_r, &pos);
      if (endif_flag == 1) a2++;
      } while (a1 != a2);
    for (i = 0; i < 5; i++) c = fgetc(fp_r);
    if (c == EOF) {
      EOF_flag == 1;
      }
    }
  fclose(fp_r);
  fclose(fp_w);
  }
int main (void) {
  rm_if0_endif("gcc_output.c",
               "final_output.c", "w", "// this is a demo of 'rm_if0_endif.c'\n");
  return 0;
  }