16|Standard Library: Date, Time and Utility Functions

In the previous lectures, some important concepts in the C standard library and the use of related interfaces were introduced in a large space. In addition, there are some common interfaces in the standard library that have very clear functions and are very simple to use. These interfaces also provide important support for daily C application development. Therefore, the next two lectures will focus on this part of the content.

Today, let’s first take a look at the content related to date, time and utility functions in the standard library. Among them, the date and time related interfaces are provided by the header file time.h; and the functions of the utility functions can be further subdivided into string and numerical conversion, random number generation, dynamic memory management, and There are different categories such as process control. The programming interfaces corresponding to these functions are provided by the header file stdlib.h.

Next, let’s take a look at how these two types of interfaces are used and some of the basic principles behind them.

Date and time

First, let’s look at the date and time related interfaces provided by the header file time.h. So, how are the concepts of date and time reflected in C language? How should they be operated and converted?

When building applications, the concepts of date and time are often used. For example, when logging, you usually need to save the exact date and time of each event; when optimizing, you need to find performance pain points by measuring the running time of the code; and when generating random numbers, you even need to use the current time. , as different random seeds.

At this point, you may have discovered that the “time” mentioned in this lecture has two different meanings. One refers to a span of time, while the other refers to an exact point in time consisting of hours, minutes, and seconds. As for the specific meaning, it needs to be understood in conjunction with the context. Of course, special explanations will be given where there may be misunderstandings.

Calendar time

In C language, time can be divided into “Calendar Time” and “Processor Time”. Among them, the former refers to the number of seconds elapsed from 00:00:00 on January 1, 1970 Coordinated Universal Time (UTC) to the current UTC time, excluding leap seconds. In the C standard library, this value is represented by the custom type keyword time_t. This type usually corresponds to an integer type, which in some older versions of the standard library may be implemented as a 32-bit signed integer.

At the same time, the time.h header file also provides a very intuitive method named time, which can be used to obtain this value. Consider the following example:

#include <stdio.h>
#include <time.h>
int main(void) {
  time_t currTime = time(NULL);
  if(currTime != (time_t)(-1))
    printf("The current timestamp is: %ld(s)", currTime);
  return 0;
}

Here, the return value after calling the time method is saved in the variable currTime. When the method call is successful (that is, the return value is not -1), the value is printed out through the printf function.

Not only that, after obtaining this integer time value, it can be further processed through other time and date processing functions provided by the standard library. For example, format it to local time and output it in a specific format. Continue to look at the following example:

#include <stdio.h>
#include <time.h>
int main(void) {
  time_t currTime = time(NULL);
  if(currTime != (time_t)(-1)) {
    char buff[64];
    struct tm* tm = localtime( & currTime);
    if (strftime(buff, sizeof buff, "%A %c", tm))
      printf("The current local time is: %s", buff); // "The current local time is: Saturday Sat Jan 8 16:30:49 2022".
  }
  return 0;
}

In this code, the current calendar time is obtained in a similar way to before, and the value is stored in the variable currTime. In line 7 of the code, we can convert this calendar time into various information related to local time by using a method called localtime. This information will be stored in the form of different fields in a structure object named tm.

Then, you can continue to format the time object by calling the strftime method. After this method is called, the generated result string will be stored in the character array corresponding to the variable buff. Here, the third parameter passed in is a string containing format control placeholders. Among them, %A is used to display the complete week day name, and %c is used to display the standard date and time string. The strftime method will output the corresponding result string according to the specific composition format of the placeholder string.

Processor time

Next, let’s look at processor time (CPU Time). As the name suggests, processor time is the time it takes for CPU resources to be scheduled to support the normal operation of a program for a certain period of time. It should be noted that by default, this time should be the sum of the time consumed by all independent CPUs involved in the application running. The C standard library also provides us with an intuitive method named clock that can be used to return this value. Consider the following example:

#include <time.h>
#include <stdio.h>
int main(void) {
  clock_t startTime = clock();
  for(int i = 0; i < 10000000; i + + ) {}
  clock_t endTime = clock();
  printf("Consumed CPU time is:%fs\
",
   (double)(endTime - startTime) / CLOCKS_PER_SEC);
  return 0;
} 

The clock method located on line 4 of the code returns a value of type clock_t after being called. This type is defined by the implementation of the standard library, so its value may be an integer or a floating point number. Different from calendar time, in order to calculate CPU time more accurately, processor time is not directly measured in “seconds”, but in “clock ticks”. To convert this time to seconds, divide it by the macro constant CLOCKS_PER_SEC provided in the standard library. This constant indicates the number of clock ticks per second on the current system.

It should be noted that the clock tick mentioned here is not directly related to the actual physical CPU frequency of the computer where the program is running. Applications can obtain the CPU usage time of the corresponding process by reading the hardware timer on the computer.

For a program to run, the processor time spent over a period of time may not be consistent with the wall-clock time. The former depends on the number of threads used by the program, the number of physical CPU cores on the platform, and the specific strategy of the operating system to schedule the CPU. The latter is the passage of time in the real world, which is a constantly increasing value. Therefore, the processor time returned by a call to the clock method generally does not mean much. Usually, this time is used as in the above example, that is, before and after a piece of code, the processor time is obtained twice, and by calculating the difference between the two, we can understand the CPU execution of this code. Time spent.

Of course, to better understand the difference between processor time and wall clock time, the following code can be compiled and run on a computer with a multi-core CPU. Regarding the specific implementation details of the code, you can try to understand it by referring to the comments.

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <threads.h>
typedef struct timespec ts_t;
int run(void* data) { // Simulated time-consuming task;
  volatile double d = 0;
  for (int n = 0; n < 10000; + + n)
    for (int m = 0; m < 10000; + + m)
      d + = d * n * m;
  return 0;
}
int main(void) {
  // Record calendar time and processor time for the first time;
  ts_t ts1;
  timespec_get( & amp;ts1, TIME_UTC);
  clock_t t1 = clock();
  //Create two threads to do some time-consuming tasks;
  thrd_t thr1, thr2;
  thrd_create( & amp;thr1, run, NULL);
  thrd_create( & amp;thr2, run, NULL);
  thrd_join(thr1, NULL);
  thrd_join(thr2, NULL);
  // Record calendar time and processor time again;
  ts_t ts2;
  timespec_get( & amp;ts2, TIME_UTC);
  clock_t t2 = clock();
  // Calculate and print processor time and wall clock time respectively;
  printf("CPU time used (per clock()): %.2f ms\
", 1000.0 * (t2 - t1) / CLOCKS_PER_SEC);
  printf("Wall time passed: %.2f ms\
",
    1000.0 * ts2.tv_sec + 1e-6 * ts2.tv_nsec - (1000.0 * ts1.tv_sec + 1e-6 * ts1.tv_nsec));
  return 0;
}

Other related processing functions

In addition to some common date and time processing functions introduced above, the C standard library also provides other related functions, which are organized in the table below for your reference. Of course, interfaces that have been marked as “deprecated” are not included here.

Y2038 problem

At this point, we have finished talking about how to use related functions for date and time operations in C code. Next, we will discuss a problem that may be caused by the time_t type.

As mentioned when talking about calendar time, some old versions of the C standard library may use 32-bit signed integers when implementing the time_t type used to store calendar time. In this case, the time span that time_t can represent will be greatly reduced, and overflow will occur shortly after UTC time of January 19, 2038, 03:14:08. When this type of variable overflows, the specific date and time it represents will be “retimed” starting from 1901.

It can be said that this is a global problem, even as serious as problems such as Y2K. Since the C language is widely used in various software and hardware systems, from common transportation facilities and communication equipment to some early computer operating systems, they may all be affected by the Y2038 problem at that time.

After reading the content related to the header file time.h, let’s take a look at the many practical functions provided by the stdlib.h header file. Since the functions of these functions are very mixed, they are divided into several different categories and introduced separately. First, let’s look at the interfaces related to numeric and string conversion. The use of interfaces such as string to value conversion is very simple. Let’s look directly at the following code:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
  // One-time string to numeric conversion;
  const char* strA = "1.0";
  printf("%f\
", atof(strA));
  // The conversion function with overflow check will save the address of the part that cannot be converted after execution;
  const char* strB = "200000000000000000000000000000.0";
  char* end;
  double num = strtol(strB, &end, 10);
  if (errno == ERANGE) { // Determine whether the conversion result overflows;
    printf("Range error, got: ");
    errno = 0;
  }
  printf("%f\
", num);
  return 0;
}

The stdlib.h header file provides numerous functions that can be used to convert a string into a numeric value of a specific type. In line 7 of the code above, we use a function called atof to convert the string strA to a double precision floating point number. In fact, such functions starting with the letter “a” can only perform a one-time conversion of the string.

In contrast, on line 11 of the code, a function named strtol converts the string strB into the corresponding long integer value. This type of function starting with “str” will determine whether the conversion result overflows every time it is executed, and at the same time save the address of the part that cannot be converted. In this case, by calling this type of function again, we can continue to convert the remaining parts of the string until the entire string is processed.

Generate random numbers

As part of the utility function, “random number generation” is an indispensable and important function. Similarly, random numbers can also be generated by using the rand and srand methods provided by stdlib.h. Their basic usage is as follows:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main (void) {
  srand(time(NULL)); // Initialize random number seed;
  while (getchar() == '\
')
    printf("%d", rand() % 10); // Generate and print random numbers from 0-9;
  return 0;
}

As you can see, first in line 5 of the code, the srand method is used to set the random number seed that needs to be used each time the program is run. In line 7 of the code, the call to the rand function generates a random number in the range [0, RAND_MAX]. By performing remainder processing on it, the result can be limited to a specified range.

For most C standard library implementations, the rand function internally uses pseudo-random algorithms such as “Linear Congruential Generator” to calculate the random numbers that need to be generated each time the function is called. This also means that the random numbers generated by this function are not random in nature. If we do not use the srand function to set a new random number seed, then the random number sequence generated by the rand function will be the same every time the program is re-run. This seed will be used as the input parameter of the algorithm used by the rand function when calculating the next random number.

In fact, computers cannot generate “truly random numbers.” As MIT professor Steve Ward said: “One of the worst things traditional computer systems are good at is flipping coins.” The execution of computer software will be carried out according to the established algorithm. Therefore, when the input and algorithm remain unchanged, the output results will become traceable. Even though we can use more complex algorithms to make the pattern of changes in the output more elusive, it is not truly random in any case.

The reason why the pseudo-random number algorithm can be used to generate random numbers is because from a statistical point of view, the numbers it generates meet the requirements of random numbers in terms of uniformity, independence and other characteristics. Moreover, the generation of pseudo-random numbers does not require special hardware support. At the same time, in most scenarios, pseudo-random numbers can also meet basic usage needs.

Dynamic memory management

Dynamic memory management is essentially heap memory management. In Lecture 08, I introduced the concept of heap in VAS and how to use the malloc and free functions to allocate and release a memory space on the heap. But in fact, in addition to these two functions, the C standard library also provides us with some other functions that can be used for more precise control when allocating heap memory. You can use the following table to understand the basic functions of these functions. Since their usage is relatively simple, they will not be introduced in detail here.

Process Control

Next, let’s take a look at the content related to process control in utility functions. Although the C standard library provides us with process control capabilities, this capability is actually very limited. With the interface provided by the standard library, we can control the exit form of the program (normal termination, abnormal termination, quick termination, etc.), obtain the environment variables of the current system, or interact with the command processor on the host. But beyond that, we can no longer control other behaviors of the process, such as creating processes, or using inter-process communication. For how to use several of these functions, you can refer to the following example:

#include <stdio.h>
#include <stdlib.h>
void exitHandler() {
  printf("%s\
", getenv("PATH"));
}
int main(void) {
  if (!atexit(exitHandler)) {
    exit(EXIT_SUCCESS);
  }
  return 0;
}

Here, on line 7 of the code, a callback function is registered for the program using the function atexit. This function will be triggered when the program explicitly calls the exit function, or when it exits normally from the main function. In the corresponding callback function exitHandler, we use the getenv function to obtain and print the value of the environment variable PATH on the current host. When the callback function is successfully registered (returns an integer value of 0), we exit the current program normally by explicitly calling the exit function. At this time, the callback function is called and the value of the environment variable PATH is printed.

There are also some functions not mentioned, and their use is also very simple. Again, they are organized in the table below for your reference.

It should be noted here that exit, quick_exit, _Exit, and abort are four functions that can be used to terminate program execution. They actually correspond to different usage scenarios.

Among them, the exit function will perform a series of resource cleaning tasks when exiting the program, such as flushing and closing various IO streams, removing temporary files created by the function tmpfile, etc. In addition, it will also trigger the user-registered callback function midway for customized finishing work. But in contrast, the quick_exit function does not perform the above resource cleanup work when terminating the program. It only performs user-defined finishing work through callback functions. The _Exit function is more thorough, it will directly terminate the execution of the program without doing any processing.

Different from these three types of functions, when the abort function is called, it will send the signal SIGABRT to the current program, and depending on the situation, choose to terminate the program or execute the corresponding signal handler. Other interfaces In addition to the important interfaces mentioned above, stdlib.h also contains some interfaces related to search sorting, integer arithmetic, and wide character (string) conversion. Their use is also very simple, so I won’t introduce them one by one here. You can refer to the following table to understand the names and functions of these most commonly used interfaces.

Summary

This lecture mainly introduces the interfaces related to time (date) processing, string and numerical conversion, random number generation, dynamic memory management, process control, and search and sorting functions in the C standard library. Among them, the interface of the first part of the function is provided by time.h, and the rest is provided by stdlib.h.

Time in C language can be divided into calendar time and processor time. Through the two interfaces named time and clock, we can obtain the two values corresponding to them respectively. Furthermore, with the help of interfaces such as localtime and strftime, calendar time can be converted to local time and formatted according to the specified form. Since the processor time is measured in clock ticks by default, it needs to be divided by the macro constant CLOCKS_PER_SEC to obtain the value in seconds.

Conversion of string to numeric values can be achieved using interfaces such as atof and strtol. Among them, the former type of interface starting with “a” can only convert a string once; while the latter type of interface starting with “str” can perform numerical conversion on the string and check the conversion result at the same time. Whether an overflow occurs and save the address of the part that cannot be converted.

By using rand with the srand interface, you can generate pseudo-random numbers in a C program. In most standard libraries, the rand function is implemented using a pseudo-random algorithm. Therefore, in order to make the random number sequence generated by each call to the rand function different, before calling this interface, you can use the srand and time functions together to set different random number seeds that change with time.

The implementation of malloc, calloc, and free interfaces facilitates us to dynamically operate heap memory in the program. By calling exit, abort, quick_exit and other interfaces, we can accurately control the specific behavior of the program when exiting. In addition, the provision of interfaces such as abs, qsort, bsearch, and mblen also enables the standard library to provide certain help for C programming in aspects such as search sorting, integer arithmetic, and wide character (string) conversion.