Formatted Output

The int fprintf(FILE *stream, char const *fmt, ...); function produces output on stream according to the format specified in fmt, the format string.

Format String Semantics

The format string is composed of directives, each of which is either,

  • An ordinary character, which are copied, unmodified, into the output stream; or,

  • A conversion specifier

A conversion specifier consists of the ‘%’ character followed by a conversion character which specifies the type of conversion to be performed. Between the ‘%’ and the conversion character, optional flags, width, precision, and length-modifier specifiers may be present,

conversion-specifier:

%[flags][width][.precision][length-modifier]conversion-character

With the exception of the %% conversion specifier, each conversion specifier refers to a subsequent variadic argument. The following selected conversion characters have the following significance,

‘%’

A literal ‘%’ character–causes a single ‘%’ to be produced on the output stream.

fprintf("100%%\n"); /* 100% */
‘d’ or ‘i’

The int argument is converted to a signed decimal representation.

fprintf("%d + %d = %d\n", 1, 2, 1 + 2); /* 1 + 2 = 3 */
‘o’, ‘u’, ‘x’, or ‘X’

The unsigned int argument is converted to unsigned octal (‘o’), unsigned decimal (‘u’) or unsigned hexadecimal (‘x’ or ‘X’) representation. The ‘x’ conversion produces lower-case representation, while the ‘X’ conversion produces an upper-case representation.

fprintf("0%o = %u = 0x%x\n", 15, 15, 15); /* 017 = 15 = 0xf */
‘c’

The int argument is converted to an unsigned char and the resulting character is written.

int c = 'x';
fprintf("'%c'\n", c); /* 'x' */
‘s’

The char* argument points to a string, which is written up to the terminating null character.

char *name = "Benny Beaver";
printf("My name is %s!\n", name); /* My name is Benny Beaver! */

The return value is the number of characters output, or a negative value if an error occurred.

There are additional conversion characters, such as floating point values; the flags, width, precision, and length modifier semantics are described in more detail in the relevant FPRINTF(3) manual page. Additionally, a detailed table of all of the combinations of length specifiers and conversion characters is available at cppreference.com.

It is important to understand the use of length modifiers, in particular ‘z’ and ‘j’, as these two are frequently used–‘z’ for size_t and ssize_t types, and ‘j’ for intmax_t and uintmax_t types. Values of type size_t are produced by the sizeof operator and returned by many library functions to represent the sizes of objects; ssize_t is a signed version which includes -1 in its range for reporting error conditions. The intmax_t and uintmax_t are the widest signed and unsigned integer types, and are frequently used for outputting arbitrary numeric types that do not have their own conversion characters.

In the following example, the type pid_t is an integer type which has no conversion character; the solution is to explicitly upcast it to intmax_t and use the %jd conversion specifier:

pid_t pid = getpid(); /* pid_t is an integer type of unknown size */
printf("My process id is: %jd\n", (intmax_t) pid);

Additional Output Functions

The following additional functions are available with the same semantics as fprintf, but different output targets:

  • int printf(char const *fmt, ...) is equivalent to fprintf with stdout as its stream argument.

  • int sprintf(char *s, char const *fmt, ...) is similar to fprintf, but the output is placed into the character buffer pointed at by s, rater than a stream. The output is null-terminated, and the return value is equal to the length of the produced string, not including the null terminator.

  • int snprintf(char *s, size_t n, char const *fmt, ...) is the same as snprintf, except that it outputs no more than n characters to the buffer pointed at by s. Importantly, it returns the number of characters that would have been printed, even if that amount exceeds n, not counting the null terminator.

The sprintf function is dangerous because it has the potential to overflow s. Typically, it is used only when the length of output is known to no larger than the buffer being written to because it is marginally faster to execute. The property that snprintf returns the number of characters that would have been printed can be used to determine the necessary size of a buffer; this is commonly used to allocate a buffer large enough to hold a formatted string and then fill said buffer,

int size = snprintf(0, 0, fmt, /* ... */); /* Calculate storage space requirement */
char *s = malloc(size + 1); /* Allocate a buffer with room for the null terminator */
sprintf(s, fmt, /* ... */); /* Fill the buffer with formatted output */

As mentioned earlier, there is no way to forward variadic arguments to a variadic function, so all of the previously mentioned functions have v* counterparts,

  • vfprintf(FILE *stream, char const *fmt, va_list ap);

  • vprintf(char const *fmt, va_list ap);

  • vsprintf(char *s, char const *fmt, va_list ap);

  • vsnprintf(char *s, size_t n, char const *fmt, va_list ap);

The separate v* implementation functions enable extensions of the basic library features by wrapping these methods in custom interfaces. One very common custom extension, which is not part of the C standard library, but often provided by implementations, is the asprintf function, which allocates a string of the appropriate size. Here is a full example implementation using the v* functions,

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <assert.h>

int
vasprintf(char **buffer, char const *fmt, va_list ap)
{
  /* Duplicate ap for later call to sprintf */
  va_list ap_copy;
  va_copy(ap_copy, ap);

  /* Get length of formatted output */
  int size = vsnprintf(0, 0, fmt, ap_copy);
  if (size < 0) goto done;

  /* Allocate buffer to hold formatted output */
  char *tmp = realloc(*buffer, size + 1);
  if (!tmp) {
    size = -1;
    goto done;
  }
  /* Write the formatted output to the buffer */
  assert(vsprintf(tmp, fmt, ap) == size);
  *buffer = tmp;
done:
  /* Destroy local copy of ap */
  va_end(ap_copy);
  return size;
}

int
asprintf(char **buffer, char const *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  int size = vasprintf(buffer, fmt, ap);
  va_end(ap);
  return size;
}

int main()
{
  char *s;
  asprintf(&s, "Hello %s!\n", "Benny Beaver");
  fputs(s, stdout); /* Hello Benny Beaver! */
  free(s);
}