During a process's lifetime, such data might include memory segments designated to the process, the arguments it's been invoked with, environment variables, counters about resource usage, user-id, group-id and group set, and maybe other types of information.
When a process terminates its execution, either by calling exit (even if implicitly, by executing a return command from the main function) or by receiving a signal that causes it to terminate abruptly, the operating system releases most of the resources and information related to that process, but still keeps the data about resource utilization and the termination status code, because a parent process might be interested in knowing if that child executed successfully (by using standard functions to decode the termination status code) and the amount of system resources it consumed during its execution.
By default, the system assumes that the parent process is indeed interested in such information at the time of the child's termination, and thus sends the parent the signal SIGCHLD to alert that there is some data about a child to be collected.
The most obvious approach is to have code that calls wait or one of its relatives somewhere after having created a new process.
If the program is expected to create many child processes that may execute asynchronously and terminate in an unpredictable order, it is generally good to create a handler for the SIGCHLD signal, calling one of the wait-family function in a loop, until no uncollected child data remains.
It is possible for the parent process to completely ignore the termination of its children and still not create zombies, but this requires the explicit definition of a handler for SIGCHLD through a call to sigaction with the special option flag SA_NOCLDWAIT.
This is an abstract of the manual page, reporting that: A subreaper fulfills the role of init(1) for its descendant processes.