Lesson 03: Input Ouput (I/O)

Welcome to the third lesson in Programming in C. In this lesson, we will explore the various Input/Output (I/O) operations in C, which are crucial for interacting with users and files. This includes using printf for output, scanf for input, and various other functions for handling different types of data and streams. You will learn how to use these functions to interact with users and handle data securely.

Objectives

By the end of this lesson, you should be able to:

  • Use printf and fprintf for output operations.
  • Use getchar, gets, scanf, and getc for input operations.
  • Understand the risks of buffer overflow and how to mitigate them.
  • Differentiate between various input functions and their use cases.

Output Using printf

The printf function is used to send formatted output to the standard output (stdout). It allows you to print variables, literals, and more complex formatted strings, and is one of the most commonly used functions for displaying data. Like all functions used for I/O in this lesson, printf is defined in the header file: stdio.h. As such, the header file should always be included when handling input and output using:

#include <stdio.h> 
#include <stdio.h> 
 
int main() { 
    int age = 25; 
    printf("Hello, World!\n"); 
    printf("I am %d years old.\n", age); 
    return 0; 
} 

In the example above, %d is a format specifier for integers, and age is the variable being printed.

In the previous lesson we discussed format specifiers for various data types. Some other ones we didn’t discuss in regards to data types are:

  • %s for strings
  • %p for pointers
  • %x for lower case hexadecimal output
  • %X for upper case hexadecimal output
#include <stdio.h> 
 
int main() { 
    int a = 10; 
    float b = 5.25; 
    char c = 'A'; 
    char str[] = "Hello"; 
 
    printf("The integer %d in hex: %x\n", a, a); 
    printf("Float: %f\n", b); 
    printf("Character: %c\n", c); 
    printf("The variable 'str' have the memory address %p\n", str, str); 
    return 0; 
} 

In this example, various data types are printed using appropriate format specifiers. The integer variable a is printed using %d (decimal output) and %x (hexadecimal output), the float variable b using %f, the character c using %c, and the address to the string pointer str using %p.

Output Using fprintf

The fprintf function works similarly to printf but allows you to specify the output stream, enabling you to direct output to different destinations like standard output (stdout), standard error (stderr), or files.

#include <stdio.h> 
 
int main() { 
    fprintf(stdout, "This is standard output.\n"); 
    fprintf(stderr, "This is standard error.\n"); 
    fprintf(stdout, "Hi %s This is stdout.\n", "Sasha"); 
    fprintf(stderr, "Hi %s This is stderr.\n", "Misha"); 
    return 0; 
} 

In this example, fprintf is used to send output to standard output and standard error. stdout is the standard output stream (typically the terminal), and stderr is the standard error stream (used for error messages). As shown, fprintf also handles format specifiers for variable output.

The fprintf function can be used for sending output to a file instead of the common file pointers stdout and stderr.

#include <stdio.h> 
 
int main() { 
    FILE *file = fopen("output.txt", "w"); 
    if (file == NULL) { 
        fprintf(stderr, "Error opening file!\n"); 
        return 1; 
    } 
    fprintf(file, "Writing this to a file.\n"); 
    fclose(file); 
    return 0; 
} 

In this example, a file named output.txt is opened for writing using fopen which returns a FILE pointer. If the file cannot be opened, an error message is printed to stderr. Otherwise, the string "Writing this to a file.\n" is written to the fil using fprintf, and the file is then closed using fclose.

Input Using scanf

The scanf function reads formatted input from the standard input stream (usually typed using the keyboard or piped to the program). It can read different types of data using format specifiers. The name of the function implies that it scans the input stream and assigned the first instance if each specified format specifier to referenced variables. Variables are referenced using (memory) pointers.

#include <stdio.h> 
 
int main() { 
    int age; 
    printf("Enter your age: "); 
    scanf("%d", &age); 
    printf("You are %d years old.\n", age); 
    return 0; 
} 

In this example, scanf is used to read an integer from the user and store it in the variable age. The format specifier %d indicates that the input should be read as an integer. The input value is then printed using printf using the same format specifier. Note: there are no mechanisms that prevents the user from inputting anything else than an integer. It’s the programmers task to handle all such cases.

Just like format specifiers are used for printf, the same specifiers can be used for specifying input formats.

#include <stdio.h> 
 
int main() { 
    int a; 
    float b; 
    char c; 
    char str[100]; 
 
    printf("Enter an integer: "); 
    scanf("%d", &a); 
    printf("Enter a float: "); 
    scanf("%f", &b); 
    printf("Enter a character: "); 
    scanf(" %c", &c);  /* Note the space before %c */ 
    printf("Enter a string: "); 
    scanf("%s", str); 
 
    printf("You entered: %d, %f, %c, %s\n", a, b, c, str); 
    return 0; 
} 

In this example, the program reads an integer, a float, a character, and a string from the user. The space before %c in the scanf format string ensures that any leftover whitespace characters are ignored when reading the character input.

Handling input with getchar, gets, getc, sprintf, and fgets

There exist various functions for handling input with specific nuances in usage.

getchar reads a single character from the standard input (stdin).

#include <stdio.h> 
 
int main() { 
    char ch; 
 
    printf("Enter a character: "); 
    ch = getchar(); 
    printf("You entered: %c\n", ch); 
    return 0; 
} 

In this example, getchar reads a single character from the user and stores it in the variable ch. The character is then printed using printf. The character is not read until the whole stream is sent, usually ending by the user pressing the return key.

gets reads a line of text from the standard input into a buffer. This is NOT recommended due to buffer overflow risks. gets is considered dangerous and many compilers will print warnings when used. More on this later.

#include <stdio.h> 
 
int main() { 
    char str[100]; 
 
    printf("Enter a string: "); 
    gets(str);  /* what if the user writes more than 99 characters? */ 
    printf("You entered: %s\n", str); 
    return 0; 
} 

In this example, gets reads a line of text from the user and stores it in the variable str. However, gets is not recommended because it does not perform bounds checking and can lead to buffer overflow if the input is larger than the buffer.

getc reads a character from a file.

#include <stdio.h> 
 
int main() { 
    FILE *file = fopen("input.txt", "r"); 
    char ch; 
 
    if (file == NULL) { 
        fprintf(stderr, "Error opening file!\n"); 
        return 1; 
    } 
    while ((ch = getc(file)) != EOF) { 
        putchar(ch); 
    } 
    fclose(file); 
    return 0; 
} 

In this example, getc is used to read characters from a file named input.txt. The characters are printed to the standard output using putchar until the end of the file (EOF) is reached. This can be very useful if you want read a stream until its end, say, from a server receiving a text stream from a client.

sprintf formats and stores a series of characters and values in a char array buffer. The ’s’ in front of ’printf’ indicates that the output is stored in as a string instead of sent to standard output.

#include <stdio.h> 
 
int main() { 
    char buffer[50]; 
    int a = 10; 
    float b = 5.25; 
 
    sprintf(buffer, "Integer: %d, Float: %f", a, b); 
    printf("%s\n", buffer); 
    return 0; 
} 

In this example, sprintf is used to format a string and store it in the variable buffer. The formatted string includes the integer a and the float b. The result is then printed using printf. It is again important to control the buffer size against the input data to prevent buffer overflows. It is however possible to control for such cases.

fgets reads a line of text from the specified stream into a buffer.

#include <stdio.h> 
 
int main() { 
    char buffer[100]; 
 
    printf("Enter a string: "); 
    fgets(buffer, sizeof(buffer), stdin); 
    printf("You entered: %s\n", buffer); 
    return 0; 
} 

In this example, fgets reads a line of text from the standard input and stores it in the variable buffer. The size of the buffer is here specified using the size of the buffer to prevent overflow. The input string is then printed using printf.

Differences Between getchar, getc, getch, and getche

  • getchar
    • • Reads a single character from the standard input.
    • • Waits for the user to press the return key.
  • getc
    • • Reads a single character from a specified stream (e.g., a file).
    • • Similar to getchar but can be used with different input streams.
  • getch
    • • Reads a single character from the console without waiting for the return key.
    • • Does not echo the character to the screen.
    • • Available in some compilers (like Turbo C/C++) and present in <conio.h> but not a standard C function.
  • getche
    • • Similar to getch but echoes the character to the screen.
    • • Not a standard C function, available in some compilers and <conio.h>.

Preventing Buffer Overflow

Buffer overflow can occur if the input exceeds the allocated space for a variable. This can lead to unexpected behavior, crashes, and security vulnerabilities. Here’s a detailed example:

#include <stdio.h> 
 
int main() { 
    char buffer[10]; 
 
    printf("Enter a string: "); 
    scanf("%9s", buffer); 
    printf("You entered: %s\n", buffer); 
    return 0; 
} 

In this example, the format specifier %9s limits the input to 9 characters plus the null terminator, preventing buffer overflow. This ensures that the input does not exceed the size of the buffer.

If scanf("%s", buffer) were used instead, and a string exceeding 9 characters were received, those exceeding character would overflow into memory allocated outside the variable. This could lead to various types of errors, and in the worst case, allow for arbitrary code execution. Example:

char           A[8] = ""; 
unsigned short B    = 1984; 

Lets say that the variables A and B allocate memory directly after each other, like this:

pic-0.png

If now scanf("%s", A) is used and the user types integer, the memory allocation will look like this:

pic-1.png

Everything is fine. The variable B is intact. Now, lets say that the user instead would have typed excessive. If so, the memory allocation will look like this:

pic-2.png

Oups! Not only did the variable erroneously end with the character ’v’ instead of ’\0’, the year got forwarded by 23872 years.

Various solutions to prevent buffer overflows from user input

fgets allows specifying the maximum number of characters to read, providing a safer alternative. This was demonstrated above.

snprintf provides a safer way to format strings by specifying the buffer size:

#include <stdio.h> 
 
int main() { 
    char buffer[50]; 
    int a = 10; 
    float b = 5.25; 
    snprintf(buffer, sizeof(buffer), "Integer: %d, Float: %f", a, b); 
    printf("%s\n", buffer); 
    return 0; 
} 

In this example, snprintf formats the string and stores it in the variable buffer, ensuring that the output does not exceed the buffer size. sprintf (no ’n’) could also be used in a safe way if specify the length for each format specifier as shown above with scanf.

sscanfallows of the scanf family supports a format modifier m for string inputs %s, %c, %[). Instead of taking a char* argument, it takes a char** argument and allocates the necessary space for the value it reads:

#include <stdio.h> 
#include <stdlib.h> 
 
int main() { 
    char *buffer = NULL; 
    char data[] = "HelloWorld"; 
    if (sscanf(data, "%ms", &buffer) == 1) { 
        printf("String is: %s\n", buffer); 
        free(buffer); 
    } 
    return 0; 
} 

In this example, sscanf dynamically allocates memory for the input string, preventing buffer overflow. The allocated memory must be freed after use to avoid memory leaks.

Exercises

  1. Using printf: Write a program that takes an integer, a float, and a string as input from the user and prints them using printf.
  2. Using fprintf: Write a program that writes "Hello, World!" to a file named output.txt. In this exercise, a file named output.txt should be opened for writing using fopen. If the file cannot be opened, an error message should be printed to stderr. Otherwise, the string "Hello, World!\n" gets written to the file using fprintf, and the file is then closed using fclose.
  3. Preventing Buffer Overflow: Write a program that safely reads a string from the user using fgets and prints it.

Solutions

Example solution to ’Using printf’:

#include <stdio.h> 
 
int main() { 
    int a; 
    float b; 
    char str[100]; 
 
    printf("Enter an integer: "); 
    scanf("%d", &a); 
    printf("Enter a float: "); 
    scanf("%f", &b); 
    printf("Enter a string: "); 
    scanf("%s", str); 
 
    printf("You entered: %d, %f, %s\n", a, b, str); 
    return 0; 
} 

Example solution to ’Using fprintf’:

#include <stdio.h> 
 
int main() { 
    FILE *file = fopen("output.txt", "w"); 
 
    if (file == NULL) { 
        fprintf(stderr, "Error opening file!\n"); 
        return 1; 
    } 
    fprintf(file, "Hello, World!\n"); 
    fclose(file); 
    return 0; 
} 

Example solution to ’Preventing Buffer Overflow’:

#include <stdio.h> 
 
int main() { 
    char buffer[100]; 
 
    printf("Enter a string: "); 
    fgets(buffer, sizeof(buffer), stdin); 
    printf("You entered: %s\n", buffer); 
    return 0; 
} 

Next Lesson

In the next lesson, we will delve into the various operators available in C programming. We will cover Arithmetic Operators, Increment and Decrement Operators (Unary Operators), Assignment Operators, Relational Operators, Logical Operators, Bitwise Operators, and Special Operators such as the Comma Operator and the sizeof Operator. Additionally, we will briefly introduce the Ternary Operator, Reference Operator, Dereference Operator, and Member Operators, which will be discussed in greater detail in a later lesson focused on pointers and structs.

Next Lesson