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>
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:
If now scanf("%s", A) is used and the user types
integer, the memory allocation will look like this:
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:
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 .
sscanf allows 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
- Using printf: Write a program that takes an integer, a
float, and a string as input from the user and prints them using
printf .
- 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 .
- 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
|